@agi-fans/dsh-tui 0.22.0 → 0.23.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 (279) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +199 -1
  3. package/lib/chrome/box.d.ts +1 -18
  4. package/lib/chrome/box.js +2 -45
  5. package/lib/chrome/box.js.map +1 -1
  6. package/lib/chrome/code-highlight.js +4 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +0 -1
  9. package/lib/chrome/command-output.js +11 -5
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/goal-bar.js +1 -1
  12. package/lib/chrome/goal-bar.js.map +1 -1
  13. package/lib/chrome/main-screen-renderer.js +4 -3
  14. package/lib/chrome/main-screen-renderer.js.map +1 -1
  15. package/lib/chrome/markdown.d.ts +3 -3
  16. package/lib/chrome/markdown.js +85 -36
  17. package/lib/chrome/markdown.js.map +1 -1
  18. package/lib/chrome/md-style.d.ts +5 -2
  19. package/lib/chrome/md-style.js +13 -4
  20. package/lib/chrome/md-style.js.map +1 -1
  21. package/lib/chrome/renderer.d.ts +37 -1
  22. package/lib/chrome/renderer.js.map +1 -1
  23. package/lib/chrome/status-line.d.ts +3 -3
  24. package/lib/chrome/status-line.js +5 -5
  25. package/lib/chrome/status-line.js.map +1 -1
  26. package/lib/chrome/theme.d.ts +8 -3
  27. package/lib/chrome/theme.js +51 -153
  28. package/lib/chrome/theme.js.map +1 -1
  29. package/lib/commands/auth-account.d.ts +9 -0
  30. package/lib/commands/auth-account.js +202 -0
  31. package/lib/commands/auth-account.js.map +1 -0
  32. package/lib/commands/auth.js +238 -414
  33. package/lib/commands/auth.js.map +1 -1
  34. package/lib/commands/diff.js +22 -16
  35. package/lib/commands/diff.js.map +1 -1
  36. package/lib/commands/export.d.ts +0 -1
  37. package/lib/commands/export.js +97 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +2 -8
  40. package/lib/commands/files.js.map +1 -1
  41. package/lib/commands/integrations.d.ts +12 -1
  42. package/lib/commands/integrations.js +120 -23
  43. package/lib/commands/integrations.js.map +1 -1
  44. package/lib/commands/jobs-browser.d.ts +3 -0
  45. package/lib/commands/jobs-browser.js +152 -0
  46. package/lib/commands/jobs-browser.js.map +1 -0
  47. package/lib/commands/jobs.d.ts +3 -14
  48. package/lib/commands/jobs.js +26 -18
  49. package/lib/commands/jobs.js.map +1 -1
  50. package/lib/commands/model.d.ts +3 -0
  51. package/lib/commands/model.js +197 -120
  52. package/lib/commands/model.js.map +1 -1
  53. package/lib/commands/open-external.d.ts +2 -0
  54. package/lib/commands/open-external.js +19 -0
  55. package/lib/commands/open-external.js.map +1 -0
  56. package/lib/commands/permission.js +65 -54
  57. package/lib/commands/permission.js.map +1 -1
  58. package/lib/commands/plugins.js +381 -139
  59. package/lib/commands/plugins.js.map +1 -1
  60. package/lib/commands/schedule-catalog.d.ts +12 -0
  61. package/lib/commands/schedule-catalog.js +41 -0
  62. package/lib/commands/schedule-catalog.js.map +1 -0
  63. package/lib/commands/schedule-history.d.ts +4 -0
  64. package/lib/commands/schedule-history.js +62 -0
  65. package/lib/commands/schedule-history.js.map +1 -0
  66. package/lib/commands/schedule.d.ts +12 -15
  67. package/lib/commands/schedule.js +220 -187
  68. package/lib/commands/schedule.js.map +1 -1
  69. package/lib/commands/session-configuration.d.ts +3 -0
  70. package/lib/commands/session-configuration.js +62 -28
  71. package/lib/commands/session-configuration.js.map +1 -1
  72. package/lib/commands/session.js +111 -66
  73. package/lib/commands/session.js.map +1 -1
  74. package/lib/commands/terminal.js +14 -8
  75. package/lib/commands/terminal.js.map +1 -1
  76. package/lib/definition.d.ts +112 -15
  77. package/lib/index.d.ts +1 -0
  78. package/lib/index.js +1 -0
  79. package/lib/index.js.map +1 -1
  80. package/lib/input/actions.d.ts +1 -1
  81. package/lib/input/actions.js +5 -2
  82. package/lib/input/actions.js.map +1 -1
  83. package/lib/input/composer-files.d.ts +2 -0
  84. package/lib/input/composer-files.js +13 -3
  85. package/lib/input/composer-files.js.map +1 -1
  86. package/lib/input/image-paste.d.ts +5 -12
  87. package/lib/input/image-paste.js +32 -28
  88. package/lib/input/image-paste.js.map +1 -1
  89. package/lib/input/key-test.d.ts +5 -0
  90. package/lib/input/key-test.js +19 -0
  91. package/lib/input/key-test.js.map +1 -0
  92. package/lib/input/keys.d.ts +2 -0
  93. package/lib/input/keys.js +4 -1
  94. package/lib/input/keys.js.map +1 -1
  95. package/lib/locale/index.d.ts +6 -0
  96. package/lib/locale/index.js +32 -0
  97. package/lib/locale/index.js.map +1 -0
  98. package/lib/locale/prompt.d.ts +4 -0
  99. package/lib/locale/prompt.js +24 -0
  100. package/lib/locale/prompt.js.map +1 -0
  101. package/lib/locale/zh.d.ts +1114 -0
  102. package/lib/locale/zh.js +1115 -0
  103. package/lib/locale/zh.js.map +1 -0
  104. package/lib/prompt-intents.d.ts +4 -0
  105. package/lib/prompt-intents.js +4 -0
  106. package/lib/prompt-intents.js.map +1 -1
  107. package/lib/runtime/account-settings.d.ts +18 -0
  108. package/lib/runtime/account-settings.js +241 -0
  109. package/lib/runtime/account-settings.js.map +1 -0
  110. package/lib/runtime/agent-profile.d.ts +3 -3
  111. package/lib/runtime/agent-profile.js +4 -4
  112. package/lib/runtime/agent-profile.js.map +1 -1
  113. package/lib/runtime/btw-policy.js +2 -1
  114. package/lib/runtime/btw-policy.js.map +1 -1
  115. package/lib/runtime/btw.js +1 -2
  116. package/lib/runtime/btw.js.map +1 -1
  117. package/lib/runtime/composer-images.d.ts +2 -2
  118. package/lib/runtime/composer-images.js +4 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.js +27 -1
  121. package/lib/runtime/composer-shell.js.map +1 -1
  122. package/lib/runtime/file-browser.d.ts +19 -0
  123. package/lib/runtime/file-browser.js +291 -0
  124. package/lib/runtime/file-browser.js.map +1 -0
  125. package/lib/runtime/file-review.d.ts +6 -9
  126. package/lib/runtime/file-review.js +91 -7
  127. package/lib/runtime/file-review.js.map +1 -1
  128. package/lib/runtime/help-reader.d.ts +12 -0
  129. package/lib/runtime/help-reader.js +50 -0
  130. package/lib/runtime/help-reader.js.map +1 -0
  131. package/lib/runtime/human-interaction.js +1 -1
  132. package/lib/runtime/human-interaction.js.map +1 -1
  133. package/lib/runtime/model-catalog-editor.d.ts +12 -0
  134. package/lib/runtime/model-catalog-editor.js +215 -0
  135. package/lib/runtime/model-catalog-editor.js.map +1 -0
  136. package/lib/runtime/model-settings.d.ts +11 -0
  137. package/lib/runtime/model-settings.js +404 -0
  138. package/lib/runtime/model-settings.js.map +1 -0
  139. package/lib/runtime/plugin-settings.d.ts +4 -1
  140. package/lib/runtime/plugin-settings.js +164 -88
  141. package/lib/runtime/plugin-settings.js.map +1 -1
  142. package/lib/runtime/prompt-browser.d.ts +9 -0
  143. package/lib/runtime/prompt-browser.js +19 -0
  144. package/lib/runtime/prompt-browser.js.map +1 -0
  145. package/lib/runtime/provider-local.d.ts +27 -27
  146. package/lib/runtime/provider-local.js +1264 -308
  147. package/lib/runtime/provider-local.js.map +1 -1
  148. package/lib/runtime/reference-reader.d.ts +15 -0
  149. package/lib/runtime/reference-reader.js +78 -0
  150. package/lib/runtime/reference-reader.js.map +1 -0
  151. package/lib/runtime/runner.js +38 -29
  152. package/lib/runtime/runner.js.map +1 -1
  153. package/lib/runtime/session-runtime.js +1 -1
  154. package/lib/runtime/session-runtime.js.map +1 -1
  155. package/lib/runtime/settings-draft.d.ts +21 -0
  156. package/lib/runtime/settings-draft.js +166 -0
  157. package/lib/runtime/settings-draft.js.map +1 -0
  158. package/lib/runtime/settings-preferences.js +1 -1
  159. package/lib/runtime/settings-preferences.js.map +1 -1
  160. package/lib/runtime/startup-notices.d.ts +0 -1
  161. package/lib/runtime/startup-notices.js +35 -6
  162. package/lib/runtime/startup-notices.js.map +1 -1
  163. package/lib/runtime/subagent-model-selection.js +2 -1
  164. package/lib/runtime/subagent-model-selection.js.map +1 -1
  165. package/lib/runtime/subagent-settings.d.ts +14 -0
  166. package/lib/runtime/subagent-settings.js +288 -0
  167. package/lib/runtime/subagent-settings.js.map +1 -0
  168. package/lib/runtime/terminal-notifications.d.ts +3 -0
  169. package/lib/runtime/terminal-notifications.js +13 -8
  170. package/lib/runtime/terminal-notifications.js.map +1 -1
  171. package/lib/runtime/terminal-probe.d.ts +2 -0
  172. package/lib/runtime/terminal-probe.js +9 -0
  173. package/lib/runtime/terminal-probe.js.map +1 -1
  174. package/lib/runtime/tool-presentation.js.map +1 -1
  175. package/lib/runtime/tool-reader.d.ts +5 -0
  176. package/lib/runtime/tool-reader.js +27 -0
  177. package/lib/runtime/tool-reader.js.map +1 -0
  178. package/lib/runtime/web-settings.d.ts +14 -0
  179. package/lib/runtime/web-settings.js +339 -0
  180. package/lib/runtime/web-settings.js.map +1 -0
  181. package/lib/session/command-attachments.d.ts +17 -0
  182. package/lib/session/command-attachments.js +50 -0
  183. package/lib/session/command-attachments.js.map +1 -0
  184. package/lib/session/context-diagnostics.d.ts +16 -0
  185. package/lib/session/context-diagnostics.js +15 -0
  186. package/lib/session/context-diagnostics.js.map +1 -1
  187. package/lib/session/external-agents.d.ts +3 -1
  188. package/lib/session/external-agents.js +35 -10
  189. package/lib/session/external-agents.js.map +1 -1
  190. package/lib/session/generation-continuation.d.ts +11 -0
  191. package/lib/session/generation-continuation.js +34 -0
  192. package/lib/session/generation-continuation.js.map +1 -0
  193. package/lib/session/goal-controls.d.ts +5 -0
  194. package/lib/session/goal-controls.js +81 -0
  195. package/lib/session/goal-controls.js.map +1 -0
  196. package/lib/session/interaction-adapter.d.ts +7 -3
  197. package/lib/session/interaction-adapter.js +121 -49
  198. package/lib/session/interaction-adapter.js.map +1 -1
  199. package/lib/session/message-queue.d.ts +2 -0
  200. package/lib/session/message-queue.js +102 -17
  201. package/lib/session/message-queue.js.map +1 -1
  202. package/lib/session/pending-inputs.d.ts +30 -0
  203. package/lib/session/pending-inputs.js +183 -0
  204. package/lib/session/pending-inputs.js.map +1 -0
  205. package/lib/session/session-configuration.js +1 -1
  206. package/lib/session/session-configuration.js.map +1 -1
  207. package/lib/session/session-controller.d.ts +32 -13
  208. package/lib/session/session-controller.js +444 -98
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/subagent-roster.d.ts +1 -0
  211. package/lib/session/subagent-roster.js +16 -5
  212. package/lib/session/subagent-roster.js.map +1 -1
  213. package/lib/session/subagent-tracker.d.ts +2 -0
  214. package/lib/session/subagent-tracker.js +18 -0
  215. package/lib/session/subagent-tracker.js.map +1 -1
  216. package/lib/session/tui-settings.d.ts +9 -5
  217. package/lib/session/tui-settings.js +3 -1
  218. package/lib/session/tui-settings.js.map +1 -1
  219. package/lib/session/workflow-projection.d.ts +2 -1
  220. package/lib/session/workflow-projection.js +34 -4
  221. package/lib/session/workflow-projection.js.map +1 -1
  222. package/lib/views/autocomplete.d.ts +10 -7
  223. package/lib/views/autocomplete.js +44 -37
  224. package/lib/views/autocomplete.js.map +1 -1
  225. package/lib/views/context-overview.d.ts +4 -0
  226. package/lib/views/context-overview.js +43 -0
  227. package/lib/views/context-overview.js.map +1 -0
  228. package/lib/views/copy-targets.d.ts +6 -2
  229. package/lib/views/copy-targets.js +19 -14
  230. package/lib/views/copy-targets.js.map +1 -1
  231. package/lib/views/document-reader.d.ts +10 -1
  232. package/lib/views/document-reader.js +59 -5
  233. package/lib/views/document-reader.js.map +1 -1
  234. package/lib/views/event-views.js +29 -11
  235. package/lib/views/event-views.js.map +1 -1
  236. package/lib/views/file-actions.js +21 -0
  237. package/lib/views/file-actions.js.map +1 -1
  238. package/lib/views/file-browser.d.ts +98 -0
  239. package/lib/views/file-browser.js +120 -0
  240. package/lib/views/file-browser.js.map +1 -0
  241. package/lib/views/history-search.js +4 -4
  242. package/lib/views/history-search.js.map +1 -1
  243. package/lib/views/hotkey-format.d.ts +3 -2
  244. package/lib/views/hotkey-format.js +4 -3
  245. package/lib/views/hotkey-format.js.map +1 -1
  246. package/lib/views/hotkeys.js +10 -12
  247. package/lib/views/hotkeys.js.map +1 -1
  248. package/lib/views/list-view.js +16 -11
  249. package/lib/views/list-view.js.map +1 -1
  250. package/lib/views/prompt-selector.d.ts +23 -1
  251. package/lib/views/prompt-selector.js +123 -60
  252. package/lib/views/prompt-selector.js.map +1 -1
  253. package/lib/views/settings-list.d.ts +6 -1
  254. package/lib/views/settings-list.js +69 -46
  255. package/lib/views/settings-list.js.map +1 -1
  256. package/lib/views/tool-actions.d.ts +16 -0
  257. package/lib/views/tool-actions.js +29 -0
  258. package/lib/views/tool-actions.js.map +1 -0
  259. package/lib/views/trajectory-timeline.d.ts +5 -6
  260. package/lib/views/trajectory-timeline.js +25 -15
  261. package/lib/views/trajectory-timeline.js.map +1 -1
  262. package/lib/views/trajectory.d.ts +23 -2
  263. package/lib/views/trajectory.js +404 -186
  264. package/lib/views/trajectory.js.map +1 -1
  265. package/lib/views/transcript-render.d.ts +39 -15
  266. package/lib/views/transcript-render.js +406 -208
  267. package/lib/views/transcript-render.js.map +1 -1
  268. package/lib/views/transcript-types.d.ts +3 -2
  269. package/lib/views/transcript-types.js.map +1 -1
  270. package/lib/views/workflow.d.ts +16 -0
  271. package/lib/views/workflow.js +42 -0
  272. package/lib/views/workflow.js.map +1 -0
  273. package/package.json +61 -58
  274. package/lib/chrome/welcome-tips.d.ts +0 -23
  275. package/lib/chrome/welcome-tips.js +0 -24
  276. package/lib/chrome/welcome-tips.js.map +0 -1
  277. package/lib/session/steering.d.ts +0 -3
  278. package/lib/session/steering.js +0 -9
  279. 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 { isUserInvocable } from '@deepseek-ai/dsh-skill';
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";
@@ -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, selection) {
37
- const configured = await configureAgentContext(agentCtx, agent, selection);
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
- ...(projection?.plan === undefined ? {} : { plan: { ...projection.plan } }),
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: `skill:${skill.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
- const attachment = refs[segment.index];
333
- if (attachment !== undefined)
334
- content.push({ type: 'image', attachment });
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
- for (const attachment of submission.files ?? [])
344
- content.push({ type: 'file', attachment });
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,17 +386,36 @@ 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
- return { text, images, ...(files.length === 0 ? {} : { files }) };
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
- #skillCommands = [];
418
+ #skills = [];
392
419
  /**
393
420
  * Live argument vocabularies, keyed by command.
394
421
  *
@@ -403,6 +430,7 @@ export class SessionRuntime {
403
430
  #releasedHandles = new WeakSet();
404
431
  #disposed = false;
405
432
  #queueAbort;
433
+ #workflowAbort;
406
434
  #off = [];
407
435
  #tracker;
408
436
  #streamAttempts = new LiveAttemptTracker();
@@ -413,11 +441,21 @@ export class SessionRuntime {
413
441
  #refreshInFlight = null;
414
442
  #refreshDirty = false;
415
443
  #inspectEpoch = 0;
444
+ #inspectDeliveryTail = Promise.resolve();
445
+ #activeInteraction = new AbortController();
446
+ #pendingSends = new Map();
447
+ #inspectDeliveries = new Set();
448
+ #descendantApprovals = new Map();
416
449
  #activationEpoch = 0;
417
450
  #inspectedId;
418
451
  constructor(ctx, tui, options = {}) {
419
452
  this.#ctx = ctx;
420
453
  this.#tui = tui;
454
+ const commands = ctx.get('commands');
455
+ if (commands !== undefined) {
456
+ this.#commandAttachments = new CommandAttachments(commands);
457
+ this.#off.push(() => this.#commandAttachments?.dispose());
458
+ }
421
459
  this.#libraryPath = sessionLibraryPath(options.stateDir);
422
460
  this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
423
461
  this.#tracker = new SubagentTracker({
@@ -436,9 +474,24 @@ export class SessionRuntime {
436
474
  this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
437
475
  },
438
476
  });
439
- this.#off.push(tui.setSteerHandler(text => { steerAgent(this.agent, text); }));
477
+ const offGoal = tui.setGoalHandler?.(signal => openGoalControls(ctx, tui, this.#requiredAgent(), AbortSignal.any([signal, this.interactionSignal])));
478
+ if (offGoal !== undefined)
479
+ this.#off.push(offGoal);
440
480
  this.#off.push(tui.setQueueHandler(signal => this.openQueue(signal)));
481
+ const offContinue = tui.setContinueHandler?.(async (request) => {
482
+ const agent = this.#requiredAgent();
483
+ if (agent.id !== request.sessionId)
484
+ throw new Error('The continuation belongs to another session.');
485
+ await this.continueGeneration(agent, request.failureSeq);
486
+ });
487
+ if (offContinue !== undefined)
488
+ this.#off.push(offContinue);
441
489
  this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
490
+ const offWorkflow = tui.onInspectWorkflow?.(id => {
491
+ void this.openWorkflow(id).catch(error => { this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); });
492
+ });
493
+ if (offWorkflow !== undefined)
494
+ this.#off.push(offWorkflow);
442
495
  this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
443
496
  this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
444
497
  tui.setSessionSearch(async (query, signal) => {
@@ -489,6 +542,7 @@ export class SessionRuntime {
489
542
  this.#off.push(ctx.on('agent/status', (payload) => {
490
543
  if (payload.agent === this.#active?.handle.agent) {
491
544
  tui.setStatus(payload.status, { root: true });
545
+ this.#pushSessionInfo();
492
546
  return;
493
547
  }
494
548
  if (payload.agent.id === this.#inspectedId)
@@ -560,6 +614,10 @@ export class SessionRuntime {
560
614
  if (delta !== undefined)
561
615
  this.#forwardLiveDelta(payload.agent, delta);
562
616
  }, { global: true }));
617
+ this.#off.push(ctx.on('goal/activation-changed', payload => {
618
+ if (payload.sessionId === this.#active?.handle.agent.session.id)
619
+ this.#pushSessionInfo();
620
+ }));
563
621
  if (ctx.get('commands') !== undefined) {
564
622
  this.#off.push(ctx.on('commands/change', () => { this.#pushCommands(); }));
565
623
  }
@@ -585,6 +643,8 @@ export class SessionRuntime {
585
643
  }));
586
644
  }
587
645
  }
646
+ /** Presentation requests from a replaced root must not survive into its successor. */
647
+ get interactionSignal() { return this.#activeInteraction.signal; }
588
648
  get agent() {
589
649
  return this.#active?.handle.agent;
590
650
  }
@@ -597,10 +657,48 @@ export class SessionRuntime {
597
657
  return false;
598
658
  const root = this.#active?.handle.agent;
599
659
  if (root !== undefined) {
600
- this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
660
+ try {
661
+ this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
662
+ }
663
+ catch (error) {
664
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
665
+ }
601
666
  }
602
667
  return true;
603
668
  }
669
+ #isOwnedDescendant(agent, root) {
670
+ const agents = this.#ctx.get('agents');
671
+ if (agents?.get(agent.id) !== agent
672
+ || descendantDepth(agent.session, root.id, this.#lookupSession) === undefined)
673
+ return false;
674
+ // Durable lineage alone also matches an independently resumed runtime root.
675
+ let child = agent;
676
+ while (child !== root) {
677
+ const parentId = child.session.header.parentSession;
678
+ const parent = parentId === undefined ? undefined : agents.get(parentId);
679
+ if (parent === undefined || !agents.isOwnedBy(child.id, parent))
680
+ return false;
681
+ child = parent;
682
+ }
683
+ return true;
684
+ }
685
+ /** Accept only live descendants of the active root for shared approval prompts. */
686
+ beginSubagentApproval(agent) {
687
+ const root = this.agent;
688
+ if (root === undefined || !this.#isOwnedDescendant(agent, root))
689
+ return undefined;
690
+ this.#tracker.noteSession(agent.session);
691
+ const abort = new AbortController();
692
+ this.#descendantApprovals.set(abort, root);
693
+ const finish = this.#tracker.beginApprovalRequest(agent.id);
694
+ const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
695
+ abort.abort(); });
696
+ return { label: this.#inspectView(agent.id).label, signal: abort.signal, finish: () => {
697
+ off();
698
+ this.#descendantApprovals.delete(abort);
699
+ finish();
700
+ } };
701
+ }
604
702
  async start() {
605
703
  if (this.#started)
606
704
  return;
@@ -611,77 +709,266 @@ export class SessionRuntime {
611
709
  await this.refreshRecent();
612
710
  await this.#activate(await this.#create(defaults), 'initial');
613
711
  }
614
- /** Submit one human composer value; active turns retain it as a later follow-up. */
712
+ /** Admit a complete composer value through the selected Harness inbox entry. */
615
713
  async send(input, agent = this.#requiredAgent()) {
616
- this.assertActive(agent);
617
714
  const submission = typeof input === 'string' ? { text: input, images: [] } : input;
618
- const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
619
- this.assertActive(agent);
620
- agent.followup(message);
715
+ await this.#sendSubmission(submission, agent);
716
+ }
717
+ async #sendSubmission(submission, agent, signal) {
718
+ const assertDelivery = () => {
719
+ signal?.throwIfAborted();
720
+ this.assertActive(agent);
721
+ if (submission.delivery === 'steer' && agent.status !== 'running') {
722
+ throw new Error('Steering needs an active turn. Use Enter to send the next message.');
723
+ }
724
+ };
725
+ assertDelivery();
726
+ this.#pendingSends.set(agent, (this.#pendingSends.get(agent) ?? 0) + 1);
727
+ try {
728
+ const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
729
+ assertDelivery();
730
+ this.#tui.pendingInputs?.prepare?.(submission, message.id);
731
+ this.#admit(agent, message, submission.delivery);
732
+ }
733
+ finally {
734
+ const remaining = (this.#pendingSends.get(agent) ?? 1) - 1;
735
+ if (remaining === 0)
736
+ this.#pendingSends.delete(agent);
737
+ else
738
+ this.#pendingSends.set(agent, remaining);
739
+ }
621
740
  }
622
- async openQueue(signal, agent = this.#requiredAgent()) {
741
+ #admit(agent, message, delivery) {
742
+ const offset = agent.session.snapshotEvents().length;
743
+ try {
744
+ if (delivery === 'steer')
745
+ agent.steer(message);
746
+ else
747
+ agent.followup(message);
748
+ }
749
+ catch (error) {
750
+ // An event subscriber may throw after admission, even after the message was claimed.
751
+ // Only a pre-admission failure may return the submission to the composer.
752
+ const accepted = agent.session.snapshotEvents().slice(offset).some(event => event.type === 'agent/inbox/spliced' && event.data.inserted.some(item => item.id === message.id));
753
+ if (!accepted)
754
+ throw error;
755
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
756
+ }
757
+ }
758
+ #assertCanRepeat(agent, signal) {
759
+ signal?.throwIfAborted();
623
760
  this.assertActive(agent);
624
761
  if (this.#inspectedId !== undefined)
625
- throw new Error('Return to the parent session to manage queued messages.');
626
- if (this.#queueAbort !== undefined)
762
+ throw new Error('Return to the parent session to continue or resend a prompt.');
763
+ if (agent.status !== 'idle' || this.#pendingSends.has(agent))
764
+ throw new Error('Wait for the current work to finish before continuing or resending.');
765
+ if (agent.inbox.nextTurn.length > 0 || agent.inbox.nextStep.some(message => message.source.kind === 'user')
766
+ || (this.#tui.pendingInputs?.list().length ?? 0) > 0)
767
+ throw new Error('Resolve pending input in /queue before continuing or resending.');
768
+ if (this.#descendantApprovals.size > 0 || this.#ctx.get('sessionProjections')?.snapshot(agent.session).values.userQuestions?.active.some(question => question.state !== 'continued')) {
769
+ throw new Error('Answer the pending question or approval before continuing or resending.');
770
+ }
771
+ }
772
+ /** Wake an eligible failed generation with empty retry input; never duplicate the original prompt. */
773
+ async continueGeneration(agent = this.#requiredAgent(), expectedFailure, signal) {
774
+ this.#assertCanRepeat(agent, signal);
775
+ await agent.runMaintenance(async (maintenance) => {
776
+ maintenance.throwIfAborted();
777
+ this.#assertCanRepeat(agent, signal);
778
+ const failure = continuableFailure(agent.session.snapshotEvents());
779
+ if (failure === undefined || expectedFailure !== undefined && expectedFailure !== failure) {
780
+ throw new Error('No recoverable model failure is available to continue. Use /retry to resend the last prompt.');
781
+ }
782
+ this.#admit(agent, createUserMessage({ content: [], source: { kind: 'retry' } }));
783
+ });
784
+ }
785
+ /** Explicitly resend the complete latest human message with a fresh Harness identity. */
786
+ async retryLastPrompt(agent = this.#requiredAgent(), signal) {
787
+ this.#assertCanRepeat(agent, signal);
788
+ return agent.runMaintenance(async (maintenance) => {
789
+ maintenance.throwIfAborted();
790
+ this.#assertCanRepeat(agent, signal);
791
+ const previous = agent.session.snapshotEvents().findLast(event => event.type === 'user/message'
792
+ && event.data.source.kind === 'user' && event.data.content.length > 0);
793
+ if (previous?.type !== 'user/message')
794
+ return false;
795
+ this.#admit(agent, createUserMessage({ content: previous.data.content, source: { kind: 'user' } }));
796
+ return true;
797
+ });
798
+ }
799
+ /** Resolve one visible live inbox, retaining its identity across asynchronous UI work. */
800
+ #queueTarget() {
801
+ const root = this.#requiredAgent();
802
+ const inspectedId = this.#inspectedId;
803
+ if (inspectedId !== undefined && !this.#inspectView(inspectedId).writable)
804
+ throw new Error('This subagent is read-only.');
805
+ const agent = inspectedId === undefined ? root : this.#ctx.get('agents')?.get(SessionId(inspectedId));
806
+ if (agent === undefined)
807
+ throw new Error('This subagent is not loaded locally. Send it a message to continue before managing its queue.');
808
+ const check = () => {
809
+ this.assertActive(root);
810
+ if (this.#inspectedId !== inspectedId || inspectedId !== undefined && !this.#isOwnedDescendant(agent, root)) {
811
+ throw new Error('The queue session is no longer active.');
812
+ }
813
+ };
814
+ check();
815
+ return { agent, check, ...(inspectedId === undefined ? {} : { label: this.#inspectView(inspectedId).label }) };
816
+ }
817
+ async openQueue(signal, requestingAgent) {
818
+ if (requestingAgent !== undefined)
819
+ this.assertActive(requestingAgent);
820
+ const target = this.#queueTarget();
821
+ const { agent } = target;
822
+ if (this.#queueAbort !== undefined && !this.#queueAbort.signal.aborted)
627
823
  throw new Error('Message Queue is already open.');
824
+ // Validate recall state before opening; child views must never list parent delivery entries.
825
+ this.#tui.pendingInputs.list();
628
826
  const abort = new AbortController();
629
827
  this.#queueAbort = abort;
630
828
  const onAbort = () => { abort.abort(signal.reason); };
631
829
  signal.addEventListener('abort', onAbort, { once: true });
632
830
  if (signal.aborted)
633
831
  onAbort();
832
+ const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
833
+ abort.abort(); });
634
834
  try {
635
- await openMessageQueue({ agent, tui: this.#tui, signal: abort.signal, assertActive: () => { this.assertActive(agent); },
835
+ await openMessageQueue({ agent, signal: abort.signal, assertActive: target.check,
836
+ ...(target.label === undefined ? {} : { ownerLabel: target.label }),
837
+ tui: {
838
+ prompt: request => this.#tui.prompt(request), notice: (text, options) => this.#tui.notice(text, options),
839
+ restoreInput: submission => this.#tui.restoreInput(submission, agent.id),
840
+ pendingInputs: target.label === undefined ? this.#tui.pendingInputs : { list: () => [], update: () => false },
841
+ },
636
842
  restore: message => restoreSubmissionMessage(message, this.#ctx.get('attachments')) });
637
843
  }
844
+ catch (error) {
845
+ if (!abort.signal.aborted)
846
+ throw error;
847
+ }
638
848
  finally {
849
+ off();
639
850
  signal.removeEventListener('abort', onAbort);
640
851
  if (this.#queueAbort === abort)
641
852
  this.#queueAbort = undefined;
642
853
  }
643
854
  }
644
- /** Remove and rehydrate the newest durable human follow-up for queue browsing. */
645
- async editLatestFollowup(agent = this.#requiredAgent()) {
646
- this.assertActive(agent);
855
+ /** Inspect durable membership; only a currently running, owned local member can be opened. */
856
+ async openWorkflow(id) {
857
+ const root = this.#requiredAgent();
858
+ const inspectedId = this.#inspectedId;
859
+ this.#workflowAbort?.abort();
860
+ const abort = new AbortController();
861
+ this.#workflowAbort = abort;
862
+ const check = () => {
863
+ abort.signal.throwIfAborted();
864
+ this.assertActive(root);
865
+ if (this.#inspectedId !== inspectedId)
866
+ throw new Error('The workflow session is no longer active.');
867
+ };
868
+ const canOpen = (run, member) => {
869
+ const child = this.#ctx.get('agents')?.get(SessionId(member.childId));
870
+ return workflowMemberStatus(run, member) === 'open' && child?.status === 'running'
871
+ && child.session.header.parentSession === root.id && this.#isOwnedDescendant(child, root) && this.#tracker.owns(child.id);
872
+ };
873
+ try {
874
+ let lastRun;
875
+ let lastOpen = '';
876
+ let options = [];
877
+ const refresh = () => {
878
+ check();
879
+ const run = this.#workflows.get(id);
880
+ if (run === undefined)
881
+ throw new Error('This workflow is no longer available.');
882
+ const open = run.members.filter(member => canOpen(run, member)).map(member => member.seq).join(',');
883
+ if (run !== lastRun || open !== lastOpen) {
884
+ options = workflowOptions(run, member => canOpen(run, member));
885
+ lastRun = run;
886
+ lastOpen = open;
887
+ }
888
+ return options;
889
+ };
890
+ let selected;
891
+ while (!abort.signal.aborted) {
892
+ const answer = await this.#tui.prompt({ title: 'Workflow',
893
+ question: 'Browse phases and members. Only running local members can be opened.',
894
+ localize: ['title', 'question'], notify: false, options: refresh(), refreshOptions: refresh,
895
+ presentation: 'tree', filterable: true, allowCustom: false, submitLabel: 'open',
896
+ emptyText: 'No matching workflow members.', signal: abort.signal,
897
+ ...(selected === undefined ? {} : { initialValue: selected }) });
898
+ if (answer === null)
899
+ return;
900
+ check();
901
+ selected = answer;
902
+ const run = this.#workflows.get(id);
903
+ const member = run?.members.find(member => `member:${member.seq}` === answer);
904
+ if (run === undefined || member === undefined || !canOpen(run, member)) {
905
+ this.#tui.notice('This workflow member is no longer available to open.');
906
+ continue;
907
+ }
908
+ await this.#inspectSubagent(member.childId);
909
+ return;
910
+ }
911
+ }
912
+ catch (error) {
913
+ if (!abort.signal.aborted)
914
+ throw error;
915
+ }
916
+ finally {
917
+ if (this.#workflowAbort === abort)
918
+ this.#workflowAbort = undefined;
919
+ }
920
+ }
921
+ /** Rehydrate before withdrawing so navigation or missing attachments leave the inbox intact. */
922
+ async editLatestFollowup(request) {
923
+ request?.signal.throwIfAborted();
924
+ const target = this.#queueTarget();
925
+ const { agent } = target;
926
+ if (request?.sessionId !== undefined && request.sessionId !== agent.id)
927
+ throw new Error('The queue session is no longer active.');
647
928
  const message = agent.inbox.nextTurn.findLast(candidate => candidate.source.kind === 'user');
648
929
  if (message === undefined)
649
930
  return undefined;
650
931
  const submission = await restoreSubmissionMessage(message, this.#ctx.get('attachments'));
651
- this.assertActive(agent);
652
- return agent.inbox.remove(message.id) ? { ...submission, literal: true } : undefined;
932
+ request?.signal.throwIfAborted();
933
+ target.check();
934
+ return agent.inbox.remove(message.id) ? { ...submission, literal: true, delivery: 'queue' } : undefined;
653
935
  }
654
936
  /** Execute a plugin-owned slash command, falling back to user-invocable skills. */
655
- async execute(line, signal, images = []) {
937
+ async execute(input, signal, images = []) {
938
+ const submission = typeof input === 'string' ? { text: input, images } : input;
939
+ const { text: line } = submission;
940
+ images = submission.images;
656
941
  const commandLine = stripComposerImageMarkers(line, images);
657
942
  const parsed = parseControl(commandLine);
658
943
  if (parsed === undefined)
659
944
  return false;
660
945
  const commands = this.#ctx.get('commands');
946
+ const agent = this.#requiredAgent();
947
+ const staged = this.#commandAttachments?.stage(agent, submission);
661
948
  const execution = await (async () => {
662
949
  try {
663
- return await commands?.execute(this.#requiredAgent(), commandLine, encodeCommandAttachments(images), signal);
950
+ return await commands?.execute(agent, commandLine, staged?.attachments ?? encodeCommandAttachments(images), signal);
664
951
  }
665
952
  finally {
953
+ staged?.dispose();
666
954
  await this.#disposeRetired();
667
955
  }
668
956
  })();
669
957
  if (execution === undefined) {
670
- const skill = await this.#findUserSkill(skillNameFromCommand(parsed.name), signal);
958
+ const skill = await this.#findUserSkill(parsed.name, signal);
959
+ signal.throwIfAborted();
960
+ this.assertActive(agent);
671
961
  if (skill === undefined)
672
962
  return false;
673
- if (images.length > 0) {
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));
963
+ await this.#sendSubmission(submission, agent, signal);
679
964
  return true;
680
965
  }
681
966
  const result = execution.result;
682
- if (result.kind === 'error' && images.length > 0)
683
- this.#tui.restoreInput({ text: line, images });
684
- if (result.text !== undefined) {
967
+ if (result.kind === 'error' && (images.length > 0 || submission.files?.length))
968
+ this.#tui.restoreInput(submission, agent.session.id);
969
+ const presented = result.kind === 'success' && result.sourceEventSeq !== undefined
970
+ && agent.session.snapshotEvents().some(event => event.seq === result.sourceEventSeq && event.type === 'compaction/summary');
971
+ if (result.text !== undefined && !presented) {
685
972
  if (result.kind === 'error')
686
973
  this.#tui.notice(result.text, { level: 'error', focus: true });
687
974
  else
@@ -700,10 +987,10 @@ export class SessionRuntime {
700
987
  get recentSessions() {
701
988
  return this.#recent;
702
989
  }
703
- /** Current model selection for the active Agent. */
990
+ /** Current model selection for the active Agent, read from its durable projection. */
704
991
  selection(agent = this.#requiredAgent()) {
705
992
  this.assertActive(agent);
706
- const current = this.#requiredActive().selection.current;
993
+ const current = agent.model.current();
707
994
  if (current === undefined)
708
995
  throw new Error('active agent has no model selection');
709
996
  return current;
@@ -712,34 +999,42 @@ export class SessionRuntime {
712
999
  async changeSelection(agent, selection, info, options) {
713
1000
  this.assertActive(agent);
714
1001
  const active = this.#requiredActive();
715
- active.selection.current = selection;
716
1002
  const resolved = info ?? await this.#resolveModelInfo(selection);
1003
+ this.assertActive(agent);
1004
+ const current = agent.model.current();
1005
+ if (current?.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort) {
1006
+ agent.model.select(selection);
1007
+ }
717
1008
  active.contextWindow = resolved?.context?.contextWindow;
718
1009
  const status = modelStatus(selection, resolved);
719
1010
  active.reasoningEffort = status.reasoningEffort;
720
- if (options?.persist !== false) {
721
- await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
722
- }
723
1011
  this.#tui.setModel(status.model, status.reasoningEffort);
724
1012
  this.#pushSessionInfo();
1013
+ if (options?.persist !== false) {
1014
+ try {
1015
+ await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
1016
+ }
1017
+ catch (error) {
1018
+ throw new DefaultModelSaveError(selection, error);
1019
+ }
1020
+ }
725
1021
  }
726
1022
  /** Start a new top-level session with the current model selection. */
727
- async newSession(agent) {
1023
+ async newSession(agent, presetId) {
728
1024
  this.assertActive(agent);
729
- await this.#activate(await this.#create(this.selection(agent)), 'new');
1025
+ await this.#activate(await this.#create(this.selection(agent), presetId), 'new');
730
1026
  await this.refreshRecent();
731
1027
  }
732
1028
  /** Replace the active top-level session with one durable session. */
733
1029
  async resumeSession(agent, id, signal) {
734
1030
  this.assertActive(agent);
735
1031
  const selection = this.selection(agent);
736
- const ref = { current: selection, assembled: undefined };
737
1032
  let configured;
738
1033
  const handle = await this.#ctx.agents.resume({
739
1034
  resumeSessionId: SessionId(id),
740
1035
  agentOptions: { provider: selection.provider, model: selection.model },
741
1036
  signal,
742
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1037
+ setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
743
1038
  });
744
1039
  const configuration = configured;
745
1040
  if (configuration === undefined) {
@@ -748,7 +1043,6 @@ export class SessionRuntime {
748
1043
  }
749
1044
  await this.#activate({
750
1045
  handle,
751
- selection: ref,
752
1046
  contextWindow: undefined,
753
1047
  reasoningEffort: undefined,
754
1048
  agentPreset: configuration.agentPreset,
@@ -764,12 +1058,11 @@ export class SessionRuntime {
764
1058
  return;
765
1059
  }
766
1060
  const selection = this.selection(agent);
767
- const ref = { current: selection, assembled: undefined };
768
1061
  const handle = await this.#ctx.agents.resume({
769
1062
  resumeSessionId: SessionId(id),
770
1063
  agentOptions: { provider: selection.provider, model: selection.model },
771
1064
  signal,
772
- setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent, ref); },
1065
+ setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent); },
773
1066
  });
774
1067
  try {
775
1068
  this.#ctx.sessionTitle.rename(handle.agent.session, title);
@@ -956,7 +1249,6 @@ export class SessionRuntime {
956
1249
  }));
957
1250
  this.assertActive(agent);
958
1251
  const selection = this.selection(agent);
959
- const ref = { current: selection, assembled: undefined };
960
1252
  const active = this.#requiredActive();
961
1253
  let configured;
962
1254
  const handle = await this.#ctx.agents.create({
@@ -971,7 +1263,7 @@ export class SessionRuntime {
971
1263
  inheritedEventCount: SessionLogOffset(selected.branchIndex),
972
1264
  agentOptions: { provider: selection.provider, model: selection.model },
973
1265
  signal,
974
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1266
+ setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
975
1267
  });
976
1268
  try {
977
1269
  this.assertActive(agent);
@@ -987,7 +1279,6 @@ export class SessionRuntime {
987
1279
  }
988
1280
  await this.#activate({
989
1281
  handle,
990
- selection: ref,
991
1282
  contextWindow: undefined,
992
1283
  reasoningEffort: undefined,
993
1284
  agentPreset: configuration.agentPreset,
@@ -1220,10 +1511,15 @@ export class SessionRuntime {
1220
1511
  }
1221
1512
  async dispose() {
1222
1513
  this.#queueAbort?.abort(new Error('session runtime disposed'));
1514
+ this.#workflowAbort?.abort();
1515
+ for (const abort of this.#descendantApprovals.keys())
1516
+ abort.abort();
1223
1517
  if (this.#disposed)
1224
1518
  return;
1225
1519
  this.#disposed = true;
1520
+ this.#activeInteraction.abort();
1226
1521
  this.#activationEpoch += 1;
1522
+ this.#cancelInspectDeliveries();
1227
1523
  this.#inspectedId = undefined;
1228
1524
  this.#tracker.reset();
1229
1525
  this.#tui.setInspectedSubagent(undefined);
@@ -1243,15 +1539,14 @@ export class SessionRuntime {
1243
1539
  this.#releasedHandles.add(handle);
1244
1540
  await this.#disposeHandles(handles);
1245
1541
  }
1246
- async #create(selection) {
1247
- const ref = { current: selection, assembled: undefined };
1248
- const preset = await this.#ctx.agentPresets.resolve();
1542
+ async #create(selection, presetId) {
1543
+ const preset = await this.#ctx.agentPresets.resolve(presetId);
1249
1544
  let configured;
1250
1545
  const handle = await this.#ctx.agents.create({
1251
1546
  sessionId: SessionId('session-' + randomUUID()),
1252
1547
  meta: { cwd: process.cwd(), agentPreset: preset.id },
1253
1548
  agentOptions: { provider: selection.provider, model: selection.model },
1254
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1549
+ setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
1255
1550
  });
1256
1551
  const configuration = configured;
1257
1552
  if (configuration === undefined) {
@@ -1260,7 +1555,6 @@ export class SessionRuntime {
1260
1555
  }
1261
1556
  return {
1262
1557
  handle,
1263
- selection: ref,
1264
1558
  contextWindow: undefined,
1265
1559
  reasoningEffort: undefined,
1266
1560
  agentPreset: configuration.agentPreset,
@@ -1305,6 +1599,9 @@ export class SessionRuntime {
1305
1599
  const previous = this.#active;
1306
1600
  const epoch = ++this.#activationEpoch;
1307
1601
  this.#queueAbort?.abort(new Error('active session changed'));
1602
+ this.#workflowAbort?.abort();
1603
+ this.#activeInteraction.abort();
1604
+ this.#activeInteraction = new AbortController();
1308
1605
  this.#active = next;
1309
1606
  void this.#claimWorkspace(next);
1310
1607
  try {
@@ -1338,6 +1635,8 @@ export class SessionRuntime {
1338
1635
  // and the activation failure remains the actionable error.
1339
1636
  try {
1340
1637
  if (this.#activationEpoch === epoch) {
1638
+ this.#activeInteraction.abort();
1639
+ this.#activeInteraction = new AbortController();
1341
1640
  this.#active = previous;
1342
1641
  if (previous !== undefined) {
1343
1642
  try {
@@ -1370,8 +1669,12 @@ export class SessionRuntime {
1370
1669
  }
1371
1670
  /** Show one active session on the terminal: transcript, tools, controls, model. */
1372
1671
  #presentAgent(active, reason = 'open') {
1672
+ for (const [abort, root] of this.#descendantApprovals)
1673
+ if (root !== active.handle.agent)
1674
+ abort.abort();
1373
1675
  this.#workflows.reset(active.handle.agent.session.snapshotEvents());
1374
1676
  const agent = active.handle.agent;
1677
+ this.#cancelInspectDeliveries();
1375
1678
  this.#inspectedId = undefined;
1376
1679
  this.#tui.setInspectedSubagent(undefined);
1377
1680
  this.#tui.setStatus(agent.status);
@@ -1410,8 +1713,9 @@ export class SessionRuntime {
1410
1713
  ...(options === undefined ? {} : { complete: (prefix) => matchArgumentOptions(options, prefix) }),
1411
1714
  };
1412
1715
  });
1413
- const names = new Set(commands.map(command => command.name));
1414
- for (const skill of this.#skillCommands) {
1716
+ const names = new Set([...commands.map(command => command.name),
1717
+ ...BUILTIN_SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])])]);
1718
+ for (const skill of userSkillCommands(this.#skills, names)) {
1415
1719
  if (names.has(skill.name))
1416
1720
  continue;
1417
1721
  commands.push(skill);
@@ -1433,14 +1737,14 @@ export class SessionRuntime {
1433
1737
  const agent = this.agent;
1434
1738
  const skills = this.#ctx.get('skills');
1435
1739
  if (agent === undefined || skills === undefined) {
1436
- this.#skillCommands = [];
1740
+ this.#skills = [];
1437
1741
  }
1438
1742
  else {
1439
1743
  const cwd = currentWorkingDirectory(this.#ctx, agent.session);
1440
1744
  const list = await skills.list({ cwd, scope: agent, signal });
1441
1745
  if (this.#disposed || this.agent !== agent || currentWorkingDirectory(this.#ctx, agent.session) !== cwd)
1442
1746
  return;
1443
- this.#skillCommands = userSkillCommands(list);
1747
+ this.#skills = list;
1444
1748
  }
1445
1749
  this.#pushCommands();
1446
1750
  }
@@ -1487,6 +1791,7 @@ export class SessionRuntime {
1487
1791
  id,
1488
1792
  label: view?.label ?? id,
1489
1793
  phase: view?.phase ?? fallbackPhase,
1794
+ needsApproval: view?.needsApproval === true,
1490
1795
  ...(mode === undefined ? {} : { mode }),
1491
1796
  writable: isSteerableSubagent(mode),
1492
1797
  };
@@ -1512,6 +1817,7 @@ export class SessionRuntime {
1512
1817
  #replaceTranscript(agent, reason = 'refresh') {
1513
1818
  this.#tui.setWorkingDirectory?.(currentWorkingDirectory(this.#ctx, agent.session));
1514
1819
  const events = agent.session.snapshotEvents();
1820
+ this.#tui.pendingInputs?.reconcile?.(agent.id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
1515
1821
  this.#tracker.observeCatalog(agent.session, events);
1516
1822
  this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
1517
1823
  this.#replayLivePrefix(agent.session.id);
@@ -1525,10 +1831,18 @@ export class SessionRuntime {
1525
1831
  if (agent !== undefined)
1526
1832
  this.#replaceTranscript(agent);
1527
1833
  }
1834
+ #cancelInspectDeliveries() {
1835
+ for (const delivery of this.#inspectDeliveries)
1836
+ delivery.abort();
1837
+ this.#inspectDeliveries.clear();
1838
+ }
1528
1839
  #closeInspect() {
1529
1840
  if (this.#inspectedId === undefined)
1530
1841
  return;
1531
1842
  this.#inspectEpoch += 1;
1843
+ this.#queueAbort?.abort(new Error('viewed session changed'));
1844
+ this.#workflowAbort?.abort();
1845
+ this.#cancelInspectDeliveries();
1532
1846
  this.#inspectedId = undefined;
1533
1847
  this.#tui.setInspectedSubagent(undefined);
1534
1848
  const agent = this.#active?.handle.agent;
@@ -1573,6 +1887,12 @@ export class SessionRuntime {
1573
1887
  }
1574
1888
  if (request !== this.#inspectEpoch)
1575
1889
  return;
1890
+ if (this.#inspectedId !== id) {
1891
+ this.#queueAbort?.abort(new Error('viewed session changed'));
1892
+ this.#workflowAbort?.abort();
1893
+ this.#cancelInspectDeliveries();
1894
+ }
1895
+ this.#tui.pendingInputs?.reconcile?.(id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
1576
1896
  this.#inspectedId = id;
1577
1897
  if (cwd !== undefined)
1578
1898
  this.#tui.setWorkingDirectory?.(cwd);
@@ -1586,12 +1906,12 @@ export class SessionRuntime {
1586
1906
  const childId = this.#inspectedId;
1587
1907
  const root = this.#active?.handle.agent;
1588
1908
  if (childId === undefined || root === undefined) {
1589
- this.#tui.restoreInput(submission);
1909
+ this.#tui.restoreInput(submission, childId);
1590
1910
  return;
1591
1911
  }
1592
1912
  const view = this.#inspectView(childId);
1593
1913
  if (!view.writable) {
1594
- this.#tui.restoreInput(submission);
1914
+ this.#tui.restoreInput(submission, childId);
1595
1915
  this.#tui.notice('This subagent is a completed run and cannot take more messages.');
1596
1916
  return;
1597
1917
  }
@@ -1603,34 +1923,60 @@ export class SessionRuntime {
1603
1923
  : this.#ctx.get('agents')?.get(SessionId(parentId));
1604
1924
  const subagents = this.#ctx.get('subagents');
1605
1925
  if (parent === undefined || subagents === undefined) {
1606
- this.#tui.restoreInput(submission);
1926
+ this.#tui.restoreInput(submission, childId);
1607
1927
  this.#tui.notice(parent === undefined
1608
1928
  ? 'The parent of this subagent is not live, so it cannot take a follow-up.'
1609
1929
  : 'Subagent follow-up is unavailable in this composition.');
1610
1930
  return;
1611
1931
  }
1932
+ const previous = this.#inspectDeliveryTail;
1933
+ let release = () => { };
1934
+ this.#inspectDeliveryTail = new Promise(resolve => { release = resolve; });
1935
+ const delivery = new AbortController();
1936
+ this.#inspectDeliveries.add(delivery);
1937
+ let cancelUpload = () => { };
1938
+ const cancelled = new Promise((_resolve, reject) => {
1939
+ cancelUpload = () => { reject(delivery.signal.reason); };
1940
+ delivery.signal.addEventListener('abort', cancelUpload, { once: true });
1941
+ });
1612
1942
  try {
1613
- const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
1614
- if (this.#inspectedId !== childId || this.#active?.handle.agent !== root) {
1615
- this.#tui.restoreInput(submission);
1616
- return;
1617
- }
1618
- await queueHostSubagentPrompt(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, new AbortController().signal);
1943
+ // Recalled messages can include attachments; admission must retain submission order.
1944
+ await Promise.race([previous, cancelled]);
1945
+ delivery.signal.throwIfAborted();
1946
+ // Attachment storage need not support cancellation; release the draft now
1947
+ // and ignore a later upload result before crossing Harness admission.
1948
+ const message = await Promise.race([createSubmissionMessage(submission, this.#ctx.get('attachments')), cancelled]);
1949
+ delivery.signal.removeEventListener('abort', cancelUpload);
1950
+ delivery.signal.throwIfAborted();
1951
+ if (this.#inspectedId !== childId || this.#active?.handle.agent !== root)
1952
+ throw new Error('The viewed session changed before delivery.');
1953
+ const send = submission.delivery === 'steer' ? steerHostSubagentPrompt : queueHostSubagentPrompt;
1954
+ const messageId = await send(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, delivery.signal);
1955
+ this.#tui.pendingInputs?.prepare?.(submission, messageId);
1956
+ this.#tui.pendingInputs?.acknowledge?.(submission);
1619
1957
  }
1620
1958
  catch (error) {
1621
- this.#tui.restoreInput(submission);
1622
- this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1959
+ this.#tui.restoreInput(submission, childId);
1960
+ if (!delivery.signal.aborted)
1961
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1962
+ }
1963
+ finally {
1964
+ delivery.signal.removeEventListener('abort', cancelUpload);
1965
+ this.#inspectDeliveries.delete(delivery);
1966
+ release();
1623
1967
  }
1624
1968
  }
1625
1969
  #sessionControls(active, projection) {
1626
- const controls = sessionControls(projection);
1627
- const livePlan = this.#ctx.get('planMode')?.get(active.handle.agent);
1970
+ const liveGoal = this.#ctx.get('goals')?.get(active.handle.agent);
1971
+ const controls = sessionControls(projection, this.#ctx.get('planMode')?.get(active.handle.agent));
1972
+ if (controls.goal !== undefined && liveGoal !== undefined && liveGoal.id === projection?.goal?.goal.id && liveGoal.revision === projection?.goal?.goal.revision) {
1973
+ controls.goal.activation = liveGoal.activation;
1974
+ }
1975
+ const failure = active.handle.agent.status === 'idle' ? continuableFailure(active.handle.agent.session.snapshotEvents()) : undefined;
1628
1976
  return {
1629
1977
  ...controls,
1978
+ ...(failure === undefined ? {} : { continuation: failure }),
1630
1979
  agentPreset: active.agentPreset,
1631
- ...(livePlan === undefined
1632
- ? {}
1633
- : { plan: { active: livePlan.active, pending: livePlan.pending !== undefined } }),
1634
1980
  };
1635
1981
  }
1636
1982
  /** Read one client-visible snapshot. Host-only projection state is never consulted. */