@jr2/orchestrator 0.6.0 → 0.7.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 (301) hide show
  1. package/console/components/app.ts +55 -15
  2. package/console/components/bottom-drawer.ts +259 -0
  3. package/console/components/drawer.ts +14 -6
  4. package/console/components/fleet.ts +36 -13
  5. package/console/components/machine-pane.ts +61 -5
  6. package/console/components/nav.ts +54 -8
  7. package/console/conversation.ts +161 -0
  8. package/console/login.ts +123 -0
  9. package/console/main.ts +600 -60
  10. package/console/store.ts +611 -64
  11. package/console/style.css +231 -0
  12. package/docs/agents-block.md +2 -2
  13. package/docs/concepts/agents-and-turns.md +86 -9
  14. package/docs/concepts/architecture.md +41 -28
  15. package/docs/concepts/callers-and-hooks.md +205 -0
  16. package/docs/concepts/charts.md +7 -5
  17. package/docs/concepts/composition.md +10 -1
  18. package/docs/concepts/converge.md +4 -2
  19. package/docs/concepts/durable-runs.md +43 -16
  20. package/docs/concepts/effects.md +50 -22
  21. package/docs/concepts/gates.md +95 -25
  22. package/docs/concepts/machines-and-workflows.md +5 -4
  23. package/docs/concepts/pools-and-sources.md +187 -11
  24. package/docs/concepts/secrets-and-trust.md +73 -33
  25. package/docs/concepts/the-menu.md +11 -0
  26. package/docs/concepts/workspaces.md +6 -5
  27. package/docs/get-started/a-model-decides.md +14 -13
  28. package/docs/get-started/an-agent-in-a-sandbox.md +43 -34
  29. package/docs/get-started/connect-a-model.md +1 -1
  30. package/docs/get-started/create-an-instance.md +17 -14
  31. package/docs/guides/add-tools-to-the-sandbox.md +55 -11
  32. package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
  33. package/docs/guides/answer-a-gate.md +74 -37
  34. package/docs/guides/bind-open-parts.md +24 -8
  35. package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
  36. package/docs/guides/cancel-a-run.md +26 -16
  37. package/docs/guides/fix-a-failed-converge.md +18 -6
  38. package/docs/guides/give-a-caller-a-scope.md +203 -0
  39. package/docs/guides/give-an-agent-mcp-tools.md +422 -0
  40. package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
  41. package/docs/guides/let-an-agent-write-a-chart.md +9 -6
  42. package/docs/guides/look-inside-a-sandbox.md +101 -31
  43. package/docs/guides/open-the-console.md +63 -14
  44. package/docs/guides/read-a-conversation.md +225 -0
  45. package/docs/guides/run-a-chart-by-name.md +17 -16
  46. package/docs/guides/run-one-worker-per-item.md +15 -9
  47. package/docs/guides/set-an-agents-access-and-dials.md +2 -0
  48. package/docs/guides/start-a-run-for-each-alert.md +290 -0
  49. package/docs/guides/start-a-run-from-a-run.md +18 -16
  50. package/docs/guides/start-a-run.md +24 -18
  51. package/docs/guides/steer-an-agent.md +332 -0
  52. package/docs/guides/use-a-hosted-provider.md +1 -1
  53. package/docs/guides/watch-a-run.md +127 -121
  54. package/docs/guides/write-a-hook-kind.md +238 -0
  55. package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
  56. package/docs/guides/write-a-workflow.md +17 -7
  57. package/docs/llms.txt +81 -15
  58. package/docs/reference/authoring-surface/agent.md +1 -1
  59. package/docs/reference/authoring-surface/answer.md +1 -1
  60. package/docs/reference/authoring-surface/arg.md +1 -1
  61. package/docs/reference/authoring-surface/basic.md +1 -1
  62. package/docs/reference/authoring-surface/bearer.md +1 -1
  63. package/docs/reference/authoring-surface/branchname.md +1 -1
  64. package/docs/reference/authoring-surface/carry.md +23 -0
  65. package/docs/reference/authoring-surface/chartschema.md +1 -1
  66. package/docs/reference/authoring-surface/chartschemafor.md +1 -1
  67. package/docs/reference/authoring-surface/command.md +1 -1
  68. package/docs/reference/authoring-surface/customize.md +4 -1
  69. package/docs/reference/authoring-surface/defineconfig.md +1 -1
  70. package/docs/reference/authoring-surface/defineevent.md +1 -1
  71. package/docs/reference/authoring-surface/dispatch.md +5 -4
  72. package/docs/reference/authoring-surface/fail.md +1 -1
  73. package/docs/reference/authoring-surface/file.md +1 -1
  74. package/docs/reference/authoring-surface/frompick.md +1 -1
  75. package/docs/reference/authoring-surface/gate.md +118 -0
  76. package/docs/reference/authoring-surface/group-agents.md +15 -0
  77. package/docs/reference/authoring-surface/group-charts.md +16 -0
  78. package/docs/reference/authoring-surface/group-composition.md +25 -0
  79. package/docs/reference/authoring-surface/group-config.md +17 -0
  80. package/docs/reference/authoring-surface/group-effects.md +48 -0
  81. package/docs/reference/authoring-surface/group-failure.md +13 -0
  82. package/docs/reference/authoring-surface/group-gates.md +16 -0
  83. package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
  84. package/docs/reference/authoring-surface/group-steering.md +12 -0
  85. package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
  86. package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
  87. package/docs/reference/authoring-surface/http.md +1 -1
  88. package/docs/reference/authoring-surface/isfailure.md +1 -1
  89. package/docs/reference/authoring-surface/isready.md +1 -1
  90. package/docs/reference/authoring-surface/jr2setup.md +13 -3
  91. package/docs/reference/authoring-surface/mcp.md +107 -0
  92. package/docs/reference/authoring-surface/open.md +5 -3
  93. package/docs/reference/authoring-surface/openbody.md +1 -1
  94. package/docs/reference/authoring-surface/paramsof.md +1 -1
  95. package/docs/reference/authoring-surface/pool.md +77 -9
  96. package/docs/reference/authoring-surface/push.md +1 -1
  97. package/docs/reference/authoring-surface/readyof.md +1 -1
  98. package/docs/reference/authoring-surface/reread.md +19 -0
  99. package/docs/reference/authoring-surface/secret.md +1 -1
  100. package/docs/reference/authoring-surface/sentby.md +1 -1
  101. package/docs/reference/authoring-surface/skip.md +22 -0
  102. package/docs/reference/authoring-surface/source.md +56 -1
  103. package/docs/reference/authoring-surface/steer.md +93 -0
  104. package/docs/reference/authoring-surface/text.md +1 -1
  105. package/docs/reference/authoring-surface/type-agentdefinition.md +14 -1
  106. package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
  107. package/docs/reference/authoring-surface/type-anyframepath.md +1 -1
  108. package/docs/reference/authoring-surface/type-anyworktreepath.md +1 -1
  109. package/docs/reference/authoring-surface/type-binding.md +1 -1
  110. package/docs/reference/authoring-surface/type-caller.md +40 -0
  111. package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
  112. package/docs/reference/authoring-surface/type-carry.md +19 -0
  113. package/docs/reference/authoring-surface/type-chainlink.md +3 -3
  114. package/docs/reference/authoring-surface/type-commandoutput.md +1 -1
  115. package/docs/reference/authoring-surface/type-customize.md +16 -1
  116. package/docs/reference/authoring-surface/type-delivered.md +17 -0
  117. package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
  118. package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
  119. package/docs/reference/authoring-surface/type-dispatchoutput.md +1 -1
  120. package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
  121. package/docs/reference/authoring-surface/type-framepath.md +1 -1
  122. package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
  123. package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
  124. package/docs/reference/authoring-surface/type-gatelook.md +37 -0
  125. package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
  126. package/docs/reference/authoring-surface/type-gatereply.md +19 -0
  127. package/docs/reference/authoring-surface/type-gateslot.md +21 -0
  128. package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
  129. package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
  130. package/docs/reference/authoring-surface/type-gateslots.md +12 -0
  131. package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
  132. package/docs/reference/authoring-surface/type-hostinjectedinput.md +1 -1
  133. package/docs/reference/authoring-surface/type-httpoutput.md +1 -1
  134. package/docs/reference/authoring-surface/type-jr2config.md +37 -2
  135. package/docs/reference/authoring-surface/type-lateroutcome.md +1 -1
  136. package/docs/reference/authoring-surface/type-lends.md +5 -1
  137. package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
  138. package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
  139. package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
  140. package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
  141. package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
  142. package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
  143. package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
  144. package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
  145. package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
  146. package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
  147. package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
  148. package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
  149. package/docs/reference/authoring-surface/type-poolnextinput.md +1 -1
  150. package/docs/reference/authoring-surface/type-poolspec.md +11 -6
  151. package/docs/reference/authoring-surface/type-pushoutput.md +1 -1
  152. package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
  153. package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
  154. package/docs/reference/authoring-surface/type-reposlot.md +1 -1
  155. package/docs/reference/authoring-surface/type-reread.md +18 -0
  156. package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
  157. package/docs/reference/authoring-surface/type-sender.md +26 -1
  158. package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
  159. package/docs/reference/authoring-surface/type-size.md +1 -1
  160. package/docs/reference/authoring-surface/type-skip.md +19 -0
  161. package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
  162. package/docs/reference/authoring-surface/type-sourcein.md +25 -0
  163. package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
  164. package/docs/reference/authoring-surface/type-sourcespec.md +16 -7
  165. package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
  166. package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
  167. package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
  168. package/docs/reference/authoring-surface/type-steerargs.md +20 -0
  169. package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
  170. package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
  171. package/docs/reference/authoring-surface/type-workspaced.md +1 -1
  172. package/docs/reference/authoring-surface/type-worktreefound.md +3 -3
  173. package/docs/reference/authoring-surface/type-worktreeinput.md +1 -1
  174. package/docs/reference/authoring-surface/type-worktreeoptions.md +1 -1
  175. package/docs/reference/authoring-surface/type-worktreeoutput.md +1 -1
  176. package/docs/reference/authoring-surface/type-worktreepath.md +1 -1
  177. package/docs/reference/authoring-surface/type-worktreeready.md +1 -1
  178. package/docs/reference/authoring-surface/type-worktreerefused.md +4 -4
  179. package/docs/reference/authoring-surface/workspace.md +1 -1
  180. package/docs/reference/authoring-surface/worktree.md +1 -1
  181. package/docs/reference/authoring-surface.md +88 -167
  182. package/docs/reference/cli.md +413 -74
  183. package/docs/reference/config.md +235 -2
  184. package/docs/reference/decisions/0002-duplex-streaming-actor.md +71 -11
  185. package/docs/reference/decisions/0005-sandbox-pod-composition.md +14 -10
  186. package/docs/reference/decisions/0006-agent-control-surface.md +4 -4
  187. package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
  188. package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
  189. package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
  190. package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
  191. package/docs/reference/decisions/0012-workspace-wrapper-machine.md +9 -2
  192. package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +20 -14
  193. package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
  194. package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +18 -14
  195. package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -12
  196. package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
  197. package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +32 -18
  198. package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +14 -1
  199. package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
  200. package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +116 -34
  201. package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +38 -37
  202. package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
  203. package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +7 -6
  204. package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +23 -7
  205. package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
  206. package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
  207. package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -18
  208. package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
  209. package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +4 -3
  210. package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
  211. package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +5 -4
  212. package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +19 -19
  213. package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
  214. package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
  215. package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
  216. package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +7 -0
  217. package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +13 -7
  218. package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +38 -30
  219. package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +64 -17
  220. package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +2 -1
  221. package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
  222. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
  223. package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +51 -23
  224. package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +94 -54
  225. package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +101 -47
  226. package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
  227. package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +17 -13
  228. package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
  229. package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
  230. package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +4 -3
  231. package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +37 -10
  232. package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
  233. package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
  234. package/docs/reference/decisions.md +6 -4
  235. package/docs/reference/glossary.md +96 -42
  236. package/docs/reference/hooks.md +1077 -0
  237. package/docs/reference/http-api.md +976 -149
  238. package/docs/why.md +2 -1
  239. package/package.json +10 -3
  240. package/src/actor.ts +36 -10
  241. package/src/agent.ts +50 -0
  242. package/src/assembly-checks.ts +530 -4
  243. package/src/callers.ts +245 -0
  244. package/src/chart.ts +52 -7
  245. package/src/config.ts +550 -1
  246. package/src/conversations.ts +453 -0
  247. package/src/custodian.ts +8 -2
  248. package/src/customize.ts +143 -4
  249. package/src/dispatch-effect.ts +142 -26
  250. package/src/durability.ts +2 -0
  251. package/src/duration.ts +20 -0
  252. package/src/effect-checks.ts +2 -1
  253. package/src/effect.ts +60 -7
  254. package/src/effector-wire.ts +272 -27
  255. package/src/gate-brand.ts +16 -0
  256. package/src/gate.ts +634 -48
  257. package/src/harness-client.ts +10 -4
  258. package/src/held-secrets.ts +72 -23
  259. package/src/holds.ts +2 -2
  260. package/src/hook-kinds/discord.ts +92 -0
  261. package/src/hook-kinds/door.ts +107 -0
  262. package/src/hook-kinds/github.ts +89 -0
  263. package/src/hook-kinds/gitlab.ts +72 -0
  264. package/src/hook-kinds/hook.ts +7 -0
  265. package/src/hook-kinds/index.ts +465 -0
  266. package/src/hook-kinds/slack.ts +151 -0
  267. package/src/hook-kinds/webhook.ts +113 -0
  268. package/src/hooks.ts +506 -0
  269. package/src/http-effect.ts +76 -38
  270. package/src/http.ts +889 -94
  271. package/src/index.ts +16 -5
  272. package/src/instance.ts +196 -26
  273. package/src/internal.ts +22 -3
  274. package/src/machine-doc.ts +26 -1
  275. package/src/mcp-effect.ts +424 -0
  276. package/src/mount-digest.ts +41 -7
  277. package/src/names.ts +64 -2
  278. package/src/notices.ts +138 -10
  279. package/src/open-body.ts +25 -11
  280. package/src/open-effect.ts +17 -4
  281. package/src/open.ts +4 -2
  282. package/src/parts.ts +87 -9
  283. package/src/party.ts +2 -1
  284. package/src/pick.ts +2 -2
  285. package/src/pool.ts +1142 -163
  286. package/src/registration.ts +203 -13
  287. package/src/run-host.ts +1509 -171
  288. package/src/run-list.ts +191 -0
  289. package/src/sandbox-kube.ts +53 -5
  290. package/src/secret-placement.ts +239 -33
  291. package/src/server.ts +53 -4
  292. package/src/setup.ts +72 -32
  293. package/src/snapshot-store.ts +565 -48
  294. package/src/steer.ts +111 -0
  295. package/src/stub-harness.ts +6 -2
  296. package/src/tokens.ts +263 -22
  297. package/src/wire.ts +17 -0
  298. package/src/workspace.ts +50 -6
  299. package/src/worktree.ts +3 -3
  300. package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
  301. package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
@@ -1,5 +1,6 @@
1
1
  // The Console shell (ADR-0032): nav over a master–detail main — the fleet rail on the left, the
2
- // selected workflow's Machine in the center, the Attention/Activity drawer on the right. One pure
2
+ // selected workflow's Machine in the center, the Attention/Activity drawer on the right, and under
3
+ // those two the bottom drawer with the selected run's conversations (ADR-0023). One pure
3
4
  // render over the store plus the bootstrap's working state; Preact's diff replaces main.js's
4
5
  // hand-rolled memo()/paint() machinery (ADR-0034). Which drawer tab the reader chose is view state
5
6
  // like the zoom — component state, never the store's.
@@ -7,8 +8,11 @@
7
8
  import { Fragment, h, type JSX } from "preact";
8
9
  import { useState } from "preact/hooks";
9
10
  import {
11
+ may,
12
+ olderAssemblyMark,
10
13
  selectedRun,
11
14
  selectedRunFailure,
15
+ selectedRunStarter,
12
16
  selectedRunGates,
13
17
  selectedRunHeld,
14
18
  selectedRunWaiting,
@@ -16,10 +20,12 @@ import {
16
20
  type Store,
17
21
  } from "../store.ts";
18
22
  import type { MachineDoc } from "../canvas.ts";
23
+ import type { LoginIssuer } from "../login.ts";
19
24
  import { Nav } from "./nav.ts";
20
25
  import { Fleet } from "./fleet.ts";
21
26
  import { Drawer, type DrawerTab } from "./drawer.ts";
22
27
  import { MachinePane } from "./machine-pane.ts";
28
+ import { BottomDrawer } from "./bottom-drawer.ts";
23
29
 
24
30
  /**
25
31
  * What the bootstrap (main.ts) hands the components: every write path in one object, so no
@@ -36,7 +42,14 @@ export type AppApi = {
36
42
  startRun(name: string, body: Record<string, unknown>): Promise<string | null>;
37
43
  /** POST one gate event; same contract as {@link AppApi.startRun}. */
38
44
  deliverGate(runId: string, gate: string, event: string, body: Record<string, unknown>): Promise<string | null>;
45
+ /** POST CANCEL to a run (ADR-0002); same contract as {@link AppApi.startRun}. */
46
+ cancelRun(runId: string): Promise<string | null>;
47
+ /** POST one Steering message to an Agent slot of a run; same contract as {@link AppApi.startRun}. */
48
+ steer(runId: string, agent: string, text: string): Promise<string | null>;
49
+ /** Keep a typed credential (empty clears it) and ask `GET /whoami` what it may do. */
39
50
  setToken(value: string): void;
51
+ /** Go to an issuer to log in (OIDC code + PKCE, ADR-0032). */
52
+ login(issuer: LoginIssuer): void;
40
53
  /** The canvas reports its zoom readout through here (nav renders it). */
41
54
  onScale(pct: number): void;
42
55
  zoomIn(): void;
@@ -61,30 +74,57 @@ export function App(props: {
61
74
  view: MachineView;
62
75
  zoomPct: number;
63
76
  initialToken: string;
77
+ logins: readonly LoginIssuer[];
78
+ loginNote: string | null;
64
79
  schemas: ReadonlyMap<string, unknown>;
65
80
  api: AppApi;
66
81
  }): JSX.Element {
67
- const { store, view, zoomPct, initialToken, schemas, api } = props;
82
+ const { store, view, zoomPct, initialToken, logins, loginNote, schemas, api } = props;
83
+ const run = selectedRun(store);
84
+ // A settled run takes no cancel and no Steering message; a live one, as its Caller's scopes allow.
85
+ const live = run !== null && store.runs.has(run.runId);
68
86
  const [chosenTab, setChosenTab] = useState<DrawerTab>("attention");
69
87
  return h(
70
88
  Fragment,
71
89
  null,
72
- h(Nav, { store, machineId: view.doc?.id ?? null, zoomPct, initialToken, onChooseTab: setChosenTab, api }),
90
+ h(Nav, {
91
+ store,
92
+ machineId: view.doc?.id ?? null,
93
+ zoomPct,
94
+ initialToken,
95
+ logins,
96
+ loginNote,
97
+ onChooseTab: setChosenTab,
98
+ api,
99
+ }),
73
100
  h(
74
101
  "main",
75
102
  null,
76
- h(Fleet, { store, schemas, api }),
77
- h(MachinePane, {
78
- view,
79
- run: selectedRun(store),
80
- selectedNodeId: store.selectedNodeId,
81
- gates: selectedRunGates(store),
82
- waiting: selectedRunWaiting(store),
83
- held: selectedRunHeld(store),
84
- failure: selectedRunFailure(store),
85
- api,
86
- }),
87
- h(Drawer, { store, chosenTab, onChooseTab: setChosenTab, api }),
103
+ h(
104
+ "div",
105
+ { id: "work" },
106
+ h(Fleet, { store, schemas, api }),
107
+ h(
108
+ "div",
109
+ { id: "upper" },
110
+ h(MachinePane, {
111
+ view,
112
+ run,
113
+ selectedNodeId: store.selectedNodeId,
114
+ gates: selectedRunGates(store),
115
+ waiting: selectedRunWaiting(store),
116
+ held: selectedRunHeld(store),
117
+ failure: selectedRunFailure(store),
118
+ starter: selectedRunStarter(store),
119
+ older: store.selectedRunId ? olderAssemblyMark(store, store.selectedRunId) : null,
120
+ canCancel: live && may(store, "cancel", run.workflow),
121
+ api,
122
+ }),
123
+ h(Drawer, { store, chosenTab, onChooseTab: setChosenTab, api }),
124
+ ),
125
+ ),
126
+ // Full width under the rail, the Machine and the right drawer (ADR-0032).
127
+ h(BottomDrawer, { store, api }),
88
128
  ),
89
129
  );
90
130
  }
@@ -0,0 +1,259 @@
1
+ // The bottom drawer (ADR-0032, ADR-0023): the selected run's kept conversations, full width across
2
+ // the bottom of the page. Closed, it is a bar of tabs, one for each Agent slot of the run,
3
+ // with a dot on a slot whose Turn is open. Open, it shows the chosen slot's conversation and follows
4
+ // a live Turn: the model's words, each tool call folded with its answer, the picks, the Steering
5
+ // messages, the notices, the refusals, and the cut and swept marks. The Steering box is beside it,
6
+ // for a Caller with `steer` on a live run.
7
+ //
8
+ // The entries are store belief (main.ts reads them); the height is the viewer's own, kept in
9
+ // localStorage (a per-viewer convenience — the page works the same without it).
10
+
11
+ import { h, type JSX } from "preact";
12
+ import { useEffect, useRef, useState } from "preact/hooks";
13
+ import { activeSlot, may, slotConversations, slotTabs, type SlotConversation, type Store } from "../store.ts";
14
+ import { foldEntries, type ConversationItem } from "../conversation.ts";
15
+ import type { AppApi } from "./app.ts";
16
+
17
+ const HEIGHT_KEY = "jr2.console.bottom.height";
18
+ const HEIGHT_DEFAULT = 300;
19
+ const HEIGHT_MIN = 120;
20
+
21
+ function storedHeight(): number {
22
+ try {
23
+ const value = Number(localStorage.getItem(HEIGHT_KEY));
24
+ const kept = Number.isFinite(value) && value >= HEIGHT_MIN ? value : HEIGHT_DEFAULT;
25
+ // A height kept on a taller window must leave room for the Machine on this one.
26
+ return Math.min(kept, maxHeight());
27
+ } catch {
28
+ return HEIGHT_DEFAULT;
29
+ }
30
+ }
31
+
32
+ /** The tallest the drawer goes: the Machine keeps at least 160px. */
33
+ function maxHeight(): number {
34
+ return Math.max(HEIGHT_MIN, window.innerHeight - 160);
35
+ }
36
+
37
+ function keepHeight(height: number): void {
38
+ try {
39
+ localStorage.setItem(HEIGHT_KEY, String(Math.round(height)));
40
+ } catch {
41
+ // no storage (a private window, or blocked): the height lasts for this page only
42
+ }
43
+ }
44
+
45
+ export function BottomDrawer(props: { store: Store; api: AppApi }): JSX.Element | null {
46
+ const { store, api } = props;
47
+ const [height, setHeight] = useState(storedHeight);
48
+ const run = store.selectedRunId;
49
+ const workflow = store.workflow;
50
+ const live = run !== null && store.runs.has(run);
51
+ const canRead = workflow !== null && may(store, "read", workflow);
52
+ const canSteer = live && workflow !== null && may(store, "steer", workflow);
53
+ // The observer's page has no conversation to show and no Agent to steer (ADR-0014).
54
+ if (run === null || (!canRead && !canSteer)) return null;
55
+
56
+ const tabs = slotTabs(store);
57
+ const slot = activeSlot(store);
58
+ const open = store.bottom.open;
59
+ const tab = tabs.find((t) => t.slot === slot);
60
+
61
+ const startResize = (e: PointerEvent): void => {
62
+ e.preventDefault();
63
+ const startY = e.clientY;
64
+ const startHeight = height;
65
+ let last = startHeight;
66
+ const move = (ev: PointerEvent): void => {
67
+ last = Math.min(maxHeight(), Math.max(HEIGHT_MIN, startHeight + (startY - ev.clientY)));
68
+ setHeight(last);
69
+ };
70
+ const up = (): void => {
71
+ removeEventListener("pointermove", move);
72
+ removeEventListener("pointerup", up);
73
+ removeEventListener("pointercancel", up);
74
+ keepHeight(last);
75
+ };
76
+ addEventListener("pointermove", move);
77
+ addEventListener("pointerup", up);
78
+ addEventListener("pointercancel", up);
79
+ };
80
+
81
+ return h(
82
+ "section",
83
+ {
84
+ id: "bottom-drawer",
85
+ class: open ? "bottom--open" : "",
86
+ style: open ? { height: `${Math.min(height, maxHeight())}px` } : undefined,
87
+ },
88
+ open ? h("div", { class: "bottom-resize", title: "drag to change the height", onPointerDown: startResize }) : null,
89
+ h(
90
+ "div",
91
+ { class: "bottom-tabs" },
92
+ h(
93
+ "button",
94
+ {
95
+ id: "bottom-toggle",
96
+ title: open ? "close the conversations" : "open the conversations",
97
+ onClick: () => api.dispatch({ kind: "bottom", open: !open }),
98
+ },
99
+ open ? "▾" : "▴",
100
+ ),
101
+ h("span", { class: "bottom-title" }, "Conversations"),
102
+ tabs.length
103
+ ? tabs.map((t) =>
104
+ h(
105
+ "button",
106
+ {
107
+ key: t.slot,
108
+ class: `slot-tab${t.slot === slot ? " tab--active" : ""}`,
109
+ title: t.open ? `${t.slot}: a Turn is open` : t.slot,
110
+ onClick: () => api.dispatch({ kind: "bottom", open: true, slot: t.slot }),
111
+ },
112
+ t.open ? h("span", { class: "live-dot" }) : null,
113
+ t.slot,
114
+ ),
115
+ )
116
+ : h("span", { class: "dim" }, canRead ? "no Agent has a conversation in this run yet" : ""),
117
+ ),
118
+ open
119
+ ? h(
120
+ "div",
121
+ { class: "bottom-body" },
122
+ h(
123
+ "div",
124
+ { class: "conversation-pane" },
125
+ canRead
126
+ ? h(Conversation, { key: `${run}/${slot ?? ""}`, conversations: slotConversations(store, slot) })
127
+ : h("div", { class: "dim" }, "to read a conversation, the Caller needs the read scope"),
128
+ ),
129
+ canSteer && slot !== null
130
+ ? h(SteeringBox, { key: `${run}/${slot}`, runId: run, slot, steerable: tab?.steerable ?? false, api })
131
+ : null,
132
+ )
133
+ : null,
134
+ );
135
+ }
136
+
137
+ /** One slot's conversations, the first first. It stays at the end while the reader is at the end,
138
+ * so a live Turn is followed; a reader who scrolls up stays where they are. */
139
+ function Conversation(props: { conversations: SlotConversation[] }): JSX.Element {
140
+ const { conversations } = props;
141
+ const ref = useRef<HTMLDivElement>(null);
142
+ const atEnd = useRef(true);
143
+ const count = conversations.reduce((n, c) => n + c.entries.length, 0);
144
+ useEffect(() => {
145
+ const el = ref.current;
146
+ if (el && atEnd.current) el.scrollTop = el.scrollHeight;
147
+ }, [count]);
148
+ const onScroll = (): void => {
149
+ const el = ref.current;
150
+ if (el) atEnd.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
151
+ };
152
+ return h(
153
+ "div",
154
+ { id: "conversation", ref, onScroll },
155
+ conversations.length
156
+ ? conversations.map((c) => conversationBlock(c))
157
+ : h("div", { class: "dim" }, "this Agent has no kept conversation yet"),
158
+ );
159
+ }
160
+
161
+ function conversationBlock(c: SlotConversation): JSX.Element {
162
+ const { row, entries, ended } = c;
163
+ const swept = row.swept ?? ended?.swept;
164
+ const cut = (row.cut || ended?.cut) && !entries.some((e) => e.kind === "cut");
165
+ return h(
166
+ "div",
167
+ { key: row.instanceId, class: "conversation-block" },
168
+ h("div", { class: "conversation-head dim" }, `${row.agent} · ${row.instanceId}${row.open ? " · Turn open" : ""}`),
169
+ swept ? h("div", { class: "conv-mark conv-swept" }, `conversation swept: ${swept}`) : null,
170
+ foldEntries(entries).map((item, i) => itemView(item, i)),
171
+ cut ? h("div", { class: "conv-mark conv-cut" }, "cut: the conversation reached its size limit") : null,
172
+ );
173
+ }
174
+
175
+ /** The first line of a text, short, for a folded item's summary. */
176
+ function firstLine(text: string, max = 120): string {
177
+ const line = text.split("\n", 1)[0] ?? "";
178
+ return line.length > max ? `${line.slice(0, max)}…` : line;
179
+ }
180
+
181
+ function itemView(item: ConversationItem, i: number): JSX.Element {
182
+ switch (item.type) {
183
+ case "text":
184
+ return h("div", { key: i, class: "conv-text" }, item.text);
185
+ case "thinking":
186
+ return h(
187
+ "details",
188
+ { key: i, class: "conv-thinking" },
189
+ h("summary", null, h("span", { class: "conv-label" }, "thinking"), ` ${firstLine(item.text)}`),
190
+ h("div", { class: "conv-body" }, item.text),
191
+ );
192
+ case "prompt":
193
+ case "notice":
194
+ case "steering":
195
+ return h(
196
+ "details",
197
+ { key: i, class: `conv-${item.type}`, open: item.type !== "prompt" },
198
+ h("summary", null, h("span", { class: "conv-label" }, item.type), ` ${firstLine(item.text)}`),
199
+ h("div", { class: "conv-body" }, item.text),
200
+ );
201
+ case "call": {
202
+ const result = item.result;
203
+ const label = result ? (result.error ? "refused" : "pick") : "tool";
204
+ return h(
205
+ "details",
206
+ { key: i, class: `conv-call${result?.error ? " conv-refused" : ""}` },
207
+ h(
208
+ "summary",
209
+ null,
210
+ h("span", { class: "conv-label" }, label),
211
+ ` ${item.tool}`,
212
+ item.input ? h("span", { class: "dim" }, ` ${firstLine(item.input, 100)}`) : null,
213
+ ),
214
+ item.input ? h("div", { class: "conv-body" }, item.input) : null,
215
+ result ? h("div", { class: "conv-body conv-answer" }, result.text) : null,
216
+ );
217
+ }
218
+ default:
219
+ return h(
220
+ "div",
221
+ { key: i, class: `conv-mark conv-${item.type}` },
222
+ item.label ? `${item.label}: ${item.text}` : item.text,
223
+ );
224
+ }
225
+ }
226
+
227
+ /**
228
+ * The Steering box (ADR-0002) beside the slot it steers. What the person types is theirs
229
+ * (uncontrolled), and a refusal shows where they typed. A slot that the run says is not steerable
230
+ * shows the box and cannot send.
231
+ */
232
+ function SteeringBox(props: { runId: string; slot: string; steerable: boolean; api: AppApi }): JSX.Element {
233
+ const { runId, slot, steerable, api } = props;
234
+ const [note, setNote] = useState<string | null>(null);
235
+ const [busy, setBusy] = useState(false);
236
+ const send = (e: Event): void => {
237
+ e.preventDefault();
238
+ const form = e.currentTarget as HTMLFormElement;
239
+ const area = form.elements.namedItem("text") as HTMLTextAreaElement | null;
240
+ const text = area?.value.trim() ?? "";
241
+ if (!text) return setNote("write the message");
242
+ setBusy(true);
243
+ setNote(null);
244
+ void api.steer(runId, slot, text).then((failure) => {
245
+ setBusy(false);
246
+ if (failure) return setNote(`not sent: ${failure}`);
247
+ if (area) area.value = "";
248
+ setNote(`sent to ${slot}: it reads the message at its next step`);
249
+ });
250
+ };
251
+ return h(
252
+ "form",
253
+ { id: "steering-box", class: "schema-form steering-box", onSubmit: send },
254
+ h("div", { class: "dim" }, steerable ? `steer ${slot}` : `${slot} cannot be steered now`),
255
+ h("textarea", { name: "text", rows: 4, disabled: !steerable }),
256
+ h("button", { type: "submit", disabled: busy || !steerable }, "send"),
257
+ h("div", { class: note?.startsWith("sent to ") ? "dim" : "form-error", hidden: note === null }, note ?? ""),
258
+ );
259
+ }
@@ -1,9 +1,10 @@
1
- // The drawer: Attention (the gate inbox) + Activity (the emit log). Tokenless there is no
2
- // Attention tab at all — the observer band of ADR-0014/0032 — and a card is only ever a re-fetched
3
- // fact: it empties when the server's next answer says so, never through bookkeeping on this side.
1
+ // The drawer: Attention (the gate inbox) + Activity (the emit log). The Attention tab is there only
2
+ // for a Caller with `answer` on some Workflow (ADR-0032, ADR-0078) — without it the page is the
3
+ // observer band of ADR-0014 — and a card is only ever a fact that `GET /gates` stated: it empties
4
+ // when the server's next answer leaves it out, never through bookkeeping on this side.
4
5
 
5
6
  import { h, type JSX } from "preact";
6
- import { visibleGates, type GateCard, type Store } from "../store.ts";
7
+ import { may, visibleGates, type GateCard, type Store } from "../store.ts";
7
8
  import { SchemaForm } from "./schema-form.ts";
8
9
  import type { AppApi } from "./app.ts";
9
10
 
@@ -18,8 +19,8 @@ export function Drawer(props: {
18
19
  api: AppApi;
19
20
  }): JSX.Element {
20
21
  const { store, chosenTab, onChooseTab, api } = props;
21
- const unlocked = store.token === "live";
22
- const tab = unlocked ? chosenTab : "activity"; // tokenless: no Attention tab — today's observer
22
+ const unlocked = may(store, "answer");
23
+ const tab = unlocked ? chosenTab : "activity"; // no `answer`: no Attention tab — the observer's drawer
23
24
  return h(
24
25
  "aside",
25
26
  { id: "drawer" },
@@ -94,6 +95,13 @@ function inboxCard(runId: string, workflow: string, view: GateCard, store: Store
94
95
  },
95
96
  h("span", { class: "gate-id" }, `⚑ ${view.gate}`),
96
97
  h("span", { class: "dim" }, `${workflow} · ${runId.slice(0, 8)}`),
98
+ view.openedAt
99
+ ? h(
100
+ "span",
101
+ { class: "dim", title: view.openedAt },
102
+ ` · open since ${new Date(view.openedAt).toLocaleTimeString()}`,
103
+ )
104
+ : null,
97
105
  ),
98
106
  view.meta && Object.keys(view.meta).length
99
107
  ? h(
@@ -5,9 +5,23 @@
5
5
  //
6
6
  // A named Chart is a Workflow (ADR-0070): it lists under its Chart wrapper, indented, with its origin
7
7
  // (`chart file` or `chart`) and, when a publish no longer admits it, `unrunnable` with the reason.
8
+ //
9
+ // Under the selected Workflow's live runs are its ended runs (ADR-0032): those `GET /runs` lists,
10
+ // with a Caller that has `read`, each with its status, and a `drifted` run with its reason. A run on
11
+ // an older Assembly carries the mark "older Assembly · <hash>" (ADR-0069).
8
12
 
9
13
  import { h, type JSX } from "preact";
10
- import { dispatchBadge, fleetEntries, fleetRuns, type FleetEntry, type ObservedRun, type Store } from "../store.ts";
14
+ import {
15
+ dispatchBadge,
16
+ fleetEntries,
17
+ fleetRuns,
18
+ heldAssembly,
19
+ may,
20
+ olderAssemblyMark,
21
+ type FleetEntry,
22
+ type RailRun,
23
+ type Store,
24
+ } from "../store.ts";
11
25
  import { SchemaForm } from "./schema-form.ts";
12
26
  import type { AppApi } from "./app.ts";
13
27
 
@@ -79,7 +93,7 @@ function workflowItem(
79
93
  )
80
94
  : null,
81
95
  h("span", { class: "wf-count" }, String(rows.filter((r) => !r.settled).length)),
82
- store.token === "live" && !entry.unrunnable
96
+ may(store, "start", name) && !entry.unrunnable
83
97
  ? h(
84
98
  "button",
85
99
  {
@@ -113,31 +127,32 @@ function startForm(name: string, schemas: ReadonlyMap<string, unknown>, api: App
113
127
  );
114
128
  }
115
129
 
116
- function runRows(
117
- name: string,
118
- rows: Array<{ run: ObservedRun; settled: boolean }>,
119
- store: Store,
120
- api: AppApi,
121
- ): JSX.Element {
130
+ function runRows(name: string, rows: RailRun[], store: Store, api: AppApi): JSX.Element {
131
+ const live = rows.filter((r) => !r.settled);
132
+ const ended = rows.filter((r) => r.settled);
122
133
  return h(
123
134
  "ul",
124
135
  { class: "wf-runs" },
125
- rows.length
126
- ? rows.map(({ run, settled }) => runRow(name, run, settled, store, api))
127
- : h("li", { class: "empty" }, "no live runs"),
136
+ live.length
137
+ ? live.map((run) => runRow(name, run, store, api))
138
+ : h("li", { key: "no-live", class: "empty" }, "no live runs"),
139
+ ended.length ? h("li", { key: "ended-head", class: "runs-head dim" }, "ended") : null,
140
+ ended.map((run) => runRow(name, run, store, api)),
128
141
  );
129
142
  }
130
143
 
131
- function runRow(name: string, run: ObservedRun, settled: boolean, store: Store, api: AppApi): JSX.Element {
144
+ function runRow(name: string, run: RailRun, store: Store, api: AppApi): JSX.Element {
132
145
  const gates = store.gates.get(run.runId)?.gates.length ?? 0;
133
146
  const selected = name === store.workflow && run.runId === store.selectedRunId;
134
147
  // A dispatched run names its caller on the row (ADR-0071), off the open observation.
135
148
  const badge = dispatchBadge(run);
149
+ const older = olderAssemblyMark(store, run.runId);
136
150
  return h(
137
151
  "li",
138
152
  {
139
153
  key: run.runId,
140
- class: `${settled ? "settled" : ""}${selected ? " selected" : ""}`.trim(),
154
+ class: `${run.settled ? "settled" : ""}${selected ? " selected" : ""}`.trim(),
155
+ title: run.reason !== undefined ? `${run.status}: ${run.reason}` : undefined,
141
156
  onClick: () => {
142
157
  if (name === store.workflow) api.dispatch({ kind: "selectRun", runId: run.runId });
143
158
  else void api.selectWorkflow(name, { push: true, runId: run.runId });
@@ -158,6 +173,14 @@ function runRow(name: string, run: ObservedRun, settled: boolean, store: Store,
158
173
  gates
159
174
  ? h("span", { class: "gate-badge", title: `${gates} open gate${gates === 1 ? "" : "s"}` }, `⚑${gates}`)
160
175
  : null,
176
+ older ? h("span", { class: "older-assembly dim", title: olderAssemblyTitle(store, run.runId) }, older) : null,
161
177
  h("span", { class: `run-status status--${run.status}` }, run.status),
178
+ run.status === "drifted" && run.reason !== undefined ? h("div", { class: "run-reason dim" }, run.reason) : null,
162
179
  );
163
180
  }
181
+
182
+ /** The whole hash, for the mark's title. */
183
+ function olderAssemblyTitle(store: Store, runId: string): string {
184
+ const hash = heldAssembly(store, runId);
185
+ return `this run is on Assembly ${hash ?? "?"}; the current one is ${store.assembly.current ?? "?"}`;
186
+ }
@@ -4,7 +4,7 @@
4
4
  // deliberately NOT vdom-ified; elk's layout is async and pan/zoom is a 60Hz transform).
5
5
 
6
6
  import { h, type JSX } from "preact";
7
- import { useEffect, useRef } from "preact/hooks";
7
+ import { useEffect, useRef, useState } from "preact/hooks";
8
8
  import {
9
9
  dispatchStrip,
10
10
  dispatchedStrip as dispatchedStripText,
@@ -32,9 +32,15 @@ export function MachinePane(props: {
32
32
  waiting: WaitingLine[];
33
33
  held: HeldLine[];
34
34
  failure: string | null;
35
+ /** Who started the selected run (ADR-0078), as one line; null when the page does not know it. */
36
+ starter: string | null;
37
+ /** "older Assembly · <hash>" for a run on an Assembly that is not the current one (ADR-0069). */
38
+ older: string | null;
39
+ /** The page's Caller may cancel the selected run, and it is live (ADR-0032). */
40
+ canCancel: boolean;
35
41
  api: AppApi;
36
42
  }): JSX.Element {
37
- const { view, run, selectedNodeId, gates, waiting, held, failure, api } = props;
43
+ const { view, run, selectedNodeId, gates, waiting, held, failure, starter, older, canCancel, api } = props;
38
44
  const canvasRef = useRef<HTMLElement>(null);
39
45
  const svgRef = useRef<SVGSVGElement>(null);
40
46
 
@@ -69,6 +75,9 @@ export function MachinePane(props: {
69
75
  h("div", { id: "error", hidden: !view.note, class: view.note?.notice ? "notice" : "" }, view.note?.text ?? ""),
70
76
  dispatchedStrip(run),
71
77
  startedStrip(run),
78
+ starterStrip(starter),
79
+ olderStrip(older),
80
+ run && canCancel ? h(RunControls, { key: run.runId, runId: run.runId, api }) : null,
72
81
  rootTransitionStrip(view.doc),
73
82
  seatsPanel(view.doc),
74
83
  failureStrip(failure),
@@ -98,6 +107,48 @@ function startedStrip(run: ObservedRun | null): JSX.Element | null {
98
107
  return h("div", { id: "dispatched", class: "dim", style: { margin: "8px 28px 0", fontFamily: "var(--mono)" } }, text);
99
108
  }
100
109
 
110
+ /** Who started the selected run (ADR-0078): a Caller, a run through a `dispatch`, or jr2. On the
111
+ * run's guarded status, so here only for a Caller with `read`. */
112
+ function starterStrip(text: string | null): JSX.Element | null {
113
+ if (!text) return null;
114
+ return h("div", { id: "started-by", class: "dim", style: { margin: "8px 28px 0", fontFamily: "var(--mono)" } }, text);
115
+ }
116
+
117
+ /** The mark of a run on an older Assembly (ADR-0069): the graph below is the run's own Machine,
118
+ * not the current one. Open structure, so it shows for every viewer. */
119
+ function olderStrip(text: string | null): JSX.Element | null {
120
+ if (!text) return null;
121
+ return h("div", { id: "older-assembly", class: "notice-line", style: { margin: "8px 28px 0" } }, text);
122
+ }
123
+
124
+ /**
125
+ * The control on the selected live run that its Caller's `cancel` opens (ADR-0032). Keyed by run,
126
+ * so a new selection starts clean. The Steering box is in the bottom drawer, beside the Agent slot
127
+ * it steers. A refusal shows where the person clicked, as the forms do.
128
+ */
129
+ function RunControls(props: { runId: string; api: AppApi }): JSX.Element {
130
+ const { runId, api } = props;
131
+ const [note, setNote] = useState<string | null>(null);
132
+ const [busy, setBusy] = useState(false);
133
+
134
+ const cancel = (): void => {
135
+ if (!confirm(`Cancel run ${runId.slice(0, 8)}?`)) return;
136
+ setBusy(true);
137
+ setNote(null);
138
+ void api.cancelRun(runId).then((failure) => {
139
+ setBusy(false);
140
+ setNote(failure ? `cancel refused: ${failure}` : null);
141
+ });
142
+ };
143
+
144
+ return h(
145
+ "div",
146
+ { id: "run-controls", style: { margin: "8px 28px 0", maxWidth: "720px" } },
147
+ h("button", { id: "cancel-run", disabled: busy, onClick: cancel }, "cancel run"),
148
+ h("div", { id: "run-controls-note", class: "form-error", hidden: note === null }, note ?? ""),
149
+ );
150
+ }
151
+
101
152
  /** Machine-level transitions ("from any state") as the strip above the Machine — an edge from the
102
153
  * root would lie, because the root has no box (it renders as the page). */
103
154
  function rootTransitionStrip(doc: MachineDoc | null): JSX.Element {
@@ -192,8 +243,9 @@ function waitingStrip(waiting: WaitingLine[]): JSX.Element {
192
243
  );
193
244
  }
194
245
 
195
- /** The selected run's outstanding picks (ADR-0024): the Agent, its pick, and since when it is held.
196
- * A held Turn has no default time bound, so this is where a human sees one that waits too long.
246
+ /** The selected run's outstanding picks (ADR-0024): the Agent, its pick, and since when it is
247
+ * outstanding. A held Turn has no default time bound, so this is where a human sees one that waits
248
+ * too long.
197
249
  * Guarded like the waiting strip: here only with a live token. */
198
250
  function heldStrip(held: HeldLine[]): JSX.Element {
199
251
  return h(
@@ -204,7 +256,11 @@ function heldStrip(held: HeldLine[]): JSX.Element {
204
256
  "li",
205
257
  { key: `${line.agent} ${line.pick}` },
206
258
  h("span", { class: "held-head" }, `${line.agent || "Agent"}'s pick ${line.pick} runs`),
207
- h("span", { class: "dim", title: line.since }, ` held since ${new Date(line.since).toLocaleTimeString()}`),
259
+ h(
260
+ "span",
261
+ { class: "dim", title: line.since },
262
+ ` outstanding since ${new Date(line.since).toLocaleTimeString()}`,
263
+ ),
208
264
  ),
209
265
  ),
210
266
  );