@north-light/crouter 0.3.196 → 0.3.198

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 (148) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +90 -168
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/termrender/display.d.ts +7 -3
  91. package/dist/core/termrender/display.js +45 -9
  92. package/dist/core/termrender/termrender.d.ts +3 -12
  93. package/dist/core/termrender/termrender.js +6 -37
  94. package/dist/core/user-settings.d.ts +17 -2
  95. package/dist/core/user-settings.js +32 -11
  96. package/dist/daemon/api/handlers/human.js +1 -1
  97. package/dist/daemon/api/handlers/inbox.js +22 -7
  98. package/dist/daemon/cron-run.js +16 -2
  99. package/dist/daemon/crtrd.js +1 -1
  100. package/dist/daemon/fleet.d.ts +22 -10
  101. package/dist/daemon/fleet.js +43 -24
  102. package/dist/daemon/human/finish.d.ts +2 -1
  103. package/dist/daemon/human/finish.js +45 -28
  104. package/dist/daemon/human/sweep.js +4 -2
  105. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  106. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  107. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  108. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  109. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  110. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  111. package/dist/daemon/review/sweep.js +12 -1
  112. package/dist/pages/bundle.css +1 -1
  113. package/dist/pages/bundle.js +1008 -577
  114. package/dist/pages/comments.d.ts +29 -0
  115. package/dist/pages/comments.js +51 -0
  116. package/dist/pages/controls.d.ts +98 -0
  117. package/dist/pages/controls.js +261 -0
  118. package/dist/pages/elements/cards.d.ts +15 -4
  119. package/dist/pages/elements/cards.js +299 -186
  120. package/dist/pages/elements/chart.d.ts +22 -0
  121. package/dist/pages/elements/chart.js +107 -91
  122. package/dist/pages/elements/options.d.ts +9 -0
  123. package/dist/pages/elements/options.js +268 -155
  124. package/dist/pages/elements/pages.d.ts +6 -0
  125. package/dist/pages/elements/pages.js +60 -38
  126. package/dist/pages/elements/slot.d.ts +5 -0
  127. package/dist/pages/elements/slot.js +17 -10
  128. package/dist/pages/elements/table.d.ts +17 -0
  129. package/dist/pages/elements/table.js +336 -228
  130. package/dist/pages/elements/text.d.ts +9 -3
  131. package/dist/pages/elements/text.js +350 -143
  132. package/dist/pages/host.d.ts +0 -24
  133. package/dist/pages/host.js +0 -17
  134. package/dist/pages/readonly.d.ts +24 -0
  135. package/dist/pages/readonly.js +31 -0
  136. package/dist/pages/responses.d.ts +37 -0
  137. package/dist/pages/responses.js +56 -0
  138. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  139. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  140. package/dist/types.d.ts +19 -5
  141. package/dist/types.js +2 -1
  142. package/package.json +1 -1
  143. package/runtime.lock.json +2 -2
  144. package/scripts/install-runtime.mjs +19 -24
  145. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  146. package/dist/clients/attach/render/html-markdown.js +0 -206
  147. package/dist/core/human/markdown-html.d.ts +0 -8
  148. package/dist/core/human/markdown-html.js +0 -581
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  import { buildPageSummary } from './summary.js';
6
6
  import { atomicWriteJson, clearProgress, claimPath, pageManifestPath, pagePath, progressPath, publishJsonExclusive, requireTicket, responsePath, reviewPath } from './convention.js';
7
7
  import { validateReviewDescriptor, validateReviewProjection } from './review-schema.js';
8
- import { derivePageManifestFromDocument, parsePage, PageAuthoringError } from './page.js';
8
+ import { derivePageManifestFromDocument, parsePage, PageAuthoringError, snapshotPageSources } from './page.js';
9
9
  import { validatePageResponses, validatePartialPageResponses } from './page-schema.js';
10
10
  import { signalInboxActivity } from './root.js';
11
11
  import { readTicketClaim, releaseClaimLocked, withTicketLock } from './claim.js';
@@ -62,7 +62,9 @@ export function preparePage(opts) {
62
62
  throw new PageAuthoringError(`page source file does not exist: ${opts.sourceFile}`);
63
63
  const document = readFileSync(opts.sourceFile, 'utf8');
64
64
  const stamped = { ...(opts.source ?? {}), blockedSince: opts.source?.blockedSince ?? new Date().toISOString() };
65
- return { dialect: opts.dialect, document, manifest: derivePageManifestFromDocument(document, opts.dialect, opts.productKinds ?? [], stamped) };
65
+ const authored = derivePageManifestFromDocument(document, opts.dialect, opts.productKinds ?? [], stamped);
66
+ const manifest = snapshotPageSources(authored, opts.productKinds ?? []);
67
+ return { dialect: opts.dialect, document, manifest };
66
68
  }
67
69
  /** Publish one prepared page ticket: the authored source plus its manifest, exclusively. */
68
70
  export function publishPage(dir, prepared) {
@@ -170,7 +172,7 @@ function completePageLocked(dir, responses, claimToken, opts) {
170
172
  requireClaimOwnership(dir, claimToken);
171
173
  const manifest = requirePage(dir, opts.productKinds ?? []);
172
174
  const parsedResponses = validatePageResponses(manifest, responses);
173
- const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString() };
175
+ const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses, opts.productKinds), completedAt: opts.completedAt ?? new Date().toISOString() };
174
176
  const won = exclusiveResult(dir, result);
175
177
  clearOwnedWork(dir, claimToken);
176
178
  return { won, result: won ? result : readTicketResult(dir) ?? result };
@@ -4,6 +4,8 @@ export interface TicketSource {
4
4
  sessionName?: string;
5
5
  askedBy?: string;
6
6
  blockedSince?: string;
7
+ /** Profile name the originating node runs under. Product consumers use this stable name to resolve the applet or worker identity behind the ticket. */
8
+ profileName?: string;
7
9
  /** Originating canvas node id (CRTR_NODE_ID) when the ask was raised inside a crouter canvas node. */
8
10
  nodeId?: string;
9
11
  }
@@ -48,6 +50,8 @@ export interface ReviewTicketSummary extends TicketSummaryBase {
48
50
  /** Page triage fields: a client picks a transcript rendering without a second fetch. */
49
51
  export interface PageTicketSummary extends TicketSummaryBase {
50
52
  kind: 'page';
53
+ /** Present on resolved history entries; the stored one-line response digest. */
54
+ answerDigest?: string;
51
55
  dialect: 'md' | 'html';
52
56
  placement: 'inline' | 'panel';
53
57
  steps: number;
@@ -3,13 +3,13 @@ import type { AttachBindingId, AttachMenuBindingId } from './attach-control.js';
3
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
- 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';
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' | 'yank-id';
7
7
  type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' | `slot.${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` | 'create' | 'search-existing' | 'use-root' | 'set-default' | 'add-directory' | 'unpin';
8
8
  type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
9
9
  type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
10
10
  type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
11
11
  export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | AttachMenuBindingId | `crtr.tmux.menu-global.${TmuxMenuEntryAction}` | `crtr.tmux.node.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.scroll.${'up' | 'down' | 'page-up' | 'page-down' | 'live'}` | 'crtr.attach.mouse.toggle' | 'crtr.attach.copy.message' | 'crtr.attach.help.toggle' | AttachBindingId | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.inspector.${'force-quit' | 'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
12
- export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
12
+ export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.attach">, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.tmux.node.next"> | BindingDefinition<"crtr.tmux.node.previous"> | BindingDefinition<"crtr.tmux.inbox.toggle"> | BindingDefinition<"crtr.attach.scroll.up"> | BindingDefinition<"crtr.attach.scroll.down"> | BindingDefinition<"crtr.attach.scroll.page-up"> | BindingDefinition<"crtr.attach.scroll.page-down"> | BindingDefinition<"crtr.attach.scroll.live"> | BindingDefinition<"crtr.attach.mouse.toggle"> | BindingDefinition<"crtr.attach.copy.message"> | BindingDefinition<"crtr.graph.down"> | BindingDefinition<"crtr.graph.up"> | BindingDefinition<"crtr.graph.first"> | BindingDefinition<"crtr.graph.last"> | BindingDefinition<"crtr.graph.collapse-or-manager"> | BindingDefinition<"crtr.graph.expand-or-child"> | BindingDefinition<"crtr.graph.focus"> | BindingDefinition<"crtr.graph.revive"> | BindingDefinition<"crtr.graph.focus-manager"> | BindingDefinition<"crtr.graph.close-subtree"> | BindingDefinition<"crtr.graph.dismiss"> | BindingDefinition<"crtr.graph.confirm.accept"> | BindingDefinition<"crtr.graph.confirm.cancel"> | BindingDefinition<"crtr.inspector.force-quit"> | BindingDefinition<"crtr.inspector.down"> | BindingDefinition<"crtr.inspector.up"> | BindingDefinition<"crtr.inspector.section-next"> | BindingDefinition<"crtr.inspector.section-prev"> | BindingDefinition<"crtr.inspector.page-down"> | BindingDefinition<"crtr.inspector.page-up"> | BindingDefinition<"crtr.inspector.toggle-detail"> | BindingDefinition<"crtr.inspector.cancel-item"> | BindingDefinition<"crtr.inspector.open-log"> | BindingDefinition<"crtr.inspector.confirm"> | BindingDefinition<"crtr.inspector.dismiss"> | BindingDefinition<"crtr.inspector.retarget"> | BindingDefinition<"crtr.inspector.refresh"> | BindingDefinition<"crtr.inspector.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.yank-id"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.6" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.9">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
13
13
  export declare const BINDING_IDS: ReadonlySet<string>;
14
14
  /** True for an action whose direct gesture is handled inside an attach viewer.
15
15
  * Tmux menu selectors and root shortcuts are chrome, not pane input. */
@@ -142,6 +142,7 @@ function browseBindings() {
142
142
  b('crtr.browse.revive-now', 'Browse and pickers', 'Revive now', 'Revive the selected node now.', ['shift+k'], 'terminal', [nav]),
143
143
  b('crtr.browse.focus', 'Browse and pickers', 'Focus node', 'Focus the selected node.', ['enter', 'o'], 'terminal', [nav]),
144
144
  b('crtr.browse.focus-background', 'Browse and pickers', 'Focus node (keep browsing)', 'Focus the selected node without leaving the navigator.', ['shift+o'], 'terminal', [nav]),
145
+ b('crtr.browse.yank-id', 'Browse and pickers', 'Yank node id', 'Copy the selected node id to the system clipboard.', ['y'], 'terminal', [nav]),
145
146
  b('crtr.browse.search.cancel', 'Browse and pickers', 'Cancel search', 'Cancel browse search.', ['escape'], 'terminal', ['browse.search'], 'dismiss-modal'),
146
147
  b('crtr.browse.search.commit', 'Browse and pickers', 'Commit search', 'Apply browse search.', ['enter'], 'terminal', ['browse.search']),
147
148
  b('crtr.browse.search.delete', 'Browse and pickers', 'Delete search text', 'Delete the previous search character.', ['backspace'], 'terminal', ['browse.search']),
@@ -6,6 +6,7 @@ import { atomicWriteJson } from '../human/convention.js';
6
6
  import { ticketDir } from '../human/root.js';
7
7
  import { submitReview } from '../human/tickets.js';
8
8
  import { NodeIdConflictError, spawnNode } from '../runtime/nodes.js';
9
+ import { loadProfileManifest } from '../profiles/manifest.js';
9
10
  import { transition } from '../runtime/lifecycle.js';
10
11
  import { reviveNode } from '../runtime/revive.js';
11
12
  import { awaitCompanionBind, spawnReviewCompanion } from './companion.js';
@@ -126,6 +127,10 @@ function publishTicket(review) {
126
127
  throw bridgeUnsubscribed(review, '');
127
128
  if (review.subtitle === null)
128
129
  throw new Error(`ticket review has no subtitle: ${review.review_id}`);
130
+ const origin = requireOrigin(review);
131
+ const profileName = origin.profile_id == null
132
+ ? undefined
133
+ : loadProfileManifest(origin.profile_id).manifest.name;
129
134
  submitReview({
130
135
  dir: ticketDir(review.bridge_node_id),
131
136
  review: {
@@ -133,7 +138,10 @@ function publishTicket(review) {
133
138
  ...(review.output_path !== null ? { output: review.output_path } : {}),
134
139
  title: basename(review.file),
135
140
  subtitle: review.subtitle,
136
- source: { nodeId: review.origin_node_id },
141
+ source: {
142
+ nodeId: review.origin_node_id,
143
+ ...(profileName === undefined ? {} : { profileName }),
144
+ },
137
145
  blockedSince: review.created,
138
146
  },
139
147
  });
@@ -89,7 +89,12 @@ export async function bootRoot(opts) {
89
89
  // user's OWN pre-existing shell pane (not one crtr split open): on a
90
90
  // detach-reap of an empty root it must delete the node WITHOUT kill-pane, else
91
91
  // a plain ctrl+c closes the user's whole tmux pane.
92
- const attachEnv = { ...process.env, ...viewerSplitEnv(), CRTR_ATTACH_WAIT_MS: '30000' };
92
+ const attachEnv = {
93
+ ...process.env,
94
+ ...viewerSplitEnv(),
95
+ CRTR_ATTACH_WAIT_MS: '30000',
96
+ ...(opts.prompt !== undefined && opts.prompt !== '' ? { CRTR_ATTACH_INITIAL_WHIP: '1' } : {}),
97
+ };
93
98
  if (here !== null)
94
99
  attachEnv['CRTR_ATTACH_BORROWED_PANE'] = here.pane;
95
100
  mark('boot_root.viewer_spawn');
@@ -120,7 +120,8 @@ export function disposeActiveSession() {
120
120
  // ---------------------------------------------------------------------------
121
121
  export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), startupTs = new Date().toISOString()) {
122
122
  // A clean-abort certificate belongs only to the dead broker that wrote it.
123
- // Fleet classification consumes it before launching this successor.
123
+ // Fleet classification consumes it as proof that the dead broker had a turn
124
+ // in flight even though Pi's settled abort already cleared the busy marker.
124
125
  clearCleanAbort(nodeId);
125
126
  // The canonical broker event source (job/log.jsonl) is bound before any other
126
127
  // work in this function — no path/env/state setup may precede it, and the
@@ -594,9 +595,10 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
594
595
  // A daemon fleet bounce gives a busy Pi a short, bounded chance to persist
595
596
  // its aborted turn before teardown. `session.abort()` resolves only after Pi
596
597
  // reaches idle (and thus after its message_end persistence); only then may we
597
- // leave the durable clean-abort certificate that changes recovery to resume.
598
- // A timeout/rejection deliberately leaves no certificate, preserving dirty
599
- // crash → recovery-cycle behavior and the fleet's shared SIGKILL deadline.
598
+ // leave the durable clean-abort certificate that preserves the otherwise
599
+ // cleared evidence that this broker had a turn in flight. A timeout/rejection
600
+ // deliberately leaves no certificate: the surviving busy marker carries the
601
+ // same interrupted-turn fact through the fleet's shared SIGKILL deadline.
600
602
  const ABORT_BEFORE_EXIT_MS = 2_500;
601
603
  let signalExitInFlight = false;
602
604
  const abortAndExit = async (reason) => {
@@ -633,8 +635,8 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
633
635
  }
634
636
  else if (frameDispatch.pendingPromptDispatches() > 0 || rebind.isAgentRunActive()) {
635
637
  // The engine never reached an abortable persisted turn in the budget.
636
- // Preserve the existing dirty-death recovery cycle rather than certifying
637
- // a session that may not contain the prompt it was preparing.
638
+ // Preserve the dirty interrupted-turn witness so recovery resumes the
639
+ // saved transcript and explicitly re-drives the stopped work.
638
640
  markBusy(nodeId);
639
641
  }
640
642
  }
@@ -4,12 +4,13 @@ export declare function markBusy(nodeId: string): void;
4
4
  /** Clear the mid-run marker after Pi settles, however it routed. Best-effort. */
5
5
  export declare function clearBusy(nodeId: string): void;
6
6
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
7
- * aborted entry before teardown. The daemon consumes this durable certificate
8
- * once to resume with a continuation; a dirty death never writes it. */
7
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
8
+ * consumes this durable certificate as the surviving interrupted-turn witness
9
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
9
10
  export declare function markCleanAbort(nodeId: string): void;
10
11
  /** Remove a consumed clean-abort certificate as a broker boots. */
11
12
  export declare function clearCleanAbort(nodeId: string): void;
12
- /** Whether the dead broker left a persisted clean-abort certificate. */
13
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
13
14
  export declare function hasCleanAbort(nodeId: string): boolean;
14
15
  /** Is the node currently inside a turn? AND this with `pidAlive` at the call
15
16
  * site — a stale marker from a crashed pi is harmless because the dead pid
@@ -44,8 +44,9 @@ export function clearBusy(nodeId) {
44
44
  }
45
45
  }
46
46
  /** Certify that this broker aborted a busy turn and Pi persisted its settled
47
- * aborted entry before teardown. The daemon consumes this durable certificate
48
- * once to resume with a continuation; a dirty death never writes it. */
47
+ * aborted entry before teardown. Settling clears the busy marker, so the daemon
48
+ * consumes this durable certificate as the surviving interrupted-turn witness
49
+ * and resumes with a continuation. A dirty death instead leaves `busy` behind. */
49
50
  export function markCleanAbort(nodeId) {
50
51
  try {
51
52
  mkdirSync(jobDir(nodeId), { recursive: true });
@@ -64,7 +65,7 @@ export function clearCleanAbort(nodeId) {
64
65
  /* best-effort */
65
66
  }
66
67
  }
67
- /** Whether the dead broker left a persisted clean-abort certificate. */
68
+ /** Whether the dead broker left a persisted clean-abort turn witness. */
68
69
  export function hasCleanAbort(nodeId) {
69
70
  return existsSync(cleanAbortPath(nodeId));
70
71
  }
@@ -401,15 +401,15 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
401
401
  // have bound view.sock yet. Hand the attach pane CRTR_ATTACH_WAIT_MS so
402
402
  // it self-polls the socket until it accepts instead of connecting once,
403
403
  // finding a cold socket, and closing the pane instantly — the exact
404
- // mechanism bootRoot uses. Mark this as the initial auto-opened child
405
- // viewer too, so whip_mode can greet it without firing on ordinary
406
- // focus/attach paths. No pre-wait in this CLI subprocess.
404
+ // mechanism bootRoot uses. A child with a kickoff prompt marks its
405
+ // initial viewer for whip_mode; ordinary focus/attach paths stay
406
+ // silent. No pre-wait in this CLI subprocess.
407
407
  const viewer = openViewerWindow(meta.node_id, parentViewer.session ?? '', {
408
408
  name: fullName(meta),
409
409
  cwd: opts.cwd,
410
410
  besidePane: parentViewer.pane,
411
411
  waitForBroker: true,
412
- initialWhip: true,
412
+ initialWhip: hasKickoff,
413
413
  });
414
414
  if (viewer !== null && viewer.pane !== null && viewer.pane !== '') {
415
415
  session = viewer.session;
@@ -6,7 +6,11 @@ export interface DisplayOpts {
6
6
  /** Pane budget per window before `'auto'` opens a new window. Default 3. */
7
7
  maxPanes?: number;
8
8
  }
9
- export declare function countPanesInCurrentWindow(): number;
10
- export declare function display(path: string, opts?: DisplayOpts): {
11
- paneId?: string;
9
+ export type DisplayResult = {
10
+ paneId: string;
11
+ reason?: never;
12
+ } | {
13
+ paneId?: never;
14
+ reason: string;
12
15
  };
16
+ export declare function display(path: string, opts?: DisplayOpts): DisplayResult;
@@ -1,19 +1,55 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { displayInPane } from './termrender.js';
3
- export function countPanesInCurrentWindow() {
4
- // -t '' targets the current window of the current session.
5
- const result = spawnSync('tmux', ['list-panes', '-F', '#{pane_id}'], {
2
+ import { resolve } from 'node:path';
3
+ import { breakPane, closePane, currentTmux, paneOfNode, splitWindow, } from '../runtime/placement-tmux.js';
4
+ import { documentWatchCommand } from './termrender.js';
5
+ function targetPane() {
6
+ const callerPane = process.env['TMUX_PANE'];
7
+ if (callerPane !== undefined && callerPane !== '')
8
+ return callerPane;
9
+ const nodeId = process.env['CRTR_NODE_ID'];
10
+ if (nodeId !== undefined && nodeId !== '') {
11
+ const viewerPane = paneOfNode(nodeId);
12
+ if (viewerPane !== null)
13
+ return viewerPane;
14
+ }
15
+ return currentTmux()?.pane ?? null;
16
+ }
17
+ function countPanes(target) {
18
+ const result = spawnSync('tmux', ['list-panes', '-t', target, '-F', '#{pane_id}'], {
6
19
  encoding: 'utf8',
7
20
  });
8
21
  if (result.status !== 0)
9
- return 0;
22
+ return null;
10
23
  return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
11
24
  }
12
25
  export function display(path, opts) {
13
26
  const window = (opts?.window === 'split' || opts?.window === 'new') ? opts.window : 'auto';
14
27
  const maxPanes = (opts?.maxPanes !== undefined && opts.maxPanes > 0) ? opts.maxPanes : 3;
15
- const newWindow = window === 'new' ||
16
- (window === 'auto' && countPanesInCurrentWindow() >= maxPanes);
17
- // The pane always watches the file — displayed docs are live by definition.
18
- return displayInPane(path, { newWindow });
28
+ const command = documentWatchCommand(resolve(path));
29
+ if (command === null)
30
+ return { reason: 'renderer unavailable' };
31
+ const target = targetPane();
32
+ if (target === null)
33
+ return { reason: 'no pane on screen' };
34
+ let newWindow = window === 'new';
35
+ if (window === 'auto') {
36
+ const paneCount = countPanes(target);
37
+ if (paneCount === null)
38
+ return { reason: `tmux could not inspect target pane ${target}` };
39
+ newWindow = paneCount >= maxPanes;
40
+ }
41
+ const pane = splitWindow(target, {
42
+ cwd: process.cwd(),
43
+ env: {},
44
+ command,
45
+ });
46
+ if (pane === null)
47
+ return { reason: `tmux could not split target pane ${target}` };
48
+ if (!newWindow)
49
+ return { paneId: pane };
50
+ const opened = breakPane(pane);
51
+ if (opened !== null)
52
+ return { paneId: opened.pane };
53
+ closePane(pane);
54
+ return { reason: 'tmux could not move the document pane into a new window' };
19
55
  }
@@ -59,15 +59,6 @@ export declare function checkMarkdown(md: string): {
59
59
  ok: false;
60
60
  error: string;
61
61
  };
62
- export interface DisplayInPaneOpts {
63
- /** Open in a new tmux window instead of splitting the current one. */
64
- newWindow?: boolean;
65
- }
66
- /**
67
- * Spawn termrender into a live tmux pane. The pane-budget policy (whether to
68
- * split vs open a new window) is decided by the caller (`src/surfaces/
69
- * display.ts`); this is the thin managed-binary spawn it delegates to.
70
- */
71
- export declare function displayInPane(path: string, opts?: DisplayInPaneOpts): {
72
- paneId?: string;
73
- };
62
+ /** Shell command for a pane that live-renders `path` with crouter's pinned
63
+ * termrender. Null means the managed renderer could not be provisioned. */
64
+ export declare function documentWatchCommand(path: string): string | null;
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, writeFileSync, statSync, mkdirSync, unlinkSyn
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import stringWidth from 'string-width';
6
+ import { shellQuote } from '../../shared/shell-quote.js';
6
7
  import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
7
8
  import { TERMRENDER_VERSION } from './version.js';
8
9
  // ── The sole org-wide termrender binding ─────────────────────────────────────
@@ -703,43 +704,11 @@ export function checkMarkdown(md) {
703
704
  }
704
705
  return { ok: true };
705
706
  }
706
- /**
707
- * Spawn termrender into a live tmux pane. The pane-budget policy (whether to
708
- * split vs open a new window) is decided by the caller (`src/surfaces/
709
- * display.ts`); this is the thin managed-binary spawn it delegates to.
710
- */
711
- export function displayInPane(path, opts = {}) {
707
+ /** Shell command for a pane that live-renders `path` with crouter's pinned
708
+ * termrender. Null means the managed renderer could not be provisioned. */
709
+ export function documentWatchCommand(path) {
712
710
  ensureRenderer();
713
711
  if (rendererState !== 'ready')
714
- return {};
715
- // Always watch: a displayed pane is a live view of the file by definition.
716
- const args = ['pane', 'open', path, '--watch'];
717
- args.push('--window', opts.newWindow ? 'new' : 'split');
718
- const result = spawnSync(VENV_BIN, args, {
719
- encoding: 'utf-8',
720
- stdio: ['pipe', 'pipe', 'pipe'],
721
- });
722
- // A spawn fault (binary broke) invalidates readiness; a non-zero exit (e.g.
723
- // tmux refused, bad path) is not an environment fault and must not.
724
- if (result.error) {
725
- invalidateRenderer('pane open');
726
- return {};
727
- }
728
- if (result.status !== 0)
729
- return {};
730
- // `encoding: 'utf-8'` makes spawnSync return stdout as a string.
731
- const rawStdout = result.stdout;
732
- if (!rawStdout)
733
- return {};
734
- let parsed = null;
735
- try {
736
- parsed = JSON.parse(rawStdout.trim());
737
- }
738
- catch {
739
- return {};
740
- }
741
- if (parsed && typeof parsed.pane_id === 'string' && parsed.pane_id) {
742
- return { paneId: parsed.pane_id };
743
- }
744
- return {};
712
+ return null;
713
+ return `${shellQuote(VENV_BIN)} doc watch ${shellQuote(path)}`;
745
714
  }
@@ -147,6 +147,21 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
147
147
  setFromString: (config: ScopeConfig, raw: string) => string | null;
148
148
  };
149
149
  };
150
+ html_surface: {
151
+ surface: "settings";
152
+ setting: {
153
+ key: "html_surface";
154
+ label: string;
155
+ description: string;
156
+ valueConstraint: string;
157
+ applies: string;
158
+ group: "display";
159
+ order: number;
160
+ format: (config: ScopeConfig) => "on" | "off";
161
+ cycle: (config: ScopeConfig) => void;
162
+ setFromString: (config: ScopeConfig, raw: string) => string | null;
163
+ };
164
+ };
150
165
  whip_messages: {
151
166
  surface: "specialized";
152
167
  owner: string;
@@ -196,10 +211,10 @@ export declare const CONFIG_SETTINGS_COVERAGE: {
196
211
  setFromString: (config: ScopeConfig, raw: string) => string | null;
197
212
  };
198
213
  };
199
- html_pages: {
214
+ bash_tool_purpose: {
200
215
  surface: "settings";
201
216
  setting: {
202
- key: "html_pages";
217
+ key: "bash_tool_purpose";
203
218
  label: string;
204
219
  description: string;
205
220
  valueConstraint: string;
@@ -176,7 +176,7 @@ export const CONFIG_SETTINGS_COVERAGE = {
176
176
  setting: {
177
177
  key: 'whip_mode',
178
178
  label: 'Whip mode',
179
- description: 'Play the whip animation when you send a prompt and when an automatically opened child viewer starts.',
179
+ description: 'Play the whip animation when you send a prompt and when a viewer opens for an agent with an initial prompt.',
180
180
  valueConstraint: 'true|false',
181
181
  applies: 'Applies on the next attach.',
182
182
  group: 'display',
@@ -192,6 +192,27 @@ export const CONFIG_SETTINGS_COVERAGE = {
192
192
  },
193
193
  },
194
194
  },
195
+ html_surface: {
196
+ surface: 'settings',
197
+ setting: {
198
+ key: 'html_surface',
199
+ label: 'HTML human surface',
200
+ description: 'Prompt agents to use HTML pages by default for structured or interactive content shown to you.',
201
+ valueConstraint: 'true|false',
202
+ applies: 'Applies when human command help is assembled.',
203
+ group: 'display',
204
+ order: 37,
205
+ format: (config) => config.html_surface ? 'on' : 'off',
206
+ cycle: (config) => { config.html_surface = !config.html_surface; },
207
+ setFromString: (config, raw) => {
208
+ const value = booleanFromString(raw, 'html_surface');
209
+ if (typeof value === 'string')
210
+ return value;
211
+ config.html_surface = value;
212
+ return null;
213
+ },
214
+ },
215
+ },
195
216
  whip_messages: { surface: 'specialized', owner: 'sys config set whip_messages' },
196
217
  mouse_mode_default: {
197
218
  surface: 'settings',
@@ -254,23 +275,23 @@ export const CONFIG_SETTINGS_COVERAGE = {
254
275
  },
255
276
  },
256
277
  },
257
- html_pages: {
278
+ bash_tool_purpose: {
258
279
  surface: 'settings',
259
280
  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.',
281
+ key: 'bash_tool_purpose',
282
+ label: 'Bash purpose field',
283
+ description: 'Expose the optional purpose label to agents in the bash tool schema. Off removes the field without disabling the bash valve.',
263
284
  valueConstraint: 'true|false',
264
- applies: 'Applies on the next help request.',
285
+ applies: 'Applies on the next node session.',
265
286
  group: 'display',
266
- order: 70,
267
- format: (config) => config.html_pages ? 'on' : 'off',
268
- cycle: (config) => { config.html_pages = !config.html_pages; },
287
+ order: 25,
288
+ format: (config) => config.bash_tool_purpose ? 'on' : 'off',
289
+ cycle: (config) => { config.bash_tool_purpose = !config.bash_tool_purpose; },
269
290
  setFromString: (config, raw) => {
270
- const value = booleanFromString(raw, 'html_pages');
291
+ const value = booleanFromString(raw, 'bash_tool_purpose');
271
292
  if (typeof value === 'string')
272
293
  return value;
273
- config.html_pages = value;
294
+ config.bash_tool_purpose = value;
274
295
  return null;
275
296
  },
276
297
  },
@@ -93,7 +93,7 @@ async function handleResolve(ctx) {
93
93
  const request = requireResolveRequest(ctx);
94
94
  const dir = ticketDir(nodeId);
95
95
  requirePageTicket(dir);
96
- const productKinds = readConfig('user').page_components ?? [];
96
+ const productKinds = readConfig('user').page_components;
97
97
  const responses = preflightResolve(() => validatePageTicketResponses(dir, request.responses, productKinds));
98
98
  await resolvePageTicket(nodeId, responses, productKinds);
99
99
  return { status: 200, body: { delivered: true, kind: 'page' } };