@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.
- package/README.md +7 -1
- package/lib/types/approvals.d.ts +80 -0
- package/lib/types/approvals.d.ts.map +1 -0
- package/lib/types/approvals.js +170 -0
- package/lib/types/channel.d.ts +5 -0
- package/lib/types/channel.d.ts.map +1 -1
- package/lib/types/channel.js +32 -25
- package/lib/types/commands.d.ts +14 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +13 -0
- package/lib/types/compat/index.d.ts +1 -1
- package/lib/types/compat/index.d.ts.map +1 -1
- package/lib/types/compat/index.js +1 -1
- package/lib/types/compat/sessionLog.d.ts +21 -0
- package/lib/types/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/compat/sessionLog.js +122 -37
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +5 -3
- package/lib/types/components/HelpMenu.d.ts +1 -0
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +4 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +16 -15
- package/lib/types/components/approvals/ApprovalPanel.d.ts +21 -0
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -0
- package/lib/types/components/approvals/ApprovalPanel.js +48 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +6 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +9 -2
- package/lib/types/components/questions/PlanReviewPanel.d.ts +39 -0
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -0
- package/lib/types/components/questions/PlanReviewPanel.js +168 -0
- package/lib/types/i18n.d.ts +189 -2
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +86 -1
- package/lib/types/ink/stringWidth.d.ts.map +1 -1
- package/lib/types/ink/stringWidth.js +61 -0
- package/lib/types/plugin.d.ts.map +1 -1
- package/lib/types/plugin.js +15 -0
- package/lib/types/questions.d.ts +3 -0
- package/lib/types/questions.d.ts.map +1 -1
- package/lib/types/questions.js +13 -1
- package/lib/types/screens/Chat.d.ts +8 -13
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +47 -18
- package/lib/types/utils/modifiers.d.ts +19 -0
- package/lib/types/utils/modifiers.d.ts.map +1 -0
- package/lib/types/utils/modifiers.js +17 -0
- package/package.json +2 -1
package/lib/types/questions.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
806
|
-
// panel's own useInput handles ↑/↓/Space/Tab/Enter/Esc;
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|