@deepseek-harness-tui/dsh-tui 0.5.2 → 0.6.0

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 (49) hide show
  1. package/README.md +7 -1
  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/channel.d.ts +5 -0
  6. package/lib/types/channel.d.ts.map +1 -1
  7. package/lib/types/channel.js +32 -25
  8. package/lib/types/commands.d.ts +14 -1
  9. package/lib/types/commands.d.ts.map +1 -1
  10. package/lib/types/commands.js +13 -0
  11. package/lib/types/compat/index.d.ts +1 -1
  12. package/lib/types/compat/index.d.ts.map +1 -1
  13. package/lib/types/compat/index.js +1 -1
  14. package/lib/types/compat/sessionLog.d.ts +21 -0
  15. package/lib/types/compat/sessionLog.d.ts.map +1 -1
  16. package/lib/types/compat/sessionLog.js +122 -37
  17. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  18. package/lib/types/components/CommandSuggestions.js +5 -3
  19. package/lib/types/components/HelpMenu.d.ts +1 -0
  20. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  21. package/lib/types/components/HelpMenu.js +4 -1
  22. package/lib/types/components/PromptInput.d.ts.map +1 -1
  23. package/lib/types/components/PromptInput.js +16 -15
  24. package/lib/types/components/approvals/ApprovalPanel.d.ts +21 -0
  25. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -0
  26. package/lib/types/components/approvals/ApprovalPanel.js +48 -0
  27. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +6 -0
  28. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  29. package/lib/types/components/questions/AskUserQuestionPanel.js +9 -2
  30. package/lib/types/components/questions/PlanReviewPanel.d.ts +39 -0
  31. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -0
  32. package/lib/types/components/questions/PlanReviewPanel.js +168 -0
  33. package/lib/types/i18n.d.ts +189 -2
  34. package/lib/types/i18n.d.ts.map +1 -1
  35. package/lib/types/i18n.js +86 -1
  36. package/lib/types/ink/stringWidth.d.ts.map +1 -1
  37. package/lib/types/ink/stringWidth.js +61 -0
  38. package/lib/types/plugin.d.ts.map +1 -1
  39. package/lib/types/plugin.js +15 -0
  40. package/lib/types/questions.d.ts +3 -0
  41. package/lib/types/questions.d.ts.map +1 -1
  42. package/lib/types/questions.js +13 -1
  43. package/lib/types/screens/Chat.d.ts +8 -13
  44. package/lib/types/screens/Chat.d.ts.map +1 -1
  45. package/lib/types/screens/Chat.js +47 -18
  46. package/lib/types/utils/modifiers.d.ts +19 -0
  47. package/lib/types/utils/modifiers.d.ts.map +1 -0
  48. package/lib/types/utils/modifiers.js +17 -0
  49. package/package.json +2 -1
@@ -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,qBAsyCA"}
@@ -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);
@@ -461,6 +476,20 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
461
476
  })();
462
477
  return true;
463
478
  }
479
+ case 'rename': {
480
+ setHelpOpen(false);
481
+ const title = rawInput.trim();
482
+ if (title.length === 0) {
483
+ channel.pushLocal('/rename', [
484
+ t('rename-current', { title: channel.sessionTitle || '—' }),
485
+ t('rename-usage'),
486
+ ]);
487
+ return true;
488
+ }
489
+ channel.renameSession(title);
490
+ channel.notify(t('rename-done', { title }));
491
+ return true;
492
+ }
464
493
  case 'rewind':
465
494
  // Same picker as PromptInput's double-Esc on an empty input (CC
466
495
  // rewind); `openRewind` notifies when there is nothing to rewind.
@@ -616,7 +645,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
616
645
  setHelpOpen(false);
617
646
  channel.pushLocal('/terminal-setup', [
618
647
  t('terminal-setup-hint'),
619
- t('terminal-paste-hint'),
648
+ t('terminal-paste-hint', { mod: modLabel }),
620
649
  ]);
621
650
  return true;
622
651
  case 'connect':
@@ -802,10 +831,10 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
802
831
  rowRefsRef.current.delete(rowId);
803
832
  }, []);
804
833
  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)
834
+ // The questionnaire / approval panel owns the keyboard while one is
835
+ // pending (the panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
836
+ // the prompt input is unmounted, so nothing else should see these keys).
837
+ if (questionSnapshot !== null || approvalSnapshot !== null)
809
838
  return;
810
839
  // Mouse wheel scrolls the transcript — in fullscreen there is no
811
840
  // terminal scrollback (alt-screen), so this is the only way back.
@@ -862,7 +891,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
862
891
  else if (key.end) {
863
892
  setSearchCursor(searchQuery.length);
864
893
  }
865
- else if (!key.ctrl && !key.meta && input) {
894
+ else if (!key.ctrl && !key.meta && !key.super && input) {
866
895
  const next = searchQuery.slice(0, searchCursor) + input + searchQuery.slice(searchCursor);
867
896
  setSearchQuery(next);
868
897
  setSearchCursor(searchCursor + input.length);
@@ -873,12 +902,12 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
873
902
  // After Enter closed the search bar, n/N keep walking the matches
874
903
  // (CC: "Query persists across bar open/close so n/N keep working").
875
904
  // Transcript mode only — in prompt mode n/N are ordinary input chars.
876
- if (expanded && input === 'n' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta) {
905
+ if (expanded && input === 'n' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta && !key.super) {
877
906
  setSearchCurrent(i => (i >= searchCount - 1 ? 0 : i + 1));
878
907
  event.stopImmediatePropagation();
879
908
  return;
880
909
  }
881
- if (expanded && input === 'N' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta) {
910
+ if (expanded && input === 'N' && searchQuery && searchCount > 0 && !key.ctrl && !key.meta && !key.super) {
882
911
  setSearchCurrent(i => (i <= 0 ? searchCount - 1 : i - 1));
883
912
  event.stopImmediatePropagation();
884
913
  return;
@@ -1053,7 +1082,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1053
1082
  else if (key.upArrow) {
1054
1083
  setHistoryFocus(index => historyMatches.length === 0 ? 0 : (index <= 0 ? historyMatches.length - 1 : index - 1));
1055
1084
  }
1056
- else if (key.downArrow || (key.ctrl && input === 'r')) {
1085
+ else if (key.downArrow || (isMod(key) && input === 'r')) {
1057
1086
  // CC's historySearch:next — ↓ and repeat ctrl+r walk to the next match.
1058
1087
  setHistoryFocus(index => historyMatches.length === 0 ? 0 : (index >= historyMatches.length - 1 ? 0 : index + 1));
1059
1088
  }
@@ -1083,7 +1112,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1083
1112
  else if (key.end) {
1084
1113
  setHistoryCursor(historyQuery.length);
1085
1114
  }
1086
- else if (!key.ctrl && !key.meta && input) {
1115
+ else if (!key.ctrl && !key.meta && !key.super && input) {
1087
1116
  const next = historyQuery.slice(0, historyCursor) + input + historyQuery.slice(historyCursor);
1088
1117
  setHistoryQuery(next);
1089
1118
  setHistoryCursor(historyCursor + input.length);
@@ -1122,12 +1151,12 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1122
1151
  }
1123
1152
  return;
1124
1153
  }
1125
- if (key.ctrl && input === 't') {
1154
+ if (isMod(key) && input === 't') {
1126
1155
  // Toggle the startup loaded-context panel (keyboard only — the
1127
1156
  // ported ink core handles no mouse clicks).
1128
1157
  setLoadedContextOpen(previous => !previous);
1129
1158
  }
1130
- if (key.ctrl && input === 'r' && !helpOpen) {
1159
+ if (isMod(key) && input === 'r' && !helpOpen) {
1131
1160
  setHistoryQuery('');
1132
1161
  setHistoryCursor(0);
1133
1162
  setHistoryFocus(0);
@@ -1169,11 +1198,11 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1169
1198
  }
1170
1199
  event.stopImmediatePropagation();
1171
1200
  }
1172
- else if (key.ctrl && input === 'o') {
1201
+ else if (isMod(key) && input === 'o') {
1173
1202
  // Leaving transcript mode (Ctrl+O) — search was already handled above.
1174
1203
  setExpanded(previous => !previous);
1175
1204
  }
1176
- else if (input === '/' && !key.ctrl && !key.meta) {
1205
+ else if (input === '/' && !key.ctrl && !key.meta && !key.super) {
1177
1206
  // `/` in transcript mode (Ctrl+O expanded, CC's REPL semantics:
1178
1207
  // search is active on the transcript screen where `/` isn't a command).
1179
1208
  if (expanded) {
@@ -1205,11 +1234,11 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1205
1234
  requestExit();
1206
1235
  }
1207
1236
  }
1208
- else if (key.ctrl && input === 'l') {
1237
+ else if (isMod(key) && input === 'l') {
1209
1238
  // CC's app:redraw — clear the physical terminal and repaint.
1210
1239
  instances.get(process.stdout)?.forceRedraw();
1211
1240
  }
1212
- else if (key.ctrl && input === 'e') {
1241
+ else if (isMod(key) && input === 'e') {
1213
1242
  setShowAllMessages(previous => !previous);
1214
1243
  }
1215
1244
  else if (key.return && showPill) {
@@ -1233,7 +1262,7 @@ export function Chat({ channel, questionStore, onExit, onUpdate, }) {
1233
1262
  (channel.activityEnabled &&
1234
1263
  channel.workingActivity !== undefined &&
1235
1264
  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 })] })] }));
1265
+ 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
1266
  }
1238
1267
  /**
1239
1268
  * 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.6.0",
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",