Skip to content

UIをgocuiからBubble Teaへ移行 - #14

Merged
myuron merged 2 commits into
mainfrom
feat/migrate-bubbletea
Jun 28, 2026
Merged

myuron merged 2 commits into
mainfrom
feat/migrate-bubbletea

Conversation

@myuron

@myuron myuron commented Jun 28, 2026 •

Copy link
Copy Markdown
Owner

概要

TUIの描画・入力処理を jroimartin/gocui から Charm 系(charmbracelet/bubbletea + lipgloss + bubbles)へ移行しました。

  • App を tea.Model(Init/Update/View)として再実装
  • キー入力は Update 内でモード別(通常/テキスト入力/確認/リポジトリ選択)にルーティング
  • 入力欄・セレクタフィルタは bubbles の textinput を採用
  • 描画は lipgloss(左右ペイン枠・カーソル/選択ハイライト・ステータスバー・中央モーダル)
  • リポジトリ選択はアプリ内の中央モーダルとして再実装(gocui時代のMainLoop再起動が不要に)

再利用した層(変更なし)

ドメイン層 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.go
改修 main.go, internal/ui/layout.go(App=tea.Model), keybinding.go(ハンドラ署名変換), selector.go
削除 internal/ui/layout_test.go(→ model_test.go へ), docs/lib/gocui.md
設定 go.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 issues
  • go test ./... → 全パス
  • nix build .#default 成功(vendorHash 更新済み)
  • バイナリ起動スモーク(AltScreen初期化・描画を確認)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 新機能

    • 画面UIが新しいTUI基盤に移行し、検索・フィルタ・リネーム・作成・削除確認などの操作がより一貫した操作感になりました。
    • リポジトリ選択がアプリ内モーダルで行えるようになりました。
  • 改善

    • 画面の表示と操作ヘルプが整理され、ウィンドウサイズ変更にも対応するようになりました。
    • 選択中の項目や検索結果の一致部分が見やすく表示されます。
  • バグ修正

    • キーボード操作、ペイン切り替え、入力キャンセル、削除確認の挙動を見直しました。

描画・入力処理を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>
@coderabbitai

coderabbitai Bot commented Jun 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@myuron, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 23fb9fa0-5d0a-46dd-9097-ef39065b94e0

📥 Commits

Reviewing files that changed from the base of the PR and between 6f7106f and d72f459.

📒 Files selected for processing (3)
  • internal/ui/keybinding.go
  • internal/ui/model_test.go
  • result
📝 Walkthrough

ウォークスルー

TUIフレームワークを jroimartin/gocui から charmbracelet/bubbletea へ全面移行した。App 構造体を tea.Model(Init/Update/View)として再実装し、描画(render.go)・キーバインド(keybinding.go)・セレクタ(selector.go)をBubble Tea前提に書き換えた。あわせてAPIリファレンスドキュメントを差し替えた。

変更内容

gocui → Bubble Tea UI移行

レイヤー / ファイル 概要
依存関係更新とエントリポイント切替
go.mod, main.go, flake.nix
go.mod で charmbracelet/bubbles・bubbletea・lipgloss と muesli/termenv を追加し gocui を削除。main.go で tea.NewProgram(app, tea.WithAltScreen()) + p.Run() による起動へ置換。flake.nix の vendorHash を更新。
App構造体の再設計
internal/ui/layout.go
App 構造体から gocui.Gui 参照・入力バッファ・キーバインド初期化フラグを除去し、textinput.Model(input/filterInput)と端末サイズ(width/height)を追加。NewApp で textinput.New() 初期化。inputPrefix・isTextInputMode ヘルパを追加。
tea.Model実装(Init/Update/View)とキー入力ルーティング
internal/ui/model.go
Init で textinput.Blink を返す。Update が WindowSizeMsg でサイズ更新・KeyMsg を handleKey へ委譲。handleKey が入力モード別に handleSelectorKey・handleInputKey・handleConfirmKey・handleNormalKey へ振り分ける。
keybinding.goのBubble Tea移行
internal/ui/keybinding.go
全ハンドラを gocui 署名(func(...)(error))から tea.Cmd 返却または戻り値なし関数へ変更。enterTextInput を追加。paste・trash・delete・create・rename・search・filter 等を Status 設定+早期 return パターンに統一。
selector.goのBubble Tea移行
internal/ui/selector.go
handleSelectorKey(tea.KeyMsg) を新設し enter/esc/ctrl+c/カーソル移動とフィルタ入力更新を処理。openRepoSelector を tea.Cmd 返却に変更。closeRepoSelector・selectorConfirm・selectorCancel を gocui 引数なし・状態更新のみへ変更。
render.goの描画実装
internal/ui/render.go
新規追加。配色定数、highlightMatch、render・renderPane・renderEntries・renderStatus・renderSelectorModal・renderSelectorList・truncateToWidth を実装。lipgloss で枠・色・幅を制御。
テスト群の更新・追加
internal/ui/model_test.go, internal/ui/render_test.go, internal/ui/keybinding_test.go, internal/ui/layout_test.go
model_test.go を新規追加(Update/View の各挙動テスト)。render_test.go を新規追加(highlightMatch・renderEntries のテスト)。keybinding_test.go の yank 呼び出しをシグネチャ変更に追従。layout_test.go の旧 gocui 依存テストを全削除。
ドキュメント更新
docs/design.md, docs/lib/bubbletea.md, docs/lib/lipgloss.md, docs/lib/textinput.md, CLAUDE.md, .claude/plans/plan.md
docs/design.md を Bubble Tea 前提に全面更新。docs/lib/bubbletea.md・lipgloss.md・textinput.md を gomarkdoc で新規生成追加。docs/lib/gocui.md を削除。

シーケンス図

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: 端末描画
Loading

推定コードレビュー工数

🎯 4 (Complex) | ⏱️ ~60 minutes

関連する可能性のあるPR

  • myuron/graftx#4: internal/ui/keybinding.go の yank/paste/delete/create/rename/search/filter 等のハンドラを gocui 前提から Bubble Tea へ移植した同一ファイル・同種実装の先行フェーズに相当する。

詩

🐰 ふわふわうさぎ、UIを跳ねる
gocuiの檻を飛び出して
Bubble Teaの海へダイブ!
tea.Model で Init・Update・View
lipgloss の色で着飾り
にんじんよりも甘い画面が咲いた 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed gocuiからBubble TeaへのTUI移行という主要な変更を端的に表しており、変更内容と一致しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/migrate-bubbletea

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

toggleSelectでToggleSelect後のMoveDownを削除し、スペースは
カーソル行のマークのトグルのみを行うようにした。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5c9688e and 6f7106f.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (19)
  • .claude/plans/plan.md
  • CLAUDE.md
  • docs/design.md
  • docs/lib/bubbletea.md
  • docs/lib/gocui.md
  • docs/lib/lipgloss.md
  • docs/lib/textinput.md
  • flake.nix
  • go.mod
  • internal/ui/keybinding.go
  • internal/ui/keybinding_test.go
  • internal/ui/layout.go
  • internal/ui/layout_test.go
  • internal/ui/model.go
  • internal/ui/model_test.go
  • internal/ui/render.go
  • internal/ui/render_test.go
  • internal/ui/selector.go
  • main.go
💤 Files with no reviewable changes (2)
  • docs/lib/gocui.md
  • internal/ui/layout_test.go

Comment thread docs/design.md
Comment on lines 85 to +91
| パッケージ | 責務 |
|-----------|------|
| `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ライブラリに依存しない |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
| パッケージ | 責務 |
|-----------|------|
| `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.

Comment thread internal/ui/keybinding.go
Comment on lines 270 to 276
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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.

Comment thread internal/ui/keybinding.go
Comment on lines 451 to 461
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread internal/ui/model.go
Comment on lines +9 to +10
func (a *App) Init() tea.Cmd {
return textinput.Blink

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread internal/ui/model.go
Comment on lines +84 to +85
case "/":
return a, a.searchForward()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread internal/ui/render.go
Comment on lines +23 to +35
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):]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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}")
PY

Repository: 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.go

Repository: 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.

@myuron
myuron merged commit d6f894c into main Jun 28, 2026
2 checks passed
@myuron
myuron deleted the feat/migrate-bubbletea branch June 28, 2026 15:39
myuron added a commit that referenced this pull request Jun 28, 2026
fix: CodeRabbitレビュー指摘の修正(PR #14)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant