Repository navigation
UIをgocuiからBubble Teaへ移行 - #14
Conversation
描画・入力処理をjroimartin/gocuiからcharmbracelet/bubbletea + lipgloss + bubbles(textinput)へ移行。Appをtea.Model(Init/Update/View)として再実装し、 キー入力はUpdate内でモード別にルーティングする。リポジトリ選択は アプリ内の中央モーダルとして再実装した(MainLoop再起動は不要に)。 ドメイン層(fs/pane/selector)とiconはUI非依存のため変更なし。 ペイン状態管理・各操作ロジック・検索/フィルタ等の挙動はそのまま流用し、 パリティを維持している。 - main.go: tea.NewProgram(app, tea.WithAltScreen()) - internal/ui: model.go/render.go を新設、layout.go/keybinding.go/selector.go を改修 - テスト: Update/Viewを直接駆動する方式へ書き換え(実端末不要) - flake.nix: vendorHash 更新 - docs: lib配下をbubbletea/lipgloss/textinputへ更新、design.md/CLAUDE.md を追従 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 48 minutes and 22 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughウォークスルーTUIフレームワークを 変更内容gocui → Bubble Tea UI移行
シーケンス図sequenceDiagram
participant User
participant main
participant tea.Program
participant App
participant render
main->>tea.Program: NewProgram(app, WithAltScreen())
tea.Program->>App: Init()
App-->>tea.Program: textinput.Blink
User->>tea.Program: キー入力
tea.Program->>App: Update(tea.KeyMsg)
App->>App: handleKey() → handleNormalKey/handleInputKey/handleConfirmKey/handleSelectorKey
App-->>tea.Program: (App, tea.Cmd)
tea.Program->>App: View()
App->>render: render()
render-->>App: 画面文字列
App-->>tea.Program: string
tea.Program-->>User: 端末描画
推定コードレビュー工数🎯 4 (Complex) | ⏱️ ~60 minutes 関連する可能性のあるPR
詩
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
toggleSelectでToggleSelect後のMoveDownを削除し、スペースは カーソル行のマークのトグルのみを行うようにした。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/design.md (1)
29-30: 🎯 Functional Correctness | 🟠 Major前提条件に
fzfの記載が残っているが、移行後は不要では?5.4 のリポジトリ選択がアプリ内モーダル(textinput フィルタ)に置き換わったため、
fzfは必須前提ではなくなった。ghqはListRepositories()に必要だが、fzfは削除または任意ツールとして注記すべき。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/design.md` around lines 29 - 30, The prerequisites text still lists fzf as required, but repository selection has moved to the in-app modal flow, so it should no longer be a hard dependency. Update the requirements section in design.md to remove fzf from the mandatory prerequisites, or reword it as an optional tool, while keeping ghq referenced for ListRepositories() where needed.CLAUDE.md (1)
11-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winワークフローの番号付けにミスがある。
手順が 1→2→3→4→5→3 となっており、最後の「リファクタリングする」が 6 になるべき。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLAUDE.md` around lines 11 - 19, ワークフローの番号付けに誤りがあり、最後の「リファクタリングする」が重複した 3 になっています。CLAUDE.md の手順リストを見直し、TDD の流れが 1→2→3→4→5→6 になるように番号を修正してください。該当箇所は「失敗するテストを書く」「テストをパスするコードを書く」「フォーマッター」「リンター」「ユニットテスト」「リファクタリングする」の並びです。
🧹 Nitpick comments (1)
internal/ui/selector.go (1)
71-84: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftリポジトリ一覧取得を
tea.Cmd側へ移してください。
Selector.ListRepositories()がUpdate中に同期実行されているため、取得が重いと描画と入力が止まります。結果はメッセージで返し、Updateは状態更新だけにすると安全です。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/ui/selector.go` around lines 71 - 84, `openRepoSelector` is doing the blocking `Selector.ListRepositories()` call synchronously inside `Update`, which can freeze rendering and input. Move the repository fetch into a `tea.Cmd` and have it return a message that `Update` handles to set `repoList`, reset `repoFilterQuery`/`repoSelectorCursor`, call `filterRepoList`, and switch `inputMode`; keep `openRepoSelector` focused on triggering the command and updating state only.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/design.md`:
- Around line 85-91: The Markdown table in the design document has code spans in
cells that include unescaped pipe characters, which can be misread as column
separators. Update the affected table row for internal/selector so the `ghq list
| fzf` text is escaped or reformatted to preserve the intended cell boundaries,
keeping the package responsibilities table aligned and parseable.
In `@internal/ui/keybinding.go`:
- Around line 451-461: The search confirmation path in the
InputModeSearch/InputModeSearchBackward branch is not persisting the chosen
direction, so searchNext/searchPrev can reuse a stale a.searchFwd value. Update
the search handling in keybinding.go so that when input is confirmed, the
current mode is stored on a.searchFwd before calling a.doSearch, using the mode
check already present in this branch. Keep the fix localized to the search input
handling logic and ensure InputModeSearchBackward sets the backward direction
explicitly.
- Around line 270-276: The copy loop in the keybinding logic can pass the same
path as both source and destination when both panes point to the same directory,
so guard against self-copy before calling a.FS.Copy. In the handler that
iterates over a.YankBuf.Entries, compare src and dstPath (or their
resolved/cleaned equivalents) and skip the operation or report a status message
when they match, preserving the existing error handling for real copy failures.
In `@internal/ui/model.go`:
- Around line 84-85: The keymap in model.go currently routes only "/" to
searchForward, leaving the backward-search entry unreachable even though
InputModeSearchBackward and inputPrefix() still define it. Update the switch in
the relevant input handler to map "?" to the backward-search action, using the
existing backward-search path/function already defined in the UI flow, and keep
"/" mapped to forward search so both search modes remain reachable.
- Around line 9-10: The App message handling is dropping non-key `textinput`
messages, so cursor blink and related state updates never reach the inputs.
Update the `App.Update` flow to pass all relevant messages, not just
`tea.KeyMsg`, through to `a.input` and `a.filterInput`, while preserving the
existing key handling logic. Use `App.Init` and `App.Update` as the main
touchpoints when locating the fix.
In `@internal/ui/render.go`:
- Around line 23-35: The highlightMatch function is using byte indexes from
strings.ToLower/strings.Index directly on the original name, which can split
UTF-8 characters. Update highlightMatch to search and slice using []rune
boundaries instead of byte offsets, while preserving the existing highlight
behavior and keeping the original casing of name intact.
---
Outside diff comments:
In `@CLAUDE.md`:
- Around line 11-19: ワークフローの番号付けに誤りがあり、最後の「リファクタリングする」が重複した 3 になっています。CLAUDE.md
の手順リストを見直し、TDD の流れが 1→2→3→4→5→6
になるように番号を修正してください。該当箇所は「失敗するテストを書く」「テストをパスするコードを書く」「フォーマッター」「リンター」「ユニットテスト」「リファクタリングする」の並びです。
In `@docs/design.md`:
- Around line 29-30: The prerequisites text still lists fzf as required, but
repository selection has moved to the in-app modal flow, so it should no longer
be a hard dependency. Update the requirements section in design.md to remove fzf
from the mandatory prerequisites, or reword it as an optional tool, while
keeping ghq referenced for ListRepositories() where needed.
---
Nitpick comments:
In `@internal/ui/selector.go`:
- Around line 71-84: `openRepoSelector` is doing the blocking
`Selector.ListRepositories()` call synchronously inside `Update`, which can
freeze rendering and input. Move the repository fetch into a `tea.Cmd` and have
it return a message that `Update` handles to set `repoList`, reset
`repoFilterQuery`/`repoSelectorCursor`, call `filterRepoList`, and switch
`inputMode`; keep `openRepoSelector` focused on triggering the command and
updating state only.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e6ab12b2-56f6-4444-b816-af682d69579d
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (19)
.claude/plans/plan.mdCLAUDE.mddocs/design.mddocs/lib/bubbletea.mddocs/lib/gocui.mddocs/lib/lipgloss.mddocs/lib/textinput.mdflake.nixgo.modinternal/ui/keybinding.gointernal/ui/keybinding_test.gointernal/ui/layout.gointernal/ui/layout_test.gointernal/ui/model.gointernal/ui/model_test.gointernal/ui/render.gointernal/ui/render_test.gointernal/ui/selector.gomain.go
💤 Files with no reviewable changes (2)
- docs/lib/gocui.md
- internal/ui/layout_test.go
| | パッケージ | 責務 | | ||
| |-----------|------| | ||
| | `main` | gocui.Guiの初期化、Managerの登録、MainLoopの起動。MainLoopの再起動ループを管理する | | ||
| | `internal/ui` | gocui.ManagerインターフェースのLayout実装、View生成・配置、キーバインドの登録。`App`構造体を保持し、アプリケーション全体の状態を管理する。gocuiに直接依存する唯一のパッケージ | | ||
| | `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。gocuiに依存しない | | ||
| | `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。gocuiに依存しない | | ||
| | `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。gocuiに依存しない | | ||
| | `main` | `tea.Program` の生成と起動(`tea.WithAltScreen()`)| | ||
| | `internal/ui` | `tea.Model`(`App`構造体)の実装。`Update`でキー入力を処理し、`View`で画面を描画する。アプリケーション全体の状態を保持する。TUIライブラリに直接依存する唯一のパッケージ | | ||
| | `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。TUIライブラリに依存しない | | ||
| | `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。TUIライブラリに依存しない | | ||
| | `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
テーブルセル内のコードスパンに | が含まれて列数を誤認識させている。
| ghq list | fzf の実行... の | がテーブル区切りと解釈される。Markdown パーサーによっては列ずれが発生する。
-| `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない |
+| `internal/selector` | `ghq list \| fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | パッケージ | 責務 | | |
| |-----------|------| | |
| | `main` | gocui.Guiの初期化、Managerの登録、MainLoopの起動。MainLoopの再起動ループを管理する | | |
| | `internal/ui` | gocui.ManagerインターフェースのLayout実装、View生成・配置、キーバインドの登録。`App`構造体を保持し、アプリケーション全体の状態を管理する。gocuiに直接依存する唯一のパッケージ | | |
| | `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。gocuiに依存しない | | |
| | `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。gocuiに依存しない | | |
| | `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。gocuiに依存しない | | |
| | `main` | `tea.Program` の生成と起動(`tea.WithAltScreen()`)| | |
| | `internal/ui` | `tea.Model`(`App`構造体)の実装。`Update`でキー入力を処理し、`View`で画面を描画する。アプリケーション全体の状態を保持する。TUIライブラリに直接依存する唯一のパッケージ | | |
| | `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。TUIライブラリに依存しない | | |
| | `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。TUIライブラリに依存しない | | |
| | `internal/selector` | `ghq list | fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない | | |
| | パッケージ | 責務 | | |
| |-----------|------| | |
| | `main` | `tea.Program` の生成と起動(`tea.WithAltScreen()`)| | |
| | `internal/ui` | `tea.Model`(`App`構造体)の実装。`Update`でキー入力を処理し、`View`で画面を描画する。アプリケーション全体の状態を保持する。TUIライブラリに直接依存する唯一のパッケージ | | |
| | `internal/pane` | ペインの状態管理。現在のディレクトリパス、エントリ一覧、カーソル位置、選択中ファイル一覧、`YankBuffer`を保持。TUIライブラリに依存しない | | |
| | `internal/fs` | ファイルシステム操作。`FileSystem`インターフェースの定義と実装。ディレクトリのエントリ一覧取得、ファイル/ディレクトリのコピー、削除を提供。`Entry`構造体もここで定義する。TUIライブラリに依存しない | | |
| | `internal/selector` | `ghq list \| fzf` の実行とリポジトリパスの取得。`CommandRunner`インターフェースの定義と実装。TUIライブラリに依存しない | |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 91-91: Spaces inside code span elements
(MD038, no-space-in-code)
[warning] 91-91: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/design.md` around lines 85 - 91, The Markdown table in the design
document has code spans in cells that include unescaped pipe characters, which
can be misread as column separators. Update the affected table row for
internal/selector so the `ghq list | fzf` text is escaped or reformatted to
preserve the intended cell boundaries, keeping the package responsibilities
table aligned and parseable.
| for _, src := range a.YankBuf.Entries { | ||
| name := filepath.Base(src) | ||
| dstPath := filepath.Join(dst.Dir, name) | ||
| if err := a.FS.Copy(src, dstPath); err != nil { | ||
| a.Status = fmt.Sprintf("copy error: %v", err) | ||
| return nil | ||
| return | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
同一パスへの上書きコピーを防いでください。
同じディレクトリを両ペインで開いた状態などでは src と dstPath が同一になり得ます。このまま FS.Copy(src, dstPath) に渡すと、実装によってはコピー元を破壊する可能性があります。
修正案
for _, src := range a.YankBuf.Entries {
name := filepath.Base(src)
dstPath := filepath.Join(dst.Dir, name)
+ if filepath.Clean(src) == filepath.Clean(dstPath) {
+ a.Status = "copy error: source and destination are the same"
+ return
+ }
if err := a.FS.Copy(src, dstPath); err != nil {
a.Status = fmt.Sprintf("copy error: %v", err)
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for _, src := range a.YankBuf.Entries { | |
| name := filepath.Base(src) | |
| dstPath := filepath.Join(dst.Dir, name) | |
| if err := a.FS.Copy(src, dstPath); err != nil { | |
| a.Status = fmt.Sprintf("copy error: %v", err) | |
| return nil | |
| return | |
| } | |
| for _, src := range a.YankBuf.Entries { | |
| name := filepath.Base(src) | |
| dstPath := filepath.Join(dst.Dir, name) | |
| if filepath.Clean(src) == filepath.Clean(dstPath) { | |
| a.Status = "copy error: source and destination are the same" | |
| return | |
| } | |
| if err := a.FS.Copy(src, dstPath); err != nil { | |
| a.Status = fmt.Sprintf("copy error: %v", err) | |
| return | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/ui/keybinding.go` around lines 270 - 276, The copy loop in the
keybinding logic can pass the same path as both source and destination when both
panes point to the same directory, so guard against self-copy before calling
a.FS.Copy. In the handler that iterates over a.YankBuf.Entries, compare src and
dstPath (or their resolved/cleaned equivalents) and skip the operation or report
a status message when they match, preserving the existing error handling for
real copy failures.
| case InputModeSearch, InputModeSearchBackward: | ||
| a.searchQuery = input | ||
| if input == "" { | ||
| a.Status = "" | ||
| return nil | ||
| return | ||
| } | ||
| p := a.focusedPane() | ||
| if p == nil { | ||
| return nil | ||
| return | ||
| } | ||
| a.doSearch(p, mode == InputModeSearch) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
後方検索時に検索方向を保存してください。
searchNext / searchPrev は a.searchFwd を参照しますが、確定時に InputModeSearchBackward の方向が保存されていないため、直前の方向が残る可能性があります。
修正案
case InputModeSearch, InputModeSearchBackward:
a.searchQuery = input
+ a.searchFwd = mode == InputModeSearch
if input == "" {
a.Status = ""
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case InputModeSearch, InputModeSearchBackward: | |
| a.searchQuery = input | |
| if input == "" { | |
| a.Status = "" | |
| return nil | |
| return | |
| } | |
| p := a.focusedPane() | |
| if p == nil { | |
| return nil | |
| return | |
| } | |
| a.doSearch(p, mode == InputModeSearch) | |
| case InputModeSearch, InputModeSearchBackward: | |
| a.searchQuery = input | |
| a.searchFwd = mode == InputModeSearch | |
| if input == "" { | |
| a.Status = "" | |
| return | |
| } | |
| p := a.focusedPane() | |
| if p == nil { | |
| return | |
| } | |
| a.doSearch(p, mode == InputModeSearch) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/ui/keybinding.go` around lines 451 - 461, The search confirmation
path in the InputModeSearch/InputModeSearchBackward branch is not persisting the
chosen direction, so searchNext/searchPrev can reuse a stale a.searchFwd value.
Update the search handling in keybinding.go so that when input is confirmed, the
current mode is stored on a.searchFwd before calling a.doSearch, using the mode
check already present in this branch. Keep the fix localized to the search input
handling logic and ensure InputModeSearchBackward sets the backward direction
explicitly.
| func (a *App) Init() tea.Cmd { | ||
| return textinput.Blink |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
textinput の内部メッセージも Update に流してください。
tea.KeyMsg だけを a.input / a.filterInput に渡しているため、textinput.Blink などの非キー系メッセージが捨てられ、カーソル点滅を含む状態更新が進みません。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/ui/model.go` around lines 9 - 10, The App message handling is
dropping non-key `textinput` messages, so cursor blink and related state updates
never reach the inputs. Update the `App.Update` flow to pass all relevant
messages, not just `tea.KeyMsg`, through to `a.input` and `a.filterInput`, while
preserving the existing key handling logic. Use `App.Init` and `App.Update` as
the main touchpoints when locating the fix.
| case "/": | ||
| return a, a.searchForward() |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
? の後方検索入口が消えています。
Line 84-85 では / しか検索入力に接続しておらず、internal/ui/layout.go の InputModeSearchBackward と inputPrefix() が示している後方検索モードにこのキーマップから入れません。今回の移行で ? バインドが到達不能になると、検索の互換性が崩れます。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/ui/model.go` around lines 84 - 85, The keymap in model.go currently
routes only "/" to searchForward, leaving the backward-search entry unreachable
even though InputModeSearchBackward and inputPrefix() still define it. Update
the switch in the relevant input handler to map "?" to the backward-search
action, using the existing backward-search path/function already defined in the
UI flow, and keep "/" mapped to forward search so both search modes remain
reachable.
| func highlightMatch(name, query string) string { | ||
| if query == "" { | ||
| return name | ||
| } | ||
| lower := strings.ToLower(name) | ||
| q := strings.ToLower(query) | ||
| idx := strings.Index(lower, q) | ||
| if idx < 0 { | ||
| return name | ||
| } | ||
| // 元の文字列の大文字小文字を保持してハイライト | ||
| return name[:idx] + "\x1b[30;43m" + name[idx:idx+len(query)] + "\x1b[0m" + name[idx+len(query):] | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focus on the function's behavior with Unicode edge cases using a small read-only probe.
python3 - <<'PY'
import unicodedata
def old(name, query):
if query == "":
return name
lower = name.lower()
q = query.lower()
idx = lower.find(q)
if idx < 0:
return name
return name[:idx] + "[H]" + name[idx:idx+len(query)] + "[/H]" + name[idx+len(query):]
cases = [
("Straße", "ss"),
("straße", "ss"),
("İstanbul", "i"),
("Café", "é"), # combining mark
("Göteborg", "ö"),
("mañana", "ñ"),
]
for name, query in cases:
res = old(name, query)
print(f"name={name!r} query={query!r} -> {res!r}")
PYRepository: myuron/graftx
Length of output: 399
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Probe the exact Go stdlib behavior used by highlightMatch on Unicode edge cases.
cat <<'EOF' >/tmp/highlight_probe.go
package main
import (
"fmt"
"strings"
)
func highlightMatch(name, query string) string {
if query == "" {
return name
}
lower := strings.ToLower(name)
q := strings.ToLower(query)
idx := strings.Index(lower, q)
if idx < 0 {
return name
}
return name[:idx] + "[H]" + name[idx:idx+len(query)] + "[/H]" + name[idx+len(query):]
}
func main() {
cases := [][2]string{
{"Straße", "ss"},
{"straße", "ss"},
{"İstanbul", "i"},
{"Café", "é"},
{"Göteborg", "ö"},
{"mañana", "ñ"},
}
for _, c := range cases {
fmt.Printf("name=%q query=%q -> %q\n", c[0], c[1], highlightMatch(c[0], c[1]))
}
}
EOF
go run /tmp/highlight_probe.goRepository: myuron/graftx
Length of output: 406
Unicode を壊さない位置計算に置き換えてください。
strings.ToLower の byte index をそのまま元の name に使うと、İ などで UTF-8 を途中で切ってしまいます。[]rune ベースで一致位置を探して、元文字列の境界を保ったままハイライトしてください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/ui/render.go` around lines 23 - 35, The highlightMatch function is
using byte indexes from strings.ToLower/strings.Index directly on the original
name, which can split UTF-8 characters. Update highlightMatch to search and
slice using []rune boundaries instead of byte offsets, while preserving the
existing highlight behavior and keeping the original casing of name intact.
fix: CodeRabbitレビュー指摘の修正(PR #14)
概要
TUIの描画・入力処理を
jroimartin/gocuiから Charm 系(charmbracelet/bubbletea+lipgloss+bubbles)へ移行しました。Appをtea.Model(Init/Update/View)として再実装Update内でモード別(通常/テキスト入力/確認/リポジトリ選択)にルーティングtextinputを採用再利用した層(変更なし)
ドメイン層
internal/fs/internal/pane/internal/selectorとinternal/ui/icon.goは UI 非依存のためそのまま再利用。ペイン操作・検索/フィルタ・ヤンク/ペースト・作成/リネーム・削除確認などの挙動・見た目のパリティを維持しています。主な変更
internal/ui/model.go(Init/Update/View),render.go(lipgloss描画),model_test.go,render_test.gomain.go,internal/ui/layout.go(App=tea.Model),keybinding.go(ハンドラ署名変換),selector.gointernal/ui/layout_test.go(→ model_test.go へ),docs/lib/gocui.mdgo.mod/go.sum,flake.nix(vendorHash更新)docs/lib/{bubbletea,lipgloss,textinput}.md生成,docs/design.md/CLAUDE.md追従テスト
Update/Viewを実端末なしで直接駆動するテストへ書き換え(色出力は ANSI プロファイルを強制して決定的に)filterRepoList等)は維持検証
gofmt -lクリーンgo vet ./...golangci-lint run→ 0 issuesgo test ./...→ 全パスnix build .#default成功(vendorHash 更新済み)🤖 Generated with Claude Code
Summary by CodeRabbit
新機能
改善
バグ修正