@north-light/crouter 0.3.192 → 0.3.194

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 (190) hide show
  1. package/dist/api/client.d.ts +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +24 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +7 -0
  12. package/dist/builtin-memory/02-lifecycle/01-resident.md +1 -1
  13. package/dist/builtin-memory/internal/examples/imessage-assistant.md +1 -1
  14. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  15. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  20. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  21. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  22. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  23. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  24. package/dist/clients/attach/input/overlay-owner.js +1 -1
  25. package/dist/clients/attach/overlays/auth.js +34 -8
  26. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  27. package/dist/clients/attach/overlays/pickers.js +37 -43
  28. package/dist/clients/attach/viewer.js +643 -620
  29. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  30. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  31. package/dist/clients/inbox/controller.d.ts +2 -3
  32. package/dist/clients/inbox/controller.js +41 -42
  33. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  34. package/dist/clients/inbox/page-adapter.js +54 -0
  35. package/dist/clients/inbox/resolve.d.ts +2 -2
  36. package/dist/clients/inbox/resolve.js +1 -1
  37. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  38. package/dist/clients/inbox/review-visibility.js +1 -1
  39. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  40. package/dist/clients/inbox/tui/ansi.js +2 -2
  41. package/dist/clients/inbox/tui/input.d.ts +1 -3
  42. package/dist/clients/inbox/tui/input.js +242 -553
  43. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  44. package/dist/clients/inbox/tui/panel.js +59 -187
  45. package/dist/clients/inbox/tui/render.d.ts +0 -25
  46. package/dist/clients/inbox/tui/render.js +165 -416
  47. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  48. package/dist/clients/inbox/tui/slots.js +156 -0
  49. package/dist/clients/inbox/tui/types.d.ts +25 -13
  50. package/dist/clients/inbox/tui.js +5 -3
  51. package/dist/commands/attention.js +2 -2
  52. package/dist/commands/human/doc.js +66 -20
  53. package/dist/commands/human/prompts.d.ts +3 -3
  54. package/dist/commands/human/prompts.js +158 -112
  55. package/dist/commands/human/queue.d.ts +0 -1
  56. package/dist/commands/human/queue.js +34 -94
  57. package/dist/commands/human/shared.d.ts +10 -4
  58. package/dist/commands/human/shared.js +62 -13
  59. package/dist/commands/human.js +8 -4
  60. package/dist/commands/memory/read.js +13 -5
  61. package/dist/commands/node/bash.js +18 -30
  62. package/dist/commands/node/lifecycle.js +2 -2
  63. package/dist/commands/surface/node/focus.js +1 -1
  64. package/dist/core/__tests__/human-deliver.test.js +40 -46
  65. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  66. package/dist/core/__tests__/seam/dormancy-release.test.js +64 -10
  67. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  68. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  69. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  70. package/dist/core/auth-interaction.d.ts +2 -2
  71. package/dist/core/bash-jobs.d.ts +43 -0
  72. package/dist/core/bash-jobs.js +96 -2
  73. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  74. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  75. package/dist/core/canvas/attention.js +1 -1
  76. package/dist/core/canvas/browse/app.js +1 -1
  77. package/dist/core/canvas/nav-model.d.ts +1 -1
  78. package/dist/core/canvas/nav-model.js +1 -1
  79. package/dist/core/canvas/remote-canvas-source.js +1 -1
  80. package/dist/core/canvas/types.d.ts +2 -1
  81. package/dist/core/config.js +4 -1
  82. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  83. package/dist/core/human/__tests__/page-render.test.js +49 -0
  84. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  85. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  86. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  87. package/dist/core/human/__tests__/page.test.js +139 -0
  88. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  89. package/dist/core/human/claim.js +2 -2
  90. package/dist/core/human/convention.d.ts +4 -4
  91. package/dist/core/human/convention.js +6 -13
  92. package/dist/core/human/markdown-html.d.ts +8 -0
  93. package/dist/core/human/markdown-html.js +581 -0
  94. package/dist/core/human/page-catalog.d.ts +6 -0
  95. package/dist/core/human/page-catalog.js +43 -0
  96. package/dist/core/human/page-render.d.ts +3 -0
  97. package/dist/core/human/page-render.js +131 -0
  98. package/dist/core/human/page-schema.d.ts +312 -0
  99. package/dist/core/human/page-schema.js +291 -0
  100. package/dist/core/human/page-synth.d.ts +9 -0
  101. package/dist/core/human/page-synth.js +17 -0
  102. package/dist/core/human/page.d.ts +22 -0
  103. package/dist/core/human/page.js +427 -0
  104. package/dist/core/human/review-schema.d.ts +19 -0
  105. package/dist/core/human/review-schema.js +34 -0
  106. package/dist/core/human/scan.d.ts +12 -3
  107. package/dist/core/human/scan.js +69 -26
  108. package/dist/core/human/summary.d.ts +5 -6
  109. package/dist/core/human/summary.js +54 -37
  110. package/dist/core/human/tickets.d.ts +61 -11
  111. package/dist/core/human/tickets.js +100 -57
  112. package/dist/core/human/types.d.ts +21 -66
  113. package/dist/core/human/types.js +1 -5
  114. package/dist/core/inspector/model.d.ts +5 -0
  115. package/dist/core/inspector/model.js +5 -0
  116. package/dist/core/inspector/text.js +2 -2
  117. package/dist/core/inspector/tui.js +10 -3
  118. package/dist/core/keybindings/catalog.d.ts +1 -1
  119. package/dist/core/keybindings/catalog.js +4 -2
  120. package/dist/core/preview-registry.js +0 -1
  121. package/dist/core/review/ticket-filter.d.ts +1 -1
  122. package/dist/core/review/ticket-filter.js +2 -8
  123. package/dist/core/runtime/broker/read-ops.js +3 -0
  124. package/dist/core/runtime/broker.js +19 -10
  125. package/dist/core/runtime/package-health.js +1 -1
  126. package/dist/core/runtime/persona.js +2 -1
  127. package/dist/core/runtime/tmux-bindings.js +8 -1
  128. package/dist/core/termrender/termrender.d.ts +1 -1
  129. package/dist/core/termrender/termrender.js +2 -2
  130. package/dist/core/termrender/version.d.ts +1 -1
  131. package/dist/core/termrender/version.js +1 -1
  132. package/dist/core/tui/page-host.d.ts +1 -1
  133. package/dist/core/tui/page-host.js +2 -2
  134. package/dist/core/user-settings.d.ts +19 -0
  135. package/dist/core/user-settings.js +22 -0
  136. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  137. package/dist/daemon/api/handlers/bash-jobs.js +90 -0
  138. package/dist/daemon/api/handlers/human.js +16 -14
  139. package/dist/daemon/api/handlers/inbox.js +311 -227
  140. package/dist/daemon/api/router.d.ts +5 -1
  141. package/dist/daemon/api/router.js +5 -0
  142. package/dist/daemon/api/server.js +2 -0
  143. package/dist/daemon/cron-run.js +23 -10
  144. package/dist/daemon/human/finish.d.ts +8 -3
  145. package/dist/daemon/human/finish.js +35 -15
  146. package/dist/daemon/reconcilers/broker-supervision.js +7 -1
  147. package/dist/daemon/reconcilers/dormant-inbox.js +8 -0
  148. package/dist/daemon/reconcilers/live-obligation.d.ts +2 -0
  149. package/dist/daemon/reconcilers/live-obligation.js +11 -0
  150. package/dist/pages/bundle.css +1 -0
  151. package/dist/pages/bundle.js +1347 -0
  152. package/dist/pages/comments.d.ts +34 -0
  153. package/dist/pages/comments.js +72 -0
  154. package/dist/pages/elements/cards.d.ts +17 -0
  155. package/dist/pages/elements/cards.js +785 -0
  156. package/dist/pages/elements/chart.d.ts +1 -0
  157. package/dist/pages/elements/chart.js +648 -0
  158. package/dist/pages/elements/options.d.ts +21 -0
  159. package/dist/pages/elements/options.js +604 -0
  160. package/dist/pages/elements/pages.d.ts +24 -0
  161. package/dist/pages/elements/pages.js +355 -0
  162. package/dist/pages/elements/slot.d.ts +21 -0
  163. package/dist/pages/elements/slot.js +105 -0
  164. package/dist/pages/elements/table.d.ts +19 -0
  165. package/dist/pages/elements/table.js +906 -0
  166. package/dist/pages/elements/text.d.ts +25 -0
  167. package/dist/pages/elements/text.js +951 -0
  168. package/dist/pages/entry.d.ts +8 -0
  169. package/dist/pages/entry.js +8 -0
  170. package/dist/pages/host.d.ts +132 -0
  171. package/dist/pages/host.js +149 -0
  172. package/dist/pages/register.d.ts +2 -0
  173. package/dist/pages/register.js +3 -0
  174. package/dist/pages/slot-config.d.ts +36 -0
  175. package/dist/pages/slot-config.js +50 -0
  176. package/dist/pages/types.d.ts +98 -0
  177. package/dist/pages/types.js +23 -0
  178. package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -2
  179. package/dist/pi-extensions/canvas-bash-valve.js +25 -7
  180. package/dist/types.d.ts +4 -0
  181. package/dist/types.js +2 -0
  182. package/package.json +8 -8
  183. package/runtime.lock.json +156 -101
  184. package/scripts/install-runtime.mjs +172 -5
  185. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  186. package/dist/clients/inbox/deck-adapter.js +0 -63
  187. package/dist/core/human/deck-factories.d.ts +0 -8
  188. package/dist/core/human/deck-factories.js +0 -15
  189. package/dist/core/human/deck-schema.d.ts +0 -70
  190. package/dist/core/human/deck-schema.js +0 -92
@@ -1,6 +1,6 @@
1
1
  import type { BindingDefinition, SafetyRequirement } from './types.js';
2
2
  import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
- type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'resume' | 'fork' | 'demote' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
3
+ type TmuxMenuAction = 'open' | 'attach' | 'show' | 'view-settings' | 'fold-tools' | 'metadata' | 'promote' | 'demote-mode' | 'resume' | 'fork' | 'demote' | 'make-resident' | 'detach' | 'close-subtree' | 'background-bash' | 'file-review' | 'pin' | 'reload-viewer' | 'recap' | 'context' | 'providers' | 'plugins' | 'graph' | 'focus-manager' | 'copy-id' | 'issues';
4
4
  type TmuxMenuEntryAction = Exclude<TmuxMenuAction, 'open' | 'attach'> | `issue.${'idea' | 'bug'}`;
5
5
  type GraphAction = 'down' | 'up' | 'first' | 'last' | 'collapse-or-manager' | 'expand-or-child' | 'focus' | 'revive' | 'focus-manager' | 'close-subtree' | 'dismiss';
6
6
  type BrowseAction = 'quit' | 'down' | 'up' | 'first' | 'last' | 'expand-or-child' | 'collapse-or-parent' | 'toggle-fold' | 'tab-next' | 'tab-previous' | `tab.${1 | 2 | 3 | 4 | 5}` | 'close-subtree' | 'confirm-close' | 'sort' | 'scope' | 'profile' | 'residents' | 'preview' | 'search' | 'revive-now' | 'focus' | 'focus-background';
@@ -38,10 +38,12 @@ const SETUP_TAB_CONTEXTS = ['setup.login', 'setup.list', 'setup.keybindings', 's
38
38
  export const BINDING_CATALOG = Object.freeze([
39
39
  b('crtr.tmux.menu.open', 'Global tmux', 'Open crouter menu', 'Open the crouter action menu.', ['alt+c'], 'host-global', ['tmux.root']),
40
40
  b('crtr.tmux.menu.attach', 'Global tmux', 'Attach actions', 'Open the attach-viewer action submenu.', ['a'], 'tmux-menu-selector', ['tmux.menu']),
41
- ...menuEntry('promote', 'Promote node', 'Promote the focused node to orchestrator.', ['o'], ['tmux.menu']),
41
+ ...menuEntry('promote', 'Promote to orchestrator', 'Promote the focused node to orchestrator — it holds a roadmap and delegates.', ['o'], ['tmux.menu']),
42
+ ...menuEntry('demote-mode', 'Demote to base node', 'Demote the focused orchestrator back to a base node — hands-on again in its own window, no roadmap to drive.', ['shift+o'], ['tmux.menu']),
42
43
  ...menuEntry('resume', 'Find node', 'Open the canvas navigator and go to the chosen node.', ['/'], ['tmux.menu']),
43
44
  ...menuEntry('fork', 'Fork node', 'Fork the focused node into an independent idle root.', ['f'], ['tmux.menu']),
44
- ...menuEntry('demote', 'Demote node', 'Demote the focused node to terminal — it keeps running, owes a final up the spine, and reaps when done.', ['d'], ['tmux.menu']),
45
+ ...menuEntry('demote', 'Demote to terminal', 'Demote the focused node to terminal — it keeps running, owes a final up the spine, and reaps when done.', ['d'], ['tmux.menu']),
46
+ ...menuEntry('make-resident', 'Make resident', 'Flip the focused node to resident — interactable, never forced to submit a final, and wakeable while attached or awaiting something live.', ['shift+r'], ['tmux.menu']),
45
47
  ...menuEntry('detach', 'Demote and close viewer', 'Demote the focused node and close its viewer; its broker keeps running off-screen.', ['shift+d'], ['tmux.menu']),
46
48
  ...menuEntry('close-subtree', 'Close subtree', 'Close the focused node and its subtree.', ['x'], ['tmux.menu']),
47
49
  ...menuEntry('background-bash', 'Background running bash', 'Hand the focused node’s running bash command to its background job system.', ['b'], ['tmux.menu']),
@@ -110,7 +110,6 @@ const LEAF_ICON_CP = {
110
110
  'human show': 0xf108,
111
111
  'human cancel': 0xf00d,
112
112
  'human list': 0xf03a,
113
- 'human deck': 0xf0db,
114
113
  'human resolve': 0xf00c,
115
114
  'memory list': 0xf03a,
116
115
  'memory read': 0xf02d,
@@ -12,6 +12,6 @@ import type { TicketSummary } from '../human/types.js';
12
12
  export declare function isReviewTicketVisible(bridgeNodeId: string): boolean;
13
13
  /**
14
14
  * Filters a ticket summary array to remove hidden terminal-review tickets.
15
- * Deck tickets pass through unchanged. Review tickets are filtered by state.
15
+ * Page tickets pass through unchanged. Review tickets are filtered by state.
16
16
  */
17
17
  export declare function filterTerminalReviewTickets(items: TicketSummary[]): TicketSummary[];
@@ -19,14 +19,8 @@ export function isReviewTicketVisible(bridgeNodeId) {
19
19
  }
20
20
  /**
21
21
  * Filters a ticket summary array to remove hidden terminal-review tickets.
22
- * Deck tickets pass through unchanged. Review tickets are filtered by state.
22
+ * Page tickets pass through unchanged. Review tickets are filtered by state.
23
23
  */
24
24
  export function filterTerminalReviewTickets(items) {
25
- return items.filter((item) => {
26
- // Deck tickets always pass through
27
- if (item.kind === 'deck')
28
- return true;
29
- // Review tickets are filtered by visibility
30
- return isReviewTicketVisible(item.id);
31
- });
25
+ return items.filter((item) => (item.kind === 'review' ? isReviewTicketVisible(item.id) : true));
32
26
  }
@@ -117,6 +117,7 @@ export function buildSettingsData(id, session) {
117
117
  terminalTheme: 'dark',
118
118
  availableThemes,
119
119
  hideThinkingBlock: manager.getHideThinkingBlock(),
120
+ mermaidRenderingMode: manager.getMermaidRenderingMode(),
120
121
  collapseChangelog: manager.getCollapseChangelog(),
121
122
  enableInstallTelemetry: manager.getEnableInstallTelemetry(),
122
123
  doubleEscapeAction: manager.getDoubleEscapeAction(),
@@ -127,6 +128,8 @@ export function buildSettingsData(id, session) {
127
128
  quietStartup: manager.getQuietStartup(),
128
129
  clearOnShrink: manager.getClearOnShrink(),
129
130
  showTerminalProgress: manager.getShowTerminalProgress(),
131
+ tuiMode: manager.getTuiMode(),
132
+ fullscreenScrollbar: manager.getFullscreenScrollbar(),
130
133
  warnings: manager.getWarnings(),
131
134
  defaultProjectTrust: manager.getDefaultProjectTrust(),
132
135
  showCacheMissNotices: manager.getShowCacheMissNotices(),
@@ -722,14 +722,20 @@ async function resolveLaunchModel(services, cfg) {
722
722
  /**
723
723
  * Wrap a live ModelRuntime so a model-less `createAgentSessionFromServices`
724
724
  * call can't have the SDK's own `findInitialModel` silently pick a model
725
- * behind crouter's launch policy. Both `getModel()` (step 3: settingsManager's
726
- * durable `defaultProvider`/`defaultModel` — persisted into a GLOBAL
727
- * settings.json by every `session.setModel()` call the broker itself already
728
- * makes at broker.ts:609/675/1606/1650, so this is a live bypass on any
729
- * machine that has ever picked a model, not a theoretical corner case) and
730
- * `getAvailable()` (step 4: registry default) must both be intercepted —
731
- * wrapping only `getAvailable()` leaves `getModel()`'s step-3 path (and the
732
- * resume path's model restore, sdk.js:88) wide open. Everything else
725
+ * behind crouter's launch policy. Every model-list read the SDK performs during
726
+ * construction must be intercepted:
727
+ * - `getModel()` — findInitialModel step 3 (settingsManager's durable
728
+ * `defaultProvider`/`defaultModel`, persisted into a GLOBAL settings.json by
729
+ * every `session.setModel()` call the broker itself already makes, so this
730
+ * is a live bypass on any machine that has ever picked a model, not a
731
+ * theoretical corner case) and the resume path's model restore.
732
+ * - `getAvailableSnapshot()` — findInitialModel step 4 and
733
+ * restoreModelFromSession's fallback both take the registry default off this
734
+ * SYNCHRONOUS snapshot (model-resolver.js:511/556).
735
+ * - `getAvailable()` — the async refresh-and-list sibling, masked alongside it
736
+ * so no construction-time list read can reach the real registry through
737
+ * either accessor.
738
+ * Masking any one of them alone leaves the others wide open. Everything else
733
739
  * (`getModels`, `hasConfiguredAuth`, `refresh`, …) passes through untouched via
734
740
  * `Reflect.get`. Used ONLY
735
741
  * for the one model-less construction call; the `services` returned to every
@@ -739,7 +745,7 @@ async function resolveLaunchModel(services, cfg) {
739
745
  * CONSTRUCTION-ONLY MASK: the SDK stores `options.services.modelRuntime` as the
740
746
  * session's private `_modelRuntime` for its ENTIRE lifetime and never replaces
741
747
  * it (agent-session-services.js:104-119, sdk.js:253). A permanent mask would
742
- * leave list_models (getAvailable→[]), cycle_model, and provider-rotation
748
+ * leave list_models (the available list→[]), cycle_model, and provider-rotation
743
749
  * fallback (getModel()→undefined) broken forever for any model-less-booted node,
744
750
  * even after a later /login selects a model. Model selection can only happen
745
751
  * DURING construction, so this proxy self-heals: it masks while `masked`, then
@@ -750,7 +756,10 @@ function modelLessRegistry(real) {
750
756
  let masked = true;
751
757
  const runtime = new Proxy(real, {
752
758
  get(target, prop, receiver) {
753
- // `getAvailable` is async on ModelRuntime; findInitialModel awaits it.
759
+ // `getAvailableSnapshot` is the sync list findInitialModel reads; its
760
+ // async sibling `getAvailable` refreshes first and returns the same set.
761
+ if (masked && prop === 'getAvailableSnapshot')
762
+ return () => [];
754
763
  if (masked && prop === 'getAvailable')
755
764
  return async () => [];
756
765
  if (masked && prop === 'getModel')
@@ -23,7 +23,7 @@ import { expandTilde } from '../fs-utils.js';
23
23
  export function rendererWarning() {
24
24
  if (isRendererReady())
25
25
  return null;
26
- return 'crtr: the pinned termrender renderer is not provisioned — decks, reviews, and inline diagrams render as plaintext.\n' +
26
+ return 'crtr: the pinned termrender renderer is not provisioned — pages, reviews, and inline diagrams render as plaintext.\n' +
27
27
  ' Fix: run `crtr sys setup` (requires `uv`: curl -LsSf https://astral.sh/uv/install.sh | sh).';
28
28
  }
29
29
  /**
@@ -4,7 +4,8 @@
4
4
  // • mode — base (hands-on; yields mode-preservingly when another window is needed) ↔ orchestrator
5
5
  // (delegates, holds a roadmap, survives refresh cycles + yields)
6
6
  // • lifecycle — terminal (owes a final up the spine, reaps when done) ↔
7
- // resident (interactable, stays dormant, never forced to submit)
7
+ // resident (interactable, never forced to submit; unattended
8
+ // obligation-free conversations complete after the daemon grace)
8
9
  //
9
10
  // Whenever EITHER axis changes from the value the node was last GIVEN guidance
10
11
  // for, the node must be prompt-injected with guidance for its new state —
@@ -19,7 +19,12 @@ const LEAF_MENU_ACTIONS = {
19
19
  'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
20
20
  'crtr.tmux.menu.resume': { description: 'find node', action: { kind: 'keys', keys: '/resume-node' } },
21
21
  'crtr.tmux.menu.fork': { description: 'fork into idle root', action: { kind: 'direct', run: 'node fork --pane {pane}' } },
22
- 'crtr.tmux.menu.demote': { description: 'demote (finish when done)', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
22
+ // Mode demote is the mirror of `/promote`: `node config` is the durable
23
+ // attribute setter, and the persona injector delivers the hands-on-again
24
+ // guidance at the node's next turn boundary.
25
+ 'crtr.tmux.menu.demote-mode': { description: 'demote to base node', action: { kind: 'command', run: 'node config --mode base --pane {pane}' } },
26
+ 'crtr.tmux.menu.demote': { description: 'demote to terminal (finish when done)', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
27
+ 'crtr.tmux.menu.make-resident': { description: 'make resident (interactable)', action: { kind: 'command', run: 'node config --lifecycle resident --pane {pane}' } },
23
28
  'crtr.tmux.menu.detach': { description: 'demote + close viewer', action: { kind: 'command', run: 'node lifecycle demote --pane {pane} --detach' } },
24
29
  'crtr.tmux.menu.close-subtree': { description: 'close agent + subtree', action: { kind: 'command', run: 'node lifecycle close --pane {pane}' } },
25
30
  'crtr.tmux.menu.background-bash': { description: 'background running bash', action: { kind: 'direct', run: 'node bash background --pane {pane}' } },
@@ -296,7 +301,9 @@ function menuItems(bindings, entries) {
296
301
  addGroup('Lifecycle', boundItems([
297
302
  'crtr.tmux.menu.fork',
298
303
  'crtr.tmux.menu.promote',
304
+ 'crtr.tmux.menu.demote-mode',
299
305
  'crtr.tmux.menu.demote',
306
+ 'crtr.tmux.menu.make-resident',
300
307
  'crtr.tmux.menu.detach',
301
308
  'crtr.tmux.menu.close-subtree',
302
309
  ]));
@@ -41,7 +41,7 @@ export declare function renderMarkdownWithMap(md: string, width: number): Render
41
41
  * The shared block-aware mapped render: prose blocks keep the readability cap
42
42
  * (`proseWidth`), while diagram blocks are re-rendered at the pane's full
43
43
  * `paneWidth` and spliced back in by block index. Both surfaces (terminal
44
- * review and the ask deck) go through this, so "which blocks may be wide" is
44
+ * review and the ask page) go through this, so "which blocks may be wide" is
45
45
  * decided once, from the renderer's own block types, never by heuristics on
46
46
  * the emitted rows.
47
47
  *
@@ -611,7 +611,7 @@ export function renderMarkdownWithMap(md, width) {
611
611
  * The shared block-aware mapped render: prose blocks keep the readability cap
612
612
  * (`proseWidth`), while diagram blocks are re-rendered at the pane's full
613
613
  * `paneWidth` and spliced back in by block index. Both surfaces (terminal
614
- * review and the ask deck) go through this, so "which blocks may be wide" is
614
+ * review and the ask page) go through this, so "which blocks may be wide" is
615
615
  * decided once, from the renderer's own block types, never by heuristics on
616
616
  * the emitted rows.
617
617
  *
@@ -664,7 +664,7 @@ export function renderMarkdownBlockAwareLines(md, proseWidth, paneWidth) {
664
664
  export function checkMarkdown(md) {
665
665
  ensureRenderer();
666
666
  // Renderer unavailable → don't block validation; the body just renders as
667
- // plaintext later. Bricking deck validation here would be the wrong default.
667
+ // plaintext later. Bricking page validation here would be the wrong default.
668
668
  if (rendererState !== 'ready')
669
669
  return { ok: true };
670
670
  const result = spawnSync(VENV_BIN, ['doc', 'check'], {
@@ -1 +1 @@
1
- export declare const TERMRENDER_VERSION = "4.12.6";
1
+ export declare const TERMRENDER_VERSION = "4.12.7";
@@ -1 +1 @@
1
- export const TERMRENDER_VERSION = '4.12.6';
1
+ export const TERMRENDER_VERSION = '4.12.7';
@@ -1,4 +1,4 @@
1
- import { ProcessTerminal, TUI, type Component, type Focusable } from "@earendil-works/pi-tui";
1
+ import { ProcessTerminal, type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
2
2
  import { readRawScopeConfig } from "../config.js";
3
3
  import { type BindingId, type BindingResolution } from "../keybindings/index.js";
4
4
  /** A full-screen page: a focusable TUI component that cleans up after itself. */
@@ -6,7 +6,7 @@
6
6
  // restore path that survives an exception, a signal, and process exit. Anything
7
7
  // a single page needs beyond that — pi's model runtime, a canvas client — the
8
8
  // page's own runner builds and closes over before calling in here.
9
- import { ProcessTerminal, TUI, } from "@earendil-works/pi-tui";
9
+ import { ProcessTerminal, TuiMainScreen, } from "@earendil-works/pi-tui";
10
10
  import { applyTheme } from "../../clients/attach/config.js";
11
11
  import { readRawScopeConfig } from "../config.js";
12
12
  import { resolveKeybindings, } from "../keybindings/index.js";
@@ -20,7 +20,7 @@ export async function runTuiPage(createSurface, interceptCancel) {
20
20
  const rawUserConfig = readRawScopeConfig("user");
21
21
  const bindings = resolveKeybindings(rawUserConfig?.keybindings);
22
22
  const terminal = new ProcessTerminal();
23
- const tui = new TUI(terminal, true);
23
+ const tui = new TuiMainScreen(terminal, true);
24
24
  return new Promise((resolve, reject) => {
25
25
  let surface;
26
26
  let restored = false;
@@ -196,6 +196,21 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
196
196
  setFromString: (config: ScopeConfig, raw: string) => string | null;
197
197
  };
198
198
  };
199
+ html_pages: {
200
+ surface: "settings";
201
+ setting: {
202
+ key: "html_pages";
203
+ label: string;
204
+ description: string;
205
+ valueConstraint: string;
206
+ applies: string;
207
+ group: "display";
208
+ order: number;
209
+ format: (config: ScopeConfig) => "on" | "off";
210
+ cycle: (config: ScopeConfig) => void;
211
+ setFromString: (config: ScopeConfig, raw: string) => string | null;
212
+ };
213
+ };
199
214
  fold_finished_tools: {
200
215
  surface: "settings";
201
216
  setting: {
@@ -265,6 +280,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
265
280
  surface: "specialized";
266
281
  owner: string;
267
282
  };
283
+ page_components: {
284
+ surface: "specialized";
285
+ owner: string;
286
+ };
268
287
  remoteCanvas: {
269
288
  surface: "specialized";
270
289
  owner: string;
@@ -254,6 +254,27 @@ export const CONFIG_SETTINGS_COVERAGE = {
254
254
  },
255
255
  },
256
256
  },
257
+ html_pages: {
258
+ surface: 'settings',
259
+ setting: {
260
+ key: 'html_pages',
261
+ label: 'HTML pages',
262
+ description: 'Teach agent help the HTML page-authoring dialect. Off keeps page authoring Markdown-only.',
263
+ valueConstraint: 'true|false',
264
+ applies: 'Applies on the next help request.',
265
+ group: 'display',
266
+ order: 70,
267
+ format: (config) => config.html_pages ? 'on' : 'off',
268
+ cycle: (config) => { config.html_pages = !config.html_pages; },
269
+ setFromString: (config, raw) => {
270
+ const value = booleanFromString(raw, 'html_pages');
271
+ if (typeof value === 'string')
272
+ return value;
273
+ config.html_pages = value;
274
+ return null;
275
+ },
276
+ },
277
+ },
257
278
  fold_finished_tools: {
258
279
  surface: 'settings',
259
280
  setting: {
@@ -323,6 +344,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
323
344
  modelRoutes: { surface: 'specialized', owner: 'Models route declarations' },
324
345
  modelRouting: { surface: 'specialized', owner: 'Models route declarations' },
325
346
  kinds: { surface: 'specialized', owner: 'Models tab and kind registry' },
347
+ page_components: { surface: 'specialized', owner: 'Page component catalog: product-registered page slot kinds' },
326
348
  remoteCanvas: { surface: 'specialized', owner: 'canvas config commands' },
327
349
  spawnEnv: { surface: 'specialized', owner: 'spawn environment allow-list' },
328
350
  };
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const bashJobRoutes: RouteTable;
@@ -0,0 +1,90 @@
1
+ // Daemon-owned read and stop surfaces for one node's file-backed background
2
+ // bash jobs. The attach roster continues to read the same directory directly;
3
+ // these routes exist for remote presenters that cannot see the node host.
4
+ import { appendInbox } from '../../../core/feed/inbox.js';
5
+ import { contextDir, isSafeNodeId } from '../../../core/canvas/paths.js';
6
+ import { getNode } from '../../../core/canvas/canvas.js';
7
+ import { isBrokerLive } from '../../../core/runtime/model-swap.js';
8
+ import { assertNotFinalized } from '../../../core/runtime/reopen.js';
9
+ import { reviveNode } from '../../../core/runtime/revive.js';
10
+ import { activeBackgroundBashJobs, bashJobPaths, isBashJobProcessGroupAlive, readJobPgid, isSafeBashJobId as isSafeCoreBashJobId, stopBackgroundBashJob, } from '../../../core/bash-jobs.js';
11
+ import { ApiError } from '../../../api/index.js';
12
+ import { notFound } from '../../../core/errors.js';
13
+ function requireTarget(ctx) {
14
+ const nodeId = ctx.params['id'];
15
+ if (nodeId === undefined || !isSafeNodeId(nodeId)) {
16
+ throw new ApiError(400, 'invalid_node_id', `invalid node id: ${JSON.stringify(nodeId)}`);
17
+ }
18
+ const node = getNode(nodeId);
19
+ if (node === null)
20
+ throw notFound(`unknown node: ${nodeId}`, { received: nodeId });
21
+ const rawJobId = ctx.params['jobId'];
22
+ if (rawJobId !== undefined) {
23
+ if (!isSafeCoreBashJobId(rawJobId)) {
24
+ throw new ApiError(400, 'invalid_job_id', `invalid background job id: ${JSON.stringify(rawJobId)}`);
25
+ }
26
+ return { nodeId, jobId: rawJobId, dir: contextDir(nodeId) };
27
+ }
28
+ return { nodeId, dir: contextDir(nodeId) };
29
+ }
30
+ function handleList(ctx) {
31
+ const { nodeId, dir } = requireTarget(ctx);
32
+ const now = Date.now();
33
+ const body = activeBackgroundBashJobs(dir).map((job) => {
34
+ const pgid = readJobPgid(dir, job.jobId) ?? null;
35
+ return {
36
+ job_id: job.jobId,
37
+ command: job.command,
38
+ purpose: job.purpose,
39
+ started_at: new Date(job.startedAtMs).toISOString(),
40
+ elapsed_ms: Math.max(0, now - job.startedAtMs),
41
+ pgid,
42
+ pgid_alive: pgid !== null && isBashJobProcessGroupAlive(pgid),
43
+ };
44
+ });
45
+ return { status: 200, body };
46
+ }
47
+ async function handleStop(ctx) {
48
+ const { nodeId, jobId, dir } = requireTarget(ctx);
49
+ if (jobId === undefined)
50
+ throw new ApiError(400, 'invalid_request', 'a job id is required');
51
+ const stopped = await stopBackgroundBashJob(dir, jobId);
52
+ if (stopped.kind === 'missing') {
53
+ throw new ApiError(404, 'job_not_found', `no background job ${jobId} for ${nodeId}`);
54
+ }
55
+ // A presenter's roster can be a snapshot; a row whose job has since finished
56
+ // (or was already stopped) is gone, not stoppable.
57
+ if (stopped.kind === 'retired') {
58
+ throw new ApiError(404, 'job_not_found', `background job ${jobId} for ${nodeId} is no longer running`, { log_path: stopped.logPath });
59
+ }
60
+ if (stopped.kind === 'not-stoppable') {
61
+ throw new ApiError(409, 'job_not_stoppable', `job ${jobId} recorded no process group`, { log_path: stopped.logPath });
62
+ }
63
+ // Match `crtr node bash kill`: retire the job, then send an urgent durable
64
+ // notice to the owning node. The terminal path sends that notice through the
65
+ // message endpoint, whose finalized-node gate runs before append; preserve
66
+ // that same fail-loud behavior here after the job control-plane mutation.
67
+ assertNotFinalized(nodeId);
68
+ const paths = bashJobPaths(dir, jobId);
69
+ const body = `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`;
70
+ appendInbox(nodeId, {
71
+ from: null,
72
+ tier: 'urgent',
73
+ kind: 'message',
74
+ label: body.split('\n')[0].slice(0, 120),
75
+ data: { body },
76
+ });
77
+ const node = getNode(nodeId);
78
+ if (node !== null && !isBrokerLive(node)) {
79
+ try {
80
+ reviveNode(nodeId, { resume: true });
81
+ }
82
+ catch { /* the notice remains durable */ }
83
+ }
84
+ const result = { node_id: nodeId, job_id: jobId, signaled: stopped.signaled };
85
+ return { status: 200, body: result };
86
+ }
87
+ export const bashJobRoutes = [
88
+ { method: 'GET', pattern: '/v1/nodes/:id/jobs', handler: handleList },
89
+ { method: 'DELETE', pattern: '/v1/nodes/:id/jobs/:jobId', handler: handleStop },
90
+ ];
@@ -4,13 +4,14 @@
4
4
  import { existsSync } from 'node:fs';
5
5
  import { ApiError } from '../../../api/index.js';
6
6
  import { isSafeNodeId } from '../../../core/canvas/paths.js';
7
- import { deckPath, reviewPath } from '../../../core/human/convention.js';
7
+ import { pageManifestPath, reviewPath } from '../../../core/human/convention.js';
8
8
  import { ticketDir } from '../../../core/human/root.js';
9
- import { readTicketResult, validateDeckTicketResponses } from '../../../core/human/tickets.js';
9
+ import { readTicketResult, validatePageTicketResponses } from '../../../core/human/tickets.js';
10
+ import { readConfig } from '../../../core/config.js';
10
11
  import { getReviewByBridge } from '../../../core/review/store.js';
11
12
  import { ReviewOperationError } from '../../../core/review/types.js';
12
13
  import { spawnNode } from '../../../core/runtime/nodes.js';
13
- import { cancelHumanTicket, resolveDeckTicket } from '../../human/finish.js';
14
+ import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
14
15
  import { cancelReview } from '../../review/finish.js';
15
16
  function requireTicketNodeId(ctx) {
16
17
  const nodeId = ctx.params['node_id'];
@@ -52,7 +53,7 @@ function handleCreateBridge(ctx) {
52
53
  function reviewSubmitGuidance() {
53
54
  return new ApiError(400, 'invalid_request', 'review approvals use POST /v1/human/reviews/:review_id/submit');
54
55
  }
55
- /** The ticket resolve route accepts only a deck response body. */
56
+ /** The ticket resolve route accepts only a page response body. */
56
57
  function requireResolveRequest(ctx) {
57
58
  const request = ctx.body;
58
59
  if (request === undefined || request === null || typeof request !== 'object' || Array.isArray(request)) {
@@ -66,13 +67,13 @@ function requireResolveRequest(ctx) {
66
67
  throw reviewSubmitGuidance();
67
68
  throw new ApiError(400, 'invalid_request', 'ticket resolve body requires exactly `responses`');
68
69
  }
69
- function requireDeckTicket(dir) {
70
- const deck = existsSync(deckPath(dir));
70
+ function requirePageTicket(dir) {
71
+ const page = existsSync(pageManifestPath(dir));
71
72
  const review = existsSync(reviewPath(dir));
72
- if (review && !deck)
73
+ if (review && !page)
73
74
  throw reviewSubmitGuidance();
74
- if (!deck || review) {
75
- throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous deck kind');
75
+ if (!page || review) {
76
+ throw new ApiError(400, 'invalid_request', 'ticket has no unambiguous page kind');
76
77
  }
77
78
  }
78
79
  function invalidResolveRequest(error) {
@@ -91,13 +92,14 @@ async function handleResolve(ctx) {
91
92
  const nodeId = requireTicketNodeId(ctx);
92
93
  const request = requireResolveRequest(ctx);
93
94
  const dir = ticketDir(nodeId);
94
- requireDeckTicket(dir);
95
- const responses = preflightResolve(() => validateDeckTicketResponses(dir, request.responses));
96
- await resolveDeckTicket(nodeId, responses);
97
- return { status: 200, body: { delivered: true, kind: 'deck' } };
95
+ requirePageTicket(dir);
96
+ const productKinds = readConfig('user').page_components ?? [];
97
+ const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
98
+ await resolvePageTicket(nodeId, responses, productKinds);
99
+ return { status: 200, body: { delivered: true, kind: 'page' } };
98
100
  }
99
101
  function hasPublishedTicket(dir) {
100
- return existsSync(deckPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
102
+ return existsSync(pageManifestPath(dir)) || existsSync(reviewPath(dir)) || readTicketResult(dir) !== null;
101
103
  }
102
104
  async function handleCancel(ctx) {
103
105
  const nodeId = requireTicketNodeId(ctx);