オートコンプリートの設計

English version is autocomplete-design.md.

Mytty のインラインオートコンプリートは実装済みの機能としてすでにリリースされています(README.md の「Local autocomplete」の記述と、実装本体である Sources/MyTTYApp/TerminalAutocomplete.swift / TerminalAutocompleteCoordinator.swift を参照)。このページはその設計判断を説明します。リリースされた機能が、当初の実現可能性調査(feasibility study)が描いていた計画よりも意図的に小さくなっている点も含めて振り返ります。

実際にリリースされたもの

Mytty のオートコンプリートは、一度に一つだけのインライン候補をカーソル直後に薄いゴーストテキストとして表示し、Tab で確定します。候補の出所は 2 つだけに絞られています。ペインが「成功したコマンド」を学習していく中で、直近 200 件のうちのどれかの先頭部分と一致する入力があれば、続きを提案します。もう一つは単一のヒューリスティックで、mkdir <path> が成功したあとに cd <path> を次のコマンドとして提案します。

候補パネルは存在せず、コマンド・フラグ・引数の静的データベースも持ちません。アプリを再起動すればそれまでに学習したコマンド履歴も消えます。ペインが生きている間だけメモリ上に保持される仕組みです。これは、以前の実現可能性調査(この文書を書いている時点で、そこにあった「保留中」という記載自体がすでに古くなっていた)が描いていた、Fig 互換の多段階システムに比べるとかなり小さく収まっています。その計画と実際に作られたものとのギャップこそが、ここで説明する価値のある設計判断の結果であり、単なる未達ではありません。

シェルブリッジなしで入力バッファを追跡する

実現可能性調査が出した中心的な結論は、libghostty のキーイベントと画面上のテキストだけからシェルの入力バッファを再構成するのは一般には信頼できない、というものでした。シェルが履歴、補完プラグイン、複数行編集、ペースト、vi モード、カーソル移動を自前で処理し始めた瞬間に、その再構成は正しさの根拠を失います。そこで提案されていたのは zsh の ZLE(Zsh Line Editor)と連携し、シェル本体が持つ実際の BUFFER と CURSOR を、サーフェスごとに常設した IPC チャネル経由で報告させるという方式でした。これなら Mytty 側は推測ではなく、シェル本体が持つ確定した値を直接受け取れます。

そのブリッジは結局作られませんでした。代わりに実装されたのは、実現可能性調査が「却下すべき方式」として挙げていたものに近いです。ただし、バッファの内容がわからなくなった瞬間にそれを正直に認めて動作を止めることで安全側に倒してあります。TerminalAutocomplete.swift の TerminalAutocompleteSession は、TerminalAutocompleteEventMapper を通してキーイベントを監視し、現在の行の内容をそれなりに再現しようとします。通常の文字入力はローカルの currentInput 文字列に追記され、Delete は末尾を削り、Return/Enter でコマンドとして確定されます。矢印キー、Control/Option 修飾を伴うナビゲーション、IME 変換中のマークテキストなど、正しく追跡できないものを検知すると、inputIsReliable フラグを false にして、行がリセットされる(Return、または Control-U のような明示的なリセット)まで候補提案自体を止めます。誤った推測が画面に出ることはなく、セッションはただ黙るだけです。

これが成立するのは、この機能の影響範囲が設計上そもそも小さいからです。候補はカーソル位置に表示される 1 行のゴーストテキストにすぎず、Tab 以外のどんなキー入力も候補を消すか更新し、ユーザーが Tab を押さない限り端末には何も注入されません。ときどき間違うシャドウバッファでも、間違えたときの結果が「候補が消えるだけ」であれば許容できます。もしそれが端末を勝手に編集する機能だったら、この程度の不確実性は許容できなかったはずです。この許容度の高さは、シェル固有のブリッジが結局不要だったことの理由でもあります。実装された仕組みはペインが zsh を動かしているか bash を動かしているか fish を動かしているかを一切気にしません。そもそもシェルと通信していないからです。AppKit 層で NSEvent を読んでいるだけであり、当初の段階計画(まず zsh、その後 bash と fish を後の段階で追加)は今実際に存在するものには当てはまりません。拡張すべきシェル固有の統合そのものが最初から存在しないからです。

コマンド完了の検知

コマンドがいつ終わったか、終了コードは何かを知ることは、それを履歴として学習するかどうか、mkdir → cd の追加提案を出すかどうかを判断するために必要になります。Mytty 自身は OSC 133 のシェル統合マークを直接パースしていません。libghostty がすでに内部でそれを追跡していて、終了コードと実行時間を持つ構造化された GHOSTTY_ACTION_COMMAND_FINISHED アクションとして公開してくれます。GhosttyRuntime がそれを GhosttySurfaceView.receiveCommandFinished に転送し、.commandFinished イベントになったところで TerminalWindowController が TerminalAutocompleteCoordinator.handleCommandFinished にルーティングします。MyTTYApp 側で OSC ストリームをもう一度スキャンするのではなく、アダプター境界から構造化されたアクションを受け取る形にしてあるのは、Ghostty 固有の処理を GhosttyAdapter の内側に留めておくためです(この境界がなぜ一つに絞られているかは architecture_ja.mdを参照)。

オーバーレイの位置合わせ

ゴーストテキストのラベルは、シェル自身のカーソルのちょうど右隣、最後に入力した文字の 1 セル右に正確に重ならなければならず、フォントサイズやウィンドウのリサイズにも遅延なく追従する必要があります。GhosttySurfaceView.terminalCursorRect はこれを libghostty から直接、ghostty_surface_ime_point 経由で取得しています。これは IME 変換中に候補ウィンドウを配置するのに使っているのと同じキャレット矩形です。テキストグリッドから別途カーソル位置を計算するのではなく同じ呼び出しを再利用しているおかげで、オートコンプリートのオーバーレイと IME 変換がカーソル位置について食い違うことは構造上起こりえません。両者は同じ座標を読んでいるからです。

意図的に見送ったもの

実現可能性調査の後半の段階では、bash と fish 向けのブリッジ、tmux サポート、Git ブランチのような動的候補、MIT ライセンスの Fig 互換補完データベースの同梱、コマンド発見のための --help パースが提案されていました。そのいずれも実装されておらず、それは「もっと大きな実装のうち欠けている部分」ではありません。リリースされた設計そのものが補完データベースを差し込む場所を持っていません。一度に一つだけ、そのペイン自身がすでに行ったことに由来する候補だけを出す、という作りだからです。静的補完へ拡張するとしたら、今のバージョンが意図的に持っていない候補パネルとキーボードナビゲーションを新たに作ることになり、それは今日の「Local autocomplete」が指しているものとは別の、より大きな機能になります。

コマンド履歴の学習にも意図的に狭い安全網しか張っていません。TerminalAutocompleteEngine と TerminalAutocompleteSession はコマンドラインの文字列をそのまま保存するだけで、しかも終了コードが 0 のときに限って保存します。フラグや引数に含まれる秘密情報を伏せ字にする処理は存在しません。実際の緩和策はフィルタリングではなく範囲の狭さで、ディスクには何も書き込まれず、リストはメモリ上で最大 200 件までに制限され、ペインを閉じれば消えます。