@agi-fans/dsh-tui 0.18.0 → 0.20.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 +115 -1
- package/lib/chrome/box.d.ts +2 -0
- package/lib/chrome/box.js +6 -12
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.d.ts +26 -18
- package/lib/chrome/code-highlight.js +114 -39
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/diff-render.d.ts +1 -2
- package/lib/chrome/diff-render.js +41 -90
- package/lib/chrome/diff-render.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +52 -30
- package/lib/chrome/main-screen-renderer.js +1730 -79
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +144 -101
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/math.d.ts +4 -0
- package/lib/chrome/math.js +340 -0
- package/lib/chrome/math.js.map +1 -0
- package/lib/chrome/md-style.d.ts +3 -0
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/mermaid.d.ts +6 -0
- package/lib/chrome/mermaid.js +56 -0
- package/lib/chrome/mermaid.js.map +1 -0
- package/lib/chrome/renderer.d.ts +78 -0
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-config.d.ts +5 -0
- package/lib/chrome/status-config.js +3 -0
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/status-line.d.ts +0 -21
- package/lib/chrome/status-line.js +8 -65
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/terminal-images.d.ts +31 -0
- package/lib/chrome/terminal-images.js +34 -0
- package/lib/chrome/terminal-images.js.map +1 -0
- package/lib/chrome/theme.d.ts +23 -6
- package/lib/chrome/theme.js +217 -109
- 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 +2 -2
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/chrome/width.d.ts +7 -3
- package/lib/chrome/width.js +78 -14
- package/lib/chrome/width.js.map +1 -1
- package/lib/commands/diff.js +49 -3
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.js +28 -5
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +8 -0
- package/lib/commands/files.js +137 -0
- package/lib/commands/files.js.map +1 -0
- package/lib/commands/plugins.d.ts +8 -0
- package/lib/commands/plugins.js +158 -0
- package/lib/commands/plugins.js.map +1 -0
- package/lib/commands/session-configuration.d.ts +4 -0
- package/lib/commands/session-configuration.js +39 -8
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +47 -15
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/steer.js +2 -2
- package/lib/commands/steer.js.map +1 -1
- package/lib/commands/terminal.d.ts +7 -0
- package/lib/commands/terminal.js +99 -0
- package/lib/commands/terminal.js.map +1 -0
- package/lib/definition.d.ts +180 -14
- package/lib/definition.js.map +1 -1
- package/lib/input/clipboard.d.ts +12 -7
- package/lib/input/clipboard.js +79 -20
- package/lib/input/clipboard.js.map +1 -1
- package/lib/input/composer-files.d.ts +26 -0
- package/lib/input/composer-files.js +50 -0
- package/lib/input/composer-files.js.map +1 -0
- package/lib/input/editor-discovery.d.ts +21 -0
- package/lib/input/editor-discovery.js +99 -0
- package/lib/input/editor-discovery.js.map +1 -0
- package/lib/input/editor.d.ts +26 -1
- package/lib/input/editor.js +126 -12
- package/lib/input/editor.js.map +1 -1
- package/lib/input/external-editor.d.ts +5 -2
- package/lib/input/external-editor.js +14 -12
- package/lib/input/external-editor.js.map +1 -1
- package/lib/input/fuzzy-search.d.ts +9 -0
- package/lib/input/fuzzy-search.js +113 -0
- package/lib/input/fuzzy-search.js.map +1 -0
- package/lib/input/keybindings-config.d.ts +1 -1
- package/lib/input/keybindings-config.js +7 -1
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +17 -0
- package/lib/input/keys.js +21 -5
- package/lib/input/keys.js.map +1 -1
- package/lib/input/mouse-policy.d.ts +3 -0
- package/lib/input/mouse-policy.js +14 -0
- package/lib/input/mouse-policy.js.map +1 -0
- package/lib/input/paste-burst.d.ts +16 -0
- package/lib/input/paste-burst.js +61 -0
- package/lib/input/paste-burst.js.map +1 -0
- package/lib/input/text-selection.d.ts +20 -0
- package/lib/input/text-selection.js +195 -0
- package/lib/input/text-selection.js.map +1 -0
- package/lib/runtime/composer-images.d.ts +1 -0
- package/lib/runtime/composer-images.js +11 -3
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/file-open.d.ts +2 -0
- package/lib/runtime/file-open.js +12 -0
- package/lib/runtime/file-open.js.map +1 -0
- package/lib/runtime/file-review.d.ts +24 -0
- package/lib/runtime/file-review.js +188 -0
- package/lib/runtime/file-review.js.map +1 -0
- package/lib/runtime/herdr-agent.d.ts +7 -15
- package/lib/runtime/herdr-agent.js +151 -79
- package/lib/runtime/herdr-agent.js.map +1 -1
- package/lib/runtime/hook-settings.d.ts +17 -0
- package/lib/runtime/hook-settings.js +57 -0
- package/lib/runtime/hook-settings.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/image-preview.d.ts +8 -0
- package/lib/runtime/image-preview.js +119 -0
- package/lib/runtime/image-preview.js.map +1 -0
- package/lib/runtime/plain-tui.d.ts +2 -0
- package/lib/runtime/plain-tui.js +2 -2
- package/lib/runtime/plain-tui.js.map +1 -1
- package/lib/runtime/plugin-settings.d.ts +17 -0
- package/lib/runtime/plugin-settings.js +154 -0
- package/lib/runtime/plugin-settings.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +58 -7
- package/lib/runtime/provider-local.js +1394 -137
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +3 -3
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-archive.d.ts +5 -0
- package/lib/runtime/session-archive.js +114 -0
- package/lib/runtime/session-archive.js.map +1 -0
- package/lib/runtime/subagent-model-selection.d.ts +5 -0
- package/lib/runtime/subagent-model-selection.js +15 -0
- package/lib/runtime/subagent-model-selection.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +8 -0
- package/lib/runtime/terminal-notifications.js +16 -0
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-reader.d.ts +11 -0
- package/lib/runtime/terminal-reader.js +87 -0
- package/lib/runtime/terminal-reader.js.map +1 -0
- package/lib/runtime/tool-presentation.js +43 -30
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/composer-drafts.d.ts +20 -0
- package/lib/session/composer-drafts.js +103 -0
- package/lib/session/composer-drafts.js.map +1 -0
- package/lib/session/conversation-turns.d.ts +16 -0
- package/lib/session/conversation-turns.js +33 -0
- package/lib/session/conversation-turns.js.map +1 -0
- package/lib/session/feature-toggles.d.ts +79 -0
- package/lib/session/feature-toggles.js +139 -0
- package/lib/session/feature-toggles.js.map +1 -0
- package/lib/session/fold-policy.d.ts +4 -0
- package/lib/session/fold-policy.js +4 -0
- package/lib/session/fold-policy.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +2 -2
- package/lib/session/interaction-adapter.js +156 -25
- package/lib/session/interaction-adapter.js.map +1 -1
- 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/message-queue.d.ts +22 -0
- package/lib/session/message-queue.js +157 -0
- package/lib/session/message-queue.js.map +1 -0
- package/lib/session/project-file-search.js +3 -21
- package/lib/session/project-file-search.js.map +1 -1
- package/lib/session/session-controller.d.ts +11 -18
- package/lib/session/session-controller.js +246 -100
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +11 -3
- package/lib/session/session-library.js +48 -8
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/session-tree.d.ts +26 -0
- package/lib/session/session-tree.js +112 -0
- package/lib/session/session-tree.js.map +1 -0
- package/lib/session/steering.d.ts +3 -0
- package/lib/session/steering.js +9 -0
- package/lib/session/steering.js.map +1 -0
- package/lib/session/tui-settings.d.ts +23 -1
- package/lib/session/tui-settings.js +15 -2
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +9 -0
- package/lib/session/workflow-projection.js +44 -0
- package/lib/session/workflow-projection.js.map +1 -0
- package/lib/views/agent-hub.js +9 -4
- package/lib/views/agent-hub.js.map +1 -1
- package/lib/views/autocomplete.js +5 -25
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/document-reader.d.ts +34 -0
- package/lib/views/document-reader.js +145 -0
- package/lib/views/document-reader.js.map +1 -0
- package/lib/views/event-views.d.ts +26 -2
- package/lib/views/event-views.js +196 -42
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/hotkeys.d.ts +2 -0
- package/lib/views/hotkeys.js +20 -8
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +33 -3
- package/lib/views/prompt-selector.js +287 -43
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/prompt-tree.d.ts +11 -0
- package/lib/views/prompt-tree.js +74 -0
- package/lib/views/prompt-tree.js.map +1 -0
- package/lib/views/session-files.d.ts +7 -0
- package/lib/views/session-files.js +62 -0
- package/lib/views/session-files.js.map +1 -0
- package/lib/views/settings-list.d.ts +32 -4
- package/lib/views/settings-list.js +148 -37
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-render.d.ts +117 -18
- package/lib/views/transcript-render.js +1084 -126
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +80 -2
- 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 +77 -41
- package/lib/chrome/style.d.ts +0 -22
- package/lib/chrome/style.js +0 -23
- package/lib/chrome/style.js.map +0 -1
|
@@ -19,6 +19,8 @@ import type { SessionStatsProjection } from '@deepseek-ai/dsh-session-stats/type
|
|
|
19
19
|
import type { ContextBreakdownProjection, ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client';
|
|
20
20
|
import { type SessionEvent } from '@deepseek-ai/dsh-session';
|
|
21
21
|
import type { GoalProjection } from '@deepseek-ai/dsh-goal/types';
|
|
22
|
+
import type { UserQuestionProjectionView } from '@deepseek-ai/dsh-user-questions';
|
|
23
|
+
export { conversationTurns, type ConversationTurn } from './conversation-turns.ts';
|
|
22
24
|
import type { TuiCommand, TuiRecentSession, TuiService, TuiSessionControls, TuiSessionStats, TuiSubmission, TuiInputImage } from '../definition.ts';
|
|
23
25
|
import type { ContextDiagnostics } from './context-diagnostics.ts';
|
|
24
26
|
/** Projection values consumed as one consistent snapshot when the units exist. */
|
|
@@ -30,6 +32,7 @@ export interface TuiStatsProjection {
|
|
|
30
32
|
plan?: PlanProjection;
|
|
31
33
|
permissions?: PermissionSelection;
|
|
32
34
|
goal?: GoalProjection | null;
|
|
35
|
+
userQuestions?: UserQuestionProjectionView;
|
|
33
36
|
/** Last logged sandbox-mode override, or null before one; outranks the preset while set. */
|
|
34
37
|
sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access' | null;
|
|
35
38
|
}
|
|
@@ -53,21 +56,6 @@ export declare function sessionStats(events: readonly SessionEvent[], contextWin
|
|
|
53
56
|
* fallback fold when projections are absent.
|
|
54
57
|
*/
|
|
55
58
|
export declare function shouldRefreshSessionInfoAfter(event: SessionEvent): boolean;
|
|
56
|
-
/** One direct human turn that can become a safe fork boundary. */
|
|
57
|
-
export interface ConversationTurn {
|
|
58
|
-
/** Harness turn number shown to the user. */
|
|
59
|
-
turn: number;
|
|
60
|
-
/** Index of the direct user/message event in the immutable log. */
|
|
61
|
-
messageIndex: number;
|
|
62
|
-
/** Balanced seed boundary immediately before this turn starts. */
|
|
63
|
-
branchIndex: number;
|
|
64
|
-
/** Single-line selector preview. */
|
|
65
|
-
preview: string;
|
|
66
|
-
/** Number of image blocks in the selected message. */
|
|
67
|
-
imageCount: number;
|
|
68
|
-
}
|
|
69
|
-
/** Find direct human messages whose preceding log prefix is safe to seed into a fork. */
|
|
70
|
-
export declare function conversationTurns(events: readonly SessionEvent[]): ConversationTurn[];
|
|
71
59
|
/** Title and latest-human-message preview for durable session discovery. */
|
|
72
60
|
export declare function recentSessionContent(events: readonly SessionEvent[]): {
|
|
73
61
|
title: string;
|
|
@@ -86,6 +74,9 @@ export declare function createSubmissionMessage(submission: TuiSubmission, attac
|
|
|
86
74
|
content: ({
|
|
87
75
|
type: "image";
|
|
88
76
|
attachment: ImageAttachmentRef;
|
|
77
|
+
} | {
|
|
78
|
+
type: "file";
|
|
79
|
+
attachment: import("@deepseek-ai/dsh-attachment").FileAttachmentRef;
|
|
89
80
|
} | {
|
|
90
81
|
type: "text";
|
|
91
82
|
text: string;
|
|
@@ -113,12 +104,15 @@ export declare class SessionRuntime {
|
|
|
113
104
|
start(): Promise<void>;
|
|
114
105
|
/** Submit one human composer value; active turns retain it as a later follow-up. */
|
|
115
106
|
send(input: string | TuiSubmission, agent?: Agent): Promise<void>;
|
|
107
|
+
openQueue(signal: AbortSignal, agent?: Agent): Promise<void>;
|
|
116
108
|
/** Remove and rehydrate the newest durable human follow-up for queue browsing. */
|
|
117
109
|
editLatestFollowup(agent?: Agent): Promise<TuiSubmission | undefined>;
|
|
118
110
|
/** Execute a plugin-owned slash command, falling back to user-invocable skills. */
|
|
119
111
|
execute(line: string, signal: AbortSignal, images?: readonly TuiInputImage[]): Promise<boolean>;
|
|
120
112
|
/** Fail when a command invocation targets a stale or background Agent. */
|
|
121
113
|
assertActive(agent: Agent): void;
|
|
114
|
+
/** Shared local metadata document for Session Library and Tree actions. */
|
|
115
|
+
get sessionLibraryPath(): string;
|
|
122
116
|
/** Immutable recent-session view used by the resume command. */
|
|
123
117
|
get recentSessions(): readonly TuiRecentSession[];
|
|
124
118
|
/** Current model selection for the active Agent. */
|
|
@@ -133,8 +127,8 @@ export declare class SessionRuntime {
|
|
|
133
127
|
resumeSession(agent: Agent, id: string, signal: AbortSignal): Promise<void>;
|
|
134
128
|
/** Rename a live or durable top-level session through the public log-backed title service. */
|
|
135
129
|
renameSession(agent: Agent, id: string, title: string, signal: AbortSignal): Promise<void>;
|
|
136
|
-
/**
|
|
137
|
-
|
|
130
|
+
/** Browse conversation forks and preview turns before editing or continuing a branch. */
|
|
131
|
+
openSessionTree(signal: AbortSignal): Promise<void>;
|
|
138
132
|
/** Whole-session figures for the active Agent. */
|
|
139
133
|
stats(agent?: Agent): TuiSessionStats;
|
|
140
134
|
/** Current client-visible context projections, read as one consistent snapshot. */
|
|
@@ -152,4 +146,3 @@ export declare class SessionRuntime {
|
|
|
152
146
|
refreshRecent(): Promise<void>;
|
|
153
147
|
dispose(): Promise<void>;
|
|
154
148
|
}
|
|
155
|
-
export {};
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* @module @agi-fans/dsh-tui/session-controller
|
|
8
8
|
*/
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { steerAgent } from "./steering.js";
|
|
11
|
+
import { openMessageQueue } from "./message-queue.js";
|
|
12
|
+
import { WorkflowProjection } from "./workflow-projection.js";
|
|
10
13
|
import { join } from 'node:path';
|
|
11
14
|
import { installModelSelection, } from '@deepseek-ai/dsh-agent';
|
|
12
15
|
import { createUserMessage, } from '@deepseek-ai/dsh-llm';
|
|
@@ -18,9 +21,12 @@ import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
|
18
21
|
import { firstVisibleStreamTime } from "../views/stream-time.js";
|
|
19
22
|
import { LiveAttemptTracker } from "./live-attempt-tracker.js";
|
|
20
23
|
import { blocksText } from "./content-text.js";
|
|
24
|
+
import { conversationTurns } from "./conversation-turns.js";
|
|
25
|
+
import { buildSessionTree, markedSessionTree, sessionTreeFamily } from "./session-tree.js";
|
|
26
|
+
export { conversationTurns } from "./conversation-turns.js";
|
|
21
27
|
import { jobNoticeFor } from "./job-notice.js";
|
|
22
28
|
import { descendantDepth, isSteerableSubagent } from "./subagent-roster.js";
|
|
23
|
-
import { readRecentRows, writeRecentRows } from "./session-library.js";
|
|
29
|
+
import { readRecentRows, writeRecentRows, readSessionLibrary, sessionLibraryPath, setSessionLabel } from "./session-library.js";
|
|
24
30
|
import { readJsonFile, writeJsonAtomic } from "./json-file.js";
|
|
25
31
|
import { SubagentTracker } from "./subagent-tracker.js";
|
|
26
32
|
import { commandPermission } from "../commands/permission.js";
|
|
@@ -55,6 +61,9 @@ export function sessionControls(projection) {
|
|
|
55
61
|
? (projection?.permissions === undefined ? {} : { permission: projection.permissions.currentValue })
|
|
56
62
|
: { permission: override }),
|
|
57
63
|
...goalControl(projection?.goal),
|
|
64
|
+
...(projection?.userQuestions === undefined ? {} : {
|
|
65
|
+
pendingQuestions: projection.userQuestions.active.filter(question => question.state === 'continued').length,
|
|
66
|
+
}),
|
|
58
67
|
};
|
|
59
68
|
}
|
|
60
69
|
/** A completed goal renders nothing, mirroring the projection's own visibility rule. */
|
|
@@ -252,37 +261,6 @@ function humanMessageText(event) {
|
|
|
252
261
|
const text = blocksText(event.data.content, { collapse: true });
|
|
253
262
|
return text === '' ? undefined : text;
|
|
254
263
|
}
|
|
255
|
-
/** Find direct human messages whose preceding log prefix is safe to seed into a fork. */
|
|
256
|
-
export function conversationTurns(events) {
|
|
257
|
-
const turns = [];
|
|
258
|
-
let open;
|
|
259
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
260
|
-
const event = events[index];
|
|
261
|
-
if (event.type === 'turn/start') {
|
|
262
|
-
open = { turn: event.data.turn, branchIndex: index };
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (event.type === 'turn/end') {
|
|
266
|
-
if (open?.turn === event.data.turn)
|
|
267
|
-
open = undefined;
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
if (open === undefined || event.type !== 'user/message' || event.data.source.kind !== 'user')
|
|
271
|
-
continue;
|
|
272
|
-
const text = blocksText(event.data.content, { collapse: true });
|
|
273
|
-
const imageCount = event.data.content.filter(block => block.type === 'image').length;
|
|
274
|
-
if (text === '' && imageCount === 0)
|
|
275
|
-
continue;
|
|
276
|
-
turns.push({
|
|
277
|
-
turn: open.turn,
|
|
278
|
-
messageIndex: index,
|
|
279
|
-
branchIndex: open.branchIndex,
|
|
280
|
-
preview: text === '' ? (imageCount === 1 ? 'Image' : `${imageCount} images`) : text,
|
|
281
|
-
imageCount,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
return turns;
|
|
285
|
-
}
|
|
286
264
|
/** Title and latest-human-message preview for durable session discovery. */
|
|
287
265
|
export function recentSessionContent(events) {
|
|
288
266
|
const generatedTitle = explicitSessionTitle(events);
|
|
@@ -342,6 +320,7 @@ export async function createSubmissionMessage(submission, attachments) {
|
|
|
342
320
|
const content = [
|
|
343
321
|
...(submission.text === '' ? [] : [{ type: 'text', text: submission.text }]),
|
|
344
322
|
...refs.map(attachment => ({ type: 'image', attachment })),
|
|
323
|
+
...(submission.files ?? []).map(attachment => ({ type: 'file', attachment })),
|
|
345
324
|
];
|
|
346
325
|
if (content.length === 0)
|
|
347
326
|
throw new Error('Cannot submit an empty message.');
|
|
@@ -377,24 +356,29 @@ export async function restoreSubmissionMessage(message, attachments) {
|
|
|
377
356
|
height: stored.ref.height,
|
|
378
357
|
};
|
|
379
358
|
}));
|
|
380
|
-
|
|
359
|
+
const files = message.content.filter((block) => block.type === 'file').map(block => block.attachment);
|
|
360
|
+
return { text, images, ...(files.length === 0 ? {} : { files }) };
|
|
381
361
|
}
|
|
382
362
|
/** Own one switchable top-level Agent and project it onto a TuiService. */
|
|
383
363
|
export class SessionRuntime {
|
|
364
|
+
#workflows = new WorkflowProjection();
|
|
384
365
|
#ctx;
|
|
385
366
|
#tui;
|
|
386
367
|
#active;
|
|
387
368
|
#recent = [];
|
|
369
|
+
#archivedSessions = new Set();
|
|
388
370
|
#skillCommands = [];
|
|
389
371
|
#started = false;
|
|
390
372
|
#retired = [];
|
|
391
373
|
// Weak: deduplication must not retain disposed session graphs.
|
|
392
374
|
#releasedHandles = new WeakSet();
|
|
393
375
|
#disposed = false;
|
|
376
|
+
#queueAbort;
|
|
394
377
|
#off = [];
|
|
395
378
|
#tracker;
|
|
396
379
|
#streamAttempts = new LiveAttemptTracker();
|
|
397
380
|
#recentCache = new Map();
|
|
381
|
+
#libraryPath;
|
|
398
382
|
#recentRowsPath;
|
|
399
383
|
#upgradeMarkerPath;
|
|
400
384
|
#recentRowsLoaded = false;
|
|
@@ -406,6 +390,7 @@ export class SessionRuntime {
|
|
|
406
390
|
constructor(ctx, tui, options = {}) {
|
|
407
391
|
this.#ctx = ctx;
|
|
408
392
|
this.#tui = tui;
|
|
393
|
+
this.#libraryPath = sessionLibraryPath(options.stateDir);
|
|
409
394
|
this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
|
|
410
395
|
this.#upgradeMarkerPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'sessions-upgraded.json');
|
|
411
396
|
this.#tracker = new SubagentTracker({
|
|
@@ -418,12 +403,14 @@ export class SessionRuntime {
|
|
|
418
403
|
sessions: () => this.#ctx.get('sessions')?.list() ?? [],
|
|
419
404
|
listChildren: rootId => this.#ctx.get('subagents')?.listChildren(SessionId(rootId)),
|
|
420
405
|
publish: (roster) => {
|
|
421
|
-
this.#tui.setSubagents(roster);
|
|
406
|
+
this.#tui.setSubagents(this.#workflows.roster(roster));
|
|
422
407
|
if (this.#inspectedId === undefined)
|
|
423
408
|
return;
|
|
424
409
|
this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
|
|
425
410
|
},
|
|
426
411
|
});
|
|
412
|
+
this.#off.push(tui.setSteerHandler(text => { steerAgent(this.agent, text); }));
|
|
413
|
+
this.#off.push(tui.setQueueHandler(signal => this.openQueue(signal)));
|
|
427
414
|
this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
|
|
428
415
|
this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
|
|
429
416
|
this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
|
|
@@ -460,14 +447,21 @@ export class SessionRuntime {
|
|
|
460
447
|
return;
|
|
461
448
|
const owner = event.job.owner === undefined ? undefined : this.#ctx.get('agents')?.get(event.job.owner);
|
|
462
449
|
const notice = jobNoticeFor(event.job, owner, this.#active?.handle.agent);
|
|
463
|
-
if (notice !== undefined)
|
|
464
|
-
|
|
450
|
+
if (notice !== undefined) {
|
|
451
|
+
const job = event.job;
|
|
452
|
+
tui.notice(notice, {
|
|
453
|
+
process: true, processSource: `job:${job.id}`,
|
|
454
|
+
...(/^(bash|pwsh)$/u.test(job.kind) && (job.status === 'completed' || job.status === 'failed' || job.status === 'killed')
|
|
455
|
+
? { job: { id: job.id, label: job.label, status: job.status, startedAt: job.startedAt, ...(job.detail === undefined ? {} : { detail: job.detail }) } }
|
|
456
|
+
: {}),
|
|
457
|
+
...(job.status === 'failed' || job.status === 'killed' ? { level: 'error' } : {}),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
465
460
|
}));
|
|
466
461
|
}
|
|
467
462
|
this.#off.push(ctx.on('agent/status', (payload) => {
|
|
468
463
|
if (payload.agent === this.#active?.handle.agent) {
|
|
469
|
-
|
|
470
|
-
tui.setStatus(payload.status);
|
|
464
|
+
tui.setStatus(payload.status, { root: true });
|
|
471
465
|
return;
|
|
472
466
|
}
|
|
473
467
|
if (payload.agent.id === this.#inspectedId)
|
|
@@ -491,9 +485,13 @@ export class SessionRuntime {
|
|
|
491
485
|
return;
|
|
492
486
|
}
|
|
493
487
|
if (session === active.handle.agent.session) {
|
|
488
|
+
if (this.#workflows.apply(event))
|
|
489
|
+
tui.setSubagents(this.#workflows.roster(this.#tracker.snapshot()));
|
|
494
490
|
if (this.#inspectedId === undefined) {
|
|
495
491
|
tui.event(event, ctx.get('tuiToolPresentation')?.event(active.handle.agent, event));
|
|
496
492
|
}
|
|
493
|
+
if (event.type === 'workspace/changes')
|
|
494
|
+
this.#serveWorkspaceSummary(ctx, tui, session, event);
|
|
497
495
|
if (event.type === 'subagent/catalog')
|
|
498
496
|
this.#tracker.observeCatalog(session, [event]);
|
|
499
497
|
if (shouldRefreshSessionInfoAfter(event))
|
|
@@ -543,7 +541,7 @@ export class SessionRuntime {
|
|
|
543
541
|
if (session !== this.#active?.handle.agent.session)
|
|
544
542
|
return;
|
|
545
543
|
if (key === 'sessionStats' || key === 'tokenUsage' || key === 'contextPressure' || key === 'contextBreakdown'
|
|
546
|
-
|| key === 'plan' || key === 'permissions' || key === 'goal')
|
|
544
|
+
|| key === 'plan' || key === 'permissions' || key === 'goal' || key === 'userQuestions')
|
|
547
545
|
this.#pushSessionInfo();
|
|
548
546
|
}));
|
|
549
547
|
}
|
|
@@ -579,7 +577,7 @@ export class SessionRuntime {
|
|
|
579
577
|
if (defaults === undefined)
|
|
580
578
|
throw new Error('agent default model is unavailable');
|
|
581
579
|
await this.refreshRecent();
|
|
582
|
-
await this.#activate(await this.#create(defaults));
|
|
580
|
+
await this.#activate(await this.#create(defaults), 'initial');
|
|
583
581
|
}
|
|
584
582
|
/** Submit one human composer value; active turns retain it as a later follow-up. */
|
|
585
583
|
async send(input, agent = this.#requiredAgent()) {
|
|
@@ -589,24 +587,37 @@ export class SessionRuntime {
|
|
|
589
587
|
this.assertActive(agent);
|
|
590
588
|
agent.followup(message);
|
|
591
589
|
}
|
|
590
|
+
async openQueue(signal, agent = this.#requiredAgent()) {
|
|
591
|
+
this.assertActive(agent);
|
|
592
|
+
if (this.#inspectedId !== undefined)
|
|
593
|
+
throw new Error('Return to the parent session to manage queued messages.');
|
|
594
|
+
if (this.#queueAbort !== undefined)
|
|
595
|
+
throw new Error('Message Queue is already open.');
|
|
596
|
+
const abort = new AbortController();
|
|
597
|
+
this.#queueAbort = abort;
|
|
598
|
+
const onAbort = () => { abort.abort(signal.reason); };
|
|
599
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
600
|
+
if (signal.aborted)
|
|
601
|
+
onAbort();
|
|
602
|
+
try {
|
|
603
|
+
await openMessageQueue({ agent, tui: this.#tui, signal: abort.signal, assertActive: () => { this.assertActive(agent); },
|
|
604
|
+
restore: message => restoreSubmissionMessage(message, this.#ctx.get('attachments')) });
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
signal.removeEventListener('abort', onAbort);
|
|
608
|
+
if (this.#queueAbort === abort)
|
|
609
|
+
this.#queueAbort = undefined;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
592
612
|
/** Remove and rehydrate the newest durable human follow-up for queue browsing. */
|
|
593
613
|
async editLatestFollowup(agent = this.#requiredAgent()) {
|
|
594
614
|
this.assertActive(agent);
|
|
595
615
|
const message = agent.inbox.nextTurn.findLast(candidate => candidate.source.kind === 'user');
|
|
596
|
-
if (message === undefined
|
|
616
|
+
if (message === undefined)
|
|
597
617
|
return undefined;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return submission;
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
try {
|
|
605
|
-
agent.inbox.append('next-turn', message);
|
|
606
|
-
}
|
|
607
|
-
catch { /* agent retired or message was restored elsewhere */ }
|
|
608
|
-
throw error;
|
|
609
|
-
}
|
|
618
|
+
const submission = await restoreSubmissionMessage(message, this.#ctx.get('attachments'));
|
|
619
|
+
this.assertActive(agent);
|
|
620
|
+
return agent.inbox.remove(message.id) ? { ...submission, literal: true } : undefined;
|
|
610
621
|
}
|
|
611
622
|
/** Execute a plugin-owned slash command, falling back to user-invocable skills. */
|
|
612
623
|
async execute(line, signal, images = []) {
|
|
@@ -640,9 +651,9 @@ export class SessionRuntime {
|
|
|
640
651
|
this.#tui.restoreInput({ text: line, images });
|
|
641
652
|
if (result.text !== undefined) {
|
|
642
653
|
if (result.kind === 'error')
|
|
643
|
-
this.#tui.notice(result.text, { level: 'error' });
|
|
654
|
+
this.#tui.notice(result.text, { level: 'error', focus: true });
|
|
644
655
|
else
|
|
645
|
-
this.#tui.commandOutput(parsed.name, result.text);
|
|
656
|
+
this.#tui.commandOutput(parsed.name, result.text, { focus: true });
|
|
646
657
|
}
|
|
647
658
|
return true;
|
|
648
659
|
}
|
|
@@ -651,6 +662,8 @@ export class SessionRuntime {
|
|
|
651
662
|
if (agent !== this.#requiredAgent())
|
|
652
663
|
throw new Error('the command does not target the active omdsh session');
|
|
653
664
|
}
|
|
665
|
+
/** Shared local metadata document for Session Library and Tree actions. */
|
|
666
|
+
get sessionLibraryPath() { return this.#libraryPath; }
|
|
654
667
|
/** Immutable recent-session view used by the resume command. */
|
|
655
668
|
get recentSessions() {
|
|
656
669
|
return this.#recent;
|
|
@@ -681,7 +694,7 @@ export class SessionRuntime {
|
|
|
681
694
|
/** Start a new top-level session with the current model selection. */
|
|
682
695
|
async newSession(agent) {
|
|
683
696
|
this.assertActive(agent);
|
|
684
|
-
await this.#activate(await this.#create(this.selection(agent)));
|
|
697
|
+
await this.#activate(await this.#create(this.selection(agent)), 'new');
|
|
685
698
|
await this.refreshRecent();
|
|
686
699
|
}
|
|
687
700
|
/** Replace the active top-level session with one durable session. */
|
|
@@ -734,44 +747,143 @@ export class SessionRuntime {
|
|
|
734
747
|
}
|
|
735
748
|
await this.refreshRecent();
|
|
736
749
|
}
|
|
737
|
-
/**
|
|
738
|
-
async
|
|
750
|
+
/** Browse conversation forks and preview turns before editing or continuing a branch. */
|
|
751
|
+
async openSessionTree(signal) {
|
|
739
752
|
const agent = this.#requiredAgent();
|
|
740
753
|
if (agent.status !== 'idle')
|
|
741
754
|
return;
|
|
742
|
-
const
|
|
743
|
-
const
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
755
|
+
const persistence = this.#ctx.get('sessionPersistence');
|
|
756
|
+
const snapshots = persistence === undefined ? [] : await persistence.list({ signal });
|
|
757
|
+
const candidates = snapshots.filter(item => item.header.origin !== 'subagent'
|
|
758
|
+
&& item.header.cwd === agent.session.header.cwd).map(item => ({
|
|
759
|
+
id: String(item.header.id),
|
|
760
|
+
...(item.header.isSeeded && item.header.parentSession !== undefined ? { parentId: String(item.header.parentSession) } : {}),
|
|
761
|
+
}));
|
|
762
|
+
const current = { id: String(agent.id),
|
|
763
|
+
...(agent.session.header.isSeeded && agent.session.header.parentSession !== undefined
|
|
764
|
+
? { parentId: String(agent.session.header.parentSession) } : {}) };
|
|
765
|
+
const headers = [...candidates.filter(item => item.id !== current.id), current];
|
|
766
|
+
const family = sessionTreeFamily(headers, current.id);
|
|
767
|
+
const branches = [];
|
|
768
|
+
for (const header of headers) {
|
|
769
|
+
if (!family.has(header.id))
|
|
770
|
+
continue;
|
|
771
|
+
signal.throwIfAborted();
|
|
772
|
+
if (header.id === current.id) {
|
|
773
|
+
const events = agent.session.snapshotEvents();
|
|
774
|
+
const title = explicitSessionTitle(events.slice(header.parentId === undefined ? 0 : Number(agent.session.inheritedEventCount)));
|
|
775
|
+
branches.push({ ...header, events, inheritedEventCount: Number(agent.session.inheritedEventCount), ...(title === undefined ? {} : { title }),
|
|
776
|
+
...(agent.session.header.agentPreset === undefined ? {} : { agentPreset: agent.session.header.agentPreset }) });
|
|
777
|
+
}
|
|
778
|
+
else if (persistence !== undefined) {
|
|
779
|
+
const log = await readColdSessionLog(persistence, SessionId(header.id), signal);
|
|
780
|
+
const title = explicitSessionTitle(log.events.slice(header.parentId === undefined ? 0 : Number(log.inheritedEventCount)));
|
|
781
|
+
branches.push({ ...header, events: log.events, inheritedEventCount: Number(log.inheritedEventCount), ...(title === undefined ? {} : { title }),
|
|
782
|
+
...(log.header.agentPreset === undefined ? {} : { agentPreset: log.header.agentPreset }) });
|
|
783
|
+
}
|
|
747
784
|
}
|
|
748
|
-
const newestFirst = [...turns].reverse();
|
|
749
|
-
const answer = await this.#tui.prompt({
|
|
750
|
-
title: 'Rewind Conversation',
|
|
751
|
-
question: '',
|
|
752
|
-
detail: 'original session preserved',
|
|
753
|
-
options: newestFirst.map(turn => ({
|
|
754
|
-
label: `Turn ${turn.turn}`,
|
|
755
|
-
value: String(turn.messageIndex),
|
|
756
|
-
preview: turn.preview,
|
|
757
|
-
description: turn.imageCount === 0
|
|
758
|
-
? 'Branch before this message'
|
|
759
|
-
: `Branch before this message · ${turn.imageCount} ${turn.imageCount === 1 ? 'image' : 'images'}`,
|
|
760
|
-
})),
|
|
761
|
-
initialValue: String(newestFirst[0]?.messageIndex),
|
|
762
|
-
presentation: 'fullscreen-list',
|
|
763
|
-
optionLayout: 'spacious',
|
|
764
|
-
filterable: true,
|
|
765
|
-
allowCustom: false,
|
|
766
|
-
submitLabel: 'rewind',
|
|
767
|
-
signal,
|
|
768
|
-
});
|
|
769
|
-
if (answer === null)
|
|
770
|
-
return;
|
|
771
785
|
this.assertActive(agent);
|
|
772
786
|
if (agent.status !== 'idle')
|
|
773
|
-
|
|
774
|
-
const
|
|
787
|
+
return;
|
|
788
|
+
const nodes = buildSessionTree(branches, current.id);
|
|
789
|
+
if (!nodes.some(node => node.kind === 'turn')) {
|
|
790
|
+
this.#tui.notice('No conversation turns are available in the Session Tree.');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const target = nodes.findLast(node => node.sessionId === current.id && node.kind === 'turn')
|
|
794
|
+
?? nodes.find(node => node.sessionId === current.id);
|
|
795
|
+
let markedOnly = false;
|
|
796
|
+
let selectedId = target?.id;
|
|
797
|
+
let answer;
|
|
798
|
+
while (true) {
|
|
799
|
+
const library = readSessionLibrary(this.#libraryPath);
|
|
800
|
+
const visible = markedOnly ? markedSessionTree(nodes, library.labels) : nodes;
|
|
801
|
+
answer = await this.#tui.prompt({
|
|
802
|
+
title: markedOnly ? 'Session Tree · Marked' : 'Session Tree', question: '', notify: false,
|
|
803
|
+
emptyText: 'No marked nodes. Alt+B shows the full tree.',
|
|
804
|
+
options: visible.map(node => {
|
|
805
|
+
const label = library.labels[node.id];
|
|
806
|
+
return {
|
|
807
|
+
label: label === undefined ? node.label : `◆ ${label} · ${node.label}`,
|
|
808
|
+
value: node.id, ...(node.parentId === undefined ? {} : { parentValue: node.parentId }),
|
|
809
|
+
preview: node.preview, description: node.description,
|
|
810
|
+
submitLabel: node.kind === 'turn' ? 'edit from here' : 'continue branch',
|
|
811
|
+
...(node.current ? { badge: { label: 'Current', tone: 'success' } } : {}),
|
|
812
|
+
};
|
|
813
|
+
}),
|
|
814
|
+
actions: [
|
|
815
|
+
{ key: 'Alt+Enter', label: 'continue branch', valuePrefix: 'continue:' },
|
|
816
|
+
{ key: 'Alt+L', label: 'label', valuePrefix: 'label:' },
|
|
817
|
+
{ key: 'Alt+U', label: 'clear label', valuePrefix: 'unlabel:' },
|
|
818
|
+
{ key: 'Alt+B', label: markedOnly ? 'full tree' : 'marked nodes', valuePrefix: 'marked:', scope: 'list' },
|
|
819
|
+
],
|
|
820
|
+
...(selectedId === undefined ? {} : { initialValue: selectedId }), presentation: 'fullscreen-tree', filterable: true, allowCustom: false, signal,
|
|
821
|
+
});
|
|
822
|
+
if (answer === null)
|
|
823
|
+
return;
|
|
824
|
+
this.assertActive(agent);
|
|
825
|
+
signal.throwIfAborted();
|
|
826
|
+
if (agent.status !== 'idle')
|
|
827
|
+
throw new Error('Finish or interrupt the active turn before navigating the Session Tree.');
|
|
828
|
+
if (answer === 'marked:') {
|
|
829
|
+
markedOnly = !markedOnly;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const clear = answer.startsWith('unlabel:');
|
|
833
|
+
if (clear || answer.startsWith('label:')) {
|
|
834
|
+
const id = answer.slice(clear ? 'unlabel:'.length : 'label:'.length);
|
|
835
|
+
const node = visible.find(item => item.id === id);
|
|
836
|
+
if (node === undefined)
|
|
837
|
+
continue;
|
|
838
|
+
selectedId = id;
|
|
839
|
+
if (clear) {
|
|
840
|
+
setSessionLabel(this.#libraryPath, id, '');
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
let draft = library.labels[id] ?? '';
|
|
844
|
+
let error;
|
|
845
|
+
while (true) {
|
|
846
|
+
const label = await this.#tui.prompt({
|
|
847
|
+
title: 'Label Conversation Node', question: `Label for ${node.label}`,
|
|
848
|
+
initialInput: draft, detail: error ?? 'Up to 120 characters. Alt+U in the tree clears a label.',
|
|
849
|
+
allowCustom: true, notify: false, signal,
|
|
850
|
+
});
|
|
851
|
+
if (label === null)
|
|
852
|
+
break;
|
|
853
|
+
this.assertActive(agent);
|
|
854
|
+
signal.throwIfAborted();
|
|
855
|
+
try {
|
|
856
|
+
setSessionLabel(this.#libraryPath, id, label);
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
catch (failure) {
|
|
860
|
+
error = failure instanceof Error ? failure.message : String(failure);
|
|
861
|
+
draft = label;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
const continuing = answer.startsWith('continue:');
|
|
869
|
+
const node = nodes.find(item => item.id === (continuing ? answer.slice('continue:'.length) : answer));
|
|
870
|
+
if (node === undefined)
|
|
871
|
+
throw new Error('The selected conversation node is no longer available.');
|
|
872
|
+
if (continuing || node.kind === 'branch') {
|
|
873
|
+
if (node.sessionId === current.id) {
|
|
874
|
+
this.#tui.notice('This branch is already active.');
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
await this.resumeSession(agent, node.sessionId, signal);
|
|
878
|
+
this.#tui.notice('Continued the selected conversation branch.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
// Draft preparation forks the selected branch; browsing itself never activates it.
|
|
882
|
+
const source = branches.find(branch => branch.id === node.sessionId);
|
|
883
|
+
if (source === undefined)
|
|
884
|
+
throw new Error('The selected conversation branch is no longer available.');
|
|
885
|
+
const events = source.events;
|
|
886
|
+
const selected = conversationTurns(events).find(turn => turn.messageIndex === node.messageIndex);
|
|
775
887
|
if (selected === undefined)
|
|
776
888
|
throw new Error('The selected conversation turn is no longer available.');
|
|
777
889
|
const message = events[selected.messageIndex];
|
|
@@ -808,9 +920,9 @@ export class SessionRuntime {
|
|
|
808
920
|
seed: events.slice(0, selected.branchIndex),
|
|
809
921
|
meta: {
|
|
810
922
|
...(agent.session.header.cwd === undefined ? {} : { cwd: agent.session.header.cwd }),
|
|
811
|
-
parentSession:
|
|
923
|
+
parentSession: SessionId(source.id),
|
|
812
924
|
isSeeded: true,
|
|
813
|
-
agentPreset: active.agentPreset,
|
|
925
|
+
agentPreset: source.agentPreset ?? active.agentPreset,
|
|
814
926
|
},
|
|
815
927
|
inheritedEventCount: SessionLogOffset(selected.branchIndex),
|
|
816
928
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
@@ -836,8 +948,9 @@ export class SessionRuntime {
|
|
|
836
948
|
reasoningEffort: undefined,
|
|
837
949
|
agentPreset: configuration.agentPreset,
|
|
838
950
|
});
|
|
839
|
-
|
|
840
|
-
this.#tui.
|
|
951
|
+
const files = message.data.content.filter((block) => block.type === 'file').map(block => block.attachment);
|
|
952
|
+
this.#tui.restoreInput({ text, images, ...(files.length === 0 ? {} : { files }) });
|
|
953
|
+
this.#tui.notice(`Editing from before turn ${selected.turn}. The original branch remains available in the Session Tree.`);
|
|
841
954
|
await this.#disposeRetired();
|
|
842
955
|
await this.refreshRecent();
|
|
843
956
|
}
|
|
@@ -901,6 +1014,34 @@ export class SessionRuntime {
|
|
|
901
1014
|
this.#pushSessionInfo();
|
|
902
1015
|
return outcome;
|
|
903
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Resolve and attach the changed-file summary a `workspace/changes` event
|
|
1019
|
+
* announces.
|
|
1020
|
+
*
|
|
1021
|
+
* The Harness appends the event and only then files the summary under its
|
|
1022
|
+
* sequence, so a listener running inside that append always reads
|
|
1023
|
+
* `undefined`. The lookup is therefore deferred by one microtask, which runs
|
|
1024
|
+
* after the recorder's own synchronous store, and enriches the turn-only
|
|
1025
|
+
* block the transcript already rendered. A session without the plugin, or a
|
|
1026
|
+
* recorder that has already been disposed, simply keeps the fallback.
|
|
1027
|
+
*
|
|
1028
|
+
* @param ctx - host context carrying the optional service.
|
|
1029
|
+
* @param tui - the mounted presentation service.
|
|
1030
|
+
* @param session - the Session that appended the event.
|
|
1031
|
+
* @param event - the `workspace/changes` event.
|
|
1032
|
+
*/
|
|
1033
|
+
#serveWorkspaceSummary(ctx, tui, session, event) {
|
|
1034
|
+
const service = ctx.get('workspaceChanges');
|
|
1035
|
+
if (service === undefined)
|
|
1036
|
+
return;
|
|
1037
|
+
const { id } = session;
|
|
1038
|
+
const { seq, data } = event;
|
|
1039
|
+
queueMicrotask(() => {
|
|
1040
|
+
const summary = service.summary(id, seq);
|
|
1041
|
+
if (summary !== undefined)
|
|
1042
|
+
tui.setWorkspaceSummary(data.turn, summary);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
904
1045
|
/**
|
|
905
1046
|
* Publish a current-format generation for every stored session an earlier
|
|
906
1047
|
* release wrote. A read open never upgrades a log, so without this pass
|
|
@@ -966,6 +1107,7 @@ export class SessionRuntime {
|
|
|
966
1107
|
return run;
|
|
967
1108
|
}
|
|
968
1109
|
async #refreshRecentNow() {
|
|
1110
|
+
this.#archivedSessions = new Set(readSessionLibrary(this.#libraryPath).archived);
|
|
969
1111
|
const persistence = this.#ctx.get('sessionPersistence');
|
|
970
1112
|
if (persistence === undefined) {
|
|
971
1113
|
this.#recent = [];
|
|
@@ -1030,6 +1172,7 @@ export class SessionRuntime {
|
|
|
1030
1172
|
this.#pushSessionInfo();
|
|
1031
1173
|
}
|
|
1032
1174
|
async dispose() {
|
|
1175
|
+
this.#queueAbort?.abort(new Error('session runtime disposed'));
|
|
1033
1176
|
if (this.#disposed)
|
|
1034
1177
|
return;
|
|
1035
1178
|
this.#disposed = true;
|
|
@@ -1106,7 +1249,7 @@ export class SessionRuntime {
|
|
|
1106
1249
|
return undefined;
|
|
1107
1250
|
}
|
|
1108
1251
|
}
|
|
1109
|
-
async #activate(next) {
|
|
1252
|
+
async #activate(next, reason = 'open') {
|
|
1110
1253
|
// Reject a late create before it publishes UI on a disposed tree.
|
|
1111
1254
|
if (this.#disposed) {
|
|
1112
1255
|
this.#releaseHandle(next.handle);
|
|
@@ -1114,10 +1257,11 @@ export class SessionRuntime {
|
|
|
1114
1257
|
}
|
|
1115
1258
|
const previous = this.#active;
|
|
1116
1259
|
const epoch = ++this.#activationEpoch;
|
|
1260
|
+
this.#queueAbort?.abort(new Error('active session changed'));
|
|
1117
1261
|
this.#active = next;
|
|
1118
1262
|
void this.#claimWorkspace(next);
|
|
1119
1263
|
try {
|
|
1120
|
-
this.#presentAgent(next);
|
|
1264
|
+
this.#presentAgent(next, reason);
|
|
1121
1265
|
const selected = this.selection(next.handle.agent);
|
|
1122
1266
|
const info = await this.#resolveModelInfo(selected);
|
|
1123
1267
|
if (this.#disposed || this.#activationEpoch !== epoch) {
|
|
@@ -1178,16 +1322,18 @@ export class SessionRuntime {
|
|
|
1178
1322
|
this.#releaseHandle(previous.handle);
|
|
1179
1323
|
}
|
|
1180
1324
|
/** Show one active session on the terminal: transcript, tools, controls, model. */
|
|
1181
|
-
#presentAgent(active) {
|
|
1325
|
+
#presentAgent(active, reason = 'open') {
|
|
1326
|
+
this.#workflows.reset(active.handle.agent.session.snapshotEvents());
|
|
1182
1327
|
const agent = active.handle.agent;
|
|
1183
1328
|
this.#inspectedId = undefined;
|
|
1184
1329
|
this.#tui.setInspectedSubagent(undefined);
|
|
1185
1330
|
this.#tui.setStatus(agent.status);
|
|
1186
1331
|
this.#tracker.sync();
|
|
1332
|
+
this.#tui.setSubagents(this.#workflows.roster(this.#tracker.snapshot()));
|
|
1187
1333
|
// Seed welcome metadata before replaceSession commits the startup header to
|
|
1188
1334
|
// native scrollback; later updates cannot rewrite that frozen first frame.
|
|
1189
1335
|
this.#pushSessionInfo();
|
|
1190
|
-
this.#replaceTranscript(agent);
|
|
1336
|
+
this.#replaceTranscript(agent, reason);
|
|
1191
1337
|
this.#pushTools();
|
|
1192
1338
|
this.#pushCommands();
|
|
1193
1339
|
const selected = this.selection(agent);
|
|
@@ -1304,15 +1450,15 @@ export class SessionRuntime {
|
|
|
1304
1450
|
this.#tui.setSession({
|
|
1305
1451
|
id: agent.id,
|
|
1306
1452
|
...(title === undefined ? {} : { title }),
|
|
1307
|
-
recent: this.#recent.filter(row => row.id !== agent.id),
|
|
1453
|
+
recent: this.#recent.filter(row => row.id !== agent.id && !this.#archivedSessions.has(row.id)),
|
|
1308
1454
|
stats: sessionStats(events, active.contextWindow, projection),
|
|
1309
1455
|
controls: this.#sessionControls(active, projection),
|
|
1310
1456
|
});
|
|
1311
1457
|
}
|
|
1312
|
-
#replaceTranscript(agent) {
|
|
1458
|
+
#replaceTranscript(agent, reason = 'refresh') {
|
|
1313
1459
|
const events = agent.session.snapshotEvents();
|
|
1314
1460
|
this.#tracker.observeCatalog(agent.session, events);
|
|
1315
|
-
this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status);
|
|
1461
|
+
this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
|
|
1316
1462
|
this.#replayLivePrefix(agent.session.id);
|
|
1317
1463
|
}
|
|
1318
1464
|
#replaceVisibleTranscript() {
|
|
@@ -1367,7 +1513,7 @@ export class SessionRuntime {
|
|
|
1367
1513
|
return;
|
|
1368
1514
|
this.#inspectedId = id;
|
|
1369
1515
|
const child = this.#ctx.get('agents')?.get(SessionId(id));
|
|
1370
|
-
this.#tui.replaceSession(events, child === undefined ? undefined : this.#ctx.get('tuiToolPresentation')?.session(child, events), child?.status ?? 'idle');
|
|
1516
|
+
this.#tui.replaceSession(events, child === undefined ? undefined : this.#ctx.get('tuiToolPresentation')?.session(child, events), child?.status ?? 'idle', 'refresh');
|
|
1371
1517
|
this.#replayLivePrefix(id);
|
|
1372
1518
|
this.#tui.setInspectedSubagent(this.#inspectView(id, child?.status === 'running' ? 'running' : 'waiting'));
|
|
1373
1519
|
this.#tui.setStatus(child?.status ?? 'idle');
|