@agi-fans/dsh-tui 0.17.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 +67 -1
- 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/status-config.d.ts +4 -1
- package/lib/chrome/status-config.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 -7
- 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/commands/auth.js +4 -2
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/jobs.d.ts +2 -2
- package/lib/commands/jobs.js +2 -2
- package/lib/commands/jobs.js.map +1 -1
- package/lib/definition.d.ts +62 -7
- package/lib/definition.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.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 +63 -11
- package/lib/runtime/provider-local.js +506 -91
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/session-runtime.d.ts +9 -1
- package/lib/runtime/session-runtime.js +2 -2
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/startup-notices.js +6 -9
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/tool-presentation.js +44 -31
- 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 +6 -2
- package/lib/session/job-notice.js +44 -3
- package/lib/session/job-notice.js.map +1 -1
- package/lib/session/session-configuration.d.ts +0 -3
- package/lib/session/session-configuration.js +0 -4
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +6 -4
- package/lib/session/session-controller.js +147 -55
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +20 -0
- package/lib/session/session-library.js +52 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.js +3 -3
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -9
- package/lib/session/subagent-tracker.js +3 -4
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +46 -18
- package/lib/session/tui-settings.js +59 -39
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/at-complete.d.ts +2 -0
- package/lib/views/at-complete.js +2 -2
- package/lib/views/at-complete.js.map +1 -1
- package/lib/views/event-views.d.ts +26 -2
- package/lib/views/event-views.js +162 -48
- 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 +12 -6
- package/lib/views/settings-list.js +41 -18
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-export.js +0 -2
- package/lib/views/transcript-export.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 +40 -38
|
@@ -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,22 +34,24 @@ 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";
|
|
37
41
|
import { MainScreenRenderer } from "../chrome/main-screen-renderer.js";
|
|
38
|
-
import { colorDisabledByEnv, detectTrueColor
|
|
42
|
+
import { colorDisabledByEnv, detectTrueColor } from "../chrome/theme.js";
|
|
39
43
|
import { renderTool } from "../chrome/tool-renderers.js";
|
|
40
|
-
import {
|
|
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";
|
|
44
49
|
import { editExternally } from "../input/external-editor.js";
|
|
50
|
+
import z from '@deepseek-ai/schemastery';
|
|
45
51
|
import { movePromptSelection, filteredPromptOptions, selectedPromptAnswer, selectedFilteredPromptAnswer, togglePromptSelection, } from "../views/prompt-selector.js";
|
|
46
52
|
import { refreshProjectContext, resolveProjectContext } from "../session/project-context.js";
|
|
47
53
|
import { pickWelcomeTips } from "../chrome/welcome-tips.js";
|
|
48
|
-
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount } from "../views/hotkeys.js";
|
|
54
|
+
import { formatEssentialHotkeysText, formatHotkeysText, hotkeyCount, keysForAction } from "../views/hotkeys.js";
|
|
49
55
|
import { imagePathCandidates, stripComposerImageMarkers, readImageFile, readImageFromClipboard, readMacClipboardFiles, } from "../input/image-paste.js";
|
|
50
56
|
import { APP_NAME, APP_VERSION } from "../session/package-metadata.js";
|
|
51
57
|
const DOUBLE_CTRL_C_MS = 500;
|
|
@@ -89,6 +95,18 @@ function windowTitleText(title) {
|
|
|
89
95
|
return [...cleaned].slice(0, WINDOW_TITLE_MAX).join('');
|
|
90
96
|
}
|
|
91
97
|
export const name = 'omdsh-tui';
|
|
98
|
+
/**
|
|
99
|
+
* Row schema. The durable preferences ride as volatile fields, so a settings
|
|
100
|
+
* form edit commits into their references and reaches this fiber through
|
|
101
|
+
* `loader/volatile-update` without remounting the terminal provider.
|
|
102
|
+
*/
|
|
103
|
+
export const Config = z.object({
|
|
104
|
+
model: z.string(),
|
|
105
|
+
historyPath: z.string(),
|
|
106
|
+
keybindingsPath: z.string(),
|
|
107
|
+
dshHome: z.string(),
|
|
108
|
+
...TUI_SETTINGS_FIELDS,
|
|
109
|
+
});
|
|
92
110
|
/**
|
|
93
111
|
* Local terminal presentation service.
|
|
94
112
|
*/
|
|
@@ -110,6 +128,25 @@ function resolveColors(preference, isTty) {
|
|
|
110
128
|
return preference;
|
|
111
129
|
return isTty && !colorDisabledByEnv();
|
|
112
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
|
+
}
|
|
113
150
|
export class LocalTui {
|
|
114
151
|
#deferInitialRender = false;
|
|
115
152
|
#terminalProfile;
|
|
@@ -133,6 +170,10 @@ export class LocalTui {
|
|
|
133
170
|
#search = null;
|
|
134
171
|
#transcriptSearch = null;
|
|
135
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;
|
|
136
177
|
#copySelector = null;
|
|
137
178
|
#trajectory = null;
|
|
138
179
|
#agentHub = null;
|
|
@@ -179,9 +220,16 @@ export class LocalTui {
|
|
|
179
220
|
#scrollStart = 0;
|
|
180
221
|
#maxStart = 0;
|
|
181
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;
|
|
182
228
|
#follow = true;
|
|
183
229
|
#focusBlock;
|
|
184
|
-
#
|
|
230
|
+
#focusBlockEdge;
|
|
231
|
+
#foldDensity = DEFAULT_FOLD_DENSITY;
|
|
232
|
+
#foldPolicy = foldPolicy(DEFAULT_FOLD_DENSITY);
|
|
185
233
|
#motion = 'full';
|
|
186
234
|
#terminalProgress = false;
|
|
187
235
|
#terminalProgressActive = false;
|
|
@@ -199,7 +247,24 @@ export class LocalTui {
|
|
|
199
247
|
#notificationThreshold = '30s';
|
|
200
248
|
#statusBar = defaultStatusBarConfig();
|
|
201
249
|
#toolsExpanded = false;
|
|
202
|
-
|
|
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();
|
|
203
268
|
#tools = [];
|
|
204
269
|
#runtimeCommands = [];
|
|
205
270
|
#prompt = null;
|
|
@@ -269,6 +334,17 @@ export class LocalTui {
|
|
|
269
334
|
this.#historyStore = paths.historyPath === undefined ? undefined : new HistoryStore(paths.historyPath);
|
|
270
335
|
this.#history = this.#historyStore?.load() ?? [];
|
|
271
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
|
+
}
|
|
272
348
|
const fallback = defaultPathSource();
|
|
273
349
|
this.#cwd = paths.cwd ?? fallback.cwd;
|
|
274
350
|
this.#readImagePath = paths.readImagePath ?? (path => readImageFile(path, this.#cwd));
|
|
@@ -300,7 +376,6 @@ export class LocalTui {
|
|
|
300
376
|
width: this.#term.width(),
|
|
301
377
|
height: this.#term.height(),
|
|
302
378
|
synchronized: this.#tty,
|
|
303
|
-
clearScrollback: this.#terminalProfile === 'direct',
|
|
304
379
|
alternateScreenOverlays: paths.alternateScreenOverlays === true,
|
|
305
380
|
});
|
|
306
381
|
if (this.#tty) {
|
|
@@ -340,8 +415,15 @@ export class LocalTui {
|
|
|
340
415
|
if (this.#tty)
|
|
341
416
|
this.#render();
|
|
342
417
|
}
|
|
343
|
-
event(event, presentation) {
|
|
344
|
-
|
|
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);
|
|
345
427
|
this.#emitNotification(this.#notifications.event({
|
|
346
428
|
type: event.type,
|
|
347
429
|
time: event.time,
|
|
@@ -368,6 +450,16 @@ export class LocalTui {
|
|
|
368
450
|
this.#plain.print();
|
|
369
451
|
}
|
|
370
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
|
+
}
|
|
371
463
|
/**
|
|
372
464
|
* Fold one live `agent/assistant-stream` chunk into the transcript and
|
|
373
465
|
* schedule the streaming reveal. Durable settlement follows on the session
|
|
@@ -532,20 +624,41 @@ export class LocalTui {
|
|
|
532
624
|
.join(' · ');
|
|
533
625
|
}
|
|
534
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;
|
|
535
641
|
const block = {
|
|
536
642
|
kind: 'notice',
|
|
537
643
|
level: options.level ?? 'info',
|
|
538
644
|
text,
|
|
539
645
|
...(options.framed === true ? { framed: true } : {}),
|
|
646
|
+
...(running
|
|
647
|
+
? { process: { turn: this.#state.turn, ...(options.processSource === undefined ? {} : { source: options.processSource }) } }
|
|
648
|
+
: {}),
|
|
540
649
|
};
|
|
541
650
|
this.#state = { ...this.#state, blocks: [...this.#state.blocks, block] };
|
|
651
|
+
if (options.focus === true)
|
|
652
|
+
this.#followTail();
|
|
542
653
|
if (this.#tty)
|
|
543
654
|
this.#render();
|
|
544
655
|
else
|
|
545
656
|
this.#plain.print();
|
|
546
657
|
}
|
|
547
|
-
commandOutput(command, text) {
|
|
658
|
+
commandOutput(command, text, options = {}) {
|
|
548
659
|
this.#state = { ...this.#state, blocks: [...this.#state.blocks, { kind: 'commandOutput', command, text }] };
|
|
660
|
+
if (options.focus === true)
|
|
661
|
+
this.#followTail();
|
|
549
662
|
if (this.#tty)
|
|
550
663
|
this.#render();
|
|
551
664
|
else
|
|
@@ -590,14 +703,18 @@ export class LocalTui {
|
|
|
590
703
|
}
|
|
591
704
|
});
|
|
592
705
|
}
|
|
593
|
-
replaceSession(events, presentations, status = 'idle') {
|
|
706
|
+
replaceSession(events, presentations, status = 'idle', reason = 'open') {
|
|
594
707
|
this.#trajectory = null;
|
|
595
708
|
const state = replayEvents(events, presentations);
|
|
596
|
-
|
|
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 };
|
|
597
714
|
this.#plain.resetPrinted();
|
|
598
715
|
this.#followTail();
|
|
599
716
|
this.#deferInitialRender = false;
|
|
600
|
-
this.#renderer.startEpoch({ replay: status === 'idle' ? 'full' : 'pinned' });
|
|
717
|
+
this.#renderer.startEpoch({ replay: reason === 'refresh' ? 'viewport' : status === 'idle' ? 'full' : 'pinned' });
|
|
601
718
|
if (this.#tty)
|
|
602
719
|
this.#render();
|
|
603
720
|
else
|
|
@@ -645,7 +762,7 @@ export class LocalTui {
|
|
|
645
762
|
#syncTrueColor() {
|
|
646
763
|
this.#trueColor = this.#colors && detectTrueColor();
|
|
647
764
|
}
|
|
648
|
-
/** Apply prefs loaded from
|
|
765
|
+
/** Apply prefs loaded from this entry's config (does not persist). */
|
|
649
766
|
applyStoredPrefs(prefs) {
|
|
650
767
|
this.#applyPrefs(prefs, { persist: false, forceToolsSync: true });
|
|
651
768
|
if (this.#settings !== null)
|
|
@@ -653,6 +770,29 @@ export class LocalTui {
|
|
|
653
770
|
if (this.#tty)
|
|
654
771
|
this.#render();
|
|
655
772
|
}
|
|
773
|
+
/** Currently applied preferences; sibling rows read these instead of the config references. */
|
|
774
|
+
prefs() {
|
|
775
|
+
return {
|
|
776
|
+
theme: this.#themeName,
|
|
777
|
+
colors: this.#colors,
|
|
778
|
+
motion: this.#motion,
|
|
779
|
+
terminalProgress: this.#terminalProgress,
|
|
780
|
+
foldDensity: this.#foldDensity,
|
|
781
|
+
checkUpdates: this.#checkUpdates,
|
|
782
|
+
startupChangelog: this.#startupChangelog,
|
|
783
|
+
notifications: this.#notificationPolicy,
|
|
784
|
+
notificationThreshold: this.#notificationThreshold,
|
|
785
|
+
statusBar: {
|
|
786
|
+
...this.#statusBar,
|
|
787
|
+
groups: [...this.#statusBar.groups],
|
|
788
|
+
...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
|
|
789
|
+
...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
|
|
790
|
+
...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
|
|
791
|
+
...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
|
|
792
|
+
...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
656
796
|
/** Called after a live `/settings` change. */
|
|
657
797
|
setPrefsPersist(persist) {
|
|
658
798
|
this.#persistPrefs = persist;
|
|
@@ -675,7 +815,7 @@ export class LocalTui {
|
|
|
675
815
|
this.#agentBehavior = undefined;
|
|
676
816
|
this.#agentBehaviorPending = false;
|
|
677
817
|
if (this.#settings !== null)
|
|
678
|
-
this.#settings = createSettings(this
|
|
818
|
+
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features);
|
|
679
819
|
if (this.#tty && !this.#disposed)
|
|
680
820
|
this.#render();
|
|
681
821
|
};
|
|
@@ -804,6 +944,7 @@ export class LocalTui {
|
|
|
804
944
|
if (this.#tty) {
|
|
805
945
|
this.#offData?.();
|
|
806
946
|
this.#offResize?.();
|
|
947
|
+
this.#setMouseTracking(false);
|
|
807
948
|
this.#term.input.setRawMode?.(false);
|
|
808
949
|
// Leave the cursor on a fresh line below the last frame so the shell
|
|
809
950
|
// prompt does not overwrite the transcript. Disable bracketed paste
|
|
@@ -989,7 +1130,7 @@ export class LocalTui {
|
|
|
989
1130
|
trueColor: this.#trueColor,
|
|
990
1131
|
themeName: this.#themeName,
|
|
991
1132
|
scrollStart: this.#follow ? Number.POSITIVE_INFINITY : this.#scrollStart,
|
|
992
|
-
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock }),
|
|
1133
|
+
...(this.#focusBlock === undefined ? {} : { focusBlock: this.#focusBlock, focusBlockEdge: this.#focusBlockEdge ?? 'start' }),
|
|
993
1134
|
...(this.#transcriptSearch === null ? {} : {
|
|
994
1135
|
transcriptSearch: {
|
|
995
1136
|
query: this.#transcriptSearch.query,
|
|
@@ -999,7 +1140,10 @@ export class LocalTui {
|
|
|
999
1140
|
},
|
|
1000
1141
|
}),
|
|
1001
1142
|
toolsExpanded: this.#toolsExpanded,
|
|
1002
|
-
expandedTools: this.#
|
|
1143
|
+
expandedTools: this.#detailedToolIds(),
|
|
1144
|
+
toolDetailsKey: keysForAction(this.#keybindings, 'toggle-tool-details'),
|
|
1145
|
+
fold: this.#foldPolicy,
|
|
1146
|
+
openedGroups: this.#openedGroups,
|
|
1003
1147
|
commands: this.#commands(),
|
|
1004
1148
|
recentSessions: this.#recentSessions,
|
|
1005
1149
|
welcomeTips: this.#welcomeTips,
|
|
@@ -1023,8 +1167,23 @@ export class LocalTui {
|
|
|
1023
1167
|
})
|
|
1024
1168
|
: { lines: [] };
|
|
1025
1169
|
this.#focusBlock = undefined;
|
|
1170
|
+
this.#focusBlockEdge = undefined;
|
|
1026
1171
|
this.#promptDocument = frame.promptDocument;
|
|
1027
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
|
+
}
|
|
1028
1187
|
this.#renderer.render(frame);
|
|
1029
1188
|
}
|
|
1030
1189
|
#scheduleStreamRender() {
|
|
@@ -1038,16 +1197,17 @@ export class LocalTui {
|
|
|
1038
1197
|
}
|
|
1039
1198
|
#syncScroll(scroll) {
|
|
1040
1199
|
if (scroll === undefined) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
this.#scrollBudget = 0;
|
|
1044
|
-
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.
|
|
1045
1202
|
return;
|
|
1046
1203
|
}
|
|
1047
1204
|
this.#scrollStart = scroll.start;
|
|
1048
1205
|
this.#maxStart = scroll.maxStart;
|
|
1049
1206
|
this.#scrollBudget = scroll.budget;
|
|
1050
|
-
|
|
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)) {
|
|
1051
1211
|
this.#follow = true;
|
|
1052
1212
|
this.#scrollStart = this.#maxStart;
|
|
1053
1213
|
}
|
|
@@ -1056,8 +1216,22 @@ export class LocalTui {
|
|
|
1056
1216
|
return Math.max(1, this.#scrollBudget > 2 ? this.#scrollBudget - 2 : 1);
|
|
1057
1217
|
}
|
|
1058
1218
|
#scrollBy(delta) {
|
|
1219
|
+
if (this.#follow && delta > 0)
|
|
1220
|
+
return;
|
|
1059
1221
|
if (delta === 0 && this.#maxStart === 0)
|
|
1060
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
|
+
}
|
|
1061
1235
|
this.#follow = false;
|
|
1062
1236
|
this.#scrollStart += delta;
|
|
1063
1237
|
if (this.#scrollStart <= 0)
|
|
@@ -1067,6 +1241,47 @@ export class LocalTui {
|
|
|
1067
1241
|
#followTail() {
|
|
1068
1242
|
this.#follow = true;
|
|
1069
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
|
+
}
|
|
1070
1285
|
}
|
|
1071
1286
|
#utf8 = new StringDecoder('utf8');
|
|
1072
1287
|
#onData(chunk) {
|
|
@@ -1192,7 +1407,27 @@ export class LocalTui {
|
|
|
1192
1407
|
await this.#acceptPastedText(text);
|
|
1193
1408
|
}
|
|
1194
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
|
+
}
|
|
1195
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
|
+
}
|
|
1196
1431
|
if (event.type === 'key' && event.id === 'pageUp') {
|
|
1197
1432
|
this.#scrollBy(-this.#pageSize());
|
|
1198
1433
|
return;
|
|
@@ -1272,6 +1507,12 @@ export class LocalTui {
|
|
|
1272
1507
|
this.#applyTranscriptSearch(event);
|
|
1273
1508
|
return;
|
|
1274
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
|
+
}
|
|
1275
1516
|
if (event.type === 'key') {
|
|
1276
1517
|
const action = this.#boundAction(event.id);
|
|
1277
1518
|
if (action !== undefined && this.#runAction(action))
|
|
@@ -1620,45 +1861,24 @@ export class LocalTui {
|
|
|
1620
1861
|
this.#agentHub = createAgentHub({ agents });
|
|
1621
1862
|
this.#render();
|
|
1622
1863
|
}
|
|
1623
|
-
#prefs() {
|
|
1624
|
-
return {
|
|
1625
|
-
theme: this.#themeName,
|
|
1626
|
-
colors: this.#colors,
|
|
1627
|
-
motion: this.#motion,
|
|
1628
|
-
terminalProgress: this.#terminalProgress,
|
|
1629
|
-
expandTools: this.#expandTools,
|
|
1630
|
-
checkUpdates: this.#checkUpdates,
|
|
1631
|
-
startupChangelog: this.#startupChangelog,
|
|
1632
|
-
notifications: this.#notificationPolicy,
|
|
1633
|
-
notificationThreshold: this.#notificationThreshold,
|
|
1634
|
-
statusBar: {
|
|
1635
|
-
...this.#statusBar,
|
|
1636
|
-
groups: [...this.#statusBar.groups],
|
|
1637
|
-
...(this.#statusBar.order === undefined ? {} : { order: [...this.#statusBar.order] }),
|
|
1638
|
-
...(this.#statusBar.meta === undefined ? {} : { meta: [...this.#statusBar.meta] }),
|
|
1639
|
-
...(this.#statusBar.metaOrder === undefined ? {} : { metaOrder: [...this.#statusBar.metaOrder] }),
|
|
1640
|
-
...(this.#statusBar.colors === undefined ? {} : { colors: { ...this.#statusBar.colors } }),
|
|
1641
|
-
...(this.#statusBar.sides === undefined ? {} : { sides: { ...this.#statusBar.sides } }),
|
|
1642
|
-
},
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
1864
|
#applyPrefs(prefs, options) {
|
|
1646
1865
|
const previousMotion = this.#motion;
|
|
1647
|
-
const
|
|
1866
|
+
const density = resolveFoldDensity(prefs);
|
|
1648
1867
|
this.#themeName = prefs.theme;
|
|
1649
1868
|
this.#colors = prefs.colors;
|
|
1650
1869
|
this.#syncTrueColor();
|
|
1651
1870
|
this.#motion = prefs.motion ?? 'full';
|
|
1652
1871
|
this.#terminalProgress = prefs.terminalProgress ?? false;
|
|
1653
|
-
this.#
|
|
1872
|
+
this.#foldDensity = density;
|
|
1873
|
+
this.#foldPolicy = foldPolicy(density);
|
|
1654
1874
|
this.#checkUpdates = prefs.checkUpdates ?? true;
|
|
1655
1875
|
this.#startupChangelog = prefs.startupChangelog ?? 'summary';
|
|
1656
1876
|
this.#notificationPolicy = prefs.notifications ?? 'off';
|
|
1657
1877
|
this.#notificationThreshold = prefs.notificationThreshold ?? '30s';
|
|
1658
1878
|
this.#configureNotifications();
|
|
1659
1879
|
this.#statusBar = resolveStatusBarConfig(prefs.statusBar, prefs.statusPreset);
|
|
1660
|
-
if (options.forceToolsSync
|
|
1661
|
-
this.#toolsExpanded =
|
|
1880
|
+
if (options.forceToolsSync)
|
|
1881
|
+
this.#toolsExpanded = false;
|
|
1662
1882
|
if (previousMotion !== this.#motion) {
|
|
1663
1883
|
this.#stopRevealTick();
|
|
1664
1884
|
this.#reveal = undefined;
|
|
@@ -1695,6 +1915,36 @@ export class LocalTui {
|
|
|
1695
1915
|
this.#render();
|
|
1696
1916
|
}
|
|
1697
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
|
+
}
|
|
1698
1948
|
#applySettings(event) {
|
|
1699
1949
|
if (this.#settings === null)
|
|
1700
1950
|
return;
|
|
@@ -1705,6 +1955,15 @@ export class LocalTui {
|
|
|
1705
1955
|
return;
|
|
1706
1956
|
}
|
|
1707
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
|
+
}
|
|
1708
1967
|
if (command.domain === 'agent') {
|
|
1709
1968
|
if (this.#agentBehaviorPending || command.state.agent === undefined)
|
|
1710
1969
|
return;
|
|
@@ -1977,6 +2236,11 @@ export class LocalTui {
|
|
|
1977
2236
|
}
|
|
1978
2237
|
if (command.kind === 'suspend') {
|
|
1979
2238
|
if (process.platform !== 'win32') {
|
|
2239
|
+
if (this.#inspecting) {
|
|
2240
|
+
this.#followTail();
|
|
2241
|
+
this.#render();
|
|
2242
|
+
}
|
|
2243
|
+
this.#setMouseTracking(false);
|
|
1980
2244
|
try {
|
|
1981
2245
|
process.kill(process.pid, 'SIGTSTP');
|
|
1982
2246
|
}
|
|
@@ -2087,7 +2351,6 @@ export class LocalTui {
|
|
|
2087
2351
|
this.#copySelector = null;
|
|
2088
2352
|
this.#trajectory = null;
|
|
2089
2353
|
this.#agentHub = null;
|
|
2090
|
-
this.#followTail();
|
|
2091
2354
|
// Image placeholders are TUI-owned. Mixed image+slash drafts stay a
|
|
2092
2355
|
// submission so restore can keep the original markers; the runner strips
|
|
2093
2356
|
// them only to detect and execute Harness commands.
|
|
@@ -2098,6 +2361,7 @@ export class LocalTui {
|
|
|
2098
2361
|
this.#runSlash(slash.name, slash.args);
|
|
2099
2362
|
return;
|
|
2100
2363
|
}
|
|
2364
|
+
this.#followTail();
|
|
2101
2365
|
const pending = this.#pending;
|
|
2102
2366
|
if (pending !== null) {
|
|
2103
2367
|
pending.offAbort?.();
|
|
@@ -2131,12 +2395,12 @@ export class LocalTui {
|
|
|
2131
2395
|
this.#images.clear();
|
|
2132
2396
|
this.#ac = null;
|
|
2133
2397
|
this.#search = null;
|
|
2134
|
-
this.#followTail();
|
|
2135
2398
|
const slash = images.length === 0 ? parseSlashInput(submittedText) : null;
|
|
2136
2399
|
if (slash !== null) {
|
|
2137
2400
|
this.#runSlash(slash.name, slash.args);
|
|
2138
2401
|
return;
|
|
2139
2402
|
}
|
|
2403
|
+
this.#followTail();
|
|
2140
2404
|
if (submittedText === '' && images.length === 0) {
|
|
2141
2405
|
this.#render();
|
|
2142
2406
|
return;
|
|
@@ -2152,25 +2416,13 @@ export class LocalTui {
|
|
|
2152
2416
|
}
|
|
2153
2417
|
const command = resolveSlashCommand(name, this.#commands());
|
|
2154
2418
|
if (command === undefined) {
|
|
2155
|
-
this.#notice('unknown command: /' + name);
|
|
2156
|
-
this.#render();
|
|
2157
|
-
return;
|
|
2158
|
-
}
|
|
2159
|
-
if (command.name === 'quit') {
|
|
2160
|
-
this.#render();
|
|
2161
|
-
this.#quit();
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
if (command.name === 'clear') {
|
|
2165
|
-
// Presentation-only reset: the agent may still own an active turn with
|
|
2166
|
-
// live status, todos, and queued inbox state. Clearing those would make
|
|
2167
|
-
// the next Ctrl-C (or follow-up) behave as if the session had finished.
|
|
2168
|
-
this.#state = { ...this.#state, blocks: [] };
|
|
2169
2419
|
this.#followTail();
|
|
2170
|
-
this.#
|
|
2420
|
+
this.#notice('unknown command: /' + name);
|
|
2171
2421
|
this.#render();
|
|
2172
2422
|
return;
|
|
2173
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.
|
|
2174
2426
|
if (command.name === 'settings') {
|
|
2175
2427
|
this.#runSettings(args);
|
|
2176
2428
|
return;
|
|
@@ -2179,11 +2431,8 @@ export class LocalTui {
|
|
|
2179
2431
|
void this.#runCopy(args);
|
|
2180
2432
|
return;
|
|
2181
2433
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
this.#render();
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2434
|
+
// A plugin decides whether its command opens a reading surface or changes
|
|
2435
|
+
// the document. Forwarding alone must not discard the return position.
|
|
2187
2436
|
if (!BUILTIN_SLASH_COMMANDS.some((entry) => entry.name === command.name)) {
|
|
2188
2437
|
if (this.#inspected !== undefined) {
|
|
2189
2438
|
this.#notice('Return to the parent session to run /' + command.name + '.');
|
|
@@ -2203,6 +2452,27 @@ export class LocalTui {
|
|
|
2203
2452
|
this.#render();
|
|
2204
2453
|
return;
|
|
2205
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
|
+
}
|
|
2206
2476
|
if (args !== '' && args !== 'full') {
|
|
2207
2477
|
this.#notice('Usage: /help [full]');
|
|
2208
2478
|
this.#render();
|
|
@@ -2324,7 +2594,7 @@ export class LocalTui {
|
|
|
2324
2594
|
}
|
|
2325
2595
|
this.#search = null;
|
|
2326
2596
|
this.#ac = null;
|
|
2327
|
-
this.#settings = createSettings(this
|
|
2597
|
+
this.#settings = createSettings(this.prefs(), undefined, this.#agentBehavior, this.#features);
|
|
2328
2598
|
this.#render();
|
|
2329
2599
|
}
|
|
2330
2600
|
#notice(text) {
|
|
@@ -2393,7 +2663,13 @@ export class LocalTui {
|
|
|
2393
2663
|
if (action === 'toggle-tools') {
|
|
2394
2664
|
if (this.#search !== null)
|
|
2395
2665
|
return false;
|
|
2396
|
-
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();
|
|
2397
2673
|
return true;
|
|
2398
2674
|
}
|
|
2399
2675
|
if (action === 'search-history') {
|
|
@@ -2450,6 +2726,7 @@ export class LocalTui {
|
|
|
2450
2726
|
return true;
|
|
2451
2727
|
}
|
|
2452
2728
|
try {
|
|
2729
|
+
this.#setMouseTracking(false);
|
|
2453
2730
|
this.#term.input.setRawMode?.(false);
|
|
2454
2731
|
const text = editExternally(this.#editor.text);
|
|
2455
2732
|
this.#editor.setText(text);
|
|
@@ -2466,26 +2743,130 @@ export class LocalTui {
|
|
|
2466
2743
|
}
|
|
2467
2744
|
return true;
|
|
2468
2745
|
}
|
|
2469
|
-
/**
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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;
|
|
2477
2773
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
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;
|
|
2483
2802
|
}
|
|
2484
|
-
|
|
2803
|
+
if (anchor !== undefined && this.#state.blocks[anchor]?.kind === 'toolCatalog') {
|
|
2485
2804
|
this.#toolsExpanded = !this.#toolsExpanded;
|
|
2805
|
+
this.#inspect(anchor);
|
|
2806
|
+
this.#render();
|
|
2486
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
|
+
}
|
|
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';
|
|
2487
2862
|
this.#render();
|
|
2488
2863
|
}
|
|
2864
|
+
#toggleSet(set, key) {
|
|
2865
|
+
if (set.has(key))
|
|
2866
|
+
set.delete(key);
|
|
2867
|
+
else
|
|
2868
|
+
set.add(key);
|
|
2869
|
+
}
|
|
2489
2870
|
/** Fold one event into the active transcript search. */
|
|
2490
2871
|
#applyTranscriptSearch(event) {
|
|
2491
2872
|
const state = this.#transcriptSearch;
|
|
@@ -2521,21 +2902,55 @@ export function apply(ctx, config) {
|
|
|
2521
2902
|
return () => { process.stdout.removeListener('resize', listener); };
|
|
2522
2903
|
},
|
|
2523
2904
|
};
|
|
2524
|
-
const tui = new LocalTui(term, config.model, config.colors,
|
|
2905
|
+
const tui = new LocalTui(term, config.model, config.colors.get(), config.theme.get(), copyToClipboard, {
|
|
2525
2906
|
deferInitialRender: true,
|
|
2526
2907
|
terminalProfile,
|
|
2527
2908
|
alternateScreenOverlays: terminalProfile === 'direct',
|
|
2528
2909
|
herdrReporter: new HerdrAgentReporter(),
|
|
2529
2910
|
historyPath: config.historyPath ?? join(dshHome, 'omdsh', 'history.jsonl'),
|
|
2530
2911
|
keybindingsPath: config.keybindingsPath ?? join(dshHome, 'omdsh', 'keybindings.json'),
|
|
2912
|
+
...(() => {
|
|
2913
|
+
const path = featurePatchPath(config.dshHome ?? dshHome);
|
|
2914
|
+
return existsSync(path) ? { featurePatch: path } : {};
|
|
2915
|
+
})(),
|
|
2531
2916
|
});
|
|
2532
2917
|
ctx.provide(TUI_SERVICE, tui);
|
|
2533
2918
|
ctx.effect(() => () => { tui.dispose(); });
|
|
2919
|
+
// Preferences are this entry's own volatile config: the initial read applies
|
|
2920
|
+
// them without persisting, a committed form edit dispatches a volatile update
|
|
2921
|
+
// to this fiber only, and an in-app `/settings` change writes back through the
|
|
2922
|
+
// settings service, which persists the edit into the active profile patch.
|
|
2923
|
+
const storedPrefs = () => {
|
|
2924
|
+
const prefs = {
|
|
2925
|
+
theme: config.theme.get(),
|
|
2926
|
+
colors: resolveColors(config.colors.get(), term.output.isTTY === true),
|
|
2927
|
+
motion: config.motion.get(),
|
|
2928
|
+
terminalProgress: config.terminalProgress.get(),
|
|
2929
|
+
foldDensity: config.foldDensity.get(),
|
|
2930
|
+
checkUpdates: config.checkUpdates.get(),
|
|
2931
|
+
startupChangelog: config.startupChangelog.get(),
|
|
2932
|
+
notifications: config.notifications.get(),
|
|
2933
|
+
notificationThreshold: config.notificationThreshold.get(),
|
|
2934
|
+
};
|
|
2935
|
+
const statusBar = config.statusBar.get();
|
|
2936
|
+
// The reference holds a frozen snapshot; the provider keeps a mutable copy.
|
|
2937
|
+
if (statusBar !== undefined)
|
|
2938
|
+
prefs.statusBar = structuredClone(statusBar);
|
|
2939
|
+
const statusPreset = config.statusPreset.get();
|
|
2940
|
+
if (statusPreset !== undefined)
|
|
2941
|
+
prefs.statusPreset = statusPreset;
|
|
2942
|
+
return prefs;
|
|
2943
|
+
};
|
|
2944
|
+
tui.applyStoredPrefs(storedPrefs());
|
|
2945
|
+
ctx.on('loader/volatile-update', () => { tui.applyStoredPrefs(storedPrefs()); });
|
|
2946
|
+
// Persistence needs the settings service; without one the provider still runs
|
|
2947
|
+
// and an in-app change stays session-local, as it did before.
|
|
2534
2948
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2949
|
+
tui.setPrefsPersist((prefs) => {
|
|
2950
|
+
const ops = Object.entries(prefs).map(([key, value]) => ({ op: 'set', path: [key], value }));
|
|
2951
|
+
void settingsCtx.settings.mutate(TUI_SETTINGS_ENTRY, ops)
|
|
2952
|
+
.catch((error) => { settingsCtx.logger.warn(error); });
|
|
2953
|
+
});
|
|
2539
2954
|
});
|
|
2540
2955
|
}
|
|
2541
2956
|
//# sourceMappingURL=provider-local.js.map
|