@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
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* marked, and already-known events are never touched.
|
|
17
17
|
*
|
|
18
18
|
* Storage notes: the jsonl persistence flushes by APPENDING zstd frames, so
|
|
19
|
-
* the file is a concatenation of frames
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* the file is a concatenation of frames. Frame layout is load-bearing: the
|
|
20
|
+
* backend asserts frame 0 holds EXACTLY the header line (listings read only
|
|
21
|
+
* that frame; `assertZstdHeaderFrame`), so the repair re-encodes each frame
|
|
22
|
+
* with its original line set — frame boundaries are preserved 1:1, and any
|
|
23
|
+
* frame whose lines were untouched is copied verbatim. Any decode/parse
|
|
24
|
+
* anomaly aborts the repair and resume proceeds unpatched — the failure
|
|
25
|
+
* mode degrades to the pre-patch behavior.
|
|
23
26
|
* @module @deepseek-harness-tui/dsh-tui/compat/sessionLog
|
|
24
27
|
*/
|
|
25
28
|
import { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session';
|
|
@@ -61,11 +64,12 @@ function findSessionLogFile(sessionId) {
|
|
|
61
64
|
return undefined;
|
|
62
65
|
}
|
|
63
66
|
/**
|
|
64
|
-
* Decode a (possibly multi-frame) zstd jsonl log
|
|
65
|
-
* scan; any frame failing to decode or any line
|
|
66
|
-
* callers abort instead of rewriting a log they
|
|
67
|
+
* Decode a (possibly multi-frame) zstd jsonl log, keeping frames separate.
|
|
68
|
+
* Frames are split by magic scan; any frame failing to decode or any line
|
|
69
|
+
* failing to parse throws, so callers abort instead of rewriting a log they
|
|
70
|
+
* did not fully understand.
|
|
67
71
|
* @param buf - Raw file bytes.
|
|
68
|
-
* @returns
|
|
72
|
+
* @returns Per-frame byte spans and parsed event envelopes, in log order.
|
|
69
73
|
*/
|
|
70
74
|
function decodeFrames(buf) {
|
|
71
75
|
const offsets = [];
|
|
@@ -75,20 +79,21 @@ function decodeFrames(buf) {
|
|
|
75
79
|
}
|
|
76
80
|
if (offsets.length === 0)
|
|
77
81
|
throw new Error('no zstd frame found');
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < offsets.length; i++) {
|
|
82
|
+
return offsets.map((start, i) => {
|
|
80
83
|
const end = i + 1 < offsets.length ? offsets[i + 1] : buf.length;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const raw = buf.subarray(start, end);
|
|
85
|
+
const text = zstdDecompressSync(raw).toString('utf8');
|
|
86
|
+
const events = text
|
|
87
|
+
.split('\n')
|
|
88
|
+
.filter((line) => line.length > 0)
|
|
89
|
+
.map((line) => {
|
|
90
|
+
const parsed = JSON.parse(line);
|
|
91
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
92
|
+
throw new Error('session log line is not an event envelope');
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
});
|
|
96
|
+
return { raw, events };
|
|
92
97
|
});
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
@@ -103,25 +108,35 @@ export function repairSessionLogForResume(sessionId) {
|
|
|
103
108
|
const file = findSessionLogFile(sessionId);
|
|
104
109
|
if (file === undefined)
|
|
105
110
|
return 'unavailable';
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
const frames = decodeFrames(readFileSync(file));
|
|
112
|
+
// Mark unknown types in place, tracking which frames actually changed:
|
|
113
|
+
// untouched frames are copied back verbatim, so the header frame keeps
|
|
114
|
+
// its exact original bytes (and the one-header-line invariant with it).
|
|
115
|
+
const dirty = new Set();
|
|
116
|
+
frames.forEach((frame, index) => {
|
|
117
|
+
for (const event of frame.events) {
|
|
118
|
+
const type = event['type'];
|
|
119
|
+
// Only real log entries carry a numeric seq — the seq-less header row
|
|
120
|
+
// is parsed by a separate path that must not see an extra field.
|
|
121
|
+
if (typeof type === 'string' &&
|
|
122
|
+
typeof event['seq'] === 'number' &&
|
|
123
|
+
!KNOWN_SESSION_EVENT_TYPES.has(type) &&
|
|
124
|
+
event['ignorable'] === undefined) {
|
|
125
|
+
event['ignorable'] = true;
|
|
126
|
+
dirty.add(index);
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
|
-
}
|
|
120
|
-
if (
|
|
129
|
+
});
|
|
130
|
+
if (dirty.size === 0)
|
|
121
131
|
return 'clean';
|
|
122
|
-
const
|
|
132
|
+
const parts = frames.map((frame, index) => {
|
|
133
|
+
if (!dirty.has(index))
|
|
134
|
+
return frame.raw;
|
|
135
|
+
const payload = frame.events.map((event) => JSON.stringify(event)).join('\n') + '\n';
|
|
136
|
+
return zstdCompressSync(Buffer.from(payload, 'utf8'));
|
|
137
|
+
});
|
|
123
138
|
const tmp = `${file}.compat-${randomUUID()}.tmp`;
|
|
124
|
-
writeFileSync(tmp,
|
|
139
|
+
writeFileSync(tmp, Buffer.concat(parts));
|
|
125
140
|
renameSync(tmp, file);
|
|
126
141
|
return 'repaired';
|
|
127
142
|
}
|
|
@@ -129,6 +144,76 @@ export function repairSessionLogForResume(sessionId) {
|
|
|
129
144
|
return 'unavailable';
|
|
130
145
|
}
|
|
131
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Read a session's display title from its persisted log, tolerantly.
|
|
149
|
+
*
|
|
150
|
+
* Why not `persistence.load()`: the backend validates every event against
|
|
151
|
+
* KNOWN_SESSION_EVENT_TYPES and throws the WHOLE load when a third-party
|
|
152
|
+
* plugin wrote an unmarked unknown type (e.g. activity/status before the
|
|
153
|
+
* resume repair touched it) — which is exactly why pickers fell back to the
|
|
154
|
+
* cwd basename for every working-activity session. A picker label is
|
|
155
|
+
* read-only UI state: decoding frames directly here keeps titles working
|
|
156
|
+
* for logs the strict path refuses, now and for future plugin event types.
|
|
157
|
+
*
|
|
158
|
+
* Title precedence: the LAST `session/title` event wins (a /rename append
|
|
159
|
+
* overrides the first-prompt auto title), falling back to the first user
|
|
160
|
+
* message text. `hasUserMessage` drives the picker's launch-artifact filter.
|
|
161
|
+
* @param sessionId - Session whose log should be read.
|
|
162
|
+
* @returns The title info, or undefined when the log is absent/undecodable.
|
|
163
|
+
*/
|
|
164
|
+
export function readSessionTitleFromLog(sessionId) {
|
|
165
|
+
try {
|
|
166
|
+
const file = findSessionLogFile(sessionId);
|
|
167
|
+
if (file === undefined)
|
|
168
|
+
return undefined;
|
|
169
|
+
const frames = decodeFrames(readFileSync(file));
|
|
170
|
+
let titled;
|
|
171
|
+
let firstUser;
|
|
172
|
+
let hasUserMessage = false;
|
|
173
|
+
for (const frame of frames) {
|
|
174
|
+
for (const event of frame.events) {
|
|
175
|
+
if (event['type'] === 'session/title') {
|
|
176
|
+
const title = event['data']?.['title'];
|
|
177
|
+
if (typeof title === 'string' && title.trim().length > 0)
|
|
178
|
+
titled = title;
|
|
179
|
+
}
|
|
180
|
+
else if (event['type'] === 'user/message') {
|
|
181
|
+
hasUserMessage = true;
|
|
182
|
+
if (firstUser === undefined) {
|
|
183
|
+
firstUser = firstTextOfContent(event['data']?.['content']);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { title: titled ?? firstUser, hasUserMessage };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Extract the first text block from a user/message `content` payload.
|
|
196
|
+
* Content is normally a block array; a bare string is accepted defensively.
|
|
197
|
+
* @param content - The event's content field.
|
|
198
|
+
* @returns The trimmed text, or undefined when no text block exists.
|
|
199
|
+
*/
|
|
200
|
+
function firstTextOfContent(content) {
|
|
201
|
+
if (typeof content === 'string')
|
|
202
|
+
return content.trim() || undefined;
|
|
203
|
+
if (!Array.isArray(content))
|
|
204
|
+
return undefined;
|
|
205
|
+
for (const block of content) {
|
|
206
|
+
if (block !== null &&
|
|
207
|
+
typeof block === 'object' &&
|
|
208
|
+
block.type === 'text' &&
|
|
209
|
+
typeof block.text === 'string') {
|
|
210
|
+
const text = (block.text).trim();
|
|
211
|
+
if (text.length > 0)
|
|
212
|
+
return text;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
132
217
|
/**
|
|
133
218
|
* Compat entry for the resume path: repair the target session's log, then
|
|
134
219
|
* let resume proceed regardless of outcome. Never throws, never blocks on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommandSuggestions.d.ts","sourceRoot":"","sources":["../../../src/components/CommandSuggestions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"CommandSuggestions.d.ts","sourceRoot":"","sources":["../../../src/components/CommandSuggestions.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGlD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,aAAa,EACb,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,KAAK,CAAC,SAAS,CAqDlB"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from '../ui.js';
|
|
3
3
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
4
4
|
import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
5
|
+
import { localizedDescription } from '../commands.js';
|
|
5
6
|
/**
|
|
6
7
|
* The slash-command suggestion overlay, ported from the leak's
|
|
7
8
|
* `PromptInputFooterSuggestions.tsx` (command layout only): a name column
|
|
@@ -25,9 +26,10 @@ export function CommandSuggestions({ commands, selectedIndex, columns, }) {
|
|
|
25
26
|
const tagText = command.tag ? `[${command.tag}] ` : '';
|
|
26
27
|
const tagWidth = stringWidth(tagText);
|
|
27
28
|
const descriptionWidth = Math.max(0, columns - nameWidth - tagWidth - 4);
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const rawDescription = localizedDescription(command);
|
|
30
|
+
const description = stringWidth(rawDescription) > descriptionWidth
|
|
31
|
+
? truncateToWidth(rawDescription, descriptionWidth - 1) + '…'
|
|
32
|
+
: rawDescription;
|
|
31
33
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isSelected ? 'suggestion' : undefined, dimColor: !isSelected, children: padded }), tagText ? _jsx(Text, { dimColor: true, children: tagText }) : null, _jsx(Text, { color: isSelected ? 'suggestion' : undefined, dimColor: !isSelected, children: description })] }, command.name));
|
|
32
34
|
}) }));
|
|
33
35
|
}
|
|
@@ -5,6 +5,7 @@ import type { LocalCommand } from '../commands.js';
|
|
|
5
5
|
* (three-column shortcut layout, trimmed to the keys dsh-tui actually binds).
|
|
6
6
|
* The command column lists the merged slash-command surface: built-in
|
|
7
7
|
* commands plus plugin-registered ones from the DSH registry (plan/goal/…).
|
|
8
|
+
* Modifier labels follow the platform convention: ⌘ on macOS, ctrl elsewhere.
|
|
8
9
|
*/
|
|
9
10
|
export declare function HelpMenu({ commands, }: {
|
|
10
11
|
commands: readonly LocalCommand[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HelpMenu.d.ts","sourceRoot":"","sources":["../../../src/components/HelpMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"HelpMenu.d.ts","sourceRoot":"","sources":["../../../src/components/HelpMenu.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAIlD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAClC,GAAG,KAAK,CAAC,SAAS,CA6DlB"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import Box from '../ink/components/Box.js';
|
|
3
3
|
import Text from '../ink/components/Text.js';
|
|
4
|
+
import { localizedDescription } from '../commands.js';
|
|
5
|
+
import { modLabel } from '../utils/modifiers.js';
|
|
4
6
|
/**
|
|
5
7
|
* The `?` help menu, ported from the leak's `PromptInputHelpMenu.tsx`
|
|
6
8
|
* (three-column shortcut layout, trimmed to the keys dsh-tui actually binds).
|
|
7
9
|
* The command column lists the merged slash-command surface: built-in
|
|
8
10
|
* commands plus plugin-registered ones from the DSH registry (plan/goal/…).
|
|
11
|
+
* Modifier labels follow the platform convention: ⌘ on macOS, ctrl elsewhere.
|
|
9
12
|
*/
|
|
10
13
|
export function HelpMenu({ commands, }) {
|
|
11
|
-
return (_jsxs(Box, { paddingX: 2, flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", width: 26, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "/ for commands" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "? for this help" }) }), _jsx(Box, { children:
|
|
14
|
+
return (_jsxs(Box, { paddingX: 2, flexDirection: "row", gap: 4, children: [_jsxs(Box, { flexDirection: "column", width: 26, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "/ for commands" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "? for this help" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "o for verbose output"] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "t to toggle context"] }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "r to search history"] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+c to interrupt" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "ctrl+d to exit" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "l to redraw"] }) })] }), _jsxs(Box, { flexDirection: "column", width: 24, flexShrink: 0, children: [_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "esc to clear input" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 for history" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "\u2190/\u2192 to move cursor" }) }), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [modLabel, "\u2190/\u2192 for word jumps"] }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "tab to complete command" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "shift+tab to cycle effort" }) })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 1, children: [_jsx(Text, { dimColor: true, children: "commands:" }), commands.map(command => (_jsx(Box, { children: _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["/", command.name, " \u2014 ", localizedDescription(command)] }) }, command.name)))] })] }));
|
|
12
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAyC5C;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBA+xBlB"}
|
|
@@ -8,6 +8,7 @@ import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
|
|
|
8
8
|
import { filterCommands, parseCommandName } from '../commands.js';
|
|
9
9
|
import { appendHistory } from '../history.js';
|
|
10
10
|
import { mentionAtCaret } from '../utils/mentions.js';
|
|
11
|
+
import { isMod } from '../utils/modifiers.js';
|
|
11
12
|
import { CommandSuggestions } from './CommandSuggestions.js';
|
|
12
13
|
import { FileSuggestions } from './FileSuggestions.js';
|
|
13
14
|
import { HelpMenu } from './HelpMenu.js';
|
|
@@ -347,10 +348,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
347
348
|
insertAtCaret(input.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
348
349
|
return;
|
|
349
350
|
}
|
|
350
|
-
// Ctrl+V: raw mode hands the key to the app, so the clipboard is
|
|
351
|
-
// here — text, or file paths when Explorer copied files (pasted
|
|
352
|
-
// insert their paths).
|
|
353
|
-
if (key
|
|
351
|
+
// Ctrl+V / Cmd+V: raw mode hands the key to the app, so the clipboard is
|
|
352
|
+
// read here — text, or file paths when Explorer copied files (pasted
|
|
353
|
+
// files insert their paths).
|
|
354
|
+
if (isMod(key) && input === 'v') {
|
|
354
355
|
if (clipboardBusyRef.current)
|
|
355
356
|
return;
|
|
356
357
|
clipboardBusyRef.current = true;
|
|
@@ -425,9 +426,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
425
426
|
submitText(line);
|
|
426
427
|
return;
|
|
427
428
|
}
|
|
428
|
-
if (key.return && key
|
|
429
|
-
// Ctrl+Enter: interrupt the running turn and process this
|
|
430
|
-
// immediately (Windows Terminal sends CSI 13;5u / 13;1;5u).
|
|
429
|
+
if (key.return && isMod(key)) {
|
|
430
|
+
// Ctrl+Enter / Cmd+Enter: interrupt the running turn and process this
|
|
431
|
+
// message immediately (Windows Terminal sends CSI 13;5u / 13;1;5u).
|
|
431
432
|
interruptSend();
|
|
432
433
|
return;
|
|
433
434
|
}
|
|
@@ -537,12 +538,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
537
538
|
setCursor(previous => Math.min(value.length, previous + 1));
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
540
|
-
if (key
|
|
541
|
+
if (isMod(key) && key.leftArrow) {
|
|
541
542
|
// Jump to the previous word boundary (readline alt+b).
|
|
542
543
|
setCursor(previous => wordBoundaryLeft(value, previous));
|
|
543
544
|
return;
|
|
544
545
|
}
|
|
545
|
-
if (key
|
|
546
|
+
if (isMod(key) && key.rightArrow) {
|
|
546
547
|
// Jump to the next word boundary (readline alt+f).
|
|
547
548
|
setCursor(previous => wordBoundaryRight(value, previous));
|
|
548
549
|
return;
|
|
@@ -572,31 +573,31 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
572
573
|
setCursor(nextLine === -1 ? value.length : nextLine);
|
|
573
574
|
return;
|
|
574
575
|
}
|
|
575
|
-
if (key
|
|
576
|
+
if (isMod(key) && input === 'a') {
|
|
576
577
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
577
578
|
setCursor(lineStart);
|
|
578
579
|
return;
|
|
579
580
|
}
|
|
580
|
-
if (key
|
|
581
|
+
if (isMod(key) && input === 'e') {
|
|
581
582
|
const nextLine = value.indexOf('\n', cursor);
|
|
582
583
|
setCursor(nextLine === -1 ? value.length : nextLine);
|
|
583
584
|
return;
|
|
584
585
|
}
|
|
585
|
-
if (key
|
|
586
|
+
if (isMod(key) && input === 'u') {
|
|
586
587
|
// Delete to start of line.
|
|
587
588
|
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
588
589
|
setValue(value.slice(0, lineStart) + value.slice(cursor));
|
|
589
590
|
setCursor(lineStart);
|
|
590
591
|
return;
|
|
591
592
|
}
|
|
592
|
-
if (key
|
|
593
|
+
if (isMod(key) && input === 'k') {
|
|
593
594
|
// Delete to end of line.
|
|
594
595
|
const nextLine = value.indexOf('\n', cursor);
|
|
595
596
|
const end = nextLine === -1 ? value.length : nextLine;
|
|
596
597
|
setValue(value.slice(0, cursor) + value.slice(end));
|
|
597
598
|
return;
|
|
598
599
|
}
|
|
599
|
-
if (key
|
|
600
|
+
if (isMod(key) && input === 'w') {
|
|
600
601
|
// Delete the word before the cursor (CC/readline behavior): skip
|
|
601
602
|
// trailing whitespace, then the whitespace-delimited word.
|
|
602
603
|
const before = value.slice(0, cursor);
|
|
@@ -678,7 +679,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
678
679
|
onToggleHelp();
|
|
679
680
|
return;
|
|
680
681
|
}
|
|
681
|
-
if (input && !key.ctrl && !key.meta && !key.tab && !key.escape) {
|
|
682
|
+
if (input && !key.ctrl && !key.meta && !key.super && !key.tab && !key.escape) {
|
|
682
683
|
// Typing anything else dismisses the help menu (CC behavior).
|
|
683
684
|
if (helpOpen)
|
|
684
685
|
onToggleHelp();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The approval panel — Claude Code style permission prompt for the DSH
|
|
3
|
+
* approval seam (`ctx.approval`). One ask per panel: a permission-colored
|
|
4
|
+
* divider header naming the tool, the gated command recovered from the
|
|
5
|
+
* paired tool call (CC verbose full-command semantics), the asker's reason,
|
|
6
|
+
* "Do you want to proceed?", and a numbered Yes/No list.
|
|
7
|
+
*
|
|
8
|
+
* The protocol's outcome set is closed (allowed-once / rejected /
|
|
9
|
+
* cancelled / unavailable) with no allow-always or feedback channel, so
|
|
10
|
+
* the panel deliberately offers exactly two rows; Esc and Ctrl+C reject
|
|
11
|
+
* (fail closed, CC's "Esc to cancel" semantics).
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import type { ApprovalSnapshot } from '../../approvals.js';
|
|
15
|
+
export type ApprovalPanelProps = {
|
|
16
|
+
/** The approval to render (from the ApprovalStore snapshot). */
|
|
17
|
+
readonly approval: ApprovalSnapshot;
|
|
18
|
+
readonly onDecide: (outcome: 'allowed-once' | 'rejected') => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function ApprovalPanel({ approval, onDecide }: ApprovalPanelProps): React.ReactNode;
|
|
21
|
+
//# sourceMappingURL=ApprovalPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApprovalPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/approvals/ApprovalPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,KAAK,IAAI,CAAA;CAClE,CAAA;AAID,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,kBAAkB,GAAG,KAAK,CAAC,SAAS,CAmEzF"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The approval panel — Claude Code style permission prompt for the DSH
|
|
4
|
+
* approval seam (`ctx.approval`). One ask per panel: a permission-colored
|
|
5
|
+
* divider header naming the tool, the gated command recovered from the
|
|
6
|
+
* paired tool call (CC verbose full-command semantics), the asker's reason,
|
|
7
|
+
* "Do you want to proceed?", and a numbered Yes/No list.
|
|
8
|
+
*
|
|
9
|
+
* The protocol's outcome set is closed (allowed-once / rejected /
|
|
10
|
+
* cancelled / unavailable) with no allow-always or feedback channel, so
|
|
11
|
+
* the panel deliberately offers exactly two rows; Esc and Ctrl+C reject
|
|
12
|
+
* (fail closed, CC's "Esc to cancel" semantics).
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { t } from '../../i18n.js';
|
|
16
|
+
import { Box, Text, useInput } from '../../ui.js';
|
|
17
|
+
import { Divider } from '../design-system/Divider.js';
|
|
18
|
+
import { POINTER } from '../../cc/figures.js';
|
|
19
|
+
const OUTCOMES = ['allowed-once', 'rejected'];
|
|
20
|
+
export function ApprovalPanel({ approval, onDecide }) {
|
|
21
|
+
const [focusIndex, setFocusIndex] = React.useState(0);
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (key.escape || (key.ctrl && input === 'c')) {
|
|
24
|
+
onDecide('rejected');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (key.upArrow) {
|
|
28
|
+
setFocusIndex(index => (index + OUTCOMES.length - 1) % OUTCOMES.length);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (key.downArrow) {
|
|
32
|
+
setFocusIndex(index => (index + 1) % OUTCOMES.length);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (input === '1' || input === '2') {
|
|
36
|
+
onDecide(OUTCOMES[Number(input) - 1]);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.return) {
|
|
40
|
+
onDecide(OUTCOMES[focusIndex]);
|
|
41
|
+
}
|
|
42
|
+
}, { isActive: true });
|
|
43
|
+
const optionLabels = [t('approval-yes'), t('approval-no')];
|
|
44
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 2, paddingRight: 2, width: "100%", children: [_jsx(Divider, { color: "permission", title: t('approval-waiting', { tool: approval.toolName }), padding: 4 }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [approval.command !== undefined && (_jsx(Box, { flexDirection: "column", paddingX: 2, children: _jsx(Text, { dimColor: true, wrap: "wrap", children: approval.command }) })), approval.reason !== undefined && (_jsx(Text, { dimColor: true, wrap: "wrap", children: approval.reason })), _jsx(Text, { dimColor: true, children: t('approval-proceed') })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: optionLabels.map((label, index) => {
|
|
45
|
+
const focused = index === focusIndex;
|
|
46
|
+
return (_jsxs(Box, { flexDirection: "row", marginTop: focused ? 1 : 0, children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { color: focused ? 'claude' : undefined, bold: focused, children: focused ? POINTER : ' ' }) }), _jsxs(Text, { bold: focused, color: focused ? 'claude' : undefined, wrap: "wrap", children: [index + 1, ". ", label] })] }, label));
|
|
47
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('approval-hint') }) })] }));
|
|
48
|
+
}
|
|
@@ -24,6 +24,12 @@ export type AskUserQuestionPanelProps = {
|
|
|
24
24
|
readonly description?: string;
|
|
25
25
|
}>;
|
|
26
26
|
readonly multiSelect?: boolean;
|
|
27
|
+
/** Presentation intent tag (rc.6): 'plan-review' switches to the
|
|
28
|
+
* decision-card layout; an intent never changes the protocol. */
|
|
29
|
+
readonly intent?: {
|
|
30
|
+
readonly kind: 'plan-review';
|
|
31
|
+
readonly approve: string;
|
|
32
|
+
};
|
|
27
33
|
};
|
|
28
34
|
/** 1-based position within the batch (progress header). */
|
|
29
35
|
readonly position: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"AskUserQuestionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/AskUserQuestionPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAO3D,MAAM,MAAM,yBAAyB,GAAG;IACtC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;QAC9B;0EACkE;QAClE,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,yBAAyB,GAAG,KAAK,CAAC,SAAS,CAoT7C"}
|
|
@@ -17,10 +17,17 @@ import { t } from '../../i18n.js';
|
|
|
17
17
|
import { Box, Text, useInput } from '../../ui.js';
|
|
18
18
|
import { Divider } from '../design-system/Divider.js';
|
|
19
19
|
import { POINTER } from '../../cc/figures.js';
|
|
20
|
+
import { PlanReviewPanel } from './PlanReviewPanel.js';
|
|
20
21
|
const CHECKED = '◉';
|
|
21
22
|
const UNCHECKED = '○';
|
|
22
23
|
const PENCIL = '✎';
|
|
23
24
|
export function AskUserQuestionPanel({ question, position, total, answered, onAnswer, onCancel, }) {
|
|
25
|
+
// Plan-mode's exit_plan_mode ask carries a presentation intent: render
|
|
26
|
+
// the CC-style decision card instead of the generic questionnaire. The
|
|
27
|
+
// branch precedes every hook so hook order stays stable per remount key.
|
|
28
|
+
if (question.intent?.kind === 'plan-review') {
|
|
29
|
+
return _jsx(PlanReviewPanel, { question: question, onAnswer: onAnswer, onCancel: onCancel });
|
|
30
|
+
}
|
|
24
31
|
const options = question.options ?? [];
|
|
25
32
|
const multiSelect = question.multiSelect === true;
|
|
26
33
|
/** Rows: the real options plus the inline input row at the tail. */
|
|
@@ -147,7 +154,7 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
|
|
|
147
154
|
setCustomCursor(customText.length);
|
|
148
155
|
return;
|
|
149
156
|
}
|
|
150
|
-
if (!key.ctrl && !key.meta && input) {
|
|
157
|
+
if (!key.ctrl && !key.meta && !key.super && input) {
|
|
151
158
|
setCustomText(text => text.slice(0, customCursor) + input + text.slice(customCursor));
|
|
152
159
|
setCustomCursor(cursor => cursor + input.length);
|
|
153
160
|
setError(null);
|
|
@@ -191,7 +198,7 @@ export function AskUserQuestionPanel({ question, position, total, answered, onAn
|
|
|
191
198
|
}
|
|
192
199
|
// Typing on an option appends into the input row; single-select also
|
|
193
200
|
// attaches this option's label so Enter carries label + text (#9).
|
|
194
|
-
if (!key.ctrl && !key.meta && input) {
|
|
201
|
+
if (!key.ctrl && !key.meta && !key.super && input) {
|
|
195
202
|
appendText(input);
|
|
196
203
|
if (!multiSelect)
|
|
197
204
|
setAttached(options[focusIndex]?.label ?? null);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan-review panel — Claude Code style exit-plan-mode decision card
|
|
3
|
+
* for the DSH user-interaction seam. plan-mode's `exit_plan_mode` tool asks
|
|
4
|
+
* through `ctx.userQuestions` with `intent: { kind: 'plan-review',
|
|
5
|
+
* approve }`: the plan markdown arrives in `detail`, the approve/decline
|
|
6
|
+
* choices in `options` (labels verbatim — the protocol answers with the
|
|
7
|
+
* asker's own labels).
|
|
8
|
+
*
|
|
9
|
+
* Protocol-exact answer mapping (dsh-plan-mode):
|
|
10
|
+
* - Approve: `{ selected: [intent.approve] }` — custom MUST be absent, or
|
|
11
|
+
* plan-mode treats it as keep-planning-with-feedback.
|
|
12
|
+
* - Keep planning / feedback: `{ selected: [declineLabel], custom? }` where
|
|
13
|
+
* declineLabel is the first option that is not the approve label.
|
|
14
|
+
* - Esc / Ctrl+C: the store rejects with ASK_CANCELLED, which plan-mode
|
|
15
|
+
* reads as "the user dismissed the review to speak instead".
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import type { QuestionSelection } from '../../questions.js';
|
|
19
|
+
export type PlanReviewPanelProps = {
|
|
20
|
+
/** The plan-review question (intent.kind === 'plan-review'). */
|
|
21
|
+
readonly question: {
|
|
22
|
+
readonly question: string;
|
|
23
|
+
readonly header?: string;
|
|
24
|
+
readonly detail?: string;
|
|
25
|
+
readonly options?: ReadonlyArray<{
|
|
26
|
+
readonly label: string;
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
}>;
|
|
29
|
+
readonly intent?: {
|
|
30
|
+
readonly kind: 'plan-review';
|
|
31
|
+
readonly approve: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
readonly onAnswer: (selection: QuestionSelection) => void;
|
|
35
|
+
/** Esc / Ctrl+C — dismissed to speak instead (ASK_CANCELLED). */
|
|
36
|
+
readonly onCancel: () => void;
|
|
37
|
+
};
|
|
38
|
+
export declare function PlanReviewPanel({ question, onAnswer, onCancel, }: PlanReviewPanelProps): React.ReactNode;
|
|
39
|
+
//# sourceMappingURL=PlanReviewPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlanReviewPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/PlanReviewPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAI3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE;QACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;QACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;QACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;YAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE;YAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;YAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAC7E,CAAA;IACD,QAAQ,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACzD,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAC9B,CAAA;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,GAAG,KAAK,CAAC,SAAS,CAgOxC"}
|