@xingular/harness 1.0.0 → 1.1.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.
Files changed (257) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/README.md +70 -19
  3. package/dist/cli.js +10321 -5785
  4. package/dist/sdk/agents/builtin.d.ts +2 -0
  5. package/dist/sdk/agents/builtin.js +34 -0
  6. package/dist/sdk/agents/narrow.d.ts +13 -0
  7. package/dist/sdk/agents/narrow.js +29 -0
  8. package/dist/sdk/agents/profiles.d.ts +80 -0
  9. package/dist/sdk/agents/profiles.js +221 -0
  10. package/dist/sdk/annotations/bridge.d.ts +42 -0
  11. package/dist/sdk/annotations/bridge.js +264 -0
  12. package/dist/sdk/annotations/capture.d.ts +116 -0
  13. package/dist/sdk/annotations/capture.js +308 -0
  14. package/dist/sdk/annotations/delivery.d.ts +72 -0
  15. package/dist/sdk/annotations/delivery.js +130 -0
  16. package/dist/sdk/annotations/inspect.d.ts +10 -0
  17. package/dist/sdk/annotations/inspect.js +33 -0
  18. package/dist/sdk/annotations/isolated.d.ts +6 -0
  19. package/dist/sdk/annotations/isolated.js +51 -0
  20. package/dist/sdk/annotations/pageScript.d.ts +28 -0
  21. package/dist/sdk/annotations/pageScript.js +570 -0
  22. package/dist/sdk/annotations/part.d.ts +10 -0
  23. package/dist/sdk/annotations/part.js +144 -0
  24. package/dist/sdk/annotations/queue.d.ts +4 -0
  25. package/dist/sdk/annotations/queue.js +40 -0
  26. package/dist/sdk/annotations/serverMethods.d.ts +3 -0
  27. package/dist/sdk/annotations/serverMethods.js +62 -0
  28. package/dist/sdk/annotations/store.d.ts +160 -0
  29. package/dist/sdk/annotations/store.js +386 -0
  30. package/dist/sdk/browser/index.d.ts +14 -3
  31. package/dist/sdk/browser/index.js +11 -4
  32. package/dist/sdk/browser/serverMethods.d.ts +3 -0
  33. package/dist/sdk/browser/serverMethods.js +7 -0
  34. package/dist/sdk/browser/session.d.ts +1 -0
  35. package/dist/sdk/browser/session.js +45 -9
  36. package/dist/sdk/cli.js +19 -11
  37. package/dist/sdk/core/bus.js +1 -1
  38. package/dist/sdk/core/env.d.ts +1 -1
  39. package/dist/sdk/core/env.js +1 -1
  40. package/dist/sdk/doctor/serverMethods.d.ts +3 -0
  41. package/dist/sdk/doctor/serverMethods.js +8 -0
  42. package/dist/sdk/harness.d.ts +15 -0
  43. package/dist/sdk/harness.js +44 -5
  44. package/dist/sdk/history/recall.js +8 -8
  45. package/dist/sdk/history/serverMethods.d.ts +3 -0
  46. package/dist/sdk/history/serverMethods.js +8 -0
  47. package/dist/sdk/hub/cli.d.ts +1 -1
  48. package/dist/sdk/hub/cli.js +1 -1
  49. package/dist/sdk/hub/schema.d.ts +1 -1
  50. package/dist/sdk/hub/serverMethods.d.ts +3 -0
  51. package/dist/sdk/hub/serverMethods.js +11 -0
  52. package/dist/sdk/index.d.ts +10 -0
  53. package/dist/sdk/index.js +7 -0
  54. package/dist/sdk/mcp/connections.js +14 -7
  55. package/dist/sdk/mcp/serverMethods.d.ts +3 -0
  56. package/dist/sdk/mcp/serverMethods.js +22 -0
  57. package/dist/sdk/plugins/builtin.d.ts +1 -0
  58. package/dist/sdk/plugins/builtin.js +128 -33
  59. package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
  60. package/dist/sdk/plugins/builtinRegistry.js +49 -0
  61. package/dist/sdk/plugins/cli.d.ts +1 -1
  62. package/dist/sdk/plugins/cli.js +4 -3
  63. package/dist/sdk/prompt/compile.d.ts +5 -2
  64. package/dist/sdk/prompt/compile.js +25 -8
  65. package/dist/sdk/refine/cli.d.ts +1 -1
  66. package/dist/sdk/refine/cli.js +2 -2
  67. package/dist/sdk/refine/serverMethods.d.ts +3 -0
  68. package/dist/sdk/refine/serverMethods.js +20 -0
  69. package/dist/sdk/server/methods.js +9 -86
  70. package/dist/sdk/server/params.d.ts +4 -0
  71. package/dist/sdk/server/params.js +11 -0
  72. package/dist/sdk/sessions/contracts.d.ts +17 -0
  73. package/dist/sdk/sessions/contracts.js +42 -3
  74. package/dist/sdk/sessions/planMode.d.ts +5 -2
  75. package/dist/sdk/sessions/planMode.js +12 -3
  76. package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
  77. package/dist/sdk/sessions/reasoningSummaries.js +3 -3
  78. package/dist/sdk/sessions/registry.d.ts +1 -0
  79. package/dist/sdk/sessions/registry.js +4 -1
  80. package/dist/sdk/sessions/runIndex.js +7 -7
  81. package/dist/sdk/sessions/runStore.js +3 -1
  82. package/dist/sdk/sessions/runner.js +2 -2
  83. package/dist/sdk/sessions/sessions.d.ts +17 -5
  84. package/dist/sdk/sessions/sessions.js +79 -12
  85. package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
  86. package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
  87. package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
  88. package/dist/sdk/sessions/sideQuestions.js +290 -0
  89. package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
  90. package/dist/sdk/sessions/subagentServerMethods.js +41 -0
  91. package/dist/sdk/sessions/subagentService.d.ts +13 -2
  92. package/dist/sdk/sessions/subagentService.js +34 -16
  93. package/dist/sdk/sessions/subagents.d.ts +9 -1
  94. package/dist/sdk/sessions/subagents.js +19 -0
  95. package/dist/sdk/terminal/browser.d.ts +62 -0
  96. package/dist/sdk/terminal/browser.js +41 -0
  97. package/dist/sdk/terminal/browserControl.d.ts +29 -0
  98. package/dist/sdk/terminal/browserControl.js +186 -0
  99. package/dist/sdk/terminal/commandHelp.d.ts +17 -0
  100. package/dist/sdk/terminal/commandHelp.js +20 -0
  101. package/dist/sdk/terminal/controller.d.ts +7 -1
  102. package/dist/sdk/terminal/controller.js +34 -2
  103. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
  104. package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
  105. package/dist/sdk/terminal/piExtension.js +33 -48
  106. package/dist/sdk/terminal/socket.d.ts +6 -3
  107. package/dist/sdk/terminal/socket.js +40 -9
  108. package/dist/sdk/terminal/standalone.d.ts +2 -0
  109. package/dist/sdk/terminal/standalone.js +50 -10
  110. package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
  111. package/dist/sdk/terminal/subagentMonitor.js +121 -0
  112. package/dist/sdk/terminal/terminals.d.ts +42 -6
  113. package/dist/sdk/terminal/terminals.js +359 -18
  114. package/dist/sdk/terminal/trajectory.js +2 -2
  115. package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
  116. package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
  117. package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
  118. package/dist/sdk/terminal/tui/commandPages.js +54 -6
  119. package/dist/sdk/terminal/tui/commands.d.ts +8 -4
  120. package/dist/sdk/terminal/tui/commands.js +181 -58
  121. package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
  122. package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
  123. package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
  124. package/dist/sdk/terminal/tui/dialogs.js +9 -2
  125. package/dist/sdk/terminal/tui/modelPicker.js +1 -6
  126. package/dist/sdk/terminal/tui/page.d.ts +3 -0
  127. package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
  128. package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
  129. package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
  130. package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
  131. package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
  132. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
  133. package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
  134. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
  135. package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
  136. package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
  137. package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
  138. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
  139. package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
  140. package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
  141. package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
  142. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
  143. package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
  144. package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
  145. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
  146. package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
  147. package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
  148. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  149. package/dist/sdk/terminal/tui/planMode.js +1 -1
  150. package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
  151. package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
  152. package/dist/sdk/terminal/tui/questions.d.ts +2 -0
  153. package/dist/sdk/terminal/tui/runtime.js +4 -2
  154. package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
  155. package/dist/sdk/terminal/tui/settingsModel.js +32 -2
  156. package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
  157. package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
  158. package/dist/sdk/terminal/tui/ui.d.ts +2 -0
  159. package/dist/sdk/terminal/tui/ui.js +1 -0
  160. package/dist/sdk/tools/askUser.js +26 -8
  161. package/dist/sdk/tools/checklist.js +2 -2
  162. package/dist/sdk/tools/harnessTools.d.ts +2 -0
  163. package/dist/sdk/tools/harnessTools.js +1 -1
  164. package/dist/sdk/tools/spawnSubagent.js +13 -4
  165. package/dist/sdk/web/part.d.ts +10 -0
  166. package/dist/sdk/web/part.js +6 -0
  167. package/dist/sdk/web/serverMethods.d.ts +3 -0
  168. package/dist/sdk/web/serverMethods.js +7 -0
  169. package/dist/sdk/wiki/serverMethods.d.ts +3 -0
  170. package/dist/sdk/wiki/serverMethods.js +26 -0
  171. package/dist/sdk/worktrees/cli.d.ts +11 -0
  172. package/dist/sdk/worktrees/cli.js +146 -0
  173. package/dist/sdk/worktrees/copy.d.ts +9 -0
  174. package/dist/sdk/worktrees/copy.js +111 -0
  175. package/dist/sdk/worktrees/git.d.ts +16 -0
  176. package/dist/sdk/worktrees/git.js +81 -0
  177. package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
  178. package/dist/sdk/worktrees/serverMethods.js +30 -0
  179. package/dist/sdk/worktrees/service.d.ts +73 -0
  180. package/dist/sdk/worktrees/service.js +483 -0
  181. package/dist/sdk/worktrees/setup.d.ts +23 -0
  182. package/dist/sdk/worktrees/setup.js +65 -0
  183. package/dist/sdk/worktrees/types.d.ts +91 -0
  184. package/dist/sdk/worktrees/types.js +1 -0
  185. package/dist/sdk/world/cli.d.ts +2 -2
  186. package/dist/sdk/world/cli.js +6 -6
  187. package/dist/sdk/world/world.d.ts +3 -1
  188. package/dist/sdk/world/world.js +8 -9
  189. package/dist/terminal-opentui.js +14540 -7374
  190. package/dist/terminal-pi.js +285 -666
  191. package/docs/README.md +22 -2
  192. package/docs/architecture/00-overview.md +28 -16
  193. package/docs/architecture/02-sessions-and-runs.md +72 -15
  194. package/docs/architecture/03-variants.md +10 -2
  195. package/docs/architecture/04-prompt.md +26 -6
  196. package/docs/architecture/06-plugins-and-capabilities.md +39 -3
  197. package/docs/architecture/07-tools-and-gate.md +21 -6
  198. package/docs/architecture/09-accounts-and-connectors.md +8 -0
  199. package/docs/architecture/10-server-protocol.md +111 -2
  200. package/docs/architecture/11-terminal.md +185 -5
  201. package/docs/architecture/12-storage.md +15 -1
  202. package/docs/architecture/13-world-tools.md +28 -6
  203. package/docs/architecture/14-surfaces-and-parity.md +89 -0
  204. package/docs/architecture/README.md +15 -4
  205. package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
  206. package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
  207. package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
  208. package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
  209. package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
  210. package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
  211. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
  212. package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
  213. package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
  214. package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
  215. package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
  216. package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
  217. package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
  218. package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
  219. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
  220. package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
  221. package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
  222. package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
  223. package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
  224. package/docs/architecture/proposals/README.md +25 -0
  225. package/docs/decisions/ADR-0009-xharness-command.md +6 -5
  226. package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
  227. package/docs/decisions/ADR-0015-resource-browser.md +28 -0
  228. package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
  229. package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
  230. package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
  231. package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
  232. package/docs/decisions/ADR-0020-page-annotations.md +37 -0
  233. package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
  234. package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
  235. package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
  236. package/docs/decisions/ADR-0024-xagent-command.md +40 -0
  237. package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
  238. package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
  239. package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
  240. package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
  241. package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
  242. package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
  243. package/docs/doctor.md +10 -4
  244. package/docs/features/README.md +26 -0
  245. package/docs/features/interfaces/README.md +1 -1
  246. package/docs/future-work.md +48 -0
  247. package/docs/parity.md +78 -0
  248. package/docs/releases/1.0.0.md +2 -2
  249. package/docs/releases/4.0.0.md +46 -0
  250. package/docs/releases/README.md +20 -15
  251. package/docs/study/00-index.md +5 -0
  252. package/docs/study/autonomous-engineering-sources.md +277 -0
  253. package/docs/study/autonomy-sources.md +147 -0
  254. package/docs/study/proactive-autonomy.md +408 -0
  255. package/docs/study/tui-resource-browser.md +78 -0
  256. package/docs/terminal-tui.md +325 -93
  257. package/package.json +3 -3
@@ -21,8 +21,13 @@ import * as variantStore from "../variant/store.js";
21
21
  import { compile, copyToRun, record, render, saveForSession } from "../prompt/compile.js";
22
22
  import { isActiveStatus } from "../sessions/contracts.js";
23
23
  import { findRun, loadRun, saveRun, updateRun } from "../sessions/runStore.js";
24
+ import { CHECKLIST_WRITE } from "../tools/checklist.js";
25
+ import { createProfile, listProfiles, profileDirs, profileSection, removeProfile, resolveProfile } from "../agents/profiles.js";
24
26
  import { SESSION_NAME } from "../tools/sessionName.js";
25
27
  import { contributionId } from "../plugins/identity.js";
28
+ import { setupScript } from "../worktrees/setup.js";
29
+ import { inspectStep } from "../annotations/inspect.js";
30
+ import * as pageNotes from "../annotations/store.js";
26
31
  import { APPS_COMMAND, WIKI_COMMAND } from "./cli.js";
27
32
  import { SOCKET_ENV, TOKEN_ENV } from "./client.js";
28
33
  import { TerminalController } from "./controller.js";
@@ -30,6 +35,7 @@ import { terminalCommandFiles } from "./launch.js";
30
35
  import { SessionSocket, socketPath } from "./socket.js";
31
36
  import { harnessLogInfo, readHarnessLogChunk } from "./logs.js";
32
37
  import { listTerminalTrajectoryRuns, readTerminalTrajectory } from "./trajectory.js";
38
+ import { BrowserControl } from "./browserControl.js";
33
39
  /** Identity of the exact resource decision that this terminal was prepared with. */
34
40
  function resourceIdentity(loading) {
35
41
  if (!loading)
@@ -63,8 +69,27 @@ export class Terminals {
63
69
  worldReadOnly = new Set();
64
70
  socket;
65
71
  assets;
66
- /** A session's `/accounts` switch, taken once its Pi process has ended. */
72
+ /** A session's `/accounts` or `/worktree` switch, taken once its Pi process has ended. */
67
73
  relaunches = new Map();
74
+ /** The managed worktree each live session holds. */
75
+ worktreeLeases = new Map();
76
+ /** Each child's profile name and colour, looked up once per child run. */
77
+ childProfiles = new Map();
78
+ subagentProfile(record) {
79
+ if (!record.profile)
80
+ return null;
81
+ if (!this.childProfiles.has(record.id)) {
82
+ if (this.childProfiles.size > 500)
83
+ this.childProfiles.clear();
84
+ let color = 'blue';
85
+ try {
86
+ color = resolveProfile(this.options.env.root, record.workingDirectory, record.profile)?.color ?? color;
87
+ }
88
+ catch { }
89
+ this.childProfiles.set(record.id, { name: record.profile, color });
90
+ }
91
+ return this.childProfiles.get(record.id) ?? null;
92
+ }
68
93
  constructor(options) {
69
94
  this.options = options;
70
95
  this.assets = options.assets ?? defaultAssets();
@@ -83,6 +108,7 @@ export class Terminals {
83
108
  const owner = this.sessions.get(parent.sessionId);
84
109
  if (owner?.controller.activeRun?.runId === parent.id) {
85
110
  owner.controller.observe(event, true);
111
+ owner.controller.observeSubagent(parent.id, record, event, this.subagentProfile(record));
86
112
  break;
87
113
  }
88
114
  parentId = parent.parentRunId;
@@ -235,6 +261,14 @@ export class Terminals {
235
261
  const settings = params.settings && typeof params.settings === 'object' && !Array.isArray(params.settings) ? params.settings : {};
236
262
  return api.configure({ ...scope, pluginId: text('pluginId'), settings });
237
263
  }
264
+ case 'plugins.install':
265
+ if (!api.install)
266
+ throw invalid('Package installation is unavailable in this host.');
267
+ return api.install({ ...scope, source: text('source') });
268
+ case 'plugins.remove':
269
+ if (!api.remove)
270
+ throw invalid('Package removal is unavailable in this host.');
271
+ return api.remove({ ...scope, pluginId: text('pluginId') });
238
272
  default:
239
273
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
240
274
  }
@@ -324,6 +358,194 @@ export class Terminals {
324
358
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
325
359
  }
326
360
  }
361
+ /** `/agents`: the agent profiles this session's folder can use (PROP-0029). */
362
+ agentsCall(live, call, params) {
363
+ const root = this.options.env.root;
364
+ const cwd = live.input.cwd;
365
+ const name = () => {
366
+ if (typeof params.name !== 'string' || !params.name.trim())
367
+ throw invalid('A profile name is required.');
368
+ return params.name.trim();
369
+ };
370
+ const scope = () => {
371
+ if (params.scope !== 'global' && params.scope !== 'project')
372
+ throw invalid('scope must be global or project.');
373
+ return params.scope;
374
+ };
375
+ switch (call.slice('terminal.agents.'.length)) {
376
+ case 'list': return { ...listProfiles(root, cwd, this.variantDirs.get(live.input.sessionId) ?? null), dirs: profileDirs(root, cwd) };
377
+ case 'create': return createProfile(root, cwd, { name: name(), scope: scope(), from: typeof params.from === 'string' && params.from ? params.from : null });
378
+ case 'remove':
379
+ removeProfile(root, cwd, { name: name(), scope: scope() });
380
+ return {};
381
+ default: throw invalid('Unknown profile operation.');
382
+ }
383
+ }
384
+ /** `/worktree`: the managed worktrees of the repository this session works in. */
385
+ async worktreesCall(live, call, params) {
386
+ const worktrees = this.options.worktrees;
387
+ if (!worktrees)
388
+ throw invalid('Worktrees are unavailable in this host.');
389
+ const sessionId = live.input.sessionId;
390
+ const cwd = live.input.cwd;
391
+ const current = this.worktreeLeases.get(sessionId) ?? null;
392
+ const id = () => {
393
+ if (typeof params.id !== 'string' || !params.id)
394
+ throw invalid('A worktree id is required.');
395
+ return params.id;
396
+ };
397
+ const action = call.slice('terminal.worktrees.'.length);
398
+ switch (action) {
399
+ case 'list': {
400
+ let repo;
401
+ try {
402
+ repo = await worktrees.repositoryOf(cwd);
403
+ }
404
+ catch (error) {
405
+ return { repo: null, current, worktrees: [], removed: [], reason: error instanceof Error ? error.message : String(error) };
406
+ }
407
+ const statuses = await Promise.all((await worktrees.list(repo)).map((record) => worktrees.status(record.id)));
408
+ return { repo, current, worktrees: statuses, removed: worktrees.removed(repo), reason: null };
409
+ }
410
+ case 'status': return worktrees.status(id());
411
+ case 'create': return worktrees.create({
412
+ repo: cwd,
413
+ ...(typeof params.name === 'string' && params.name ? { name: params.name } : {}),
414
+ ...(typeof params.branch === 'string' && params.branch ? { branch: params.branch } : {}),
415
+ kind: params.kind === 'permanent' ? 'permanent' : 'managed'
416
+ });
417
+ case 'branch':
418
+ if (typeof params.name !== 'string' || !params.name)
419
+ throw invalid('A branch name is required.');
420
+ return worktrees.branch(id(), params.name);
421
+ case 'handoff': {
422
+ const direction = params.direction === 'toSource' || params.direction === 'toWorktree' ? params.direction : null;
423
+ if (!direction)
424
+ throw invalid('direction is toSource or toWorktree.');
425
+ return worktrees.handoff(id(), direction, { move: params.move !== false });
426
+ }
427
+ case 'remove':
428
+ if (id() === current)
429
+ throw new HarnessError('worktree_in_use', 'This session works in that worktree. Switch to another folder first.');
430
+ return worktrees.remove(id(), { force: params.force === true });
431
+ case 'restore': return worktrees.restore(id());
432
+ case 'prune': return worktrees.prune({ repo: cwd, ...(typeof params.keep === 'number' ? { keep: params.keep } : {}) });
433
+ case 'trustSetup': {
434
+ const repo = await worktrees.repositoryOf(cwd);
435
+ const script = setupScript(repo);
436
+ if (!script)
437
+ throw invalid('This repository has no .xharness/worktree-setup.');
438
+ await worktrees.trustSetup(repo, script.hash);
439
+ return { hash: script.hash };
440
+ }
441
+ case 'switch': {
442
+ // A new conversation in the chosen folder: Pi keeps one history per working folder.
443
+ const target = params.main === true ? null : id();
444
+ const folder = target ? worktrees.pathOf(target) : await worktrees.repositoryOf(cwd);
445
+ if (!folder)
446
+ throw new HarnessError('worktree_missing', 'That worktree has no folder.');
447
+ this.relaunches.set(sessionId, { accountId: live.account.id, nativeSessionId: null, cwd: folder, worktreeId: target });
448
+ return { cwd: folder, worktreeId: target };
449
+ }
450
+ default:
451
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
452
+ }
453
+ }
454
+ /** `/plugin:notes` and `/plugin:annotate`: the page notes a person keeps, and the visible browser they capture them in. */
455
+ async annotationsCall(live, call, params) {
456
+ const root = this.options.env.root;
457
+ const sessionId = live.input.sessionId;
458
+ const capture = this.options.noteCapture;
459
+ const id = () => {
460
+ if (typeof params.id !== 'string' || !params.id)
461
+ throw invalid('A note id is required.');
462
+ return params.id;
463
+ };
464
+ const version = () => {
465
+ if (typeof params.expectedVersion !== 'number')
466
+ throw invalid('The note version you last read is required.');
467
+ return params.expectedVersion;
468
+ };
469
+ const needCapture = () => {
470
+ if (!capture)
471
+ throw invalid('This host has no capture browser.');
472
+ return capture;
473
+ };
474
+ const needNotes = () => {
475
+ if (!this.options.pageNotes)
476
+ throw invalid('This host cannot send page notes.');
477
+ return this.options.pageNotes;
478
+ };
479
+ const action = call.slice('terminal.annotations.'.length);
480
+ switch (action) {
481
+ case 'list': {
482
+ const result = pageNotes.list(root, {
483
+ query: typeof params.query === 'string' ? params.query : null,
484
+ url: typeof params.url === 'string' ? params.url : null,
485
+ status: params.status === 'open' || params.status === 'resolved' ? params.status : null,
486
+ limit: 200
487
+ });
488
+ return { ...result, sessionId };
489
+ }
490
+ case 'read': return pageNotes.read(root, id());
491
+ case 'create': {
492
+ const note = pageNotes.create(root, { source: params.source, target: params.target, body: params.body, tags: params.tags });
493
+ return params.share === true ? pageNotes.share(root, note.id, { sessionId }) : note;
494
+ }
495
+ case 'update': return pageNotes.update(root, id(), version(), (params.patch ?? {}));
496
+ case 'delete':
497
+ pageNotes.remove(root, id(), version());
498
+ return {};
499
+ // A terminal shares with, and withdraws from, its own session only.
500
+ case 'share': return pageNotes.share(root, id(), { sessionId });
501
+ case 'unshare': return pageNotes.unshare(root, id(), { sessionId });
502
+ case 'open': return needCapture().open(sessionId, typeof params.url === 'string' && params.url ? params.url : null);
503
+ case 'identity': return needCapture().identity(sessionId);
504
+ case 'begin':
505
+ if (params.kind !== 'text' && params.kind !== 'element')
506
+ throw invalid('kind is text or element.');
507
+ return needCapture().begin(sessionId, params.kind);
508
+ case 'capture':
509
+ if (typeof params.captureId !== 'string')
510
+ throw invalid('A capture id is required.');
511
+ return needCapture().capture(sessionId, params.captureId);
512
+ case 'cancel':
513
+ await needCapture().cancel(sessionId, typeof params.captureId === 'string' ? params.captureId : undefined);
514
+ return {};
515
+ case 'reveal': {
516
+ const note = pageNotes.read(root, id());
517
+ return needCapture().reveal(sessionId, note.source.url, note.target);
518
+ }
519
+ // ---- The inspector and the queue (PROP-0018); a terminal queues and sends to its own session.
520
+ case 'target': return { ...needCapture().describe(sessionId), windows: await needCapture().windows(sessionId).catch(() => []) };
521
+ case 'attach': {
522
+ const kind = params.kind;
523
+ if (kind !== 'web' && kind !== 'cdp' && kind !== 'bridge')
524
+ throw invalid('kind is web, cdp or bridge.');
525
+ const spec = kind === 'cdp'
526
+ ? { kind, endpoint: typeof params.endpoint === 'string' ? params.endpoint : '', name: typeof params.name === 'string' ? params.name : null }
527
+ : kind === 'bridge' ? { kind, name: typeof params.name === 'string' ? params.name : null } : { kind };
528
+ return needCapture().attach(sessionId, spec);
529
+ }
530
+ case 'windows': return needCapture().windows(sessionId);
531
+ case 'selectWindow':
532
+ if (typeof params.window !== 'string')
533
+ throw invalid('A window id is required.');
534
+ await needCapture().selectWindow(sessionId, params.window);
535
+ return {};
536
+ case 'inspect': return needCapture().inspect(sessionId);
537
+ case 'stopInspect':
538
+ await needCapture().stopInspect(sessionId);
539
+ return {};
540
+ case 'next': return inspectStep(needCapture(), needNotes(), sessionId, sessionId, 'terminal');
541
+ case 'queue': return needNotes().queue(id(), version(), sessionId);
542
+ case 'dequeue': return needNotes().dequeue(id(), version());
543
+ case 'confirm': return needNotes().confirm(id(), version());
544
+ case 'send': return needNotes().send(sessionId, { ids: Array.isArray(params.ids) ? params.ids.filter((entry) => typeof entry === 'string') : null });
545
+ default:
546
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
547
+ }
548
+ }
327
549
  /** The account a session asked to switch to. Taken once, after its Pi process has ended and before `dispose`. */
328
550
  takeRelaunch(sessionId) {
329
551
  const request = this.relaunches.get(sessionId) ?? null;
@@ -413,13 +635,15 @@ export class Terminals {
413
635
  }
414
636
  // With no application policy: reads run, a read-only session refuses the rest, and anything else asks.
415
637
  const name = tool.toLowerCase();
416
- if (name === SESSION_NAME || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
638
+ if (name === SESSION_NAME || name === CHECKLIST_WRITE || name === SPAWN_SUBAGENT || name === WAIT_SUBAGENTS || READ_ONLY_TOOLS.has(name) || this.worldReadOnly.has(name) || this.options.apps.toolNames.includes(name))
417
639
  return { allowed: true };
418
640
  if (live.input.readOnly)
419
641
  return { allowed: false, reason: 'This session is read-only.' };
420
642
  return { allowed: false, requiresApproval: true, rememberable: true, reason: 'This native action requires approval.' };
421
643
  }
422
644
  async subagentsCall(live, call, params) {
645
+ if (call === 'terminal.subagents.monitor')
646
+ return { children: live.controller.subagentSnapshot() };
423
647
  const service = this.options.subagents;
424
648
  if (!service || live.input.subagents === null) {
425
649
  if (call === 'terminal.subagents.tools')
@@ -435,7 +659,7 @@ export class Terminals {
435
659
  if (!Array.isArray(params.tools) || params.tools.some((name) => typeof name !== 'string'))
436
660
  throw invalid('Native tool names are required.');
437
661
  const readOnly = live.input.readOnly === true;
438
- const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
662
+ const readTools = new Set([...READ_ONLY_TOOLS, SESSION_NAME, CHECKLIST_WRITE, SPAWN_SUBAGENT, WAIT_SUBAGENTS, 'ask_user', 'mcp_list_tools', 'mcp_call_tool', 'history_search', 'history_read', 'wiki_search', 'wiki_read', 'wiki_list', 'memory_facts', 'skill_load']);
439
663
  const tools = params.tools.filter((name) => !readOnly || readTools.has(name));
440
664
  const capabilities = this.options.resolveCapabilities ? await this.options.resolveCapabilities(live.caller, tools, live.input.cwd) : { version: 1, tools, connectors: [], contributions: [] };
441
665
  freezeRun(this.options.env.root, run.runId, live.caller, capabilities);
@@ -446,6 +670,7 @@ export class Terminals {
446
670
  effort: typeof params.effort === 'string' ? params.effort : null,
447
671
  launch: live.input.launch, tools: { allow: capabilities.tools, gated: capabilities.tools.filter(name => !readTools.has(name)) },
448
672
  connections: live.input.connections === false ? false : { readOnly }, subagents: live.input.subagents,
673
+ variant: this.variantDirs.has(live.input.sessionId) ? { dir: this.variantDirs.get(live.input.sessionId), scope: live.input.variant && 'scope' in live.input.variant ? live.input.variant.scope ?? null : null } : null,
449
674
  capabilities
450
675
  }, live.account.id, capabilities);
451
676
  service.restore(run.runId, params.history ?? { owners: [], receipts: [] });
@@ -519,6 +744,43 @@ export class Terminals {
519
744
  }
520
745
  throw new HarnessError('unknown_method', `Unknown call ${call}.`);
521
746
  }
747
+ async sideQuestionCall(live, call, params) {
748
+ const service = this.options.sideQuestions;
749
+ if (!service)
750
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
751
+ if (call === 'terminal.sideQuestions.start') {
752
+ if (typeof params.question !== 'string')
753
+ throw invalid('A side question is required.');
754
+ const context = params.context && typeof params.context === 'object' && !Array.isArray(params.context)
755
+ ? params.context
756
+ : undefined;
757
+ const latest = listTerminalTrajectoryRuns(this.options.env.root, live.input.sessionId, live.controller.nativeSessionId)
758
+ .find(run => findRun(this.options.env.root, run.id)?.trigger !== 'side-question');
759
+ return service.start({
760
+ sessionId: live.input.sessionId,
761
+ parentRunId: live.controller.activeRun?.runId ?? latest?.id ?? null,
762
+ parentSessionId: live.input.sessionId,
763
+ accountId: live.account.id,
764
+ model: typeof params.model === 'string' ? params.model : live.input.model ?? null,
765
+ effort: typeof params.effort === 'string' ? params.effort : undefined,
766
+ cwd: live.input.cwd,
767
+ caller: live.caller,
768
+ question: params.question,
769
+ context
770
+ });
771
+ }
772
+ if (typeof params.runId !== 'string')
773
+ throw invalid('A side-question run id is required.');
774
+ if (call === 'terminal.sideQuestions.read') {
775
+ const afterSequence = params.afterSequence === undefined ? -1 : params.afterSequence;
776
+ if (typeof afterSequence !== 'number')
777
+ throw invalid('A side-question event sequence is required.');
778
+ return service.read(params.runId, live.input.sessionId, afterSequence);
779
+ }
780
+ if (call === 'terminal.sideQuestions.interrupt')
781
+ return service.interrupt(params.runId, live.input.sessionId);
782
+ throw new HarnessError('unknown_method', `Unknown call ${call}.`);
783
+ }
522
784
  /** The only terminal frontend: the Bun-run OpenTUI application. */
523
785
  frontendCommand() {
524
786
  return {
@@ -538,32 +800,51 @@ export class Terminals {
538
800
  if (!input?.sessionId || !input.cwd || !input.sessionFolder)
539
801
  throw invalid('A terminal session needs an id, a working folder, and its own folder.');
540
802
  await this.dispose(input.sessionId);
541
- const command = this.frontendCommand();
542
- // A pinned account is used as it is; it is never exchanged for another one.
543
- const account = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
544
- const launch = launchEnv(accounts.dir, account);
545
- const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
546
- await this.socket.listen();
547
- await accounts.terminalBegin(account);
548
803
  const sessionId = input.sessionId;
549
- const caller = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' };
804
+ let acquiredWorktreeId = null;
805
+ let account = null;
806
+ let accountStarted = false;
550
807
  let token = null;
551
808
  try {
809
+ const worktreeId = input.run?.worktreeId ?? null;
810
+ const worktreePath = worktreeId ? this.options.worktrees?.pathOf(worktreeId) ?? null : null;
811
+ if (worktreeId && worktreePath) {
812
+ await this.options.worktrees.acquire(worktreeId, sessionId);
813
+ acquiredWorktreeId = worktreeId;
814
+ this.worktreeLeases.set(sessionId, worktreeId);
815
+ input.cwd = worktreePath;
816
+ }
817
+ const command = this.frontendCommand();
818
+ // A pinned account is used as it is; it is never exchanged for another one.
819
+ const preparedAccount = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null);
820
+ account = preparedAccount;
821
+ const launch = launchEnv(accounts.dir, preparedAccount);
822
+ const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: true, caller: { projectId: input.projectId ?? null } })) ?? null;
823
+ await this.socket.listen();
824
+ await accounts.terminalBegin(preparedAccount);
825
+ accountStarted = true;
826
+ const caller = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' };
552
827
  const broker = input.connections === false ? null : apps.grant({ sessionId, runId: null, caller, readOnly: !!input.readOnly, depth: 0 });
553
828
  // The session keeps these instructions; each of its runs records a copy, and a memory
554
829
  // block that changes later is sent along as a note.
555
830
  const variant = input.variant ? materialise(root, input.variant, { trust: this.options.trust ?? null }) : null;
831
+ const browser = new BrowserControl({ root, cwd: input.cwd, pinned: variant,
832
+ writable: Boolean(input.variant && !('dir' in input.variant)), apps, hub: this.options.hub,
833
+ plugins: () => this.options.pluginControl?.list({ cwd: input.cwd, accountId: preparedAccount.id, projectId: input.projectId ?? null }) ?? Promise.resolve([]) });
556
834
  if (variant)
557
835
  this.variantDirs.set(sessionId, variant.dir);
558
- const worldTools = await this.worldToolNames(input, account);
559
- const launchTools = [...new Set([...(input.launch?.tools ?? []), SESSION_NAME, ...worldTools])];
560
- const pack = compile(env, caller, { ...input.launch, tools: launchTools }, variant);
836
+ const worldTools = await this.worldToolNames(input, preparedAccount);
837
+ const launchTools = [...new Set([...(input.launch?.tools ?? []), CHECKLIST_WRITE, SESSION_NAME, ...worldTools])];
838
+ // The profiles replace the variant's own subagent list, which they include.
839
+ const profiles = this.options.subagents && input.subagents !== null ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
840
+ const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
841
+ const pack = compile(env, caller, { ...input.launch, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
561
842
  record(root, pack);
562
843
  mkdirSync(this.historyDir(input), { recursive: true });
563
844
  saveForSession(input.sessionFolder, pack);
564
845
  const live = {
565
846
  input,
566
- account,
847
+ account: preparedAccount,
567
848
  broker,
568
849
  caller,
569
850
  token: '',
@@ -586,6 +867,46 @@ export class Terminals {
586
867
  caller: () => ({ ...caller, runId: live.controller.activeRun?.runId ?? null }),
587
868
  broker,
588
869
  terminal: { accept: (event, data) => live.controller.accept(event, data), disconnected: () => live.controller.disconnected(), control: async (call, params) => {
870
+ if (call === 'terminal.browser.config') {
871
+ const world = this.options.world;
872
+ if (!world)
873
+ throw invalid('Browser settings are unavailable in this host.');
874
+ return { allowPrivate: world.browserConfig().allowPrivate };
875
+ }
876
+ if (call === 'terminal.browser.configure') {
877
+ const world = this.options.world;
878
+ if (!world)
879
+ throw invalid('Browser settings are unavailable in this host.');
880
+ if (typeof params.allowPrivate !== 'boolean')
881
+ throw invalid('allowPrivate must be a boolean.');
882
+ return { allowPrivate: world.configureBrowser({ allowPrivate: params.allowPrivate }).allowPrivate };
883
+ }
884
+ if (call === 'terminal.browser.snapshot')
885
+ return browser.snapshot(params.refresh === true);
886
+ if (call === 'terminal.browser.read') {
887
+ if (typeof params.id !== 'string')
888
+ throw invalid('A resource id is required.');
889
+ return browser.read(params.id, params.latest === true);
890
+ }
891
+ if (call === 'terminal.browser.importSkill') {
892
+ if (typeof params.path !== 'string' || !params.path.trim())
893
+ throw invalid('A skill folder is required.');
894
+ browser.importSkill(params.path);
895
+ bus.emit({ type: 'plugins.changed' });
896
+ return {};
897
+ }
898
+ if (call.startsWith('terminal.browser.connections.'))
899
+ return browser.connection(call.slice('terminal.browser.connections.'.length), params);
900
+ if (call === 'terminal.browser.hubPreview') {
901
+ if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
902
+ throw invalid('A catalog entry and version are required.');
903
+ return this.options.hub.preview(params.id, params.version, typeof params.offset === 'number' ? params.offset : 0);
904
+ }
905
+ if (call === 'terminal.browser.hubDocument') {
906
+ if (!this.options.hub || typeof params.id !== 'string' || typeof params.version !== 'string')
907
+ throw invalid('An installed document and version are required.');
908
+ return this.options.hub.readDocument(params.id, params.version);
909
+ }
589
910
  if (call.startsWith('terminal.doctor.')) {
590
911
  const doctor = this.options.doctor;
591
912
  if (!doctor)
@@ -616,8 +937,16 @@ export class Terminals {
616
937
  return doctor.cancel(id);
617
938
  throw invalid('Unknown diagnostic operation.');
618
939
  }
940
+ if (call.startsWith('terminal.agents.'))
941
+ return this.agentsCall(live, call, params);
619
942
  if (call.startsWith('terminal.subagents.'))
620
943
  return this.subagentsCall(live, call, params);
944
+ if (call.startsWith('terminal.sideQuestions.'))
945
+ return this.sideQuestionCall(live, call, params);
946
+ if (call.startsWith('terminal.worktrees.'))
947
+ return this.worktreesCall(live, call, params);
948
+ if (call.startsWith('terminal.annotations.'))
949
+ return this.annotationsCall(live, call, params);
621
950
  if (call === 'terminal.logs.info')
622
951
  return harnessLogInfo(this.options.stateDir);
623
952
  if (call === 'terminal.logs.read') {
@@ -653,7 +982,7 @@ export class Terminals {
653
982
  }
654
983
  if (call !== 'terminal.resources')
655
984
  return live.controller.control(call, params);
656
- const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
985
+ const current = await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, preparedAccount), install: false, caller: { projectId: input.projectId ?? null } }) ?? null;
657
986
  if (resourceIdentity(current) !== resourceIdentity(loading))
658
987
  throw new HarnessError('terminal_resources_changed', 'Trusted resources changed since this terminal was prepared. Review /plugins and reopen the terminal to load the new selection.');
659
988
  return {};
@@ -691,7 +1020,7 @@ export class Terminals {
691
1020
  [SOCKET_ENV]: this.socket.path,
692
1021
  [TOKEN_ENV]: token
693
1022
  },
694
- accountId: account.id,
1023
+ accountId: preparedAccount.id,
695
1024
  nativeSessionId: input.nativeSessionId ?? null
696
1025
  };
697
1026
  }
@@ -699,8 +1028,15 @@ export class Terminals {
699
1028
  if (token)
700
1029
  this.socket.revoke(token);
701
1030
  this.sessions.delete(sessionId);
1031
+ this.variantDirs.delete(sessionId);
1032
+ this.worldNames.delete(sessionId);
1033
+ if (this.worktreeLeases.get(sessionId) === acquiredWorktreeId)
1034
+ this.worktreeLeases.delete(sessionId);
702
1035
  apps.revokeSession(sessionId);
703
- await accounts.terminalEnd(account).catch(() => undefined);
1036
+ if (accountStarted && account)
1037
+ await accounts.terminalEnd(account).catch(() => undefined);
1038
+ if (acquiredWorktreeId)
1039
+ await this.options.worktrees?.release(acquiredWorktreeId, sessionId).catch(() => undefined);
704
1040
  throw error;
705
1041
  }
706
1042
  }
@@ -754,6 +1090,11 @@ export class Terminals {
754
1090
  }
755
1091
  this.sessions.delete(sessionId);
756
1092
  this.worldNames.delete(sessionId);
1093
+ const worktreeId = this.worktreeLeases.get(sessionId);
1094
+ this.worktreeLeases.delete(sessionId);
1095
+ if (worktreeId)
1096
+ await this.options.worktrees?.release(worktreeId, sessionId).catch(() => undefined);
1097
+ void this.options.noteCapture?.release(sessionId).catch(() => undefined);
757
1098
  void this.options.world?.release(sessionId).catch(() => undefined);
758
1099
  live.controller.dispose();
759
1100
  this.socket.revoke(live.token);
@@ -15,7 +15,7 @@ export function listTerminalTrajectoryRuns(root, sessionId, nativeSessionId) {
15
15
  if (!nativeSessionId)
16
16
  return [];
17
17
  return listRuns(root, { sessionId })
18
- .filter(run => run.nativeSessionId === nativeSessionId)
18
+ .filter(run => run.nativeSessionId === nativeSessionId || run.trigger === 'side-question' && run.sessionId === sessionId)
19
19
  .map(summary);
20
20
  }
21
21
  /** Read a run only when it belongs to both this terminal and its current native conversation. */
@@ -23,7 +23,7 @@ export function readTerminalTrajectory(root, sessionId, nativeSessionId, runId,
23
23
  if (!nativeSessionId || !Number.isSafeInteger(afterSequence ?? 0) || (afterSequence ?? 0) < 0)
24
24
  throw notFound('Run');
25
25
  const run = findRun(root, runId);
26
- if (!run || run.sessionId !== sessionId || run.nativeSessionId !== nativeSessionId)
26
+ if (!run || run.sessionId !== sessionId || (run.nativeSessionId !== nativeSessionId && run.trigger !== 'side-question'))
27
27
  throw notFound('Run');
28
28
  const events = readEvents(root, runId).filter(event => afterSequence === undefined || event.sequence > afterSequence);
29
29
  return { run: summary(run), events };
@@ -0,0 +1,3 @@
1
+ import type { AnnotateStart } from "./pages/annotatePage.js";
2
+ /** `/plugin:annotate` accepts a page address, DevTools target, or page-bridge target. */
3
+ export declare function parseAnnotate(argument: string): AnnotateStart | string;
@@ -0,0 +1,22 @@
1
+ /** `/plugin:annotate` accepts a page address, DevTools target, or page-bridge target. */
2
+ export function parseAnnotate(argument) {
3
+ const words = argument.trim().split(/\s+/).filter(Boolean);
4
+ if (!words.length)
5
+ return { kind: 'web' };
6
+ if (words[0] === '--app') {
7
+ const endpoint = words[1];
8
+ if (!endpoint)
9
+ return 'Give the app\'s DevTools address: /plugin:annotate --app http://127.0.0.1:9229';
10
+ const address = /^\d+$/.test(endpoint) ? `http://127.0.0.1:${endpoint}` : endpoint;
11
+ const name = words.slice(2).join(' ');
12
+ return { kind: 'cdp', endpoint: address, ...(name ? { name } : {}) };
13
+ }
14
+ if (words[0] === '--bridge') {
15
+ const name = words.slice(1).join(' ');
16
+ return { kind: 'bridge', ...(name ? { name } : {}) };
17
+ }
18
+ if (words.length > 1 || words[0].startsWith('--'))
19
+ return 'Use /plugin:annotate [url], /plugin:annotate --app <DevTools address>, or /plugin:annotate --bridge.';
20
+ const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(words[0]) ? words[0] : /^(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/.test(words[0]) ? `http://${words[0]}` : `https://${words[0]}`;
21
+ return { kind: 'web', url };
22
+ }
@@ -1,7 +1,11 @@
1
1
  import type { TerminalPage } from "./page.js";
2
2
  import type { TerminalCommandHost } from "./commands.js";
3
- export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log';
3
+ import { type AnnotateStart } from "./pages/annotatePage.js";
4
+ export type CommandPageId = 'doctor' | 'accounts' | 'permissions' | 'plugins' | 'hub' | 'components' | 'settings' | 'trajectory' | 'log' | 'harness' | 'skills' | 'connections' | 'tools' | 'worktree' | 'notes' | 'annotate' | 'subagents' | 'agents';
4
5
  /** Construct one independent full-screen page for a slash command. */
5
6
  export declare function createCommandPage(host: TerminalCommandHost, id: CommandPageId, options?: {
6
7
  runId?: string;
8
+ url?: string;
9
+ annotate?: AnnotateStart;
10
+ close?(): void;
7
11
  }): TerminalPage;