@deepseek-harness-tui/dsh-tui 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +7 -5
  2. package/bin/dsh-tui.js +42 -1
  3. package/cordis.patch.yml +10 -0
  4. package/lib/types/commands.js +1 -1
  5. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  6. package/lib/types/components/GoalTodoPanel.js +6 -1
  7. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  8. package/lib/types/components/HistorySearchDialog.js +5 -1
  9. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  10. package/lib/types/components/LoadedContextPanel.js +1 -1
  11. package/lib/types/components/MessageList.d.ts.map +1 -1
  12. package/lib/types/components/MessageList.js +5 -1
  13. package/lib/types/components/PromptInput.d.ts.map +1 -1
  14. package/lib/types/components/PromptInput.js +52 -74
  15. package/lib/types/components/SearchBox.d.ts +4 -0
  16. package/lib/types/components/SearchBox.d.ts.map +1 -1
  17. package/lib/types/components/SearchBox.js +101 -2
  18. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +4 -1
  19. package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts.map +1 -1
  20. package/lib/types/components/Spinner/SpinnerAnimationRow.js +10 -5
  21. package/lib/types/components/WorkingSpinner.d.ts +3 -1
  22. package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
  23. package/lib/types/components/WorkingSpinner.js +2 -2
  24. package/lib/types/components/design-system/ThemedText.d.ts +9 -3
  25. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  26. package/lib/types/components/design-system/ThemedText.js +3 -5
  27. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  28. package/lib/types/components/questions/AskUserQuestionPanel.js +52 -8
  29. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  30. package/lib/types/components/questions/PlanReviewPanel.js +12 -3
  31. package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
  32. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
  33. package/lib/types/components/sessions/SessionListRow.js +35 -0
  34. package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
  35. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
  36. package/lib/types/components/sessions/SessionPreview.js +60 -0
  37. package/lib/types/dsh-adapter/channel.d.ts +19 -4
  38. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  39. package/lib/types/dsh-adapter/channel.js +76 -82
  40. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  41. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  42. package/lib/types/dsh-adapter/compat/index.js +1 -1
  43. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
  44. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  45. package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
  46. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  47. package/lib/types/dsh-adapter/index.js +10 -1
  48. package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
  49. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
  50. package/lib/types/dsh-adapter/packaged-presets.js +93 -0
  51. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  52. package/lib/types/dsh-adapter/plugin.js +53 -7
  53. package/lib/types/dsh-adapter/presets.d.ts +16 -0
  54. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  55. package/lib/types/dsh-adapter/presets.js +23 -0
  56. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  57. package/lib/types/dsh-adapter/providerWizard.js +3 -1
  58. package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
  59. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
  60. package/lib/types/dsh-adapter/sessions/digest.js +224 -0
  61. package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
  62. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
  63. package/lib/types/dsh-adapter/sessions/frames.js +246 -0
  64. package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
  65. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
  66. package/lib/types/dsh-adapter/sessions/header.js +62 -0
  67. package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
  68. package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
  69. package/lib/types/dsh-adapter/sessions/index.js +15 -0
  70. package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
  71. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
  72. package/lib/types/dsh-adapter/sessions/list.js +213 -0
  73. package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
  74. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
  75. package/lib/types/dsh-adapter/sessions/store.js +164 -0
  76. package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
  77. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
  78. package/lib/types/dsh-adapter/sessions/types.js +14 -0
  79. package/lib/types/i18n.d.ts +150 -22
  80. package/lib/types/i18n.d.ts.map +1 -1
  81. package/lib/types/i18n.js +45 -9
  82. package/lib/types/ink/components/Text.d.ts +9 -1
  83. package/lib/types/ink/components/Text.d.ts.map +1 -1
  84. package/lib/types/ink/components/Text.js +7 -6
  85. package/lib/types/ink/ink.d.ts.map +1 -1
  86. package/lib/types/ink/ink.js +4 -2
  87. package/lib/types/ink/log-update.d.ts.map +1 -1
  88. package/lib/types/ink/log-update.js +76 -9
  89. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  90. package/lib/types/ink/parse-keypress.js +4 -1
  91. package/lib/types/ink/terminal.d.ts +40 -4
  92. package/lib/types/ink/terminal.d.ts.map +1 -1
  93. package/lib/types/ink/terminal.js +57 -5
  94. package/lib/types/ink/termio/tokenize.d.ts +6 -0
  95. package/lib/types/ink/termio/tokenize.d.ts.map +1 -1
  96. package/lib/types/ink/termio/tokenize.js +18 -4
  97. package/lib/types/screens/Chat.d.ts +3 -2
  98. package/lib/types/screens/Chat.d.ts.map +1 -1
  99. package/lib/types/screens/Chat.js +104 -160
  100. package/lib/types/screens/SessionBrowser.d.ts +37 -0
  101. package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
  102. package/lib/types/screens/SessionBrowser.js +452 -0
  103. package/lib/types/sessionHistory.d.ts +0 -8
  104. package/lib/types/sessionHistory.d.ts.map +1 -1
  105. package/lib/types/sessions/format.d.ts +117 -0
  106. package/lib/types/sessions/format.d.ts.map +1 -0
  107. package/lib/types/sessions/format.js +244 -0
  108. package/lib/types/sessions/view.d.ts +125 -0
  109. package/lib/types/sessions/view.d.ts.map +1 -0
  110. package/lib/types/sessions/view.js +234 -0
  111. package/lib/types/update.d.ts +21 -7
  112. package/lib/types/update.d.ts.map +1 -1
  113. package/lib/types/update.js +66 -16
  114. package/lib/types/utils/paths.d.ts.map +1 -1
  115. package/lib/types/utils/paths.js +4 -1
  116. package/package.json +37 -9
  117. package/presets/liangshen/.dsh-tui-managed.json +5 -0
  118. package/presets/liangshen/agent.cordis.yml +389 -0
  119. package/presets/liangshen/compaction-epoch.mjs +81 -0
  120. package/presets/liangshen/custom-bash.mjs +126 -0
  121. package/presets/liangshen/instruction-hint.mjs +181 -0
  122. package/presets/liangshen/preset.yml +3 -0
  123. package/presets/liangshen/skill-search.mjs +142 -0
  124. package/presets/liangshen/tool-bootstrap.mjs +300 -0
  125. package/lib/types/components/ResumePicker.d.ts +0 -26
  126. package/lib/types/components/ResumePicker.d.ts.map +0 -1
  127. package/lib/types/components/ResumePicker.js +0 -50
package/README.md CHANGED
@@ -77,7 +77,8 @@ sh install.sh
77
77
  `dsh-tui --resume` 恢复上次会话;Windows 也可用仓库里的 `dsh-tui.cmd`(等价)。
78
78
 
79
79
  在 VS Code 中运行的完整指南(内置终端直接使用 + companion 扩展
80
- `dsh-tui-vscode` 一键启动/恢复与编辑器集成)见
80
+ `dsh-tui-vscode`——**真实集成终端承载、体验与 Claude Code 官方扩展几乎一致、
81
+ 已上架 VS Code Marketplace**)见
81
82
  [在 VS Code 中运行 dsh-TUI](docs/vscode.md)。
82
83
 
83
84
  TUI 启动后会在后台检查 npm 是否有新版本;发现更新时会提示,输入 `/update`
@@ -134,7 +135,7 @@ macOS 自带 Terminal.app 会自行消费 `⌘` 快捷键,请继续使用 `Ctr
134
135
 
135
136
  | 分组 | 命令 |
136
137
  |---|---|
137
- | 会话 | `/new` 新会话 · `/resume` 切换当前工作区内的会话 · `/rename` 重命名会话 · `/workspace resume|rename|open` 管理工作区 · `/clear` 清屏 · `/compact` 压缩 · `/export` 导出 Markdown · `/trace` 轨迹场景(亦可 `Ctrl+T`) |
138
+ | 会话 | `/new` 新会话 · `/resume` 会话浏览器(搜索、预览、跨项目、折叠子 agent 运行) · `/rename` 重命名会话 · `/workspace resume|rename|open` 管理工作区 · `/clear` 清屏 · `/compact` 压缩 · `/export` 导出 Markdown · `/trace` 轨迹场景(亦可 `Ctrl+T`) |
138
139
  | 状态 | `/status` 会话信息 · `/cost` token 用量 · `/doctor` 环境自检 · `/config` 配置来源 · `/init` 创建 AGENTS.md |
139
140
  | 模型 | `/model` 选择器 · `/thinking` 思考显示 · `/tokens` token 明细 · `/theme` 主题选择器 · `/lang` 中英界面切换 |
140
141
  | 账号/策略 | `/provider` 添加模型提供方 · `/login` 凭证状态 · `/logout` 登出说明 · `/permissions` 权限说明 · `/add-dir` 文件策略范围 · `/hooks` · `/mcp` · `/memory` |
@@ -151,7 +152,7 @@ macOS 自带 Terminal.app 会自行消费 `⌘` 快捷键,请继续使用 `Ctr
151
152
  | [主题系统](docs/themes.md) | 内置主题、自动检测、自定义 JSON 主题与校验规则 |
152
153
  | [交互与命令](docs/interaction.md) | 快捷键、鼠标、问卷、slash command 与会话工作流 |
153
154
  | [架构与限制](docs/architecture.md) | 运行链路、渲染与持久化设计、安全边界、已知限制 |
154
- | [VS Code 使用指南](docs/vscode.md) | 在 VS Code 集成终端运行 dsh-tui 与 companion 扩展 `dsh-tui-vscode` |
155
+ | [VS Code 使用指南](docs/vscode.md) | 在 VS Code 集成终端运行 dsh-tui;companion 扩展 `dsh-tui-vscode` 提供与 Claude Code 官方扩展几乎一致的体验(已上架 Marketplace) |
155
156
  | [贡献与开发约定](docs/contributing.md) | 贡献流程、仓库地图、构建产物、验证矩阵与修改规则 |
156
157
  | [插件开发指南](docs/plugins.md) | 插件接缝(会话事件 / 槽位 / 技能 / 主题 / prompt 段)、契约、规范与收录 |
157
158
 
@@ -159,7 +160,8 @@ macOS 自带 Terminal.app 会自行消费 `⌘` 快捷键,请继续使用 `Ctr
159
160
 
160
161
  ## 配置与扩展
161
162
 
162
- - **Agent preset**:四种官方 Agent 模式(`standard` / `code` / `minimal` / `cordis`),
163
+ - **Agent preset**:四种官方 Agent 模式(`standard` / `code` / `minimal` / `cordis`)和
164
+ TUI 随包提供的“梁神模式”(`liangshen`),
163
165
  `/preset` 切换;已产生对话的会话不可切换,空白会话立即生效。默认 preset 持久化
164
166
  在 `~/.dsh-tui/agent-preset.json`;`/model` 的选择持久化在 `~/.dsh-tui/model.json`。
165
167
  详见[配置参考](docs/configuration.md#agent-preset)。
@@ -205,7 +207,7 @@ compaction 和持久化继续由 DSH 服务拥有。更详细的模块边界与
205
207
  - **TPS 仪表**:参考 pi-tps-meter——流式 1/8 格 gauge、历史 min-max sparkline、
206
208
  速度语义色(≥50 绿 / ≥20 黄 / <20 红)。
207
209
  - **working-activity 生态**:工作状态行复用
208
- [dsh-working-activity](https://github.com/ccch1mneyyy/dsh-working-activity)
210
+ [dsh-working-activity](https://github.com/ccch1mneyyy/working-activity)
209
211
  的纯状态机,在进程内从基础会话事件派生,不向共享日志写入 UI 状态。
210
212
  - **终端粘贴**:raw 模式下 Ctrl+V 由应用接管,按平台读取系统剪贴板——Windows
211
213
  走 PowerShell `Get-Clipboard`,macOS 走 `osascript`/`pbpaste`,Linux 自动探测
package/bin/dsh-tui.js CHANGED
@@ -9,7 +9,11 @@
9
9
  * 2. 检测 $DSH_HOME/profiles/dsh-tui 是否已初始化,未初始化则自动执行
10
10
  * `dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui@<本包版本>`
11
11
  * 自举——版本号与本包对齐,避免 pnpm store 缓存带来的旧版漂移;
12
- * 3. 已初始化但版本与本包不一致时打印一行提示(TUI 内 /update 或重新 add);
12
+ * 3. 已初始化但版本与本包不一致时按方向处理(issue #183):profile
13
+ * 更新(前向错位)打印一行提示后继续启动(TUI 内 /update 或重新
14
+ * add);profile 次版本更旧(反向错位)拒绝启动并给出对齐命令——
15
+ * 该方向 dsh CLI 会把启动器的 bundle patch 套到 profile 旧包上,
16
+ * 启动必然以模块解析错误崩溃;
13
17
  * 4. 透传全部参数启动 `dsh --profile dsh-tui`。
14
18
  *
15
19
  * `--resume` 由本启动器拦截:读取 TUI 保留的 ~/.dsh-tui/resume.txt
@@ -65,10 +69,37 @@ const MSG = {
65
69
  ` 更新 profile:在 TUI 内执行 /update,或运行:\n` +
66
70
  ` dsh plugin --profile ${PROFILE} add ${PACKAGE}@latest`,
67
71
  },
72
+ // Reverse skew (issue #183): the dsh CLI reads the bundle patch from the
73
+ // FIRST copy found from its own install anchor — this globally installed
74
+ // launcher — while the plugin modules load from the profile's copy. A
75
+ // launcher minor NEWER than the profile means the patch may reference
76
+ // subpath exports the profile's older package does not have, and boot
77
+ // crashes opaquely (ERR_PACKAGE_PATH_NOT_EXPORTED) before any TUI code
78
+ // runs. Fail loud with the fix instead. (Forward skew degrades to a
79
+ // working local-workspace fallback since 0.7.2 — soft note below.)
80
+ profileOlderThanLauncher: {
81
+ en: (installed, own) =>
82
+ `[dsh-tui] cannot start: the profile runs v${installed} but this launcher is v${own}.\n` +
83
+ ` The launcher's bundle patch would be applied to the profile's older package,\n` +
84
+ ` which does not export everything the patch references — boot would crash.\n` +
85
+ ` Align the profile with the launcher:\n` +
86
+ ` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}\n` +
87
+ ` (or update everything to the latest release: dsh plugin --profile ${PROFILE} add ${PACKAGE}@latest)`,
88
+ zh: (installed, own) =>
89
+ `[dsh-tui] 无法启动:profile 内运行的是 v${installed},而启动器是 v${own}。\n` +
90
+ ` 启动器的 bundle patch 会套用到 profile 里的旧版包上,其中缺少 patch 引用\n` +
91
+ ` 的子路径导出——启动会以模块解析错误崩溃。请让 profile 与启动器对齐:\n` +
92
+ ` dsh plugin --profile ${PROFILE} add ${PACKAGE}@${own}\n` +
93
+ ` (或全部升到最新:dsh plugin --profile ${PROFILE} add ${PACKAGE}@latest)`,
94
+ },
68
95
  launchFailed: {
69
96
  en: err => `[dsh-tui] Failed to launch: ${err.message}`,
70
97
  zh: err => `[dsh-tui] 启动失败:${err.message}`,
71
98
  },
99
+ profileExited: {
100
+ en: code => `[dsh-tui] dsh profile exited with code ${code}. Run it directly for diagnostics:\n dsh --profile ${PROFILE}`,
101
+ zh: code => `[dsh-tui] dsh profile 已退出(退出码 ${code})。可直接运行以下命令查看诊断:\n dsh --profile ${PROFILE}`,
102
+ },
72
103
  legacyEnv: {
73
104
  en: (oldName, newName) => `[dsh-tui] note: env ${oldName} was renamed to ${newName}; the old name no longer takes effect.`,
74
105
  zh: (oldName, newName) => `[dsh-tui] 提示:环境变量 ${oldName} 已更名为 ${newName},旧名不再生效。`,
@@ -126,6 +157,15 @@ if (installedVersion === undefined) {
126
157
  process.exit(add.status ?? 1)
127
158
  }
128
159
  } else if (installedVersion !== ownVersion) {
160
+ // Reverse skew is fatal (see MSG.profileOlderThanLauncher): compare
161
+ // major/minor only — patch-level differences never move the patch surface.
162
+ const majorMinor = v => v.split('-')[0].split('.').slice(0, 2).map(Number)
163
+ const [installedMajor, installedMinor] = majorMinor(installedVersion)
164
+ const [ownMajor, ownMinor] = majorMinor(ownVersion)
165
+ if (installedMajor < ownMajor || (installedMajor === ownMajor && installedMinor < ownMinor)) {
166
+ console.error(MSG.profileOlderThanLauncher[lang](installedVersion, ownVersion))
167
+ process.exit(1)
168
+ }
129
169
  console.error(MSG.versionMismatch[lang](installedVersion, ownVersion))
130
170
  }
131
171
 
@@ -197,6 +237,7 @@ child.on('exit', (code, signal) => {
197
237
  if (signal) {
198
238
  process.kill(process.pid, signal)
199
239
  } else {
240
+ if (code !== null && code !== 0) console.error(MSG.profileExited[lang](code))
200
241
  process.exit(code ?? 0)
201
242
  }
202
243
  })
package/cordis.patch.yml CHANGED
@@ -218,6 +218,16 @@
218
218
  # The entry-level dependency augments the plugin's `agents` inject and
219
219
  # prevents startup Session creation from racing WorkspaceRegistry init.
220
220
  inject: [workspaceRegistry, agents, tuiWorkspaces]
221
+ # `tuiWorkspaces` lives in the entry-level inject ONLY as an ordering
222
+ # guarantee alongside the dsh-tui-workspaces row above — it is
223
+ # deliberately absent from the plugin's code-level inject (issue
224
+ # #183): the dsh CLI may read this patch from an older copy of the
225
+ # package than the plugin module it loads, and a hard code-level
226
+ # inject then deadlocks the tree at boot ("pending (waiting for
227
+ # service: tuiWorkspaces)"). Without the row the TUI degrades to a
228
+ # local-only workspace runtime instead. (The comment sits below the
229
+ # inject line: verify-workspace-attachment.mjs asserts the
230
+ # name→inject distance.)
221
231
  config:
222
232
  provider: deepseek-official
223
233
  # Alt-screen fullscreen(CC 同款):应用内鼠标选区(选择即复制, OSC 52 +
@@ -31,7 +31,7 @@ export const LOCAL_COMMANDS = [
31
31
  { name: 'agents', description: 'Show subagents of this session' },
32
32
  // Model / display
33
33
  { name: 'activity', description: 'Switch the working-activity indicator preset' },
34
- { name: 'preset', description: 'Switch the agent preset (standard/code/minimal/cordis)' },
34
+ { name: 'preset', description: 'Switch the agent preset (including Liangshen mode)' },
35
35
  { name: 'theme', description: 'Switch the color theme (auto, built-in or custom)' },
36
36
  { name: 'lang', description: 'Switch the UI language (en / zh)' },
37
37
  { name: 'model', description: 'Show the active model' },
@@ -1 +1 @@
1
- {"version":3,"file":"GoalTodoPanel.d.ts","sourceRoot":"","sources":["../../../src/components/GoalTodoPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,OAAO,EAA8B,MAAM,2BAA2B,CAAA;AAwDpF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,SAAS,CA2DhF"}
1
+ {"version":3,"file":"GoalTodoPanel.d.ts","sourceRoot":"","sources":["../../../src/components/GoalTodoPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,OAAO,EAA8B,MAAM,2BAA2B,CAAA;AAwDpF;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG,KAAK,CAAC,SAAS,CAgEhF"}
@@ -45,7 +45,12 @@ function BranchPrefix({ last }) {
45
45
  */
46
46
  export function GoalTodoPanel({ channel }) {
47
47
  const goal = channel.goal;
48
- const todos = channel.todos ?? [];
48
+ const allTodos = channel.todos ?? [];
49
+ // Completed rows are useful progress while a turn is running, but become
50
+ // stale footer noise once the agent is idle. Keep unfinished work visible.
51
+ const todos = channel.working
52
+ ? allTodos
53
+ : allTodos.filter(todo => todo.status !== 'completed');
49
54
  if (goal === undefined && todos.length === 0)
50
55
  return null;
51
56
  const visible = todos.slice(0, MAX_TODOS);
@@ -1 +1 @@
1
- {"version":3,"file":"HistorySearchDialog.d.ts","sourceRoot":"","sources":["../../../src/components/HistorySearchDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,CAmDlB"}
1
+ {"version":3,"file":"HistorySearchDialog.d.ts","sourceRoot":"","sources":["../../../src/components/HistorySearchDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AAEjE;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,YAAY,EACZ,OAAO,EACP,UAAU,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,CAAC,SAAS,CAuDlB"}
@@ -26,7 +26,11 @@ export function HistorySearchDialog({ query, cursorOffset, matches, focusIndex,
26
26
  const { start, end } = listWindow(matches.map(() => 2), focusIndex, Math.max(terminalRows - 18, 2), 1);
27
27
  return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, color: "permission", children: t('history-search-title') }), _jsx(SearchBox, { query: query, cursorOffset: cursorOffset, isFocused: true, isTerminalFocused: isTerminalFocused, placeholder: t('history-search-placeholder') }), matches.length === 0 ? (_jsx(Text, { dimColor: true, children: t('history-search-empty') })) : (matches.slice(start, end).map((entry, index) => {
28
28
  const absoluteIndex = start + index;
29
- return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex, description: formatRelativeAge(entry.ts), showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < matches.length, children: entry.text }, historyEntryId(entry)));
29
+ return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex,
30
+ // The SearchBox owns the native-cursor declaration while this
31
+ // dialog is open — result rows must not park the cursor on
32
+ // themselves, or IME preedit lands on a list row.
33
+ declareCursor: false, description: formatRelativeAge(entry.ts), showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < matches.length, children: entry.text }, historyEntryId(entry)));
30
34
  })), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-history-search') }) })] }) }));
31
35
  }
32
36
  /** Relative age like CC's formatRelativeTimeAgo ("now" / "5m ago" / …), localized. */
@@ -1 +1 @@
1
- {"version":3,"file":"LoadedContextPanel.d.ts","sourceRoot":"","sources":["../../../src/components/LoadedContextPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,2BAA2B,CAAA;AA+BlF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,aAAa,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CAkElB"}
1
+ {"version":3,"file":"LoadedContextPanel.d.ts","sourceRoot":"","sources":["../../../src/components/LoadedContextPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,2BAA2B,CAAA;AA+BlF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,aAAa,CAAA;IACtB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CAoElB"}
@@ -26,5 +26,5 @@ export function LoadedContextPanel({ context, open, onToggle, }) {
26
26
  const summary = summarizeLoadedContext(context);
27
27
  if (summary === '')
28
28
  return null;
29
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsxs(Box, { paddingX: 1, backgroundColor: open ? 'userMessageBackground' : undefined, children: [_jsxs(Text, { bold: open, children: [open ? '▼' : '▶', " ", t('context-loaded'), " \u00B7 ", summary] }), _jsxs(Text, { dimColor: true, children: [" \uFF08Ctrl+T", open ? t('context-panel-collapse') : t('context-panel-expand'), "\uFF09"] })] }), open && (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [context.sections.length > 0 && (_jsx(Group, { title: t('context-panel-sections', { n: context.sections.length }), children: context.sections.map(section => (_jsx(Entry, { entry: section }, section.name))) })), context.files.length > 0 && (_jsx(Group, { title: t('context-panel-files', { n: context.files.length }), children: context.files.map(file => (_jsx(Text, { dimColor: true, children: file.displayPath }, file.displayPath))) })), context.contexts.length > 0 && (_jsx(Group, { title: t('context-panel-runtime', { n: context.contexts.length }), children: context.contexts.map(entry => (_jsx(Entry, { entry: entry }, entry.name))) })), context.skills.length > 0 && (_jsx(Group, { title: t('context-panel-skills', { n: context.skills.length }), children: context.skills.map(skill => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: skill.name }), _jsx(Text, { dimColor: true, wrap: "wrap", children: skill.description })] }, skill.name))) })), context.tools.length > 0 && (_jsx(Group, { title: t('context-panel-tools', { n: context.tools.length }), children: context.tools.map(tool => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: tool.name }), _jsx(Text, { dimColor: true, wrap: "wrap", children: truncateContextText(tool.description, 160) })] }, tool.name))) }))] }))] }));
29
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Box, { paddingX: 1, backgroundColor: open ? 'userMessageBackground' : undefined, children: _jsxs(Text, { wrap: "truncate", bold: open, children: [open ? '▼' : '▶', " ", t('context-loaded'), " \u00B7 ", summary, _jsxs(Text, { dimColor: true, children: [" \uFF08Ctrl+T", open ? t('context-panel-collapse') : t('context-panel-expand'), "\uFF09"] })] }) }), open && (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: [context.sections.length > 0 && (_jsx(Group, { title: t('context-panel-sections', { n: context.sections.length }), children: context.sections.map(section => (_jsx(Entry, { entry: section }, section.name))) })), context.files.length > 0 && (_jsx(Group, { title: t('context-panel-files', { n: context.files.length }), children: context.files.map(file => (_jsx(Text, { dimColor: true, children: file.displayPath }, file.displayPath))) })), context.contexts.length > 0 && (_jsx(Group, { title: t('context-panel-runtime', { n: context.contexts.length }), children: context.contexts.map(entry => (_jsx(Entry, { entry: entry }, entry.name))) })), context.skills.length > 0 && (_jsx(Group, { title: t('context-panel-skills', { n: context.skills.length }), children: context.skills.map(skill => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: skill.name }), _jsx(Text, { dimColor: true, wrap: "wrap", children: skill.description })] }, skill.name))) })), context.tools.length > 0 && (_jsx(Group, { title: t('context-panel-tools', { n: context.tools.length }), children: context.tools.map(tool => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: tool.name }), _jsx(Text, { dimColor: true, wrap: "wrap", children: truncateContextText(tool.description, 160) })] }, tool.name))) }))] }))] }));
30
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAyC,MAAM,2BAA2B,CAAA;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAuC/C,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,eAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB;2EACuE;IACvE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,oDAAoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;IACrC,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;8EAC0E;IAC1E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,qBAoPA;AAuQD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,MAAM,EACN,GAAG,GACJ,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,KAAK,CAAC,SAAS,CAMlB"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/MessageList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAyC,MAAM,2BAA2B,CAAA;AAC/F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAuC/C,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,WAAW,EACX,eAAsB,EACtB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB;2EACuE;IACvE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,IAAI,KAAK,IAAI,CAAA;IAC/D,oDAAoD;IACpD,YAAY,CAAC,EAAE,eAAe,GAAG,IAAI,CAAA;IACrC,4EAA4E;IAC5E,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B;8EAC0E;IAC1E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,qBAwPA;AAuQD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,MAAM,EACN,GAAG,GACJ,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,KAAK,CAAC,SAAS,CAMlB"}
@@ -158,8 +158,12 @@ export function MessageList({ rows, expanded, expandedRows, selectedId, onToggle
158
158
  }
159
159
  }
160
160
  }
161
+ const lastUnseenReportRef = React.useRef(-1);
161
162
  React.useEffect(() => {
162
- onUnseenCount?.(unseenCount);
163
+ if (unseenCount !== lastUnseenReportRef.current) {
164
+ lastUnseenReportRef.current = unseenCount;
165
+ onUnseenCount?.(unseenCount);
166
+ }
163
167
  });
164
168
  // Post-commit: measure mounted rows, derive the content-space base from
165
169
  // the first mounted row's Yoga top, and clamp render-time scrollTop to the
@@ -1 +1 @@
1
- {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAkDxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAq6BlB"}
1
+ {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAkDxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAg5BlB"}
@@ -103,6 +103,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
103
103
  controllerRef.current = {
104
104
  hasText: () => value.length > 0,
105
105
  clear: () => {
106
+ valueRef.current = '';
107
+ cursorRef.current = 0;
106
108
  setValue('');
107
109
  setCursor(0);
108
110
  },
@@ -120,6 +122,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
120
122
  React.useEffect(() => {
121
123
  if (fillText && fillText !== lastFill.current) {
122
124
  lastFill.current = fillText;
125
+ valueRef.current = fillText;
126
+ cursorRef.current = fillText.length;
123
127
  setValue(fillText);
124
128
  setCursor(fillText.length);
125
129
  onFillConsumed?.();
@@ -132,16 +136,6 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
132
136
  const clipboardBusyRef = React.useRef(false);
133
137
  /** True while the external editor owns the terminal (Ctrl+X round-trip). */
134
138
  const editorBusyRef = React.useRef(false);
135
- /**
136
- * Latest committed input state. The Ctrl+V clipboard read resolves
137
- * asynchronously; inserting against the render that STARTED the read
138
- * would drop whatever the user typed while waiting. The continuation
139
- * inserts against this mirror instead.
140
- */
141
- const liveInputRef = React.useRef({ value: '', cursor: 0 });
142
- React.useEffect(() => {
143
- liveInputRef.current = { value, cursor };
144
- });
145
139
  /** Enter dedupe window: cmd pipelines can deliver one Enter as `\r`+`\n`. */
146
140
  const lastEnterAtRef = React.useRef(0);
147
141
  React.useEffect(() => {
@@ -193,6 +187,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
193
187
  !helpOpen &&
194
188
  !selectionActive &&
195
189
  fileEscRef.current !== mention?.start;
190
+ const setInput = (next, cursorOffset = next.length) => {
191
+ valueRef.current = next;
192
+ cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
193
+ setValue(next);
194
+ setCursor(cursorRef.current);
195
+ };
196
196
  /**
197
197
  * Accept the selected file suggestion: replace ONLY the mention token at
198
198
  * the caret (prefix/suffix text survives), quoting whitespace paths. A
@@ -216,8 +216,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
216
216
  if (history.current.length > HISTORY_LIMIT)
217
217
  history.current.shift();
218
218
  historyIndex.current = -1;
219
- setValue('');
220
- setCursor(0);
219
+ setInput('', 0);
221
220
  setSelectedCommand(0);
222
221
  appendHistory(trimmed);
223
222
  channel.submit(trimmed);
@@ -243,8 +242,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
243
242
  if (history.current.length > HISTORY_LIMIT)
244
243
  history.current.shift();
245
244
  historyIndex.current = -1;
246
- setValue('');
247
- setCursor(0);
245
+ setInput('', 0);
248
246
  setSelectedCommand(0);
249
247
  appendHistory(trimmed);
250
248
  channel.steer(trimmed);
@@ -262,8 +260,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
262
260
  if (history.current.length > HISTORY_LIMIT)
263
261
  history.current.shift();
264
262
  historyIndex.current = -1;
265
- setValue('');
266
- setCursor(0);
263
+ setInput('', 0);
267
264
  setSelectedCommand(0);
268
265
  appendHistory(trimmed);
269
266
  channel.submit(trimmed);
@@ -282,8 +279,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
282
279
  channel.notify(t('input-cannot-retract'), { color: 'warning', timeoutMs: 2500 });
283
280
  return;
284
281
  }
285
- setValue(item.text);
286
- setCursor(item.text.length);
282
+ setInput(item.text);
287
283
  setSelectedCommand(0);
288
284
  setFileSelected(0);
289
285
  channel.notify(t('input-retracted'), { timeoutMs: 2000 });
@@ -308,8 +304,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
308
304
  if (history.current.length > HISTORY_LIMIT)
309
305
  history.current.shift();
310
306
  historyIndex.current = -1;
311
- setValue('');
312
- setCursor(0);
307
+ setInput('', 0);
313
308
  setSelectedCommand(0);
314
309
  setFileSelected(0);
315
310
  appendHistory(trimmed);
@@ -336,19 +331,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
336
331
  if (history.current.length > HISTORY_LIMIT)
337
332
  history.current.shift();
338
333
  historyIndex.current = -1;
339
- setValue('');
340
- setCursor(0);
334
+ setInput('', 0);
341
335
  setSelectedCommand(0);
342
336
  appendHistory(text.trim());
343
337
  }
344
338
  return handled;
345
339
  };
346
- const setInput = (next, cursorOffset = next.length) => {
347
- valueRef.current = next;
348
- cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
349
- setValue(next);
350
- setCursor(cursorRef.current);
351
- };
352
340
  /** Clipboard reads are asynchronous; insert against the latest render so
353
341
  * typing while PowerShell owns the clipboard never gets overwritten. */
354
342
  const insertClipboardAtCaret = (text) => {
@@ -359,13 +347,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
359
347
  setFileSelected(0);
360
348
  };
361
349
  /** Line index of the cursor; -1 when the cursor is at the very end. */
362
- const cursorLine = () => {
363
- const before = value.slice(0, cursor);
350
+ const cursorLine = (text, cursorOffset) => {
351
+ const before = text.slice(0, cursorOffset);
364
352
  return before.split('\n').length - 1;
365
353
  };
366
354
  /** Column of the cursor within its line. */
367
- const cursorColumn = () => {
368
- const before = value.slice(0, cursor);
355
+ const cursorColumn = (text, cursorOffset) => {
356
+ const before = text.slice(0, cursorOffset);
369
357
  const line = before.split('\n').pop() ?? '';
370
358
  return line.length;
371
359
  };
@@ -377,13 +365,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
377
365
  // setValue can never overwrite fresh typing (and vice versa).
378
366
  if (editorBusyRef.current)
379
367
  return;
368
+ // App deliberately dispatches every parsed key from one stdin read in a
369
+ // single React update. Read the synchronous mirrors so each event sees
370
+ // the text/caret produced by the preceding event in that batch.
371
+ const value = valueRef.current;
372
+ const cursor = cursorRef.current;
380
373
  /** Insert text at the caret (typing, paste) and dismiss overlays. */
381
374
  const insertAtCaret = (text) => {
382
375
  if (helpOpen)
383
376
  onToggleHelp();
384
377
  const next = value.slice(0, cursor) + text + value.slice(cursor);
385
- setValue(next);
386
- setCursor(cursor + text.length);
378
+ setInput(next, cursor + text.length);
387
379
  setSelectedCommand(0);
388
380
  setFileSelected(0);
389
381
  };
@@ -441,10 +433,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
441
433
  // Insert against the LIVE input state: the read above resolved
442
434
  // asynchronously and the user may have typed while waiting.
443
435
  const text = formatClipboardInsert(content);
444
- const live = liveInputRef.current;
445
- const caret = Math.min(live.cursor, live.value.length);
446
- setValue(live.value.slice(0, caret) + text + live.value.slice(caret));
447
- setCursor(caret + text.length);
436
+ insertClipboardAtCaret(text);
448
437
  })
449
438
  .catch(() => {
450
439
  channel.notify(t('input-clipboard-read-failed'), { color: 'warning' });
@@ -469,8 +458,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
469
458
  try {
470
459
  const outcome = await editInExternalEditor(value);
471
460
  if (outcome.kind === 'edited') {
472
- setValue(outcome.text);
473
- setCursor(outcome.text.length);
461
+ setInput(outcome.text);
474
462
  setSelectedCommand(0);
475
463
  setFileSelected(0);
476
464
  }
@@ -575,8 +563,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
575
563
  // ink/terminal.ts); Option+Enter (ESC CR) is the fallback on terminals
576
564
  // that can't report shift — e.g. macOS Terminal.app (issue #110).
577
565
  const next = value.slice(0, cursor) + '\n' + value.slice(cursor);
578
- setValue(next);
579
- setCursor(cursor + 1);
566
+ setInput(next, cursor + 1);
580
567
  setSelectedCommand(0);
581
568
  return;
582
569
  }
@@ -620,13 +607,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
620
607
  setFileSelected(index => index <= 0 ? fileMatches.length - 1 : index - 1);
621
608
  return;
622
609
  }
623
- const line = cursorLine();
610
+ const line = cursorLine(value, cursor);
624
611
  if (line > 0) {
625
612
  // Move to the previous line, clamping to its length.
626
613
  const upToLineStart = value.lastIndexOf('\n', cursor - 1);
627
614
  const prevLineStart = upToLineStart === -1 ? 0 : value.lastIndexOf('\n', upToLineStart - 1) + 1;
628
615
  const prevLine = value.slice(prevLineStart, upToLineStart);
629
- setCursor(prevLineStart + Math.min(cursorColumn(), prevLine.length));
616
+ setInput(value, prevLineStart + Math.min(cursorColumn(value, cursor), prevLine.length));
630
617
  return;
631
618
  }
632
619
  if (overlayOpen) {
@@ -639,8 +626,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
639
626
  ? history.current.length - 1
640
627
  : Math.max(0, historyIndex.current - 1);
641
628
  const entry = history.current[historyIndex.current] ?? '';
642
- setValue(entry);
643
- setCursor(entry.length);
629
+ setInput(entry);
644
630
  return;
645
631
  }
646
632
  if (key.downArrow) {
@@ -648,13 +634,13 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
648
634
  setFileSelected(index => index >= fileMatches.length - 1 ? 0 : index + 1);
649
635
  return;
650
636
  }
651
- const line = cursorLine();
637
+ const line = cursorLine(value, cursor);
652
638
  const lines = value.split('\n');
653
639
  if (line < lines.length - 1) {
654
640
  const nextLineStart = value.indexOf('\n', cursor) + 1;
655
641
  const nextLineEnd = value.indexOf('\n', nextLineStart);
656
642
  const nextLine = value.slice(nextLineStart, nextLineEnd === -1 ? value.length : nextLineEnd);
657
- setCursor(nextLineStart + Math.min(cursorColumn(), nextLine.length));
643
+ setInput(value, nextLineStart + Math.min(cursorColumn(value, cursor), nextLine.length));
658
644
  return;
659
645
  }
660
646
  if (overlayOpen) {
@@ -667,76 +653,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
667
653
  const entry = historyIndex.current >= history.current.length
668
654
  ? ''
669
655
  : (history.current[historyIndex.current] ?? '');
670
- setValue(entry);
671
- setCursor(entry.length);
656
+ setInput(entry);
672
657
  return;
673
658
  }
674
659
  if (isMod(key) && key.leftArrow) {
675
660
  // Jump to the previous word boundary (readline alt+b). Must precede the
676
661
  // bare-arrow arms: Ctrl+Left arrives as leftArrow + ctrl.
677
- setCursor(previous => wordBoundaryLeft(value, previous));
662
+ setInput(value, wordBoundaryLeft(value, cursor));
678
663
  return;
679
664
  }
680
665
  if (isMod(key) && key.rightArrow) {
681
666
  // Jump to the next word boundary (readline alt+f).
682
- setCursor(previous => wordBoundaryRight(value, previous));
667
+ setInput(value, wordBoundaryRight(value, cursor));
683
668
  return;
684
669
  }
685
670
  if (key.leftArrow) {
686
- setCursor(previous => Math.max(0, previous - 1));
671
+ setInput(value, Math.max(0, cursor - 1));
687
672
  return;
688
673
  }
689
674
  if (key.rightArrow) {
690
- setCursor(previous => Math.min(value.length, previous + 1));
675
+ setInput(value, Math.min(value.length, cursor + 1));
691
676
  return;
692
677
  }
693
678
  if (key.backspace) {
694
679
  if (cursor === 0)
695
680
  return;
696
- setValue(value.slice(0, cursor - 1) + value.slice(cursor));
697
- setCursor(cursor - 1);
681
+ setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
698
682
  return;
699
683
  }
700
684
  if (key.delete) {
701
685
  if (cursor >= value.length)
702
686
  return;
703
- setValue(value.slice(0, cursor) + value.slice(cursor + 1));
687
+ setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
704
688
  return;
705
689
  }
706
690
  if (key.home) {
707
691
  // Start of the current line.
708
692
  const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
709
- setCursor(lineStart);
693
+ setInput(value, lineStart);
710
694
  return;
711
695
  }
712
696
  if (key.end) {
713
697
  // End of the current line.
714
698
  const nextLine = value.indexOf('\n', cursor);
715
- setCursor(nextLine === -1 ? value.length : nextLine);
699
+ setInput(value, nextLine === -1 ? value.length : nextLine);
716
700
  return;
717
701
  }
718
702
  if (isMod(key) && input === 'a') {
719
703
  const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
720
- setCursor(lineStart);
704
+ setInput(value, lineStart);
721
705
  return;
722
706
  }
723
707
  if (isMod(key) && input === 'e') {
724
708
  const nextLine = value.indexOf('\n', cursor);
725
- setCursor(nextLine === -1 ? value.length : nextLine);
709
+ setInput(value, nextLine === -1 ? value.length : nextLine);
726
710
  return;
727
711
  }
728
712
  if (isMod(key) && input === 'u') {
729
713
  // Delete to start of line.
730
714
  const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
731
- setValue(value.slice(0, lineStart) + value.slice(cursor));
732
- setCursor(lineStart);
715
+ setInput(value.slice(0, lineStart) + value.slice(cursor), lineStart);
733
716
  return;
734
717
  }
735
718
  if (isMod(key) && input === 'k') {
736
719
  // Delete to end of line.
737
720
  const nextLine = value.indexOf('\n', cursor);
738
721
  const end = nextLine === -1 ? value.length : nextLine;
739
- setValue(value.slice(0, cursor) + value.slice(end));
722
+ setInput(value.slice(0, cursor) + value.slice(end), cursor);
740
723
  return;
741
724
  }
742
725
  if (isMod(key) && input === 'w') {
@@ -749,8 +732,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
749
732
  let start = end;
750
733
  while (start > 0 && !/\s/.test(before[start - 1]))
751
734
  start--;
752
- setValue(value.slice(0, start) + value.slice(cursor));
753
- setCursor(start);
735
+ setInput(value.slice(0, start) + value.slice(cursor), start);
754
736
  return;
755
737
  }
756
738
  if (key.escape) {
@@ -761,8 +743,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
761
743
  // A single Esc closes the open command menu first (CC/pi behavior);
762
744
  // the double-tap-clear semantics only apply to ordinary input.
763
745
  if (overlayOpen) {
764
- setValue('');
765
- setCursor(0);
746
+ setInput('', 0);
766
747
  setSelectedCommand(0);
767
748
  setFileSelected(0);
768
749
  return;
@@ -786,8 +767,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
786
767
  // A single Esc clears the current input (if any); the double-tap
787
768
  // path below handles rewind/clear on an already-empty input.
788
769
  if (value.length > 0) {
789
- setValue('');
790
- setCursor(0);
770
+ setInput('', 0);
791
771
  setSelectedCommand(0);
792
772
  setFileSelected(0);
793
773
  return;
@@ -803,8 +783,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
803
783
  onRewindRequest?.();
804
784
  }
805
785
  else {
806
- setValue('');
807
- setCursor(0);
786
+ setInput('', 0);
808
787
  }
809
788
  return;
810
789
  }
@@ -826,8 +805,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
826
805
  if (helpOpen)
827
806
  onToggleHelp();
828
807
  const next = value.slice(0, cursor) + input + value.slice(cursor);
829
- setValue(next);
830
- setCursor(cursor + input.length);
808
+ setInput(next, cursor + input.length);
831
809
  setSelectedCommand(0);
832
810
  setFileSelected(0);
833
811
  }
@@ -6,6 +6,10 @@ import React from 'react';
6
6
  * placeholder is right-aligned (dimmed) — kept off the caret's cell so the
7
7
  * terminal-painted IME preedit (pinyin) can never be overlaid on it during
8
8
  * CJK composition.
9
+ *
10
+ * The query row is strictly single-line: an overlong query is windowed
11
+ * around the caret (horizontal scroll) instead of wrapping, so the native
12
+ * cursor declaration below stays exact for any query length.
9
13
  */
10
14
  export declare function SearchBox({ query, placeholder, isFocused, isTerminalFocused, prefix, width, cursorOffset, borderless, }: {
11
15
  query: string;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../../../src/components/SearchBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAuB,EACvB,SAAS,EACT,iBAAiB,EACjB,MAAY,EACZ,KAAK,EACL,YAAY,EACZ,UAAkB,GACnB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CAqDlB"}
1
+ {"version":3,"file":"SearchBox.d.ts","sourceRoot":"","sources":["../../../src/components/SearchBox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyD,MAAM,OAAO,CAAA;AAyD7E;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,WAAuB,EACvB,SAAS,EACT,iBAAiB,EACjB,MAAY,EACZ,KAAK,EACL,YAAY,EACZ,UAAkB,GACnB,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;IAClB,iBAAiB,EAAE,OAAO,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,GAAG,KAAK,CAAC,SAAS,CA0GlB"}