feat(cli): improve export format completion navigation - #3701
Conversation
Critical: - Guard phase-2 cycling by checking buffer text starts with "/export " so a manually edited buffer is never clobbered by stale nav state (C1) - Derive export format suggestions from slashCommands.subCommands to keep a single source of truth with the command registry (C2) - Reset completionSelectionWasNavigatedRef on showSuggestions rising edge instead of on every suggestions change to avoid a race where an already-navigated selection is forgotten before Enter (C3) - Add regression tests for isPerfectMatch + navigated + Enter, including the positive path and a control case (C4) Suggestions: - Prefix-guard getExportFormatFromInput to skip regex on non-/export input (S1) - Drop trailing space from setExportCompletionInput output so buffer text is no longer implicitly coupled to the cycling heuristic (S2) - Document the two-phase state machine (one-shot fill + cycling) (S3) - Accept Tab as an additional cycling key alongside Up/Down (S4) - Remove the unconditional ref reset at the tail of handleInput; correctness is now guaranteed by the buffer-text guard (C1) and the showSuggestions edge-triggered useEffect (C3) (S5)
Review feedback addressedThanks @wenshao for the thorough review. All 4 Critical and 5 Suggestion items have been addressed in Critical
Suggestions
VerificationUnit tests ( Manual E2E (dev CLI):
Notes
|
wenshao
left a comment
There was a problem hiding this comment.
测试覆盖缺口
以下代码路径缺少测试覆盖:
- Phase 1 上箭头 —
hasExportFormatSuggestions代码块中的isCompletionUpKey路径(包括从索引 0 回绕到 lastIndex)无测试。所有导出测试仅使用下箭头 (\u001B[B)。 - Phase 2 上箭头 — 循环测试仅发送下箭头。上箭头回绕逻辑无测试覆盖。
- Phase 2 Tab 键 —
isCompletionTabKey分支完全无测试覆盖。
建议添加对应测试用例以覆盖这些路径。
— deepseek-v4-pro via Qwen Code /review
- Phase 2 cycling guard: replace startsWith('/export ') with strict
getExportFormatFromInput() to prevent overwriting inputs with extra
arguments (e.g. '/export html --verbose').
- ACCEPT_SUGGESTION in Phase 1 popup: add hasExportFormatSuggestions
branch so Tab/Enter seeds exportCompletionSelectionIndexRef,
allowing Phase 2 cycling to continue from the selected format
(consistent with Up/Down arrow behavior).
- Add 4 regression tests: Phase 1 Up wrap, Phase 2 Up wrap, Tab
seed + Phase 2 Tab cycle, guard prevents overwriting extra args.
Ref: PR QwenLM#3701 second-round review by wenshao
…rt completion - S6: use dynamic exportFormatSuggestions.findIndex() for highlight index instead of static EXPORT_FORMAT_COMPLETIONS.indexOf() - S7: derive Phase 2 cycling current index from buffer text via getExportFormatFromInput + indexOf, with defensive ref fallback - S8: extract getNextExportCompletionIndex as module-level pure function; cache exportCycleFormats via useMemo to avoid per-keystroke .map() - S9/S10: add tests for ESC and Ctrl+C reset of export cycling state
Third-round review feedback addressedThanks @wenshao for the additional review. All 5 Suggestion items from the third round have been addressed in Summary
VerificationUnit tests ( Typecheck: all workspaces pass ( Notes
|
…ough, and improve documentation
wenshao
left a comment
There was a problem hiding this comment.
Overview
Adds a two-phase state machine so /export + arrow/Tab cycles through html/md/json/jsonl and keeps the suggestion panel persistent. ~210 production lines + ~670 test lines, with 11 explicit regression tests for prior review rounds.
Strengths
- Test coverage is unusually thorough — almost every edge case I'd flag already has a regression test (ESC, Ctrl+C, manual edit, superset matching, missing-format fallthrough, Tab seeding Phase 2, Up-arrow wrap, trailing whitespace, extra-args guard).
- Format list is derived from
slashCommands.subCommandsso adding a new export sub-command picks up cycling automatically; the static fallback is only used during early renders. getExportFormatFromInputis a strict whitespace-trimmed parser that correctly prevents Phase 2 from clobbering inputs like/export html --verbose.- Documentation comments explain why (invariants like "the two phases are mutually exclusive", "do NOT widen
hasExportFormatSuggestions") rather than just what — unusually good for this codebase.
Issues raised inline
- Test typos (must fix) —
\u001B[B](stray]) at three places in the test file. - Perfect-match Enter behavior change is broader than
/export— affects any slash command with sub-commands; not documented in the PR body. - PR description / code mismatch — description says
/export md(trailing space), code writes/export md(no trailing space). - Ctrl+U missing from the ref-reset set.
- UX asymmetry — users who type
/export <fmt>directly don't get cycling because the ref is only ever seeded by Phase-1setExportCompletionInput.
Smaller notes (non-blocking)
EXPORT_FORMAT_COMPLETIONS(line 78) duplicates theslashCommandssource of truth as a static fallback; if a format is ever removed fromexportCommand.tsbut not here, the fallback will silently re-introduce it during early renders.getExportFormatFromInputisexported but no consumer outside this file uses it — if it's only for testability, prefer keeping it module-private.- The "should not clobber manually edited buffer" test depends on
useTextBufferactually applying\u0015(Ctrl+U). Anexpect(buffer.text).toBe('')between the Ctrl+U and/helpwrites would pin the intermediate state so a future hook change doesn't make the test pass for the wrong reason.
Risk
- Correctness: medium-low — well-tested for
/export, but the broader perfect-match Enter change deserves explicit validation against/memory,/agents, etc. - Regression: low for
/export; low-medium for other slash commands with sub-commands. - Performance: negligible.
- S1: Strengthen EXPORT_FORMAT_COMPLETIONS fallback comment with an IMPORTANT sync warning for format removals - S2: De-export getExportFormatFromInput (no external consumers) - S3: Add intermediate buffer-clear assertion after Ctrl+U in test to pin state and prevent false positives from future hook changes
shenyankm
left a comment
There was a problem hiding this comment.
All issues and smaller notes from this review have been addressed in cd40dae:
Issues resolved:
- Test typos — ANSI escape sequence stray
]fixed across 3 locations. - Perfect-match Enter doc — PR body updated to document that the
isPerfectMatch + navigated + Enterpath applies to all slash commands with sub-commands. - PR description trailing space — All
/export mdreferences corrected to/export md(no trailing space). - Ctrl+U ref-reset — Added
exportCompletionSelectionIndexRef.current = nullin Ctrl+U handler (L1370-1371). - UX asymmetry — Added
useEffectthat seedsexportCompletionSelectionIndexReffromgetExportFormatFromInput(buffer.text)so manually typing/export <fmt>also enters Phase 2 cycling.
Smaller notes addressed:
- S1: Strengthened the
EXPORT_FORMAT_COMPLETIONSfallback comment with an IMPORTANT sync warning for format removals. - S2: De-exported
getExportFormatFromInput(confirmed zero external consumers). - S3: Added intermediate
expect(...).not.toContain('/export')assertion after Ctrl+U to pin buffer-clear state.
Tests: 127 passed, 2 skipped. TypeScript: no new errors in changed files.
OverviewThis PR introduces a two-phase completion state machine for the
It also incidentally changes shared logic: when Strengths
Issues to discuss1. Architectural coupling: ~310 lines of
|
| Dimension | Rating | Notes |
|---|---|---|
| Correctness | Medium-Low | /export path is well-tested; the perfect-match Enter change for /memory, /agents edge cases (see #5) needs more validation. |
| Regression risk | Low | Phase-2 guard uses strict format parsing; external inputs (/help, /export html --verbose) are not silently overwritten. |
| Performance | Negligible | Seeding effect runs startsWith + slice on every buffer.text change, O(1). |
| Maintenance cost | Medium-High | Single file +366 lines of specialized logic, 3 refs, 2 effects, 1 useMemo — #1 hook extraction would significantly lower this. |
Recommended merge strategy
Overall the PR is functionally correct and well-tested, and can be merged. But I strongly recommend a follow-up to extract the export-completion logic into a useExportCompletion hook — otherwise future similar UX work for /agents / /memory will be tempted to copy this two-phase state machine.
Blocking item: #5 (navigate → backspace → retype → Enter) — please add a test to confirm behavior.
Non-blocking cleanups: #2, #3, #4, #7 — pure cleanup, can ship together in follow-up.
中文版本(点击展开)
概览
这个 PR 给 /export 命令引入了一个两阶段补全状态机:
- Phase 1:用户输入
/export,弹出格式列表(html/md/json/jsonl),按 Down 或 Tab 自动填入第一个格式。 - Phase 2:弹窗关闭后,buffer 变成
/export <fmt>,继续按 Up/Down/Tab 在格式间循环切换;建议面板持续可见。
同时顺带修改了通用逻辑:当 isPerfectMatch + 用户已经按过箭头 + Enter 时,自动补全选中项而不是提交原始 buffer。这条规则适用于所有含 sub-command 的 slash 命令(/memory、/agents 等)。
优点
- 测试覆盖罕见地完整:16+ 个新测试点逐个回归 review 提出的问题(ESC、Ctrl+C、Ctrl+U、手动编辑、superset 匹配、缺失格式 fallthrough、Tab 种入 Phase 2、上下箭头回绕、尾部空格、
--verbose额外参数防误覆盖)。 - 格式列表从
slashCommands.subCommands派生(InputPrompt.tsx:103-122),exportCommand.ts是 SSOT;新增格式自动启用循环。 getExportFormatFromInput用严格的trim+ 切片 + 显式空格判断,正确把/export html --verbose排除在 Phase-2 循环外(InputPrompt.tsx:37-56)。- 注释解释 Why 而不是 What:例如"两个 phase 是互斥的,不要扩大 hasExportFormatSuggestions"——这种 invariant 注释在该仓中不常见。
待商榷的问题
1. 架构耦合:把 ~310 行 /export 特化逻辑塞进通用输入组件
InputPrompt.tsx 是所有输入场景共享的组件。本次新增的代码里:
- 模块级常量:
EXPORT_COMMAND_INPUT、EXPORT_FORMAT_COMPLETIONS - 模块级函数:
getExportFormatFromInput、getNextExportCompletionIndex - 组件内 ref:
exportCompletionSelectionIndexRef、completionSelectionWasNavigatedRef、prevShowSuggestionsRef - 组件内 useMemo / useEffect:
exportFormatSuggestions、exportCycleFormats、seeding effect、navigated-flag reset effect - handleInput 内大段:
hasExportFormatSuggestions、getExportIndexForActiveSuggestion、setExportCompletionInput、acceptActiveCompletionSuggestion、Phase-2 guard - 渲染层:
shouldKeepExportFormatSuggestions等 5 个 displayed* 派生 props
建议:抽成 useExportCompletion(buffer, slashCommands) 自定义 hook,返回 { shouldShowFormatSuggestions, displayedSuggestions, displayedActiveIndex, handleArrowOrTab, handlePerfectMatchEnter, resetCyclingState }。InputPrompt 只调用 hook、不持有特化状态。这样:
- 该 hook 可以独立单测,不需要渲染整个
InputPrompt。 - 未来
/agents、/memory想做类似的循环,能复用 hook 而不是再复制一份。 InputPrompt.tsx净增 ~50 行(hook 调用 + 两处 dispatch),而不是 ~310 行。
非阻塞,但作为长期维护成本值得考虑。
2. exportCompletionSelectionIndexRef 的值从未被读取,只当布尔哨兵用
通读所有用法:
- 设值:
setExportCompletionInput(写 index)、seeding effect(写 index) - 读值:
shouldKeepExportFormatSuggestions用!== null,Phase-2 guard 用!== null,两处都只判 null
而循环时的 currentIndex 是从 exportCycleFormats.indexOf(parsedFormat) 现算的(InputPrompt.tsx:286),不读 ref 里的数字。
建议:把 ref 改成 exportCyclingActiveRef = useRef(false),语义更清楚。能少一处"为什么存了 index 又不用"的认知负担。
3. navigated-flag reset effect 的 if/else if 是冗余的
InputPrompt.tsx:157-166:
useEffect(() => {
if (!completion.showSuggestions) {
completionSelectionWasNavigatedRef.current = false;
} else if (!prevShowSuggestionsRef.current) {
completionSelectionWasNavigatedRef.current = false;
}
prevShowSuggestionsRef.current = completion.showSuggestions;
}, [completion.showSuggestions]);deps 只有 completion.showSuggestions,所以 effect 只在 true↔false 转换时才会跑:
- false → true:
!showSuggestions假,!prev(prev 是 false)真 → reset - true → false:
!showSuggestions真 → reset
两条路径都是 reset。可以直接:
useEffect(() => {
completionSelectionWasNavigatedRef.current = false;
}, [completion.showSuggestions]);prevShowSuggestionsRef 整个可以删掉。纯简化。
4. EXPORT_FORMAT_COMPLETIONS 静态 fallback 的实际收益不明
InputPrompt.tsx:24 与 117-121:注释说"用于早期 render——slashCommands 还没接入时"。但 slashCommands 是 InputPromptProps 必填字段,由父组件构造时传入;所有 production 调用都已经带着完整的命令列表。
问题:fallback 跟 exportCommand.ts 里的 subCommands 列表是双重维护——如果有人删掉 jsonl 但忘了同步这里,fallback 会"复活"它。注释里加了大写 IMPORTANT 提醒,但提醒不能阻止漂移。
建议:直接去掉 fallback,让 exportFormatSuggestions 在 slashCommands 没接入时返回空数组;hasExportFormatSuggestions 自然不触发。如果作者能给出一个真实的早期 render 场景(哪条 code path 会在 slashCommands 还没准备好时进入这段循环逻辑),再保留也不迟。
5. perfect-match Enter 行为变更影响范围比 PR 描述更广(建议补测试)
PR body 已经说明这条变更适用于所有有 subCommands 的 slash 命令。但有一个边缘场景测试没覆盖:
场景:用户输入 /memory,按 Down 一次(ref 翻 true),按 Backspace 退到 /memor,再输入 y 回到 /memory,按 Enter。
- 关键问题:在
/memor阶段,popup 是否消失?若消失 → ref 被 reset effect 清掉,行为正常提交;若仍显示 "memory" 这一条建议(不消失) → ref 还是 true,Enter 会自动补全成第一个 sub-command 而不是提交/memory。
这取决于 useCommandCompletion 在部分匹配时的 showSuggestions 行为。如果 popup 在跨编辑过程中持续可见,ref 会"粘住",导致 Enter 行为出乎意料。
建议:加一个测试模拟"navigate → backspace → 重新输入 → Enter",验证 popup visibility 翻转能正确清状态;如果能复现"粘住",则要在 buffer.text 变化时也加一道清理。
6. handleInput 内的局部闭包每次按键都会重建
acceptActiveCompletionSuggestion、setExportCompletionInput、getExportIndexForActiveSuggestion、hasExportFormatSuggestions 都是 handleInput useCallback 内部的局部值/函数。
性能影响微乎其微(闭包很便宜),但可读性上让 handleInput 进一步膨胀。配合第 1 条建议,抽到 hook 后这部分自然也就出去了。
7. 渲染层 4 个并列 ternary 可聚合
InputPrompt.tsx:473-484,displayedSuggestions / displayedActiveSuggestionIndex / displayedSuggestionsScrollOffset / displayedSuggestionsLoading 4 个独立 ternary 共用同一个 shouldKeepExportFormatSuggestions gate。
可以聚成单个 displayProps 对象的 ternary:
const suggestionDisplayProps = shouldKeepExportFormatSuggestions
? { suggestions: exportFormatSuggestions, activeIndex: selectedExportFormatIndex, isLoading: false, scrollOffset: 0 }
: { suggestions: activeCompletion.suggestions, activeIndex: activeCompletion.activeSuggestionIndex, isLoading: activeCompletion.isLoadingSuggestions, scrollOffset: activeCompletion.visibleStartIndex };JSX 里 <SuggestionsDisplay {...suggestionDisplayProps} ... />。纯结构清理。
风险评估
| 维度 | 评级 | 说明 |
|---|---|---|
| 正确性 | 中-低 | /export 路径测试充分;perfect-match Enter 改动对 /memory、/agents 的边缘场景(见第 5 条)需要再验一次。 |
| 回归风险 | 低 | Phase-2 guard 用严格的格式解析,外部输入(/help、/export html --verbose)不会被误覆盖。 |
| 性能 | 可忽略 | seeding effect 每次 buffer.text 变化跑一次 startsWith + slice,O(1)。 |
| 维护成本 | 中-高 | 单一文件 +366 行特化逻辑、3 个 ref、2 个 effect、1 个 useMemo——第 1 条的 hook 抽离能显著降低这部分。 |
建议合并策略
总体上 PR 的功能正确、测试到位,可以合并;但强烈建议在 follow-up 里把 export 完成逻辑抽到 useExportCompletion hook——否则未来给 /agents / /memory 做类似 UX 时会有"复制一遍这套两阶段状态机"的诱惑。
阻塞项:第 5 条(navigate → backspace → 重新输入 → Enter)建议补一个测试,确认行为符合预期。
非阻塞优化:第 2、3、4、7 条都是纯清理,可以一起放进 follow-up。
Address all feedback from PR QwenLM#3701 review comment: - Extract ~310 lines of /export state machine from InputPrompt into dedicated useExportCompletion hook - Replace exportCompletionSelectionIndexRef (number|null) with cyclingActiveRef (boolean) since index was never read - Simplify navigated-flag lifecycle: reset on buffer.text changes instead of popup visibility transitions; add navigatedTextRef snapshot to prevent sticky autocomplete after buffer edits - Remove static EXPORT_FORMAT_COMPLETIONS fallback; derive entirely from slashCommands.subCommands - Aggregate 4 parallel ternaries into single suggestionDisplayProps - Add regression test: navigate + backspace + retype + Enter should submit raw buffer, not autocomplete - Remove redundant navigatedRef reset in ESC handler (already covered by exportCompletion.reset())
|
All seven items from this round's review have been addressed in commit Items resolved#1 — Hook extraction ✅ #2 — Boolean ref rename ✅ #3 — Simplified navigated-flag lifecycle ✅ (improved) #4 — Removed static fallback ✅ #5 — Regression test ✅ (blocking item) #6 — Closure rebuilding ✅ #7 — Ternary aggregation ✅ Bonus fixRemoved redundant Validation
Ready for re-review. |
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
[Suggestion] useExportCompletion hook 缺少独立的单元测试文件。所有覆盖均来自 InputPrompt 集成测试(17+ 用例),但纯函数 getExportFormatFromInput、getNextExportCompletionIndex 的边界情况、ref 逻辑及 useEffect 播种行为均未做直接测试。建议创建 useExportCompletion.test.ts 覆盖至少以下场景:
getExportFormatFromInput边界(空输入、无空格、无格式名、非法格式名)getNextExportCompletionIndex边界(空列表、越界索引、单元素列表)- 播种 effect:验证手动输入
/export <fmt>后cyclingActiveRef为 true reset()清除所有 refsuggestionDisplayProps在非 cycling 状态时为 null
— deepseek-v4-pro via Qwen Code /review
|
Addressed the latest review feedback in Changes made:
Validation run:
|
wenshao
left a comment
There was a problem hiding this comment.
新一轮反馈都已落地:useExportCompletion.test.ts 的 20 个用例覆盖了纯函数边界和 hook 行为;markNextTextChangeAsUserInput 握手避免了 history/programmatic setText 误触发 phase-2;reverse/command-search 已优先于 export 面板;返回对象和 display props 都做了 memoize。本地试合并到当前 main 后 150 passed / 1 skipped,CI 三平台全绿。
* feat(cli): improve export format completion navigation * fix(cli): address PR #3701 review feedback on /export completion Critical: - Guard phase-2 cycling by checking buffer text starts with "/export " so a manually edited buffer is never clobbered by stale nav state (C1) - Derive export format suggestions from slashCommands.subCommands to keep a single source of truth with the command registry (C2) - Reset completionSelectionWasNavigatedRef on showSuggestions rising edge instead of on every suggestions change to avoid a race where an already-navigated selection is forgotten before Enter (C3) - Add regression tests for isPerfectMatch + navigated + Enter, including the positive path and a control case (C4) Suggestions: - Prefix-guard getExportFormatFromInput to skip regex on non-/export input (S1) - Drop trailing space from setExportCompletionInput output so buffer text is no longer implicitly coupled to the cycling heuristic (S2) - Document the two-phase state machine (one-shot fill + cycling) (S3) - Accept Tab as an additional cycling key alongside Up/Down (S4) - Remove the unconditional ref reset at the tail of handleInput; correctness is now guaranteed by the buffer-text guard (C1) and the showSuggestions edge-triggered useEffect (C3) (S5) * fix(cli): tighten export completion cycling guard and unify Tab behavior - Phase 2 cycling guard: replace startsWith('/export ') with strict getExportFormatFromInput() to prevent overwriting inputs with extra arguments (e.g. '/export html --verbose'). - ACCEPT_SUGGESTION in Phase 1 popup: add hasExportFormatSuggestions branch so Tab/Enter seeds exportCompletionSelectionIndexRef, allowing Phase 2 cycling to continue from the selected format (consistent with Up/Down arrow behavior). - Add 4 regression tests: Phase 1 Up wrap, Phase 2 Up wrap, Tab seed + Phase 2 Tab cycle, guard prevents overwriting extra args. Ref: PR #3701 second-round review by wenshao * fix(cli): address PR #3701 third-round review feedback on /export completion - S6: use dynamic exportFormatSuggestions.findIndex() for highlight index instead of static EXPORT_FORMAT_COMPLETIONS.indexOf() - S7: derive Phase 2 cycling current index from buffer text via getExportFormatFromInput + indexOf, with defensive ref fallback - S8: extract getNextExportCompletionIndex as module-level pure function; cache exportCycleFormats via useMemo to avoid per-keystroke .map() - S9/S10: add tests for ESC and Ctrl+C reset of export cycling state * fix(cli): tighten /export prefix guard, add superset matching fallthrough, and improve documentation * fix(cli): address review #4224860127 - smaller notes optimization - S1: Strengthen EXPORT_FORMAT_COMPLETIONS fallback comment with an IMPORTANT sync warning for format removals - S2: De-export getExportFormatFromInput (no external consumers) - S3: Add intermediate buffer-clear assertion after Ctrl+U in test to pin state and prevent false positives from future hook changes * refactor(cli): extract export completion into useExportCompletion hook Address all feedback from PR #3701 review comment: - Extract ~310 lines of /export state machine from InputPrompt into dedicated useExportCompletion hook - Replace exportCompletionSelectionIndexRef (number|null) with cyclingActiveRef (boolean) since index was never read - Simplify navigated-flag lifecycle: reset on buffer.text changes instead of popup visibility transitions; add navigatedTextRef snapshot to prevent sticky autocomplete after buffer edits - Remove static EXPORT_FORMAT_COMPLETIONS fallback; derive entirely from slashCommands.subCommands - Aggregate 4 parallel ternaries into single suggestionDisplayProps - Add regression test: navigate + backspace + retype + Enter should submit raw buffer, not autocomplete - Remove redundant navigatedRef reset in ESC handler (already covered by exportCompletion.reset()) * fix(cli): guard export completion state
Co-authored-by: Jacob Richman <jacob314@gmail.com>
* feat(cli): improve export format completion navigation * fix(cli): address PR QwenLM#3701 review feedback on /export completion Critical: - Guard phase-2 cycling by checking buffer text starts with "/export " so a manually edited buffer is never clobbered by stale nav state (C1) - Derive export format suggestions from slashCommands.subCommands to keep a single source of truth with the command registry (C2) - Reset completionSelectionWasNavigatedRef on showSuggestions rising edge instead of on every suggestions change to avoid a race where an already-navigated selection is forgotten before Enter (C3) - Add regression tests for isPerfectMatch + navigated + Enter, including the positive path and a control case (C4) Suggestions: - Prefix-guard getExportFormatFromInput to skip regex on non-/export input (S1) - Drop trailing space from setExportCompletionInput output so buffer text is no longer implicitly coupled to the cycling heuristic (S2) - Document the two-phase state machine (one-shot fill + cycling) (S3) - Accept Tab as an additional cycling key alongside Up/Down (S4) - Remove the unconditional ref reset at the tail of handleInput; correctness is now guaranteed by the buffer-text guard (C1) and the showSuggestions edge-triggered useEffect (C3) (S5) * fix(cli): tighten export completion cycling guard and unify Tab behavior - Phase 2 cycling guard: replace startsWith('/export ') with strict getExportFormatFromInput() to prevent overwriting inputs with extra arguments (e.g. '/export html --verbose'). - ACCEPT_SUGGESTION in Phase 1 popup: add hasExportFormatSuggestions branch so Tab/Enter seeds exportCompletionSelectionIndexRef, allowing Phase 2 cycling to continue from the selected format (consistent with Up/Down arrow behavior). - Add 4 regression tests: Phase 1 Up wrap, Phase 2 Up wrap, Tab seed + Phase 2 Tab cycle, guard prevents overwriting extra args. Ref: PR QwenLM#3701 second-round review by wenshao * fix(cli): address PR QwenLM#3701 third-round review feedback on /export completion - S6: use dynamic exportFormatSuggestions.findIndex() for highlight index instead of static EXPORT_FORMAT_COMPLETIONS.indexOf() - S7: derive Phase 2 cycling current index from buffer text via getExportFormatFromInput + indexOf, with defensive ref fallback - S8: extract getNextExportCompletionIndex as module-level pure function; cache exportCycleFormats via useMemo to avoid per-keystroke .map() - S9/S10: add tests for ESC and Ctrl+C reset of export cycling state * fix(cli): tighten /export prefix guard, add superset matching fallthrough, and improve documentation * fix(cli): address review #4224860127 - smaller notes optimization - S1: Strengthen EXPORT_FORMAT_COMPLETIONS fallback comment with an IMPORTANT sync warning for format removals - S2: De-export getExportFormatFromInput (no external consumers) - S3: Add intermediate buffer-clear assertion after Ctrl+U in test to pin state and prevent false positives from future hook changes * refactor(cli): extract export completion into useExportCompletion hook Address all feedback from PR QwenLM#3701 review comment: - Extract ~310 lines of /export state machine from InputPrompt into dedicated useExportCompletion hook - Replace exportCompletionSelectionIndexRef (number|null) with cyclingActiveRef (boolean) since index was never read - Simplify navigated-flag lifecycle: reset on buffer.text changes instead of popup visibility transitions; add navigatedTextRef snapshot to prevent sticky autocomplete after buffer edits - Remove static EXPORT_FORMAT_COMPLETIONS fallback; derive entirely from slashCommands.subCommands - Aggregate 4 parallel ternaries into single suggestionDisplayProps - Add regression test: navigate + backspace + retype + Enter should submit raw buffer, not autocomplete - Remove redundant navigatedRef reset in ESC handler (already covered by exportCompletion.reset()) * fix(cli): guard export completion state
Summary
/exportcommand completion flow so arrow-key navigation can insert and cycle through export formats (html,md,json,jsonl) directly in the input. TheisPerfectMatch + navigated + Enterautocomplete path now applies to all slash commands with sub-commands (e.g.,/memory,/agents), not just/export./exportcompletion behavior, especially that pressing Enter on plain/exportstill preserves the existing default behavior when the user has not navigated suggestions. Also verify that/memory+ Down + Enter autocompletes the selected sub-command.Validation
/export/export/exportshowshtml,md,json, andjsonlsuggestions./export md, then/export jsonon the next Down press (no trailing space)./exportwithout suggestion navigation keeps the existing default behavior.npm run devmatched the expected interactive behavior.InputPrompt.test.tsxpassed:122 passed, 2 skipped.git diff --checkcompleted with no whitespace errors.npm run dev./export./export md./export json.cd packages/cli && npx vitest run src/ui/components/InputPrompt.test.tsx.Scope / Risk
/export-specific completion handling insideInputPrompt, so the main risk is accidentally affecting generic slash-command completion or history navigation.isPerfectMatch + navigated + Enterautocomplete path now fires for any slash command with sub-commands, not just/export. This is intentional: when the user has navigated suggestions via arrow keys, pressing Enter should autocomplete the selected suggestion rather than submit the raw input. A non-/exportregression test covers this path.Testing Matrix
Testing matrix notes:
npm runpath was tested manually withnpm run dev.npxpath was tested withcd packages/cli && npx vitest run src/ui/components/InputPrompt.test.tsx.Linked Issues / Bugs
Closes #3700