# feat: ターミナルのファイルパスのクリック先をズーム中の右ペインにする (#910)

## 背景

右ペイン（`FilesPane.vue`）は 2.2.0 で出荷済み。入口はセルヘッダのフォルダトグル 1 つだけ。
**2 つ目の入口**として、ターミナル出力のファイルパスのクリックをここへ流す。

現状の行き先（`src/composables/terminalFilePathLinkProvider.ts`）:

| 拡張子 | 今の挙動 |
|---|---|
| ソース系 + `.txt` | アプリ内の**全画面** Files view（`filesGotoFile`） |
| `.md` / `.markdown` / `.json` / `.csv` / `.tsv` | ブラウザ新規タブ（レンダリング済みルート） |
| その他（画像 / PDF / …） | ブラウザ新規タブ（raw ルート） |

ズーム中にソースをクリックすると**全画面ビューへ飛んでグリッドが視界から消える**。
md をクリックすると**別タブが開く**。どちらも「ターミナルを見ながらファイルを見る」にならない。
右ペインは既に隣にあるのに使われていない。

## 決めたこと

| 論点 | 決定 | 理由 |
|---|---|---|
| ペインに流す拡張子 | **アプリがテキストか Markdown として描けるもの全部** = 今の in-app 集合（ソース + `.txt`）∪ レンダリングルートの集合（`.md` `.markdown` `.json` `.csv` `.tsv`） | ペインは CodeMirror ＋ Markdown プレビューを持つ。「別タブでバイト列を見せるしかないもの」だけが新規タブに残る |
| 画像 / PDF / バイナリ | **新規タブのまま** | ペインに表示手段がない。開いても空のエディタになる |
| ズームしていないとき | **今のまま**（全画面 Files view / 新規タブ） | ペインが存在しない |
| セルの cwd 配下に無いパス | **今の行き先にフォールバック** | ペインはセルのディレクトリに根を張り、その上には出られない |
| ペインが閉じているとき | **クリックで開く** | 2 つ目の入口として自然。トグルを先に押させる理由がない |

## 実装

### 1. `src/composables/pathWithinCwd.ts`（新規・純粋）

クリックされたトークンを **cwd 相対パス**に直す。配下に無ければ `null`。

ブラウザ側なので `node:path` は使えない。リンク化されるトークンは必ず `/` を含む
（トークナイザが `:` で区切り、`/` を必須にしているので `C:\...` はそもそもリンクにならない）が、
**cwd は Windows ではバックスラッシュで来る**ため、比較前にセパレータを正規化する。

`.` / `..` はセグメント単位で解決する。`a/../b.ts` は配下に留まるので通し、
`../b.ts` は外に出るので `null`。文字列の前方一致だけで判定すると cwd `/a/b` に対して
`/a/bc/d.ts` が通ってしまうので、セグメント境界で見る。

### 2. `terminalFilePathLinkProvider.ts` に `isPaneViewable()` を足す

既存の 2 つの表（`ROUTE_BY_EXTENSION` と `IN_APP_EXTENSIONS`）の**和**。
表を増やさず、既存の表から導出する — 新しい拡張子を 1 行足したらペインにも自動で乗る。

### 3. `src/composables/filesPaneOpener.ts`（新規・モジュール seam）

リンクプロバイダは composable の中、ペインは `TerminalGrid.vue` の中にいる。
`activeKeymap` / `copyOnSelect` と同じモジュールシングルトンで両者を会わせる
（全セルに prop を通すよりこちらが既存の作法）。

`openInFilesPane(cwd, pathRel): boolean` — **扱えたかどうかを返す**。false ならリンク
プロバイダは今までの行き先へ落とす。

### 4. `FilesPane.vue` に `openFile()` を expose

`loadFile` は既に「別のファイルを開くのは今のファイルを離れること」として `flush()` を
通しているので、そのまま公開すれば未保存バッファの保護は効く。

### 5. `TerminalGrid.vue` で opener を登録

ガード: ズーム中であること、クリック元の cwd が拡大中セルの cwd と一致すること。
ペインが閉じていれば開き、マウントを待ってから `openFile()`。

### 6. `useTerminalConnections.ts` の配線

ペインを先に試し、扱えなければ現状の分岐へ。

## ドキュメント

拡張子ごとの挙動表は 3 箇所にあり、`terminalFilePathLinkProvider.ts` の冒頭に
「変えたら同じコミットで 3 つとも直せ」と明記されている（#834 の再発防止）。同じコミットで直す:

1. `README.md` — "Clicking a file path"
2. `docs/guide/{en,ja}/features.md` — 両方
3. `docs/terminal-notes.md` — link-mechanisms 表

## 実物で動かして見つけたバグ: ツリーとファイルが同じ reqId を共有していた

ブラウザで実際に動かしたところ、クリックでペインが開いてファイルは表示されるのに、
**左のツリーが「Empty directory.」のまま**になった。

原因は `FilesPane.vue` の `reqId` を `loadRoot()`（ツリー）と `loadFile()`（ファイル）が
**共有**していたこと。どちらも「最新のリクエストだけ勝つ」ガードとして `++reqId` してから
`id === reqId` を見るので、ファイル読み込みがツリー読み込みの**結果を捨てて**しまう。

```
mount → start() → loadRoot() が id=1 を取り fetch 中
      → (新しい入口) openFile() → loadFile() が ++reqId で 2 にする
      → loadRoot の fetch が返る → id(1) !== reqId(2) → ツリーを捨てる
```

この 2 つが同時に走る経路が今まで無かったので露見していなかった。**カウンタを
`treeReqId` / `fileReqId` に分ける**。互いを打ち消す理由はない。

回帰テストを `FilesPane.spec.ts` に追加した。ツリーの応答を握ったまま `openFile()` を呼び、
ツリーが残ることを見る。**修正を戻すと実際に落ちる**ことを確認済み（行数 0 になる）。

## 実物での確認（Playwright、別ポート 34599 ＋ 使い捨てワークスペース）

エージェントを使わず zsh セッション（グリッドの「OR LAUNCH: zsh」）で検証した。

| クリックしたもの | 期待 | 結果 |
|---|---|---|
| `src/main.ts`（ソース、ペインは閉じた状態） | ペインが開いて隣に表示 | **ペインが自分で開き**、CodeMirror でハイライト表示。新規タブなし |
| `docs/notes.md`（従来は新規タブ） | ペインに表示 | Preview ボタン付きでペインに表示。新規タブなし |
| `docs/shot.png`（画像） | 従来どおり新規タブ | ペインは閉じたまま、`/api/files/raw?...` が新規タブで開いた |

補足: `shot.png` のようにスラッシュを含まないトークンは**そもそもリンク化されない**
（`terminalFilePathLinks.ts` が `/` を必須にしている）。最初これでテストして「動かない」と
誤読したので記録しておく。

## 関連

#812（拡張子ごとの open-with をユーザー設定にする案）は、ペインを handler の 1 つとして
持てる形にしておく。
