@agi-fans/dsh-tui 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/CHANGELOG.md +31 -0
- package/lib/chrome/box.d.ts +2 -0
- package/lib/chrome/box.js +5 -1
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +61 -11
- package/lib/chrome/main-screen-renderer.js +1674 -71
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +21 -13
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/renderer.d.ts +75 -0
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/theme.d.ts +18 -0
- package/lib/chrome/theme.js +18 -0
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-args.d.ts +24 -1
- package/lib/chrome/tool-args.js +40 -1
- package/lib/chrome/tool-args.js.map +1 -1
- package/lib/chrome/tool-renderers.d.ts +11 -0
- package/lib/chrome/tool-renderers.js +55 -5
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/chrome/welcome-tips.js +1 -1
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/definition.d.ts +59 -7
- package/lib/definition.js.map +1 -1
- package/lib/input/keybindings-config.d.ts +1 -1
- package/lib/input/keybindings-config.js +2 -1
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +7 -0
- package/lib/input/keys.js +11 -4
- package/lib/input/keys.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +20 -4
- package/lib/runtime/provider-local.js +435 -63
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/tool-presentation.js +43 -30
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/feature-toggles.d.ts +100 -0
- package/lib/session/feature-toggles.js +170 -0
- package/lib/session/feature-toggles.js.map +1 -0
- package/lib/session/fold-policy.d.ts +21 -0
- package/lib/session/fold-policy.js +15 -0
- package/lib/session/fold-policy.js.map +1 -0
- package/lib/session/job-notice.d.ts +4 -0
- package/lib/session/job-notice.js +44 -3
- package/lib/session/job-notice.js.map +1 -1
- package/lib/session/session-controller.js +51 -13
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/tui-settings.d.ts +6 -1
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/event-views.d.ts +26 -2
- package/lib/views/event-views.js +160 -43
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/hotkeys.d.ts +2 -0
- package/lib/views/hotkeys.js +7 -3
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/settings-list.d.ts +10 -4
- package/lib/views/settings-list.js +41 -18
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-render.d.ts +133 -18
- package/lib/views/transcript-render.js +1179 -112
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +79 -1
- package/lib/views/transcript-types.js +11 -0
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workspace-changes.d.ts +27 -0
- package/lib/views/workspace-changes.js +128 -0
- package/lib/views/workspace-changes.js.map +1 -0
- package/package.json +36 -35
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
import { TerminalNotificationController, terminalNotificationSequence, terminalProgressSequence, } from "./terminal-notifications.js";
|
|
14
14
|
import { HerdrAgentReporter } from "./herdr-agent.js";
|
|
15
15
|
import { ComposerImages } from "./composer-images.js";
|
|
16
|
+
import { mergeJobSettlement } from "../session/job-notice.js";
|
|
16
17
|
import { PlainTui } from "./plain-tui.js";
|
|
17
18
|
import { homedir } from 'node:os';
|
|
18
19
|
import { join, sep } from 'node:path';
|
|
19
20
|
import { StringDecoder } from 'node:string_decoder';
|
|
21
|
+
import { defaultFeatureStates, featurePatchPath, featureStatesFromPatch, renderFeatureBlock, replaceFeatureBlock, } from "../session/feature-toggles.js";
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { dirname } from 'node:path';
|
|
20
24
|
import { TUI_SERVICE, } from "../definition.js";
|
|
21
25
|
import { applySlashCompletion, formatHelpText, parseSlashInput, resolveSlashCommand, slashSuggestions, BUILTIN_SLASH_COMMANDS, } from "../views/autocomplete.js";
|
|
22
26
|
import { applyPathCompletion, defaultPathSource, editorLineAt, findPathToken, parsePathPrefix, pathSuggestions, searchPathSuggestions, } from "../views/path-complete.js";
|
|
@@ -30,7 +34,7 @@ import { InputEditor, lineEnd, lineStart } from "../input/editor.js";
|
|
|
30
34
|
import { applySettingsEvent, createSettings, } from "../views/settings-list.js";
|
|
31
35
|
import { appendTrajectoryEvent, applyTrajectoryEvent, createTrajectory, trajectoryDetailMetrics, trajectoryListMetrics, } from "../views/trajectory.js";
|
|
32
36
|
import { applyAgentHubEvent, createAgentHub, updateAgentHub, } from "../views/agent-hub.js";
|
|
33
|
-
import { applyEvent, applyStreamChunk, blockSearchText, initialTranscript, replayEvents, renderView, TRANSCRIPT_FAST_SCROLL, } from "../views/event-views.js";
|
|
37
|
+
import { applyEvent, applyStreamChunk, blockSearchText, initialTranscript, replayEvents, renderView, turnGroups, transcriptSeam, TRANSCRIPT_FAST_SCROLL, withWorkspaceSummary, } from "../views/event-views.js";
|
|
34
38
|
import { applyTranscriptSearchEvent, createTranscriptSearch, searchBlockIndexes, } from "../views/transcript-search.js";
|
|
35
39
|
import { nextRevealStep, revealStreamingAssistant, streamingAssistantKey, streamingAssistantUnits, } from "../views/streaming-reveal.js";
|
|
36
40
|
import { flushPending, parseKeys } from "../input/keys.js";
|
|
@@ -38,6 +42,7 @@ import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
|
|
|
38
42
|
import { colorDisabledByEnv, detectTrueColor } from "../chrome/theme.js";
|
|
39
43
|
import { renderTool } from "../chrome/tool-renderers.js";
|
|
40
44
|
import { TUI_SETTINGS_ENTRY, TUI_SETTINGS_FIELDS } from "../session/tui-settings.js";
|
|
45
|
+
import { DEFAULT_FOLD_DENSITY, foldPolicy, resolveFoldDensity, } from "../session/fold-policy.js";
|
|
41
46
|
import { defaultStatusBarConfig, resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
42
47
|
import { HistoryStore } from "../session/history-store.js";
|
|
43
48
|
import { loadKeybindings } from "../input/keybindings-config.js";
|
|
@@ -46,7 +51,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
46
51
|
import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "../views/prompt-selector.js";
|
|
47
52
|
import { refreshProjectContext, resolveProjectContext } from "../session/project-context.js";
|
|
48
53
|
import { pickWelcomeTips } from "../chrome/welcome-tips.js";
|
|
49
|
-
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
|
|
54
|
+
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount, keysForAction } from "../views/hotkeys.js";
|
|
50
55
|
import { imagePathCandidates, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
51
56
|
import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
|
|
52
57
|
const DOUBLE_CTRL_C_MS = 500;
|
|
@@ -99,6 +104,7 @@ export const Config = z.object({
|
|
|
99
104
|
model: z.string(),
|
|
100
105
|
historyPath: z.string(),
|
|
101
106
|
keybindingsPath: z.string(),
|
|
107
|
+
dshHome: z.string(),
|
|
102
108
|
...TUI_SETTINGS_FIELDS,
|
|
103
109
|
});
|
|
104
110
|
/**
|
|
@@ -122,6 +128,25 @@ function resolveColors(preference, isTty) {
|
|
|
122
128
|
return preference;
|
|
123
129
|
return isTty && !colorDisabledByEnv();
|
|
124
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The first body row whose shape changed between two frames: the earliest
|
|
133
|
+
* surface that opened, closed, or changed how it is open. Rows above it are
|
|
134
|
+
* the same in both frames, which is what lets the renderer keep history that
|
|
135
|
+
* is still true and repair only what moved.
|
|
136
|
+
*/
|
|
137
|
+
function firstReshapedRow(before, after) {
|
|
138
|
+
const keys = (marks) => new Set(marks.map(mark => mark.key));
|
|
139
|
+
const was = keys(before);
|
|
140
|
+
const now = keys(after);
|
|
141
|
+
let row = Number.POSITIVE_INFINITY;
|
|
142
|
+
for (const mark of before)
|
|
143
|
+
if (!now.has(mark.key))
|
|
144
|
+
row = Math.min(row, mark.row);
|
|
145
|
+
for (const mark of after)
|
|
146
|
+
if (!was.has(mark.key))
|
|
147
|
+
row = Math.min(row, mark.row);
|
|
148
|
+
return Number.isFinite(row) ? row : 0;
|
|
149
|
+
}
|
|
125
150
|
export class LocalTui {
|
|
126
151
|
#deferInitialRender = false;
|
|
127
152
|
#terminalProfile;
|
|
@@ -145,6 +170,10 @@ export class LocalTui {
|
|
|
145
170
|
#search = null;
|
|
146
171
|
#transcriptSearch = null;
|
|
147
172
|
#settings = null;
|
|
173
|
+
/** Optional-feature state read from the Profile patch this process booted with. */
|
|
174
|
+
#features;
|
|
175
|
+
/** Profile patch the Features section writes; absent hides that section. */
|
|
176
|
+
#featurePatch;
|
|
148
177
|
#copySelector = null;
|
|
149
178
|
#trajectory = null;
|
|
150
179
|
#agentHub = null;
|
|
@@ -191,9 +220,16 @@ export class LocalTui {
|
|
|
191
220
|
#scrollStart = 0;
|
|
192
221
|
#maxStart = 0;
|
|
193
222
|
#scrollBudget = 0;
|
|
223
|
+
#blockStarts = [];
|
|
224
|
+
/** Last frame's {@link TranscriptScroll.foldShape}, to notice a fold change. */
|
|
225
|
+
#foldShape;
|
|
226
|
+
/** Last frame's open surfaces, to find where a fold change starts. */
|
|
227
|
+
#foldMarks;
|
|
194
228
|
#follow = true;
|
|
195
229
|
#focusBlock;
|
|
196
|
-
#
|
|
230
|
+
#focusBlockEdge;
|
|
231
|
+
#foldDensity = DEFAULT_FOLD_DENSITY;
|
|
232
|
+
#foldPolicy = foldPolicy(DEFAULT_FOLD_DENSITY);
|
|
197
233
|
#motion = 'full';
|
|
198
234
|
#terminalProgress = false;
|
|
199
235
|
#terminalProgressActive = false;
|
|
@@ -211,7 +247,24 @@ export class LocalTui {
|
|
|
211
247
|
#notificationThreshold = '30s';
|
|
212
248
|
#statusBar = defaultStatusBarConfig();
|
|
213
249
|
#toolsExpanded = false;
|
|
214
|
-
|
|
250
|
+
/**
|
|
251
|
+
* The reader opened something with Ctrl+O or Alt+O and is reading it.
|
|
252
|
+
*
|
|
253
|
+
* Openings are shown in a browsing frame, never in the flow that follows the
|
|
254
|
+
* tail: every opening starts an inspection, and returning to the tail ends it
|
|
255
|
+
* and closes them all. The flow's rows cross into the terminal's history, which can only be
|
|
256
|
+
* appended to: opening a run in place pushed rows that closing it could not
|
|
257
|
+
* take back, so the screen either went blank where they had been or painted
|
|
258
|
+
* rows history already held, and the transcript read twice. A browsing frame
|
|
259
|
+
* repaints the screen and nothing above it, so reading an opened run leaves
|
|
260
|
+
* the flow — and the history — exactly as it was.
|
|
261
|
+
*/
|
|
262
|
+
#inspecting = false;
|
|
263
|
+
#mouseTracking = false;
|
|
264
|
+
#jumpToLatest;
|
|
265
|
+
#openedGroups = new Set();
|
|
266
|
+
/** Turn scopes whose tools stay detailed as new calls arrive during inspection. */
|
|
267
|
+
#detailedTurns = new Set();
|
|
215
268
|
#tools = [];
|
|
216
269
|
#runtimeCommands = [];
|
|
217
270
|
#prompt = null;
|
|
@@ -281,6 +334,17 @@ export class LocalTui {
|
|
|
281
334
|
this.#historyStore = paths.historyPath === undefined ? undefined : new HistoryStore(paths.historyPath);
|
|
282
335
|
this.#history = this.#historyStore?.load() ?? [];
|
|
283
336
|
this.#keybindings = loadKeybindings(paths.keybindingsPath);
|
|
337
|
+
this.#featurePatch = paths.featurePatch;
|
|
338
|
+
if (paths.featurePatch !== undefined) {
|
|
339
|
+
try {
|
|
340
|
+
this.#features = featureStatesFromPatch(readFileSync(paths.featurePatch, 'utf8'));
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// A patch that cannot be read still leaves the shipped defaults; the
|
|
344
|
+
// write path below reports the failure when the user toggles.
|
|
345
|
+
this.#features = defaultFeatureStates();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
284
348
|
const fallback = defaultPathSource();
|
|
285
349
|
this.#cwd = paths.cwd ?? fallback.cwd;
|
|
286
350
|
this.#readImagePath = paths.readImagePath ?? (path => readImageFile(path, this.#cwd));
|
|
@@ -312,7 +376,6 @@ export class LocalTui {
|
|
|
312
376
|
width: this.#term.width(),
|
|
313
377
|
height: this.#term.height(),
|
|
314
378
|
synchronized: this.#tty,
|
|
315
|
-
clearScrollback: this.#terminalProfile === 'direct',
|
|
316
379
|
alternateScreenOverlays: paths.alternateScreenOverlays === true,
|
|
317
380
|
});
|
|
318
381
|
if (this.#tty) {
|
|
@@ -352,8 +415,15 @@ export class LocalTui {
|
|
|
352
415
|
if (this.#tty)
|
|
353
416
|
this.#render();
|
|
354
417
|
}
|
|
355
|
-
event(event, presentation) {
|
|
356
|
-
|
|
418
|
+
event(event, presentation, workspace) {
|
|
419
|
+
// Paint any coalesced results and remaining streamed text before the turn's
|
|
420
|
+
// end changes the live work into its folded summary.
|
|
421
|
+
if (event.type === 'turn/end' && this.#tty && this.#state.status !== 'idle'
|
|
422
|
+
&& turnGroups(this.#state).some(group => group.live)) {
|
|
423
|
+
this.#syncStreamingReveal(undefined);
|
|
424
|
+
this.#render();
|
|
425
|
+
}
|
|
426
|
+
this.#state = applyEvent(this.#state, event, presentation, workspace);
|
|
357
427
|
this.#emitNotification(this.#notifications.event({
|
|
358
428
|
type: event.type,
|
|
359
429
|
time: event.time,
|
|
@@ -380,6 +450,16 @@ export class LocalTui {
|
|
|
380
450
|
this.#plain.print();
|
|
381
451
|
}
|
|
382
452
|
}
|
|
453
|
+
setWorkspaceSummary(turn, summary) {
|
|
454
|
+
const next = withWorkspaceSummary(this.#state, turn, summary);
|
|
455
|
+
if (next === this.#state)
|
|
456
|
+
return;
|
|
457
|
+
this.#state = next;
|
|
458
|
+
if (this.#tty)
|
|
459
|
+
this.#render();
|
|
460
|
+
else
|
|
461
|
+
this.#plain.print();
|
|
462
|
+
}
|
|
383
463
|
/**
|
|
384
464
|
* Fold one live `agent/assistant-stream` chunk into the transcript and
|
|
385
465
|
* schedule the streaming reveal. Durable settlement follows on the session
|
|
@@ -544,20 +624,41 @@ export class LocalTui {
|
|
|
544
624
|
.join(' · ');
|
|
545
625
|
}
|
|
546
626
|
notice(text, options = {}) {
|
|
627
|
+
const merged = options.job === undefined ? undefined : mergeJobSettlement(this.#state, options.job);
|
|
628
|
+
if (merged !== undefined && this.#tty) {
|
|
629
|
+
const origin = merged.blocks.find((block, index) => block !== this.#state.blocks[index]);
|
|
630
|
+
this.#state = merged;
|
|
631
|
+
// A previous turn may already be in native history. Keep its new outcome
|
|
632
|
+
// visible at the tail as well as updating the live transcript's tool.
|
|
633
|
+
if (origin?.kind === 'tool' && (origin.turn === undefined || origin.turn === this.#state.turn)) {
|
|
634
|
+
if (options.focus === true)
|
|
635
|
+
this.#followTail();
|
|
636
|
+
this.#render();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const running = options.process === true && this.#state.status !== 'idle' && this.#state.turn > 0;
|
|
547
641
|
const block = {
|
|
548
642
|
kind: 'notice',
|
|
549
643
|
level: options.level ?? 'info',
|
|
550
644
|
text,
|
|
551
645
|
...(options.framed === true ? { framed: true } : {}),
|
|
646
|
+
...(running
|
|
647
|
+
? { process: { turn: this.#state.turn, ...(options.processSource === undefined ? {} : { source: options.processSource }) } }
|
|
648
|
+
: {}),
|
|
552
649
|
};
|
|
553
650
|
this.#state = { ...this.#state, blocks: [...this.#state.blocks, block] };
|
|
651
|
+
if (options.focus === true)
|
|
652
|
+
this.#followTail();
|
|
554
653
|
if (this.#tty)
|
|
555
654
|
this.#render();
|
|
556
655
|
else
|
|
557
656
|
this.#plain.print();
|
|
558
657
|
}
|
|
559
|
-
commandOutput(command, text) {
|
|
658
|
+
commandOutput(command, text, options = {}) {
|
|
560
659
|
this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'commandOutput', command, text }] };
|
|
660
|
+
if (options.focus === true)
|
|
661
|
+
this.#followTail();
|
|
561
662
|
if (this.#tty)
|
|
562
663
|
this.#render();
|
|
563
664
|
else
|
|
@@ -602,14 +703,18 @@ export class LocalTui {
|
|
|
602
703
|
}
|
|
603
704
|
});
|
|
604
705
|
}
|
|
605
|
-
replaceSession(events, presentations, status = 'idle') {
|
|
706
|
+
replaceSession(events, presentations, status = 'idle', reason = 'open') {
|
|
606
707
|
this.#trajectory = null;
|
|
607
708
|
const state = replayEvents(events, presentations);
|
|
608
|
-
|
|
709
|
+
// Notices and tool catalogs do not turn a blank session into a document
|
|
710
|
+
// that /new needs to separate. Refreshes are the same document.
|
|
711
|
+
const boundary = reason === 'open' || (reason === 'new' && this.#state.blocks.some(block => block.kind !== 'notice' && block.kind !== 'toolCatalog'));
|
|
712
|
+
const blocks = boundary ? [transcriptSeam('session-opened'), ...state.blocks] : state.blocks;
|
|
713
|
+
this.#state = { ...state, status, compactCommandId: undefined, blocks };
|
|
609
714
|
this.#plain.resetPrinted();
|
|
610
715
|
this.#followTail();
|
|
611
716
|
this.#deferInitialRender = false;
|
|
612
|
-
this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
|
|
717
|
+
this.#renderer.startEpoch({ replay: reason === 'refresh' ? 'viewport' : status === 'idle' ? 'full' : 'pinned' });
|
|
613
718
|
if (this.#tty)
|
|
614
719
|
this.#render();
|
|
615
720
|
else
|
|
@@ -672,7 +777,7 @@ export class LocalTui {
|
|
|
672
777
|
colors: this.#colors,
|
|
673
778
|
motion: this.#motion,
|
|
674
779
|
terminalProgress: this.#terminalProgress,
|
|
675
|
-
|
|
780
|
+
foldDensity: this.#foldDensity,
|
|
676
781
|
checkUpdates: this.#checkUpdates,
|
|
677
782
|
startupChangelog: this.#startupChangelog,
|
|
678
783
|
notifications: this.#notificationPolicy,
|
|
@@ -710,7 +815,7 @@ export class LocalTui {
|
|
|
710
815
|
this.#agentBehavior = undefined;
|
|
711
816
|
this.#agentBehaviorPending = false;
|
|
712
817
|
if (this.#settings !== null)
|
|
713
|
-
this.#settings = createSettings(this.prefs());
|
|
818
|
+
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features);
|
|
714
819
|
if (this.#tty && !this.#disposed)
|
|
715
820
|
this.#render();
|
|
716
821
|
};
|
|
@@ -839,6 +944,7 @@ export class LocalTui {
|
|
|
839
944
|
if (this.#tty) {
|
|
840
945
|
this.#offData?.();
|
|
841
946
|
this.#offResize?.();
|
|
947
|
+
this.#setMouseTracking(false);
|
|
842
948
|
this.#term.input.setRawMode?.(false);
|
|
843
949
|
// Leave the cursor on a fresh line below the last frame so the shell
|
|
844
950
|
// prompt does not overwrite the transcript. Disable bracketed paste
|
|
@@ -1024,7 +1130,7 @@ export class LocalTui {
|
|
|
1024
1130
|
trueColor: this.#trueColor,
|
|
1025
1131
|
themeName: this.#themeName,
|
|
1026
1132
|
scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
|
|
1027
|
-
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock }),
|
|
1133
|
+
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start' }),
|
|
1028
1134
|
...(this.#transcriptSearch === null ? {} : {
|
|
1029
1135
|
transcriptSearch: {
|
|
1030
1136
|
query: this.#transcriptSearch.query,
|
|
@@ -1034,7 +1140,10 @@ export class LocalTui {
|
|
|
1034
1140
|
},
|
|
1035
1141
|
}),
|
|
1036
1142
|
toolsExpanded: this.#toolsExpanded,
|
|
1037
|
-
expandedTools: this.#
|
|
1143
|
+
expandedTools: this.#detailedToolIds(),
|
|
1144
|
+
toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
|
|
1145
|
+
fold: this.#foldPolicy,
|
|
1146
|
+
openedGroups: this.#openedGroups,
|
|
1038
1147
|
commands: this.#commands(),
|
|
1039
1148
|
recentSessions: this.#recentSessions,
|
|
1040
1149
|
welcomeTips: this.#welcomeTips,
|
|
@@ -1058,8 +1167,23 @@ export class LocalTui {
|
|
|
1058
1167
|
})
|
|
1059
1168
|
: { lines: [] };
|
|
1060
1169
|
this.#focusBlock = undefined;
|
|
1170
|
+
this.#focusBlockEdge = undefined;
|
|
1061
1171
|
this.#promptDocument = frame.promptDocument;
|
|
1062
1172
|
this.#syncScroll(frame.transcript);
|
|
1173
|
+
this.#jumpToLatest = frame.jumpToLatest;
|
|
1174
|
+
this.#setMouseTracking(frame.transcript !== undefined && frame.transientSurface !== 'overlay'
|
|
1175
|
+
&& (this.#inspecting || this.#maxStart > 0));
|
|
1176
|
+
// A run opening or folding reshapes rows above the screen in place; the
|
|
1177
|
+
// renderer's frozen boundary is a row index and has to be told, or a fold
|
|
1178
|
+
// blanks the screen and a large one replays the transcript into history.
|
|
1179
|
+
const scroll = frame.transcript;
|
|
1180
|
+
if (scroll?.foldShape !== undefined && scroll.foldMarks !== undefined) {
|
|
1181
|
+
if (this.#foldMarks !== undefined && scroll.foldShape !== this.#foldShape) {
|
|
1182
|
+
this.#renderer.reflow((scroll.bodyRow ?? 0) + firstReshapedRow(this.#foldMarks, scroll.foldMarks));
|
|
1183
|
+
}
|
|
1184
|
+
this.#foldShape = scroll.foldShape;
|
|
1185
|
+
this.#foldMarks = scroll.foldMarks;
|
|
1186
|
+
}
|
|
1063
1187
|
this.#renderer.render(frame);
|
|
1064
1188
|
}
|
|
1065
1189
|
#scheduleStreamRender() {
|
|
@@ -1073,16 +1197,17 @@ export class LocalTui {
|
|
|
1073
1197
|
}
|
|
1074
1198
|
#syncScroll(scroll) {
|
|
1075
1199
|
if (scroll === undefined) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
this.#scrollBudget = 0;
|
|
1079
|
-
this.#follow = true;
|
|
1200
|
+
// Full-screen surfaces temporarily cover the transcript. They carry no
|
|
1201
|
+
// scroll projection and must not replace the reader's return position.
|
|
1080
1202
|
return;
|
|
1081
1203
|
}
|
|
1082
1204
|
this.#scrollStart = scroll.start;
|
|
1083
1205
|
this.#maxStart = scroll.maxStart;
|
|
1084
1206
|
this.#scrollBudget = scroll.budget;
|
|
1085
|
-
|
|
1207
|
+
this.#blockStarts = scroll.blockStarts?.map(row => row + (scroll.bodyRow ?? 0)) ?? [];
|
|
1208
|
+
// An inspection stays a browsing frame even when everything fits: resuming
|
|
1209
|
+
// the follow would drop the openings the reader just asked for.
|
|
1210
|
+
if (!this.#inspecting && (this.#follow || this.#scrollStart >= this.#maxStart)) {
|
|
1086
1211
|
this.#follow = true;
|
|
1087
1212
|
this.#scrollStart = this.#maxStart;
|
|
1088
1213
|
}
|
|
@@ -1091,8 +1216,22 @@ export class LocalTui {
|
|
|
1091
1216
|
return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
|
|
1092
1217
|
}
|
|
1093
1218
|
#scrollBy(delta) {
|
|
1219
|
+
if (this.#follow && delta > 0)
|
|
1220
|
+
return;
|
|
1094
1221
|
if (delta === 0 && this.#maxStart === 0)
|
|
1095
1222
|
return;
|
|
1223
|
+
if (!this.#inspecting && delta > 0 && this.#scrollStart + delta >= this.#maxStart) {
|
|
1224
|
+
this.#followTail();
|
|
1225
|
+
this.#render();
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
// Scrolling past the end of an inspection is how a reader goes back to
|
|
1229
|
+
// the live tail, the same as scrolling to the end of history does.
|
|
1230
|
+
if (this.#inspecting && delta > 0 && this.#scrollStart >= this.#maxStart) {
|
|
1231
|
+
this.#followTail();
|
|
1232
|
+
this.#render();
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1096
1235
|
this.#follow = false;
|
|
1097
1236
|
this.#scrollStart += delta;
|
|
1098
1237
|
if (this.#scrollStart <= 0)
|
|
@@ -1102,6 +1241,47 @@ export class LocalTui {
|
|
|
1102
1241
|
#followTail() {
|
|
1103
1242
|
this.#follow = true;
|
|
1104
1243
|
this.#scrollStart = this.#maxStart;
|
|
1244
|
+
this.#endInspection();
|
|
1245
|
+
}
|
|
1246
|
+
/** Close every opening the reader made; they belong to one inspection. */
|
|
1247
|
+
#endInspection() {
|
|
1248
|
+
if (!this.#inspecting)
|
|
1249
|
+
return;
|
|
1250
|
+
this.#inspecting = false;
|
|
1251
|
+
this.#toolsExpanded = false;
|
|
1252
|
+
this.#openedGroups.clear();
|
|
1253
|
+
this.#detailedTurns.clear();
|
|
1254
|
+
}
|
|
1255
|
+
/** Capture transcript navigation; overlays and disposal restore terminal ownership. */
|
|
1256
|
+
#setMouseTracking(enabled) {
|
|
1257
|
+
if (!this.#tty || enabled === this.#mouseTracking)
|
|
1258
|
+
return;
|
|
1259
|
+
this.#mouseTracking = enabled;
|
|
1260
|
+
this.#term.output.write(enabled ? '\x1b[?1000h\x1b[?1006h' : '\x1b[?1000l\x1b[?1006l');
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* After a Ctrl+O toggle: read what is open in a browsing frame at `target`,
|
|
1264
|
+
* or go back to the live tail once nothing is open any more.
|
|
1265
|
+
*/
|
|
1266
|
+
#inspect(target) {
|
|
1267
|
+
const open = this.#toolsExpanded || this.#openedGroups.size > 0;
|
|
1268
|
+
if (!open) {
|
|
1269
|
+
this.#inspecting = false;
|
|
1270
|
+
this.#followTail();
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (this.#inspecting)
|
|
1274
|
+
return;
|
|
1275
|
+
this.#inspecting = true;
|
|
1276
|
+
this.#follow = false;
|
|
1277
|
+
// Opening from the tail with nothing in particular under the viewport
|
|
1278
|
+
// keeps the reader at the end of the transcript, now in a browsing frame.
|
|
1279
|
+
if (target === undefined)
|
|
1280
|
+
this.#scrollStart = Number.MAX_SAFE_INTEGER;
|
|
1281
|
+
else {
|
|
1282
|
+
this.#focusBlock = target;
|
|
1283
|
+
this.#focusBlockEdge = 'end';
|
|
1284
|
+
}
|
|
1105
1285
|
}
|
|
1106
1286
|
#utf8 = new StringDecoder('utf8');
|
|
1107
1287
|
#onData(chunk) {
|
|
@@ -1227,7 +1407,27 @@ export class LocalTui {
|
|
|
1227
1407
|
await this.#acceptPastedText(text);
|
|
1228
1408
|
}
|
|
1229
1409
|
#dispatch(event) {
|
|
1410
|
+
if (event.type === 'click') {
|
|
1411
|
+
const target = this.#jumpToLatest;
|
|
1412
|
+
if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0 && target !== undefined
|
|
1413
|
+
&& event.row === target.row && event.column >= target.column && event.column < target.column + target.width) {
|
|
1414
|
+
this.#followTail();
|
|
1415
|
+
this.#render();
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (event.type === 'wheel') {
|
|
1420
|
+
if (this.#mouseTracking && !this.#paste && this.#pasteInFlight === 0) {
|
|
1421
|
+
this.#scrollBy(event.direction === 'up' ? -3 : 3);
|
|
1422
|
+
}
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1230
1425
|
if (this.#state.status === 'compacting') {
|
|
1426
|
+
if (event.type === 'key' && event.id === 'end' && this.#jumpToLatest !== undefined) {
|
|
1427
|
+
this.#followTail();
|
|
1428
|
+
this.#render();
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1231
1431
|
if (event.type === 'key' && event.id === 'pageUp') {
|
|
1232
1432
|
this.#scrollBy(-this.#pageSize());
|
|
1233
1433
|
return;
|
|
@@ -1307,6 +1507,12 @@ export class LocalTui {
|
|
|
1307
1507
|
this.#applyTranscriptSearch(event);
|
|
1308
1508
|
return;
|
|
1309
1509
|
}
|
|
1510
|
+
if (event.type === 'key' && event.id === 'end' && this.#jumpToLatest !== undefined
|
|
1511
|
+
&& this.#settings === null && this.#copySelector === null && this.#search === null) {
|
|
1512
|
+
this.#followTail();
|
|
1513
|
+
this.#render();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1310
1516
|
if (event.type === 'key') {
|
|
1311
1517
|
const action = this.#boundAction(event.id);
|
|
1312
1518
|
if (action !== undefined && this.#runAction(action))
|
|
@@ -1657,21 +1863,22 @@ export class LocalTui {
|
|
|
1657
1863
|
}
|
|
1658
1864
|
#applyPrefs(prefs, options) {
|
|
1659
1865
|
const previousMotion = this.#motion;
|
|
1660
|
-
const
|
|
1866
|
+
const density = resolveFoldDensity(prefs);
|
|
1661
1867
|
this.#themeName = prefs.theme;
|
|
1662
1868
|
this.#colors = prefs.colors;
|
|
1663
1869
|
this.#syncTrueColor();
|
|
1664
1870
|
this.#motion = prefs.motion ?? 'full';
|
|
1665
1871
|
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
1666
|
-
this.#
|
|
1872
|
+
this.#foldDensity = density;
|
|
1873
|
+
this.#foldPolicy = foldPolicy(density);
|
|
1667
1874
|
this.#checkUpdates = prefs.checkUpdates ?? true;
|
|
1668
1875
|
this.#startupChangelog = prefs.startupChangelog ?? 'summary';
|
|
1669
1876
|
this.#notificationPolicy = prefs.notifications ?? 'off';
|
|
1670
1877
|
this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
|
|
1671
1878
|
this.#configureNotifications();
|
|
1672
1879
|
this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
|
|
1673
|
-
if (options.forceToolsSync
|
|
1674
|
-
this.#toolsExpanded =
|
|
1880
|
+
if (options.forceToolsSync)
|
|
1881
|
+
this.#toolsExpanded = false;
|
|
1675
1882
|
if (previousMotion !== this.#motion) {
|
|
1676
1883
|
this.#stopRevealTick();
|
|
1677
1884
|
this.#reveal = undefined;
|
|
@@ -1708,6 +1915,36 @@ export class LocalTui {
|
|
|
1708
1915
|
this.#render();
|
|
1709
1916
|
}
|
|
1710
1917
|
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Write the Features section's state into the Profile patch.
|
|
1920
|
+
*
|
|
1921
|
+
* `disabled` is a Loader option rather than plugin config, so the settings
|
|
1922
|
+
* service cannot express it; the Profile patch is the documented surface
|
|
1923
|
+
* that can, and the Harness reads it while composing the tree. Only the
|
|
1924
|
+
* managed block is rewritten, so hand-authored rows and comments survive.
|
|
1925
|
+
* A failure is reported and the overlay keeps the value it shows.
|
|
1926
|
+
*/
|
|
1927
|
+
#writeFeaturePatch() {
|
|
1928
|
+
const path = this.#featurePatch;
|
|
1929
|
+
const features = this.#features;
|
|
1930
|
+
if (path === undefined || features === undefined)
|
|
1931
|
+
return;
|
|
1932
|
+
try {
|
|
1933
|
+
let current = '';
|
|
1934
|
+
try {
|
|
1935
|
+
current = readFileSync(path, 'utf8');
|
|
1936
|
+
}
|
|
1937
|
+
catch {
|
|
1938
|
+
current = '';
|
|
1939
|
+
}
|
|
1940
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1941
|
+
writeFileSync(path, replaceFeatureBlock(current, renderFeatureBlock(features)), 'utf8');
|
|
1942
|
+
}
|
|
1943
|
+
catch (error) {
|
|
1944
|
+
this.notice(`Feature toggles were not saved: ${error instanceof Error ? error.message : String(error)}`);
|
|
1945
|
+
this.#render();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1711
1948
|
#applySettings(event) {
|
|
1712
1949
|
if (this.#settings === null)
|
|
1713
1950
|
return;
|
|
@@ -1718,6 +1955,15 @@ export class LocalTui {
|
|
|
1718
1955
|
return;
|
|
1719
1956
|
}
|
|
1720
1957
|
if (command.kind === 'apply') {
|
|
1958
|
+
if (command.domain === 'features') {
|
|
1959
|
+
if (command.state.features === undefined)
|
|
1960
|
+
return;
|
|
1961
|
+
this.#settings = command.state;
|
|
1962
|
+
this.#features = command.state.features;
|
|
1963
|
+
this.#render();
|
|
1964
|
+
this.#writeFeaturePatch();
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1721
1967
|
if (command.domain === 'agent') {
|
|
1722
1968
|
if (this.#agentBehaviorPending || command.state.agent === undefined)
|
|
1723
1969
|
return;
|
|
@@ -1990,6 +2236,11 @@ export class LocalTui {
|
|
|
1990
2236
|
}
|
|
1991
2237
|
if (command.kind === 'suspend') {
|
|
1992
2238
|
if (process.platform !== 'win32') {
|
|
2239
|
+
if (this.#inspecting) {
|
|
2240
|
+
this.#followTail();
|
|
2241
|
+
this.#render();
|
|
2242
|
+
}
|
|
2243
|
+
this.#setMouseTracking(false);
|
|
1993
2244
|
try {
|
|
1994
2245
|
process.kill(process.pid, 'SIGTSTP');
|
|
1995
2246
|
}
|
|
@@ -2100,7 +2351,6 @@ export class LocalTui {
|
|
|
2100
2351
|
this.#copySelector = null;
|
|
2101
2352
|
this.#trajectory = null;
|
|
2102
2353
|
this.#agentHub = null;
|
|
2103
|
-
this.#followTail();
|
|
2104
2354
|
// Image placeholders are TUI-owned. Mixed image+slash drafts stay a
|
|
2105
2355
|
// submission so restore can keep the original markers; the runner strips
|
|
2106
2356
|
// them only to detect and execute Harness commands.
|
|
@@ -2111,6 +2361,7 @@ export class LocalTui {
|
|
|
2111
2361
|
this.#runSlash(slash.name, slash.args);
|
|
2112
2362
|
return;
|
|
2113
2363
|
}
|
|
2364
|
+
this.#followTail();
|
|
2114
2365
|
const pending = this.#pending;
|
|
2115
2366
|
if (pending !== null) {
|
|
2116
2367
|
pending.offAbort?.();
|
|
@@ -2144,12 +2395,12 @@ export class LocalTui {
|
|
|
2144
2395
|
this.#images.clear();
|
|
2145
2396
|
this.#ac = null;
|
|
2146
2397
|
this.#search = null;
|
|
2147
|
-
this.#followTail();
|
|
2148
2398
|
const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
|
|
2149
2399
|
if (slash !== null) {
|
|
2150
2400
|
this.#runSlash(slash.name, slash.args);
|
|
2151
2401
|
return;
|
|
2152
2402
|
}
|
|
2403
|
+
this.#followTail();
|
|
2153
2404
|
if (submittedText === '' && images.length === 0) {
|
|
2154
2405
|
this.#render();
|
|
2155
2406
|
return;
|
|
@@ -2165,25 +2416,13 @@ export class LocalTui {
|
|
|
2165
2416
|
}
|
|
2166
2417
|
const command = resolveSlashCommand(name, this.#commands());
|
|
2167
2418
|
if (command === undefined) {
|
|
2168
|
-
this.#notice('unknown command: /' + name);
|
|
2169
|
-
this.#render();
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
if (command.name === 'quit') {
|
|
2173
|
-
this.#render();
|
|
2174
|
-
this.#quit();
|
|
2175
|
-
return;
|
|
2176
|
-
}
|
|
2177
|
-
if (command.name === 'clear') {
|
|
2178
|
-
// Presentation-only reset: the agent may still own an active turn with
|
|
2179
|
-
// live status, todos, and queued inbox state. Clearing those would make
|
|
2180
|
-
// the next Ctrl-C (or follow-up) behave as if the session had finished.
|
|
2181
|
-
this.#state = { ...this.#state, blocks: [] };
|
|
2182
2419
|
this.#followTail();
|
|
2183
|
-
this.#
|
|
2420
|
+
this.#notice('unknown command: /' + name);
|
|
2184
2421
|
this.#render();
|
|
2185
2422
|
return;
|
|
2186
2423
|
}
|
|
2424
|
+
// Reading controls cover the current view; submitting a command that
|
|
2425
|
+
// opens one must preserve the same return position as a keyboard action.
|
|
2187
2426
|
if (command.name === 'settings') {
|
|
2188
2427
|
this.#runSettings(args);
|
|
2189
2428
|
return;
|
|
@@ -2192,11 +2431,8 @@ export class LocalTui {
|
|
|
2192
2431
|
void this.#runCopy(args);
|
|
2193
2432
|
return;
|
|
2194
2433
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
this.#render();
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2434
|
+
// A plugin decides whether its command opens a reading surface or changes
|
|
2435
|
+
// the document. Forwarding alone must not discard the return position.
|
|
2200
2436
|
if (!BUILTIN_SLASH_COMMANDS.some((entry) => entry.name === command.name)) {
|
|
2201
2437
|
if (this.#inspected !== undefined) {
|
|
2202
2438
|
this.#notice('Return to the parent session to run /' + command.name + '.');
|
|
@@ -2216,6 +2452,27 @@ export class LocalTui {
|
|
|
2216
2452
|
this.#render();
|
|
2217
2453
|
return;
|
|
2218
2454
|
}
|
|
2455
|
+
this.#followTail();
|
|
2456
|
+
if (command.name === 'quit') {
|
|
2457
|
+
this.#render();
|
|
2458
|
+
this.#quit();
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (command.name === 'clear') {
|
|
2462
|
+
// Presentation-only reset: the agent may still own an active turn with
|
|
2463
|
+
// live status, todos, and queued inbox state. Clearing those would make
|
|
2464
|
+
// the next Ctrl-C (or follow-up) behave as if the session had finished.
|
|
2465
|
+
this.#state = { ...this.#state, blocks: [transcriptSeam('cleared')] };
|
|
2466
|
+
this.#followTail();
|
|
2467
|
+
this.#renderer.startEpoch();
|
|
2468
|
+
this.#render();
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
if (command.name === 'tools') {
|
|
2472
|
+
this.#toolCatalog();
|
|
2473
|
+
this.#render();
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2219
2476
|
if (args !== '' && args !== 'full') {
|
|
2220
2477
|
this.#notice('Usage: /help [full]');
|
|
2221
2478
|
this.#render();
|
|
@@ -2337,7 +2594,7 @@ export class LocalTui {
|
|
|
2337
2594
|
}
|
|
2338
2595
|
this.#search = null;
|
|
2339
2596
|
this.#ac = null;
|
|
2340
|
-
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior);
|
|
2597
|
+
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features);
|
|
2341
2598
|
this.#render();
|
|
2342
2599
|
}
|
|
2343
2600
|
#notice(text) {
|
|
@@ -2406,7 +2663,13 @@ export class LocalTui {
|
|
|
2406
2663
|
if (action === 'toggle-tools') {
|
|
2407
2664
|
if (this.#search !== null)
|
|
2408
2665
|
return false;
|
|
2409
|
-
this.#
|
|
2666
|
+
this.#toggleFoldTarget();
|
|
2667
|
+
return true;
|
|
2668
|
+
}
|
|
2669
|
+
if (action === 'toggle-tool-details') {
|
|
2670
|
+
if (this.#search !== null)
|
|
2671
|
+
return false;
|
|
2672
|
+
this.#toggleToolDetails();
|
|
2410
2673
|
return true;
|
|
2411
2674
|
}
|
|
2412
2675
|
if (action === 'search-history') {
|
|
@@ -2463,6 +2726,7 @@ export class LocalTui {
|
|
|
2463
2726
|
return true;
|
|
2464
2727
|
}
|
|
2465
2728
|
try {
|
|
2729
|
+
this.#setMouseTracking(false);
|
|
2466
2730
|
this.#term.input.setRawMode?.(false);
|
|
2467
2731
|
const text = editExternally(this.#editor.text);
|
|
2468
2732
|
this.#editor.setText(text);
|
|
@@ -2479,26 +2743,130 @@ export class LocalTui {
|
|
|
2479
2743
|
}
|
|
2480
2744
|
return true;
|
|
2481
2745
|
}
|
|
2482
|
-
/**
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2746
|
+
/**
|
|
2747
|
+
* Block index the reader is looking at: the first one that starts at or below
|
|
2748
|
+
* the top of the transcript window. A collapsed row has to stay reachable
|
|
2749
|
+
* after the reader scrolls away from the tail, so the toggle aims here rather
|
|
2750
|
+
* than at the newest call.
|
|
2751
|
+
*/
|
|
2752
|
+
#viewportBlock() {
|
|
2753
|
+
const starts = this.#blockStarts;
|
|
2754
|
+
if (starts.length === 0)
|
|
2755
|
+
return undefined;
|
|
2756
|
+
const at = this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart;
|
|
2757
|
+
let index = starts.findIndex(row => row >= at);
|
|
2758
|
+
if (index < 0)
|
|
2759
|
+
index = starts.length - 1;
|
|
2760
|
+
return index;
|
|
2761
|
+
}
|
|
2762
|
+
/** Restore the completed turn under the viewport, or close its inspection. */
|
|
2763
|
+
#toggleFoldTarget() {
|
|
2764
|
+
// Flush coalesced events so hit testing uses the blocks that are on screen.
|
|
2765
|
+
if (this.#streamRenderTimer !== null)
|
|
2766
|
+
this.#render();
|
|
2767
|
+
// Inside an inspection the key is the way back out: it closes what was
|
|
2768
|
+
// opened and returns to the live tail, wherever the reader has scrolled.
|
|
2769
|
+
if (this.#inspecting) {
|
|
2770
|
+
this.#followTail();
|
|
2771
|
+
this.#render();
|
|
2772
|
+
return;
|
|
2490
2773
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2774
|
+
const anchor = this.#viewportBlock();
|
|
2775
|
+
const groups = turnGroups(this.#state).filter(item => !item.live);
|
|
2776
|
+
// The answer, changed-file record, and following notices still address
|
|
2777
|
+
// their turn when the reader is following the tail.
|
|
2778
|
+
const reach = (group) => {
|
|
2779
|
+
let at = group.until;
|
|
2780
|
+
const blocks = this.#state.blocks;
|
|
2781
|
+
while (at < blocks.length) {
|
|
2782
|
+
const block = blocks[at];
|
|
2783
|
+
if (block?.kind === 'workspace' || (block?.kind === 'notice' && block.boundary !== true)) {
|
|
2784
|
+
at += 1;
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
break;
|
|
2788
|
+
}
|
|
2789
|
+
return at - 1;
|
|
2790
|
+
};
|
|
2791
|
+
const group = anchor === undefined
|
|
2792
|
+
? undefined
|
|
2793
|
+
: groups.findLast(item => {
|
|
2794
|
+
const first = this.#state.blocks[item.start - 1]?.kind === 'user' ? item.start - 1 : item.start;
|
|
2795
|
+
return anchor >= first && anchor <= reach(item);
|
|
2796
|
+
});
|
|
2797
|
+
if (group !== undefined) {
|
|
2798
|
+
this.#toggleSet(this.#openedGroups, group.key);
|
|
2799
|
+
this.#inspect((group.answer ?? group.until) - 1);
|
|
2800
|
+
this.#render();
|
|
2801
|
+
return;
|
|
2496
2802
|
}
|
|
2497
|
-
|
|
2803
|
+
if (anchor !== undefined && this.#state.blocks[anchor]?.kind === 'toolCatalog') {
|
|
2498
2804
|
this.#toolsExpanded = !this.#toolsExpanded;
|
|
2805
|
+
this.#inspect(anchor);
|
|
2806
|
+
this.#render();
|
|
2807
|
+
}
|
|
2808
|
+
// A live turn already has the process presentation; Alt+O owns tool details.
|
|
2809
|
+
}
|
|
2810
|
+
#detailScope(group) {
|
|
2811
|
+
return group.turn === undefined ? group.key : `turn:${group.turn}`;
|
|
2812
|
+
}
|
|
2813
|
+
#detailedToolIds() {
|
|
2814
|
+
const calls = new Set();
|
|
2815
|
+
if (this.#detailedTurns.size === 0)
|
|
2816
|
+
return calls;
|
|
2817
|
+
for (const group of turnGroups(this.#state)) {
|
|
2818
|
+
if (!this.#detailedTurns.has(this.#detailScope(group)))
|
|
2819
|
+
continue;
|
|
2820
|
+
for (let at = group.start; at < group.until; at++) {
|
|
2821
|
+
const block = this.#state.blocks[at];
|
|
2822
|
+
if (block?.kind === 'tool')
|
|
2823
|
+
calls.add(block.callId);
|
|
2824
|
+
}
|
|
2499
2825
|
}
|
|
2826
|
+
return calls;
|
|
2827
|
+
}
|
|
2828
|
+
#toggleToolDetails() {
|
|
2829
|
+
if (this.#streamRenderTimer !== null)
|
|
2830
|
+
this.#render();
|
|
2831
|
+
// A long tool can fill the viewport. Target the block containing its top,
|
|
2832
|
+
// rather than the next block starting below it (possibly another turn).
|
|
2833
|
+
const anchor = this.#follow ? this.#state.blocks.length - 1
|
|
2834
|
+
: Math.max(0, this.#blockStarts.findLastIndex(row => row <= this.#scrollStart));
|
|
2835
|
+
const groups = turnGroups(this.#state);
|
|
2836
|
+
const group = groups.findLast(item => {
|
|
2837
|
+
const first = this.#state.blocks[item.start - 1]?.kind === 'user' ? item.start - 1 : item.start;
|
|
2838
|
+
let end = item.until;
|
|
2839
|
+
while (end < this.#state.blocks.length) {
|
|
2840
|
+
const block = this.#state.blocks[end];
|
|
2841
|
+
if (block?.kind !== 'workspace' && !(block?.kind === 'notice' && block.boundary !== true))
|
|
2842
|
+
break;
|
|
2843
|
+
end++;
|
|
2844
|
+
}
|
|
2845
|
+
return anchor >= first && anchor < end;
|
|
2846
|
+
});
|
|
2847
|
+
if (group === undefined)
|
|
2848
|
+
return;
|
|
2849
|
+
const scope = this.#detailScope(group);
|
|
2850
|
+
const members = groups.filter(item => this.#detailScope(item) === scope);
|
|
2851
|
+
if (members.every(member => member.calls === 0))
|
|
2852
|
+
return;
|
|
2853
|
+
this.#toggleSet(this.#detailedTurns, scope);
|
|
2854
|
+
for (const member of members)
|
|
2855
|
+
this.#openedGroups.add(member.key);
|
|
2856
|
+
const tools = members.flatMap(member => this.#state.blocks.slice(member.start, member.until)
|
|
2857
|
+
.flatMap((block, offset) => block.kind === 'tool' ? [member.start + offset] : []));
|
|
2858
|
+
const target = tools.includes(anchor) ? anchor : tools[0];
|
|
2859
|
+
this.#inspect(target);
|
|
2860
|
+
this.#focusBlock = target;
|
|
2861
|
+
this.#focusBlockEdge = 'start';
|
|
2500
2862
|
this.#render();
|
|
2501
2863
|
}
|
|
2864
|
+
#toggleSet(set, key) {
|
|
2865
|
+
if (set.has(key))
|
|
2866
|
+
set.delete(key);
|
|
2867
|
+
else
|
|
2868
|
+
set.add(key);
|
|
2869
|
+
}
|
|
2502
2870
|
/** Fold one event into the active transcript search. */
|
|
2503
2871
|
#applyTranscriptSearch(event) {
|
|
2504
2872
|
const state = this.#transcriptSearch;
|
|
@@ -2541,6 +2909,10 @@ export function apply(ctx, config) {
|
|
|
2541
2909
|
herdrReporter: new HerdrAgentReporter(),
|
|
2542
2910
|
historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
|
|
2543
2911
|
keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
|
|
2912
|
+
...(() => {
|
|
2913
|
+
const path = featurePatchPath(config.dshHome ?? dshHome);
|
|
2914
|
+
return existsSync(path) ? { featurePatch: path } : {};
|
|
2915
|
+
})(),
|
|
2544
2916
|
});
|
|
2545
2917
|
ctx.provide(TUI_SERVICE, tui);
|
|
2546
2918
|
ctx.effect(() => () => { tui.dispose(); });
|
|
@@ -2554,7 +2926,7 @@ export function apply(ctx, config) {
|
|
|
2554
2926
|
colors: resolveColors(config.colors.get(), term.output.isTTY === true),
|
|
2555
2927
|
motion: config.motion.get(),
|
|
2556
2928
|
terminalProgress: config.terminalProgress.get(),
|
|
2557
|
-
|
|
2929
|
+
foldDensity: config.foldDensity.get(),
|
|
2558
2930
|
checkUpdates: config.checkUpdates.get(),
|
|
2559
2931
|
startupChangelog: config.startupChangelog.get(),
|
|
2560
2932
|
notifications: config.notifications.get(),
|