@agi-fans/dsh-tui 0.16.0 → 0.18.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 +53 -1
- package/lib/chrome/status-config.d.ts +4 -1
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/tool-args.d.ts +9 -0
- package/lib/chrome/tool-args.js +20 -0
- package/lib/chrome/tool-args.js.map +1 -0
- package/lib/chrome/tool-renderers.js +4 -15
- package/lib/chrome/tool-renderers.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/commands/session.js +2 -4
- package/lib/commands/session.js.map +1 -1
- package/lib/definition.d.ts +3 -0
- 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/runtime/composer-images.d.ts +44 -0
- package/lib/runtime/composer-images.js +149 -0
- package/lib/runtime/composer-images.js.map +1 -0
- package/lib/runtime/herdr-agent.d.ts +95 -0
- package/lib/runtime/herdr-agent.js +258 -0
- package/lib/runtime/herdr-agent.js.map +1 -0
- package/lib/runtime/plain-tui.d.ts +34 -0
- package/lib/runtime/plain-tui.js +119 -0
- package/lib/runtime/plain-tui.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +46 -7
- package/lib/runtime/provider-local.js +181 -299
- 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 +18 -3
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/content-text.d.ts +12 -0
- package/lib/session/content-text.js +13 -0
- package/lib/session/content-text.js.map +1 -0
- package/lib/session/history-store.js.map +1 -0
- package/lib/session/job-notice.d.ts +2 -2
- package/lib/session/job-notice.js.map +1 -1
- package/lib/session/json-file.d.ts +5 -0
- package/lib/session/json-file.js +21 -0
- package/lib/session/json-file.js.map +1 -0
- package/lib/session/model-favorites.js +15 -25
- package/lib/session/model-favorites.js.map +1 -1
- package/lib/session/release-notes.js +1 -18
- package/lib/session/release-notes.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 +160 -182
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +20 -0
- package/lib/session/session-library.js +55 -13
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.js +16 -46
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +51 -0
- package/lib/session/subagent-tracker.js +128 -0
- package/lib/session/subagent-tracker.js.map +1 -0
- package/lib/session/tui-settings.d.ts +41 -18
- package/lib/session/tui-settings.js +57 -39
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/update-check.js +2 -15
- package/lib/session/update-check.js.map +1 -1
- package/lib/session/version-compare.d.ts +3 -0
- package/lib/session/version-compare.js +21 -0
- package/lib/session/version-compare.js.map +1 -0
- 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 +11 -270
- package/lib/views/event-views.js +21 -976
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/settings-list.d.ts +2 -2
- package/lib/views/settings-list.js +12 -11
- 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 +179 -0
- package/lib/views/transcript-render.js +916 -0
- package/lib/views/transcript-render.js.map +1 -0
- package/lib/views/transcript-types.d.ts +104 -0
- package/lib/views/transcript-types.js +53 -0
- package/lib/views/transcript-types.js.map +1 -0
- package/package.json +39 -37
- package/lib/views/history-store.js.map +0 -1
- /package/lib/{views → session}/history-store.d.ts +0 -0
- /package/lib/{views → session}/history-store.js +0 -0
|
@@ -7,32 +7,36 @@
|
|
|
7
7
|
* @module @agi-fans/dsh-tui/session-controller
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { join } from 'node:path';
|
|
10
11
|
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
11
12
|
import { createUserMessage, } from '@deepseek-ai/dsh-llm';
|
|
12
13
|
import { isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
14
|
+
import { SessionAlreadyOwnedError } from '@deepseek-ai/dsh-session-persistence';
|
|
13
15
|
import { readColdSessionLog } from '@deepseek-ai/dsh-session-query';
|
|
14
|
-
import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
16
|
+
import { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
15
17
|
import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
16
18
|
import { firstVisibleStreamTime } from "../views/stream-time.js";
|
|
17
19
|
import { LiveAttemptTracker } from "./live-attempt-tracker.js";
|
|
20
|
+
import { blocksText } from "./content-text.js";
|
|
18
21
|
import { jobNoticeFor } from "./job-notice.js";
|
|
19
|
-
import { descendantDepth, isSteerableSubagent
|
|
22
|
+
import { descendantDepth, isSteerableSubagent } from "./subagent-roster.js";
|
|
23
|
+
import { readRecentRows, writeRecentRows } from "./session-library.js";
|
|
24
|
+
import { readJsonFile, writeJsonAtomic } from "./json-file.js";
|
|
25
|
+
import { SubagentTracker } from "./subagent-tracker.js";
|
|
20
26
|
import { commandPermission } from "../commands/permission.js";
|
|
21
|
-
import { isBlankSession,
|
|
27
|
+
import { isBlankSession, } from "./session-configuration.js";
|
|
22
28
|
import { stripComposerImageMarkers } from "../input/image-paste.js";
|
|
23
29
|
async function setupAgentContext(agentCtx, agent, selection) {
|
|
24
30
|
installModelSelection(agentCtx, selection);
|
|
25
31
|
const agentPresets = agentCtx.get('agentPresets');
|
|
26
32
|
const sessionProjections = agentCtx.get('sessionProjections');
|
|
27
|
-
|
|
28
|
-
if (agentPresets === undefined || sessionProjections === undefined || tools === undefined) {
|
|
33
|
+
if (agentPresets === undefined || sessionProjections === undefined) {
|
|
29
34
|
throw new Error('agent configuration services are unavailable');
|
|
30
35
|
}
|
|
31
36
|
const agentPreset = sessionProjections.stateOf(agent.session, 'agentPreset') ?? agentPresets.defaultId;
|
|
32
37
|
const mounted = await agentPresets.mount(agentCtx, agentPreset);
|
|
33
|
-
const disposeToolPresentation = tools.presentAs(toolPresentationForPreset(mounted.id));
|
|
34
38
|
await agentCtx.plugin(commandPermission(agent));
|
|
35
|
-
return { agentPreset: mounted.id
|
|
39
|
+
return { agentPreset: mounted.id };
|
|
36
40
|
}
|
|
37
41
|
function parseControl(line) {
|
|
38
42
|
const match = /^\/([a-z][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)(?:\s+(.*))?$/su.exec(line.trim());
|
|
@@ -245,12 +249,7 @@ function explicitSessionTitle(events) {
|
|
|
245
249
|
function humanMessageText(event) {
|
|
246
250
|
if (event.type !== 'user/message' || event.data.source.kind !== 'user')
|
|
247
251
|
return undefined;
|
|
248
|
-
const text = event.data.content
|
|
249
|
-
.filter((block) => block.type === 'text')
|
|
250
|
-
.map(block => block.text)
|
|
251
|
-
.join(' ')
|
|
252
|
-
.replace(/\s+/gu, ' ')
|
|
253
|
-
.trim();
|
|
252
|
+
const text = blocksText(event.data.content, { collapse: true });
|
|
254
253
|
return text === '' ? undefined : text;
|
|
255
254
|
}
|
|
256
255
|
/** Find direct human messages whose preceding log prefix is safe to seed into a fork. */
|
|
@@ -270,12 +269,7 @@ export function conversationTurns(events) {
|
|
|
270
269
|
}
|
|
271
270
|
if (open === undefined || event.type !== 'user/message' || event.data.source.kind !== 'user')
|
|
272
271
|
continue;
|
|
273
|
-
const text = event.data.content
|
|
274
|
-
.filter((block) => block.type === 'text')
|
|
275
|
-
.map(block => block.text)
|
|
276
|
-
.join('\n')
|
|
277
|
-
.replace(/\s+/gu, ' ')
|
|
278
|
-
.trim();
|
|
272
|
+
const text = blocksText(event.data.content, { collapse: true });
|
|
279
273
|
const imageCount = event.data.content.filter(block => block.type === 'image').length;
|
|
280
274
|
if (text === '' && imageCount === 0)
|
|
281
275
|
continue;
|
|
@@ -364,10 +358,7 @@ export function encodeCommandAttachments(images) {
|
|
|
364
358
|
}
|
|
365
359
|
/** Rehydrate one durable human inbox message into an editable composer draft. */
|
|
366
360
|
export async function restoreSubmissionMessage(message, attachments) {
|
|
367
|
-
const text = message.content
|
|
368
|
-
.filter((block) => block.type === 'text')
|
|
369
|
-
.map(block => block.text)
|
|
370
|
-
.join('\n');
|
|
361
|
+
const text = blocksText(message.content);
|
|
371
362
|
const refs = message.content
|
|
372
363
|
.filter((block) => block.type === 'image')
|
|
373
364
|
.map(block => block.attachment);
|
|
@@ -401,19 +392,38 @@ export class SessionRuntime {
|
|
|
401
392
|
#releasedHandles = new WeakSet();
|
|
402
393
|
#disposed = false;
|
|
403
394
|
#off = [];
|
|
404
|
-
#
|
|
405
|
-
#publishedSubagents;
|
|
395
|
+
#tracker;
|
|
406
396
|
#streamAttempts = new LiveAttemptTracker();
|
|
407
397
|
#recentCache = new Map();
|
|
398
|
+
#recentRowsPath;
|
|
399
|
+
#upgradeMarkerPath;
|
|
400
|
+
#recentRowsLoaded = false;
|
|
408
401
|
#refreshInFlight = null;
|
|
409
402
|
#refreshDirty = false;
|
|
410
|
-
#subagentEpoch = 0;
|
|
411
403
|
#inspectEpoch = 0;
|
|
412
404
|
#activationEpoch = 0;
|
|
413
405
|
#inspectedId;
|
|
414
|
-
constructor(ctx, tui) {
|
|
406
|
+
constructor(ctx, tui, options = {}) {
|
|
415
407
|
this.#ctx = ctx;
|
|
416
408
|
this.#tui = tui;
|
|
409
|
+
this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
|
|
410
|
+
this.#upgradeMarkerPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'sessions-upgraded.json');
|
|
411
|
+
this.#tracker = new SubagentTracker({
|
|
412
|
+
rootId: () => this.#active?.handle.agent.id,
|
|
413
|
+
depth: (session) => {
|
|
414
|
+
const rootId = this.#active?.handle.agent.id;
|
|
415
|
+
return rootId === undefined ? undefined : descendantDepth(session, rootId, this.#lookupSession);
|
|
416
|
+
},
|
|
417
|
+
agentStatus: id => this.#ctx.get('agents')?.get(SessionId(id))?.status,
|
|
418
|
+
sessions: () => this.#ctx.get('sessions')?.list() ?? [],
|
|
419
|
+
listChildren: rootId => this.#ctx.get('subagents')?.listChildren(SessionId(rootId)),
|
|
420
|
+
publish: (roster) => {
|
|
421
|
+
this.#tui.setSubagents(roster);
|
|
422
|
+
if (this.#inspectedId === undefined)
|
|
423
|
+
return;
|
|
424
|
+
this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
|
|
425
|
+
},
|
|
426
|
+
});
|
|
417
427
|
this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
|
|
418
428
|
this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
|
|
419
429
|
this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
|
|
@@ -443,8 +453,13 @@ export class SessionRuntime {
|
|
|
443
453
|
}
|
|
444
454
|
const jobs = this.#ctx.get('jobs');
|
|
445
455
|
if (jobs !== undefined) {
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
// Settlements speak session ids, so the owning Agent is resolved here;
|
|
457
|
+
// a job whose owner is gone has no notice to show.
|
|
458
|
+
this.#off.push(jobs.events.subscribe({ owners: 'scope' }, (event) => {
|
|
459
|
+
if (event.type !== 'settled')
|
|
460
|
+
return;
|
|
461
|
+
const owner = event.job.owner === undefined ? undefined : this.#ctx.get('agents')?.get(event.job.owner);
|
|
462
|
+
const notice = jobNoticeFor(event.job, owner, this.#active?.handle.agent);
|
|
448
463
|
if (notice !== undefined)
|
|
449
464
|
tui.notice(notice);
|
|
450
465
|
}));
|
|
@@ -457,16 +472,13 @@ export class SessionRuntime {
|
|
|
457
472
|
}
|
|
458
473
|
if (payload.agent.id === this.#inspectedId)
|
|
459
474
|
tui.setStatus(payload.status);
|
|
460
|
-
this.#
|
|
475
|
+
this.#tracker.noteStatus(payload.agent.session, payload.status);
|
|
461
476
|
}));
|
|
462
477
|
this.#off.push(ctx.on('session/created', (session) => {
|
|
463
|
-
this.#
|
|
478
|
+
this.#tracker.noteSession(session);
|
|
464
479
|
}));
|
|
465
480
|
this.#off.push(ctx.on('session/disposed', (session) => {
|
|
466
|
-
|
|
467
|
-
return;
|
|
468
|
-
this.#subagents.setAgentStatus(session.id, 'gone');
|
|
469
|
-
this.#pushSubagents();
|
|
481
|
+
this.#tracker.noteGone(session);
|
|
470
482
|
}));
|
|
471
483
|
this.#off.push(ctx.on('session/event', (session, event) => {
|
|
472
484
|
const active = this.#active;
|
|
@@ -475,7 +487,7 @@ export class SessionRuntime {
|
|
|
475
487
|
if (session.id === this.#inspectedId) {
|
|
476
488
|
const child = ctx.get('agents')?.get(session.id);
|
|
477
489
|
tui.event(event, child === undefined ? undefined : ctx.get('tuiToolPresentation')?.event(child, event));
|
|
478
|
-
this.#
|
|
490
|
+
this.#tracker.noteEvent(session, event);
|
|
479
491
|
return;
|
|
480
492
|
}
|
|
481
493
|
if (session === active.handle.agent.session) {
|
|
@@ -483,14 +495,14 @@ export class SessionRuntime {
|
|
|
483
495
|
tui.event(event, ctx.get('tuiToolPresentation')?.event(active.handle.agent, event));
|
|
484
496
|
}
|
|
485
497
|
if (event.type === 'subagent/catalog')
|
|
486
|
-
this.#
|
|
498
|
+
this.#tracker.observeCatalog(session, [event]);
|
|
487
499
|
if (shouldRefreshSessionInfoAfter(event))
|
|
488
500
|
this.#pushSessionInfo();
|
|
489
501
|
if (event.type === 'session/title')
|
|
490
502
|
void this.refreshRecent();
|
|
491
503
|
return;
|
|
492
504
|
}
|
|
493
|
-
this.#
|
|
505
|
+
this.#tracker.noteEvent(session, event);
|
|
494
506
|
}));
|
|
495
507
|
// Format v2 moved live assistant increments off the session log. The
|
|
496
508
|
// durable `session/event` firehose now carries only settlements; chunk
|
|
@@ -535,6 +547,13 @@ export class SessionRuntime {
|
|
|
535
547
|
this.#pushSessionInfo();
|
|
536
548
|
}));
|
|
537
549
|
}
|
|
550
|
+
// One-time per session format, off the first render: stored logs an earlier
|
|
551
|
+
// release wrote get their current-format generation here rather than on the
|
|
552
|
+
// next listing or search that has to decode them.
|
|
553
|
+
ctx.inject(['sessionPersistence'], (persistenceCtx) => {
|
|
554
|
+
void this.#upgradeStoredSessions(persistenceCtx.sessionPersistence)
|
|
555
|
+
.catch((error) => { persistenceCtx.logger.warn(error); });
|
|
556
|
+
});
|
|
538
557
|
}
|
|
539
558
|
get agent() {
|
|
540
559
|
return this.#active?.handle.agent;
|
|
@@ -688,7 +707,6 @@ export class SessionRuntime {
|
|
|
688
707
|
contextWindow: undefined,
|
|
689
708
|
reasoningEffort: undefined,
|
|
690
709
|
agentPreset: configuration.agentPreset,
|
|
691
|
-
disposeToolPresentation: configuration.disposeToolPresentation,
|
|
692
710
|
});
|
|
693
711
|
await this.refreshRecent();
|
|
694
712
|
}
|
|
@@ -760,10 +778,7 @@ export class SessionRuntime {
|
|
|
760
778
|
if (message?.type !== 'user/message' || message.data.source.kind !== 'user') {
|
|
761
779
|
throw new Error('The selected conversation turn is no longer available.');
|
|
762
780
|
}
|
|
763
|
-
const text = message.data.content
|
|
764
|
-
.filter((block) => block.type === 'text')
|
|
765
|
-
.map(block => block.text)
|
|
766
|
-
.join('\n');
|
|
781
|
+
const text = blocksText(message.data.content);
|
|
767
782
|
const imageRefs = message.data.content
|
|
768
783
|
.filter((block) => block.type === 'image')
|
|
769
784
|
.map(block => block.attachment);
|
|
@@ -820,7 +835,6 @@ export class SessionRuntime {
|
|
|
820
835
|
contextWindow: undefined,
|
|
821
836
|
reasoningEffort: undefined,
|
|
822
837
|
agentPreset: configuration.agentPreset,
|
|
823
|
-
disposeToolPresentation: configuration.disposeToolPresentation,
|
|
824
838
|
});
|
|
825
839
|
this.#tui.restoreInput({ text, images });
|
|
826
840
|
this.#tui.notice(`Rewound to before turn ${selected.turn}. The original session remains available in /resume.`);
|
|
@@ -867,15 +881,7 @@ export class SessionRuntime {
|
|
|
867
881
|
const active = this.#requiredActive();
|
|
868
882
|
if (active.agentPreset === id)
|
|
869
883
|
return active.agentPreset;
|
|
870
|
-
const previousPreset = active.agentPreset;
|
|
871
884
|
const preset = await this.#ctx.agentPresets.recompose(agent.ctx, id);
|
|
872
|
-
try {
|
|
873
|
-
this.#replaceToolPresentation(active, toolPresentationForPreset(preset.id));
|
|
874
|
-
}
|
|
875
|
-
catch (error) {
|
|
876
|
-
await this.#ctx.agentPresets.recompose(agent.ctx, previousPreset);
|
|
877
|
-
throw error;
|
|
878
|
-
}
|
|
879
885
|
active.agentPreset = preset.id;
|
|
880
886
|
agent.session.append('agent-preset/selected', { agentPreset: preset.id });
|
|
881
887
|
this.#pushTools();
|
|
@@ -895,6 +901,50 @@ export class SessionRuntime {
|
|
|
895
901
|
this.#pushSessionInfo();
|
|
896
902
|
return outcome;
|
|
897
903
|
}
|
|
904
|
+
/**
|
|
905
|
+
* Publish a current-format generation for every stored session an earlier
|
|
906
|
+
* release wrote. A read open never upgrades a log, so without this pass
|
|
907
|
+
* every listing and search keeps decoding the historical corpus — the pass
|
|
908
|
+
* runs once per session format, in the background, and skips a session
|
|
909
|
+
* another live process owns.
|
|
910
|
+
*/
|
|
911
|
+
async #upgradeStoredSessions(persistence) {
|
|
912
|
+
const markerPath = this.#upgradeMarkerPath;
|
|
913
|
+
if (markerPath === undefined)
|
|
914
|
+
return;
|
|
915
|
+
const marker = readJsonFile(markerPath);
|
|
916
|
+
if (typeof marker?.sessionFormat === 'number' && marker.sessionFormat >= SESSION_FORMAT_VERSION)
|
|
917
|
+
return;
|
|
918
|
+
const before = await persistence.list();
|
|
919
|
+
const revisions = new Map(before.map(snapshot => [String(snapshot.header.id), snapshot.revision]));
|
|
920
|
+
const active = this.#active?.handle.agent.id;
|
|
921
|
+
for (const snapshot of before) {
|
|
922
|
+
if (this.#disposed)
|
|
923
|
+
return;
|
|
924
|
+
if (String(snapshot.header.id) === active)
|
|
925
|
+
continue;
|
|
926
|
+
try {
|
|
927
|
+
const handle = await persistence.open(snapshot.header.id, 'write');
|
|
928
|
+
await handle.close();
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
// Another process owns it, or the log is unreadable: leave it alone.
|
|
932
|
+
if (!(error instanceof SessionAlreadyOwnedError))
|
|
933
|
+
this.#ctx.logger.debug(error);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (this.#disposed)
|
|
937
|
+
return;
|
|
938
|
+
let upgraded = 0;
|
|
939
|
+
for (const snapshot of await persistence.list()) {
|
|
940
|
+
if (revisions.get(String(snapshot.header.id)) !== snapshot.revision)
|
|
941
|
+
upgraded += 1;
|
|
942
|
+
}
|
|
943
|
+
writeJsonAtomic(markerPath, { sessionFormat: SESSION_FORMAT_VERSION });
|
|
944
|
+
if (upgraded > 0) {
|
|
945
|
+
this.#tui.notice(`Upgraded ${upgraded} stored session${upgraded === 1 ? '' : 's'} to the current format.`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
898
948
|
async refreshRecent() {
|
|
899
949
|
// Coalesce concurrent callers: the recent list is one snapshot, and a
|
|
900
950
|
// burst of title/status events must not queue a read of every stored log.
|
|
@@ -924,24 +974,40 @@ export class SessionRuntime {
|
|
|
924
974
|
}
|
|
925
975
|
const snapshots = (await persistence.list()).filter(snapshot => snapshot.header.origin !== 'subagent')
|
|
926
976
|
.sort((left, right) => right.header.createdAt - left.header.createdAt);
|
|
977
|
+
// A stored row stays valid while its revision holds, so a cold process
|
|
978
|
+
// renders labels for unchanged sessions without reading their logs; only a
|
|
979
|
+
// new or changed session pays a read.
|
|
980
|
+
if (!this.#recentRowsLoaded && this.#recentRowsPath !== undefined) {
|
|
981
|
+
this.#recentRowsLoaded = true;
|
|
982
|
+
for (const [id, memo] of readRecentRows(this.#recentRowsPath))
|
|
983
|
+
this.#recentCache.set(id, memo);
|
|
984
|
+
}
|
|
927
985
|
const rows = [];
|
|
986
|
+
let memoChanged = false;
|
|
928
987
|
const liveIds = new Set(snapshots.map(snapshot => snapshot.header.id));
|
|
929
|
-
for (const id of this.#recentCache.keys())
|
|
930
|
-
if (
|
|
931
|
-
|
|
988
|
+
for (const id of this.#recentCache.keys()) {
|
|
989
|
+
if (liveIds.has(id))
|
|
990
|
+
continue;
|
|
991
|
+
this.#recentCache.delete(id);
|
|
992
|
+
memoChanged = true;
|
|
993
|
+
}
|
|
932
994
|
for (const snapshot of snapshots) {
|
|
933
995
|
const header = snapshot.header;
|
|
934
996
|
const cached = this.#recentCache.get(header.id);
|
|
935
997
|
if (cached?.revision === snapshot.revision) {
|
|
936
|
-
|
|
998
|
+
if (cached.row !== null)
|
|
999
|
+
rows.push(cached.row);
|
|
937
1000
|
continue;
|
|
938
1001
|
}
|
|
939
1002
|
try {
|
|
940
1003
|
const log = await readColdSessionLog(persistence, header.id);
|
|
941
1004
|
const status = recentSessionStatus(log.events);
|
|
942
1005
|
const content = recentSessionContent(log.events);
|
|
943
|
-
if (content === undefined)
|
|
1006
|
+
if (content === undefined) {
|
|
1007
|
+
this.#recentCache.set(header.id, { revision: snapshot.revision, row: null });
|
|
1008
|
+
memoChanged = true;
|
|
944
1009
|
continue;
|
|
1010
|
+
}
|
|
945
1011
|
const row = {
|
|
946
1012
|
id: header.id,
|
|
947
1013
|
...content,
|
|
@@ -951,12 +1017,15 @@ export class SessionRuntime {
|
|
|
951
1017
|
...(status === undefined ? {} : { status }),
|
|
952
1018
|
};
|
|
953
1019
|
this.#recentCache.set(header.id, { revision: snapshot.revision, row });
|
|
1020
|
+
memoChanged = true;
|
|
954
1021
|
rows.push(row);
|
|
955
1022
|
}
|
|
956
1023
|
catch {
|
|
957
1024
|
rows.push({ id: header.id, title: '(unavailable session)', createdAt: header.createdAt });
|
|
958
1025
|
}
|
|
959
1026
|
}
|
|
1027
|
+
if (memoChanged && this.#recentRowsPath !== undefined)
|
|
1028
|
+
writeRecentRows(this.#recentRowsPath, this.#recentCache);
|
|
960
1029
|
this.#recent = rows;
|
|
961
1030
|
this.#pushSessionInfo();
|
|
962
1031
|
}
|
|
@@ -964,10 +1033,9 @@ export class SessionRuntime {
|
|
|
964
1033
|
if (this.#disposed)
|
|
965
1034
|
return;
|
|
966
1035
|
this.#disposed = true;
|
|
967
|
-
this.#subagentEpoch += 1;
|
|
968
1036
|
this.#activationEpoch += 1;
|
|
969
1037
|
this.#inspectedId = undefined;
|
|
970
|
-
this.#
|
|
1038
|
+
this.#tracker.reset();
|
|
971
1039
|
this.#tui.setInspectedSubagent(undefined);
|
|
972
1040
|
this.#tui.setSubagents(undefined);
|
|
973
1041
|
this.#tui.setSessionSearch();
|
|
@@ -1006,9 +1074,30 @@ export class SessionRuntime {
|
|
|
1006
1074
|
contextWindow: undefined,
|
|
1007
1075
|
reasoningEffort: undefined,
|
|
1008
1076
|
agentPreset: configuration.agentPreset,
|
|
1009
|
-
disposeToolPresentation: configuration.disposeToolPresentation,
|
|
1010
1077
|
};
|
|
1011
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Claim the activated session into the cwd workspace so grouped session
|
|
1081
|
+
* surfaces (the harness web sidebar) file it instead of leaving it
|
|
1082
|
+
* ungrouped. Best-effort like `#resolveModelInfo`: the registry is an
|
|
1083
|
+
* optional service and a failed claim must not disturb activation.
|
|
1084
|
+
*/
|
|
1085
|
+
async #claimWorkspace(active) {
|
|
1086
|
+
const registry = this.#ctx.get('workspaceRegistry');
|
|
1087
|
+
if (registry === undefined)
|
|
1088
|
+
return;
|
|
1089
|
+
try {
|
|
1090
|
+
const { session } = active.handle.agent;
|
|
1091
|
+
const cwd = session.header.cwd;
|
|
1092
|
+
if (cwd === undefined)
|
|
1093
|
+
return;
|
|
1094
|
+
const workspace = await registry.resolveByPath(cwd) ?? await registry.create(cwd);
|
|
1095
|
+
await workspace.attachSession(session.id);
|
|
1096
|
+
}
|
|
1097
|
+
catch {
|
|
1098
|
+
// The workspace claim is cosmetic interop; the session itself is fine.
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1012
1101
|
async #resolveModelInfo(selection) {
|
|
1013
1102
|
try {
|
|
1014
1103
|
return await this.#ctx.get('llm')?.resolveModelInfo(selection.provider, selection.model);
|
|
@@ -1026,6 +1115,7 @@ export class SessionRuntime {
|
|
|
1026
1115
|
const previous = this.#active;
|
|
1027
1116
|
const epoch = ++this.#activationEpoch;
|
|
1028
1117
|
this.#active = next;
|
|
1118
|
+
void this.#claimWorkspace(next);
|
|
1029
1119
|
try {
|
|
1030
1120
|
this.#presentAgent(next);
|
|
1031
1121
|
const selected = this.selection(next.handle.agent);
|
|
@@ -1093,7 +1183,7 @@ export class SessionRuntime {
|
|
|
1093
1183
|
this.#inspectedId = undefined;
|
|
1094
1184
|
this.#tui.setInspectedSubagent(undefined);
|
|
1095
1185
|
this.#tui.setStatus(agent.status);
|
|
1096
|
-
this.#
|
|
1186
|
+
this.#tracker.sync();
|
|
1097
1187
|
// Seed welcome metadata before replaceSession commits the startup header to
|
|
1098
1188
|
// native scrollback; later updates cannot rewrite that frozen first frame.
|
|
1099
1189
|
this.#pushSessionInfo();
|
|
@@ -1163,45 +1253,6 @@ export class SessionRuntime {
|
|
|
1163
1253
|
return list.find(skill => skill.name === name && isUserInvocable(skill));
|
|
1164
1254
|
}
|
|
1165
1255
|
#lookupSession = (id) => this.#ctx.get('sessions')?.get(SessionId(id));
|
|
1166
|
-
#subagentDepth(session) {
|
|
1167
|
-
const rootId = this.#active?.handle.agent.id;
|
|
1168
|
-
if (rootId === undefined)
|
|
1169
|
-
return undefined;
|
|
1170
|
-
return descendantDepth(session, rootId, this.#lookupSession);
|
|
1171
|
-
}
|
|
1172
|
-
#noteSubagentSession(session) {
|
|
1173
|
-
const depth = this.#subagentDepth(session);
|
|
1174
|
-
if (depth === undefined)
|
|
1175
|
-
return;
|
|
1176
|
-
const parentId = session.header.parentSession;
|
|
1177
|
-
this.#subagents.remember({
|
|
1178
|
-
id: session.id,
|
|
1179
|
-
...(parentId === undefined ? {} : { parentId }),
|
|
1180
|
-
depth,
|
|
1181
|
-
phase: this.#ctx.get('agents')?.get(session.id)?.status === 'running' ? 'running' : 'starting',
|
|
1182
|
-
});
|
|
1183
|
-
this.#pushSubagents();
|
|
1184
|
-
}
|
|
1185
|
-
#noteSubagentEvent(session, event) {
|
|
1186
|
-
const depth = this.#subagentDepth(session);
|
|
1187
|
-
if (depth === undefined)
|
|
1188
|
-
return;
|
|
1189
|
-
this.#subagents.apply(session, depth, event, this.#ctx.get('agents')?.get(session.id)?.status);
|
|
1190
|
-
this.#pushSubagents();
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Add the direct children a Session's durable catalog declares.
|
|
1194
|
-
*
|
|
1195
|
-
* The live roster is built from `session/created` and child events, so a
|
|
1196
|
-
* resumed parent would otherwise show nothing: its children are neither
|
|
1197
|
-
* loaded nor replayed. Catalog facts are durable, so a restored roster still
|
|
1198
|
-
* lists those children, and `#inspectSubagent` opens each transcript from
|
|
1199
|
-
* disk. Children the live path already knows keep their own state.
|
|
1200
|
-
*/
|
|
1201
|
-
#observeCatalogChildren(session, events) {
|
|
1202
|
-
if (this.#subagents.observeCatalog(session.id, events))
|
|
1203
|
-
this.#pushSubagents();
|
|
1204
|
-
}
|
|
1205
1256
|
/** Route live output only to the visible session; background frames remain buffered for inspection. */
|
|
1206
1257
|
#forwardLiveDelta(agent, delta) {
|
|
1207
1258
|
const active = this.#active;
|
|
@@ -1229,57 +1280,8 @@ export class SessionRuntime {
|
|
|
1229
1280
|
this.#tui.streamDelta(delta);
|
|
1230
1281
|
}
|
|
1231
1282
|
}
|
|
1232
|
-
#noteSubagentStatus(session, status) {
|
|
1233
|
-
if (this.#subagents.owns(session.id)) {
|
|
1234
|
-
this.#subagents.setAgentStatus(session.id, status);
|
|
1235
|
-
this.#pushSubagents();
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
const depth = this.#subagentDepth(session);
|
|
1239
|
-
if (depth === undefined)
|
|
1240
|
-
return;
|
|
1241
|
-
this.#subagents.hydrate(session, depth, this.#ctx.get('agents')?.get(session.id)?.status ?? status);
|
|
1242
|
-
this.#pushSubagents();
|
|
1243
|
-
}
|
|
1244
|
-
#syncSubagents() {
|
|
1245
|
-
const root = this.#active?.handle.agent;
|
|
1246
|
-
const epoch = this.#subagentEpoch + 1;
|
|
1247
|
-
this.#subagentEpoch = epoch;
|
|
1248
|
-
if (root === undefined) {
|
|
1249
|
-
this.#subagents.reset();
|
|
1250
|
-
this.#tui.setSubagents(undefined);
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
this.#subagents.reset(root.id);
|
|
1254
|
-
for (const session of this.#ctx.get('sessions')?.list() ?? []) {
|
|
1255
|
-
const depth = descendantDepth(session, root.id, this.#lookupSession);
|
|
1256
|
-
if (depth === undefined)
|
|
1257
|
-
continue;
|
|
1258
|
-
this.#subagents.hydrate(session, depth, this.#ctx.get('agents')?.get(session.id)?.status);
|
|
1259
|
-
}
|
|
1260
|
-
this.#pushSubagents();
|
|
1261
|
-
const listed = this.#ctx.get('subagents')?.listChildren(root.id);
|
|
1262
|
-
if (listed === undefined)
|
|
1263
|
-
return;
|
|
1264
|
-
void listed.then((entries) => {
|
|
1265
|
-
if (epoch !== this.#subagentEpoch || this.#active?.handle.agent !== root)
|
|
1266
|
-
return;
|
|
1267
|
-
for (const entry of entries) {
|
|
1268
|
-
if (entry.kind !== 'child')
|
|
1269
|
-
continue;
|
|
1270
|
-
this.#subagents.remember({
|
|
1271
|
-
id: entry.id,
|
|
1272
|
-
depth: 1,
|
|
1273
|
-
mode: entry.mode,
|
|
1274
|
-
...(entry.label === undefined ? {} : { label: entry.label }),
|
|
1275
|
-
phase: entry.activity === 'running' ? 'running' : 'waiting',
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
this.#pushSubagents();
|
|
1279
|
-
}, () => undefined);
|
|
1280
|
-
}
|
|
1281
1283
|
#inspectView(id, fallbackPhase = 'waiting') {
|
|
1282
|
-
const view = this.#
|
|
1284
|
+
const view = this.#tracker.snapshot()?.agents.find(agent => agent.id === id);
|
|
1283
1285
|
const mode = view?.mode;
|
|
1284
1286
|
return {
|
|
1285
1287
|
id,
|
|
@@ -1289,51 +1291,27 @@ export class SessionRuntime {
|
|
|
1289
1291
|
writable: isSteerableSubagent(mode),
|
|
1290
1292
|
};
|
|
1291
1293
|
}
|
|
1292
|
-
#pushSubagents() {
|
|
1293
|
-
const roster = this.#subagents.snapshot();
|
|
1294
|
-
if (roster === this.#publishedSubagents)
|
|
1295
|
-
return;
|
|
1296
|
-
this.#publishedSubagents = roster;
|
|
1297
|
-
this.#tui.setSubagents(roster);
|
|
1298
|
-
if (this.#inspectedId === undefined)
|
|
1299
|
-
return;
|
|
1300
|
-
this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
|
|
1301
|
-
}
|
|
1302
1294
|
#pushSessionInfo() {
|
|
1303
1295
|
const active = this.#active;
|
|
1304
1296
|
if (active === undefined)
|
|
1305
1297
|
return;
|
|
1306
1298
|
const agent = active.handle.agent;
|
|
1307
1299
|
const projection = this.#projection(active);
|
|
1308
|
-
|
|
1300
|
+
// One snapshot feeds both folds; each append invalidates the session's
|
|
1301
|
+
// cached snapshot, so a second call would pay another O(log) copy.
|
|
1302
|
+
const events = agent.session.snapshotEvents();
|
|
1303
|
+
const title = explicitSessionTitle(events);
|
|
1309
1304
|
this.#tui.setSession({
|
|
1310
1305
|
id: agent.id,
|
|
1311
1306
|
...(title === undefined ? {} : { title }),
|
|
1312
1307
|
recent: this.#recent.filter(row => row.id !== agent.id),
|
|
1313
|
-
stats:
|
|
1308
|
+
stats: sessionStats(events, active.contextWindow, projection),
|
|
1314
1309
|
controls: this.#sessionControls(active, projection),
|
|
1315
1310
|
});
|
|
1316
1311
|
}
|
|
1317
|
-
#replaceToolPresentation(active, mode) {
|
|
1318
|
-
const previousMode = toolPresentationForPreset(active.agentPreset);
|
|
1319
|
-
active.disposeToolPresentation();
|
|
1320
|
-
try {
|
|
1321
|
-
const tools = active.handle.agent.ctx.get('tools');
|
|
1322
|
-
if (tools === undefined)
|
|
1323
|
-
throw new Error('tool registry is unavailable');
|
|
1324
|
-
active.disposeToolPresentation = tools.presentAs(mode);
|
|
1325
|
-
}
|
|
1326
|
-
catch (error) {
|
|
1327
|
-
const tools = active.handle.agent.ctx.get('tools');
|
|
1328
|
-
if (tools === undefined)
|
|
1329
|
-
throw error;
|
|
1330
|
-
active.disposeToolPresentation = tools.presentAs(previousMode);
|
|
1331
|
-
throw error;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
1312
|
#replaceTranscript(agent) {
|
|
1335
1313
|
const events = agent.session.snapshotEvents();
|
|
1336
|
-
this.#
|
|
1314
|
+
this.#tracker.observeCatalog(agent.session, events);
|
|
1337
1315
|
this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status);
|
|
1338
1316
|
this.#replayLivePrefix(agent.session.id);
|
|
1339
1317
|
}
|
|
@@ -1361,7 +1339,7 @@ export class SessionRuntime {
|
|
|
1361
1339
|
async #inspectSubagent(id) {
|
|
1362
1340
|
const request = this.#inspectEpoch + 1;
|
|
1363
1341
|
this.#inspectEpoch = request;
|
|
1364
|
-
if (!this.#
|
|
1342
|
+
if (!this.#tracker.owns(id)) {
|
|
1365
1343
|
this.#tui.notice('That subagent is no longer available.', { level: 'error' });
|
|
1366
1344
|
return;
|
|
1367
1345
|
}
|
|
@@ -1408,7 +1386,7 @@ export class SessionRuntime {
|
|
|
1408
1386
|
return;
|
|
1409
1387
|
}
|
|
1410
1388
|
const live = this.#ctx.get('sessions')?.get(SessionId(childId));
|
|
1411
|
-
const rosterParent = this.#
|
|
1389
|
+
const rosterParent = this.#tracker.snapshot()?.agents.find(agent => agent.id === childId)?.parentId;
|
|
1412
1390
|
const parentId = live?.header.parentSession ?? rosterParent;
|
|
1413
1391
|
const parent = parentId === undefined || parentId === root.id
|
|
1414
1392
|
? root
|