@itookit/dsht 0.5.1 → 0.5.2
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 +4 -2
- package/README.zh.md +6 -4
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +4 -1
- package/dist/cli/startup.js +30 -11
- package/dist/cli/verifier.d.ts +4 -0
- package/dist/cli/verifier.js +28 -5
- package/dist/contracts.d.ts +20 -5
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +14 -3
- package/dist/controller/connection.js +40 -69
- package/dist/controller/controller.d.ts +17 -233
- package/dist/controller/controller.js +108 -811
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/verifier.d.ts +4 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/scanner.js +1 -0
- package/dist/session/controller.d.ts +23 -35
- package/dist/session/controller.js +113 -363
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/info.d.ts +3 -38
- package/dist/session/info.js +14 -1
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +15 -6
- package/dist/session/telemetry.js +44 -7
- package/dist/session/transcript.d.ts +5 -1
- package/dist/slash/index.d.ts +1 -1
- package/dist/slash/parse.d.ts +2 -126
- package/dist/slash/registry.d.ts +1 -1
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +5 -17
- package/dist/state.js +1 -1
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/ui/app.js +86 -301
- package/dist/ui/chat/shell-view.d.ts +2 -0
- package/dist/ui/chat/shell-view.js +8 -0
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/package.json +1 -1
package/dist/ui/app.js
CHANGED
|
@@ -5,6 +5,9 @@ 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 { useComposer } from "./input/use-composer.js";
|
|
9
|
+
import { useDeferredLines } from "./input/use-deferred-lines.js";
|
|
10
|
+
import { useHistoryRecall } from "./input/use-history-recall.js";
|
|
8
11
|
import { ReferenceMenu } from "./input/references.js";
|
|
9
12
|
import { costText } from "./status/model.js";
|
|
10
13
|
import { toolLine } from "../text.js";
|
|
@@ -14,11 +17,13 @@ import { StatusBar } from "./chat/status.js";
|
|
|
14
17
|
import { ChatHeader } from "./chat/header.js";
|
|
15
18
|
import { LoopStatus } from "./chat/loop-status.js";
|
|
16
19
|
import { ChatViewport } from "./chat/viewport.js";
|
|
17
|
-
import {
|
|
20
|
+
import { plainRows } from "./chat/shell-view.js";
|
|
21
|
+
import { useHistoryView } from "./chat/use-history-view.js";
|
|
18
22
|
import { Frozen } from "./frozen.js";
|
|
19
23
|
import { CopyMode } from "./copy-mode.js";
|
|
20
24
|
import { HelpPanel, HistoryDialog, ModelDialog, PickerScreen, PromptsDialog, QueueDialog, QueuedPreview, RemovalDialog, SearchResultsDialog, ThoughtsDialog } from "./dialogs/index.js";
|
|
21
25
|
import { LoopDialog, LoopMenu } from "./dialogs/loop.js";
|
|
26
|
+
import { usePanels } from "./dialogs/use-panels.js";
|
|
22
27
|
import { PeekPanel } from "./dialogs/peek.js";
|
|
23
28
|
import { COMMAND_HINTS, argumentHint, completeCommand as completeDraft, suggestedCommands } from "../slash/registry.js";
|
|
24
29
|
import { interpret, normalize, authorize } from "../slash/index.js";
|
|
@@ -31,8 +36,6 @@ import { errorText } from "../text.js";
|
|
|
31
36
|
import { safeText } from "../text.js";
|
|
32
37
|
/** Transient notices expire; interactive panels remain open until dismissed. */
|
|
33
38
|
const PANEL_LIFETIME_MS = 10_000;
|
|
34
|
-
/** Pages one boundary recall press may walk before it reports that nothing older holds a prompt. */
|
|
35
|
-
const RECALL_PAGE_SCAN = 20;
|
|
36
39
|
/** No folds: the read-only view has no expand state of its own, and one empty set keeps layout cached. */
|
|
37
40
|
const NO_FOLDS = new Set();
|
|
38
41
|
/** Rows one PgUp/PgDn moves in the read-only view. */
|
|
@@ -51,43 +54,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
51
54
|
const { exit } = useApp();
|
|
52
55
|
const { stdout } = useStdout();
|
|
53
56
|
const [copyMode, setCopyMode] = useState(false);
|
|
54
|
-
// The composer belongs to the selected session: it resets when another session opens, and every
|
|
55
|
-
// callback reads the controller rather than a render closure Ink may not have refreshed yet.
|
|
56
|
-
const [input, setInputValue] = useState('');
|
|
57
|
-
const [cursor, setCursor] = useState(0);
|
|
58
|
-
// The freshest draft, readable from an async continuation: a command that finishes while the
|
|
59
|
-
// operator is already writing the next line must not clear that line (D1).
|
|
60
|
-
const inputRef = useRef(input);
|
|
61
|
-
inputRef.current = input;
|
|
62
|
-
const parkedDraft = useRef('');
|
|
63
|
-
const historyPaging = useRef(false);
|
|
64
|
-
const setInput = (value, recalled = false) => {
|
|
65
|
-
if (!recalled)
|
|
66
|
-
controller.actions.resetRecall();
|
|
67
|
-
// A new message draft returns the view to the live end, so composing never needs a scroll first.
|
|
68
|
-
// A slash command is not a message, and the reader keeps their place while typing one.
|
|
69
|
-
if (state.screen === 'chat' && input === '' && value !== '' && !value.startsWith('/'))
|
|
70
|
-
setScroll(0);
|
|
71
|
-
// Replacing the text parks the caret at its end, exactly as the session-owned composer did; the
|
|
72
|
-
// editor reports its real caret straight afterwards when the change came from an in-place edit.
|
|
73
|
-
setInputValue(value);
|
|
74
|
-
setCursor(value.length);
|
|
75
|
-
};
|
|
76
|
-
// The reading view is session-owned too: which record is shown, where the reader is, and what is
|
|
77
|
-
// expanded. `SessionInfo` releases the detached window, so the composer and this view reset together.
|
|
78
|
-
// Reading position, folds and the live-reasoning fold mode are component state: switching the
|
|
79
|
-
// session remounts the screen, and the record's own detached window stays session-owned.
|
|
80
|
-
const [scroll, setScroll] = useState(0);
|
|
81
|
-
const [reasoningOverrides, setReasoningOverrides] = useState(new Set());
|
|
82
|
-
const [liveReasoning, setLiveReasoning] = useState('row');
|
|
83
57
|
const historyWindow = state.session.window;
|
|
84
58
|
/** The operation the controller owns right now; the front end only renders it. */
|
|
85
59
|
const foreground = controller.queries.foreground;
|
|
86
|
-
/** Lines the policy accepted but held until a turn, a loop or the foreground slot clears (A2). */
|
|
87
|
-
const [queuedLines, setQueuedLines] = useState([]);
|
|
88
60
|
const displayTranscript = historyWindow ?? state.session.record;
|
|
89
|
-
const displayRef = useRef(displayTranscript);
|
|
90
|
-
displayRef.current = displayTranscript;
|
|
91
61
|
const conversationBox = useRef(null);
|
|
92
62
|
const [conversationRows, setConversationRows] = useState(20);
|
|
93
63
|
useLayoutEffect(() => {
|
|
@@ -97,30 +67,34 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
97
67
|
setConversationRows(height);
|
|
98
68
|
}
|
|
99
69
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
// sending, and Esc clears the draft. The composition root stays generic about who asked.
|
|
105
|
-
const [composerIntent, setComposerIntent] = useState();
|
|
106
|
-
// Panels are component state: visibility and query text only, because every row comes from the record.
|
|
107
|
-
const [panels, setPanels] = useState({ thoughts: false, queue: false });
|
|
108
|
-
const { thoughts: thoughtList, queue: queueOpen, model: models, history, search: searchResults, prompts: promptsOpen } = panels;
|
|
109
|
-
const openThoughts = (open) => setPanels(current => ({ ...current, thoughts: open }));
|
|
110
|
-
const openQueue = (open) => setPanels(current => ({ ...current, queue: open }));
|
|
111
|
-
const openPrompts = (open) => setPanels(current => ({ ...current, prompts: open }));
|
|
112
|
-
const setModelPanel = (model) => setPanels(current => ({ ...current, model }));
|
|
113
|
-
const setHistoryPanel = (history) => setPanels(current => ({ ...current, history }));
|
|
114
|
-
const setSearchPanel = (search) => setPanels(current => ({ ...current, search }));
|
|
70
|
+
const panelState = usePanels({ sessionId: state.sessionId, pendingId: state.pending[0]?.eventId,
|
|
71
|
+
peekOpen: controller.queries.peek !== undefined, closePeek: () => controller.actions.closePeek() });
|
|
72
|
+
const { openThoughts, openQueue, openPrompts, setModelPanel, setHistoryPanel, setSearchPanel, setRemoval, setLoopForm, setHelp, setCostExpanded, setStatusExpanded } = panelState;
|
|
73
|
+
const { thoughts: thoughtList, queue: queueOpen, model: models, history, search: searchResults, prompts: promptsOpen, removal, loop: loopForm, help, cost: costExpanded, status: statusExpanded } = panelState.state;
|
|
115
74
|
const historyQuery = history?.query;
|
|
116
75
|
const contentSearch = history?.contentSearch === true;
|
|
117
76
|
const historyMatches = history?.matches;
|
|
118
|
-
const [costExpanded, setCostExpanded] = useState(false);
|
|
119
|
-
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
120
77
|
const reasoning = 'row';
|
|
121
|
-
useEffect(() => { setLiveReasoning('row'); }, [state.session.record, state.session.record.liveAttemptKey]);
|
|
122
78
|
const [notice, setNotice] = useState();
|
|
123
|
-
const
|
|
79
|
+
const composer = useComposer({
|
|
80
|
+
sessionId: state.sessionId,
|
|
81
|
+
currentSession: () => controller.snapshot().sessionId,
|
|
82
|
+
resetRecall: () => controller.actions.resetRecall(),
|
|
83
|
+
messageStarted: () => { if (controller.snapshot().screen === 'chat')
|
|
84
|
+
setScroll(0); },
|
|
85
|
+
notify: setNotice,
|
|
86
|
+
});
|
|
87
|
+
const { input, cursor, intent: composerIntent, setInput, setCursor, setIntent: setComposerIntent } = composer;
|
|
88
|
+
const recallOlderPrompts = useHistoryRecall({
|
|
89
|
+
source: state.session.record, draft: input,
|
|
90
|
+
capture: composer.capture, restore: composer.recallIfCurrent,
|
|
91
|
+
previous: draft => controller.queries.recall(-1, draft),
|
|
92
|
+
refill: () => controller.actions.refillRecall(),
|
|
93
|
+
hasPrevious: () => controller.queries.recallLength > 0 && !controller.queries.recallAtOldest,
|
|
94
|
+
older: (signal, source) => controller.actions.older(signal, source),
|
|
95
|
+
run: work => controller.actions.foreground('history', 'Loading older prompts…', work),
|
|
96
|
+
notify: setNotice,
|
|
97
|
+
});
|
|
124
98
|
const [helpPage, setHelpPage] = useState(0);
|
|
125
99
|
const helpPageSize = Math.max(1, (stdout.rows ?? 30) - 12);
|
|
126
100
|
const helpPages = Math.ceil(COMMAND_HINTS.length / helpPageSize);
|
|
@@ -194,24 +168,11 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
194
168
|
setChromeRows({ header, prefix });
|
|
195
169
|
});
|
|
196
170
|
useEffect(() => {
|
|
197
|
-
setPanels({ thoughts: false, queue: false });
|
|
198
171
|
setReferenceIndex(0);
|
|
199
172
|
setDismissedReference(undefined);
|
|
200
|
-
setInputValue('');
|
|
201
|
-
setCursor(0);
|
|
202
|
-
parkedDraft.current = '';
|
|
203
|
-
setComposerIntent(undefined);
|
|
204
|
-
setScroll(0);
|
|
205
|
-
setReasoningOverrides(new Set());
|
|
206
|
-
setLiveReasoning('row');
|
|
207
|
-
setLoopForm(undefined);
|
|
208
173
|
setLoopMenuIndex(0);
|
|
209
174
|
setDismissedLoopMenu(undefined);
|
|
210
175
|
}, [state.sessionId]);
|
|
211
|
-
useEffect(() => { openQueue(false); }, [state.sessionId, pending?.eventId]);
|
|
212
|
-
// A pending interaction owns the keyboard, so the loop form yields rather than swallowing its keys.
|
|
213
|
-
useEffect(() => { if (pending)
|
|
214
|
-
setLoopForm(undefined); }, [pending?.eventId]);
|
|
215
176
|
// A replayed interaction (same eventId after a reconnect) starts unselected again.
|
|
216
177
|
useEffect(() => { controller.actions.setApproval(undefined); }, [state.sessionId, state.online, pending?.eventId]);
|
|
217
178
|
const token = state.screen === 'chat' && state.online && foreground === undefined && !pending
|
|
@@ -228,25 +189,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
228
189
|
: controller.queries.loopRecords.filter(record => record.name.startsWith(loopDraft));
|
|
229
190
|
const loopMenuOpen = loopCandidates.length > 0 && loopForm === undefined && dismissedLoopMenu !== input;
|
|
230
191
|
const loopMenuCursor = Math.min(loopMenuIndex, Math.max(0, loopCandidates.length - 1));
|
|
231
|
-
|
|
232
|
-
const surfaces = [
|
|
233
|
-
// The read-only view is a whole screen, so its rule leads: any other panel open under it is
|
|
234
|
-
// unreachable until it closes, and it is the only surface the reader can see.
|
|
235
|
-
{ name: 'peek', open: !!peek, arrows: true, blocksKeys: true, close: () => controller.actions.closePeek() },
|
|
236
|
-
{ name: 'queue', open: queueOpen && !pending, reserved: ['d'], close: () => openQueue(false) },
|
|
237
|
-
{ name: 'prompts', open: !!promptsOpen, arrows: true, blocksKeys: true, reserved: ['d', 'e'], close: () => openPrompts(false) },
|
|
238
|
-
{ name: 'removal', open: !!removal, arrows: true, blocksKeys: true, close: () => setRemoval(undefined) },
|
|
239
|
-
{ name: 'model', open: !!models, arrows: true, blocksKeys: true, close: () => setModelPanel(undefined) },
|
|
240
|
-
{ name: 'loop', open: !!loopForm, arrows: true, blocksKeys: true, close: () => setLoopForm(undefined) },
|
|
241
|
-
{ name: 'thoughts', open: thoughtList, arrows: true, blocksKeys: true, close: () => openThoughts(false) },
|
|
242
|
-
{ name: 'history', open: historyQuery !== undefined, arrows: true, blocksKeys: true, close: () => setHistoryPanel(undefined) },
|
|
243
|
-
{ name: 'search', open: !!searchResults, arrows: true, blocksKeys: true, close: () => setSearchPanel(undefined) },
|
|
244
|
-
{ name: 'help', open: help, blocksKeys: true, close: () => setHelp(false) },
|
|
245
|
-
{ name: 'cost', open: costExpanded, blocksKeys: true, close: () => setCostExpanded(false) },
|
|
246
|
-
{ name: 'status', open: statusExpanded, blocksKeys: true, close: () => setStatusExpanded(false) },
|
|
247
|
-
];
|
|
248
|
-
const openSurfaces = surfaces.filter(surface => surface.open);
|
|
249
|
-
const surfaceReservedKeys = [...new Set(openSurfaces.flatMap(surface => surface.reserved ?? []))];
|
|
192
|
+
const { openSurfaces, reservedKeys: surfaceReservedKeys } = panelState;
|
|
250
193
|
const dialogOpen = !!(openSurfaces.length || pending || referenceOpen || state.screen !== 'chat');
|
|
251
194
|
const displayPaused = copyMode || dialogOpen;
|
|
252
195
|
// Startup screens need live connection feedback even while their picker remains open.
|
|
@@ -304,29 +247,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
304
247
|
// the composer itself for a free-text or "Other" answer, so it keeps the keys then.
|
|
305
248
|
const answerPending = !!pending && (pending.kind === 'approval' || options.length > 0 && !choiceState.custom);
|
|
306
249
|
useEffect(() => {
|
|
307
|
-
|
|
308
|
-
// keyboard and given back when it closes. Restoring writes the draft directly, so it neither
|
|
309
|
-
// counts as a recall nor moves a reader's scroll position.
|
|
310
|
-
if (answerPending) {
|
|
311
|
-
// A composer intent cannot survive an answer dialog taking the keyboard: the dialog's text is
|
|
312
|
-
// not what the intent edits, so the intent is dropped rather than committed.
|
|
313
|
-
if (composerIntent)
|
|
314
|
-
setComposerIntent(undefined);
|
|
315
|
-
if (input !== '') {
|
|
316
|
-
parkedDraft.current = input;
|
|
317
|
-
setInputValue('');
|
|
318
|
-
setCursor(0);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (pending)
|
|
323
|
-
return;
|
|
324
|
-
if (parkedDraft.current !== '') {
|
|
325
|
-
const parked = parkedDraft.current;
|
|
326
|
-
parkedDraft.current = '';
|
|
327
|
-
setInputValue(parked);
|
|
328
|
-
setCursor(parked.length);
|
|
329
|
-
}
|
|
250
|
+
composer.interactionChanged(answerPending, pending !== undefined);
|
|
330
251
|
}, [answerPending, pending]);
|
|
331
252
|
const approvalKeysActive = pending?.kind === 'approval' && !copyMode && !panelBlocksKeys;
|
|
332
253
|
const approvalIndex = approvalSelection?.eventId === eventId ? approvalSelection.index : -1;
|
|
@@ -651,7 +572,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
651
572
|
// past it fetches the page before the retained window, and the step is applied once the page
|
|
652
573
|
// lands, so recall covers prompts from before this client connected. Paging stays with the
|
|
653
574
|
// conversation: on a picker screen the arrows only ever walked what was already retained.
|
|
654
|
-
if (recallPrevious && state.screen === 'chat'
|
|
575
|
+
if (recallPrevious && state.screen === 'chat'
|
|
655
576
|
&& (controller.queries.recallAtOldest || controller.queries.recallLength === 0)
|
|
656
577
|
&& state.session.record.ready && controller.queries.recallHasOlder) {
|
|
657
578
|
void recallOlderPrompts();
|
|
@@ -671,15 +592,19 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
671
592
|
* pass one door and not the other (§3.4: authorize is per submission, not per user action).
|
|
672
593
|
*/
|
|
673
594
|
function applicationFacts() {
|
|
595
|
+
const current = controller.snapshot();
|
|
674
596
|
return {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
597
|
+
sessionId: current.sessionId,
|
|
598
|
+
ready: current.online && controller.queries.connectionSettled && (current.sessionId === undefined || controller.queries.record.ready),
|
|
599
|
+
sessionSelected: current.sessionId !== undefined,
|
|
600
|
+
pending: current.pending.length > 0,
|
|
601
|
+
foreground: controller.queries.foreground !== undefined,
|
|
678
602
|
// A loop is reported as such because stopping it is what the refusal has to name.
|
|
679
603
|
during: controller.queries.loop?.active === true ? 'loop'
|
|
680
604
|
: controller.queries.running ? 'turn' : 'idle',
|
|
681
605
|
};
|
|
682
606
|
}
|
|
607
|
+
const deferredLines = useDeferredLines({ facts: applicationFacts, run: command => runLine(command), notify: setNotice });
|
|
683
608
|
/** What a held line is waiting for, in the words the notice uses. */
|
|
684
609
|
function describeDefer(defer) {
|
|
685
610
|
return defer === 'busy' ? 'the running operation finishes' : defer === 'loop' ? 'the loop ends' : 'the turn finishes';
|
|
@@ -697,7 +622,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
697
622
|
return false;
|
|
698
623
|
const defer = verdict.defer;
|
|
699
624
|
controller.traceNote('command', { phase: 'queued', kind: verdict.command.kind, reason: defer });
|
|
700
|
-
|
|
625
|
+
deferredLines.enqueue(verdict.command, defer);
|
|
701
626
|
setNotice(`Queued ${label} · runs when ${describeDefer(verdict.defer)}`);
|
|
702
627
|
return true;
|
|
703
628
|
}
|
|
@@ -708,17 +633,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
708
633
|
during: facts.during, foreground: facts.foreground });
|
|
709
634
|
}
|
|
710
635
|
const submit = async (raw) => {
|
|
636
|
+
const receipt = composer.capture();
|
|
711
637
|
// A borrowed composer commits on Enter instead of sending, so an edit is never delivered.
|
|
712
638
|
if (composerIntent) {
|
|
713
|
-
|
|
714
|
-
if (!text) {
|
|
715
|
-
setNotice(composerIntent.emptyNotice);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
if (await composerIntent.commit(text)) {
|
|
719
|
-
setComposerIntent(undefined);
|
|
720
|
-
setInput('');
|
|
721
|
-
}
|
|
639
|
+
await composer.commit(raw);
|
|
722
640
|
return;
|
|
723
641
|
}
|
|
724
642
|
// A composer menu completes the draft before it is submitted: Enter on the record list accepts the
|
|
@@ -755,11 +673,11 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
755
673
|
const value = line.trim();
|
|
756
674
|
if (!pending && !/^\/feedback(?:\s|$)/.test(value))
|
|
757
675
|
controller.actions.recordRecall(value);
|
|
758
|
-
await runLine(executable,
|
|
676
|
+
await runLine(executable, receipt);
|
|
759
677
|
};
|
|
760
678
|
/** Run one authorized line under the UI's port and apply what it returns.
|
|
761
679
|
*
|
|
762
|
-
* `draft`
|
|
680
|
+
* `draft` identifies the draft to clear when the line is consumed; a line the UI runs from its own
|
|
763
681
|
* queue passes none, because that draft was used up when the operator submitted it.
|
|
764
682
|
*/
|
|
765
683
|
async function runLine(executable, draft) {
|
|
@@ -774,40 +692,13 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
774
692
|
// Consuming the line is the application's decision, so the composer never guesses it. Only the
|
|
775
693
|
// line that was submitted is cleared: the operation may have taken long enough for the operator
|
|
776
694
|
// to write the next one (D1), and that draft is theirs, not this result's to discard.
|
|
777
|
-
if (result.disposition === 'consume' && draft !== undefined
|
|
778
|
-
|
|
695
|
+
if (result.disposition === 'consume' && draft !== undefined)
|
|
696
|
+
composer.consume(draft);
|
|
779
697
|
}
|
|
780
698
|
catch (error) {
|
|
781
699
|
setNotice(errorText(error));
|
|
782
700
|
}
|
|
783
701
|
}
|
|
784
|
-
/** Drain one held line when the fact that deferred it has cleared.
|
|
785
|
-
*
|
|
786
|
-
* One per run, in arrival order: the state update re-runs this effect, so the queue empties without
|
|
787
|
-
* a loop of its own. A line whose session is gone is dropped rather than run against another
|
|
788
|
-
* conversation, and one that is deferred again goes back to the front.
|
|
789
|
-
*/
|
|
790
|
-
useEffect(() => {
|
|
791
|
-
const next = queuedLines[0];
|
|
792
|
-
if (next === undefined)
|
|
793
|
-
return;
|
|
794
|
-
const facts = applicationFacts();
|
|
795
|
-
const ready = !facts.foreground && (next.defer === 'busy' || facts.during === 'idle');
|
|
796
|
-
if (!ready)
|
|
797
|
-
return;
|
|
798
|
-
if (next.sessionId !== state.sessionId) {
|
|
799
|
-
setQueuedLines(list => list.slice(1));
|
|
800
|
-
setNotice('Dropped a queued line: the selected session changed');
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
const verdict = verdictFor(next.command);
|
|
804
|
-
if (verdict.allow && verdict.defer !== undefined)
|
|
805
|
-
return;
|
|
806
|
-
setQueuedLines(list => list.slice(1));
|
|
807
|
-
void runLine(verdict.allow ? verdict.command : verdict.error);
|
|
808
|
-
// The facts that decide readiness are the dependencies; `queuedLines` re-runs the drain per line.
|
|
809
|
-
}, [queuedLines, foreground !== undefined, state.pending, state.sessionId,
|
|
810
|
-
controller.queries.loop?.active, controller.queries.running]);
|
|
811
702
|
/** Apply one command result: its effects in the order the application emitted them.
|
|
812
703
|
*
|
|
813
704
|
* This is the whole front-end half of a command — the UI names no command here and never reorders.
|
|
@@ -831,10 +722,10 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
831
722
|
setCopyMode(true);
|
|
832
723
|
return;
|
|
833
724
|
case 'closePanels':
|
|
834
|
-
|
|
725
|
+
panelState.closeExcept(keepPanel);
|
|
835
726
|
return;
|
|
836
727
|
case 'close':
|
|
837
|
-
|
|
728
|
+
panelState.close(effect.panel);
|
|
838
729
|
return;
|
|
839
730
|
case 'open':
|
|
840
731
|
if (effect.panel === 'queue')
|
|
@@ -878,7 +769,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
878
769
|
return;
|
|
879
770
|
}
|
|
880
771
|
case 'live':
|
|
881
|
-
controller.actions.
|
|
772
|
+
controller.actions.showLatest();
|
|
882
773
|
return;
|
|
883
774
|
case 'pinLive':
|
|
884
775
|
controller.actions.pinHistory(false);
|
|
@@ -896,7 +787,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
896
787
|
else {
|
|
897
788
|
next.add(effect.seq);
|
|
898
789
|
setReasoningOverrides(next);
|
|
899
|
-
await jumpHistory(effect.seq
|
|
790
|
+
await jumpHistory(effect.seq);
|
|
900
791
|
}
|
|
901
792
|
return;
|
|
902
793
|
}
|
|
@@ -914,16 +805,6 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
914
805
|
return;
|
|
915
806
|
}
|
|
916
807
|
}
|
|
917
|
-
/** Close every panel except the one a result keeps open. */
|
|
918
|
-
function closePanelsExcept(keep) {
|
|
919
|
-
for (const surface of surfaces)
|
|
920
|
-
if (surface.name !== keep)
|
|
921
|
-
surface.close?.();
|
|
922
|
-
}
|
|
923
|
-
/** Close one named panel. */
|
|
924
|
-
function closePanel(name) {
|
|
925
|
-
surfaces.find(surface => surface.name === name)?.close?.();
|
|
926
|
-
}
|
|
927
808
|
const width = Math.max(10, (stdout.columns ?? 80) - 2);
|
|
928
809
|
// The composer never trades the conversation away for input. Its window grows with the body and
|
|
929
810
|
// is clamped so a few conversation rows always survive; width never buys height, so landscape and
|
|
@@ -977,140 +858,48 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
977
858
|
action: () => { setScroll(0); operate(() => controller.actions.selectSession(string(session.sessionId))); } })),
|
|
978
859
|
{ key: '@back', label: '← Workspaces', action: () => operate(() => controller.actions.showPicker('workspaces')) },
|
|
979
860
|
];
|
|
980
|
-
const
|
|
981
|
-
const
|
|
861
|
+
const showHistoryHint = dialogOpen || displayTranscript.hasMore || !!historyWindow;
|
|
862
|
+
const historyView = useHistoryView({
|
|
863
|
+
sessionId: state.sessionId, source: state.session.record, display: displayTranscript, detached: !!historyWindow,
|
|
864
|
+
width, rows: conversationRows, showHint: showHistoryHint,
|
|
865
|
+
protect: thoughtList || historyQuery !== undefined && !contentSearch,
|
|
866
|
+
enabled: !copyMode && state.screen === 'chat', online: state.online, busy: foreground !== undefined,
|
|
867
|
+
revision: state.version, blocks: state.shell.blocks,
|
|
868
|
+
current: source => controller.queries.record === source && controller.snapshot().screen === 'chat',
|
|
869
|
+
render: input => controller.queries.render(input),
|
|
870
|
+
run: (work, label) => controller.actions.foreground('history', label, work),
|
|
871
|
+
older: (signal, source) => controller.actions.older(signal, source),
|
|
872
|
+
open: (target, signal) => controller.actions.openHistory(target, signal),
|
|
873
|
+
pin: value => controller.actions.pinHistory(value),
|
|
874
|
+
closePanels: () => { setHistoryPanel(undefined); openThoughts(false); }, notify: setNotice,
|
|
875
|
+
});
|
|
876
|
+
const { layout, merged, totalRows, pageSize, position, reasoningOverrides, liveReasoning, setScroll, setReasoningOverrides, setLiveReasoning, scrollHistory, jumpHistory, historyOperation } = historyView;
|
|
877
|
+
const { length } = layout;
|
|
982
878
|
// The read-only view lays out its own transcript, so its fold mode never leaks into the
|
|
983
879
|
// conversation's, and a local source needs no layout at all: it already holds plain lines.
|
|
984
880
|
const peekLayout = useMemo(() => peek?.transcript === undefined ? undefined
|
|
985
881
|
: controller.queries.render({ transcript: peek.transcript, width, folds: NO_FOLDS, liveReasoning: 'row' }), [controller, peek?.transcript, peek?.transcript?.version, width]);
|
|
986
882
|
const peekPlainRows = useMemo(() => peek?.lines === undefined ? []
|
|
987
883
|
: peek.lines.flatMap(line => plainRows(line, width, 'shell')), [peek?.lines, width]);
|
|
988
|
-
// Local `!` blocks live at the end of the transcript: not host records, not persisted, but they
|
|
989
|
-
// scroll with the conversation and are counted into its total so the viewport math stays honest.
|
|
990
|
-
const merged = useMemo(() => mergeShellRuns(layout, state.shell.blocks, width), [layout, controller, state.version, width]);
|
|
991
|
-
const totalRows = merged.total;
|
|
992
884
|
const statusNotice = !['Connected', 'Idle', 'Running…', 'Responding…'].includes(state.status);
|
|
993
|
-
const showHistoryHint = dialogOpen || displayTranscript.hasMore || !!historyWindow;
|
|
994
|
-
const pageSize = Math.max(1, conversationRows - (showHistoryHint ? 1 : 0));
|
|
995
|
-
const previousView = useRef({ transcript: displayTranscript, session: state.session.record, count: totalRows, first, folds: reasoningOverrides, liveReasoning });
|
|
996
|
-
const previous = previousView.current;
|
|
997
|
-
const prepended = previous.first !== undefined && first !== undefined && first < previous.first;
|
|
998
|
-
const adjustedScroll = previous.session !== state.session.record ? 0
|
|
999
|
-
: scroll > 0 && previous.transcript === displayTranscript && !prepended && previous.folds === reasoningOverrides && previous.liveReasoning === liveReasoning ? Math.max(0, scroll + totalRows - previous.count) : scroll;
|
|
1000
|
-
const maxScroll = Math.max(0, totalRows - pageSize);
|
|
1001
|
-
const position = Math.min(adjustedScroll, maxScroll);
|
|
1002
|
-
useLayoutEffect(() => {
|
|
1003
|
-
previousView.current = { transcript: displayTranscript, session: state.session.record, count: totalRows, first, folds: reasoningOverrides, liveReasoning };
|
|
1004
|
-
if (position !== scroll)
|
|
1005
|
-
setScroll(position);
|
|
1006
|
-
}, [state.session.record, displayTranscript, totalRows, position, scroll, reasoningOverrides, liveReasoning]);
|
|
1007
|
-
useLayoutEffect(() => {
|
|
1008
|
-
controller.actions.pinHistory(!historyWindow && (position > 0 || thoughtList || historyQuery !== undefined && !contentSearch));
|
|
1009
|
-
}, [controller, historyWindow, position, thoughtList, historyQuery, contentSearch, state.session.record]);
|
|
1010
|
-
useEffect(() => {
|
|
1011
|
-
const first = displayTranscript.beforeSeq;
|
|
1012
|
-
if (first === undefined)
|
|
1013
|
-
return;
|
|
1014
|
-
const previous = reasoningOverrides;
|
|
1015
|
-
const next = new Set([...previous].filter(seq => seq >= first));
|
|
1016
|
-
if (next.size !== previous.size)
|
|
1017
|
-
setReasoningOverrides(next);
|
|
1018
|
-
}, [displayTranscript, displayTranscript.memoryRevision]);
|
|
1019
|
-
const loadingPage = useRef(false);
|
|
1020
|
-
const scrollIntent = useRef(0);
|
|
1021
|
-
const mounted = useRef(true);
|
|
1022
|
-
useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
1023
|
-
function scrollHistory(delta) {
|
|
1024
|
-
if (copyMode || state.screen !== 'chat')
|
|
1025
|
-
return;
|
|
1026
|
-
const intent = ++scrollIntent.current;
|
|
1027
|
-
const next = Math.max(0, Math.min(maxScroll, scroll + delta));
|
|
1028
|
-
setScroll(next);
|
|
1029
|
-
if (delta <= 0 || next < maxScroll || loadingPage.current || foreground !== undefined || !state.online || !displayTranscript.ready || !displayTranscript.hasMore)
|
|
1030
|
-
return;
|
|
1031
|
-
loadingPage.current = true;
|
|
1032
|
-
const transcript = displayTranscript;
|
|
1033
|
-
void historyOperation(signal => controller.actions.older(signal, transcript)).then(accepted => {
|
|
1034
|
-
if (accepted && mounted.current && displayRef.current === transcript && scrollIntent.current === intent) {
|
|
1035
|
-
setScroll(scroll + delta);
|
|
1036
|
-
}
|
|
1037
|
-
}).finally(() => { loadingPage.current = false; });
|
|
1038
|
-
}
|
|
1039
|
-
/** Run one front-end-orchestrated operation in the controller's foreground slot.
|
|
1040
|
-
*
|
|
1041
|
-
* The slot, its abort controller and its label live in the controller (§6.2), so this is only a
|
|
1042
|
-
* typed name for the common case: paging through history.
|
|
1043
|
-
* @param operation - Work handed the signal the controller aborts.
|
|
1044
|
-
* @param label - Label the status line shows while it runs.
|
|
1045
|
-
* @returns The value, or undefined when the slot was taken or the work was cancelled.
|
|
1046
|
-
*/
|
|
1047
|
-
async function historyOperation(operation, label = 'Loading history…') {
|
|
1048
|
-
return await controller.actions.foreground('history', label, operation);
|
|
1049
|
-
}
|
|
1050
|
-
/** Reach one prompt older than the index's boundary, from memory first and the host second.
|
|
1051
|
-
*
|
|
1052
|
-
* The transcript window outlives the prompt index's budgets, so an evicted prompt is usually still
|
|
1053
|
-
* loaded: refilling from it costs nothing and keeps the key immediate. Only when the window itself
|
|
1054
|
-
* is exhausted does this page back, skipping tool-only pages inside one bounded request loop; the
|
|
1055
|
-
* fetched page stays readable in the conversation above the composer.
|
|
1056
|
-
*/
|
|
1057
|
-
async function recallOlderPrompts() {
|
|
1058
|
-
if (historyPaging.current)
|
|
1059
|
-
return;
|
|
1060
|
-
const transcript = state.session.record;
|
|
1061
|
-
const sessionId = state.sessionId;
|
|
1062
|
-
// Recover from the loaded window first: no request, no focus change, no loading label.
|
|
1063
|
-
if (controller.actions.refillRecall() || !transcript.hasMore) {
|
|
1064
|
-
setInput(controller.queries.recall(-1, input), true);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
historyPaging.current = true;
|
|
1068
|
-
try {
|
|
1069
|
-
const accepted = await historyOperation(async (signal) => {
|
|
1070
|
-
// A prompt can be many pages back in an agent session, so scan a healthy stretch before
|
|
1071
|
-
// giving up; the loading label stays visible and Esc cancels the scan.
|
|
1072
|
-
for (let page = 0; page < RECALL_PAGE_SCAN; page++) {
|
|
1073
|
-
if (transcript.beforeSeq === undefined || !transcript.hasMore)
|
|
1074
|
-
break;
|
|
1075
|
-
await controller.actions.older(signal, transcript);
|
|
1076
|
-
signal.throwIfAborted();
|
|
1077
|
-
if (controller.actions.refillRecall())
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
}, 'Loading older prompts…');
|
|
1081
|
-
if (accepted && mounted.current && controller.state.sessionId === sessionId) {
|
|
1082
|
-
setInput(controller.queries.recall(-1, input), true);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
finally {
|
|
1086
|
-
historyPaging.current = false;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
885
|
async function openSearchSession(sessionId, query) {
|
|
1090
886
|
await historyOperation(async (signal) => {
|
|
1091
|
-
await controller.actions.selectSession(sessionId)
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (state.screen !== 'chat')
|
|
1099
|
-
throw new Error('Select a session first');
|
|
1100
|
-
++scrollIntent.current;
|
|
1101
|
-
await controller.actions.foreground('history', 'Jumping to a message…', async (signal) => {
|
|
1102
|
-
const transcript = displayTranscript.messages.some(message => message.seq === target) ? displayTranscript
|
|
1103
|
-
: state.session.record.messages.some(message => message.seq === target) ? state.session.record
|
|
1104
|
-
: await controller.queries.historyAt(target, signal);
|
|
887
|
+
if (!await controller.actions.selectSession(sessionId))
|
|
888
|
+
return;
|
|
889
|
+
const record = controller.queries.record;
|
|
890
|
+
const current = () => controller.snapshot().sessionId === sessionId && controller.queries.record === record
|
|
891
|
+
&& controller.snapshot().screen === 'chat';
|
|
892
|
+
if (!current() || !await controller.actions.waitForHistory(signal))
|
|
893
|
+
return;
|
|
1105
894
|
signal.throwIfAborted();
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
895
|
+
if (!current())
|
|
896
|
+
return;
|
|
897
|
+
const matches = await controller.actions.searchHistory(query, signal);
|
|
898
|
+
signal.throwIfAborted();
|
|
899
|
+
if (!matches || !current())
|
|
900
|
+
return;
|
|
901
|
+
setHistoryPanel({ query, contentSearch: true, matches });
|
|
902
|
+
setSearchPanel(undefined);
|
|
1114
903
|
});
|
|
1115
904
|
}
|
|
1116
905
|
const trailingGap = dialogOpen && length > 0 && layout.viewport(length - 1, length)[0]?.text === '' ? 1 : 0;
|
|
@@ -1172,7 +961,7 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1172
961
|
const thoughtEntries = thoughtList ? displayTranscript.thoughts : undefined;
|
|
1173
962
|
const thoughtChoices = useMemo(() => [...(thoughtEntries ?? [])].reverse().map(entry => ({
|
|
1174
963
|
key: String(entry.seq), label: `${toolLine(`#${entry.seq} User · ${entry.prompt}`, width - 2)}\n ${toolLine(`◇ ${entry.preview}`, width - 4)}`,
|
|
1175
|
-
action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq); setReasoningOverrides(next); await jumpHistory(entry.seq
|
|
964
|
+
action: () => operate(async () => { const next = new Set(reasoningOverrides); next.add(entry.seq); setReasoningOverrides(next); await jumpHistory(entry.seq); }),
|
|
1176
965
|
})), [thoughtEntries, width, pageSize, reasoningOverrides, liveReasoning, displayTranscript]);
|
|
1177
966
|
const thoughtOptions = useMemo(() => [
|
|
1178
967
|
...(liveThought ? [{ key: 'live', label: `${toolLine(`Now · User · ${state.session.record.latestPrompt}`, width - 2)}\n ${toolLine(liveThought.text, width - 4)}`,
|
|
@@ -1257,12 +1046,8 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1257
1046
|
setComposerIntent({
|
|
1258
1047
|
hint: 'Editing saved prompt · Enter saves · Esc cancels',
|
|
1259
1048
|
emptyNotice: 'Type the prompt text; Enter then saves it',
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (saved)
|
|
1263
|
-
setNotice('Saved prompt updated');
|
|
1264
|
-
return saved;
|
|
1265
|
-
},
|
|
1049
|
+
successNotice: 'Saved prompt updated',
|
|
1050
|
+
commit: text => controller.actions.updatePrompt(prompt.id, text),
|
|
1266
1051
|
});
|
|
1267
1052
|
setInput(prompt.text);
|
|
1268
1053
|
openPrompts(false);
|
|
@@ -1272,5 +1057,5 @@ export function App({ controller, panelLifetimeMs = PANEL_LIFETIME_MS, theme = m
|
|
|
1272
1057
|
? '↑ ↓ 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
|
|
1273
1058
|
? 'Choose "Other answer" to type · the draft is kept while this is open'
|
|
1274
1059
|
: 'Esc dismisses the question · the draft is kept while this is open'
|
|
1275
|
-
: '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: setCursor, onSubmit: () => { void submit(input); }, reservedKeys: approvalKeysActive ? ['1', '2', '3'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : surfaceReservedKeys.length ? surfaceReservedKeys : undefined, width: draftWidth, maxRows: composerRows, promptColor: answerPending ? theme.colors.muted : theme.accent, focus:
|
|
1060
|
+
: '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: setCursor, onSubmit: () => { void submit(input); }, reservedKeys: approvalKeysActive ? ['1', '2', '3'] : questionKeysActive ? ['1', '2', '3', '4', '5', '6', '7', '8', '9', ...(question?.multiSelect === true ? [' '] : [])] : !removal && !models && !searchResults && (state.screen === 'workspaces' || state.screen === 'sessions') ? ['d'] : surfaceReservedKeys.length ? surfaceReservedKeys : undefined, width: draftWidth, maxRows: composerRows, promptColor: answerPending ? theme.colors.muted : theme.accent, focus: !copyMode && !answerPending && !loopForm, placeholder: state.screen === 'path' ? 'Absolute directory path on host' : 'Message, @host-file, or /help' }), referenceOpen && _jsx(ReferenceMenu, { matches: matches, index: referenceIndex }), loopMenuOpen && _jsx(LoopMenu, { records: loopCandidates, index: loopMenuCursor })] }), commandSuggestions && _jsx(Text, { dimColor: true, children: commandSuggestions.join(' ') }), commandHint && _jsxs(Text, { dimColor: true, children: [_jsxs(Text, { color: theme.accent, children: [commandHint.command, commandHint.usage === undefined ? '' : ` ${commandHint.usage}`] }), " \u00B7 ", commandHint.description] }), help && _jsx(HelpPanel, { page: currentHelpPage, pages: helpPages, pageSize: helpPageSize }), costExpanded && _jsx(CostPanel, { source: costSource }), _jsx(Frozen, { frozen: statusFrozen, identity: `${width}:${state.sessionId}:${statusExpanded}:${statusScroll}:${pauseReason ?? ''}`, children: _jsx(StatusBar, { source: statusSource, width: width, expanded: statusExpanded, scroll: statusScroll, pageSize: statusViewRows, onScroll: setStatusScroll, onOverflow: setStatusOverflow, onRows: setStatusBarRows, pauseReason: pauseReason, revision: state.version }) })] })] }) }) }) });
|
|
1276
1061
|
}
|
|
@@ -30,6 +30,8 @@ export declare function blockRows(run: ShellBlock, width: number): HistoryRow[];
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function mergeShellRuns(layout: RowSource, runs: readonly ShellBlock[], width: number): {
|
|
32
32
|
total: number;
|
|
33
|
+
/** Translate a host row to its position after local blocks are inserted. */
|
|
34
|
+
hostRow(row: number): number;
|
|
33
35
|
/** Merged row index of each block bar that opens a readable source, to its source id. */
|
|
34
36
|
sources: ReadonlyMap<number, string>;
|
|
35
37
|
viewport(start: number, end: number): HistoryRow[];
|
|
@@ -109,6 +109,14 @@ export function mergeShellRuns(layout, runs, width) {
|
|
|
109
109
|
return {
|
|
110
110
|
total,
|
|
111
111
|
sources,
|
|
112
|
+
hostRow(row) {
|
|
113
|
+
for (const segment of segments) {
|
|
114
|
+
if (segment.host !== undefined && row >= segment.host && row < segment.host + segment.count) {
|
|
115
|
+
return segment.from + row - segment.host;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return total;
|
|
119
|
+
},
|
|
112
120
|
viewport(start, end) {
|
|
113
121
|
const rows = [];
|
|
114
122
|
for (const segment of segments) {
|