@agi-fans/dsh-tui 0.22.0 → 0.23.1
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/LICENSE +26 -0
- package/lib/CHANGELOG.md +219 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +99 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +11 -9
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +212 -128
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +116 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +115 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1288 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +33 -20
- package/lib/session/session-controller.js +500 -129
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +3 -1
- package/lib/session/session-library.js +15 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +12 -12
- package/lib/views/autocomplete.js +54 -45
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +63 -6
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
* sessions, projections, command routing, and cleanup stay behind one API.
|
|
7
7
|
* @module @agi-fans/dsh-tui/session-controller
|
|
8
8
|
*/
|
|
9
|
+
import { openGoalControls } from "./goal-controls.js";
|
|
10
|
+
import { continuableFailure } from "./generation-continuation.js";
|
|
11
|
+
import { workflowMemberStatus, workflowOptions } from "../views/workflow.js";
|
|
9
12
|
import { randomUUID } from 'node:crypto';
|
|
10
|
-
import { steerAgent } from "./steering.js";
|
|
11
13
|
import { openMessageQueue } from "./message-queue.js";
|
|
12
14
|
import { WorkflowProjection } from "./workflow-projection.js";
|
|
13
15
|
import { join } from 'node:path';
|
|
14
16
|
import { createUserMessage, } from '@deepseek-ai/dsh-llm';
|
|
15
|
-
import {
|
|
17
|
+
import { CommandAttachments, encodeCommandAttachments } from "./command-attachments.js";
|
|
18
|
+
export { encodeCommandAttachments } from "./command-attachments.js";
|
|
19
|
+
import { isModelInvocable, isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
20
|
+
import { BUILTIN_SLASH_COMMANDS } from "../views/autocomplete.js";
|
|
16
21
|
import { readColdSessionLog } from '@deepseek-ai/dsh-session-query';
|
|
17
22
|
import { appendPluginRecord, pluginRecordOf, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
18
|
-
import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
23
|
+
import { queueHostSubagentPrompt, steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
19
24
|
import { firstVisibleStreamTime } from "../views/stream-time.js";
|
|
20
25
|
import { LiveAttemptTracker } from "./live-attempt-tracker.js";
|
|
21
26
|
import { blocksText } from "./content-text.js";
|
|
@@ -24,7 +29,7 @@ import { buildSessionTree, markedSessionTree, sessionTreeFamily } from "./sessio
|
|
|
24
29
|
export { conversationTurns } from "./conversation-turns.js";
|
|
25
30
|
import { jobNoticeFor } from "./job-notice.js";
|
|
26
31
|
import { descendantDepth, isSteerableSubagent } from "./subagent-roster.js";
|
|
27
|
-
import { readRecentRows, writeRecentRows, readSessionLibrary, sessionLibraryPath, setSessionLabel } from "./session-library.js";
|
|
32
|
+
import { readRecentRows, writeRecentRows, readSessionLibrary, sessionArgumentOptions, sessionLibraryPath, setSessionLabel } from "./session-library.js";
|
|
28
33
|
import { currentWorkingDirectory } from "./working-directory.js";
|
|
29
34
|
import { SubagentTracker } from "./subagent-tracker.js";
|
|
30
35
|
import { commandPermission } from "../commands/permission.js";
|
|
@@ -33,8 +38,8 @@ import { BTW_PRESET } from "../runtime/btw-policy.js";
|
|
|
33
38
|
import { reviewFileRecovery } from "../runtime/file-recovery-review.js";
|
|
34
39
|
import { isBlankSession, } from "./session-configuration.js";
|
|
35
40
|
import { stripComposerImageMarkers } from "../input/image-paste.js";
|
|
36
|
-
async function setupAgentContext(agentCtx, agent
|
|
37
|
-
const configured = await configureAgentContext(agentCtx, agent
|
|
41
|
+
async function setupAgentContext(agentCtx, agent) {
|
|
42
|
+
const configured = await configureAgentContext(agentCtx, agent);
|
|
38
43
|
await agentCtx.plugin(commandPermission(agent));
|
|
39
44
|
return configured;
|
|
40
45
|
}
|
|
@@ -45,10 +50,14 @@ function parseControl(line) {
|
|
|
45
50
|
return { name: match[1].toLowerCase(), input: match[2]?.trim() ?? '' };
|
|
46
51
|
}
|
|
47
52
|
/** Present only the session controls whose owning Harness plugins are composed. */
|
|
48
|
-
export function sessionControls(projection) {
|
|
53
|
+
export function sessionControls(projection, livePlan) {
|
|
49
54
|
const override = projection?.sandboxMode;
|
|
55
|
+
// Host pending is a target value, while the UI projection says whether a
|
|
56
|
+
// change remains. Cancelling a pending change can retain the current target.
|
|
57
|
+
const plan = livePlan === undefined ? projection?.plan
|
|
58
|
+
: { active: livePlan.active, pending: livePlan.pending !== undefined && livePlan.pending !== livePlan.active };
|
|
50
59
|
return {
|
|
51
|
-
...(
|
|
60
|
+
...(plan === undefined ? {} : { plan: { ...plan } }),
|
|
52
61
|
// A logged sandbox override (for example an approved widening) is the mode
|
|
53
62
|
// actually in force; the preset only describes the user's standing choice.
|
|
54
63
|
...(override == null
|
|
@@ -286,15 +295,13 @@ function recentSessionStatus(events) {
|
|
|
286
295
|
return 'interrupted';
|
|
287
296
|
}
|
|
288
297
|
/** Convert the human-visible part of a skill catalog into slash commands. */
|
|
289
|
-
export function userSkillCommands(skills) {
|
|
290
|
-
return skills.filter(isUserInvocable).map(skill => ({
|
|
291
|
-
name:
|
|
298
|
+
export function userSkillCommands(skills, reserved = new Set()) {
|
|
299
|
+
return skills.filter(skill => isUserInvocable(skill) && !reserved.has(skill.name)).map(skill => ({
|
|
300
|
+
name: skill.name,
|
|
292
301
|
description: compactDescription(skill.description),
|
|
302
|
+
skill: { userOnly: !isModelInvocable(skill) },
|
|
293
303
|
}));
|
|
294
304
|
}
|
|
295
|
-
function skillNameFromCommand(name) {
|
|
296
|
-
return name.startsWith('skill:') ? name.slice('skill:'.length) : name;
|
|
297
|
-
}
|
|
298
305
|
function compactDescription(value, maxLength = 140) {
|
|
299
306
|
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
300
307
|
return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength - 1).trimEnd() + '…';
|
|
@@ -329,9 +336,16 @@ export async function createSubmissionMessage(submission, attachments) {
|
|
|
329
336
|
content.push({ type: 'text', text: segment.text });
|
|
330
337
|
continue;
|
|
331
338
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
if (segment.kind === 'file') {
|
|
340
|
+
const attachment = submission.files?.[segment.index];
|
|
341
|
+
if (attachment !== undefined)
|
|
342
|
+
content.push({ type: 'file', attachment });
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const attachment = refs[segment.index];
|
|
346
|
+
if (attachment !== undefined)
|
|
347
|
+
content.push({ type: 'image', attachment });
|
|
348
|
+
}
|
|
335
349
|
}
|
|
336
350
|
// An attachment the composer did not mark in the text still belongs to the
|
|
337
351
|
// message: keep it rather than dropping it with the marker.
|
|
@@ -340,22 +354,16 @@ export async function createSubmissionMessage(submission, attachments) {
|
|
|
340
354
|
if (!placedImages.has(index))
|
|
341
355
|
content.push({ type: 'image', attachment });
|
|
342
356
|
}
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
const placedFiles = new Set(submission.segments.flatMap(segment => segment.kind === 'file' ? [segment.index] : []));
|
|
358
|
+
for (const [index, attachment] of (submission.files ?? []).entries()) {
|
|
359
|
+
if (!placedFiles.has(index))
|
|
360
|
+
content.push({ type: 'file', attachment });
|
|
361
|
+
}
|
|
345
362
|
}
|
|
346
363
|
if (content.length === 0)
|
|
347
364
|
throw new Error('Cannot submit an empty message.');
|
|
348
365
|
return createUserMessage({ content, source: { kind: 'user' } });
|
|
349
366
|
}
|
|
350
|
-
/** Encode composer drafts for `ctx.commands.execute`. */
|
|
351
|
-
export function encodeCommandAttachments(images) {
|
|
352
|
-
return images.map(image => ({
|
|
353
|
-
type: 'image',
|
|
354
|
-
mediaType: image.mediaType,
|
|
355
|
-
data: Buffer.from(image.data).toString('base64'),
|
|
356
|
-
...(image.name === undefined ? {} : { name: image.name }),
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
359
367
|
/** Rehydrate one durable human inbox message into an editable composer draft. */
|
|
360
368
|
export async function restoreSubmissionMessage(message, attachments) {
|
|
361
369
|
const text = blocksText(message.content);
|
|
@@ -378,31 +386,52 @@ export async function restoreSubmissionMessage(message, attachments) {
|
|
|
378
386
|
};
|
|
379
387
|
}));
|
|
380
388
|
const files = message.content.filter((block) => block.type === 'file').map(block => block.attachment);
|
|
381
|
-
|
|
389
|
+
let imageIndex = 0, fileIndex = 0;
|
|
390
|
+
const segments = message.content.flatMap(block => {
|
|
391
|
+
if (block.type === 'text')
|
|
392
|
+
return [{ kind: 'text', text: block.text }];
|
|
393
|
+
if (block.type === 'image')
|
|
394
|
+
return [{ kind: 'image', index: imageIndex++ }];
|
|
395
|
+
if (block.type === 'file')
|
|
396
|
+
return [{ kind: 'file', index: fileIndex++ }];
|
|
397
|
+
return [];
|
|
398
|
+
});
|
|
399
|
+
return { text, images, ...(files.length === 0 ? {} : { files }),
|
|
400
|
+
...(images.length === 0 && files.length === 0 ? {} : { segments }) };
|
|
401
|
+
}
|
|
402
|
+
/** The live selection succeeded, but the profile default still needs saving. */
|
|
403
|
+
export class DefaultModelSaveError extends Error {
|
|
404
|
+
constructor(selection, cause) {
|
|
405
|
+
super(`Session model is now ${selection.provider}/${selection.model}. The default model could not be saved.`, { cause });
|
|
406
|
+
this.name = 'DefaultModelSaveError';
|
|
407
|
+
}
|
|
382
408
|
}
|
|
383
409
|
/** Own one switchable top-level Agent and project it onto a TuiService. */
|
|
384
410
|
export class SessionRuntime {
|
|
411
|
+
#commandAttachments;
|
|
385
412
|
#workflows = new WorkflowProjection();
|
|
386
413
|
#ctx;
|
|
387
414
|
#tui;
|
|
388
415
|
#active;
|
|
389
416
|
#recent = [];
|
|
390
417
|
#archivedSessions = new Set();
|
|
391
|
-
#
|
|
418
|
+
#skills = [];
|
|
392
419
|
/**
|
|
393
420
|
* Live argument vocabularies, keyed by command.
|
|
394
421
|
*
|
|
395
422
|
* A snapshot rather than a callback into the services: completion runs on every
|
|
396
|
-
* keystroke and must not await a catalog.
|
|
397
|
-
*
|
|
423
|
+
* keystroke and must not await a catalog. Session refreshes publish recent
|
|
424
|
+
* rows immediately and load each provider independently.
|
|
398
425
|
*/
|
|
399
426
|
#argumentOptions = new Map();
|
|
427
|
+
#argumentEpoch = 0;
|
|
400
428
|
#started = false;
|
|
401
429
|
#retired = [];
|
|
402
430
|
// Weak: deduplication must not retain disposed session graphs.
|
|
403
431
|
#releasedHandles = new WeakSet();
|
|
404
432
|
#disposed = false;
|
|
405
433
|
#queueAbort;
|
|
434
|
+
#workflowAbort;
|
|
406
435
|
#off = [];
|
|
407
436
|
#tracker;
|
|
408
437
|
#streamAttempts = new LiveAttemptTracker();
|
|
@@ -413,11 +442,21 @@ export class SessionRuntime {
|
|
|
413
442
|
#refreshInFlight = null;
|
|
414
443
|
#refreshDirty = false;
|
|
415
444
|
#inspectEpoch = 0;
|
|
445
|
+
#inspectDeliveryTail = Promise.resolve();
|
|
446
|
+
#activeInteraction = new AbortController();
|
|
447
|
+
#pendingSends = new Map();
|
|
448
|
+
#inspectDeliveries = new Set();
|
|
449
|
+
#descendantApprovals = new Map();
|
|
416
450
|
#activationEpoch = 0;
|
|
417
451
|
#inspectedId;
|
|
418
452
|
constructor(ctx, tui, options = {}) {
|
|
419
453
|
this.#ctx = ctx;
|
|
420
454
|
this.#tui = tui;
|
|
455
|
+
const commands = ctx.get('commands');
|
|
456
|
+
if (commands !== undefined) {
|
|
457
|
+
this.#commandAttachments = new CommandAttachments(commands);
|
|
458
|
+
this.#off.push(() => this.#commandAttachments?.dispose());
|
|
459
|
+
}
|
|
421
460
|
this.#libraryPath = sessionLibraryPath(options.stateDir);
|
|
422
461
|
this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
|
|
423
462
|
this.#tracker = new SubagentTracker({
|
|
@@ -436,9 +475,24 @@ export class SessionRuntime {
|
|
|
436
475
|
this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
|
|
437
476
|
},
|
|
438
477
|
});
|
|
439
|
-
|
|
478
|
+
const offGoal = tui.setGoalHandler?.(signal => openGoalControls(ctx, tui, this.#requiredAgent(), AbortSignal.any([signal, this.interactionSignal])));
|
|
479
|
+
if (offGoal !== undefined)
|
|
480
|
+
this.#off.push(offGoal);
|
|
440
481
|
this.#off.push(tui.setQueueHandler(signal => this.openQueue(signal)));
|
|
482
|
+
const offContinue = tui.setContinueHandler?.(async (request) => {
|
|
483
|
+
const agent = this.#requiredAgent();
|
|
484
|
+
if (agent.id !== request.sessionId)
|
|
485
|
+
throw new Error('The continuation belongs to another session.');
|
|
486
|
+
await this.continueGeneration(agent, request.failureSeq);
|
|
487
|
+
});
|
|
488
|
+
if (offContinue !== undefined)
|
|
489
|
+
this.#off.push(offContinue);
|
|
441
490
|
this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
|
|
491
|
+
const offWorkflow = tui.onInspectWorkflow?.(id => {
|
|
492
|
+
void this.openWorkflow(id).catch(error => { this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); });
|
|
493
|
+
});
|
|
494
|
+
if (offWorkflow !== undefined)
|
|
495
|
+
this.#off.push(offWorkflow);
|
|
442
496
|
this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
|
|
443
497
|
this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
|
|
444
498
|
tui.setSessionSearch(async (query, signal) => {
|
|
@@ -489,6 +543,7 @@ export class SessionRuntime {
|
|
|
489
543
|
this.#off.push(ctx.on('agent/status', (payload) => {
|
|
490
544
|
if (payload.agent === this.#active?.handle.agent) {
|
|
491
545
|
tui.setStatus(payload.status, { root: true });
|
|
546
|
+
this.#pushSessionInfo();
|
|
492
547
|
return;
|
|
493
548
|
}
|
|
494
549
|
if (payload.agent.id === this.#inspectedId)
|
|
@@ -512,8 +567,10 @@ export class SessionRuntime {
|
|
|
512
567
|
if (session.id === (this.#inspectedId ?? active.handle.agent.id)) {
|
|
513
568
|
tui.setWorkingDirectory?.(currentWorkingDirectory(ctx, session));
|
|
514
569
|
}
|
|
515
|
-
if (session === active.handle.agent.session)
|
|
570
|
+
if (session === active.handle.agent.session) {
|
|
516
571
|
void this.#refreshSkills().catch(error => { ctx.logger.warn(error); });
|
|
572
|
+
void this.refreshRecent().catch(error => { ctx.logger.warn(error); });
|
|
573
|
+
}
|
|
517
574
|
}
|
|
518
575
|
if (session.id === this.#inspectedId) {
|
|
519
576
|
const child = ctx.get('agents')?.get(session.id);
|
|
@@ -560,6 +617,10 @@ export class SessionRuntime {
|
|
|
560
617
|
if (delta !== undefined)
|
|
561
618
|
this.#forwardLiveDelta(payload.agent, delta);
|
|
562
619
|
}, { global: true }));
|
|
620
|
+
this.#off.push(ctx.on('goal/activation-changed', payload => {
|
|
621
|
+
if (payload.sessionId === this.#active?.handle.agent.session.id)
|
|
622
|
+
this.#pushSessionInfo();
|
|
623
|
+
}));
|
|
563
624
|
if (ctx.get('commands') !== undefined) {
|
|
564
625
|
this.#off.push(ctx.on('commands/change', () => { this.#pushCommands(); }));
|
|
565
626
|
}
|
|
@@ -585,6 +646,8 @@ export class SessionRuntime {
|
|
|
585
646
|
}));
|
|
586
647
|
}
|
|
587
648
|
}
|
|
649
|
+
/** Presentation requests from a replaced root must not survive into its successor. */
|
|
650
|
+
get interactionSignal() { return this.#activeInteraction.signal; }
|
|
588
651
|
get agent() {
|
|
589
652
|
return this.#active?.handle.agent;
|
|
590
653
|
}
|
|
@@ -597,10 +660,48 @@ export class SessionRuntime {
|
|
|
597
660
|
return false;
|
|
598
661
|
const root = this.#active?.handle.agent;
|
|
599
662
|
if (root !== undefined) {
|
|
600
|
-
|
|
663
|
+
try {
|
|
664
|
+
this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
668
|
+
}
|
|
601
669
|
}
|
|
602
670
|
return true;
|
|
603
671
|
}
|
|
672
|
+
#isOwnedDescendant(agent, root) {
|
|
673
|
+
const agents = this.#ctx.get('agents');
|
|
674
|
+
if (agents?.get(agent.id) !== agent
|
|
675
|
+
|| descendantDepth(agent.session, root.id, this.#lookupSession) === undefined)
|
|
676
|
+
return false;
|
|
677
|
+
// Durable lineage alone also matches an independently resumed runtime root.
|
|
678
|
+
let child = agent;
|
|
679
|
+
while (child !== root) {
|
|
680
|
+
const parentId = child.session.header.parentSession;
|
|
681
|
+
const parent = parentId === undefined ? undefined : agents.get(parentId);
|
|
682
|
+
if (parent === undefined || !agents.isOwnedBy(child.id, parent))
|
|
683
|
+
return false;
|
|
684
|
+
child = parent;
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
/** Accept only live descendants of the active root for shared approval prompts. */
|
|
689
|
+
beginSubagentApproval(agent) {
|
|
690
|
+
const root = this.agent;
|
|
691
|
+
if (root === undefined || !this.#isOwnedDescendant(agent, root))
|
|
692
|
+
return undefined;
|
|
693
|
+
this.#tracker.noteSession(agent.session);
|
|
694
|
+
const abort = new AbortController();
|
|
695
|
+
this.#descendantApprovals.set(abort, root);
|
|
696
|
+
const finish = this.#tracker.beginApprovalRequest(agent.id);
|
|
697
|
+
const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
|
|
698
|
+
abort.abort(); });
|
|
699
|
+
return { label: this.#inspectView(agent.id).label, signal: abort.signal, finish: () => {
|
|
700
|
+
off();
|
|
701
|
+
this.#descendantApprovals.delete(abort);
|
|
702
|
+
finish();
|
|
703
|
+
} };
|
|
704
|
+
}
|
|
604
705
|
async start() {
|
|
605
706
|
if (this.#started)
|
|
606
707
|
return;
|
|
@@ -611,77 +712,266 @@ export class SessionRuntime {
|
|
|
611
712
|
await this.refreshRecent();
|
|
612
713
|
await this.#activate(await this.#create(defaults), 'initial');
|
|
613
714
|
}
|
|
614
|
-
/**
|
|
715
|
+
/** Admit a complete composer value through the selected Harness inbox entry. */
|
|
615
716
|
async send(input, agent = this.#requiredAgent()) {
|
|
616
|
-
this.assertActive(agent);
|
|
617
717
|
const submission = typeof input === 'string' ? { text: input, images: [] } : input;
|
|
618
|
-
|
|
619
|
-
this.assertActive(agent);
|
|
620
|
-
agent.followup(message);
|
|
718
|
+
await this.#sendSubmission(submission, agent);
|
|
621
719
|
}
|
|
622
|
-
async
|
|
720
|
+
async #sendSubmission(submission, agent, signal) {
|
|
721
|
+
const assertDelivery = () => {
|
|
722
|
+
signal?.throwIfAborted();
|
|
723
|
+
this.assertActive(agent);
|
|
724
|
+
if (submission.delivery === 'steer' && agent.status !== 'running') {
|
|
725
|
+
throw new Error('Steering needs an active turn. Use Enter to send the next message.');
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
assertDelivery();
|
|
729
|
+
this.#pendingSends.set(agent, (this.#pendingSends.get(agent) ?? 0) + 1);
|
|
730
|
+
try {
|
|
731
|
+
const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
|
|
732
|
+
assertDelivery();
|
|
733
|
+
this.#tui.pendingInputs?.prepare?.(submission, message.id);
|
|
734
|
+
this.#admit(agent, message, submission.delivery);
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
const remaining = (this.#pendingSends.get(agent) ?? 1) - 1;
|
|
738
|
+
if (remaining === 0)
|
|
739
|
+
this.#pendingSends.delete(agent);
|
|
740
|
+
else
|
|
741
|
+
this.#pendingSends.set(agent, remaining);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
#admit(agent, message, delivery) {
|
|
745
|
+
const offset = agent.session.snapshotEvents().length;
|
|
746
|
+
try {
|
|
747
|
+
if (delivery === 'steer')
|
|
748
|
+
agent.steer(message);
|
|
749
|
+
else
|
|
750
|
+
agent.followup(message);
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
// An event subscriber may throw after admission, even after the message was claimed.
|
|
754
|
+
// Only a pre-admission failure may return the submission to the composer.
|
|
755
|
+
const accepted = agent.session.snapshotEvents().slice(offset).some(event => event.type === 'agent/inbox/spliced' && event.data.inserted.some(item => item.id === message.id));
|
|
756
|
+
if (!accepted)
|
|
757
|
+
throw error;
|
|
758
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
#assertCanRepeat(agent, signal) {
|
|
762
|
+
signal?.throwIfAborted();
|
|
623
763
|
this.assertActive(agent);
|
|
624
764
|
if (this.#inspectedId !== undefined)
|
|
625
|
-
throw new Error('Return to the parent session to
|
|
626
|
-
if (
|
|
765
|
+
throw new Error('Return to the parent session to continue or resend a prompt.');
|
|
766
|
+
if (agent.status !== 'idle' || this.#pendingSends.has(agent))
|
|
767
|
+
throw new Error('Wait for the current work to finish before continuing or resending.');
|
|
768
|
+
if (agent.inbox.nextTurn.length > 0 || agent.inbox.nextStep.some(message => message.source.kind === 'user')
|
|
769
|
+
|| (this.#tui.pendingInputs?.list().length ?? 0) > 0)
|
|
770
|
+
throw new Error('Resolve pending input in /queue before continuing or resending.');
|
|
771
|
+
if (this.#descendantApprovals.size > 0 || this.#ctx.get('sessionProjections')?.snapshot(agent.session).values.userQuestions?.active.some(question => question.state !== 'continued')) {
|
|
772
|
+
throw new Error('Answer the pending question or approval before continuing or resending.');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/** Wake an eligible failed generation with empty retry input; never duplicate the original prompt. */
|
|
776
|
+
async continueGeneration(agent = this.#requiredAgent(), expectedFailure, signal) {
|
|
777
|
+
this.#assertCanRepeat(agent, signal);
|
|
778
|
+
await agent.runMaintenance(async (maintenance) => {
|
|
779
|
+
maintenance.throwIfAborted();
|
|
780
|
+
this.#assertCanRepeat(agent, signal);
|
|
781
|
+
const failure = continuableFailure(agent.session.snapshotEvents());
|
|
782
|
+
if (failure === undefined || expectedFailure !== undefined && expectedFailure !== failure) {
|
|
783
|
+
throw new Error('No recoverable model failure is available to continue. Use /retry to resend the last prompt.');
|
|
784
|
+
}
|
|
785
|
+
this.#admit(agent, createUserMessage({ content: [], source: { kind: 'retry' } }));
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/** Explicitly resend the complete latest human message with a fresh Harness identity. */
|
|
789
|
+
async retryLastPrompt(agent = this.#requiredAgent(), signal) {
|
|
790
|
+
this.#assertCanRepeat(agent, signal);
|
|
791
|
+
return agent.runMaintenance(async (maintenance) => {
|
|
792
|
+
maintenance.throwIfAborted();
|
|
793
|
+
this.#assertCanRepeat(agent, signal);
|
|
794
|
+
const previous = agent.session.snapshotEvents().findLast(event => event.type === 'user/message'
|
|
795
|
+
&& event.data.source.kind === 'user' && event.data.content.length > 0);
|
|
796
|
+
if (previous?.type !== 'user/message')
|
|
797
|
+
return false;
|
|
798
|
+
this.#admit(agent, createUserMessage({ content: previous.data.content, source: { kind: 'user' } }));
|
|
799
|
+
return true;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/** Resolve one visible live inbox, retaining its identity across asynchronous UI work. */
|
|
803
|
+
#queueTarget() {
|
|
804
|
+
const root = this.#requiredAgent();
|
|
805
|
+
const inspectedId = this.#inspectedId;
|
|
806
|
+
if (inspectedId !== undefined && !this.#inspectView(inspectedId).writable)
|
|
807
|
+
throw new Error('This subagent is read-only.');
|
|
808
|
+
const agent = inspectedId === undefined ? root : this.#ctx.get('agents')?.get(SessionId(inspectedId));
|
|
809
|
+
if (agent === undefined)
|
|
810
|
+
throw new Error('This subagent is not loaded locally. Send it a message to continue before managing its queue.');
|
|
811
|
+
const check = () => {
|
|
812
|
+
this.assertActive(root);
|
|
813
|
+
if (this.#inspectedId !== inspectedId || inspectedId !== undefined && !this.#isOwnedDescendant(agent, root)) {
|
|
814
|
+
throw new Error('The queue session is no longer active.');
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
check();
|
|
818
|
+
return { agent, check, ...(inspectedId === undefined ? {} : { label: this.#inspectView(inspectedId).label }) };
|
|
819
|
+
}
|
|
820
|
+
async openQueue(signal, requestingAgent) {
|
|
821
|
+
if (requestingAgent !== undefined)
|
|
822
|
+
this.assertActive(requestingAgent);
|
|
823
|
+
const target = this.#queueTarget();
|
|
824
|
+
const { agent } = target;
|
|
825
|
+
if (this.#queueAbort !== undefined && !this.#queueAbort.signal.aborted)
|
|
627
826
|
throw new Error('Message Queue is already open.');
|
|
827
|
+
// Validate recall state before opening; child views must never list parent delivery entries.
|
|
828
|
+
this.#tui.pendingInputs.list();
|
|
628
829
|
const abort = new AbortController();
|
|
629
830
|
this.#queueAbort = abort;
|
|
630
831
|
const onAbort = () => { abort.abort(signal.reason); };
|
|
631
832
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
632
833
|
if (signal.aborted)
|
|
633
834
|
onAbort();
|
|
835
|
+
const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
|
|
836
|
+
abort.abort(); });
|
|
634
837
|
try {
|
|
635
|
-
await openMessageQueue({ agent,
|
|
838
|
+
await openMessageQueue({ agent, signal: abort.signal, assertActive: target.check,
|
|
839
|
+
...(target.label === undefined ? {} : { ownerLabel: target.label }),
|
|
840
|
+
tui: {
|
|
841
|
+
prompt: request => this.#tui.prompt(request), notice: (text, options) => this.#tui.notice(text, options),
|
|
842
|
+
restoreInput: submission => this.#tui.restoreInput(submission, agent.id),
|
|
843
|
+
pendingInputs: target.label === undefined ? this.#tui.pendingInputs : { list: () => [], update: () => false },
|
|
844
|
+
},
|
|
636
845
|
restore: message => restoreSubmissionMessage(message, this.#ctx.get('attachments')) });
|
|
637
846
|
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (!abort.signal.aborted)
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
638
851
|
finally {
|
|
852
|
+
off();
|
|
639
853
|
signal.removeEventListener('abort', onAbort);
|
|
640
854
|
if (this.#queueAbort === abort)
|
|
641
855
|
this.#queueAbort = undefined;
|
|
642
856
|
}
|
|
643
857
|
}
|
|
644
|
-
/**
|
|
645
|
-
async
|
|
646
|
-
this
|
|
858
|
+
/** Inspect durable membership; only a currently running, owned local member can be opened. */
|
|
859
|
+
async openWorkflow(id) {
|
|
860
|
+
const root = this.#requiredAgent();
|
|
861
|
+
const inspectedId = this.#inspectedId;
|
|
862
|
+
this.#workflowAbort?.abort();
|
|
863
|
+
const abort = new AbortController();
|
|
864
|
+
this.#workflowAbort = abort;
|
|
865
|
+
const check = () => {
|
|
866
|
+
abort.signal.throwIfAborted();
|
|
867
|
+
this.assertActive(root);
|
|
868
|
+
if (this.#inspectedId !== inspectedId)
|
|
869
|
+
throw new Error('The workflow session is no longer active.');
|
|
870
|
+
};
|
|
871
|
+
const canOpen = (run, member) => {
|
|
872
|
+
const child = this.#ctx.get('agents')?.get(SessionId(member.childId));
|
|
873
|
+
return workflowMemberStatus(run, member) === 'open' && child?.status === 'running'
|
|
874
|
+
&& child.session.header.parentSession === root.id && this.#isOwnedDescendant(child, root) && this.#tracker.owns(child.id);
|
|
875
|
+
};
|
|
876
|
+
try {
|
|
877
|
+
let lastRun;
|
|
878
|
+
let lastOpen = '';
|
|
879
|
+
let options = [];
|
|
880
|
+
const refresh = () => {
|
|
881
|
+
check();
|
|
882
|
+
const run = this.#workflows.get(id);
|
|
883
|
+
if (run === undefined)
|
|
884
|
+
throw new Error('This workflow is no longer available.');
|
|
885
|
+
const open = run.members.filter(member => canOpen(run, member)).map(member => member.seq).join(',');
|
|
886
|
+
if (run !== lastRun || open !== lastOpen) {
|
|
887
|
+
options = workflowOptions(run, member => canOpen(run, member));
|
|
888
|
+
lastRun = run;
|
|
889
|
+
lastOpen = open;
|
|
890
|
+
}
|
|
891
|
+
return options;
|
|
892
|
+
};
|
|
893
|
+
let selected;
|
|
894
|
+
while (!abort.signal.aborted) {
|
|
895
|
+
const answer = await this.#tui.prompt({ title: 'Workflow',
|
|
896
|
+
question: 'Browse phases and members. Only running local members can be opened.',
|
|
897
|
+
localize: ['title', 'question'], notify: false, options: refresh(), refreshOptions: refresh,
|
|
898
|
+
presentation: 'tree', filterable: true, allowCustom: false, submitLabel: 'open',
|
|
899
|
+
emptyText: 'No matching workflow members.', signal: abort.signal,
|
|
900
|
+
...(selected === undefined ? {} : { initialValue: selected }) });
|
|
901
|
+
if (answer === null)
|
|
902
|
+
return;
|
|
903
|
+
check();
|
|
904
|
+
selected = answer;
|
|
905
|
+
const run = this.#workflows.get(id);
|
|
906
|
+
const member = run?.members.find(member => `member:${member.seq}` === answer);
|
|
907
|
+
if (run === undefined || member === undefined || !canOpen(run, member)) {
|
|
908
|
+
this.#tui.notice('This workflow member is no longer available to open.');
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
await this.#inspectSubagent(member.childId);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
if (!abort.signal.aborted)
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
919
|
+
finally {
|
|
920
|
+
if (this.#workflowAbort === abort)
|
|
921
|
+
this.#workflowAbort = undefined;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/** Rehydrate before withdrawing so navigation or missing attachments leave the inbox intact. */
|
|
925
|
+
async editLatestFollowup(request) {
|
|
926
|
+
request?.signal.throwIfAborted();
|
|
927
|
+
const target = this.#queueTarget();
|
|
928
|
+
const { agent } = target;
|
|
929
|
+
if (request?.sessionId !== undefined && request.sessionId !== agent.id)
|
|
930
|
+
throw new Error('The queue session is no longer active.');
|
|
647
931
|
const message = agent.inbox.nextTurn.findLast(candidate => candidate.source.kind === 'user');
|
|
648
932
|
if (message === undefined)
|
|
649
933
|
return undefined;
|
|
650
934
|
const submission = await restoreSubmissionMessage(message, this.#ctx.get('attachments'));
|
|
651
|
-
|
|
652
|
-
|
|
935
|
+
request?.signal.throwIfAborted();
|
|
936
|
+
target.check();
|
|
937
|
+
return agent.inbox.remove(message.id) ? { ...submission, literal: true, delivery: 'queue' } : undefined;
|
|
653
938
|
}
|
|
654
939
|
/** Execute a plugin-owned slash command, falling back to user-invocable skills. */
|
|
655
|
-
async execute(
|
|
940
|
+
async execute(input, signal, images = []) {
|
|
941
|
+
const submission = typeof input === 'string' ? { text: input, images } : input;
|
|
942
|
+
const { text: line } = submission;
|
|
943
|
+
images = submission.images;
|
|
656
944
|
const commandLine = stripComposerImageMarkers(line, images);
|
|
657
945
|
const parsed = parseControl(commandLine);
|
|
658
946
|
if (parsed === undefined)
|
|
659
947
|
return false;
|
|
660
948
|
const commands = this.#ctx.get('commands');
|
|
949
|
+
const agent = this.#requiredAgent();
|
|
950
|
+
const staged = this.#commandAttachments?.stage(agent, submission);
|
|
661
951
|
const execution = await (async () => {
|
|
662
952
|
try {
|
|
663
|
-
return await commands?.execute(
|
|
953
|
+
return await commands?.execute(agent, commandLine, staged?.attachments ?? encodeCommandAttachments(images), signal);
|
|
664
954
|
}
|
|
665
955
|
finally {
|
|
956
|
+
staged?.dispose();
|
|
666
957
|
await this.#disposeRetired();
|
|
667
958
|
}
|
|
668
959
|
})();
|
|
669
960
|
if (execution === undefined) {
|
|
670
|
-
const skill = await this.#findUserSkill(
|
|
961
|
+
const skill = await this.#findUserSkill(parsed.name, signal);
|
|
962
|
+
signal.throwIfAborted();
|
|
963
|
+
this.assertActive(agent);
|
|
671
964
|
if (skill === undefined)
|
|
672
965
|
return false;
|
|
673
|
-
|
|
674
|
-
this.#tui.restoreInput({ text: line, images });
|
|
675
|
-
this.#tui.notice(`/${parsed.name} does not accept image attachments`, { level: 'error' });
|
|
676
|
-
return true;
|
|
677
|
-
}
|
|
678
|
-
await this.send('/' + skill.name + (parsed.input === '' ? '' : ' ' + parsed.input));
|
|
966
|
+
await this.#sendSubmission(submission, agent, signal);
|
|
679
967
|
return true;
|
|
680
968
|
}
|
|
681
969
|
const result = execution.result;
|
|
682
|
-
if (result.kind === 'error' && images.length > 0)
|
|
683
|
-
this.#tui.restoreInput(
|
|
684
|
-
|
|
970
|
+
if (result.kind === 'error' && (images.length > 0 || submission.files?.length))
|
|
971
|
+
this.#tui.restoreInput(submission, agent.session.id);
|
|
972
|
+
const presented = result.kind === 'success' && result.sourceEventSeq !== undefined
|
|
973
|
+
&& agent.session.snapshotEvents().some(event => event.seq === result.sourceEventSeq && event.type === 'compaction/summary');
|
|
974
|
+
if (result.text !== undefined && !presented) {
|
|
685
975
|
if (result.kind === 'error')
|
|
686
976
|
this.#tui.notice(result.text, { level: 'error', focus: true });
|
|
687
977
|
else
|
|
@@ -700,10 +990,10 @@ export class SessionRuntime {
|
|
|
700
990
|
get recentSessions() {
|
|
701
991
|
return this.#recent;
|
|
702
992
|
}
|
|
703
|
-
/** Current model selection for the active Agent. */
|
|
993
|
+
/** Current model selection for the active Agent, read from its durable projection. */
|
|
704
994
|
selection(agent = this.#requiredAgent()) {
|
|
705
995
|
this.assertActive(agent);
|
|
706
|
-
const current =
|
|
996
|
+
const current = agent.model.current();
|
|
707
997
|
if (current === undefined)
|
|
708
998
|
throw new Error('active agent has no model selection');
|
|
709
999
|
return current;
|
|
@@ -712,34 +1002,42 @@ export class SessionRuntime {
|
|
|
712
1002
|
async changeSelection(agent, selection, info, options) {
|
|
713
1003
|
this.assertActive(agent);
|
|
714
1004
|
const active = this.#requiredActive();
|
|
715
|
-
active.selection.current = selection;
|
|
716
1005
|
const resolved = info ?? await this.#resolveModelInfo(selection);
|
|
1006
|
+
this.assertActive(agent);
|
|
1007
|
+
const current = agent.model.current();
|
|
1008
|
+
if (current?.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort) {
|
|
1009
|
+
agent.model.select(selection);
|
|
1010
|
+
}
|
|
717
1011
|
active.contextWindow = resolved?.context?.contextWindow;
|
|
718
1012
|
const status = modelStatus(selection, resolved);
|
|
719
1013
|
active.reasoningEffort = status.reasoningEffort;
|
|
720
|
-
if (options?.persist !== false) {
|
|
721
|
-
await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
|
|
722
|
-
}
|
|
723
1014
|
this.#tui.setModel(status.model, status.reasoningEffort);
|
|
724
1015
|
this.#pushSessionInfo();
|
|
1016
|
+
if (options?.persist !== false) {
|
|
1017
|
+
try {
|
|
1018
|
+
await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
|
|
1019
|
+
}
|
|
1020
|
+
catch (error) {
|
|
1021
|
+
throw new DefaultModelSaveError(selection, error);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
725
1024
|
}
|
|
726
1025
|
/** Start a new top-level session with the current model selection. */
|
|
727
|
-
async newSession(agent) {
|
|
1026
|
+
async newSession(agent, presetId) {
|
|
728
1027
|
this.assertActive(agent);
|
|
729
|
-
await this.#activate(await this.#create(this.selection(agent)), 'new');
|
|
1028
|
+
await this.#activate(await this.#create(this.selection(agent), presetId), 'new');
|
|
730
1029
|
await this.refreshRecent();
|
|
731
1030
|
}
|
|
732
1031
|
/** Replace the active top-level session with one durable session. */
|
|
733
1032
|
async resumeSession(agent, id, signal) {
|
|
734
1033
|
this.assertActive(agent);
|
|
735
1034
|
const selection = this.selection(agent);
|
|
736
|
-
const ref = { current: selection, assembled: undefined };
|
|
737
1035
|
let configured;
|
|
738
1036
|
const handle = await this.#ctx.agents.resume({
|
|
739
1037
|
resumeSessionId: SessionId(id),
|
|
740
1038
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
741
1039
|
signal,
|
|
742
|
-
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent
|
|
1040
|
+
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
|
|
743
1041
|
});
|
|
744
1042
|
const configuration = configured;
|
|
745
1043
|
if (configuration === undefined) {
|
|
@@ -748,7 +1046,6 @@ export class SessionRuntime {
|
|
|
748
1046
|
}
|
|
749
1047
|
await this.#activate({
|
|
750
1048
|
handle,
|
|
751
|
-
selection: ref,
|
|
752
1049
|
contextWindow: undefined,
|
|
753
1050
|
reasoningEffort: undefined,
|
|
754
1051
|
agentPreset: configuration.agentPreset,
|
|
@@ -764,12 +1061,11 @@ export class SessionRuntime {
|
|
|
764
1061
|
return;
|
|
765
1062
|
}
|
|
766
1063
|
const selection = this.selection(agent);
|
|
767
|
-
const ref = { current: selection, assembled: undefined };
|
|
768
1064
|
const handle = await this.#ctx.agents.resume({
|
|
769
1065
|
resumeSessionId: SessionId(id),
|
|
770
1066
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
771
1067
|
signal,
|
|
772
|
-
setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent
|
|
1068
|
+
setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent); },
|
|
773
1069
|
});
|
|
774
1070
|
try {
|
|
775
1071
|
this.#ctx.sessionTitle.rename(handle.agent.session, title);
|
|
@@ -956,7 +1252,6 @@ export class SessionRuntime {
|
|
|
956
1252
|
}));
|
|
957
1253
|
this.assertActive(agent);
|
|
958
1254
|
const selection = this.selection(agent);
|
|
959
|
-
const ref = { current: selection, assembled: undefined };
|
|
960
1255
|
const active = this.#requiredActive();
|
|
961
1256
|
let configured;
|
|
962
1257
|
const handle = await this.#ctx.agents.create({
|
|
@@ -971,7 +1266,12 @@ export class SessionRuntime {
|
|
|
971
1266
|
inheritedEventCount: SessionLogOffset(selected.branchIndex),
|
|
972
1267
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
973
1268
|
signal,
|
|
974
|
-
setup: async (agentCtx, agent) => {
|
|
1269
|
+
setup: async (agentCtx, agent) => {
|
|
1270
|
+
// A pre-turn prefix includes admission but excludes the later inbox claim.
|
|
1271
|
+
// Cancel that inherited work durably before publishing the editable fork.
|
|
1272
|
+
agent.inbox.clear();
|
|
1273
|
+
configured = await setupAgentContext(agentCtx, agent);
|
|
1274
|
+
},
|
|
975
1275
|
});
|
|
976
1276
|
try {
|
|
977
1277
|
this.assertActive(agent);
|
|
@@ -987,7 +1287,6 @@ export class SessionRuntime {
|
|
|
987
1287
|
}
|
|
988
1288
|
await this.#activate({
|
|
989
1289
|
handle,
|
|
990
|
-
selection: ref,
|
|
991
1290
|
contextWindow: undefined,
|
|
992
1291
|
reasoningEffort: undefined,
|
|
993
1292
|
agentPreset: configuration.agentPreset,
|
|
@@ -1095,34 +1394,41 @@ export class SessionRuntime {
|
|
|
1095
1394
|
/**
|
|
1096
1395
|
* Rebuild the argument vocabularies the composer completes.
|
|
1097
1396
|
*
|
|
1098
|
-
* A snapshot, because completion runs per keystroke
|
|
1099
|
-
* `/resume <id>`
|
|
1397
|
+
* A snapshot, because completion runs per keystroke. `/model provider/model`
|
|
1398
|
+
* and `/resume <id>` are names that already exist in
|
|
1100
1399
|
* this process, so the reader types toward them instead of opening a picker.
|
|
1101
1400
|
*/
|
|
1102
1401
|
async #refreshArgumentOptions() {
|
|
1103
|
-
const
|
|
1402
|
+
const epoch = ++this.#argumentEpoch;
|
|
1104
1403
|
const llm = this.#ctx.get('llm');
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1404
|
+
const providers = llm?.listProviders() ?? [];
|
|
1405
|
+
const previous = this.#argumentOptions.get('model') ?? [];
|
|
1406
|
+
const groups = new Map(providers.map(provider => [provider.id, previous.filter(model => model.value.startsWith(`${provider.id}/`))]));
|
|
1407
|
+
const options = new Map([['resume', sessionArgumentOptions(this.#recent)]]);
|
|
1408
|
+
const publish = () => {
|
|
1409
|
+
if (this.#disposed || epoch !== this.#argumentEpoch)
|
|
1410
|
+
return;
|
|
1411
|
+
options.set('model', providers.flatMap(provider => groups.get(provider.id) ?? []));
|
|
1412
|
+
this.#argumentOptions = options;
|
|
1413
|
+
this.#pushCommands();
|
|
1414
|
+
};
|
|
1415
|
+
publish();
|
|
1416
|
+
await Promise.all(providers.map(async (provider) => {
|
|
1417
|
+
try {
|
|
1418
|
+
const models = await llm?.listModels(provider.id) ?? [];
|
|
1419
|
+
if (this.#disposed || epoch !== this.#argumentEpoch)
|
|
1420
|
+
return;
|
|
1421
|
+
groups.set(provider.id, models.map(model => ({
|
|
1422
|
+
value: `${provider.id}/${model.id}`,
|
|
1423
|
+
...(model.name === undefined ? {} : { description: model.name }),
|
|
1424
|
+
})));
|
|
1116
1425
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
})));
|
|
1124
|
-
this.#argumentOptions = options;
|
|
1125
|
-
this.#pushCommands();
|
|
1426
|
+
catch {
|
|
1427
|
+
// Preserve the last successful catalog. The model picker owns diagnostics.
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
publish();
|
|
1431
|
+
}));
|
|
1126
1432
|
}
|
|
1127
1433
|
async refreshRecent() {
|
|
1128
1434
|
// Coalesce concurrent callers: the recent list is one snapshot, and a
|
|
@@ -1140,15 +1446,18 @@ export class SessionRuntime {
|
|
|
1140
1446
|
break;
|
|
1141
1447
|
this.#refreshDirty = false;
|
|
1142
1448
|
}
|
|
1143
|
-
})().finally(() => {
|
|
1449
|
+
})().finally(() => {
|
|
1450
|
+
this.#refreshInFlight = null;
|
|
1451
|
+
// Completion must read the newly published rows, not the previous
|
|
1452
|
+
// directory's snapshot while persistence.list() is still pending.
|
|
1453
|
+
queueMicrotask(() => { if (!this.#disposed)
|
|
1454
|
+
void this.#refreshArgumentOptions().catch(() => { }); });
|
|
1455
|
+
});
|
|
1144
1456
|
this.#refreshInFlight = run;
|
|
1145
1457
|
return run;
|
|
1146
1458
|
}
|
|
1147
1459
|
async #refreshRecentNow() {
|
|
1148
1460
|
this.#archivedSessions = new Set(readSessionLibrary(this.#libraryPath).archived);
|
|
1149
|
-
// The recent list is what `/resume` completes from, so a refresh of one is a
|
|
1150
|
-
// refresh of the other. Deferred to avoid recursing through #pushCommands.
|
|
1151
|
-
queueMicrotask(() => { void this.#refreshArgumentOptions().catch(() => { }); });
|
|
1152
1461
|
const persistence = this.#ctx.get('sessionPersistence');
|
|
1153
1462
|
if (persistence === undefined) {
|
|
1154
1463
|
this.#recent = [];
|
|
@@ -1157,6 +1466,8 @@ export class SessionRuntime {
|
|
|
1157
1466
|
}
|
|
1158
1467
|
const snapshots = (await persistence.list()).filter(snapshot => snapshot.header.origin !== 'subagent')
|
|
1159
1468
|
.sort((left, right) => right.header.createdAt - left.header.createdAt);
|
|
1469
|
+
const agent = this.agent;
|
|
1470
|
+
const cwd = agent === undefined ? process.cwd() : currentWorkingDirectory(this.#ctx, agent.session);
|
|
1160
1471
|
// A stored row stays valid while its revision holds, so a cold process
|
|
1161
1472
|
// renders labels for unchanged sessions without reading their logs; only a
|
|
1162
1473
|
// new or changed session pays a read.
|
|
@@ -1176,6 +1487,10 @@ export class SessionRuntime {
|
|
|
1176
1487
|
}
|
|
1177
1488
|
for (const snapshot of snapshots) {
|
|
1178
1489
|
const header = snapshot.header;
|
|
1490
|
+
// Scope before opening logs or consulting shared row caches. Other
|
|
1491
|
+
// directories remain available through an explicit session ID.
|
|
1492
|
+
if (header.cwd !== cwd)
|
|
1493
|
+
continue;
|
|
1179
1494
|
const cached = this.#recentCache.get(header.id);
|
|
1180
1495
|
if (snapshot.formatStatus === 'migration-required') {
|
|
1181
1496
|
// Do not open the log merely to browse it. A previous label is a hint;
|
|
@@ -1220,10 +1535,15 @@ export class SessionRuntime {
|
|
|
1220
1535
|
}
|
|
1221
1536
|
async dispose() {
|
|
1222
1537
|
this.#queueAbort?.abort(new Error('session runtime disposed'));
|
|
1538
|
+
this.#workflowAbort?.abort();
|
|
1539
|
+
for (const abort of this.#descendantApprovals.keys())
|
|
1540
|
+
abort.abort();
|
|
1223
1541
|
if (this.#disposed)
|
|
1224
1542
|
return;
|
|
1225
1543
|
this.#disposed = true;
|
|
1544
|
+
this.#activeInteraction.abort();
|
|
1226
1545
|
this.#activationEpoch += 1;
|
|
1546
|
+
this.#cancelInspectDeliveries();
|
|
1227
1547
|
this.#inspectedId = undefined;
|
|
1228
1548
|
this.#tracker.reset();
|
|
1229
1549
|
this.#tui.setInspectedSubagent(undefined);
|
|
@@ -1243,15 +1563,14 @@ export class SessionRuntime {
|
|
|
1243
1563
|
this.#releasedHandles.add(handle);
|
|
1244
1564
|
await this.#disposeHandles(handles);
|
|
1245
1565
|
}
|
|
1246
|
-
async #create(selection) {
|
|
1247
|
-
const
|
|
1248
|
-
const preset = await this.#ctx.agentPresets.resolve();
|
|
1566
|
+
async #create(selection, presetId) {
|
|
1567
|
+
const preset = await this.#ctx.agentPresets.resolve(presetId);
|
|
1249
1568
|
let configured;
|
|
1250
1569
|
const handle = await this.#ctx.agents.create({
|
|
1251
1570
|
sessionId: SessionId('session-' + randomUUID()),
|
|
1252
1571
|
meta: { cwd: process.cwd(), agentPreset: preset.id },
|
|
1253
1572
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
1254
|
-
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent
|
|
1573
|
+
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
|
|
1255
1574
|
});
|
|
1256
1575
|
const configuration = configured;
|
|
1257
1576
|
if (configuration === undefined) {
|
|
@@ -1260,7 +1579,6 @@ export class SessionRuntime {
|
|
|
1260
1579
|
}
|
|
1261
1580
|
return {
|
|
1262
1581
|
handle,
|
|
1263
|
-
selection: ref,
|
|
1264
1582
|
contextWindow: undefined,
|
|
1265
1583
|
reasoningEffort: undefined,
|
|
1266
1584
|
agentPreset: configuration.agentPreset,
|
|
@@ -1305,6 +1623,9 @@ export class SessionRuntime {
|
|
|
1305
1623
|
const previous = this.#active;
|
|
1306
1624
|
const epoch = ++this.#activationEpoch;
|
|
1307
1625
|
this.#queueAbort?.abort(new Error('active session changed'));
|
|
1626
|
+
this.#workflowAbort?.abort();
|
|
1627
|
+
this.#activeInteraction.abort();
|
|
1628
|
+
this.#activeInteraction = new AbortController();
|
|
1308
1629
|
this.#active = next;
|
|
1309
1630
|
void this.#claimWorkspace(next);
|
|
1310
1631
|
try {
|
|
@@ -1338,6 +1659,8 @@ export class SessionRuntime {
|
|
|
1338
1659
|
// and the activation failure remains the actionable error.
|
|
1339
1660
|
try {
|
|
1340
1661
|
if (this.#activationEpoch === epoch) {
|
|
1662
|
+
this.#activeInteraction.abort();
|
|
1663
|
+
this.#activeInteraction = new AbortController();
|
|
1341
1664
|
this.#active = previous;
|
|
1342
1665
|
if (previous !== undefined) {
|
|
1343
1666
|
try {
|
|
@@ -1370,8 +1693,12 @@ export class SessionRuntime {
|
|
|
1370
1693
|
}
|
|
1371
1694
|
/** Show one active session on the terminal: transcript, tools, controls, model. */
|
|
1372
1695
|
#presentAgent(active, reason = 'open') {
|
|
1696
|
+
for (const [abort, root] of this.#descendantApprovals)
|
|
1697
|
+
if (root !== active.handle.agent)
|
|
1698
|
+
abort.abort();
|
|
1373
1699
|
this.#workflows.reset(active.handle.agent.session.snapshotEvents());
|
|
1374
1700
|
const agent = active.handle.agent;
|
|
1701
|
+
this.#cancelInspectDeliveries();
|
|
1375
1702
|
this.#inspectedId = undefined;
|
|
1376
1703
|
this.#tui.setInspectedSubagent(undefined);
|
|
1377
1704
|
this.#tui.setStatus(agent.status);
|
|
@@ -1410,8 +1737,9 @@ export class SessionRuntime {
|
|
|
1410
1737
|
...(options === undefined ? {} : { complete: (prefix) => matchArgumentOptions(options, prefix) }),
|
|
1411
1738
|
};
|
|
1412
1739
|
});
|
|
1413
|
-
const names = new Set(commands.map(command => command.name)
|
|
1414
|
-
|
|
1740
|
+
const names = new Set([...commands.map(command => command.name),
|
|
1741
|
+
...BUILTIN_SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])])]);
|
|
1742
|
+
for (const skill of userSkillCommands(this.#skills, names)) {
|
|
1415
1743
|
if (names.has(skill.name))
|
|
1416
1744
|
continue;
|
|
1417
1745
|
commands.push(skill);
|
|
@@ -1433,14 +1761,14 @@ export class SessionRuntime {
|
|
|
1433
1761
|
const agent = this.agent;
|
|
1434
1762
|
const skills = this.#ctx.get('skills');
|
|
1435
1763
|
if (agent === undefined || skills === undefined) {
|
|
1436
|
-
this.#
|
|
1764
|
+
this.#skills = [];
|
|
1437
1765
|
}
|
|
1438
1766
|
else {
|
|
1439
1767
|
const cwd = currentWorkingDirectory(this.#ctx, agent.session);
|
|
1440
1768
|
const list = await skills.list({ cwd, scope: agent, signal });
|
|
1441
1769
|
if (this.#disposed || this.agent !== agent || currentWorkingDirectory(this.#ctx, agent.session) !== cwd)
|
|
1442
1770
|
return;
|
|
1443
|
-
this.#
|
|
1771
|
+
this.#skills = list;
|
|
1444
1772
|
}
|
|
1445
1773
|
this.#pushCommands();
|
|
1446
1774
|
}
|
|
@@ -1487,6 +1815,7 @@ export class SessionRuntime {
|
|
|
1487
1815
|
id,
|
|
1488
1816
|
label: view?.label ?? id,
|
|
1489
1817
|
phase: view?.phase ?? fallbackPhase,
|
|
1818
|
+
needsApproval: view?.needsApproval === true,
|
|
1490
1819
|
...(mode === undefined ? {} : { mode }),
|
|
1491
1820
|
writable: isSteerableSubagent(mode),
|
|
1492
1821
|
};
|
|
@@ -1512,6 +1841,7 @@ export class SessionRuntime {
|
|
|
1512
1841
|
#replaceTranscript(agent, reason = 'refresh') {
|
|
1513
1842
|
this.#tui.setWorkingDirectory?.(currentWorkingDirectory(this.#ctx, agent.session));
|
|
1514
1843
|
const events = agent.session.snapshotEvents();
|
|
1844
|
+
this.#tui.pendingInputs?.reconcile?.(agent.id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
|
|
1515
1845
|
this.#tracker.observeCatalog(agent.session, events);
|
|
1516
1846
|
this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
|
|
1517
1847
|
this.#replayLivePrefix(agent.session.id);
|
|
@@ -1525,10 +1855,18 @@ export class SessionRuntime {
|
|
|
1525
1855
|
if (agent !== undefined)
|
|
1526
1856
|
this.#replaceTranscript(agent);
|
|
1527
1857
|
}
|
|
1858
|
+
#cancelInspectDeliveries() {
|
|
1859
|
+
for (const delivery of this.#inspectDeliveries)
|
|
1860
|
+
delivery.abort();
|
|
1861
|
+
this.#inspectDeliveries.clear();
|
|
1862
|
+
}
|
|
1528
1863
|
#closeInspect() {
|
|
1529
1864
|
if (this.#inspectedId === undefined)
|
|
1530
1865
|
return;
|
|
1531
1866
|
this.#inspectEpoch += 1;
|
|
1867
|
+
this.#queueAbort?.abort(new Error('viewed session changed'));
|
|
1868
|
+
this.#workflowAbort?.abort();
|
|
1869
|
+
this.#cancelInspectDeliveries();
|
|
1532
1870
|
this.#inspectedId = undefined;
|
|
1533
1871
|
this.#tui.setInspectedSubagent(undefined);
|
|
1534
1872
|
const agent = this.#active?.handle.agent;
|
|
@@ -1573,6 +1911,12 @@ export class SessionRuntime {
|
|
|
1573
1911
|
}
|
|
1574
1912
|
if (request !== this.#inspectEpoch)
|
|
1575
1913
|
return;
|
|
1914
|
+
if (this.#inspectedId !== id) {
|
|
1915
|
+
this.#queueAbort?.abort(new Error('viewed session changed'));
|
|
1916
|
+
this.#workflowAbort?.abort();
|
|
1917
|
+
this.#cancelInspectDeliveries();
|
|
1918
|
+
}
|
|
1919
|
+
this.#tui.pendingInputs?.reconcile?.(id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
|
|
1576
1920
|
this.#inspectedId = id;
|
|
1577
1921
|
if (cwd !== undefined)
|
|
1578
1922
|
this.#tui.setWorkingDirectory?.(cwd);
|
|
@@ -1586,12 +1930,12 @@ export class SessionRuntime {
|
|
|
1586
1930
|
const childId = this.#inspectedId;
|
|
1587
1931
|
const root = this.#active?.handle.agent;
|
|
1588
1932
|
if (childId === undefined || root === undefined) {
|
|
1589
|
-
this.#tui.restoreInput(submission);
|
|
1933
|
+
this.#tui.restoreInput(submission, childId);
|
|
1590
1934
|
return;
|
|
1591
1935
|
}
|
|
1592
1936
|
const view = this.#inspectView(childId);
|
|
1593
1937
|
if (!view.writable) {
|
|
1594
|
-
this.#tui.restoreInput(submission);
|
|
1938
|
+
this.#tui.restoreInput(submission, childId);
|
|
1595
1939
|
this.#tui.notice('This subagent is a completed run and cannot take more messages.');
|
|
1596
1940
|
return;
|
|
1597
1941
|
}
|
|
@@ -1603,34 +1947,60 @@ export class SessionRuntime {
|
|
|
1603
1947
|
: this.#ctx.get('agents')?.get(SessionId(parentId));
|
|
1604
1948
|
const subagents = this.#ctx.get('subagents');
|
|
1605
1949
|
if (parent === undefined || subagents === undefined) {
|
|
1606
|
-
this.#tui.restoreInput(submission);
|
|
1950
|
+
this.#tui.restoreInput(submission, childId);
|
|
1607
1951
|
this.#tui.notice(parent === undefined
|
|
1608
1952
|
? 'The parent of this subagent is not live, so it cannot take a follow-up.'
|
|
1609
1953
|
: 'Subagent follow-up is unavailable in this composition.');
|
|
1610
1954
|
return;
|
|
1611
1955
|
}
|
|
1956
|
+
const previous = this.#inspectDeliveryTail;
|
|
1957
|
+
let release = () => { };
|
|
1958
|
+
this.#inspectDeliveryTail = new Promise(resolve => { release = resolve; });
|
|
1959
|
+
const delivery = new AbortController();
|
|
1960
|
+
this.#inspectDeliveries.add(delivery);
|
|
1961
|
+
let cancelUpload = () => { };
|
|
1962
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
1963
|
+
cancelUpload = () => { reject(delivery.signal.reason); };
|
|
1964
|
+
delivery.signal.addEventListener('abort', cancelUpload, { once: true });
|
|
1965
|
+
});
|
|
1612
1966
|
try {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1967
|
+
// Recalled messages can include attachments; admission must retain submission order.
|
|
1968
|
+
await Promise.race([previous, cancelled]);
|
|
1969
|
+
delivery.signal.throwIfAborted();
|
|
1970
|
+
// Attachment storage need not support cancellation; release the draft now
|
|
1971
|
+
// and ignore a later upload result before crossing Harness admission.
|
|
1972
|
+
const message = await Promise.race([createSubmissionMessage(submission, this.#ctx.get('attachments')), cancelled]);
|
|
1973
|
+
delivery.signal.removeEventListener('abort', cancelUpload);
|
|
1974
|
+
delivery.signal.throwIfAborted();
|
|
1975
|
+
if (this.#inspectedId !== childId || this.#active?.handle.agent !== root)
|
|
1976
|
+
throw new Error('The viewed session changed before delivery.');
|
|
1977
|
+
const send = submission.delivery === 'steer' ? steerHostSubagentPrompt : queueHostSubagentPrompt;
|
|
1978
|
+
const messageId = await send(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, delivery.signal);
|
|
1979
|
+
this.#tui.pendingInputs?.prepare?.(submission, messageId);
|
|
1980
|
+
this.#tui.pendingInputs?.acknowledge?.(submission);
|
|
1619
1981
|
}
|
|
1620
1982
|
catch (error) {
|
|
1621
|
-
this.#tui.restoreInput(submission);
|
|
1622
|
-
|
|
1983
|
+
this.#tui.restoreInput(submission, childId);
|
|
1984
|
+
if (!delivery.signal.aborted)
|
|
1985
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
1986
|
+
}
|
|
1987
|
+
finally {
|
|
1988
|
+
delivery.signal.removeEventListener('abort', cancelUpload);
|
|
1989
|
+
this.#inspectDeliveries.delete(delivery);
|
|
1990
|
+
release();
|
|
1623
1991
|
}
|
|
1624
1992
|
}
|
|
1625
1993
|
#sessionControls(active, projection) {
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1994
|
+
const liveGoal = this.#ctx.get('goals')?.get(active.handle.agent);
|
|
1995
|
+
const controls = sessionControls(projection, this.#ctx.get('planMode')?.get(active.handle.agent));
|
|
1996
|
+
if (controls.goal !== undefined && liveGoal !== undefined && liveGoal.id === projection?.goal?.goal.id && liveGoal.revision === projection?.goal?.goal.revision) {
|
|
1997
|
+
controls.goal.activation = liveGoal.activation;
|
|
1998
|
+
}
|
|
1999
|
+
const failure = active.handle.agent.status === 'idle' ? continuableFailure(active.handle.agent.session.snapshotEvents()) : undefined;
|
|
1628
2000
|
return {
|
|
1629
2001
|
...controls,
|
|
2002
|
+
...(failure === undefined ? {} : { continuation: failure }),
|
|
1630
2003
|
agentPreset: active.agentPreset,
|
|
1631
|
-
...(livePlan === undefined
|
|
1632
|
-
? {}
|
|
1633
|
-
: { plan: { active: livePlan.active, pending: livePlan.pending !== undefined } }),
|
|
1634
2004
|
};
|
|
1635
2005
|
}
|
|
1636
2006
|
/** Read one client-visible snapshot. Host-only projection state is never consulted. */
|
|
@@ -1672,6 +2042,7 @@ export function matchArgumentOptions(options, prefix) {
|
|
|
1672
2042
|
if (lower === '')
|
|
1673
2043
|
return options;
|
|
1674
2044
|
return options.filter(option => option.value.toLowerCase().includes(lower)
|
|
2045
|
+
|| (option.label ?? '').toLowerCase().includes(lower)
|
|
1675
2046
|
|| (option.description ?? '').toLowerCase().includes(lower));
|
|
1676
2047
|
}
|
|
1677
2048
|
//# sourceMappingURL=session-controller.js.map
|