@itookit/dsht 0.2.1 → 0.2.4
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.i18n.yaml +2 -2
- package/README.md +47 -26
- package/README.zh.md +47 -26
- package/dist/app.d.ts +16 -1
- package/dist/app.js +450 -73
- package/dist/cli.js +6 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +3 -0
- package/dist/controller.d.ts +80 -3
- package/dist/controller.js +350 -75
- package/dist/cost-view.js +3 -1
- package/dist/history-view.d.ts +8 -0
- package/dist/history-view.js +12 -0
- package/dist/history.d.ts +29 -12
- package/dist/history.js +172 -27
- package/dist/input-history.d.ts +19 -0
- package/dist/input-history.js +43 -0
- package/dist/input.d.ts +3 -1
- package/dist/input.js +3 -1
- package/dist/memory.d.ts +13 -0
- package/dist/memory.js +22 -0
- package/dist/mouse.d.ts +3 -1
- package/dist/mouse.js +15 -4
- package/dist/navigation.d.ts +1 -1
- package/dist/navigation.js +2 -2
- package/dist/status.d.ts +3 -1
- package/dist/status.js +63 -42
- package/dist/telemetry.d.ts +3 -0
- package/dist/telemetry.js +9 -0
- package/dist/theme.d.ts +15 -0
- package/dist/theme.js +15 -0
- package/dist/transcript.d.ts +91 -6
- package/dist/transcript.js +393 -49
- package/dsht.png +0 -0
- package/package.json +4 -2
package/dist/app.js
CHANGED
|
@@ -1,47 +1,148 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/** Ink terminal interface: startup pickers, transcript, and slash-command composer. */
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { HistoryViewport } from "./history-view.js";
|
|
4
|
+
import { mocha, ThemeContext, useTheme } from "./theme.js";
|
|
5
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, memo, createContext, useContext } from 'react';
|
|
6
|
+
import { Box, Text, measureElement, useApp, useInput, useStdin, useStdout } from 'ink';
|
|
5
7
|
import { useMouseWheel } from "./mouse.js";
|
|
6
8
|
import { TextInput } from "./input.js";
|
|
7
|
-
import {
|
|
9
|
+
import { InputHistory } from "./input-history.js";
|
|
10
|
+
import { historyLayout, releaseHistoryLayout } from "./history.js";
|
|
8
11
|
import { toolLine } from "./transcript.js";
|
|
9
12
|
import { activeReference, fileMention } from "./references.js";
|
|
10
13
|
import { CostPanel } from "./cost-view.js";
|
|
11
14
|
import { StatusBar } from "./status.js";
|
|
12
15
|
import { navigationCommand, sessionLabel } from "./navigation.js";
|
|
13
16
|
import { array, errorText, object, safeText, string } from "./wire.js";
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
/** Command discovery catalog shared by Tab completion and the `/help` panel. */
|
|
18
|
+
export const COMMAND_HINTS = [
|
|
19
|
+
{ command: '/ws', usage: '[name or ID]', description: 'List/switch workspaces; --delete name removes registration' },
|
|
20
|
+
{ command: '/resume', usage: '[title or ID]', description: 'List/switch sessions; --delete ID archives with confirmation' },
|
|
21
|
+
{ command: '/model', usage: '[provider model [effort]]', description: 'Choose a model and reasoning effort for subsequent requests' },
|
|
22
|
+
{ command: '/new', description: 'Create a session in the selected workspace' },
|
|
23
|
+
{ command: '/copy', description: 'Freeze for native selection; Esc resumes (Ctrl+S shortcut)' },
|
|
24
|
+
{ command: '/latest', description: 'Return to the live conversation' },
|
|
25
|
+
{ command: '/older', description: 'Load earlier history' },
|
|
26
|
+
{ command: '/history', usage: '[text]', description: 'List your prompts, optionally filtered' },
|
|
27
|
+
{ command: '/search', usage: 'text', description: 'Search history page by page and open a match' },
|
|
28
|
+
{ command: '/ssearch', usage: 'text', description: 'Search sessions in the current workspace' },
|
|
29
|
+
{ command: '/wsearch', usage: 'text', description: 'Search sessions across all workspaces' },
|
|
30
|
+
{ command: '/cancel', description: 'Cancel the active turn' },
|
|
31
|
+
{ command: '/steer', usage: 'text', description: 'Send steering input to the active turn' },
|
|
32
|
+
{ command: '/allow', description: 'Approve the pending request once' },
|
|
33
|
+
{ command: '/deny', description: 'Reject the pending request' },
|
|
34
|
+
{ command: '/status', description: 'Show full session status details' },
|
|
35
|
+
{ command: '/cost', description: 'Show cost estimates and refresh usage' },
|
|
36
|
+
{ command: '/think', usage: '[seq or live]', description: 'Inspect reasoning with user prompt summaries' },
|
|
37
|
+
{ command: '/help', description: 'Show this command list' },
|
|
38
|
+
{ command: '/quit', description: 'Exit dsht' },
|
|
39
|
+
];
|
|
40
|
+
const COMMANDS = COMMAND_HINTS.map(hint => hint.command);
|
|
41
|
+
/** Command column text per hint, aligned in the `/help` panel. */
|
|
42
|
+
const COMMAND_LABELS = COMMAND_HINTS.map(hint => hint.usage === undefined ? hint.command : `${hint.command} ${hint.usage}`);
|
|
43
|
+
/** Widest command column, so descriptions start on one column. */
|
|
44
|
+
const COMMAND_LABEL_WIDTH = Math.max(...COMMAND_LABELS.map(label => label.length)) + 2;
|
|
45
|
+
/** Longest common prefix of the candidate commands, so Tab can extend an ambiguous draft. */
|
|
46
|
+
export function commonPrefix(values) {
|
|
47
|
+
let prefix = values[0] ?? '';
|
|
48
|
+
for (const value of values) {
|
|
49
|
+
let index = 0;
|
|
50
|
+
while (index < prefix.length && index < value.length && prefix[index] === value[index])
|
|
51
|
+
index++;
|
|
52
|
+
prefix = prefix.slice(0, index);
|
|
53
|
+
}
|
|
54
|
+
return prefix;
|
|
55
|
+
}
|
|
56
|
+
/** Transient notices expire; interactive panels remain open until dismissed. */
|
|
17
57
|
const PANEL_LIFETIME_MS = 10_000;
|
|
18
|
-
|
|
58
|
+
const CopyMode = createContext(false);
|
|
59
|
+
/** Retain rendered children while frozen; local dialog interactions remain outside this wrapper. */
|
|
60
|
+
const Frozen = memo(function Frozen({ children }) {
|
|
61
|
+
return _jsx(_Fragment, { children: children });
|
|
62
|
+
}, (previous, next) => previous.frozen && next.frozen && previous.identity === next.identity);
|
|
63
|
+
function Picker({ choices, enabled, canSelect, pageSize = 12 }) {
|
|
64
|
+
const theme = useTheme();
|
|
65
|
+
const copyMode = useContext(CopyMode);
|
|
19
66
|
const [selected, setSelected] = useState(0);
|
|
20
67
|
const current = Math.min(selected, choices.length - 1);
|
|
68
|
+
const { internal_eventEmitter } = useStdin();
|
|
69
|
+
const rawKey = useRef('');
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const remember = (raw) => { rawKey.current = raw; };
|
|
72
|
+
internal_eventEmitter.prependListener('input', remember);
|
|
73
|
+
return () => { internal_eventEmitter.removeListener('input', remember); };
|
|
74
|
+
}, [internal_eventEmitter]);
|
|
21
75
|
useInput((_input, key) => {
|
|
22
|
-
if (!canSelect())
|
|
76
|
+
if (!canSelect() || key.eventType === 'release')
|
|
23
77
|
return;
|
|
24
78
|
if (key.upArrow)
|
|
25
79
|
setSelected(Math.max(0, current - 1));
|
|
26
80
|
else if (key.downArrow)
|
|
27
81
|
setSelected(Math.min(choices.length - 1, current + 1));
|
|
82
|
+
else if (_input === 'd' && !key.ctrl && !key.meta || key.delete && /^\x1b\[3(?:;\d+)?~$/.test(rawKey.current))
|
|
83
|
+
choices[current]?.remove?.();
|
|
28
84
|
else if (key.return)
|
|
29
85
|
choices[current]?.action();
|
|
30
|
-
}, { isActive: enabled });
|
|
31
|
-
const start = Math.max(0, current -
|
|
32
|
-
return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start +
|
|
86
|
+
}, { isActive: enabled && !copyMode });
|
|
87
|
+
const start = Math.max(0, current - Math.max(0, pageSize - 1));
|
|
88
|
+
return _jsxs(Box, { flexDirection: "column", children: [choices.slice(start, start + pageSize).map((choice, index) => _jsxs(Text, { color: start + index === current ? theme.accent : undefined, children: [start + index === current ? '❯ ' : ' ', safeText(choice.label)] }, choice.key)), _jsxs(Text, { dimColor: true, children: ["\u2191 \u2193 select \u00B7 Enter open", choices.some(choice => choice.remove) ? ' · d/Delete remove / archive' : '', " \u00B7 Ctrl+C stop / exit"] })] });
|
|
33
89
|
}
|
|
34
90
|
/** The caller owns starting and stopping the controller around the Ink render lifetime. */
|
|
35
|
-
export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
91
|
+
export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = mocha }) {
|
|
36
92
|
const state = useSyncExternalStore(controller.subscribe, controller.snapshot);
|
|
93
|
+
const presetId = controller.telemetry.view(state.sessionId).values.agentPreset;
|
|
94
|
+
useEffect(() => { if (typeof presetId === 'string')
|
|
95
|
+
controller.loadPresetNames(); }, [controller, state.online, presetId]);
|
|
37
96
|
const { exit } = useApp();
|
|
38
97
|
const { stdout } = useStdout();
|
|
98
|
+
const [copyMode, setCopyMode] = useState(false);
|
|
39
99
|
const [input, updateInput] = useState('');
|
|
40
100
|
const draft = useRef('');
|
|
101
|
+
const inputHistory = useRef(new InputHistory());
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
|
+
const history = new InputHistory();
|
|
104
|
+
inputHistory.current = history;
|
|
105
|
+
if (!state.transcript.ready)
|
|
106
|
+
return;
|
|
107
|
+
const messages = state.transcript.messagesForWidth(Math.max(20, (stdout.columns ?? 100) - 2));
|
|
108
|
+
// Seed once per loaded session, never scan historical messages on stream ticks or arrow presses.
|
|
109
|
+
const prompts = messages.filter(message => message.role === 'You').slice(-200);
|
|
110
|
+
for (const message of prompts)
|
|
111
|
+
history.record(message.text.replace(/\r?\n/g, ' ').trim());
|
|
112
|
+
}, [state.sessionId, state.transcript, state.transcript.ready]);
|
|
41
113
|
const [cursor, setCursor] = useState(0);
|
|
42
114
|
// Input callbacks may run before Ink refreshes the controlled field's listener.
|
|
43
|
-
const setInput = (value
|
|
115
|
+
const setInput = (value, recalled = false) => {
|
|
116
|
+
if (!recalled)
|
|
117
|
+
inputHistory.current.reset();
|
|
118
|
+
draft.current = value;
|
|
119
|
+
updateInput(value);
|
|
120
|
+
setCursor(value.length);
|
|
121
|
+
};
|
|
122
|
+
const [historyWindow, setHistoryWindow] = useState();
|
|
123
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
124
|
+
const [historyMatches, setHistoryMatches] = useState();
|
|
125
|
+
const displayTranscript = historyWindow ?? state.transcript;
|
|
126
|
+
const displayRef = useRef(displayTranscript);
|
|
127
|
+
displayRef.current = displayTranscript;
|
|
128
|
+
useEffect(() => () => { if (historyWindow) {
|
|
129
|
+
releaseHistoryLayout(historyWindow);
|
|
130
|
+
historyWindow.dispose();
|
|
131
|
+
} }, [historyWindow]);
|
|
132
|
+
const conversationBox = useRef(null);
|
|
133
|
+
const [conversationRows, setConversationRows] = useState(20);
|
|
134
|
+
useLayoutEffect(() => {
|
|
135
|
+
if (conversationBox.current) {
|
|
136
|
+
const height = Math.floor(measureElement(conversationBox.current).height);
|
|
137
|
+
if (height !== conversationRows)
|
|
138
|
+
setConversationRows(height);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
44
141
|
const [scroll, setScroll] = useState(0);
|
|
142
|
+
const [liveReasoning, setLiveReasoning] = useState('row');
|
|
143
|
+
const [removal, setRemoval] = useState();
|
|
144
|
+
const [models, setModels] = useState();
|
|
145
|
+
const [thoughtList, setThoughtList] = useState(false);
|
|
45
146
|
const [historyQuery, setHistoryQuery] = useState();
|
|
46
147
|
const [contentSearch, setContentSearch] = useState(false);
|
|
47
148
|
const [searchResults, setSearchResults] = useState();
|
|
@@ -49,23 +150,33 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
49
150
|
useEffect(() => () => historyAbort.current?.abort(), []);
|
|
50
151
|
const [costExpanded, setCostExpanded] = useState(false);
|
|
51
152
|
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
153
|
+
const [reasoningOverrides, setReasoningOverrides] = useState(new Set());
|
|
154
|
+
const reasoning = 'row';
|
|
155
|
+
useEffect(() => { setReasoningOverrides(new Set()); setModels(undefined); setThoughtList(false); setHistoryWindow(undefined); setHistoryMatches(undefined); }, [state.transcript]);
|
|
156
|
+
useEffect(() => { setLiveReasoning('row'); }, [state.transcript, state.transcript.liveAttemptKey]);
|
|
157
|
+
const [notice, setNotice] = useState();
|
|
52
158
|
const [help, setHelp] = useState(false);
|
|
53
159
|
const [answers, setAnswers] = useState({});
|
|
160
|
+
const [optionState, setOptionState] = useState();
|
|
54
161
|
const [referenceIndex, setReferenceIndex] = useState(0);
|
|
55
162
|
const [dismissedReference, dismissReference] = useState();
|
|
56
163
|
const [lookup, setLookup] = useState();
|
|
57
|
-
//
|
|
164
|
+
// Interactive panels remain stable while read; only transient notices expire.
|
|
58
165
|
useEffect(() => {
|
|
59
|
-
if (
|
|
166
|
+
if (copyMode)
|
|
167
|
+
return;
|
|
168
|
+
if (notice === undefined)
|
|
60
169
|
return;
|
|
61
|
-
const timer = setTimeout(() =>
|
|
170
|
+
const timer = setTimeout(() => setNotice(undefined), panelLifetimeMs);
|
|
62
171
|
return () => clearTimeout(timer);
|
|
63
|
-
}, [
|
|
172
|
+
}, [notice, panelLifetimeMs, copyMode]);
|
|
64
173
|
const pending = state.pending[0];
|
|
65
174
|
const token = state.screen === 'chat' && state.online && !state.busy && !pending
|
|
66
175
|
&& (!input.startsWith('/') || input.startsWith('/steer ')) && dismissedReference !== input && cursor === input.length
|
|
67
176
|
? activeReference(input) : undefined;
|
|
68
177
|
const referenceOpen = token !== undefined;
|
|
178
|
+
const dialogOpen = !!(removal || models || thoughtList || historyQuery !== undefined || searchResults || costExpanded || statusExpanded || help || pending || referenceOpen || state.screen !== 'chat');
|
|
179
|
+
const displayPaused = copyMode || dialogOpen;
|
|
69
180
|
const matches = referenceOpen && lookup?.draft === input && lookup.sessionId === state.sessionId ? lookup : undefined;
|
|
70
181
|
useEffect(() => {
|
|
71
182
|
if (!referenceOpen)
|
|
@@ -89,16 +200,130 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
89
200
|
const mention = fileMention(candidate, token.quoted);
|
|
90
201
|
setInput(input.slice(0, -token.prefix.length) + mention + (candidate.kind === 'file' ? ' ' : ''));
|
|
91
202
|
};
|
|
203
|
+
/** Complete the leading slash command; an ambiguous draft extends to the shared prefix. */
|
|
204
|
+
const completeCommand = () => {
|
|
205
|
+
if (!input.startsWith('/') || input.includes(' '))
|
|
206
|
+
return;
|
|
207
|
+
const matches = COMMANDS.filter(command => command.startsWith(input));
|
|
208
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
209
|
+
if (only !== undefined) {
|
|
210
|
+
setInput(`${only} `);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const prefix = commonPrefix(matches);
|
|
214
|
+
if (prefix.length > input.length)
|
|
215
|
+
setInput(prefix);
|
|
216
|
+
};
|
|
92
217
|
const questions = pending?.event === 'user-questions/request' ? array(object(pending.request).questions).map(object) : [];
|
|
93
218
|
const eventId = pending ? string(pending.eventId) : '';
|
|
94
219
|
const answered = answers[eventId] ?? [];
|
|
95
220
|
const question = questions[answered.length];
|
|
221
|
+
const optionKey = `${eventId}:${answered.length}`;
|
|
222
|
+
const options = question ? array(question.options ?? []).map(object) : [];
|
|
223
|
+
const choiceState = optionState?.key === optionKey ? optionState : { key: optionKey, cursor: 0, selected: [], custom: false };
|
|
224
|
+
const optionCursor = Math.min(choiceState.cursor, options.length);
|
|
225
|
+
// Reserve the header, composer and question instructions; each choice may have a description.
|
|
226
|
+
const optionPageSize = Math.max(1, Math.min(6, Math.floor(((stdout.rows ?? 30) - 16) / 2)));
|
|
227
|
+
const optionStart = Math.max(0, optionCursor - optionPageSize + 1);
|
|
228
|
+
const questionKeysActive = !!question && options.length > 0 && !choiceState.custom && !copyMode
|
|
229
|
+
&& !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded;
|
|
230
|
+
const answerQuestion = async (selected, custom) => {
|
|
231
|
+
if (controller.state.pending[0]?.eventId !== eventId)
|
|
232
|
+
throw new Error('The pending question has changed');
|
|
233
|
+
const answer = { id: string(question.id), selected, ...(custom ? { custom } : {}) };
|
|
234
|
+
const next = [...answered, answer];
|
|
235
|
+
if (next.length === questions.length) {
|
|
236
|
+
await controller.answer({ answers: next });
|
|
237
|
+
setAnswers(previous => { const rest = { ...previous }; delete rest[eventId]; return rest; });
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
setAnswers(previous => ({ ...previous, [eventId]: next }));
|
|
241
|
+
setOptionState(undefined);
|
|
242
|
+
};
|
|
96
243
|
const operate = (fn) => { void controller.perform(fn); };
|
|
244
|
+
const requestRemoval = async (kind, query) => {
|
|
245
|
+
const target = await controller.removalTarget(kind, query);
|
|
246
|
+
if (target.kind === 'session' && target.empty)
|
|
247
|
+
await controller.removeTarget(target);
|
|
248
|
+
else
|
|
249
|
+
setRemoval(target);
|
|
250
|
+
};
|
|
97
251
|
useInput((_value, key) => {
|
|
252
|
+
if (key.eventType === 'release')
|
|
253
|
+
return;
|
|
254
|
+
if (copyMode) {
|
|
255
|
+
if (key.escape || key.ctrl && (_value === 's' || _value === 'c'))
|
|
256
|
+
setCopyMode(false);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (key.ctrl && _value === 's') {
|
|
260
|
+
setCopyMode(true);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
98
263
|
if ((key.escape || key.ctrl && _value === 'c') && historyAbort.current) {
|
|
99
264
|
historyAbort.current.abort();
|
|
100
265
|
return;
|
|
101
266
|
}
|
|
267
|
+
if ((key.escape || key.ctrl && _value === 'c') && controller.state.pending.length) {
|
|
268
|
+
if (key.ctrl && draft.current)
|
|
269
|
+
setInput('');
|
|
270
|
+
if (key.escape) {
|
|
271
|
+
setOptionState({ ...choiceState, custom: false });
|
|
272
|
+
setRemoval(undefined);
|
|
273
|
+
setModels(undefined);
|
|
274
|
+
setThoughtList(false);
|
|
275
|
+
setSearchResults(undefined);
|
|
276
|
+
setHistoryQuery(undefined);
|
|
277
|
+
setHistoryMatches(undefined);
|
|
278
|
+
setHelp(false);
|
|
279
|
+
setCostExpanded(false);
|
|
280
|
+
setStatusExpanded(false);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (questionKeysActive && !draft.current && !controller.state.busy && !key.ctrl && !key.meta) {
|
|
285
|
+
const digit = /^[1-9]$/.test(_value) ? Number(_value) - 1 : -1;
|
|
286
|
+
if (key.upArrow || key.downArrow) {
|
|
287
|
+
setOptionState({ ...choiceState, cursor: Math.max(0, Math.min(options.length, optionCursor + (key.upArrow ? -1 : 1))) });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (digit >= 0 && digit <= options.length || _value === ' ' && question.multiSelect === true && optionCursor < options.length) {
|
|
291
|
+
const index = digit >= 0 ? digit : optionCursor;
|
|
292
|
+
const label = index < options.length ? string(options[index].label) : undefined;
|
|
293
|
+
const selected = question.multiSelect === true && label
|
|
294
|
+
? choiceState.selected.includes(label) ? choiceState.selected.filter(item => item !== label) : [...choiceState.selected, label]
|
|
295
|
+
: choiceState.selected;
|
|
296
|
+
setOptionState({ ...choiceState, cursor: index, selected });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (key.return) {
|
|
300
|
+
if (optionCursor === options.length) {
|
|
301
|
+
setOptionState({ ...choiceState, custom: true });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const selected = question.multiSelect === true ? choiceState.selected : [string(options[optionCursor].label)];
|
|
305
|
+
if (!selected.length) {
|
|
306
|
+
setNotice('Select at least one option with Space or a number');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
operate(() => answerQuestion(selected));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (key.escape && removal) {
|
|
314
|
+
setRemoval(undefined);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (key.escape && models) {
|
|
318
|
+
setModels(undefined);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (key.escape && thoughtList) {
|
|
322
|
+
setThoughtList(false);
|
|
323
|
+
if (controller.running)
|
|
324
|
+
void controller.interrupt(true);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
102
327
|
if (key.escape && searchResults) {
|
|
103
328
|
setSearchResults(undefined);
|
|
104
329
|
if (controller.running)
|
|
@@ -107,11 +332,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
107
332
|
}
|
|
108
333
|
if (key.escape && historyQuery !== undefined) {
|
|
109
334
|
setHistoryQuery(undefined);
|
|
335
|
+
setHistoryMatches(undefined);
|
|
110
336
|
if (controller.running)
|
|
111
337
|
void controller.interrupt(true);
|
|
112
338
|
return;
|
|
113
339
|
}
|
|
114
340
|
if (key.ctrl && _value === 'c') {
|
|
341
|
+
// A draft clears first, exactly like a shell prompt; an empty draft still stops or exits.
|
|
342
|
+
if (input !== '') {
|
|
343
|
+
setInput('');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
115
346
|
void controller.interrupt().then(shouldExit => { if (shouldExit)
|
|
116
347
|
exit(); });
|
|
117
348
|
return;
|
|
@@ -130,6 +361,27 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
130
361
|
setReferenceIndex(value => Math.max(0, Math.min((matches?.items.length ?? 1) - 1, value + 1)));
|
|
131
362
|
return;
|
|
132
363
|
}
|
|
364
|
+
const recallPrevious = key.upArrow || key.ctrl && _value === 'p';
|
|
365
|
+
const recallNext = key.downArrow || key.ctrl && _value === 'n';
|
|
366
|
+
if ((recallPrevious || recallNext) && state.online && !controller.state.busy && !pending
|
|
367
|
+
&& !removal && !models && !thoughtList && historyQuery === undefined && !searchResults && !help && !costExpanded && !statusExpanded
|
|
368
|
+
&& (state.screen === 'chat' || draft.current !== '' || key.ctrl)) {
|
|
369
|
+
setInput(inputHistory.current.move(recallPrevious ? -1 : 1, draft.current), true);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (key.tab) {
|
|
373
|
+
completeCommand();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (key.escape && (help || costExpanded || statusExpanded || notice !== undefined)) {
|
|
377
|
+
setHelp(false);
|
|
378
|
+
setCostExpanded(false);
|
|
379
|
+
setStatusExpanded(false);
|
|
380
|
+
setNotice(undefined);
|
|
381
|
+
if (controller.running)
|
|
382
|
+
void controller.interrupt(true);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
133
385
|
if (key.escape && state.screen === 'chat') {
|
|
134
386
|
void controller.interrupt(true);
|
|
135
387
|
}
|
|
@@ -143,16 +395,30 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
143
395
|
pickReference();
|
|
144
396
|
return;
|
|
145
397
|
}
|
|
398
|
+
if (copyMode)
|
|
399
|
+
return;
|
|
146
400
|
const value = raw.trim();
|
|
147
401
|
if (!value)
|
|
148
402
|
return;
|
|
403
|
+
if (!pending)
|
|
404
|
+
inputHistory.current.record(value);
|
|
405
|
+
if (value === '/copy') {
|
|
406
|
+
setInput('');
|
|
407
|
+
setCopyMode(true);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
setRemoval(undefined);
|
|
149
411
|
// Each panel belongs to the command that opened it, so any other command closes it.
|
|
412
|
+
if (!/^\/model(?: |$)/.test(value))
|
|
413
|
+
setModels(undefined);
|
|
150
414
|
if (value !== '/help')
|
|
151
415
|
setHelp(false);
|
|
152
416
|
if (value !== '/cost')
|
|
153
417
|
setCostExpanded(false);
|
|
154
418
|
if (value !== '/status')
|
|
155
419
|
setStatusExpanded(false);
|
|
420
|
+
if (!/^\/think(?: |$)/.test(value))
|
|
421
|
+
setThoughtList(false);
|
|
156
422
|
if (value === '/quit') {
|
|
157
423
|
exit();
|
|
158
424
|
return;
|
|
@@ -176,7 +442,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
176
442
|
}
|
|
177
443
|
const accepted = await controller.perform(async () => {
|
|
178
444
|
const navigation = navigationCommand(value);
|
|
179
|
-
if (navigation) {
|
|
445
|
+
if (navigation && /^--(?:delete|archive)(?:\s|$)/.test(navigation.query ?? '')) {
|
|
446
|
+
const query = navigation.query.replace(/^--(?:delete|archive)\s*/, '');
|
|
447
|
+
if (!query)
|
|
448
|
+
throw new Error('Specify the name or ID to remove');
|
|
449
|
+
await requestRemoval(navigation.kind, query);
|
|
450
|
+
}
|
|
451
|
+
else if (navigation) {
|
|
180
452
|
setHistoryQuery(undefined);
|
|
181
453
|
setSearchResults(undefined);
|
|
182
454
|
if (navigation.kind === 'workspace')
|
|
@@ -187,6 +459,31 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
187
459
|
}
|
|
188
460
|
else if (state.screen === 'path')
|
|
189
461
|
await controller.createWorkspace(value);
|
|
462
|
+
else if (value === '/latest') {
|
|
463
|
+
setHistoryWindow(undefined);
|
|
464
|
+
setHistoryMatches(undefined);
|
|
465
|
+
setHistoryQuery(undefined);
|
|
466
|
+
setSearchResults(undefined);
|
|
467
|
+
setReasoningOverrides(new Set());
|
|
468
|
+
setScroll(0);
|
|
469
|
+
controller.pinHistory(false);
|
|
470
|
+
}
|
|
471
|
+
else if (/^\/model(?: |$)/.test(value)) {
|
|
472
|
+
if (state.screen !== 'chat')
|
|
473
|
+
throw new Error('Select a session first');
|
|
474
|
+
const args = value.split(/\s+/).slice(1);
|
|
475
|
+
if (!args.length) {
|
|
476
|
+
setHistoryQuery(undefined);
|
|
477
|
+
setSearchResults(undefined);
|
|
478
|
+
setModels({ catalog: await controller.modelCatalog() });
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
if (args.length < 2 || args.length > 3)
|
|
482
|
+
throw new Error('Use /model [provider model [effort]]');
|
|
483
|
+
await controller.selectModel(args[0], args[1], args[2]);
|
|
484
|
+
setModels(undefined);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
190
487
|
else if (value === '/new')
|
|
191
488
|
await controller.createSession();
|
|
192
489
|
else if (value === '/history' || value.startsWith('/history ')) {
|
|
@@ -203,7 +500,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
203
500
|
throw new Error(`Use ${command} <text>`);
|
|
204
501
|
await historyOperation(async (signal) => {
|
|
205
502
|
if (command === '/search') {
|
|
206
|
-
await controller.
|
|
503
|
+
setHistoryMatches(await controller.searchHistory(query, signal));
|
|
207
504
|
setSearchResults(undefined);
|
|
208
505
|
setContentSearch(true);
|
|
209
506
|
setHistoryQuery(query);
|
|
@@ -215,10 +512,36 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
215
512
|
}
|
|
216
513
|
});
|
|
217
514
|
}
|
|
218
|
-
else if (
|
|
219
|
-
|
|
515
|
+
else if (/^\/think(?: |$)/.test(value)) {
|
|
516
|
+
if (state.screen !== 'chat')
|
|
517
|
+
throw new Error('Select a session first');
|
|
518
|
+
const target = value.slice(6).trim();
|
|
519
|
+
if (target === 'live') {
|
|
520
|
+
setLiveReasoning(value => value === 'row' ? 'full' : 'row');
|
|
521
|
+
setThoughtList(false);
|
|
522
|
+
setScroll(0);
|
|
523
|
+
}
|
|
524
|
+
else if (target) {
|
|
525
|
+
const seq = Number(target);
|
|
526
|
+
if (!Number.isSafeInteger(seq) || !displayTranscript.thoughts.some(entry => entry.seq === seq))
|
|
527
|
+
throw new Error('Use /think <message sequence> for a loaded reasoning block');
|
|
528
|
+
const next = new Set(reasoningOverrides);
|
|
529
|
+
if (next.delete(seq)) {
|
|
530
|
+
setReasoningOverrides(next);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
next.add(seq);
|
|
534
|
+
setReasoningOverrides(next);
|
|
535
|
+
await jumpHistory(seq, next);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
setHistoryQuery(undefined);
|
|
539
|
+
setSearchResults(undefined);
|
|
540
|
+
setThoughtList(true);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
220
543
|
else if (value === '/older') {
|
|
221
|
-
await controller.older();
|
|
544
|
+
await controller.older(undefined, displayTranscript);
|
|
222
545
|
setScroll(value => value + 10);
|
|
223
546
|
}
|
|
224
547
|
else if (value === '/cancel')
|
|
@@ -229,21 +552,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
229
552
|
await controller.approve(false);
|
|
230
553
|
else if (value.startsWith('/steer '))
|
|
231
554
|
await controller.prompt(value.slice(7), 'steer');
|
|
232
|
-
else if (question)
|
|
233
|
-
|
|
234
|
-
const next = [...answered, answer];
|
|
235
|
-
if (next.length === questions.length)
|
|
236
|
-
await controller.answer({ answers: next });
|
|
237
|
-
setAnswers(previous => ({ ...previous, [eventId]: next }));
|
|
238
|
-
}
|
|
555
|
+
else if (question)
|
|
556
|
+
await answerQuestion(question.multiSelect === true ? choiceState.selected : [], value);
|
|
239
557
|
else if (pending)
|
|
240
558
|
throw new Error('Answer the approval with /allow or /deny');
|
|
241
559
|
else if (value.startsWith('/'))
|
|
242
560
|
throw new Error('Unknown command. Use /help.');
|
|
243
561
|
else if (state.screen !== 'chat')
|
|
244
|
-
throw new Error('Choose a session or type /ws or /
|
|
562
|
+
throw new Error('Choose a session or type /ws or /resume');
|
|
245
563
|
else {
|
|
246
564
|
await controller.prompt(value);
|
|
565
|
+
setHistoryWindow(undefined);
|
|
247
566
|
setScroll(0);
|
|
248
567
|
}
|
|
249
568
|
});
|
|
@@ -253,6 +572,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
253
572
|
const choices = state.screen === 'workspaces' ? [
|
|
254
573
|
...state.workspaces.map(workspace => ({ key: string(workspace.workspaceId),
|
|
255
574
|
label: `${string(workspace.title)} ${string(workspace.path)}`,
|
|
575
|
+
remove: () => setRemoval({ kind: 'workspace', id: string(workspace.workspaceId), name: string(workspace.title), path: string(workspace.path) }),
|
|
256
576
|
action: () => controller.pickWorkspace(string(workspace.workspaceId)) })),
|
|
257
577
|
{ key: '@all', label: 'All sessions', action: () => operate(() => controller.switchSession('all')) },
|
|
258
578
|
{ key: '@new', label: '+ Add workspace (host directory)', action: () => controller.enterPath() },
|
|
@@ -260,25 +580,38 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
260
580
|
...(state.workspaceId && !state.showAllSessions ? [{ key: '@new', label: '+ New session', action: () => operate(() => controller.createSession()) }] : []),
|
|
261
581
|
...controller.visibleSessions.map(session => ({ key: string(session.sessionId),
|
|
262
582
|
label: `${session.running ? '● ' : ''}${sessionLabel(session)} ${session.sessionId}`,
|
|
583
|
+
remove: () => operate(() => requestRemoval('session', string(session.sessionId))),
|
|
263
584
|
action: () => { setScroll(0); operate(() => controller.selectSession(string(session.sessionId))); } })),
|
|
264
585
|
{ key: '@back', label: '← Workspaces', action: () => operate(() => controller.showPicker('workspaces')) },
|
|
265
586
|
];
|
|
266
587
|
const width = Math.max(10, (stdout.columns ?? 80) - 2);
|
|
267
|
-
const layout = useMemo(() => historyLayout(
|
|
268
|
-
const {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
588
|
+
const layout = useMemo(() => historyLayout(displayTranscript, width, reasoning, reasoningOverrides, liveReasoning), [displayTranscript, displayTranscript.version, width, reasoning, reasoningOverrides, liveReasoning]);
|
|
589
|
+
const { length, first } = layout;
|
|
590
|
+
const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
|
|
591
|
+
const pageSize = Math.max(1, conversationRows - (displayTranscript.hasMore || historyWindow ? 1 : 0));
|
|
592
|
+
const previousView = useRef({ transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning });
|
|
271
593
|
const previous = previousView.current;
|
|
272
594
|
const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
|
|
273
|
-
const adjustedScroll = previous.
|
|
274
|
-
: scroll > 0 && !prepended ? Math.max(0, scroll +
|
|
275
|
-
const maxScroll = Math.max(0,
|
|
595
|
+
const adjustedScroll = previous.session !== state.transcript ? 0
|
|
596
|
+
: scroll > 0 && previous.transcript === displayTranscript && !prepended && previous.folds === reasoningOverrides && previous.liveReasoning === liveReasoning ? Math.max(0, scroll + length - previous.count) : scroll;
|
|
597
|
+
const maxScroll = Math.max(0, length - pageSize);
|
|
276
598
|
const position = Math.min(adjustedScroll, maxScroll);
|
|
277
599
|
useLayoutEffect(() => {
|
|
278
|
-
previousView.current = { transcript: state.transcript, count:
|
|
600
|
+
previousView.current = { transcript: displayTranscript, session: state.transcript, count: length, first, folds: reasoningOverrides, liveReasoning };
|
|
279
601
|
if (position !== scroll)
|
|
280
602
|
setScroll(position);
|
|
281
|
-
}, [state.transcript,
|
|
603
|
+
}, [state.transcript, displayTranscript, length, position, scroll, reasoningOverrides, liveReasoning]);
|
|
604
|
+
useLayoutEffect(() => {
|
|
605
|
+
controller.pinHistory(!historyWindow && (position > 0 || thoughtList || historyQuery !== undefined && !contentSearch));
|
|
606
|
+
}, [controller, historyWindow, position, thoughtList, historyQuery, contentSearch, state.transcript]);
|
|
607
|
+
useEffect(() => {
|
|
608
|
+
const first = displayTranscript.beforeSeq;
|
|
609
|
+
if (first !== undefined)
|
|
610
|
+
setReasoningOverrides(previous => {
|
|
611
|
+
const next = new Set([...previous].filter(seq => seq >= first));
|
|
612
|
+
return next.size === previous.size ? previous : next;
|
|
613
|
+
});
|
|
614
|
+
}, [displayTranscript, displayTranscript.memoryRevision]);
|
|
282
615
|
const scrollPosition = useRef(position);
|
|
283
616
|
scrollPosition.current = position;
|
|
284
617
|
const loadingPage = useRef(false);
|
|
@@ -286,18 +619,18 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
286
619
|
const mounted = useRef(true);
|
|
287
620
|
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
288
621
|
function scrollHistory(delta) {
|
|
289
|
-
if (state.screen !== 'chat' || historyQuery !== undefined || searchResults !== undefined)
|
|
622
|
+
if (displayPaused || state.screen !== 'chat' || historyQuery !== undefined || searchResults !== undefined || thoughtList)
|
|
290
623
|
return;
|
|
291
624
|
const intent = ++scrollIntent.current;
|
|
292
625
|
const next = Math.max(0, Math.min(maxScroll, scrollPosition.current + delta));
|
|
293
626
|
scrollPosition.current = next;
|
|
294
627
|
setScroll(next);
|
|
295
|
-
if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !
|
|
628
|
+
if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !displayTranscript.ready || !displayTranscript.hasMore)
|
|
296
629
|
return;
|
|
297
630
|
loadingPage.current = true;
|
|
298
|
-
const transcript =
|
|
299
|
-
void controller.perform(() => historyOperation(signal => controller.older(signal))).then(accepted => {
|
|
300
|
-
if (accepted && mounted.current &&
|
|
631
|
+
const transcript = displayTranscript;
|
|
632
|
+
void controller.perform(() => historyOperation(signal => controller.older(signal, transcript))).then(accepted => {
|
|
633
|
+
if (accepted && mounted.current && displayRef.current === transcript && scrollIntent.current === intent) {
|
|
301
634
|
setScroll(value => value + delta);
|
|
302
635
|
}
|
|
303
636
|
}).finally(() => { loadingPage.current = false; });
|
|
@@ -305,65 +638,109 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS }) {
|
|
|
305
638
|
async function historyOperation(operation) {
|
|
306
639
|
const abort = new AbortController();
|
|
307
640
|
historyAbort.current = abort;
|
|
641
|
+
setHistoryLoading(true);
|
|
308
642
|
try {
|
|
309
643
|
await operation(abort.signal);
|
|
310
644
|
}
|
|
311
645
|
finally {
|
|
312
|
-
if (historyAbort.current === abort)
|
|
646
|
+
if (historyAbort.current === abort) {
|
|
313
647
|
historyAbort.current = undefined;
|
|
648
|
+
setHistoryLoading(false);
|
|
649
|
+
}
|
|
314
650
|
}
|
|
315
651
|
}
|
|
316
652
|
async function openSearchSession(sessionId, query) {
|
|
317
653
|
await historyOperation(async (signal) => {
|
|
318
654
|
await controller.selectSession(sessionId);
|
|
319
655
|
await controller.waitForHistory(signal);
|
|
320
|
-
await controller.
|
|
656
|
+
setHistoryMatches(await controller.searchHistory(query, signal));
|
|
321
657
|
setSearchResults(undefined);
|
|
322
658
|
setContentSearch(true);
|
|
323
659
|
setHistoryQuery(query);
|
|
324
660
|
});
|
|
325
661
|
}
|
|
326
|
-
async function jumpHistory(target) {
|
|
662
|
+
async function jumpHistory(target, folds = reasoningOverrides) {
|
|
327
663
|
if (state.screen !== 'chat')
|
|
328
664
|
throw new Error('Select a session first');
|
|
329
665
|
++scrollIntent.current;
|
|
330
|
-
if (target === 'last') {
|
|
331
|
-
setHistoryQuery(undefined);
|
|
332
|
-
setScroll(0);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
666
|
const abort = new AbortController();
|
|
336
667
|
historyAbort.current = abort;
|
|
337
668
|
try {
|
|
338
|
-
|
|
669
|
+
const transcript = displayTranscript.messages.some(message => message.seq === target) ? displayTranscript
|
|
670
|
+
: state.transcript.messages.some(message => message.seq === target) ? state.transcript
|
|
671
|
+
: await controller.historyAt(target, abort.signal);
|
|
339
672
|
abort.signal.throwIfAborted();
|
|
340
|
-
const current = historyLayout(
|
|
341
|
-
const
|
|
342
|
-
const row = seq === undefined ? undefined : current.offsets.get(seq);
|
|
673
|
+
const current = historyLayout(transcript, width, reasoning, folds, liveReasoning);
|
|
674
|
+
const row = current.offsets.get(target);
|
|
343
675
|
if (row === undefined)
|
|
344
676
|
throw new Error('No visible message at this sequence; use /history to choose a record');
|
|
677
|
+
setHistoryWindow(transcript === state.transcript ? undefined : transcript);
|
|
345
678
|
setHistoryQuery(undefined);
|
|
346
|
-
|
|
679
|
+
setHistoryMatches(undefined);
|
|
680
|
+
setThoughtList(false);
|
|
681
|
+
setScroll(Math.max(0, current.length - pageSize - row));
|
|
347
682
|
}
|
|
348
683
|
finally {
|
|
349
684
|
if (historyAbort.current === abort)
|
|
350
685
|
historyAbort.current = undefined;
|
|
351
686
|
}
|
|
352
687
|
}
|
|
353
|
-
useMouseWheel(direction => scrollHistory(direction * 3));
|
|
354
|
-
const end = Math.max(pageSize,
|
|
355
|
-
const visible =
|
|
688
|
+
useMouseWheel(direction => scrollHistory(direction * 3), !displayPaused, () => setCopyMode(true));
|
|
689
|
+
const end = Math.max(pageSize, length - position);
|
|
690
|
+
const visible = useMemo(() => thoughtList ? [] : layout.viewport(Math.max(0, end - pageSize), end), [layout, end, pageSize, thoughtList]);
|
|
691
|
+
const liveThought = thoughtList && !historyWindow ? state.transcript.liveParts(width).find(part => part.kind === 'reasoning') : undefined;
|
|
692
|
+
const thoughtEntries = thoughtList ? displayTranscript.thoughts : undefined;
|
|
693
|
+
const thoughtChoices = useMemo(() => [...(thoughtEntries ?? [])].reverse().map(entry => ({
|
|
694
|
+
key: String(entry.seq), label: `${toolLine(`#${entry.seq} User · ${entry.prompt}`, width - 2)}\n ${toolLine(`◇ ${entry.preview}`, width - 4)}`,
|
|
695
|
+
action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq); setReasoningOverrides(next); await jumpHistory(entry.seq, next); }),
|
|
696
|
+
})), [thoughtEntries, width, pageSize, reasoningOverrides, liveReasoning, displayTranscript]);
|
|
697
|
+
const thoughtOptions = useMemo(() => [
|
|
698
|
+
...(liveThought ? [{ key: 'live', label: `${toolLine(`Now · User · ${state.transcript.latestPrompt}`, width - 2)}\n ${toolLine(liveThought.text, width - 4)}`,
|
|
699
|
+
action: () => { setLiveReasoning('full'); setThoughtList(false); const expanded = historyLayout(state.transcript, width, reasoning, reasoningOverrides, 'full'); setScroll(Math.max(0, expanded.length - pageSize - expanded.liveOffset)); } }] : []),
|
|
700
|
+
...thoughtChoices,
|
|
701
|
+
...(displayTranscript.hasMore ? [{ key: 'older', label: '↑ Load older reasoning', action: () => operate(() => historyOperation(signal => controller.older(signal, displayTranscript))) }] : []),
|
|
702
|
+
{ key: 'close', label: '← Back to conversation', action: () => setThoughtList(false) },
|
|
703
|
+
], [liveThought, thoughtChoices, displayTranscript, displayTranscript.hasMore, width, pageSize, reasoningOverrides]);
|
|
356
704
|
const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
705
|
+
const workspaceName = workspace ? string(workspace.title) || string(workspace.path) : state.workspaceId;
|
|
706
|
+
const headerTitle = [workspaceName && toolLine(workspaceName, Math.max(8, Math.min(24, Math.floor(width / 4)))), controller.sessionName]
|
|
707
|
+
.filter(Boolean).join(' · ') || 'All workspaces';
|
|
708
|
+
return _jsx(ThemeContext.Provider, { value: theme, children: _jsx(CopyMode.Provider, { value: copyMode, children: _jsx(Frozen, { frozen: copyMode, identity: `${width}:${stdout.rows}`, children: _jsxs(Box, { flexDirection: "column", paddingX: 1, height: Math.max(1, (stdout.rows ?? 30) - 1), overflowY: "hidden", children: [copyMode && _jsx(Text, { color: theme.accent, children: "Copy mode \u00B7 drag to select \u00B7 Esc / Ctrl+S resumes" }), _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}`, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { width: width, children: [width >= 22 && _jsxs(_Fragment, { children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { bold: true, color: theme.accent, children: toolLine(`dsht · ${new URL(controller.base).hostname}`, Math.max(4, Math.min(28, width - 16))) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, minWidth: 0, marginX: 2, children: _jsx(Text, { wrap: "truncate-end", children: toolLine(headerTitle, width) }) })] }), width >= 60 && controller.sessionMode && _jsx(Box, { flexShrink: 0, marginRight: 2, children: _jsx(Text, { color: theme.accent, children: toolLine(controller.sessionMode, Math.min(24, Math.floor(width / 3))) }) }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: state.online ? theme.colors.success : theme.colors.context, children: toolLine(state.online ? '● Connected' : '○ Offline', width) }) })] }), _jsx(Text, { dimColor: true, children: '─'.repeat(width) })] }) }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [historyLoading && _jsx(Text, { dimColor: true, children: "Loading history\u2026 \u00B7 Esc / Ctrl+C cancel" }), _jsx(Frozen, { frozen: displayPaused, identity: state.sessionId ?? "", children: statusNotice && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: safeText(state.status) }) }), state.error && _jsx(Text, { color: theme.colors.error, children: state.error }), removal ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.context, children: removal.kind === 'workspace' ? 'Remove workspace registration?' : 'Archive session?' }), _jsx(Text, { wrap: "truncate-end", children: safeText(removal.name) }), _jsxs(Text, { wrap: "truncate-end", children: ["ID: ", safeText(removal.id)] }), removal.path && _jsxs(Text, { wrap: "truncate-end", children: ["Path: ", safeText(removal.path)] }), _jsx(Text, { children: removal.kind === 'workspace' ? 'Removes the workspace from the list. Directory and sessions are kept.' : 'Hides the session from lists. History is kept; /resume ID can reopen it.' }), _jsx(Text, { dimColor: true, children: "Running tasks continue. Esc cancels this dialog." }), _jsx(Picker, { choices: [
|
|
709
|
+
{ key: 'cancel', label: 'Cancel', action: () => setRemoval(undefined) },
|
|
710
|
+
{ key: 'confirm', label: removal.kind === 'workspace' ? 'Remove workspace' : 'Archive session',
|
|
711
|
+
action: () => operate(async () => { await controller.removeTarget(removal); setRemoval(undefined); }) },
|
|
712
|
+
], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, `${removal.kind}:${removal.id}`)] }) : models ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: models.model ? 'Choose reasoning effort' : 'Choose model' }), _jsx(Text, { dimColor: true, children: "Applies to subsequent requests; host also saves the default." }), array(models.catalog.failures).map(object).map(failure => _jsx(Text, { color: theme.colors.error, children: safeText(`${failure.name}: ${failure.message}`) }, string(failure.id))), _jsx(Picker, { pageSize: Math.max(1, Math.min(8, (stdout.rows ?? 30) - 15)), choices: models.model ? [
|
|
713
|
+
{ key: 'default', label: `Default effort${object(models.model.reasoning).defaultEffort ? ` · ${string(object(models.model.reasoning).defaultEffort)}` : ''}`,
|
|
714
|
+
action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id)); setModels(undefined); }) },
|
|
715
|
+
...array(object(models.model.reasoning).efforts).map(object).map(effort => ({ key: string(effort.id), label: toolLine(`${effort.name} (${effort.id})${effort.description ? ` · ${effort.description}` : ''}`, width - 2),
|
|
716
|
+
action: () => operate(async () => { await controller.selectModel(models.provider, string(models.model.id), string(effort.id)); setModels(undefined); }) })),
|
|
717
|
+
{ key: 'back', label: '← Models', action: () => setModels({ catalog: models.catalog }) },
|
|
718
|
+
] : [
|
|
719
|
+
...array(models.catalog.groups).map(object).flatMap(group => array(group.models).map(object).map(model => ({
|
|
720
|
+
key: `${group.id}:${model.id}`, label: toolLine(`${group.name} · ${model.name} (${model.id})`, width - 2),
|
|
721
|
+
action: () => {
|
|
722
|
+
if (array(object(model.reasoning ?? { efforts: [] }).efforts).length)
|
|
723
|
+
setModels({ catalog: models.catalog, provider: string(group.id), model });
|
|
724
|
+
else
|
|
725
|
+
operate(async () => { await controller.selectModel(string(group.id), string(model.id)); setModels(undefined); });
|
|
726
|
+
},
|
|
727
|
+
}))),
|
|
728
|
+
{ key: 'close', label: '← Back to conversation', action: () => setModels(undefined) },
|
|
729
|
+
], enabled: !input && !state.busy && state.online, canSelect: () => !draft.current && !controller.state.busy && controller.state.online }, models.model ? `${models.provider}:${models.model.id}` : 'models')] }) : searchResults ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: ["Session search \u00B7 ", safeText(searchResults.query)] }), searchResults.hasMore && _jsx(Text, { color: theme.colors.context, children: "Host returned only the first 20 global matches; workspace results may be incomplete. Refine your query." }), !searchResults.items.length && _jsx(Text, { children: "No sessions in the returned results" }), _jsx(Picker, { choices: [...searchResults.items.map(item => ({ key: string(item.sessionId),
|
|
730
|
+
label: toolLine(`${item.sessionId} · ${item.snippet}`, width - 2),
|
|
731
|
+
action: () => operate(() => openSearchSession(string(item.sessionId), searchResults.query)),
|
|
732
|
+
})), { key: 'close', label: '← Back', action: () => setSearchResults(undefined) }], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy })] }) : state.screen === 'workspaces' || state.screen === 'sessions' ? _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, children: state.screen === 'workspaces' ? 'Choose workspace' : state.showAllSessions ? 'Choose session · All workspaces' : 'Choose session' }), _jsx(Picker, { choices: choices, enabled: state.online && !state.busy && !input, canSelect: () => !draft.current && controller.state.online && !controller.state.busy }, `${state.screen}:${state.workspaceId ?? ''}`)] }) : _jsxs(_Fragment, { children: [state.screen === 'chat' && historyQuery === undefined && !searchResults && !thoughtList && !pending && _jsxs(Box, { ref: conversationBox, flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", marginY: 1, children: [visible.length ? _jsx(Frozen, { frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}`, children: _jsx(HistoryViewport, { rows: visible }) }) : _jsx(Text, { color: theme.colors.muted, children: "Start a conversation with the host agent." }), (displayTranscript.hasMore || historyWindow) && _jsx(Text, { dimColor: true, children: historyWindow ? 'Earlier history · /latest returns to live conversation' : 'Scroll up or /older to load earlier history' })] }), thoughtList && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.reasoning, children: "Reasoning history \u00B7 User prompts \u00B7 Esc close" }), !thoughtEntries?.length && !liveThought && _jsx(Text, { dimColor: true, children: "No reasoning in loaded history" }), _jsx(Picker, { choices: thoughtOptions, pageSize: Math.max(1, Math.min(6, Math.floor(((stdout.rows ?? 30) - 14) / 2))), enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `thoughts:${state.sessionId}`)] }), state.screen === 'chat' && historyQuery !== undefined && _jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { bold: true, children: [contentSearch ? 'Search · session history' : 'History · loaded records', " \u00B7 Esc close"] }), contentSearch && historyMatches?.truncated && _jsx(Text, { color: theme.colors.context, children: "Showing the first 200 matches \u00B7 Refine your search" }), _jsx(Picker, { choices: [
|
|
733
|
+
...(contentSearch && historyMatches ? historyMatches.items.map(message => ({
|
|
734
|
+
key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.preview}`, width - 2),
|
|
735
|
+
action: () => operate(() => jumpHistory(message.seq)),
|
|
736
|
+
})) : layout.messages.filter(message => message.role === 'You' && `${message.seq} ${message.text}`.toLowerCase().includes(historyQuery.toLowerCase())).map(message => ({
|
|
737
|
+
key: String(message.seq), label: toolLine(`#${message.seq} ${message.role} · ${message.text}`, width - 2),
|
|
738
|
+
action: () => operate(() => jumpHistory(message.seq)),
|
|
739
|
+
}))),
|
|
740
|
+
{ key: 'close', label: '← Back to conversation', action: () => { setHistoryQuery(undefined); setHistoryMatches(undefined); } },
|
|
741
|
+
], enabled: !input && !state.busy, canSelect: () => !draft.current && !controller.state.busy }, `history:${historyQuery}`)] }), pending && _jsxs(Box, { flexShrink: 0, borderStyle: "round", borderColor: theme.colors.context, paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: theme.colors.context, children: question ? `Question ${answered.length + 1}/${questions.length}${question.header ? ` · ${safeText(string(question.header))}` : ''}` : 'Approval required' }), _jsx(Text, { children: safeText(question ? string(question.question) : JSON.stringify(pending.request, null, 2)) }), question?.detail && _jsx(Text, { children: safeText(string(question.detail)) }), options.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [[...options, { label: 'Other answer — type below' }].map((option, index) => ({ option, index }))
|
|
742
|
+
.slice(optionStart, optionStart + optionPageSize).map(({ option, index }) => _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { color: index === optionCursor ? theme.accent : undefined, wrap: "truncate-end", children: [index === optionCursor ? '❯ ' : ' ', index + 1, ". ", question?.multiSelect === true && index < options.length ? choiceState.selected.includes(string(option.label)) ? '[x] ' : '[ ] ' : '', safeText(string(option.label))] }), option.description && _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [' ', safeText(string(option.description))] })] }, index)), _jsx(Text, { dimColor: true, children: choiceState.custom ? 'Type your answer below · Esc returns to options' : question?.multiSelect === true
|
|
743
|
+
? '↑ ↓ move · Space / 1–9 toggle · Enter confirm' : '↑ ↓ / 1–9 select · Enter confirm' })] }), _jsx(Text, { dimColor: true, children: question ? 'Text answers supported · Ctrl+C clears · /cancel stops' : '/allow approves once · /deny rejects · /cancel stops' })] })] })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: notice }), _jsxs(Box, { borderStyle: "round", borderColor: state.online ? theme.accent : theme.border, paddingX: 1, children: [_jsx(Text, { color: theme.accent, children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: setCursor, onSubmit: () => { void submit(draft.current); }, reservedKeys: questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : undefined, focus: state.online && !state.busy && !copyMode, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' })] }), referenceOpen && _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Host files \u00B7 \u2191 \u2193 select \u00B7 Tab/Enter insert \u00B7 Esc close" }), !matches ? _jsx(Text, { dimColor: true, children: "Searching\u2026" }) : matches.error ? _jsx(Text, { color: theme.colors.error, children: matches.error })
|
|
744
|
+
: matches.items.length === 0 ? _jsx(Text, { dimColor: true, children: "No matching host files" })
|
|
745
|
+
: matches.items.slice(Math.max(0, referenceIndex - 5), Math.max(0, referenceIndex - 5) + 6).map((item, index) => _jsxs(Text, { color: index + Math.max(0, referenceIndex - 5) === referenceIndex ? theme.accent : undefined, children: [index + Math.max(0, referenceIndex - 5) === referenceIndex ? '❯ ' : ' ', item.path, item.kind === 'directory' ? '/' : ''] }, `${item.kind}:${item.path}`))] }), input.startsWith('/') && !input.includes(' ') && _jsx(Text, { dimColor: true, children: COMMANDS.filter(command => command.startsWith(input)).join(' ') }), help && _jsxs(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [COMMAND_HINTS.map((hint, index) => _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: theme.accent, children: COMMAND_LABELS[index].padEnd(COMMAND_LABEL_WIDTH) }), hint.description] }, hint.command)), _jsx(Text, { dimColor: true, children: "Enter send \u00B7 Tab complete \u00B7 Esc cancel \u00B7 Wheel/PgUp/PgDn scroll \u00B7 Ctrl+C clear / stop / exit" }), _jsx(Text, { dimColor: true, children: "History: \u2191/\u2193 or Ctrl+P/N recall \u00B7 Editing: Ctrl+A/E start/end \u00B7 Ctrl+K/U kill right/left \u00B7 Ctrl+W kill word \u00B7 Ctrl+Y restore" })] }), costExpanded && _jsx(CostPanel, { controller: controller }), _jsx(Frozen, { frozen: displayPaused || position > 0, identity: `${width}:${state.sessionId}:${statusExpanded}`, children: _jsx(StatusBar, { controller: controller, width: width, expanded: statusExpanded, revision: state.version, paused: displayPaused || position > 0 }) })] })] }) }) }) });
|
|
369
746
|
}
|