@itookit/dsht 0.3.4 → 0.3.7
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 +2 -2
- package/README.zh.md +2 -2
- package/dist/controller/controller.d.ts +107 -0
- package/dist/controller/controller.js +114 -1
- package/dist/cost/controller.d.ts +5 -0
- package/dist/cost/controller.js +2 -1
- package/dist/cost/scanner.d.ts +4 -2
- package/dist/cost/scanner.js +6 -3
- package/dist/session/controller.d.ts +141 -1
- package/dist/session/controller.js +362 -29
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +1 -0
- package/dist/session/info.d.ts +262 -0
- package/dist/session/info.js +326 -0
- package/dist/session/transcript.d.ts +37 -1
- package/dist/session/transcript.js +73 -0
- package/dist/state.d.ts +3 -2
- package/dist/state.js +2 -2
- package/dist/ui/app.js +183 -173
- package/dist/ui/chat/status.js +4 -4
- package/dist/ui/dialogs/index.d.ts +3 -6
- package/dsht-m.png +0 -0
- package/package.json +1 -1
- package/dist/ui/input/history.d.ts +0 -19
- package/dist/ui/input/history.js +0 -43
package/dist/ui/app.js
CHANGED
|
@@ -5,9 +5,8 @@ import { mocha, ThemeContext } from "./theme/index.js";
|
|
|
5
5
|
import { Box, Text, measureElement, useApp, useInput, useStdout } from 'ink';
|
|
6
6
|
import { useMouseWheel } from "./input/mouse.js";
|
|
7
7
|
import { TextInput } from "./input/input.js";
|
|
8
|
-
import { InputHistory } from "./input/history.js";
|
|
9
8
|
import { ReferenceMenu } from "./input/references.js";
|
|
10
|
-
import { historyLayout
|
|
9
|
+
import { historyLayout } from "../session/history.js";
|
|
11
10
|
import { toolLine } from "../session/transcript.js";
|
|
12
11
|
import { activeReference, fileMention } from "../session/references.js";
|
|
13
12
|
import { CostPanel } from "./dialogs/cost.js";
|
|
@@ -23,6 +22,8 @@ import { ROLLUP_LEGEND, sessionLabel, sessionStatus, workspaceCounts, workspaceD
|
|
|
23
22
|
import { array, errorText, object, safeText, string } from "../transport/wire.js";
|
|
24
23
|
/** Transient notices expire; interactive panels remain open until dismissed. */
|
|
25
24
|
const PANEL_LIFETIME_MS = 10_000;
|
|
25
|
+
/** Pages one boundary recall press may walk before it reports that nothing older holds a prompt. */
|
|
26
|
+
const RECALL_PAGE_SCAN = 20;
|
|
26
27
|
/** The caller owns starting and stopping the controller around the Ink render lifetime.
|
|
27
28
|
* @param props - Controller, panel lifetime and semantic theme.
|
|
28
29
|
* @returns The rendered terminal interface.
|
|
@@ -35,43 +36,26 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
35
36
|
const { exit } = useApp();
|
|
36
37
|
const { stdout } = useStdout();
|
|
37
38
|
const [copyMode, setCopyMode] = useState(false);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const history = new InputHistory();
|
|
43
|
-
inputHistory.current = history;
|
|
44
|
-
if (!state.transcript.ready)
|
|
45
|
-
return;
|
|
46
|
-
const messages = state.transcript.messagesForWidth(Math.max(20, (stdout.columns ?? 100) - 2));
|
|
47
|
-
// Seed once per loaded session, never scan historical messages on stream ticks or arrow presses.
|
|
48
|
-
const prompts = messages.filter(message => message.role === 'You').slice(-200);
|
|
49
|
-
for (const message of prompts)
|
|
50
|
-
history.record(message.text.replace(/\r?\n/g, ' ').trim());
|
|
51
|
-
}, [state.sessionId, state.transcript, state.transcript.ready]);
|
|
52
|
-
const [cursor, setCursor] = useState(0);
|
|
53
|
-
// Input callbacks may run before Ink refreshes the controlled field's listener.
|
|
39
|
+
// The composer belongs to the selected session: it resets when another session opens, and every
|
|
40
|
+
// callback reads the controller rather than a render closure Ink may not have refreshed yet.
|
|
41
|
+
const { draft: input, cursor } = state.session.composer;
|
|
42
|
+
const historyPaging = useRef(false);
|
|
54
43
|
const setInput = (value, recalled = false) => {
|
|
55
44
|
if (!recalled)
|
|
56
|
-
|
|
45
|
+
controller.resetRecall();
|
|
57
46
|
// A new message draft returns the view to the live end, so composing never needs a scroll first.
|
|
58
47
|
// A slash command is not a message, and the reader keeps their place while typing one.
|
|
59
|
-
if (state.screen === 'chat' && draft
|
|
60
|
-
setScroll(0);
|
|
61
|
-
|
|
62
|
-
updateInput(value);
|
|
63
|
-
setCursor(value.length);
|
|
48
|
+
if (state.screen === 'chat' && controller.composer.draft === '' && value !== '' && !value.startsWith('/'))
|
|
49
|
+
controller.setScroll(0);
|
|
50
|
+
controller.setComposer(value);
|
|
64
51
|
};
|
|
65
|
-
|
|
52
|
+
// The reading view is session-owned too: which record is shown, where the reader is, and what is
|
|
53
|
+
// expanded. `SessionInfo` releases the detached window, so the composer and this view reset together.
|
|
54
|
+
const { window: historyWindow, scroll, folds: reasoningOverrides, liveReasoning } = state.session.view;
|
|
66
55
|
const [historyLoading, setHistoryLoading] = useState();
|
|
67
|
-
const
|
|
68
|
-
const displayTranscript = historyWindow ?? state.transcript;
|
|
56
|
+
const displayTranscript = historyWindow ?? state.session.record;
|
|
69
57
|
const displayRef = useRef(displayTranscript);
|
|
70
58
|
displayRef.current = displayTranscript;
|
|
71
|
-
useEffect(() => () => { if (historyWindow) {
|
|
72
|
-
releaseHistoryLayout(historyWindow);
|
|
73
|
-
historyWindow.dispose();
|
|
74
|
-
} }, [historyWindow]);
|
|
75
59
|
const conversationBox = useRef(null);
|
|
76
60
|
const [conversationRows, setConversationRows] = useState(20);
|
|
77
61
|
useLayoutEffect(() => {
|
|
@@ -81,23 +65,18 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
81
65
|
setConversationRows(height);
|
|
82
66
|
}
|
|
83
67
|
});
|
|
84
|
-
const [scroll, setScroll] = useState(0);
|
|
85
|
-
const [liveReasoning, setLiveReasoning] = useState('row');
|
|
86
68
|
const [removal, setRemoval] = useState();
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const [searchResults, setSearchResults] = useState();
|
|
69
|
+
// Session-owned panels: visibility and query text only, because every row comes from the record.
|
|
70
|
+
const { thoughts: thoughtList, queue: queueOpen, model: models, history, search: searchResults } = state.session.panels;
|
|
71
|
+
const historyQuery = history?.query;
|
|
72
|
+
const contentSearch = history?.contentSearch === true;
|
|
73
|
+
const historyMatches = history?.matches;
|
|
93
74
|
const historyAbort = useRef(undefined);
|
|
94
75
|
useEffect(() => () => historyAbort.current?.abort(), []);
|
|
95
76
|
const [costExpanded, setCostExpanded] = useState(false);
|
|
96
77
|
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
97
|
-
const [reasoningOverrides, setReasoningOverrides] = useState(new Set());
|
|
98
78
|
const reasoning = 'row';
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
useEffect(() => { setLiveReasoning('row'); }, [state.transcript, state.transcript.liveAttemptKey]);
|
|
79
|
+
useEffect(() => { controller.setLiveReasoning('row'); }, [state.session.record, state.session.record.liveAttemptKey]);
|
|
101
80
|
const [notice, setNotice] = useState();
|
|
102
81
|
const [help, setHelp] = useState(false);
|
|
103
82
|
const [helpPage, setHelpPage] = useState(0);
|
|
@@ -110,11 +89,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
110
89
|
// three-row composer above it never shrink, so only the remainder of the screen height shows rows.
|
|
111
90
|
const [statusBarRows, setStatusBarRows] = useState(1);
|
|
112
91
|
const statusViewRows = Math.max(1, (stdout.rows ?? 30) - 9 - (statusBarRows - 1));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const [dismissedReference, dismissReference] = useState();
|
|
92
|
+
// Answers and menu highlights are session-owned too: the waterfall itself is derived per session
|
|
93
|
+
// by the controller, so only the local selection is kept here.
|
|
94
|
+
const { answers, option: optionState, approval: approvalSelection } = state.session.interaction;
|
|
95
|
+
const { index: referenceIndex, dismissed: dismissedReference } = state.session.reference;
|
|
118
96
|
const [lookup, setLookup] = useState();
|
|
119
97
|
// Interactive panels remain stable while read; only transient notices expire.
|
|
120
98
|
useEffect(() => {
|
|
@@ -130,14 +108,14 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
130
108
|
useEffect(() => {
|
|
131
109
|
if (copyMode || contentSearch || historyQuery === undefined)
|
|
132
110
|
return;
|
|
133
|
-
const timer = setTimeout(() => {
|
|
111
|
+
const timer = setTimeout(() => { controller.setHistoryPanel(undefined); }, panelLifetimeMs);
|
|
134
112
|
return () => clearTimeout(timer);
|
|
135
113
|
}, [historyQuery, contentSearch, panelLifetimeMs, copyMode]);
|
|
136
114
|
const pending = state.pending[0];
|
|
137
115
|
const queued = controller.telemetry.pending(state.sessionId).filter(item => item.placement !== 'context');
|
|
138
|
-
useEffect(() => {
|
|
116
|
+
useEffect(() => { controller.openQueue(false); }, [state.sessionId, pending?.eventId]);
|
|
139
117
|
// A replayed interaction (same eventId after a reconnect) starts unselected again.
|
|
140
|
-
useEffect(() => {
|
|
118
|
+
useEffect(() => { controller.setApproval(undefined); }, [state.sessionId, state.online, pending?.eventId]);
|
|
141
119
|
const token = state.screen === 'chat' && state.online && !state.busy && !pending
|
|
142
120
|
&& !input.startsWith('/') && dismissedReference !== input && cursor === input.length
|
|
143
121
|
? activeReference(input) : undefined;
|
|
@@ -152,7 +130,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
152
130
|
return;
|
|
153
131
|
const abort = new AbortController();
|
|
154
132
|
setLookup(undefined);
|
|
155
|
-
setReferenceIndex(0);
|
|
133
|
+
controller.setReferenceIndex(0);
|
|
156
134
|
void controller.references(activeReference(input).query, abort.signal).then(items => {
|
|
157
135
|
if (!abort.signal.aborted)
|
|
158
136
|
setLookup({ draft: input, sessionId: state.sessionId, items });
|
|
@@ -164,7 +142,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
164
142
|
}, [controller, input, state.sessionId, referenceOpen]);
|
|
165
143
|
const pickReference = () => {
|
|
166
144
|
const candidate = matches?.items[referenceIndex];
|
|
167
|
-
if (!candidate || !token || draft
|
|
145
|
+
if (!candidate || !token || controller.composer.draft !== input)
|
|
168
146
|
return;
|
|
169
147
|
const mention = fileMention(candidate, token.quoted);
|
|
170
148
|
setInput(input.slice(0, -token.prefix.length) + mention + (candidate.kind === 'file' ? ' ' : ''));
|
|
@@ -197,26 +175,16 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
197
175
|
// parked rather than typed into, and comes back when the last one closes. A question switches to
|
|
198
176
|
// the composer itself for a free-text or "Other" answer, so it keeps the keys then.
|
|
199
177
|
const answerPending = !!pending && (pending.event === 'approval/request' || options.length > 0 && !choiceState.custom);
|
|
200
|
-
const parkedDraft = useRef('');
|
|
201
178
|
useEffect(() => {
|
|
202
179
|
if (answerPending) {
|
|
203
|
-
|
|
204
|
-
return;
|
|
205
|
-
parkedDraft.current = draft.current;
|
|
206
|
-
draft.current = '';
|
|
207
|
-
updateInput('');
|
|
208
|
-
setCursor(0);
|
|
180
|
+
controller.parkComposer();
|
|
209
181
|
return;
|
|
210
182
|
}
|
|
211
|
-
if (pending
|
|
183
|
+
if (pending)
|
|
212
184
|
return;
|
|
213
|
-
// Restoring writes the
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
parkedDraft.current = '';
|
|
217
|
-
draft.current = restore;
|
|
218
|
-
updateInput(restore);
|
|
219
|
-
setCursor(restore.length);
|
|
185
|
+
// Restoring writes the draft directly: it was neither recalled from history nor a new message,
|
|
186
|
+
// so it must not move a reader's scroll position.
|
|
187
|
+
controller.restoreComposer();
|
|
220
188
|
}, [answerPending, pending]);
|
|
221
189
|
const approvalKeysActive = pending?.event === 'approval/request' && !copyMode && !panelBlocksKeys;
|
|
222
190
|
const approvalIndex = approvalSelection?.eventId === eventId ? approvalSelection.index : -1;
|
|
@@ -227,11 +195,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
227
195
|
const next = [...answered, answer];
|
|
228
196
|
if (next.length === questions.length) {
|
|
229
197
|
await controller.answer({ answers: next });
|
|
230
|
-
|
|
198
|
+
const rest = { ...controller.interaction.answers };
|
|
199
|
+
delete rest[eventId];
|
|
200
|
+
controller.setAnswers(rest);
|
|
231
201
|
}
|
|
232
202
|
else
|
|
233
|
-
setAnswers(
|
|
234
|
-
|
|
203
|
+
controller.setAnswers({ ...controller.interaction.answers, [eventId]: next });
|
|
204
|
+
controller.setOption(undefined);
|
|
235
205
|
};
|
|
236
206
|
const operate = (fn) => { void controller.perform(fn); };
|
|
237
207
|
const requestRemoval = async (kind, query) => {
|
|
@@ -257,22 +227,20 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
257
227
|
historyAbort.current.abort();
|
|
258
228
|
// Esc also leaves a history list that was already on screen when the load was aborted.
|
|
259
229
|
if (key.escape && historyQuery !== undefined) {
|
|
260
|
-
|
|
261
|
-
setHistoryMatches(undefined);
|
|
230
|
+
controller.setHistoryPanel(undefined);
|
|
262
231
|
}
|
|
263
232
|
return;
|
|
264
233
|
}
|
|
265
234
|
if ((key.escape || key.ctrl && _value === 'c') && controller.state.pending.length) {
|
|
266
|
-
if (key.ctrl && draft
|
|
235
|
+
if (key.ctrl && controller.composer.draft)
|
|
267
236
|
setInput('');
|
|
268
237
|
if (key.escape) {
|
|
269
|
-
|
|
238
|
+
controller.setApproval(undefined);
|
|
270
239
|
setRemoval(undefined);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
setHistoryMatches(undefined);
|
|
240
|
+
controller.setModelPanel(undefined);
|
|
241
|
+
controller.openThoughts(false);
|
|
242
|
+
controller.setSearchPanel(undefined);
|
|
243
|
+
controller.setHistoryPanel(undefined);
|
|
276
244
|
setHelp(false);
|
|
277
245
|
setCostExpanded(false);
|
|
278
246
|
setStatusExpanded(false);
|
|
@@ -281,24 +249,26 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
281
249
|
// the options, and only the next one dismisses. Approval keeps every choice explicit.
|
|
282
250
|
if (pending?.event === 'user-questions/request' && !choiceState.custom) {
|
|
283
251
|
if (controller.state.online && !controller.state.busy && controller.state.pending[0]?.eventId === eventId) {
|
|
284
|
-
|
|
285
|
-
|
|
252
|
+
controller.setOption(undefined);
|
|
253
|
+
const rest = { ...controller.interaction.answers };
|
|
254
|
+
delete rest[eventId];
|
|
255
|
+
controller.setAnswers(rest);
|
|
286
256
|
operate(() => controller.dismissQuestion());
|
|
287
257
|
}
|
|
288
258
|
return;
|
|
289
259
|
}
|
|
290
|
-
|
|
260
|
+
controller.setOption({ ...choiceState, custom: false });
|
|
291
261
|
}
|
|
292
262
|
return;
|
|
293
263
|
}
|
|
294
|
-
if (approvalKeysActive && !draft
|
|
264
|
+
if (approvalKeysActive && !controller.composer.draft && controller.state.online && !controller.state.busy
|
|
295
265
|
&& controller.state.pending[0]?.eventId === eventId && !key.ctrl && !key.meta) {
|
|
296
266
|
const digit = /^[1-3]$/.test(_value) ? Number(_value) - 1 : -1;
|
|
297
267
|
if (digit >= 0 || key.upArrow || key.downArrow) {
|
|
298
268
|
// An unselected list enters at the first, non-destructive choice, so a stray arrow plus Enter cannot cancel.
|
|
299
269
|
const index = digit >= 0 ? digit : approvalIndex < 0 ? 0
|
|
300
270
|
: Math.max(0, Math.min(2, approvalIndex + (key.upArrow ? -1 : 1)));
|
|
301
|
-
|
|
271
|
+
controller.setApproval({ eventId, index });
|
|
302
272
|
return;
|
|
303
273
|
}
|
|
304
274
|
if (key.return) {
|
|
@@ -308,10 +278,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
308
278
|
return;
|
|
309
279
|
}
|
|
310
280
|
}
|
|
311
|
-
if (questionKeysActive && !draft
|
|
281
|
+
if (questionKeysActive && !controller.composer.draft && !controller.state.busy && !key.ctrl && !key.meta) {
|
|
312
282
|
const digit = /^[1-9]$/.test(_value) ? Number(_value) - 1 : -1;
|
|
313
283
|
if (key.upArrow || key.downArrow) {
|
|
314
|
-
|
|
284
|
+
controller.setOption({ ...choiceState, cursor: Math.max(0, Math.min(options.length, optionCursor + (key.upArrow ? -1 : 1))) });
|
|
315
285
|
return;
|
|
316
286
|
}
|
|
317
287
|
if (digit >= 0 && digit <= options.length || _value === ' ' && question.multiSelect === true && optionCursor < options.length) {
|
|
@@ -320,12 +290,12 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
320
290
|
const selected = question.multiSelect === true && label
|
|
321
291
|
? choiceState.selected.includes(label) ? choiceState.selected.filter(item => item !== label) : [...choiceState.selected, label]
|
|
322
292
|
: choiceState.selected;
|
|
323
|
-
|
|
293
|
+
controller.setOption({ ...choiceState, cursor: index, selected });
|
|
324
294
|
return;
|
|
325
295
|
}
|
|
326
296
|
if (key.return) {
|
|
327
297
|
if (optionCursor === options.length) {
|
|
328
|
-
|
|
298
|
+
controller.setOption({ ...choiceState, custom: true });
|
|
329
299
|
return;
|
|
330
300
|
}
|
|
331
301
|
const selected = question.multiSelect === true ? choiceState.selected : [string(options[optionCursor].label)];
|
|
@@ -354,7 +324,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
354
324
|
return;
|
|
355
325
|
}
|
|
356
326
|
if (key.escape && queueOpen) {
|
|
357
|
-
|
|
327
|
+
controller.openQueue(false);
|
|
358
328
|
return;
|
|
359
329
|
}
|
|
360
330
|
if (key.escape && removal) {
|
|
@@ -362,24 +332,23 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
362
332
|
return;
|
|
363
333
|
}
|
|
364
334
|
if (key.escape && models) {
|
|
365
|
-
|
|
335
|
+
controller.setModelPanel(undefined);
|
|
366
336
|
return;
|
|
367
337
|
}
|
|
368
338
|
if (key.escape && thoughtList) {
|
|
369
|
-
|
|
339
|
+
controller.openThoughts(false);
|
|
370
340
|
if (controller.running)
|
|
371
341
|
void controller.interrupt(true);
|
|
372
342
|
return;
|
|
373
343
|
}
|
|
374
344
|
if (key.escape && searchResults) {
|
|
375
|
-
|
|
345
|
+
controller.setSearchPanel(undefined);
|
|
376
346
|
if (controller.running)
|
|
377
347
|
void controller.interrupt(true);
|
|
378
348
|
return;
|
|
379
349
|
}
|
|
380
350
|
if (key.escape && historyQuery !== undefined) {
|
|
381
|
-
|
|
382
|
-
setHistoryMatches(undefined);
|
|
351
|
+
controller.setHistoryPanel(undefined);
|
|
383
352
|
if (controller.running)
|
|
384
353
|
void controller.interrupt(true);
|
|
385
354
|
return;
|
|
@@ -403,24 +372,34 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
403
372
|
}
|
|
404
373
|
if (referenceOpen) {
|
|
405
374
|
if (key.escape) {
|
|
406
|
-
|
|
375
|
+
controller.setReferenceDismissed(input);
|
|
407
376
|
if (controller.running)
|
|
408
377
|
void controller.interrupt(true);
|
|
409
378
|
}
|
|
410
379
|
else if (key.tab)
|
|
411
380
|
pickReference();
|
|
412
381
|
else if (key.upArrow)
|
|
413
|
-
setReferenceIndex(
|
|
382
|
+
controller.setReferenceIndex(Math.max(0, controller.reference.index - 1));
|
|
414
383
|
else if (key.downArrow)
|
|
415
|
-
setReferenceIndex(
|
|
384
|
+
controller.setReferenceIndex(Math.max(0, Math.min((matches?.items.length ?? 1) - 1, controller.reference.index + 1)));
|
|
416
385
|
return;
|
|
417
386
|
}
|
|
418
387
|
const recallPrevious = key.upArrow || key.ctrl && _value === 'p';
|
|
419
388
|
const recallNext = key.downArrow || key.ctrl && _value === 'n';
|
|
420
389
|
if ((recallPrevious || recallNext) && state.online && !controller.state.busy && !pending
|
|
421
390
|
&& !queueOpen && (!recallBlocked || key.ctrl)
|
|
422
|
-
&& (state.screen === 'chat' || draft
|
|
423
|
-
|
|
391
|
+
&& (state.screen === 'chat' || controller.composer.draft !== '' || key.ctrl)) {
|
|
392
|
+
// The oldest seeded entry is where the session happened to open, not where it began: stepping
|
|
393
|
+
// past it fetches the page before the retained window, and the step is applied once the page
|
|
394
|
+
// lands, so recall covers prompts from before this client connected. Paging stays with the
|
|
395
|
+
// conversation: on a picker screen the arrows only ever walked what was already retained.
|
|
396
|
+
if (recallPrevious && state.screen === 'chat' && !historyPaging.current
|
|
397
|
+
&& (controller.recallAtOldest || controller.recallLength === 0)
|
|
398
|
+
&& state.session.record.ready && controller.recallHasOlder) {
|
|
399
|
+
void recallOlderPrompts();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
setInput(controller.recall(recallPrevious ? -1 : 1, controller.composer.draft), true);
|
|
424
403
|
return;
|
|
425
404
|
}
|
|
426
405
|
if (key.tab) {
|
|
@@ -453,7 +432,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
453
432
|
}
|
|
454
433
|
const value = raw.trim();
|
|
455
434
|
if (!pending && !/^\/feedback(?:\s|$)/.test(value))
|
|
456
|
-
|
|
435
|
+
controller.recordRecall(value);
|
|
457
436
|
if (submission.kind === 'copy') {
|
|
458
437
|
setInput('');
|
|
459
438
|
setCopyMode(true);
|
|
@@ -462,9 +441,9 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
462
441
|
setRemoval(undefined);
|
|
463
442
|
// Each panel belongs to the command that opened it, so any other command closes it.
|
|
464
443
|
if (value !== '/queue')
|
|
465
|
-
|
|
444
|
+
controller.openQueue(false);
|
|
466
445
|
if (!/^\/model(?: |$)/.test(value))
|
|
467
|
-
|
|
446
|
+
controller.setModelPanel(undefined);
|
|
468
447
|
if (value !== '/help')
|
|
469
448
|
setHelp(false);
|
|
470
449
|
if (value !== '/cost')
|
|
@@ -472,7 +451,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
472
451
|
if (value !== '/status')
|
|
473
452
|
setStatusExpanded(false);
|
|
474
453
|
if (!/^\/think(?: |$)/.test(value))
|
|
475
|
-
|
|
454
|
+
controller.openThoughts(false);
|
|
476
455
|
if (submission.kind === 'quit') {
|
|
477
456
|
exit();
|
|
478
457
|
return;
|
|
@@ -502,70 +481,67 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
502
481
|
await requestRemoval(submission.target, submission.query);
|
|
503
482
|
return;
|
|
504
483
|
case 'navigate': {
|
|
505
|
-
|
|
506
|
-
|
|
484
|
+
controller.setHistoryPanel(undefined);
|
|
485
|
+
controller.setSearchPanel(undefined);
|
|
507
486
|
if (submission.target === 'workspace')
|
|
508
487
|
await controller.switchWorkspace(submission.query);
|
|
509
488
|
else
|
|
510
489
|
await controller.switchSession(submission.query);
|
|
511
|
-
setScroll(0);
|
|
490
|
+
controller.setScroll(0);
|
|
512
491
|
return;
|
|
513
492
|
}
|
|
514
493
|
case 'path':
|
|
515
494
|
await controller.createWorkspace(submission.value);
|
|
516
495
|
return;
|
|
517
496
|
case 'latest':
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
setScroll(0);
|
|
497
|
+
controller.setViewWindow(undefined);
|
|
498
|
+
controller.setHistoryPanel(undefined);
|
|
499
|
+
controller.setSearchPanel(undefined);
|
|
500
|
+
controller.setFolds(new Set());
|
|
501
|
+
controller.setScroll(0);
|
|
524
502
|
controller.pinHistory(false);
|
|
525
503
|
return;
|
|
526
504
|
case 'models': {
|
|
527
505
|
if (!submission.args.length) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
506
|
+
controller.setHistoryPanel(undefined);
|
|
507
|
+
controller.setSearchPanel(undefined);
|
|
508
|
+
controller.setModelPanel({ catalog: await controller.modelCatalog() });
|
|
531
509
|
}
|
|
532
510
|
else {
|
|
533
511
|
await controller.selectModel(submission.args[0], submission.args[1], submission.args[2]);
|
|
534
|
-
|
|
512
|
+
controller.setModelPanel(undefined);
|
|
535
513
|
}
|
|
536
514
|
return;
|
|
537
515
|
}
|
|
538
516
|
case 'queue':
|
|
539
|
-
|
|
517
|
+
controller.openQueue(true);
|
|
540
518
|
return;
|
|
541
519
|
case 'newSession':
|
|
542
520
|
await controller.createSession();
|
|
543
521
|
return;
|
|
544
522
|
case 'history':
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
setHistoryQuery(submission.query);
|
|
523
|
+
controller.setSearchPanel(undefined);
|
|
524
|
+
controller.setHistoryPanel({ query: submission.query, contentSearch: false });
|
|
548
525
|
return;
|
|
549
526
|
case 'sessionSearch':
|
|
550
527
|
await historyOperation(async (signal) => {
|
|
551
528
|
const result = await controller.searchSessions(submission.query, submission.command === '/ssearch', signal);
|
|
552
|
-
|
|
553
|
-
|
|
529
|
+
controller.setHistoryPanel(undefined);
|
|
530
|
+
controller.setSearchPanel({ query: submission.query, ...result });
|
|
554
531
|
});
|
|
555
532
|
return;
|
|
556
533
|
case 'historySearch':
|
|
557
534
|
await historyOperation(async (signal) => {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
setHistoryQuery(submission.query);
|
|
535
|
+
controller.setHistoryPanel({ query: submission.query, contentSearch: true,
|
|
536
|
+
matches: await controller.searchHistory(submission.query, signal) });
|
|
537
|
+
controller.setSearchPanel(undefined);
|
|
562
538
|
});
|
|
563
539
|
return;
|
|
564
540
|
case 'think': {
|
|
565
541
|
if (submission.target === 'live') {
|
|
566
|
-
setLiveReasoning(
|
|
567
|
-
|
|
568
|
-
setScroll(0);
|
|
542
|
+
controller.setLiveReasoning(controller.view.liveReasoning === 'row' ? 'full' : 'row');
|
|
543
|
+
controller.openThoughts(false);
|
|
544
|
+
controller.setScroll(0);
|
|
569
545
|
}
|
|
570
546
|
else if (submission.target) {
|
|
571
547
|
const seq = Number(submission.target);
|
|
@@ -573,23 +549,23 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
573
549
|
throw new Error('Use /think <message sequence> for a loaded reasoning block');
|
|
574
550
|
const next = new Set(reasoningOverrides);
|
|
575
551
|
if (next.delete(seq)) {
|
|
576
|
-
|
|
552
|
+
controller.setFolds(next);
|
|
577
553
|
return;
|
|
578
554
|
}
|
|
579
555
|
next.add(seq);
|
|
580
|
-
|
|
556
|
+
controller.setFolds(next);
|
|
581
557
|
await jumpHistory(seq, next);
|
|
582
558
|
}
|
|
583
559
|
else {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
560
|
+
controller.setHistoryPanel(undefined);
|
|
561
|
+
controller.setSearchPanel(undefined);
|
|
562
|
+
controller.openThoughts(true);
|
|
587
563
|
}
|
|
588
564
|
return;
|
|
589
565
|
}
|
|
590
566
|
case 'older':
|
|
591
567
|
await controller.older(undefined, displayTranscript);
|
|
592
|
-
setScroll(
|
|
568
|
+
controller.setScroll(controller.view.scroll + 10);
|
|
593
569
|
return;
|
|
594
570
|
case 'compact':
|
|
595
571
|
setNotice(undefined);
|
|
@@ -624,8 +600,8 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
624
600
|
case 'error': throw new Error(submission.message);
|
|
625
601
|
case 'prompt':
|
|
626
602
|
await controller.prompt(submission.text);
|
|
627
|
-
|
|
628
|
-
setScroll(0);
|
|
603
|
+
controller.setViewWindow(undefined);
|
|
604
|
+
controller.setScroll(0);
|
|
629
605
|
return;
|
|
630
606
|
}
|
|
631
607
|
});
|
|
@@ -682,7 +658,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
682
658
|
...controller.visibleSessions.map(session => ({ key: string(session.sessionId),
|
|
683
659
|
label: `${sessionStatus(session, listAge, isPending(session))} ${sessionLabel(session)} ${session.sessionId}`,
|
|
684
660
|
remove: () => operate(() => requestRemoval('session', string(session.sessionId))),
|
|
685
|
-
action: () => { setScroll(0); operate(() => controller.selectSession(string(session.sessionId))); } })),
|
|
661
|
+
action: () => { controller.setScroll(0); operate(() => controller.selectSession(string(session.sessionId))); } })),
|
|
686
662
|
{ key: '@back', label: '← Workspaces', action: () => operate(() => controller.showPicker('workspaces')) },
|
|
687
663
|
];
|
|
688
664
|
const layout = useMemo(() => historyLayout(displayTranscript, width, reasoning, reasoningOverrides, liveReasoning), [displayTranscript, displayTranscript.version, width, reasoning, reasoningOverrides, liveReasoning]);
|
|
@@ -690,31 +666,30 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
690
666
|
const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
|
|
691
667
|
const showHistoryHint = dialogOpen || displayTranscript.hasMore || !!historyWindow;
|
|
692
668
|
const pageSize = Math.max(1, conversationRows - (showHistoryHint ? 1 : 0));
|
|
693
|
-
const previousView = useRef({ transcript: displayTranscript, session: state.
|
|
669
|
+
const previousView = useRef({ transcript: displayTranscript, session: state.session.record, count: length, first, folds: reasoningOverrides, liveReasoning });
|
|
694
670
|
const previous = previousView.current;
|
|
695
671
|
const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
|
|
696
|
-
const adjustedScroll = previous.session !== state.
|
|
672
|
+
const adjustedScroll = previous.session !== state.session.record ? 0
|
|
697
673
|
: scroll > 0 && previous.transcript === displayTranscript && !prepended && previous.folds === reasoningOverrides && previous.liveReasoning === liveReasoning ? Math.max(0, scroll + length - previous.count) : scroll;
|
|
698
674
|
const maxScroll = Math.max(0, length - pageSize);
|
|
699
675
|
const position = Math.min(adjustedScroll, maxScroll);
|
|
700
676
|
useLayoutEffect(() => {
|
|
701
|
-
previousView.current = { transcript: displayTranscript, session: state.
|
|
677
|
+
previousView.current = { transcript: displayTranscript, session: state.session.record, count: length, first, folds: reasoningOverrides, liveReasoning };
|
|
702
678
|
if (position !== scroll)
|
|
703
|
-
setScroll(position);
|
|
704
|
-
}, [state.
|
|
679
|
+
controller.setScroll(position);
|
|
680
|
+
}, [state.session.record, displayTranscript, length, position, scroll, reasoningOverrides, liveReasoning]);
|
|
705
681
|
useLayoutEffect(() => {
|
|
706
682
|
controller.pinHistory(!historyWindow && (position > 0 || thoughtList || historyQuery !== undefined && !contentSearch));
|
|
707
|
-
}, [controller, historyWindow, position, thoughtList, historyQuery, contentSearch, state.
|
|
683
|
+
}, [controller, historyWindow, position, thoughtList, historyQuery, contentSearch, state.session.record]);
|
|
708
684
|
useEffect(() => {
|
|
709
685
|
const first = displayTranscript.beforeSeq;
|
|
710
|
-
if (first
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
686
|
+
if (first === undefined)
|
|
687
|
+
return;
|
|
688
|
+
const previous = controller.view.folds;
|
|
689
|
+
const next = new Set([...previous].filter(seq => seq >= first));
|
|
690
|
+
if (next.size !== previous.size)
|
|
691
|
+
controller.setFolds(next);
|
|
715
692
|
}, [displayTranscript, displayTranscript.memoryRevision]);
|
|
716
|
-
const scrollPosition = useRef(position);
|
|
717
|
-
scrollPosition.current = position;
|
|
718
693
|
const loadingPage = useRef(false);
|
|
719
694
|
const scrollIntent = useRef(0);
|
|
720
695
|
const mounted = useRef(true);
|
|
@@ -723,16 +698,15 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
723
698
|
if (copyMode || state.screen !== 'chat')
|
|
724
699
|
return;
|
|
725
700
|
const intent = ++scrollIntent.current;
|
|
726
|
-
const next = Math.max(0, Math.min(maxScroll,
|
|
727
|
-
|
|
728
|
-
setScroll(next);
|
|
701
|
+
const next = Math.max(0, Math.min(maxScroll, controller.view.scroll + delta));
|
|
702
|
+
controller.setScroll(next);
|
|
729
703
|
if (delta <= 0 || next < maxScroll || loadingPage.current || state.busy || !state.online || !displayTranscript.ready || !displayTranscript.hasMore)
|
|
730
704
|
return;
|
|
731
705
|
loadingPage.current = true;
|
|
732
706
|
const transcript = displayTranscript;
|
|
733
707
|
void controller.perform(() => historyOperation(signal => controller.older(signal, transcript))).then(accepted => {
|
|
734
708
|
if (accepted && mounted.current && displayRef.current === transcript && scrollIntent.current === intent) {
|
|
735
|
-
setScroll(
|
|
709
|
+
controller.setScroll(controller.view.scroll + delta);
|
|
736
710
|
}
|
|
737
711
|
}).finally(() => { loadingPage.current = false; });
|
|
738
712
|
}
|
|
@@ -750,14 +724,51 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
750
724
|
}
|
|
751
725
|
}
|
|
752
726
|
}
|
|
727
|
+
/** Reach one prompt older than the index's boundary, from memory first and the host second.
|
|
728
|
+
*
|
|
729
|
+
* The transcript window outlives the prompt index's budgets, so an evicted prompt is usually still
|
|
730
|
+
* loaded: refilling from it costs nothing and keeps the key immediate. Only when the window itself
|
|
731
|
+
* is exhausted does this page back, skipping tool-only pages inside one bounded request loop; the
|
|
732
|
+
* fetched page stays readable in the conversation above the composer.
|
|
733
|
+
*/
|
|
734
|
+
async function recallOlderPrompts() {
|
|
735
|
+
if (historyPaging.current)
|
|
736
|
+
return;
|
|
737
|
+
const transcript = state.session.record;
|
|
738
|
+
const sessionId = state.sessionId;
|
|
739
|
+
// Recover from the loaded window first: no request, no focus change, no loading label.
|
|
740
|
+
if (controller.refillRecall() || !transcript.hasMore) {
|
|
741
|
+
setInput(controller.recall(-1, controller.composer.draft), true);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
historyPaging.current = true;
|
|
745
|
+
try {
|
|
746
|
+
const accepted = await controller.perform(() => historyOperation(async (signal) => {
|
|
747
|
+
// A prompt can be many pages back in an agent session, so scan a healthy stretch before
|
|
748
|
+
// giving up; the loading label stays visible and Esc cancels the scan.
|
|
749
|
+
for (let page = 0; page < RECALL_PAGE_SCAN; page++) {
|
|
750
|
+
if (transcript.beforeSeq === undefined || !transcript.hasMore)
|
|
751
|
+
break;
|
|
752
|
+
await controller.older(signal, transcript);
|
|
753
|
+
signal.throwIfAborted();
|
|
754
|
+
if (controller.refillRecall())
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}, 'Loading older prompts…'));
|
|
758
|
+
if (accepted && mounted.current && controller.state.sessionId === sessionId) {
|
|
759
|
+
setInput(controller.recall(-1, controller.composer.draft), true);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
finally {
|
|
763
|
+
historyPaging.current = false;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
753
766
|
async function openSearchSession(sessionId, query) {
|
|
754
767
|
await historyOperation(async (signal) => {
|
|
755
768
|
await controller.selectSession(sessionId);
|
|
756
769
|
await controller.waitForHistory(signal);
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
setContentSearch(true);
|
|
760
|
-
setHistoryQuery(query);
|
|
770
|
+
controller.setHistoryPanel({ query, contentSearch: true, matches: await controller.searchHistory(query, signal) });
|
|
771
|
+
controller.setSearchPanel(undefined);
|
|
761
772
|
});
|
|
762
773
|
}
|
|
763
774
|
async function jumpHistory(target, folds = reasoningOverrides) {
|
|
@@ -768,18 +779,17 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
768
779
|
historyAbort.current = abort;
|
|
769
780
|
try {
|
|
770
781
|
const transcript = displayTranscript.messages.some(message => message.seq === target) ? displayTranscript
|
|
771
|
-
: state.
|
|
782
|
+
: state.session.record.messages.some(message => message.seq === target) ? state.session.record
|
|
772
783
|
: await controller.historyAt(target, abort.signal);
|
|
773
784
|
abort.signal.throwIfAborted();
|
|
774
785
|
const current = historyLayout(transcript, width, reasoning, folds, liveReasoning);
|
|
775
786
|
const row = current.offsets.get(target);
|
|
776
787
|
if (row === undefined)
|
|
777
788
|
throw new Error('No visible message at this sequence; use /history to choose a record');
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
setScroll(Math.max(0, current.length - pageSize - row));
|
|
789
|
+
controller.setViewWindow(transcript === state.session.record ? undefined : transcript);
|
|
790
|
+
controller.setHistoryPanel(undefined);
|
|
791
|
+
controller.openThoughts(false);
|
|
792
|
+
controller.setScroll(Math.max(0, current.length - pageSize - row));
|
|
783
793
|
}
|
|
784
794
|
finally {
|
|
785
795
|
if (historyAbort.current === abort)
|
|
@@ -794,18 +804,18 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
794
804
|
const trailingGap = dialogOpen && length > 0 && layout.viewport(length - 1, length)[0]?.text === '' ? 1 : 0;
|
|
795
805
|
const end = Math.max(pageSize, length - position - trailingGap);
|
|
796
806
|
const visible = useMemo(() => layout.viewport(Math.max(0, end - pageSize), end), [layout, end, pageSize]);
|
|
797
|
-
const liveThought = thoughtList && !historyWindow ? state.
|
|
807
|
+
const liveThought = thoughtList && !historyWindow ? state.session.record.liveParts(width).find(part => part.kind === 'reasoning') : undefined;
|
|
798
808
|
const thoughtEntries = thoughtList ? displayTranscript.thoughts : undefined;
|
|
799
809
|
const thoughtChoices = useMemo(() => [...(thoughtEntries ?? [])].reverse().map(entry => ({
|
|
800
810
|
key: String(entry.seq), label: `${toolLine(`#${entry.seq} User · ${entry.prompt}`, width - 2)}\n ${toolLine(`◇ ${entry.preview}`, width - 4)}`,
|
|
801
|
-
action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq);
|
|
811
|
+
action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq); controller.setFolds(next); await jumpHistory(entry.seq, next); }),
|
|
802
812
|
})), [thoughtEntries, width, pageSize, reasoningOverrides, liveReasoning, displayTranscript]);
|
|
803
813
|
const thoughtOptions = useMemo(() => [
|
|
804
|
-
...(liveThought ? [{ key: 'live', label: `${toolLine(`Now · User · ${state.
|
|
805
|
-
action: () => { setLiveReasoning('full');
|
|
814
|
+
...(liveThought ? [{ key: 'live', label: `${toolLine(`Now · User · ${state.session.record.latestPrompt}`, width - 2)}\n ${toolLine(liveThought.text, width - 4)}`,
|
|
815
|
+
action: () => { controller.setLiveReasoning('full'); controller.openThoughts(false); const expanded = historyLayout(state.session.record, width, reasoning, reasoningOverrides, 'full'); controller.setScroll(Math.max(0, expanded.length - pageSize - expanded.liveOffset)); } }] : []),
|
|
806
816
|
...thoughtChoices,
|
|
807
817
|
...(displayTranscript.hasMore ? [{ key: 'older', label: '↑ Load older reasoning', action: () => operate(() => historyOperation(signal => controller.older(signal, displayTranscript))) }] : []),
|
|
808
|
-
{ key: 'close', label: '← Back to conversation', action: () =>
|
|
818
|
+
{ key: 'close', label: '← Back to conversation', action: () => controller.openThoughts(false) },
|
|
809
819
|
], [liveThought, thoughtChoices, displayTranscript, displayTranscript.hasMore, width, pageSize, reasoningOverrides]);
|
|
810
820
|
const workspace = state.workspaces.find(item => item.workspaceId === state.workspaceId);
|
|
811
821
|
const workspaceName = workspace ? string(workspace.title) || string(workspace.path) : state.workspaceId;
|
|
@@ -820,13 +830,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
820
830
|
: state.screen === 'chat' && position > 0 ? 'history'
|
|
821
831
|
: undefined;
|
|
822
832
|
const statusFrozen = pauseReason !== undefined;
|
|
823
|
-
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(ChatHeader, { title: headerTitle, mode: controller.sessionMode, width: width, frozen: displayPaused, identity: `${width}:${state.sessionId}` }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [historyLoading && _jsxs(Text, { dimColor: true, children: [historyLoading, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: statusPaused, 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 }), state.screen === 'chat' && _jsx(ChatViewport, { rows: visible, showHistoryHint: showHistoryHint, dialogOpen: dialogOpen, historyWindow: !!historyWindow, frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, boxRef: conversationBox })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: safeText(notice) }), _jsxs(Box, { borderStyle: "round", borderColor: pending ? theme.colors.context : state.online ? theme.accent : theme.border, paddingX: 1, flexDirection: "column", flexShrink: 1, minHeight: 3, children: [_jsx(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: queueOpen && !pending ? _jsx(QueueDialog, { queued: queued, rows: stdout.rows ?? 30, width: width, unavailable: !!state.controlError, enabled: !input && !state.busy && state.online, canSelect: () => !draft
|
|
833
|
+
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(ChatHeader, { title: headerTitle, mode: controller.sessionMode, width: width, frozen: displayPaused, identity: `${width}:${state.sessionId}` }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 0, overflowY: "hidden", children: [historyLoading && _jsxs(Text, { dimColor: true, children: [historyLoading, " \u00B7 Esc / Ctrl+C cancel"] }), _jsx(Frozen, { frozen: statusPaused, 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 }), state.screen === 'chat' && _jsx(ChatViewport, { rows: visible, showHistoryHint: showHistoryHint, dialogOpen: dialogOpen, historyWindow: !!historyWindow, frozen: displayPaused, identity: `${width}:${state.sessionId}:${position}:${pageSize}`, boxRef: conversationBox })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [notice && _jsx(Text, { dimColor: true, children: safeText(notice) }), _jsxs(Box, { borderStyle: "round", borderColor: pending ? theme.colors.context : state.online ? theme.accent : theme.border, paddingX: 1, flexDirection: "column", flexShrink: 1, minHeight: 3, children: [_jsx(Box, { flexDirection: "column", flexShrink: 1, minHeight: 0, overflowY: "hidden", children: queueOpen && !pending ? _jsx(QueueDialog, { queued: queued, rows: stdout.rows ?? 30, width: width, unavailable: !!state.controlError, enabled: !input && !state.busy && state.online, canSelect: () => !controller.composer.draft && !controller.state.busy && controller.state.online && !controller.state.pending.length, onRemove: id => operate(() => controller.removeQueued(id)) }) : removal ? _jsx(RemovalDialog, { removal: removal, enabled: !input && !state.busy && state.online, canSelect: () => !controller.composer.draft && !controller.state.busy && controller.state.online, onCancel: () => setRemoval(undefined), onConfirm: () => operate(async () => { await controller.removeTarget(removal); setRemoval(undefined); }) }) : models ? _jsx(ModelDialog, { models: models, rows: stdout.rows ?? 30, width: width, enabled: !input && !state.busy && state.online, canSelect: () => !controller.composer.draft && !controller.state.busy && controller.state.online, onChoose: (provider, model, effort) => operate(async () => { await controller.selectModel(provider, model, effort); controller.setModelPanel(undefined); }), onOpen: (provider, model) => controller.setModelPanel({ catalog: models.catalog, provider, model }), onBack: () => controller.setModelPanel({ catalog: models.catalog }), onClose: () => controller.setModelPanel(undefined) }) : searchResults ? _jsx(SearchResultsDialog, { query: searchResults.query, items: searchResults.items, hasMore: searchResults.hasMore, width: width, enabled: !input && !state.busy, canSelect: () => !controller.composer.draft && !controller.state.busy, onOpen: sessionId => operate(() => openSearchSession(sessionId, searchResults.query)), onClose: () => controller.setSearchPanel(undefined) }) : state.screen === 'workspaces' || state.screen === 'sessions' ? _jsx(PickerScreen, { title: state.screen === 'workspaces' ? 'Choose workspace' : state.showAllSessions ? 'Choose session · All workspaces' : 'Choose session', identity: `${state.screen}:${state.workspaceId ?? ''}`, choices: choices, width: pickerWidth,
|
|
824
834
|
// A rollup of badges is read through the key beside it; spelled-out states need no key, and a
|
|
825
835
|
// terminal too narrow for the whole key gets the badges alone rather than half a legend.
|
|
826
|
-
legend: state.screen === 'workspaces' && rollupStyle === 'badges' && pickerWidth >= 40 ? ROLLUP_LEGEND : undefined, enabled: state.online && !state.busy && !input, canSelect: () => !draft
|
|
836
|
+
legend: state.screen === 'workspaces' && rollupStyle === 'badges' && pickerWidth >= 40 ? ROLLUP_LEGEND : undefined, enabled: state.online && !state.busy && !input, canSelect: () => !controller.composer.draft && controller.state.online && !controller.state.busy }) : _jsxs(_Fragment, { children: [thoughtList && _jsx(ThoughtsDialog, { identity: `thoughts:${state.sessionId}`, options: thoughtOptions, empty: !thoughtEntries?.length && !liveThought, rows: stdout.rows ?? 30, enabled: !input && !state.busy, canSelect: () => !controller.composer.draft && !controller.state.busy }), state.screen === 'chat' && historyQuery !== undefined && _jsx(HistoryDialog, { identity: `history:${historyQuery}`, contentSearch: contentSearch, matches: historyMatches, query: historyQuery, messages: layout.messages, width: width, enabled: !input && !state.busy, canSelect: () => !controller.composer.draft && !controller.state.busy, onJump: seq => operate(() => jumpHistory(seq)), onClose: () => controller.setHistoryPanel(undefined) }), pending && _jsxs(Box, { flexShrink: 0, 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)) }), pending.event === 'approval/request' && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [['Allow once', 'Deny', 'Stop turn'].map((label, index) => _jsxs(Text, { color: approvalIndex === index ? theme.accent : undefined, children: [approvalIndex === index ? '❯ ' : ' ', index + 1, ". ", label] }, label)), _jsx(Text, { dimColor: true, children: "\u2191 \u2193 / 1\u20133 select \u00B7 Enter confirm" })] }), options.length > 0 && _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [[...options, { label: 'Other answer — type below' }].map((option, index) => ({ option, index }))
|
|
827
837
|
.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
|
|
828
838
|
? '↑ ↓ move · Space / 1–9 toggle · Enter confirm · Esc dismisses' : '↑ ↓ / 1–9 select · Enter confirm · Esc dismisses' })] }), _jsx(Text, { dimColor: true, children: question ? options.length > 0
|
|
829
839
|
? 'Choose "Other answer" to type · the draft is kept while this is open'
|
|
830
840
|
: 'Esc dismisses the question · the draft is kept while this is open'
|
|
831
|
-
: 'Esc keeps this pending · the draft is kept while this is open' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsx(QueuedPreview, { queued: queued, width: width }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange:
|
|
841
|
+
: 'Esc keeps this pending · the draft is kept while this is open' })] })] }) }), !queueOpen && !pending && state.screen === 'chat' && queued.length > 0 && _jsx(QueuedPreview, { queued: queued, width: width }), _jsx(TextInput, { value: input, onChange: setInput, onCursorChange: cursor => controller.setComposerCursor(cursor), onSubmit: () => { void submit(controller.composer.draft); }, reservedKeys: approvalKeysActive ? ['1', '2', '3'] : queueOpen && !pending ? ['d'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : undefined, width: draftWidth, maxRows: composerRows, promptColor: answerPending ? theme.colors.muted : theme.accent, focus: state.online && !state.busy && !copyMode && !answerPending, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' }), referenceOpen && _jsx(ReferenceMenu, { matches: matches, index: referenceIndex })] }), commandSuggestions && _jsx(Text, { dimColor: true, children: commandSuggestions.join(' ') }), help && _jsx(HelpPanel, { page: currentHelpPage, pages: helpPages, pageSize: helpPageSize }), costExpanded && _jsx(CostPanel, { controller: controller }), _jsx(Frozen, { frozen: statusFrozen, identity: `${width}:${state.sessionId}:${statusExpanded}:${statusScroll}:${pauseReason ?? ''}`, children: _jsx(StatusBar, { controller: controller, width: width, expanded: statusExpanded, scroll: statusScroll, pageSize: statusViewRows, onScroll: setStatusScroll, onOverflow: setStatusOverflow, onRows: setStatusBarRows, pauseReason: pauseReason, revision: state.version }) })] })] }) }) }) });
|
|
832
842
|
}
|