Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target
**/*.rs.bk
.DS_Store
/.superpowers
222 changes: 222 additions & 0 deletions docs/superpowers/specs/2026-10-28-cross-session-search-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# 全セッション検索 設計仕様

作成日:2026-10-28
状態:設計中(利用者の承認待ち)

## 1. この文書が定めるもの

左ペインの左上に検索窓を置き、打った語を**全セッションの出力**から探し、
当たった文を左ペインに新しい順で並べ、選べばその場所へ飛ぶ。
この文書はその仕様を定める。

## 2. なぜ要るか

セッションが 14 本並ぶと、「どれに何を頼んだか」が分からなくなる。

左ペインの行が出している手がかりは 3 つある。名前(既定は作業ディレクトリ)、
`OSC 0` で名乗った題名、枝である。同じリポジトリで複数の PR を並行して作ると、
この 3 つが**全部同じ文字になる**。作業ディレクトリが同じで、枝を切る前はどれも
`main` で、題名も似る。題名は末尾を落として詰めるため、違いが落ちた側にあれば
画面からも消える。

状態の点(動いている・返事待ち・暇)は「どれが私待ちか」には答えるが、
「どれに SES マッチの話を頼んだか」には答えない。

答えられる手がかりは一つしかない。**自分が打った文そのもの**である。
それは termit の中に、すでにある。スクロールバックに載っている。

## 3. 非目標

- **結果を端末の上に重ねて広く出すこと。** 検討したが採らなかった。
窓は左上に置き、結果も左ペインの中で完結させる。
- **`⌘F`(いまのセッションの中の検索)の置き換え。** 併存させる。
- **`^R`(コマンド履歴の検索)の置き換え。** 併存させる。
- **正規表現。** 素の文字列一致だけを持つ。
- **検索語の記憶、履歴、絞り込み条件。**
- **セッション間通信と管理セッション。** 別の企画として保留する(第 11 節)。

## 4. 反目標

次のことが起きるなら、この機能は無いほうがよい。

- **打鍵に対して端末が遅くなること。** 検索のために 1 フレームでも落とすなら、
機能ごと要らない。
- **結果が多すぎて、どれが新しいか分からなくなること。**
14 本ぶんの当たりが区別なく並ぶのは、いまの一覧と同じ困り方である。

## 5. 利用者が決めたこと

- 検索窓は左ペインの左上に置く。
- 当たった文は左ペインの中に一覧で出す。
- 並びは**新しい順**。
- **1 件は 1 当たり**とする。セッション単位にまとめない。
行の時刻を作る工事(第 8 節)が要ることを承知のうえで、こちらを選んだ。

## 6. 画面

**窓は見出しの帯に置く。** 帯(`sidebar::HEADER` = 34pt)に検索窓を置き、
`+` は右端に残す。窓は常に見えている。

**打っているあいだ、セッションの一覧は結果の一覧に差し替わる。**
語を空にすると、元の一覧に戻る。

1 件は 2 段とする。

```
┌────────────────────────┐
│ 🔍 SES │ ← 見出しの帯。右端に +
├────────────────────────┤
│ ● main ⌘4 │ ← 1 段目:どのセッションか(13pt)
│ …この SESマッチ機能… │ ← 2 段目:当たった文の前後(12pt)
│ │
│ ● main ⌘6 │
│ …SESマッチの一覧を… │
└────────────────────────┘
```

1 段目は既存の行と同じ組みにする。状態の点、名前、`⌘` の札。
2 段目は当たった文で、当たった語に `search_hit` の色を敷く。

**2 段目は当たった語を中心にして詰める。** 幅は 20 字ほどしかない。
既存の `fit_tail`(末尾を落とす)でも `fit_head`(先頭を落とす)でも、
当たった語そのものが落ちうる。当たりを真ん中に置き、前後を均等に落とす
三つ目の詰め方が要る。

**2 段目にも伏せ字を通す。** いまの credential masking は描画の経路
(`main.rs` の `masked_cells`)にしかない。結果の行はそこを通らないため、
`secret.rs` の `Redactor::redact` を通してから出す。通さないと、
端末では `••••` に見えている鍵が、結果の一覧では素のまま出る。

## 7. 何を探すか

- **対象は全セッション**。動いているものも、終わったものも、docker 越しのものも、
grid は同じなので区別しない。
- **ただし代替画面にいるセッションは、いま出ている画面しか探せない。**
`vim` や `less` が開いているあいだ、本来のスクロールバックは alacritty の
`inactive_grid` にあり、外から読む口が無い。相手が代替画面を使わず本画面へ
流していくものであれば、この穴には当たらない。穴として受け入れ、README に書く。
- **範囲は画面とスクロールバック**(既定 10000 行/セッション)。
- **素の文字列一致、大文字小文字は無視する。** 正規表現は持たない。
- 行の名前・題名・枝も対象に含める(私の判断。第 12 節)。

## 8. 「新しい順」をどう決めるか

1 件は 1 当たりである。14 本ぶんの当たりを一列に並べるのだから、
**行から時刻が引けなければならない。** grid の行は時刻を持たない。

**外から取れる数では足りない。** `Grid::history_size()` はスクロールバックの
上限(既定 10000 行)で頭打ちになる。エージェントは数分でそこへ届き、
届いたあとは 1 時間前の行といま出た行の区別が付かない。
termit 自身の `CommandTracker` が同じ壁に当たっている
(`src/pty.rs` の「履歴からも押し出された」)。

**押し出された行を自前で数える。** 数える場所は pty の経路である。
`Term` へ渡す前に `Handler` を包み、**本画面にいるあいだの** `linefeed` と
`scroll_up` を数えて、セッションごとに `pushed`(これまでに押し出した行の総数、
単調増加)を持つ。代替画面にいるあいだは数えない。履歴へ入らないからである。
包みは全メソッドを素通しするだけの定型なので、macro で畳む。

**行の通し番号は `pushed + l` で出る。** alacritty の `Line` は画面の上端を 0、
履歴側を負で数える。履歴の最も古い行の通し番号は `pushed - history_size` で、
`Line(l)` の通し番号はそこから数えて `pushed + l` になる(`l` は負でもよい)。

**目盛りは `(pushed, Instant)` の環状の記録にする。** pty から 1 回読むごとに
1 つ刻む。当たりの通し番号から二分探索で時刻が出る。数千件で 32KB ほど。
分解能は 1 秒あれば足りる。溢れた古い側は、残っている最古の時刻として扱う。

**この通し番号は `Enter` でも効く。** 結果を取ってから選ぶまでに出力が流れると、
覚えた `Match` の位置はずれる。通し番号で覚えておけば、押すときの `pushed` から
いまの行位置が `通し番号 - pushed` で出る。`history_size` を超えて負の側へ
出ていれば、その行はもう消えている。

**一覧が 1 本で埋まらないようにする。** いちばんよく喋っているセッションの
当たりが新しい順の上位を占めると、残りの 13 本が一覧に出てこない。
これは第 4 節の反目標そのものである。1 本あたりの採用数に上限を置き
(既定 20 件)、全体の上限(200 件)の中で各本に席を残す。
同じ文が続けて当たるとき(全画面 UI の描き直しで同じ行が何度も履歴へ入る)は、
連続するぶんを 1 件にまとめる。

## 9. 操作

| キー | 動き |
|---|---|
| `⌘⇧F` | 検索窓へ入る |
| `↑` `↓` | 件を移動する |
| `Enter` | そのセッションへ切り替え、その位置まで送り、`⌘F` と同じ強調を点ける |
| `Esc` | 窓を出て、元の一覧へ戻る |
| クリック | 窓に入る/件を選ぶ |

**`^` の鍵は増やさない。** README は Ctrl を 6 つしか取らないと約束している。
`⌘` の 1 文字はジャンプの札(`⌘A` `⌘G` `⌘J` …)で埋まっているため、
修飾を足して `⌘⇧F` とする。`⌘F`(いまのセッションの中)の隣として覚えられる。

**`⌘⇧F` は、いまは `⌘F` として通ってしまう。** `src/input.rs` の物理キー側の
分岐は、Cmd のときに Shift を除いていない。先にここを分けないと、新しい鍵が
`FindInScreen` に落ちる。

**日本語入力は確定した文字だけを受ける。** 変換中の文字列は検索に渡さない。
`main.rs` の既存の扱い(`preedit` を `Ime::Commit` まで渡さない)に合わせる。

## 10. 速さ

**この節がこの機能の生死を決める。**

14 本 × 10000 行を毎打鍵で走ると間に合わない見込みがある。
ただし、この仕様は見込みで作りを決めない。先に測る。

- **測る。** `--bench` に「n 本 × m 行を 1 語で走る」を足し、1 回あたりの時間を出す。
数字が出てから、同期で走らせるか別スレッドへ出すかを決める。
- **打鍵ごとに走らせない。** 打ち終わって一拍おいてから走る。
- **新しい方から走り、上限(既定 200 件)で止める。**
新しい順という並びは、この早じまいと噛み合う。古い当たりは、そもそも要らない。
- **描画を止めない。** 検索中も 1 フレームを落とさないこと。
`TERMIT_FRAME_LOG=1` の毎フレーム出力で確かめる。

**同期では走らせられないことは、すでに測ってある。** `docs/performance.md` の
1 フレームは 16.61ms で、うち 16.27ms が `get_current_texture` の中にある。
UI スレッドに残っている余白は 0.3ms ほどで、10000 行の走査はそこに入らない。
走るのは別スレッドになる。

**錠は塊ごとに放す。** 各セッションの `Term` は `FairMutex` の下にあり、
pty の読み手が 64KiB ごとにそれを取る。10000 行を取りっぱなしで走れば、
その読み手が止まり、エージェントの出力そのものが止まる。
数百行ずつ走っては放し、取り直す。全文を写してから走る案は採らない。
14 本 × 10000 行 × 80 桁で 10M 文字を超え、常駐 85MB の看板に合わない。

目標:10000 行 × 14 本で、1 回の検索が描画を 1 フレームも落とさず、
どのセッションの出力も目に見えて止めない。

## 11. 隣にある企画(この文書では扱わない)

セッション間で文字をやり取りし、1 本の「管理セッション」が他に訊いて回り、
仕事を割り振る案がある。docker 越しでも届くよう、ソケットではなく
`OSC` を 1 本足して pty そのものを通す形まで出ている。
画面に出た文字が別のセッションへの入力になりうるため、既定では塞ぐことになる。

この文書では扱わない。検索が先である。

## 12. 私(設計者)が決めたこと

利用者が言っていないが、仕様として決めたもの。覆してよい。

- 鍵は `⌘⇧F`。先に `src/input.rs` の Cmd + Shift を分ける。
- 素の文字列一致、大文字小文字は無視、正規表現なし。
- 窓は常に見えていて、`⌘⇧F` は近道にすぎない。
- 行の名前・題名・枝も探す対象に含める。
- 画面の中の当たりも含める。いま出ているものこそ知りたい。
- **結果は一度取ったら、次の打鍵まで固める。** 出力を追いかけて並べ直さない。
- **`Enter` のときに場所を取り直す。** 走っているセッションでは、当たった行は
数百ミリ秒で流れる。第 8 節の通し番号から今の行位置を出し直し、
もう消えていれば、そのセッションへ切り替えて `⌘F` に語を渡すだけにする。
- **1 本あたりの採用は 20 件まで。連続する同じ文は 1 件にまとめる**(第 8 節)。
- 走査は別スレッド、錠は数百行ごとに放す(第 10 節)。
- 結果の 2 段目は `Redactor::redact` を通す(第 6 節)。
- 代替画面にいるセッションの穴は受け入れ、README に書く(第 7 節)。

## 13. 作る人に委ねること

- 打ち終わってから走り出すまでの間(デバウンス)の長さ。
- 当たりを中心に残す詰め方の実装。
- 件数が左ペインに収まらないときの送り方。
- 名前・題名・枝に当たったときの時刻の扱いと、`Enter` の動き。
- 結果の一覧に、いまの一覧と同じ並べ替え・ドラッグを効かせるかどうか(効かせない想定)。
Loading