@deepseek-harness-tui/dsh-tui 0.5.2 → 0.5.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 (42) hide show
  1. package/README.md +6 -0
  2. package/lib/types/approvals.d.ts +80 -0
  3. package/lib/types/approvals.d.ts.map +1 -0
  4. package/lib/types/approvals.js +170 -0
  5. package/lib/types/commands.d.ts +14 -1
  6. package/lib/types/commands.d.ts.map +1 -1
  7. package/lib/types/commands.js +12 -0
  8. package/lib/types/compat/sessionLog.d.ts.map +1 -1
  9. package/lib/types/compat/sessionLog.js +52 -37
  10. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  11. package/lib/types/components/CommandSuggestions.js +5 -3
  12. package/lib/types/components/HelpMenu.d.ts +1 -0
  13. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  14. package/lib/types/components/HelpMenu.js +4 -1
  15. package/lib/types/components/PromptInput.d.ts.map +1 -1
  16. package/lib/types/components/PromptInput.js +16 -15
  17. package/lib/types/components/approvals/ApprovalPanel.d.ts +21 -0
  18. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -0
  19. package/lib/types/components/approvals/ApprovalPanel.js +48 -0
  20. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +6 -0
  21. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  22. package/lib/types/components/questions/AskUserQuestionPanel.js +9 -2
  23. package/lib/types/components/questions/PlanReviewPanel.d.ts +39 -0
  24. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -0
  25. package/lib/types/components/questions/PlanReviewPanel.js +168 -0
  26. package/lib/types/i18n.d.ts +174 -2
  27. package/lib/types/i18n.d.ts.map +1 -1
  28. package/lib/types/i18n.js +82 -1
  29. package/lib/types/ink/stringWidth.d.ts.map +1 -1
  30. package/lib/types/ink/stringWidth.js +61 -0
  31. package/lib/types/plugin.d.ts.map +1 -1
  32. package/lib/types/plugin.js +15 -0
  33. package/lib/types/questions.d.ts +3 -0
  34. package/lib/types/questions.d.ts.map +1 -1
  35. package/lib/types/questions.js +13 -1
  36. package/lib/types/screens/Chat.d.ts +8 -13
  37. package/lib/types/screens/Chat.d.ts.map +1 -1
  38. package/lib/types/screens/Chat.js +33 -18
  39. package/lib/types/utils/modifiers.d.ts +19 -0
  40. package/lib/types/utils/modifiers.d.ts.map +1 -0
  41. package/lib/types/utils/modifiers.js +17 -0
  42. package/package.json +2 -1
package/lib/types/i18n.js CHANGED
@@ -168,7 +168,7 @@ const dict = {
168
168
  'update-check-failed': { zh: '无法确认新版本(网络或 registry 不可达),已尝试直接更新……', en: 'Could not confirm a newer version (network or registry unreachable); attempting the update anyway…' },
169
169
  'vim-not-implemented': { zh: 'vim 模式暂未实现', en: 'vim mode not implemented yet' },
170
170
  'terminal-setup-hint': { zh: '推荐 Windows Terminal(≥110 列、等宽字体、TrueColor)。', en: 'Recommended: Windows Terminal (≥110 columns, monospace, TrueColor).' },
171
- 'terminal-paste-hint': { zh: 'Ctrl+V 粘贴文本/文件路径;Ctrl+Shift+V 终端原生粘贴;右键粘贴同样可用。', en: 'Ctrl+V pastes text/file paths; Ctrl+Shift+V is native terminal paste; right-click paste also works.' },
171
+ 'terminal-paste-hint': { zh: '{{mod}}V 粘贴文本/文件路径;Ctrl+Shift+V 终端原生粘贴;右键粘贴同样可用。', en: '{{mod}}V pastes text/file paths; Ctrl+Shift+V is native terminal paste; right-click paste also works.' },
172
172
  'connect-none': { zh: 'DSH 暂无远程连接机制(CC 的 /connect 对应能力未适配)。', en: 'DSH has no remote connection mechanism (CC\'s /connect equivalent is not adapted).' },
173
173
  'theme-switch-failed': { zh: '主题「{{name}}」切换失败(无法写入 ~/.dsh-cc/theme.json)', en: 'Theme "{{name}}" switch failed (cannot write ~/.dsh-cc/theme.json)' },
174
174
  'interrupt-delivered': { zh: '已打断当前回合,{{n}} 条消息立即处理', en: 'Interrupted current turn, {{n}} messages processed immediately' },
@@ -247,6 +247,74 @@ const dict = {
247
247
  'question-custom-tab': { zh: '自定义回答', en: 'Custom answer' },
248
248
  'question-attached-label': { zh: '(附加:{{label}})', en: '(attached: {{label}})' },
249
249
  'question-direct-input': { zh: '直接输入…', en: 'Type directly…' },
250
+ // ── components/approvals/ApprovalPanel.tsx ──────────────────────────
251
+ 'approval-waiting': { zh: ' ⏳ 等待审批 · {{tool}} ', en: ' Awaiting approval · {{tool}} ' },
252
+ 'approval-proceed': { zh: '要允许这次操作吗?', en: 'Do you want to proceed?' },
253
+ 'approval-yes': { zh: '允许(仅本次)', en: 'Yes, allow once' },
254
+ 'approval-no': { zh: '拒绝', en: 'No' },
255
+ 'approval-hint': { zh: '↑/↓ 选择 · Enter 确认 · Esc 拒绝', en: '↑/↓ select · Enter confirm · Esc reject' },
256
+ // ── components/questions/PlanReviewPanel.tsx ────────────────────────
257
+ 'plan-review-fallback-header': { zh: '计划评审', en: 'Plan review' },
258
+ 'plan-review-feedback-placeholder': { zh: '输入反馈,告诉模型要改什么…', en: 'Tell the model what to change…' },
259
+ 'plan-review-approve-needs-empty': { zh: '请先清空反馈再批准(或在输入行回车提交反馈)', en: 'Clear the feedback to approve (or press Enter on the input row to send it)' },
260
+ 'plan-review-hint': { zh: '↑/↓ 选择 · 1/2 快选 · 打字输入反馈 · Enter 提交 · Esc 打断评审', en: '↑/↓ select · 1/2 quick-pick · type feedback · Enter submit · Esc dismiss' },
261
+ // ── commands.ts — slash-command descriptions ─────────────────────────
262
+ // zh-only on purpose: the English text stays in `LOCAL_COMMANDS` (and in
263
+ // the DSH registry for external commands) as the single source of truth,
264
+ // so `localizedDescription` falls back to it whenever the active language
265
+ // has no entry here. `cmd-desc-<name>` keys are resolved at render time,
266
+ // so `/lang` switches apply on the next repaint.
267
+ // Conversation
268
+ 'cmd-desc-new': { zh: '新开会话' },
269
+ 'cmd-desc-clear': { zh: '清空当前会话' },
270
+ 'cmd-desc-compact': { zh: '压缩会话历史' },
271
+ 'cmd-desc-resume': { zh: '恢复历史会话' },
272
+ 'cmd-desc-rewind': { zh: '回退会话到历史消息' },
273
+ 'cmd-desc-export': { zh: '导出会话为 Markdown 文件' },
274
+ // Session / environment
275
+ 'cmd-desc-status': { zh: '查看会话状态' },
276
+ 'cmd-desc-cost': { zh: '查看会话 token 用量' },
277
+ 'cmd-desc-config': { zh: '查看 dsh-cc 配置来源' },
278
+ 'cmd-desc-doctor': { zh: '运行环境检查' },
279
+ 'cmd-desc-init': { zh: '在工作目录创建 AGENTS.md' },
280
+ 'cmd-desc-agents': { zh: '查看本会话的子代理' },
281
+ // Model / display
282
+ 'cmd-desc-activity': { zh: '切换工作状态指示器预设' },
283
+ 'cmd-desc-preset': { zh: '切换 Agent 预设(standard/code/minimal/cordis)' },
284
+ 'cmd-desc-theme': { zh: '切换配色主题(内置或自定义)' },
285
+ 'cmd-desc-lang': { zh: '切换界面语言(en / zh)' },
286
+ 'cmd-desc-model': { zh: '查看当前模型' },
287
+ 'cmd-desc-thinking': { zh: '切换扩展思考显示' },
288
+ 'cmd-desc-tokens': { zh: '查看会话 token 用量' },
289
+ // Account / policy
290
+ 'cmd-desc-login': { zh: '查看 API 凭证状态' },
291
+ 'cmd-desc-logout': { zh: '清除 API 凭证' },
292
+ 'cmd-desc-permissions': { zh: '查看权限策略状态' },
293
+ 'cmd-desc-add-dir': { zh: '查看文件系统策略范围' },
294
+ 'cmd-desc-hooks': { zh: '查看 hooks 状态' },
295
+ 'cmd-desc-mcp': { zh: '查看 MCP 状态' },
296
+ 'cmd-desc-memory': { zh: '查看记忆状态' },
297
+ 'cmd-desc-update': { zh: '更新 dsh-cc-tui 并重启' },
298
+ // Built-in skills
299
+ 'cmd-desc-audit': { zh: '对当前项目做全面代码审计' },
300
+ 'cmd-desc-bug': { zh: '记录一份 bug 报告' },
301
+ 'cmd-desc-practice': { zh: '与 dsh-cc 进行编程练习' },
302
+ 'cmd-desc-review': { zh: '对当前项目做全面代码评审' },
303
+ 'cmd-desc-pr_comments': { zh: '审查拉取请求评论' },
304
+ 'cmd-desc-release-notes': { zh: '生成发布说明' },
305
+ 'cmd-desc-vuln-check': { zh: '运行安全漏洞检查' },
306
+ // Misc
307
+ 'cmd-desc-vim': { zh: '切换 vim 模式' },
308
+ 'cmd-desc-terminal-setup': { zh: '查看终端配置建议' },
309
+ 'cmd-desc-connect': { zh: '连接远程机器' },
310
+ // Help / exit
311
+ 'cmd-desc-help': { zh: '查看快捷键与命令' },
312
+ 'cmd-desc-exit': { zh: '退出 dsh-tui' },
313
+ // Registry-injected (external) commands — zh only; en falls back to the
314
+ // registry's own description, and unlisted externals always fall back.
315
+ 'cmd-desc-plan': { zh: '切换计划模式(/plan off 退出)' },
316
+ 'cmd-desc-goal': { zh: '设置或查看会话目标' },
317
+ 'cmd-desc-feedback': { zh: '提交使用反馈' },
250
318
  // ── /lang command ───────────────────────────────────────────────────
251
319
  'lang-current': { zh: '当前语言 {{lang}}', en: 'Current language {{lang}}' },
252
320
  'lang-switch-hint': { zh: '切换 /lang en | /lang zh', en: 'Switch /lang en | /lang zh' },
@@ -291,6 +359,19 @@ export function t(key, params = {}) {
291
359
  const template = entry?.[activeLang] ?? key;
292
360
  return template.replace(/\{\{(\w+)\}\}/g, (match, name) => name in params ? String(params[name]) : match);
293
361
  }
362
+ /**
363
+ * Translate a runtime-computed key (e.g. `cmd-desc-${name}`), falling back
364
+ * to the given text when the key is missing or has no entry in the active
365
+ * language — unlike {@link t}, which renders the key itself. Used where the
366
+ * fallback holds the authoritative text (command descriptions: the en copy
367
+ * lives in `LOCAL_COMMANDS` / the DSH registry, the dict carries zh only).
368
+ * @param key - Dictionary key, computed at runtime so it is not type-checked.
369
+ * @param fallback - Text used when no translation exists.
370
+ */
371
+ export function tOr(key, fallback) {
372
+ const entry = dict[key];
373
+ return entry?.[activeLang] ?? fallback;
374
+ }
294
375
  // ── persistence (~/.dsh-cc/lang.json) ──────────────────────────────────
295
376
  /**
296
377
  * Parse a persisted `{ lang }` value; anything else yields undefined.
@@ -1 +1 @@
1
- {"version":3,"file":"stringWidth.d.ts","sourceRoot":"","sources":["../../../src/ink/stringWidth.ts"],"names":[],"mappings":"AA2NA;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAElB,CAAA"}
1
+ {"version":3,"file":"stringWidth.d.ts","sourceRoot":"","sources":["../../../src/ink/stringWidth.ts"],"names":[],"mappings":"AAuRA;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAElB,CAAA"}
@@ -114,8 +114,69 @@ function getEmojiWidth(grapheme) {
114
114
  return 1;
115
115
  }
116
116
  }
117
+ // Single codepoint without VS16: only Emoji_Presentation=Yes characters
118
+ // actually render as wide emoji. Text-default emoji (✳ U+2733, ⚠ U+26A0,
119
+ // ❤ U+2764, ❄ U+2744, ✂ U+2702 …) render width 1 in terminals
120
+ // (ghostty/kitty/iTerm2/xterm.js all agree) unless VS16 forces emoji
121
+ // presentation. emoji-regex matches BOTH categories, so counting every
122
+ // match as 2 desyncs the layout from the terminal: the spinner glyph ✳
123
+ // measured 2 but painted 1, shifting the whole spinner row 1 column every
124
+ // time the animation cycle hit it — leaving stray cells ("tthinking",
125
+ // orphan "t" residue). VS16 sequences fall through to width 2 below.
126
+ if (!grapheme.includes('\ufe0f')) {
127
+ let codepoints = 0;
128
+ for (const _ of grapheme)
129
+ codepoints++;
130
+ if (codepoints === 1 && !hasEmojiPresentation(first)) {
131
+ return 1;
132
+ }
133
+ }
117
134
  return 2;
118
135
  }
136
+ /**
137
+ * Emoji_Presentation=Yes codepoints (Unicode emoji-data.txt, merged
138
+ * ranges). These render as wide emoji without VS16; everything else
139
+ * emoji-regex matches is text-default and renders narrow.
140
+ */
141
+ const EMOJI_PRESENTATION_RANGES = [
142
+ [0x231a, 0x231b], [0x23e9, 0x23ec], 0x23f0, 0x23f3, [0x25fd, 0x25fe],
143
+ [0x2614, 0x2615], [0x2648, 0x2653], 0x267f, 0x2693, 0x26a1,
144
+ [0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], 0x26ce, 0x26d4,
145
+ 0x26ea, [0x26f2, 0x26f3], 0x26f5, 0x26fa, 0x26fd, 0x2705,
146
+ [0x270a, 0x270b], 0x2728, 0x274c, 0x274e, [0x2753, 0x2755], 0x2757,
147
+ [0x2795, 0x2797], 0x27b0, 0x27bf, [0x2b1b, 0x2b1c], 0x2b50, 0x2b55,
148
+ 0x1f004, 0x1f0cf, 0x1f18e, [0x1f191, 0x1f19a], [0x1f1e6, 0x1f1ff],
149
+ 0x1f201, 0x1f21a, 0x1f22f, [0x1f232, 0x1f236], [0x1f238, 0x1f23a],
150
+ [0x1f250, 0x1f251], [0x1f300, 0x1f320], [0x1f32d, 0x1f335],
151
+ [0x1f337, 0x1f37c], [0x1f37e, 0x1f393], [0x1f3a0, 0x1f3ca],
152
+ [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], 0x1f3f4, [0x1f3f8, 0x1f43e],
153
+ 0x1f440, [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d], [0x1f54b, 0x1f54e],
154
+ [0x1f550, 0x1f567], 0x1f57a, [0x1f595, 0x1f596], 0x1f5a4,
155
+ [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], 0x1f6cc, [0x1f6d0, 0x1f6d2],
156
+ [0x1f6d5, 0x1f6d8], [0x1f6dc, 0x1f6df], [0x1f6eb, 0x1f6ec],
157
+ [0x1f6f4, 0x1f6fc], [0x1f7e0, 0x1f7eb], 0x1f7f0, [0x1f90c, 0x1f93a],
158
+ [0x1f93c, 0x1f945], [0x1f947, 0x1f9ff], [0x1fa70, 0x1fa7c],
159
+ [0x1fa80, 0x1fa8a], [0x1fa8e, 0x1fac6], 0x1fac8, [0x1facd, 0x1fadc],
160
+ [0x1fadf, 0x1faea], [0x1faef, 0x1faf8],
161
+ ];
162
+ /** Binary search over the merged, sorted Emoji_Presentation ranges. */
163
+ function hasEmojiPresentation(cp) {
164
+ let lo = 0;
165
+ let hi = EMOJI_PRESENTATION_RANGES.length - 1;
166
+ while (lo <= hi) {
167
+ const mid = (lo + hi) >> 1;
168
+ const entry = EMOJI_PRESENTATION_RANGES[mid];
169
+ const start = typeof entry === 'number' ? entry : entry[0];
170
+ const end = typeof entry === 'number' ? entry : entry[1];
171
+ if (cp < start)
172
+ hi = mid - 1;
173
+ else if (cp > end)
174
+ lo = mid + 1;
175
+ else
176
+ return true;
177
+ }
178
+ return false;
179
+ }
119
180
  function isZeroWidth(codePoint) {
120
181
  // Fast path for common printable range
121
182
  if (codePoint >= 0x20 && codePoint < 0x7f)
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAkBnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwSvE;AAyGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAmBnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuTvE;AAyGD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA"}
@@ -7,6 +7,7 @@ import { createChannel } from './channel.js';
7
7
  import { createChildStderrReporter, installChildStderrGuard } from './childStderr.js';
8
8
  import { logForDebugging } from './utils/debug.js';
9
9
  import { QuestionStore } from './questions.js';
10
+ import { ApprovalStore } from './approvals.js';
10
11
  import { registerPackagedSkills } from './packaged-skills.js';
11
12
  import { readActivityFrames } from './activityPrefs.js';
12
13
  import { readModelPref } from './modelPrefs.js';
@@ -143,6 +144,19 @@ export async function apply(ctx, config) {
143
144
  agentPreset,
144
145
  handle,
145
146
  });
147
+ // DSH approval seam: the permission layer asks ApprovalService.request(),
148
+ // which dispatches an `approval/request` waterfall. With no answerer the
149
+ // chain falls through to the fail-closed 'unavailable', so register this
150
+ // TUI as the interactive answerer for the agent it owns; requests for
151
+ // other agents delegate down the chain (next()). Guarded on the service
152
+ // being mounted — a bare composition without the dsh-base approval row
153
+ // has nothing to answer into. channel.agentId tracks agent swaps
154
+ // (/new, /resume, rewind), so ownership is re-evaluated per request.
155
+ const approvalStore = new ApprovalStore();
156
+ if (ctx.get('approval') !== undefined) {
157
+ ctx.on('approval/request', (req, next) => String(req.agent.id) === channel.agentId ? approvalStore.park(req) : next());
158
+ ctx.effect(() => () => approvalStore.settleAll('cancelled'));
159
+ }
146
160
  // Attach the stderr reporter to the live channel and flush anything a
147
161
  // startup-spawned server produced while the channel didn't exist yet.
148
162
  notifyStderr = (text, options) => channel.notify(text, options);
@@ -241,6 +255,7 @@ export async function apply(ctx, config) {
241
255
  const chat = React.createElement(Chat, {
242
256
  channel,
243
257
  questionStore,
258
+ approvalStore,
244
259
  onExit: () => handleExit(),
245
260
  // Only a `dsh --profile <name>` launch has a profile installation for
246
261
  // `/update` to act on; source checkouts and `--config` overlays get the
@@ -92,6 +92,9 @@ export declare class QuestionStore {
92
92
  cancelCurrent(): void;
93
93
  /** Reject the active and all queued asks (plugin teardown). */
94
94
  rejectAll(): void;
95
+ /** User-initiated cancel — the asker learns the user wants to speak. */
96
+ private cancel;
97
+ /** Harness-side interruption — abort signal fired or plugin teardown. */
95
98
  private fail;
96
99
  }
97
100
  //# sourceMappingURL=questions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"questions.d.ts","sourceRoot":"","sources":["../../src/questions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAEL,KAAK,qBAAqB,EAE1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC5B,MAAM,iCAAiC,CAAA;AAExC;mEACmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAgBD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;IACtC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AA0BD;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,QAAQ,CAAI;IACpB,yEAAyE;IACzE,OAAO,CAAC,SAAS,CAAwB;IACzC;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAgC;IAErD;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C;;;OAGG;IACH,WAAW,IAAI,gBAAgB,GAAG,IAAI;IAItC;;;OAGG;IACH,aAAa,IAAI,eAAe,EAAE;IAMlC,OAAO,CAAC,IAAI;IAIZ,sEAAsE;IACtE,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA6BpE,8CAA8C;IAC9C,OAAO,CAAC,SAAS;IAOjB;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAwBjD,6DAA6D;IAC7D,aAAa,IAAI,IAAI;IAUrB,+DAA+D;IAC/D,SAAS,IAAI,IAAI;IASjB,OAAO,CAAC,IAAI;CAMb"}
1
+ {"version":3,"file":"questions.d.ts","sourceRoot":"","sources":["../../src/questions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAEL,KAAK,qBAAqB,EAE1B,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC5B,MAAM,iCAAiC,CAAA;AAExC;mEACmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAgBD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAA;IACtC,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAClC;AAiCD;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,QAAQ,CAAI;IACpB,yEAAyE;IACzE,OAAO,CAAC,SAAS,CAAwB;IACzC;;;OAGG;IACH,OAAO,CAAC,aAAa,CAAgC;IAErD;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C;;;OAGG;IACH,WAAW,IAAI,gBAAgB,GAAG,IAAI;IAItC;;;OAGG;IACH,aAAa,IAAI,eAAe,EAAE;IAMlC,OAAO,CAAC,IAAI;IAIZ,sEAAsE;IACtE,OAAO,CAAC,eAAe;IAgBvB;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA6BpE,8CAA8C;IAC9C,OAAO,CAAC,SAAS;IAOjB;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAwBjD,6DAA6D;IAC7D,aAAa,IAAI,IAAI;IAUrB,+DAA+D;IAC/D,SAAS,IAAI,IAAI;IASjB,wEAAwE;IACxE,OAAO,CAAC,MAAM;IAOd,yEAAyE;IACzE,OAAO,CAAC,IAAI;CAMb"}
@@ -14,6 +14,13 @@
14
14
  import { t } from './i18n.js';
15
15
  import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
16
16
  const ASK_ABORTED = 'ASK_ABORTED';
17
+ /**
18
+ * User-initiated cancel (Esc / Ctrl+C in the panel). dsh-plan-mode keys on
19
+ * this code to report "the user dismissed the plan review to speak instead"
20
+ * rather than the generic abort message, so user cancels must be told apart
21
+ * from harness aborts (signal fired) and teardown rejects.
22
+ */
23
+ const ASK_CANCELLED = 'ASK_CANCELLED';
17
24
  /** Truncate a long answer line for the transcript summary. */
18
25
  function clip(text, max = 140) {
19
26
  if (text.length <= max)
@@ -180,7 +187,7 @@ export class QuestionStore {
180
187
  return;
181
188
  this.active = undefined;
182
189
  this.rebuildSnapshot();
183
- this.fail(pending);
190
+ this.cancel(pending);
184
191
  this.startNext();
185
192
  this.emit();
186
193
  }
@@ -195,6 +202,11 @@ export class QuestionStore {
195
202
  this.fail(pending);
196
203
  this.emit();
197
204
  }
205
+ /** User-initiated cancel — the asker learns the user wants to speak. */
206
+ cancel(pending) {
207
+ pending.reject(new UserQuestionError('the user cancelled ask_user_question', ASK_CANCELLED));
208
+ }
209
+ /** Harness-side interruption — abort signal fired or plugin teardown. */
198
210
  fail(pending) {
199
211
  pending.reject(new UserQuestionError('ask_user_question was interrupted before the user answered', ASK_ABORTED));
200
212
  }
@@ -1,21 +1,16 @@
1
1
  import React from 'react';
2
2
  import type { Channel } from '../channel.js';
3
3
  import type { QuestionStore } from '../questions.js';
4
- /**
5
- * Main chat screen in the Claude Code layout: a scrollable transcript
6
- * (with the current turn's prompt pinned above the viewport while scrolled
7
- * up), transient notifications, the working spinner, the bordered prompt
8
- * input (with slash-command overlay) and the status line pinned at the
9
- * bottom.
10
- *
11
- * Ctrl+O toggles expanded detail globally; Shift+↑ enters message-selection
12
- * mode (↑/↓ move, Enter expands the selected row, Esc exits); Ctrl+C
13
- * interrupts the running turn, or (when idle) asks for a second Ctrl+C to
14
- * exit; Enter while scrolled up jumps back to the bottom.
15
- */
16
- export declare function Chat({ channel, questionStore, onExit, onUpdate, }: {
4
+ import { ApprovalStore } from '../approvals.js';
5
+ export declare function Chat({ channel, questionStore, approvalStore, onExit, onUpdate, }: {
17
6
  channel: Channel;
18
7
  questionStore: QuestionStore;
8
+ /**
9
+ * The approval seam's UI store. Optional: hosts without an approval
10
+ * channel (headless scripts, older embeds) render Chat without it and
11
+ * simply never see an approval panel — the question panel keeps its seat.
12
+ */
13
+ approvalStore?: ApprovalStore;
19
14
  onExit: () => void;
20
15
  /** Update the installed package and restart the current TUI process. */
21
16
  onUpdate?: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,OAAO,EAAyB,MAAM,eAAe,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAqFpD;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,MAAM,EACN,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,qBA0wCA"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB,OAAO,KAAK,EAAE,OAAO,EAAyB,MAAM,eAAe,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AA0G/C,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,MAAM,EACN,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB,qBAwxCA"}
@@ -3,8 +3,11 @@ import React from 'react';
3
3
  import { t, getLang, setLang, isLang, writeLangPref, subscribeLang } from '../i18n.js';
4
4
  import { Box, Text, useInput, ScrollBox, useTheme } from '../ui.js';
5
5
  import { POINTER } from '../cc/figures.js';
6
+ import { isMod, modLabel } from '../utils/modifiers.js';
6
7
  import { formatTokens } from '../cc/format.js';
8
+ import { ApprovalStore } from '../approvals.js';
7
9
  import { AskUserQuestionPanel } from '../components/questions/AskUserQuestionPanel.js';
10
+ import { ApprovalPanel } from '../components/approvals/ApprovalPanel.js';
8
11
  import { useSearchHighlight } from '../ink/hooks/use-search-highlight.js';
9
12
  import { useTerminalTitle } from '../ink/hooks/use-terminal-title.js';
10
13
  import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js';
@@ -91,7 +94,13 @@ function searchableText(row) {
91
94
  * interrupts the running turn, or (when idle) asks for a second Ctrl+C to
92
95
  * exit; Enter while scrolled up jumps back to the bottom.
93
96
  */
94
- export function Chat({ channel, questionStore, onExit, onUpdate, }) {
97
+ /**
98
+ * Shared inert approval store for hosts that render Chat without an
99
+ * approval seam (headless verify scripts). Never parked into, so its
100
+ * snapshot stays null and the approval panel never mounts.
101
+ */
102
+ let fallbackApprovalStore;
103
+ export function Chat({ channel, questionStore, approvalStore, onExit, onUpdate, }) {
95
104
  // Re-render whenever the channel mutates; rows/status are read fresh below.
96
105
  React.useSyncExternalStore(channel.subscribe, () => channel.version);
97
106
  // Re-render on language switches so the whole UI hot-swaps its strings.
@@ -99,6 +108,12 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
99
108
  // The pending ask-user-question (DSH user-interaction seam): the model's
100
109
  // `ask_user_question` tool parks here until the panel is answered.
101
110
  const questionSnapshot = React.useSyncExternalStore(listener => questionStore.subscribe(listener), () => questionStore.getSnapshot());
111
+ // The pending tool-approval ask (DSH approval seam): the permission layer
112
+ // parks here until the panel decides; shown with priority over a pending
113
+ // questionnaire since it gates a tool about to run. Hosts that pass no
114
+ // approvalStore share one inert instance that never holds an ask.
115
+ const approvals = approvalStore ?? (fallbackApprovalStore ??= new ApprovalStore());
116
+ const approvalSnapshot = React.useSyncExternalStore(listener => approvals.subscribe(listener), () => approvals.getSnapshot());
102
117
  // When a questionnaire batch completes, fold a Q&A summary into the
103
118
  // transcript (the tool card itself is hidden from the message list).
104
119
  const questionOpenRef = React.useRef(questionSnapshot !== null);
@@ -616,7 +631,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
616
631
  setHelpOpen(false);
617
632
  channel.pushLocal('/terminal-setup', [
618
633
  t('terminal-setup-hint'),
619
- t('terminal-paste-hint'),
634
+ t('terminal-paste-hint', { mod: modLabel }),
620
635
  ]);
621
636
  return true;
622
637
  case 'connect':
@@ -802,10 +817,10 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
802
817
  rowRefsRef.current.delete(rowId);
803
818
  }, []);
804
819
  useInput((input, key, event) => {
805
- // The questionnaire owns the keyboard while a question is pending (the
806
- // panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc; the prompt
807
- // input is unmounted, so nothing else should see these keys).
808
- if (questionSnapshot !== null)
820
+ // The questionnaire / approval panel owns the keyboard while one is
821
+ // pending (the panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
822
+ // the prompt input is unmounted, so nothing else should see these keys).
823
+ if (questionSnapshot !== null || approvalSnapshot !== null)
809
824
  return;
810
825
  // Mouse wheel scrolls the transcript — in fullscreen there is no
811
826
  // terminal scrollback (alt-screen), so this is the only way back.
@@ -862,7 +877,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
862
877
  else if (key.end) {
863
878
  setSearchCursor(searchQuery.length);
864
879
  }
865
- else if (!key.ctrl && !key.meta && input) {
880
+ else if (!key.ctrl && !key.meta && !key.super && input) {
866
881
  const next = searchQuery.slice(0, searchCursor) + input + searchQuery.slice(searchCursor);
867
882
  setSearchQuery(next);
868
883
  setSearchCursor(searchCursor + input.length);
@@ -873,12 +888,12 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
873
888
  // After Enter closed the search bar, n/N keep walking the matches
874
889
  // (CC: "Query persists across bar open/close so n/N keep working").
875
890
  // Transcript mode only — in prompt mode n/N are ordinary input chars.
876
- if (expanded && input === 'n' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta) {
891
+ if (expanded && input === 'n' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta && !key.super) {
877
892
  setSearchCurrent(i => (i >= searchCount - 1 ? 0 : i + 1));
878
893
  event.stopImmediatePropagation();
879
894
  return;
880
895
  }
881
- if (expanded && input === 'N' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta) {
896
+ if (expanded && input === 'N' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta && !key.super) {
882
897
  setSearchCurrent(i => (i <= 0 ? searchCount - 1 : i - 1));
883
898
  event.stopImmediatePropagation();
884
899
  return;
@@ -1053,7 +1068,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1053
1068
  else if (key.upArrow) {
1054
1069
  setHistoryFocus(index => historyMatches.length === 0 ? 0 : (index <= 0 ? historyMatches.length - 1 : index - 1));
1055
1070
  }
1056
- else if (key.downArrow || (key.ctrl && input === 'r')) {
1071
+ else if (key.downArrow || (isMod(key) && input === 'r')) {
1057
1072
  // CC's historySearch:next — ↓ and repeat ctrl+r walk to the next match.
1058
1073
  setHistoryFocus(index => historyMatches.length === 0 ? 0 : (index >= historyMatches.length - 1 ? 0 : index + 1));
1059
1074
  }
@@ -1083,7 +1098,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1083
1098
  else if (key.end) {
1084
1099
  setHistoryCursor(historyQuery.length);
1085
1100
  }
1086
- else if (!key.ctrl && !key.meta && input) {
1101
+ else if (!key.ctrl && !key.meta && !key.super && input) {
1087
1102
  const next = historyQuery.slice(0, historyCursor) + input + historyQuery.slice(historyCursor);
1088
1103
  setHistoryQuery(next);
1089
1104
  setHistoryCursor(historyCursor + input.length);
@@ -1122,12 +1137,12 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1122
1137
  }
1123
1138
  return;
1124
1139
  }
1125
- if (key.ctrl && input === 't') {
1140
+ if (isMod(key) && input === 't') {
1126
1141
  // Toggle the startup loaded-context panel (keyboard only — the
1127
1142
  // ported ink core handles no mouse clicks).
1128
1143
  setLoadedContextOpen(previous => !previous);
1129
1144
  }
1130
- if (key.ctrl && input === 'r' && !helpOpen) {
1145
+ if (isMod(key) && input === 'r' && !helpOpen) {
1131
1146
  setHistoryQuery('');
1132
1147
  setHistoryCursor(0);
1133
1148
  setHistoryFocus(0);
@@ -1169,11 +1184,11 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1169
1184
  }
1170
1185
  event.stopImmediatePropagation();
1171
1186
  }
1172
- else if (key.ctrl && input === 'o') {
1187
+ else if (isMod(key) && input === 'o') {
1173
1188
  // Leaving transcript mode (Ctrl+O) — search was already handled above.
1174
1189
  setExpanded(previous => !previous);
1175
1190
  }
1176
- else if (input === '/' && !key.ctrl && !key.meta) {
1191
+ else if (input === '/' && !key.ctrl && !key.meta && !key.super) {
1177
1192
  // `/` in transcript mode (Ctrl+O expanded, CC's REPL semantics:
1178
1193
  // search is active on the transcript screen where `/` isn't a command).
1179
1194
  if (expanded) {
@@ -1205,11 +1220,11 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1205
1220
  requestExit();
1206
1221
  }
1207
1222
  }
1208
- else if (key.ctrl && input === 'l') {
1223
+ else if (isMod(key) && input === 'l') {
1209
1224
  // CC's app:redraw — clear the physical terminal and repaint.
1210
1225
  instances.get(process.stdout)?.forceRedraw();
1211
1226
  }
1212
- else if (key.ctrl && input === 'e') {
1227
+ else if (isMod(key) && input === 'e') {
1213
1228
  setShowAllMessages(previous => !previous);
1214
1229
  }
1215
1230
  else if (key.return && showPill) {
@@ -1233,7 +1248,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1233
1248
  (channel.activityEnabled &&
1234
1249
  channel.workingActivity !== undefined &&
1235
1250
  channel.workingActivity.line !== '' &&
1236
- channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95, suffix: ` · ↓ ${channel.responseChars} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), resumePickerOpen && resumeSessions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ResumePicker, { sessions: resumeSessions, focusIndex: resumeIndex, currentSessionId: channel.agentId }) })), modelPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: modelIndex, currentModel: `${channel.provider}/${channel.model}` })) })), activityPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: activityIndex, currentPreset: channel.activityFrames }) })), presetPickerOpen && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: presetIndex, currentPreset: channel.agentPreset }) })), themePickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: themeIndex, currentTheme: themeName }) })), historyOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: historyQuery, cursorOffset: historyCursor, matches: historyMatches, focusIndex: historyFocus }) })), rewindOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: rewindIndex, confirmRow: rewindConfirm }) })), searchOpen && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent }), _jsx(GoalTodoPanel, { channel: channel }), questionSnapshot !== null && (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent() }, questionSnapshot.key)), questionSnapshot === null && (_jsx(PromptInput, { channel: channel, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => { setHistoryFill(null); }, onRewindRequest: openRewind, controllerRef: promptControllerRef })), _jsx(StatusLine, { channel: channel, selectionActive: selectionActive, helpOpen: helpOpen })] })] }));
1251
+ channel.workingActivity.phase !== 'idle' ? (_jsx(Box, { marginTop: 1, children: _jsx(ActivityLine, { activity: channel.workingActivity, activityFrames: channel.activityFrames, warnPct: activityWarnPct, warnDanger: activityWarnPct !== undefined && activityWarnPct >= 95, suffix: ` · ↓ ${channel.responseChars} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), thinkingOpen && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: thinkingFocus, confirmationPending: thinkingConfirm })), resumePickerOpen && resumeSessions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ResumePicker, { sessions: resumeSessions, focusIndex: resumeIndex, currentSessionId: channel.agentId }) })), modelPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: modelIndex, currentModel: `${channel.provider}/${channel.model}` })) })), activityPickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: activityIndex, currentPreset: channel.activityFrames }) })), presetPickerOpen && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: presetIndex, currentPreset: channel.agentPreset }) })), themePickerOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: themeIndex, currentTheme: themeName }) })), historyOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: historyQuery, cursorOffset: historyCursor, matches: historyMatches, focusIndex: historyFocus }) })), rewindOpen && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: rewindIndex, confirmRow: rewindConfirm }) })), searchOpen && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent }), _jsx(GoalTodoPanel, { channel: channel }), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent() }, questionSnapshot.key)) : (_jsx(PromptInput, { channel: channel, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => { setHistoryFill(null); }, onRewindRequest: openRewind, controllerRef: promptControllerRef })), _jsx(StatusLine, { channel: channel, selectionActive: selectionActive, helpOpen: helpOpen })] })] }));
1237
1252
  }
1238
1253
  /**
1239
1254
  * The pinned prompt header shown above the ScrollBox while the user has
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cross-platform modifier-key helpers. Windows/Linux muscle memory uses
3
+ * Ctrl+<key>; on macOS the same action maps to Cmd+<key>. Terminals deliver
4
+ * Cmd as the `super` flag (kitty CSI-u / xterm modifyOtherKeys — see
5
+ * src/ink/parse-keypress.ts); Ctrl keeps working everywhere, so `isMod`
6
+ * accepts either on the mac and stays Ctrl-only elsewhere.
7
+ */
8
+ export declare const isMac: boolean;
9
+ /** True when the key event carries the platform's "primary" modifier. */
10
+ export declare function isMod(key: {
11
+ ctrl?: boolean;
12
+ super?: boolean;
13
+ }): boolean;
14
+ /**
15
+ * Display prefix for shortcut labels: "⌘" on macOS (Apple style, no "+"),
16
+ * "ctrl+" everywhere else. Pair with the bare key, e.g. `${modLabel}o`.
17
+ */
18
+ export declare const modLabel: string;
19
+ //# sourceMappingURL=modifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modifiers.d.ts","sourceRoot":"","sources":["../../../src/utils/modifiers.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,KAAK,SAAgC,CAAA;AAElD,yEAAyE;AACzE,wBAAgB,KAAK,CAAC,GAAG,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAEvE;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,QAAwB,CAAA"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cross-platform modifier-key helpers. Windows/Linux muscle memory uses
3
+ * Ctrl+<key>; on macOS the same action maps to Cmd+<key>. Terminals deliver
4
+ * Cmd as the `super` flag (kitty CSI-u / xterm modifyOtherKeys — see
5
+ * src/ink/parse-keypress.ts); Ctrl keeps working everywhere, so `isMod`
6
+ * accepts either on the mac and stays Ctrl-only elsewhere.
7
+ */
8
+ export const isMac = process.platform === 'darwin';
9
+ /** True when the key event carries the platform's "primary" modifier. */
10
+ export function isMod(key) {
11
+ return !!key.ctrl || (isMac && !!key.super);
12
+ }
13
+ /**
14
+ * Display prefix for shortcut labels: "⌘" on macOS (Apple style, no "+"),
15
+ * "ctrl+" everywhere else. Pair with the bare key, e.g. `${modLabel}o`.
16
+ */
17
+ export const modLabel = isMac ? '⌘' : 'ctrl+';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepseek-harness-tui/dsh-tui",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Claude Code style interactive TUI front door for DeepSeek Harness agents, built on the ported Ink core.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -69,6 +69,7 @@
69
69
  "@deepseek-ai/dsh-tool-ask-user": "^0.1.0-rc.6",
70
70
  "@deepseek-ai/dsh-tool-bash-persistent": "^0.1.0-rc.6",
71
71
  "@deepseek-ai/dsh-tool-cordis": "^0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.6",
72
73
  "@deepseek-ai/dsh-user-questions": "^0.1.0-rc.6",
73
74
  "@deepseek-ai/schemastery": "^3.18.1",
74
75
  "auto-bind": "^4.0.0 || ^5.0.0",