90 秒總覽
promptSuggestion 不是單純 UI 提示,而是跨越「回合尾端 hook、AppState、鍵盤互動、telemetry、speculation」的小型子系統。它適合拿來理解:狀態欄位如何驅動互動行為。
執行路徑(PromptSuggestion 案例)
回合結束觸發 stop hook
executePromptSuggestion 啟動
檢查 suppress 條件(plan mode / pending permission / rate limit)
generateSuggestion 產生候選文字
filter 規則淘汰不合格內容
寫入 appState.promptSuggestion
UI 顯示 ghost text
使用者 Tab 或 Right 接受,或直接輸入忽略
flowchart TD
A[stopHooks turn end] --> B[executePromptSuggestion]
B --> C{suppress?}
C -->|yes| D[記錄 suppressed]
C -->|no| E[generateSuggestion]
E --> F[shouldFilterSuggestion]
F -->|drop| D
F -->|keep| G[set appState.promptSuggestion]
G --> H[Typeahead 顯示 ghost text]
H --> I{使用者互動}
I -->|Tab/Right| J[acceptedAt]
I -->|直接送出| K[ignored]
路徑判讀重點
- 建議是「寫入狀態」而非「直接送出命令」。
- 可見 UI 行為依賴
shownAt/acceptedAt兩個時間戳來判斷接受或忽略。 - 同一路徑同時服務 CLI 與 SDK telemetry,因此 suppression reason 必須可追蹤。
關鍵決策(為什麼這樣設計)
決策 1:先寫 AppState,再由 UI 消費
原因:把建議當成狀態,才能讓 REPL、Typeahead、紀錄事件共享同一來源。
代價:狀態欄位變多,需維護 reset 與生命週期一致性。
決策 2:明確 suppression gate
原因:在權限提示、plan mode、限流時強推建議會干擾主流程。
代價:邏輯分支增加,需維護 suppressed reason 的語義穩定。
決策 3:用 forked agent 產生建議並守住 cache 約束
原因:降低主回合負擔,同時利用既有上下文。
代價:參數不能亂改,否則 cache hit rate 會顯著下降。
替代方案與取捨
| 方案 | 優點 | 缺點 | 為何未採用/何時可採用 |
|---|---|---|---|
| 直接在 UI 端規則推導建議 | 不需額外模型請求 | 上下文理解弱,容易給錯建議 | 僅適合 very simple command hints |
| 每次都自動接受建議 | 操作步驟最少 | 高誤觸風險,破壞使用者控制權 | 與目前互動哲學衝突,不採用 |
| 只記錄 accepted,不記錄 ignored | 資料模型較簡單 | 無法評估建議品質與干擾程度 | 不利迭代,不採用 |
失敗路徑與防護
Failure 1:建議內容是 meta 文字或錯誤訊息
症狀:出現「nothing to suggest」等非使用者語句。
防護:在 shouldFilterSuggestion 做多重過濾規則。
Failure 2:使用者已在輸入,但 ghost text 仍覆蓋體驗
症狀:輸入中看到不相關建議。
防護:只有在 input 為空時顯示建議;提交後 reset 狀態。
Failure 3:speculation 與新輸入衝突
症狀:背景推演結果與使用者新方向不一致。
防護:接受/重置建議時中止 speculation,避免污染後續狀態。
實作驗證(你改完要怎麼確認)
- 連續完成兩輪對話:確認第二輪後才會產生建議(early conversation gate)。
- 打開 plan mode 或觸發 permission prompt:確認建議被抑制且不干擾主互動。
- 空輸入按
Tab/Right:確認建議被接受並記錄接受路徑。 - 直接輸入不同內容送出:確認被記為 ignored 並清空舊建議。
如果你要調整建議規則,至少重新驗證「抑制、接受、忽略」三條路徑。