@agi-fans/dsh-tui 0.1.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/lib/autocomplete.d.ts +80 -0
  4. package/lib/autocomplete.js +320 -0
  5. package/lib/autocomplete.js.map +1 -0
  6. package/lib/box.d.ts +79 -0
  7. package/lib/box.js +276 -0
  8. package/lib/box.js.map +1 -0
  9. package/lib/clipboard.d.ts +19 -0
  10. package/lib/clipboard.js +77 -0
  11. package/lib/clipboard.js.map +1 -0
  12. package/lib/command-integrations.d.ts +10 -0
  13. package/lib/command-integrations.js +49 -0
  14. package/lib/command-integrations.js.map +1 -0
  15. package/lib/command-mode.d.ts +5 -0
  16. package/lib/command-mode.js +73 -0
  17. package/lib/command-mode.js.map +1 -0
  18. package/lib/command-model.d.ts +5 -0
  19. package/lib/command-model.js +87 -0
  20. package/lib/command-model.js.map +1 -0
  21. package/lib/command-output.d.ts +6 -0
  22. package/lib/command-output.js +33 -0
  23. package/lib/command-output.js.map +1 -0
  24. package/lib/command-queue.d.ts +5 -0
  25. package/lib/command-queue.js +62 -0
  26. package/lib/command-queue.js.map +1 -0
  27. package/lib/command-registration.d.ts +5 -0
  28. package/lib/command-registration.js +20 -0
  29. package/lib/command-registration.js.map +1 -0
  30. package/lib/command-session.d.ts +5 -0
  31. package/lib/command-session.js +153 -0
  32. package/lib/command-session.js.map +1 -0
  33. package/lib/command-transcript.d.ts +5 -0
  34. package/lib/command-transcript.js +85 -0
  35. package/lib/command-transcript.js.map +1 -0
  36. package/lib/copy-selector.d.ts +52 -0
  37. package/lib/copy-selector.js +136 -0
  38. package/lib/copy-selector.js.map +1 -0
  39. package/lib/copy-targets.d.ts +34 -0
  40. package/lib/copy-targets.js +170 -0
  41. package/lib/copy-targets.js.map +1 -0
  42. package/lib/definition.d.ts +171 -0
  43. package/lib/definition.js +14 -0
  44. package/lib/definition.js.map +1 -0
  45. package/lib/editor.d.ts +66 -0
  46. package/lib/editor.js +365 -0
  47. package/lib/editor.js.map +1 -0
  48. package/lib/event-views.d.ts +188 -0
  49. package/lib/event-views.js +678 -0
  50. package/lib/event-views.js.map +1 -0
  51. package/lib/external-editor.d.ts +2 -0
  52. package/lib/external-editor.js +31 -0
  53. package/lib/external-editor.js.map +1 -0
  54. package/lib/history-search.d.ts +61 -0
  55. package/lib/history-search.js +239 -0
  56. package/lib/history-search.js.map +1 -0
  57. package/lib/history-store.d.ts +8 -0
  58. package/lib/history-store.js +36 -0
  59. package/lib/history-store.js.map +1 -0
  60. package/lib/hotkeys.d.ts +14 -0
  61. package/lib/hotkeys.js +115 -0
  62. package/lib/hotkeys.js.map +1 -0
  63. package/lib/human-interaction.d.ts +5 -0
  64. package/lib/human-interaction.js +8 -0
  65. package/lib/human-interaction.js.map +1 -0
  66. package/lib/image-paste.d.ts +21 -0
  67. package/lib/image-paste.js +277 -0
  68. package/lib/image-paste.js.map +1 -0
  69. package/lib/index.d.ts +10 -0
  70. package/lib/index.js +11 -0
  71. package/lib/index.js.map +1 -0
  72. package/lib/interaction-adapter.d.ts +18 -0
  73. package/lib/interaction-adapter.js +93 -0
  74. package/lib/interaction-adapter.js.map +1 -0
  75. package/lib/keybindings-config.d.ts +5 -0
  76. package/lib/keybindings-config.js +28 -0
  77. package/lib/keybindings-config.js.map +1 -0
  78. package/lib/keys.d.ts +59 -0
  79. package/lib/keys.js +263 -0
  80. package/lib/keys.js.map +1 -0
  81. package/lib/markdown.d.ts +13 -0
  82. package/lib/markdown.js +453 -0
  83. package/lib/markdown.js.map +1 -0
  84. package/lib/path-complete.d.ts +85 -0
  85. package/lib/path-complete.js +231 -0
  86. package/lib/path-complete.js.map +1 -0
  87. package/lib/path-mentions.d.ts +4 -0
  88. package/lib/path-mentions.js +50 -0
  89. package/lib/path-mentions.js.map +1 -0
  90. package/lib/project-context.d.ts +13 -0
  91. package/lib/project-context.js +52 -0
  92. package/lib/project-context.js.map +1 -0
  93. package/lib/project-file-search.d.ts +25 -0
  94. package/lib/project-file-search.js +207 -0
  95. package/lib/project-file-search.js.map +1 -0
  96. package/lib/prompt-selector.d.ts +56 -0
  97. package/lib/prompt-selector.js +297 -0
  98. package/lib/prompt-selector.js.map +1 -0
  99. package/lib/provider-local.d.ts +115 -0
  100. package/lib/provider-local.js +1760 -0
  101. package/lib/provider-local.js.map +1 -0
  102. package/lib/pwd.d.ts +13 -0
  103. package/lib/pwd.js +14 -0
  104. package/lib/pwd.js.map +1 -0
  105. package/lib/relative-time.d.ts +2 -0
  106. package/lib/relative-time.js +11 -0
  107. package/lib/relative-time.js.map +1 -0
  108. package/lib/renderer.d.ts +112 -0
  109. package/lib/renderer.js +181 -0
  110. package/lib/renderer.js.map +1 -0
  111. package/lib/runner.d.ts +9 -0
  112. package/lib/runner.js +94 -0
  113. package/lib/runner.js.map +1 -0
  114. package/lib/session-controller.d.ts +92 -0
  115. package/lib/session-controller.js +571 -0
  116. package/lib/session-controller.js.map +1 -0
  117. package/lib/session-runtime.d.ts +16 -0
  118. package/lib/session-runtime.js +17 -0
  119. package/lib/session-runtime.js.map +1 -0
  120. package/lib/settings-list.d.ts +67 -0
  121. package/lib/settings-list.js +380 -0
  122. package/lib/settings-list.js.map +1 -0
  123. package/lib/status-config.d.ts +32 -0
  124. package/lib/status-config.js +52 -0
  125. package/lib/status-config.js.map +1 -0
  126. package/lib/status-line.d.ts +41 -0
  127. package/lib/status-line.js +351 -0
  128. package/lib/status-line.js.map +1 -0
  129. package/lib/style.d.ts +22 -0
  130. package/lib/style.js +23 -0
  131. package/lib/style.js.map +1 -0
  132. package/lib/theme.d.ts +83 -0
  133. package/lib/theme.js +317 -0
  134. package/lib/theme.js.map +1 -0
  135. package/lib/tool-presentation.d.ts +20 -0
  136. package/lib/tool-presentation.js +79 -0
  137. package/lib/tool-presentation.js.map +1 -0
  138. package/lib/tool-renderers.d.ts +21 -0
  139. package/lib/tool-renderers.js +133 -0
  140. package/lib/tool-renderers.js.map +1 -0
  141. package/lib/tools-list.d.ts +14 -0
  142. package/lib/tools-list.js +61 -0
  143. package/lib/tools-list.js.map +1 -0
  144. package/lib/transcript-export.d.ts +4 -0
  145. package/lib/transcript-export.js +66 -0
  146. package/lib/transcript-export.js.map +1 -0
  147. package/lib/tui-settings.d.ts +20 -0
  148. package/lib/tui-settings.js +23 -0
  149. package/lib/tui-settings.js.map +1 -0
  150. package/lib/welcome-tips.d.ts +10 -0
  151. package/lib/welcome-tips.js +36 -0
  152. package/lib/welcome-tips.js.map +1 -0
  153. package/lib/width.d.ts +54 -0
  154. package/lib/width.js +323 -0
  155. package/lib/width.js.map +1 -0
  156. package/package.json +119 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Active-agent/session runtime shared by the runner and command plugins.
3
+ *
4
+ * This is the deep module between Harness runtime services and the TUI:
5
+ * Agent creation, replacement, persistence lookup, model selection, recent
6
+ * sessions, projections, command routing, and cleanup stay behind one API.
7
+ * @module @agi-fans/dsh-tui/session-controller
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import { type Agent, type ModelSelection } from '@deepseek-ai/dsh-agent';
11
+ import { type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
12
+ import type { ImageAttachmentRef, SaveImageAttachment } from '@deepseek-ai/dsh-attachment';
13
+ import type { PermissionSelect } from '@deepseek-ai/dsh-permission-presets/types';
14
+ import type { PlanProjection } from '@deepseek-ai/dsh-plan-mode/types';
15
+ import { type SkillSummary } from '@deepseek-ai/dsh-skill';
16
+ import type { SessionStatsProjection } from '@deepseek-ai/dsh-session-stats/types';
17
+ import type { ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client';
18
+ import { type SessionEvent } from '@deepseek-ai/dsh-session';
19
+ import type { TuiCommand, TuiRecentSession, TuiService, TuiSessionControls, TuiSessionStats, TuiSubmission } from './definition.ts';
20
+ /** Projection values consumed as one consistent snapshot when the units exist. */
21
+ export interface TuiStatsProjection {
22
+ sessionStats?: SessionStatsProjection;
23
+ tokenUsage?: TokenUsageProjection;
24
+ contextPressure?: ContextPressureProjection;
25
+ plan?: PlanProjection;
26
+ permissions?: PermissionSelect;
27
+ }
28
+ /** Present only the session controls whose owning Harness plugins are composed. */
29
+ export declare function sessionControls(projection?: TuiStatsProjection): TuiSessionControls;
30
+ /** Composer projection of a Harness model selection and its adapter default. */
31
+ export declare function modelStatus(selection: ModelSelection, info?: Pick<LlmResolvedModelInfo, 'reasoning'>): {
32
+ model: string;
33
+ reasoningEffort?: string;
34
+ };
35
+ /** Fold a complete log as the capability-absence fallback for projections. */
36
+ export declare function sessionStats(events: readonly SessionEvent[], contextWindow?: number, projection?: TuiStatsProjection): TuiSessionStats;
37
+ /** Title and latest-human-message preview for durable session discovery. */
38
+ export declare function recentSessionContent(events: readonly SessionEvent[]): {
39
+ title: string;
40
+ preview?: string;
41
+ } | undefined;
42
+ /** Convert the human-visible part of a skill catalog into slash commands. */
43
+ export declare function userSkillCommands(skills: readonly SkillSummary[]): TuiCommand[];
44
+ interface SubmissionAttachmentStore {
45
+ validateImage(input: SaveImageAttachment): Promise<void>;
46
+ saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
47
+ }
48
+ /** Validate all drafts, persist them, then build one atomic mixed user message. */
49
+ export declare function createSubmissionMessage(submission: TuiSubmission, attachments?: SubmissionAttachmentStore): Promise<{
50
+ content: ({
51
+ type: "image";
52
+ attachment: ImageAttachmentRef;
53
+ } | {
54
+ type: "text";
55
+ text: string;
56
+ })[];
57
+ source: {
58
+ kind: "user";
59
+ };
60
+ } & Pick<import("@deepseek-ai/dsh-llm").UserMessage, "id" | "role">>;
61
+ /** Own one switchable top-level Agent and project it onto a TuiService. */
62
+ export declare class SessionRuntime {
63
+ #private;
64
+ constructor(ctx: Context, tui: TuiService);
65
+ get agent(): Agent | undefined;
66
+ start(): Promise<void>;
67
+ /** Submit one human composer value; active turns retain it as a later follow-up. */
68
+ send(input: string | TuiSubmission, agent?: Agent): Promise<void>;
69
+ /** Execute a plugin-owned slash command, falling back to user-invocable skills. */
70
+ execute(line: string, signal: AbortSignal): Promise<boolean>;
71
+ /** Fail when a command invocation targets a stale or background Agent. */
72
+ assertActive(agent: Agent): void;
73
+ /** Immutable recent-session view used by the resume command. */
74
+ get recentSessions(): readonly TuiRecentSession[];
75
+ /** Current model selection for the active Agent. */
76
+ selection(agent?: Agent): ModelSelection;
77
+ /** Replace the active Agent's selection and persist it as the next default. */
78
+ changeSelection(agent: Agent, selection: ModelSelection, info?: LlmResolvedModelInfo): Promise<void>;
79
+ /** Start a new top-level session with the current model selection. */
80
+ newSession(agent: Agent): Promise<void>;
81
+ /** Replace the active top-level session with one durable session. */
82
+ resumeSession(agent: Agent, id: string, signal: AbortSignal): Promise<void>;
83
+ /** Whole-session figures for the active Agent. */
84
+ stats(agent?: Agent): TuiSessionStats;
85
+ /** Effective reasoning effort after applying the selected model's adapter default. */
86
+ reasoningEffort(agent?: Agent): string | undefined;
87
+ /** Harness-owned collaboration and permission controls for the active Agent. */
88
+ controls(agent?: Agent): TuiSessionControls;
89
+ refreshRecent(): Promise<void>;
90
+ dispose(): Promise<void>;
91
+ }
92
+ export {};
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Active-agent/session runtime shared by the runner and command plugins.
3
+ *
4
+ * This is the deep module between Harness runtime services and the TUI:
5
+ * Agent creation, replacement, persistence lookup, model selection, recent
6
+ * sessions, projections, command routing, and cleanup stay behind one API.
7
+ * @module @agi-fans/dsh-tui/session-controller
8
+ */
9
+ import { randomUUID } from 'node:crypto';
10
+ import { installModelSelection, } from '@deepseek-ai/dsh-agent';
11
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
12
+ import { isTokenDelta } from '@deepseek-ai/dsh-llm/message';
13
+ import { isUserInvocable } from '@deepseek-ai/dsh-skill';
14
+ import { SessionId } from '@deepseek-ai/dsh-session';
15
+ function parseControl(line) {
16
+ const match = /^\/([a-z][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)(?:\s+(.*))?$/su.exec(line.trim());
17
+ if (match === null || match[1] === undefined)
18
+ return undefined;
19
+ return { name: match[1].toLowerCase(), input: match[2]?.trim() ?? '' };
20
+ }
21
+ /** Present only the session controls whose owning Harness plugins are composed. */
22
+ export function sessionControls(projection) {
23
+ return {
24
+ ...(projection?.plan === undefined ? {} : { plan: { ...projection.plan } }),
25
+ ...(projection?.permissions === undefined ? {} : { permission: projection.permissions.currentValue }),
26
+ };
27
+ }
28
+ /** Composer projection of a Harness model selection and its adapter default. */
29
+ export function modelStatus(selection, info) {
30
+ const effort = selection.reasoningEffort ?? info?.reasoning?.defaultEffort;
31
+ return {
32
+ model: selection.model,
33
+ ...(effort === undefined ? {} : { reasoningEffort: String(effort) }),
34
+ };
35
+ }
36
+ /** Fold a complete log as the capability-absence fallback for projections. */
37
+ export function sessionStats(events, contextWindow, projection) {
38
+ let turns = 0;
39
+ let steps = 0;
40
+ let llmMs = 0;
41
+ let toolMs = 0;
42
+ let ttftMs = 0;
43
+ let ttftSteps = 0;
44
+ let decodeMs = 0;
45
+ let decodeTokens = 0;
46
+ let inputTokens = 0;
47
+ let outputTokens = 0;
48
+ let cacheReadTokens = 0;
49
+ let cacheWriteTokens = 0;
50
+ let contextTokens;
51
+ let first;
52
+ let last;
53
+ let lastTurn;
54
+ let openStep;
55
+ const pendingCalls = new Map();
56
+ for (const event of events) {
57
+ first ??= event.time;
58
+ last = event.time;
59
+ switch (event.type) {
60
+ case 'step/start':
61
+ openStep = { turn: event.data.turn, step: event.data.step, startTime: event.time };
62
+ break;
63
+ case 'assistant/chunk':
64
+ if (openStep !== undefined
65
+ && openStep.turn === event.data.turn
66
+ && openStep.step === event.data.step
67
+ && openStep.firstTokenTime === undefined
68
+ && isTokenDelta(event.data.chunk)) {
69
+ openStep.firstTokenTime = event.time;
70
+ }
71
+ break;
72
+ case 'assistant/message': {
73
+ const usage = event.data.usage;
74
+ if (usage !== undefined) {
75
+ const read = usage.cacheReadTokens ?? 0;
76
+ const write = usage.cacheWriteTokens ?? 0;
77
+ const billedInput = usage.inputTokens + read + write;
78
+ inputTokens += billedInput;
79
+ outputTokens += usage.outputTokens;
80
+ cacheReadTokens += read;
81
+ cacheWriteTokens += write;
82
+ contextTokens = billedInput + usage.outputTokens;
83
+ }
84
+ if (openStep === undefined || openStep.turn !== event.data.turn || openStep.step !== event.data.step)
85
+ break;
86
+ llmMs += Math.max(0, event.time - openStep.startTime);
87
+ if (openStep.firstTokenTime !== undefined) {
88
+ ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime);
89
+ ttftSteps += 1;
90
+ if (usage !== undefined) {
91
+ decodeMs += Math.max(0, event.time - openStep.firstTokenTime);
92
+ decodeTokens += usage.outputTokens;
93
+ }
94
+ }
95
+ openStep = undefined;
96
+ break;
97
+ }
98
+ case 'tool/call':
99
+ pendingCalls.set(event.data.callId, event.time);
100
+ break;
101
+ case 'tool/result': {
102
+ const source = event.data.message.source;
103
+ if (source.kind !== 'tool')
104
+ break;
105
+ const dispatched = pendingCalls.get(source.callId);
106
+ if (dispatched === undefined)
107
+ break;
108
+ toolMs += Math.max(0, event.time - dispatched);
109
+ pendingCalls.delete(source.callId);
110
+ break;
111
+ }
112
+ case 'step/end':
113
+ turns += lastTurn === event.data.turn ? 0 : 1;
114
+ steps += 1;
115
+ lastTurn = event.data.turn;
116
+ openStep = undefined;
117
+ break;
118
+ case 'turn/end':
119
+ pendingCalls.clear();
120
+ break;
121
+ }
122
+ }
123
+ const projectedStats = projection?.sessionStats;
124
+ const projectedUsage = projection?.tokenUsage;
125
+ const pressure = projection?.contextPressure;
126
+ const projectedInput = projectedUsage === undefined
127
+ ? undefined
128
+ : projectedUsage.uncachedInputTokens + projectedUsage.cacheReadTokens + projectedUsage.cacheWriteTokens;
129
+ const projectedContext = pressure?.projectedTokens ?? pressure?.pressureTokens ?? contextTokens;
130
+ const projectedWindow = pressure?.contextWindow ?? contextWindow;
131
+ return {
132
+ turns: projectedStats?.turns ?? turns,
133
+ steps: projectedStats?.steps ?? steps,
134
+ llmMs: projectedStats?.llmMs ?? llmMs,
135
+ toolMs: projectedStats?.toolMs ?? toolMs,
136
+ ttftMs: projectedStats?.ttftMs ?? ttftMs,
137
+ ttftSteps: projectedStats?.ttftSteps ?? ttftSteps,
138
+ decodeMs: projectedStats?.decodeMs ?? decodeMs,
139
+ decodeTokens: projectedStats?.decodeTokens ?? decodeTokens,
140
+ inputTokens: projectedInput ?? inputTokens,
141
+ outputTokens: projectedUsage?.outputTokens ?? outputTokens,
142
+ cacheReadTokens: projectedUsage?.cacheReadTokens ?? cacheReadTokens,
143
+ cacheWriteTokens: projectedUsage?.cacheWriteTokens ?? cacheWriteTokens,
144
+ ...(projectedContext === undefined ? {} : { contextTokens: projectedContext }),
145
+ ...(projectedWindow === undefined ? {} : { contextWindow: projectedWindow }),
146
+ ...(first === undefined || last === undefined ? {} : { elapsedMs: Math.max(0, last - first) }),
147
+ };
148
+ }
149
+ function explicitSessionTitle(events) {
150
+ for (let i = events.length - 1; i >= 0; i -= 1) {
151
+ const event = events[i];
152
+ if (event?.type === 'session/title')
153
+ return event.data.title;
154
+ }
155
+ return undefined;
156
+ }
157
+ function humanMessageText(event) {
158
+ if (event.type !== 'user/message' || event.data.source.kind !== 'user')
159
+ return undefined;
160
+ const text = event.data.content
161
+ .filter((block) => block.type === 'text')
162
+ .map(block => block.text)
163
+ .join(' ')
164
+ .replace(/\s+/gu, ' ')
165
+ .trim();
166
+ return text === '' ? undefined : text;
167
+ }
168
+ /** Title and latest-human-message preview for durable session discovery. */
169
+ export function recentSessionContent(events) {
170
+ const generatedTitle = explicitSessionTitle(events);
171
+ const firstMessage = events.map(humanMessageText).find((text) => text !== undefined);
172
+ if (firstMessage === undefined)
173
+ return undefined;
174
+ let lastMessage;
175
+ for (let index = events.length - 1; index >= 0; index -= 1) {
176
+ lastMessage = humanMessageText(events[index]);
177
+ if (lastMessage !== undefined)
178
+ break;
179
+ }
180
+ const title = generatedTitle ?? firstMessage;
181
+ return {
182
+ title,
183
+ ...(lastMessage === undefined || lastMessage === title ? {} : { preview: lastMessage }),
184
+ };
185
+ }
186
+ function recentSessionStatus(events) {
187
+ const end = events.findLast(event => event.type === 'turn/end');
188
+ if (end?.type !== 'turn/end')
189
+ return undefined;
190
+ if (end.data.reason.kind === 'completed')
191
+ return 'done';
192
+ if (end.data.reason.kind === 'error')
193
+ return 'failed';
194
+ if (end.data.reason.kind === 'blocked' || end.data.reason.kind === 'max-tokens')
195
+ return 'blocked';
196
+ return 'interrupted';
197
+ }
198
+ /** Convert the human-visible part of a skill catalog into slash commands. */
199
+ export function userSkillCommands(skills) {
200
+ return skills.filter(isUserInvocable).map(skill => ({
201
+ name: `skill:${skill.name}`,
202
+ description: compactDescription(skill.description),
203
+ }));
204
+ }
205
+ function skillNameFromCommand(name) {
206
+ return name.startsWith('skill:') ? name.slice('skill:'.length) : name;
207
+ }
208
+ function compactDescription(value, maxLength = 140) {
209
+ const normalized = value.replace(/\s+/gu, ' ').trim();
210
+ return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength - 1).trimEnd() + '…';
211
+ }
212
+ /** Validate all drafts, persist them, then build one atomic mixed user message. */
213
+ export async function createSubmissionMessage(submission, attachments) {
214
+ const inputs = submission.images.map(image => ({
215
+ data: image.data,
216
+ mediaType: image.mediaType,
217
+ ...(image.name === undefined ? {} : { name: image.name }),
218
+ }));
219
+ if (inputs.length > 0 && attachments === undefined)
220
+ throw new Error('Attachment storage is not configured.');
221
+ const refs = [];
222
+ if (attachments !== undefined) {
223
+ for (const input of inputs)
224
+ await attachments.validateImage(input);
225
+ for (const input of inputs)
226
+ refs.push(await attachments.saveImage(input));
227
+ }
228
+ const content = [
229
+ ...(submission.text === '' ? [] : [{ type: 'text', text: submission.text }]),
230
+ ...refs.map(attachment => ({ type: 'image', attachment })),
231
+ ];
232
+ if (content.length === 0)
233
+ throw new Error('Cannot submit an empty message.');
234
+ return createUserMessage({ content, source: { kind: 'user' } });
235
+ }
236
+ /** Own one switchable top-level Agent and project it onto a TuiService. */
237
+ export class SessionRuntime {
238
+ #ctx;
239
+ #tui;
240
+ #active;
241
+ #recent = [];
242
+ #skillCommands = [];
243
+ #started = false;
244
+ #retired = [];
245
+ #disposed = false;
246
+ #off = [];
247
+ constructor(ctx, tui) {
248
+ this.#ctx = ctx;
249
+ this.#tui = tui;
250
+ this.#off.push(ctx.on('agent/status', (payload) => {
251
+ if (payload.agent === this.#active?.handle.agent)
252
+ tui.setStatus(payload.status);
253
+ }));
254
+ this.#off.push(ctx.on('session/event', (session, event) => {
255
+ const active = this.#active;
256
+ if (active === undefined || session !== active.handle.agent.session)
257
+ return;
258
+ tui.event(event, ctx.get('tuiToolPresentation')?.event(active.handle.agent, event));
259
+ this.#pushSessionInfo();
260
+ if (event.type === 'session/title')
261
+ void this.refreshRecent();
262
+ }));
263
+ if (ctx.get('commands') !== undefined) {
264
+ this.#off.push(ctx.on('commands/change', () => { this.#pushCommands(); }));
265
+ }
266
+ if (ctx.get('skills') !== undefined) {
267
+ this.#off.push(ctx.on('skills/change', () => { void this.#refreshSkills(); }));
268
+ }
269
+ if (ctx.get('tools') !== undefined) {
270
+ this.#off.push(ctx.on('tools/change', () => {
271
+ this.#pushTools();
272
+ const active = this.#active;
273
+ if (active !== undefined)
274
+ this.#replaceTranscript(active.handle.agent);
275
+ }));
276
+ }
277
+ const projections = ctx.get('sessionProjections');
278
+ if (projections !== undefined) {
279
+ this.#off.push(projections.onChanged((session, key) => {
280
+ if (session !== this.#active?.handle.agent.session)
281
+ return;
282
+ if (key === 'sessionStats' || key === 'tokenUsage' || key === 'contextPressure'
283
+ || key === 'plan' || key === 'permissions')
284
+ this.#pushSessionInfo();
285
+ }));
286
+ }
287
+ }
288
+ get agent() {
289
+ return this.#active?.handle.agent;
290
+ }
291
+ async start() {
292
+ if (this.#started)
293
+ return;
294
+ this.#started = true;
295
+ const defaults = this.#ctx.get('agentDefaultModel')?.currentSelection();
296
+ if (defaults === undefined)
297
+ throw new Error('agent default model is unavailable');
298
+ await this.#activate(await this.#create(defaults));
299
+ await this.refreshRecent();
300
+ }
301
+ /** Submit one human composer value; active turns retain it as a later follow-up. */
302
+ async send(input, agent = this.#requiredAgent()) {
303
+ this.assertActive(agent);
304
+ const submission = typeof input === 'string' ? { text: input, images: [] } : input;
305
+ const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
306
+ this.assertActive(agent);
307
+ agent.followup(message);
308
+ }
309
+ /** Execute a plugin-owned slash command, falling back to user-invocable skills. */
310
+ async execute(line, signal) {
311
+ const parsed = parseControl(line);
312
+ if (parsed === undefined)
313
+ return false;
314
+ const commands = this.#ctx.get('commands');
315
+ const execution = await (async () => {
316
+ try {
317
+ return await commands?.execute(this.#requiredAgent(), line, signal);
318
+ }
319
+ finally {
320
+ await this.#disposeRetired();
321
+ }
322
+ })();
323
+ if (execution === undefined) {
324
+ const skill = await this.#findUserSkill(skillNameFromCommand(parsed.name), signal);
325
+ if (skill === undefined)
326
+ return false;
327
+ await this.send('/' + skill.name + (parsed.input === '' ? '' : ' ' + parsed.input));
328
+ return true;
329
+ }
330
+ const result = execution.result;
331
+ if (result.text !== undefined) {
332
+ if (result.kind === 'error')
333
+ this.#tui.notice(result.text, 'error');
334
+ else
335
+ this.#tui.commandOutput(parsed.name, result.text);
336
+ }
337
+ return true;
338
+ }
339
+ /** Fail when a command invocation targets a stale or background Agent. */
340
+ assertActive(agent) {
341
+ if (agent !== this.#requiredAgent())
342
+ throw new Error('the command does not target the active omdsh session');
343
+ }
344
+ /** Immutable recent-session view used by the resume command. */
345
+ get recentSessions() {
346
+ return this.#recent;
347
+ }
348
+ /** Current model selection for the active Agent. */
349
+ selection(agent = this.#requiredAgent()) {
350
+ this.assertActive(agent);
351
+ const current = this.#requiredActive().selection.current;
352
+ if (current === undefined)
353
+ throw new Error('active agent has no model selection');
354
+ return current;
355
+ }
356
+ /** Replace the active Agent's selection and persist it as the next default. */
357
+ async changeSelection(agent, selection, info) {
358
+ this.assertActive(agent);
359
+ const active = this.#requiredActive();
360
+ active.selection.current = selection;
361
+ const resolved = info ?? await this.#resolveModelInfo(selection);
362
+ active.contextWindow = resolved?.context?.contextWindow;
363
+ const status = modelStatus(selection, resolved);
364
+ active.reasoningEffort = status.reasoningEffort;
365
+ this.#tui.setModel(status.model, status.reasoningEffort);
366
+ this.#pushSessionInfo();
367
+ await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
368
+ }
369
+ /** Start a new top-level session with the current model selection. */
370
+ async newSession(agent) {
371
+ this.assertActive(agent);
372
+ await this.#activate(await this.#create(this.selection(agent)));
373
+ await this.refreshRecent();
374
+ }
375
+ /** Replace the active top-level session with one durable session. */
376
+ async resumeSession(agent, id, signal) {
377
+ this.assertActive(agent);
378
+ const selection = this.selection(agent);
379
+ const ref = { current: selection, assembled: undefined };
380
+ const handle = await this.#ctx.agents.resume({
381
+ resumeSessionId: SessionId(id),
382
+ agentOptions: { provider: selection.provider, model: selection.model },
383
+ signal,
384
+ setup: agentCtx => { installModelSelection(agentCtx, ref); },
385
+ });
386
+ await this.#activate({ handle, selection: ref, contextWindow: undefined, reasoningEffort: undefined });
387
+ await this.refreshRecent();
388
+ }
389
+ /** Whole-session figures for the active Agent. */
390
+ stats(agent = this.#requiredAgent()) {
391
+ this.assertActive(agent);
392
+ return this.#stats(this.#requiredActive());
393
+ }
394
+ /** Effective reasoning effort after applying the selected model's adapter default. */
395
+ reasoningEffort(agent = this.#requiredAgent()) {
396
+ this.assertActive(agent);
397
+ return this.#requiredActive().reasoningEffort;
398
+ }
399
+ /** Harness-owned collaboration and permission controls for the active Agent. */
400
+ controls(agent = this.#requiredAgent()) {
401
+ this.assertActive(agent);
402
+ return sessionControls(this.#projection(this.#requiredActive()));
403
+ }
404
+ async refreshRecent() {
405
+ const persistence = this.#ctx.get('sessionPersistence');
406
+ if (persistence === undefined) {
407
+ this.#recent = [];
408
+ this.#pushSessionInfo();
409
+ return;
410
+ }
411
+ const headers = (await persistence.list()).filter(header => header.origin !== 'subagent')
412
+ .sort((left, right) => right.createdAt - left.createdAt);
413
+ const rows = [];
414
+ for (const header of headers) {
415
+ try {
416
+ const inspected = await persistence.inspect(header.id);
417
+ const status = recentSessionStatus(inspected.events);
418
+ const content = recentSessionContent(inspected.events);
419
+ if (content === undefined)
420
+ continue;
421
+ rows.push({
422
+ id: header.id,
423
+ ...content,
424
+ createdAt: header.createdAt,
425
+ updatedAt: inspected.events.at(-1)?.time ?? header.createdAt,
426
+ eventCount: inspected.events.length,
427
+ ...(status === undefined ? {} : { status }),
428
+ });
429
+ if (rows.length >= 8)
430
+ break;
431
+ }
432
+ catch {
433
+ rows.push({ id: header.id, title: '(unavailable session)', createdAt: header.createdAt });
434
+ if (rows.length >= 8)
435
+ break;
436
+ }
437
+ }
438
+ this.#recent = rows;
439
+ this.#pushSessionInfo();
440
+ }
441
+ async dispose() {
442
+ if (this.#disposed)
443
+ return;
444
+ this.#disposed = true;
445
+ for (const off of this.#off.splice(0).reverse())
446
+ off();
447
+ await Promise.allSettled(this.#retired.splice(0).map(handle => handle.dispose()));
448
+ await this.#active?.handle.dispose();
449
+ this.#active = undefined;
450
+ }
451
+ async #create(selection) {
452
+ const ref = { current: selection, assembled: undefined };
453
+ const handle = await this.#ctx.agents.create({
454
+ sessionId: SessionId('session-' + randomUUID()),
455
+ meta: { cwd: process.cwd() },
456
+ agentOptions: { provider: selection.provider, model: selection.model },
457
+ setup: agentCtx => { installModelSelection(agentCtx, ref); },
458
+ });
459
+ return { handle, selection: ref, contextWindow: undefined, reasoningEffort: undefined };
460
+ }
461
+ async #resolveModelInfo(selection) {
462
+ try {
463
+ return await this.#ctx.get('llm')?.resolveModelInfo(selection.provider, selection.model);
464
+ }
465
+ catch {
466
+ return undefined;
467
+ }
468
+ }
469
+ async #activate(next) {
470
+ const previous = this.#active;
471
+ this.#active = next;
472
+ const agent = next.handle.agent;
473
+ this.#tui.setStatus(agent.status);
474
+ this.#replaceTranscript(agent);
475
+ this.#pushTools();
476
+ const selected = this.selection(agent);
477
+ const info = await this.#resolveModelInfo(selected);
478
+ next.contextWindow = info?.context?.contextWindow;
479
+ const status = modelStatus(selected, info);
480
+ next.reasoningEffort = status.reasoningEffort;
481
+ this.#tui.setModel(status.model, status.reasoningEffort);
482
+ await this.#refreshSkills();
483
+ this.#pushSessionInfo();
484
+ if (previous !== undefined)
485
+ this.#retired.push(previous.handle);
486
+ }
487
+ #pushCommands() {
488
+ const agent = this.agent;
489
+ const runtime = agent === undefined ? [] : (this.#ctx.get('commands')?.list(agent) ?? []);
490
+ const commands = runtime.map(command => ({
491
+ name: command.name,
492
+ description: command.description,
493
+ ...(command.input?.hint === undefined ? {} : { inputHint: command.input.hint }),
494
+ }));
495
+ const names = new Set(commands.map(command => command.name));
496
+ for (const skill of this.#skillCommands) {
497
+ if (names.has(skill.name))
498
+ continue;
499
+ commands.push(skill);
500
+ names.add(skill.name);
501
+ }
502
+ this.#tui.setCommands(commands);
503
+ }
504
+ #pushTools() {
505
+ const agent = this.agent;
506
+ const tools = agent === undefined
507
+ ? []
508
+ : (this.#ctx.get('tools')?.schemas(agent).map(schema => ({
509
+ name: schema.name,
510
+ description: schema.description,
511
+ })) ?? []);
512
+ this.#tui.setTools(tools);
513
+ }
514
+ async #refreshSkills(signal) {
515
+ const agent = this.agent;
516
+ const skills = this.#ctx.get('skills');
517
+ if (agent === undefined || skills === undefined) {
518
+ this.#skillCommands = [];
519
+ }
520
+ else {
521
+ const list = await skills.list({ cwd: agent.session.header.cwd, scope: agent, signal });
522
+ this.#skillCommands = userSkillCommands(list);
523
+ }
524
+ this.#pushCommands();
525
+ }
526
+ async #findUserSkill(name, signal) {
527
+ const agent = this.#requiredAgent();
528
+ const skills = this.#ctx.get('skills');
529
+ if (skills === undefined)
530
+ return undefined;
531
+ const list = await skills.list({ cwd: agent.session.header.cwd, scope: agent, signal });
532
+ return list.find(skill => skill.name === name && isUserInvocable(skill));
533
+ }
534
+ #pushSessionInfo() {
535
+ const active = this.#active;
536
+ if (active === undefined)
537
+ return;
538
+ const agent = active.handle.agent;
539
+ const projection = this.#projection(active);
540
+ this.#tui.setSession({
541
+ id: agent.id,
542
+ recent: this.#recent.filter(row => row.id !== agent.id),
543
+ stats: this.#stats(active, projection),
544
+ controls: sessionControls(projection),
545
+ });
546
+ }
547
+ #replaceTranscript(agent) {
548
+ const events = agent.session.events;
549
+ this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events));
550
+ }
551
+ /** Read one consistent projection cut, with the complete-log fold as fallback. */
552
+ #projection(active) {
553
+ return this.#ctx.get('sessionProjections')?.snapshot(active.handle.agent.session).values;
554
+ }
555
+ #stats(active, projection = this.#projection(active)) {
556
+ const agent = active.handle.agent;
557
+ return sessionStats(agent.session.events, active.contextWindow, projection);
558
+ }
559
+ #requiredActive() {
560
+ if (this.#active === undefined)
561
+ throw new Error('no active session');
562
+ return this.#active;
563
+ }
564
+ #requiredAgent() {
565
+ return this.#requiredActive().handle.agent;
566
+ }
567
+ async #disposeRetired() {
568
+ await Promise.allSettled(this.#retired.splice(0).map(handle => handle.dispose()));
569
+ }
570
+ }
571
+ //# sourceMappingURL=session-controller.js.map