@north-light/crouter 0.3.207 → 0.3.209

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 (172) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/api/dto/broker-ops.d.ts +2 -12
  4. package/dist/api/dto/nodes.d.ts +16 -0
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +3 -2
  8. package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
  9. package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
  10. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
  11. package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
  12. package/dist/builtin-memory/04-base-worker.md +3 -2
  13. package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
  14. package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
  15. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
  16. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
  17. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
  18. package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
  19. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
  20. package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
  21. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
  22. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
  23. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
  31. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
  32. package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
  33. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
  34. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
  35. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
  36. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
  37. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
  38. package/dist/builtin-memory/advisor/council.md +0 -2
  39. package/dist/builtin-memory/design.md +3 -2
  40. package/dist/builtin-memory/development.md +3 -2
  41. package/dist/builtin-memory/insights/capture.md +1 -3
  42. package/dist/builtin-memory/insights/init.md +1 -3
  43. package/dist/builtin-memory/insights/listen.md +3 -2
  44. package/dist/builtin-memory/internal/INDEX.md +5 -4
  45. package/dist/builtin-memory/internal/agent-shaping.md +5 -4
  46. package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
  47. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
  48. package/dist/builtin-memory/internal/marketplaces.md +3 -2
  49. package/dist/builtin-memory/internal/memory-loading.md +31 -20
  50. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
  51. package/dist/builtin-memory/internal/plugins.md +7 -6
  52. package/dist/builtin-memory/internal/storage-tiers.md +3 -2
  53. package/dist/builtin-memory/plan/roadmap.md +3 -2
  54. package/dist/builtin-memory/spec/guide.md +0 -2
  55. package/dist/builtin-memory/spec/requirements.md +0 -2
  56. package/dist/builtin-memory/spec/roadmap.md +3 -2
  57. package/dist/builtin-memory/testing.md +1 -3
  58. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
  62. package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
  63. package/dist/clients/attach/render/card-presentation.d.ts +31 -0
  64. package/dist/clients/attach/render/card-presentation.js +163 -0
  65. package/dist/clients/attach/render/chat-view.d.ts +0 -2
  66. package/dist/clients/attach/render/chat-view.js +12 -23
  67. package/dist/clients/attach/render/context-message.d.ts +5 -9
  68. package/dist/clients/attach/render/context-message.js +20 -43
  69. package/dist/clients/attach/render/group-activity.d.ts +28 -4
  70. package/dist/clients/attach/render/group-activity.js +65 -11
  71. package/dist/clients/attach/render/group-recap.d.ts +3 -0
  72. package/dist/clients/attach/render/group-recap.js +17 -0
  73. package/dist/clients/attach/viewer.js +516 -516
  74. package/dist/commands/memory/delete.js +3 -18
  75. package/dist/commands/memory/edit.js +23 -30
  76. package/dist/commands/memory/history.js +1 -1
  77. package/dist/commands/memory/lint.d.ts +7 -8
  78. package/dist/commands/memory/lint.js +178 -132
  79. package/dist/commands/memory/list.d.ts +0 -1
  80. package/dist/commands/memory/list.js +2 -12
  81. package/dist/commands/memory/move.d.ts +1 -0
  82. package/dist/commands/memory/move.js +195 -0
  83. package/dist/commands/memory/read.js +135 -141
  84. package/dist/commands/memory/shared.d.ts +18 -17
  85. package/dist/commands/memory/shared.js +93 -39
  86. package/dist/commands/memory/write.js +23 -33
  87. package/dist/commands/memory.js +5 -4
  88. package/dist/commands/pkg/browse/catalog.js +2 -4
  89. package/dist/commands/pkg/browse/doc-view.js +17 -11
  90. package/dist/commands/pkg/browse/model.d.ts +7 -9
  91. package/dist/commands/sys/migrate.d.ts +1 -0
  92. package/dist/commands/sys/migrate.js +106 -0
  93. package/dist/commands/sys/sync-deps.js +5 -10
  94. package/dist/commands/sys/sync-project-guidance.js +36 -16
  95. package/dist/commands/sys/sync-skills.js +8 -4
  96. package/dist/commands/sys.js +3 -2
  97. package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
  98. package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
  99. package/dist/core/__tests__/helpers/broker-clients.js +7 -2
  100. package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
  101. package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
  102. package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
  103. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
  104. package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
  105. package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
  106. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
  107. package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
  108. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
  109. package/dist/core/canvas/db.d.ts +3 -1
  110. package/dist/core/canvas/db.js +12 -2
  111. package/dist/core/memory/history.d.ts +4 -1
  112. package/dist/core/memory/history.js +1 -0
  113. package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
  114. package/dist/core/memory/inline-ref-inventory.js +23 -19
  115. package/dist/core/memory-resolver.d.ts +4 -4
  116. package/dist/core/memory-resolver.js +16 -43
  117. package/dist/core/runtime/bearings.d.ts +4 -3
  118. package/dist/core/runtime/bearings.js +4 -4
  119. package/dist/core/runtime/broker/fault-retry.js +2 -7
  120. package/dist/core/runtime/broker/frame-dispatch.js +1 -1
  121. package/dist/core/runtime/broker/tool-groups.js +7 -6
  122. package/dist/core/runtime/broker-extension-render.d.ts +3 -2
  123. package/dist/core/runtime/broker-extension-render.js +3 -3
  124. package/dist/core/runtime/broker.js +4 -4
  125. package/dist/core/runtime/kickoff.js +4 -10
  126. package/dist/core/runtime/memory.js +2 -3
  127. package/dist/core/runtime/node-read.js +7 -1
  128. package/dist/core/runtime/stop-guard.js +3 -3
  129. package/dist/core/substrate/index.d.ts +7 -4
  130. package/dist/core/substrate/index.js +6 -4
  131. package/dist/core/substrate/injected-store.d.ts +24 -12
  132. package/dist/core/substrate/injected-store.js +80 -33
  133. package/dist/core/substrate/listings.d.ts +21 -0
  134. package/dist/core/substrate/listings.js +88 -0
  135. package/dist/core/substrate/on-read-node.d.ts +5 -5
  136. package/dist/core/substrate/on-read-node.js +4 -5
  137. package/dist/core/substrate/on-read.d.ts +25 -4
  138. package/dist/core/substrate/on-read.js +81 -102
  139. package/dist/core/substrate/render-node.d.ts +5 -2
  140. package/dist/core/substrate/render-node.js +5 -3
  141. package/dist/core/substrate/render.d.ts +9 -8
  142. package/dist/core/substrate/render.js +104 -96
  143. package/dist/core/substrate/schema.d.ts +34 -18
  144. package/dist/core/substrate/schema.js +75 -32
  145. package/dist/core/substrate/surface-match.d.ts +32 -0
  146. package/dist/core/substrate/surface-match.js +179 -0
  147. package/dist/daemon/api/handlers/nodes.js +9 -0
  148. package/dist/daemon/review/comment-notify.js +1 -8
  149. package/dist/daemon/review/deliver.js +1 -1
  150. package/dist/daemon/review/finish.js +1 -1
  151. package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
  152. package/dist/migrations/001-surfaces-frontmatter.js +276 -0
  153. package/dist/migrations/convergent.d.ts +31 -0
  154. package/dist/migrations/convergent.js +71 -0
  155. package/dist/migrations/registry.d.ts +2 -0
  156. package/dist/migrations/registry.js +19 -0
  157. package/dist/migrations/types.d.ts +40 -0
  158. package/dist/migrations/types.js +11 -0
  159. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
  160. package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
  161. package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
  162. package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
  163. package/dist/pi-extensions/canvas-review-boundary.js +1 -1
  164. package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
  165. package/dist/shared/generated-context.d.ts +0 -18
  166. package/dist/shared/generated-context.js +16 -120
  167. package/dist/shared/tool-groups.d.ts +1 -0
  168. package/dist/shared/tool-groups.js +7 -0
  169. package/package.json +1 -1
  170. package/runtime.lock.json +2 -2
  171. package/dist/core/substrate/ceiling.d.ts +0 -17
  172. package/dist/core/substrate/ceiling.js +0 -67
@@ -0,0 +1,163 @@
1
+ // The terminal's presentation policy for runtime cards. One kind, one
2
+ // definition: what the collapsed row says, what the global detail toggle
3
+ // discloses, and what survives when the surrounding tool group folds into a
4
+ // recap. Renderers read these policies and never compare kinds.
5
+ //
6
+ // This is display policy, not a wire contract — nothing outside
7
+ // clients/attach/** reads it. The one fact the broker must agree with lives in
8
+ // shared/tool-groups.ts (`isGroupBoundaryKind`), because segmenting a tool run
9
+ // on different seams would make the recap describe a span nobody saw.
10
+ //
11
+ // Every summary derives from the PARSED card — its body and its sender
12
+ // attribution — never from envelope facts, which are leaving the wire.
13
+ import { AUTH_FAULT_RECOVERY_BODY, CONNECTION_FAULT_RECOVERY_BODY, PROVIDER_FAULT_RECOVERY_BODY, } from '../../../shared/generated-context.js';
14
+ const NEVER = { disclose: 'never' };
15
+ const PAYLOAD = { disclose: 'payload' };
16
+ /** Every label in the transcript reads the same way; no definition stores one. */
17
+ export function cardLabel(subject) {
18
+ return `crtr ${subject}`;
19
+ }
20
+ /** Body of a `section` disclosure. An absent section discloses nothing rather
21
+ * than falling back to the whole payload, which is what the section policy
22
+ * exists to avoid showing. */
23
+ function extractCardSection(body, section) {
24
+ const match = new RegExp(`<${section}\\b[^>]*>([\\s\\S]*?)</${section}>`).exec(body);
25
+ return match ? match[1].trim() : '';
26
+ }
27
+ /** The disclosure a card actually offers, given its policy and body. A kind
28
+ * whose section is missing from an old transcript offers nothing to expand. */
29
+ export function cardDisclosure(presentation, body) {
30
+ switch (presentation.expanded.disclose) {
31
+ case 'never': return '';
32
+ case 'payload': return body;
33
+ case 'section': return extractCardSection(body, presentation.expanded.section);
34
+ }
35
+ }
36
+ export function cardIsExpandable(presentation, body) {
37
+ return cardDisclosure(presentation, body).length > 0;
38
+ }
39
+ function inboxSummary(card) {
40
+ if (card.senders.length === 0)
41
+ return 'inbox update';
42
+ const updates = card.senders.reduce((total, sender) => total + sender.updates, 0);
43
+ if (card.senders.length === 1)
44
+ return `${updates === 1 ? 'message' : `${updates} messages`} received`;
45
+ return `${updates} messages received from ${card.senders.length} nodes`;
46
+ }
47
+ /** Recovery bodies are the fixed prompts shared with the producer; the
48
+ * model-fallback prompt names its two routes, so it is recognized by the one
49
+ * clause both of its variants build around. */
50
+ function recoverySummary(card) {
51
+ switch (card.body) {
52
+ case AUTH_FAULT_RECOVERY_BODY: return 'provider credentials updated';
53
+ case CONNECTION_FAULT_RECOVERY_BODY: return 'network connection restored';
54
+ case PROVIDER_FAULT_RECOVERY_BODY: return 'provider retry';
55
+ default: return card.body.includes('automatically switched to ') ? 'model route changed' : 'runtime recovery';
56
+ }
57
+ }
58
+ /** The comment notice opens by naming what happened to the comment
59
+ * (daemon/review/comment-notify.ts). */
60
+ const REVIEW_COMMENT_VERB = /^Review comment \S+ was (created|edited|resolved|reopened|deleted)\b/;
61
+ function reviewCommentSummary(card) {
62
+ const verb = REVIEW_COMMENT_VERB.exec(card.body)?.[1];
63
+ return verb === undefined ? 'review comment updated' : `review comment ${verb}`;
64
+ }
65
+ /** Both structured-output reprompts open this way (core/runtime/stop-guard.ts);
66
+ * the stall reprompt does not. */
67
+ const STRUCTURED_OUTPUT_REPROMPT = /^(?:You must call the `submit` tool|Structured-output request file is invalid)/;
68
+ function stopGuardSummary(card) {
69
+ return STRUCTURED_OUTPUT_REPROMPT.test(card.body) ? 'structured output required' : 'completion required';
70
+ }
71
+ /** Presentation for crouter's own kinds. `situational` is deliberately absent:
72
+ * it is migrating under separate ownership and adopts this contract after. */
73
+ const CORE_CARD_PRESENTATIONS = {
74
+ inbox: {
75
+ subject: 'inbox',
76
+ summary: inboxSummary,
77
+ expanded: PAYLOAD,
78
+ groupFolded: 'senders',
79
+ },
80
+ revive: {
81
+ subject: 'revive',
82
+ summary: () => 'fresh context',
83
+ expanded: { disclose: 'section', section: 'yield-message' },
84
+ groupFolded: 'omit',
85
+ },
86
+ // A boundary kind: `isGroupBoundaryKind` keeps it out of every group, so its
87
+ // `groupFolded` is never read.
88
+ 'restart-continuation': {
89
+ subject: 'continuation',
90
+ summary: () => 'continuing after restart',
91
+ expanded: NEVER,
92
+ groupFolded: 'omit',
93
+ },
94
+ 'stop-guard': {
95
+ subject: 'stop guard',
96
+ summary: stopGuardSummary,
97
+ expanded: NEVER,
98
+ groupFolded: 'event',
99
+ },
100
+ recovery: {
101
+ subject: 'recovery',
102
+ summary: recoverySummary,
103
+ expanded: NEVER,
104
+ groupFolded: 'event',
105
+ },
106
+ 'persona-transition': {
107
+ subject: 'persona',
108
+ summary: () => 'role updated',
109
+ expanded: NEVER,
110
+ groupFolded: 'event',
111
+ },
112
+ 'review-approval': {
113
+ subject: 'review',
114
+ summary: () => 'approved by the user',
115
+ expanded: PAYLOAD,
116
+ groupFolded: 'activity',
117
+ },
118
+ 'review-comment': {
119
+ subject: 'review',
120
+ summary: reviewCommentSummary,
121
+ expanded: PAYLOAD,
122
+ groupFolded: 'activity',
123
+ },
124
+ 'review-queued': {
125
+ subject: 'review',
126
+ summary: () => 'submitted; finishing',
127
+ expanded: NEVER,
128
+ groupFolded: 'omit',
129
+ },
130
+ 'context-nudge': {
131
+ subject: 'context',
132
+ summary: () => 'context-window guidance',
133
+ expanded: NEVER,
134
+ groupFolded: 'omit',
135
+ },
136
+ bearings: {
137
+ subject: 'context',
138
+ summary: () => 'orienting context',
139
+ expanded: { disclose: 'section', section: 'auto-loaded-context' },
140
+ groupFolded: 'omit',
141
+ },
142
+ // A boundary kind, as above.
143
+ 'review-boundary': {
144
+ subject: 'review',
145
+ summary: () => 'earlier conversation hidden',
146
+ expanded: NEVER,
147
+ groupFolded: 'omit',
148
+ },
149
+ };
150
+ /** Any kind without a definition — an unregistered product kind, a removed
151
+ * plugin, an old transcript. It stays readable and is never misattributed to
152
+ * the person, so a missing definition is not an error condition. */
153
+ const GENERIC_CARD_PRESENTATION = {
154
+ subject: 'runtime',
155
+ summary: () => 'runtime context',
156
+ expanded: PAYLOAD,
157
+ groupFolded: 'event',
158
+ };
159
+ /** The one lookup. Every display surface deciding how a card behaves resolves it
160
+ * here, so row, disclosure, and recap can never disagree about a kind. */
161
+ export function presentationFor(kind) {
162
+ return CORE_CARD_PRESENTATIONS[kind] ?? GENERIC_CARD_PRESENTATION;
163
+ }
@@ -255,8 +255,6 @@ export declare class ChatView {
255
255
  private ensureGroup;
256
256
  private closeOpenGroup;
257
257
  private appendGroupMember;
258
- /** Used for context that may join an existing run but must not create one. */
259
- private appendExistingGroupMember;
260
258
  private removeGroupMember;
261
259
  /** Add one tool as a group member, moving the assistant's boundary separator
262
260
  * behind the new summary row without changing the separator's identity. */
@@ -23,12 +23,13 @@
23
23
  import { Container, Loader, Spacer, Text } from '@earendil-works/pi-tui';
24
24
  import { AssistantMessageComponent, BashExecutionComponent, BranchSummaryMessageComponent, CompactionSummaryMessageComponent, CustomMessageComponent, parseSkillBlock, SkillInvocationMessageComponent, ToolExecutionComponent, UserMessageComponent, } from '@earendil-works/pi-coding-agent';
25
25
  import { attachMarkdownTheme } from '../config.js';
26
- import { ContextMessageComponent, contextCardBody, expandsWithTools } from './context-message.js';
26
+ import { ContextMessageComponent } from './context-message.js';
27
+ import { presentationFor } from './card-presentation.js';
27
28
  import { detectInlinePageResult, PageBlockComponent } from './page-block.js';
28
29
  import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashToolDefinition, } from './crtr-output.js';
29
30
  import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE, endedByAbort } from '../../../core/runtime/session-cycles.js';
30
31
  import { parseCard } from '../../../shared/generated-context.js';
31
- import { assistantVisibleText, isTrueUserMessage, } from '../../../shared/tool-groups.js';
32
+ import { assistantVisibleText, isGroupBoundaryKind, isTrueUserMessage, } from '../../../shared/tool-groups.js';
32
33
  import { transformDiagramFences } from './diagram.js';
33
34
  import { styleAttachAgentMarkdown, styleAttachMarkdownSource, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-source.js';
34
35
  import { createCrtrEditToolDefinition } from './edit-diff.js';
@@ -1047,25 +1048,19 @@ export class ChatView {
1047
1048
  && card.body.replace(INBOX_SENDER_SECTIONS, '').trim() === ''
1048
1049
  && card.senders.every((sender) => sender.entries.every((entry) => entry.disposition === 'human-answer'));
1049
1050
  if (!onlyHumanWords) {
1050
- const component = new ContextMessageComponent(contextCardBody(card), this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)), card.label, ` ${card.summary} — ctrl+o to expand`, expandsWithTools(card));
1051
- // A successor broker's exact restart continuation is a visible seam, not
1052
- // a group member: the preceding span keeps its own recap and the next tool
1053
- // call begins a broker-aligned fresh group.
1054
- if (card.kind === 'restart-continuation') {
1051
+ const presentation = presentationFor(card.kind);
1052
+ const component = new ContextMessageComponent(card, presentation, this.toolOutputExpanded, this.dimStyle, (s) => this.spinnerStyle(this.labelStyle(s)));
1053
+ // A boundary card is a visible seam, not a group member: the preceding
1054
+ // span keeps its own recap and the next tool call begins a
1055
+ // broker-aligned fresh group.
1056
+ if (isGroupBoundaryKind(card.kind)) {
1055
1057
  this.closeOpenGroup();
1056
1058
  this.append(component);
1057
- // Bearings and revive kickoffs only belong to an already-open run; they
1058
- // never create a summary block by themselves. Other injected context may.
1059
- }
1060
- else if (card.kind === 'bearings' || card.kind === 'revive') {
1061
- this.appendExistingGroupMember(component);
1062
1059
  }
1063
1060
  else {
1064
- const group = this.appendGroupMember(component);
1065
- // A child's final arrives as an inbox digest, not a tool call: without
1066
- // this the folded recap would hide the one row saying it finished.
1067
- if (card.kind === 'inbox')
1068
- group?.activity.noteInboxDigest(card);
1061
+ // A folded group hides its members, so whatever the card should leave
1062
+ // behind — a child's final, a recovery — reaches the recap here.
1063
+ this.appendGroupMember(component)?.activity.noteCard(card, presentation);
1069
1064
  }
1070
1065
  }
1071
1066
  for (const answer of answers)
@@ -1259,12 +1254,6 @@ export class ChatView {
1259
1254
  group.members.push(component);
1260
1255
  return group;
1261
1256
  }
1262
- /** Used for context that may join an existing run but must not create one. */
1263
- appendExistingGroupMember(component) {
1264
- this.append(component);
1265
- if (this.isLiveGroupRegion() && this.openGroup)
1266
- this.openGroup.members.push(component);
1267
- }
1268
1257
  removeGroupMember(component) {
1269
1258
  for (const group of this.groups) {
1270
1259
  const index = group.members.indexOf(component);
@@ -1,19 +1,15 @@
1
1
  import { type Component } from '@earendil-works/pi-tui';
2
+ import { type CardPresentation } from './card-presentation.js';
2
3
  import type { GeneratedCard } from '../../../shared/generated-context.js';
3
- export declare function expandsWithTools(card: GeneratedCard): boolean;
4
- /** What a card discloses when expanded. Bearings and a revive kickoff carry a
5
- * long mandate around one region worth re-reading; every other card discloses
6
- * the body the agent was given. */
7
- export declare function contextCardBody(card: GeneratedCard): string;
8
4
  export declare class ContextMessageComponent implements Component {
9
5
  private readonly dim;
10
6
  private readonly label;
11
- private readonly labelText;
12
- private readonly collapsedHint;
13
- private readonly expandsWithTools;
14
7
  private expanded;
8
+ private readonly expandable;
9
+ private readonly labelTag;
10
+ private readonly collapsedHint;
15
11
  private readonly expandedBody;
16
- constructor(body: string, expanded: boolean, dim: (s: string) => string, label: (s: string) => string, labelText?: string, collapsedHint?: string, expandsWithTools?: boolean);
12
+ constructor(card: GeneratedCard, presentation: CardPresentation, expanded: boolean, dim: (s: string) => string, label: (s: string) => string);
17
13
  /** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
18
14
  setExpanded(expanded: boolean): void;
19
15
  render(width: number): string[];
@@ -1,58 +1,37 @@
1
1
  // render/context-message.ts — the terminal viewer's foldable renderer for
2
2
  // crouter-generated context. Session bearings, situational updates, lifecycle
3
3
  // steering, recovery prompts, and inbox digests retain their model-visible body
4
- // and runtime role. This display-only component renders the disclosure body it
5
- // receives: collapsed → a padded dim line; expanded (Ctrl+O) → the ordinary
6
- // user-message surface, regardless of the message's runtime role. Selected
7
- // cards implement `setExpanded` for the global tool-display cycle; other cards
8
- // remain folded while tool output is shown. Classification lives in
9
- // shared/generated-context.ts; this module owns only what a parsed card
10
- // discloses and how it renders.
4
+ // and runtime role. This display-only component paints one parsed card through
5
+ // its presentation: collapsed → a padded dim line; expanded (Ctrl+O) → the
6
+ // ordinary user-message surface, regardless of the message's runtime role.
7
+ // Classification lives in shared/generated-context.ts and every per-kind
8
+ // decision in ./card-presentation.ts; this module owns only the painting.
11
9
  import { UserMessageComponent } from '@earendil-works/pi-coding-agent';
12
10
  import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
13
11
  import { attachMarkdownTheme } from '../config.js';
12
+ import { cardDisclosure, cardIsExpandable, cardLabel, } from './card-presentation.js';
14
13
  import { styleAttachMarkdownSource } from './markdown-source.js';
15
- /** Default label/hint for the `crtr-context` bearings block. */
16
- const LABEL = 'crtr context';
17
- const COLLAPSED_HINT = ' orienting bearings — ctrl+o to expand';
18
- const AUTO_LOADED_CONTEXT_BLOCK = /<auto-loaded-context>([\s\S]*?)<\/auto-loaded-context>/;
19
- const YIELD_MESSAGE_BLOCK = /<yield-message>([\s\S]*?)<\/yield-message>/;
20
- /** Cards the global ctrl+o cycle expands; every other card stays folded while
21
- * tool output is shown. */
22
- const EXPANDS_WITH_TOOLS = new Set(['bearings', 'revive', 'inbox', 'review-approval']);
23
- export function expandsWithTools(card) {
24
- return EXPANDS_WITH_TOOLS.has(card.kind);
25
- }
26
- /** What a card discloses when expanded. Bearings and a revive kickoff carry a
27
- * long mandate around one region worth re-reading; every other card discloses
28
- * the body the agent was given. */
29
- export function contextCardBody(card) {
30
- if (card.kind === 'bearings')
31
- return AUTO_LOADED_CONTEXT_BLOCK.exec(card.body)?.[1]?.trim() ?? '';
32
- if (card.kind === 'revive')
33
- return YIELD_MESSAGE_BLOCK.exec(card.body)?.[1]?.trim() ?? '';
34
- return card.body;
35
- }
36
14
  export class ContextMessageComponent {
37
15
  dim;
38
16
  label;
39
- labelText;
40
- collapsedHint;
41
- expandsWithTools;
42
17
  expanded;
18
+ expandable;
19
+ labelTag;
20
+ collapsedHint;
43
21
  expandedBody;
44
- constructor(body, expanded, dim, label, labelText = LABEL, collapsedHint = COLLAPSED_HINT, expandsWithTools = true) {
22
+ constructor(card, presentation, expanded, dim, label) {
45
23
  this.dim = dim;
46
24
  this.label = label;
47
- this.labelText = labelText;
48
- this.collapsedHint = collapsedHint;
49
- this.expandsWithTools = expandsWithTools;
50
- this.expanded = expandsWithTools && expanded;
51
- this.expandedBody = new UserMessageComponent(styleAttachMarkdownSource(body), attachMarkdownTheme());
25
+ this.expandable = cardIsExpandable(presentation, card.body);
26
+ this.expanded = this.expandable && expanded;
27
+ this.labelTag = `[${cardLabel(presentation.subject)}]`;
28
+ // A card with nothing to disclose must not advertise the toggle.
29
+ this.collapsedHint = ` ${presentation.summary(card)}${this.expandable ? ' — ctrl+o to expand' : ''}`;
30
+ this.expandedBody = new UserMessageComponent(styleAttachMarkdownSource(cardDisclosure(presentation, card.body)), attachMarkdownTheme());
52
31
  }
53
32
  /** Ctrl+O global tool-output toggle (ChatView.isExpandable duck-type). */
54
33
  setExpanded(expanded) {
55
- if (this.expandsWithTools)
34
+ if (this.expandable)
56
35
  this.expanded = expanded;
57
36
  }
58
37
  render(width) {
@@ -62,12 +41,10 @@ export class ContextMessageComponent {
62
41
  // reads distinctly in BOTH states. Truncate BEFORE painting so the ANSI
63
42
  // wrappers never inflate the measured width (an over-wide line aborts the
64
43
  // whole TUI render).
65
- const labelTag = `[${this.labelText}]`;
66
- const rest = this.collapsedHint;
67
- const labelW = visibleWidth(labelTag);
44
+ const labelW = visibleWidth(this.labelTag);
68
45
  if (labelW >= w)
69
- return ['', this.label(truncateToWidth(labelTag, w, '…')), ''];
70
- return ['', this.label(labelTag) + this.dim(truncateToWidth(rest, w - labelW, '…')), ''];
46
+ return ['', this.label(truncateToWidth(this.labelTag, w, '…')), ''];
47
+ return ['', this.label(this.labelTag) + this.dim(truncateToWidth(this.collapsedHint, w - labelW, '…')), ''];
71
48
  }
72
49
  const lines = this.expandedBody.render(w);
73
50
  if (lines.at(-1) !== '')
@@ -1,3 +1,4 @@
1
+ import type { CardPresentation } from './card-presentation.js';
1
2
  import type { GeneratedCard } from '../../../shared/generated-context.js';
2
3
  export type FileActivity = {
3
4
  key: string;
@@ -20,10 +21,24 @@ export type NodeActivity = {
20
21
  name: string;
21
22
  id: string;
22
23
  };
24
+ /** A card whose fold-time contribution is structured: its own summary line. */
25
+ export type CardActivity = {
26
+ summary: string;
27
+ };
28
+ /** A card that folds to one terse line. Repeats compress into `count` so a run
29
+ * of recoveries costs one row, not one row each. */
30
+ export type EventActivity = {
31
+ text: string;
32
+ count: number;
33
+ };
23
34
  export type GroupActivity = {
24
35
  files: readonly FileActivity[];
25
36
  memories: readonly MemoryActivity[];
26
37
  nodes: readonly NodeActivity[];
38
+ cards: readonly CardActivity[];
39
+ events: readonly EventActivity[];
40
+ /** Distinct events past the budget, so the recap can say what it dropped. */
41
+ eventsOmitted: number;
27
42
  };
28
43
  /** Per-group recorder. Calls retain only their current transcript references
29
44
  * until they settle; the settled activity is plain immutable evidence. */
@@ -36,17 +51,26 @@ export declare class GroupActivityRecorder {
36
51
  private readonly memorySet;
37
52
  private readonly nodes;
38
53
  private readonly nodeSet;
54
+ private readonly cards;
55
+ private readonly cardSet;
56
+ private readonly events;
57
+ private eventsOmitted;
39
58
  /** `roster` is the ChatView-wide node id → display name record: spawns fill
40
59
  * it, and a later group's finish reads the name a sibling group recorded. */
41
60
  constructor(cwd: string, roster?: Map<string, string>);
42
- /** Record the nodes an inbox digest reports finished. The spawn roster supplies
43
- * the display name when it has one. A sender carrying only the person's own
44
- * words is the human speaking, not a node that finished. */
45
- noteInboxDigest(card: GeneratedCard): void;
61
+ /** What one runtime card leaves behind when its group folds, decided entirely
62
+ * by the card's presentation policy. */
63
+ noteCard(card: GeneratedCard, presentation: CardPresentation): void;
46
64
  noteCall(id: string, name: string, args: unknown): void;
47
65
  settle(id: string, result: unknown, isError: boolean): void;
48
66
  resolveWithoutResult(id: string): void;
49
67
  activity(): GroupActivity;
68
+ /** The nodes an inbox digest reports finished. The spawn roster supplies the
69
+ * display name when it has one. A sender carrying only the person's own words
70
+ * is the human speaking, not a node that finished. */
71
+ private addFinishedSenders;
72
+ private addCard;
73
+ private addEvent;
50
74
  private addNode;
51
75
  private extract;
52
76
  }
@@ -4,10 +4,15 @@
4
4
  import { homedir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { countDisplayDiffLines } from './edit-diff.js';
7
+ /** Distinct event lines one recap will show before it starts counting. */
8
+ const MAX_RECAP_EVENTS = 3;
7
9
  const EMPTY_ACTIVITY = Object.freeze({
8
10
  files: Object.freeze([]),
9
11
  memories: Object.freeze([]),
10
12
  nodes: Object.freeze([]),
13
+ cards: Object.freeze([]),
14
+ events: Object.freeze([]),
15
+ eventsOmitted: 0,
11
16
  });
12
17
  function object(value) {
13
18
  return typeof value === 'object' && value !== null ? value : undefined;
@@ -155,22 +160,31 @@ export class GroupActivityRecorder {
155
160
  memorySet = new Set();
156
161
  nodes = [];
157
162
  nodeSet = new Set();
163
+ cards = [];
164
+ cardSet = new Set();
165
+ events = new Map();
166
+ eventsOmitted = 0;
158
167
  /** `roster` is the ChatView-wide node id → display name record: spawns fill
159
168
  * it, and a later group's finish reads the name a sibling group recorded. */
160
169
  constructor(cwd, roster = new Map()) {
161
170
  this.cwd = cwd;
162
171
  this.roster = roster;
163
172
  }
164
- /** Record the nodes an inbox digest reports finished. The spawn roster supplies
165
- * the display name when it has one. A sender carrying only the person's own
166
- * words is the human speaking, not a node that finished. */
167
- noteInboxDigest(card) {
168
- for (const sender of card.senders) {
169
- const onlyHumanWords = sender.entries.length > 0
170
- && sender.entries.every((entry) => entry.disposition === 'human-answer');
171
- if (!sender.finished || onlyHumanWords)
172
- continue;
173
- this.addNode({ action: 'finished', name: this.roster.get(sender.id) ?? sender.id, id: sender.id });
173
+ /** What one runtime card leaves behind when its group folds, decided entirely
174
+ * by the card's presentation policy. */
175
+ noteCard(card, presentation) {
176
+ switch (presentation.groupFolded) {
177
+ case 'omit':
178
+ return;
179
+ case 'event':
180
+ this.addEvent(presentation.summary(card));
181
+ return;
182
+ case 'activity':
183
+ this.addCard({ summary: presentation.summary(card) });
184
+ return;
185
+ case 'senders':
186
+ this.addFinishedSenders(card);
187
+ return;
174
188
  }
175
189
  }
176
190
  noteCall(id, name, args) {
@@ -197,17 +211,57 @@ export class GroupActivityRecorder {
197
211
  entry.args = undefined;
198
212
  }
199
213
  activity() {
200
- if (this.files.size === 0 && this.memories.length === 0 && this.nodes.length === 0)
214
+ if (this.files.size === 0 && this.memories.length === 0 && this.nodes.length === 0
215
+ && this.cards.length === 0 && this.events.size === 0)
201
216
  return EMPTY_ACTIVITY;
202
217
  const files = [...this.files.values()].map((file) => Object.freeze({ ...file }));
203
218
  const memories = this.memories.map((memory) => Object.freeze({ ...memory }));
204
219
  const nodes = this.nodes.map((node) => Object.freeze({ ...node }));
220
+ const cards = this.cards.map((card) => Object.freeze({ ...card }));
221
+ const events = [...this.events].map(([text, count]) => Object.freeze({ text, count }));
205
222
  return Object.freeze({
206
223
  files: Object.freeze(files),
207
224
  memories: Object.freeze(memories),
208
225
  nodes: Object.freeze(nodes),
226
+ cards: Object.freeze(cards),
227
+ events: Object.freeze(events),
228
+ eventsOmitted: this.eventsOmitted,
209
229
  });
210
230
  }
231
+ /** The nodes an inbox digest reports finished. The spawn roster supplies the
232
+ * display name when it has one. A sender carrying only the person's own words
233
+ * is the human speaking, not a node that finished. */
234
+ addFinishedSenders(card) {
235
+ for (const sender of card.senders) {
236
+ const onlyHumanWords = sender.entries.length > 0
237
+ && sender.entries.every((entry) => entry.disposition === 'human-answer');
238
+ if (!sender.finished || onlyHumanWords)
239
+ continue;
240
+ this.addNode({ action: 'finished', name: this.roster.get(sender.id) ?? sender.id, id: sender.id });
241
+ }
242
+ }
243
+ addCard(card) {
244
+ const key = card.summary;
245
+ if (this.cardSet.has(key))
246
+ return;
247
+ this.cardSet.add(key);
248
+ this.cards.push(card);
249
+ }
250
+ addEvent(text) {
251
+ const line = text.trim();
252
+ if (line === '')
253
+ return;
254
+ const seen = this.events.get(line);
255
+ if (seen !== undefined) {
256
+ this.events.set(line, seen + 1);
257
+ return;
258
+ }
259
+ if (this.events.size >= MAX_RECAP_EVENTS) {
260
+ this.eventsOmitted += 1;
261
+ return;
262
+ }
263
+ this.events.set(line, 1);
264
+ }
211
265
  addNode(node) {
212
266
  const key = `${node.action}:${node.id}`;
213
267
  if (this.nodeSet.has(key))
@@ -6,5 +6,8 @@ import type { GroupActivity } from './group-activity.js';
6
6
  * group is eligible to collapse, preserving the member rows for Ctrl+O. */
7
7
  export declare class GroupRecapComponent extends Container {
8
8
  setRecap(summary: ToolGroupSummary | undefined, toolCount: number, activity: GroupActivity, detailed: boolean, palette: AttachPalette): void;
9
+ /** Runtime cards the fold hid. Terse events are already compressed by count,
10
+ * so every card contributes exactly one row. */
11
+ private addCardEvidence;
9
12
  private addFile;
10
13
  }
@@ -8,6 +8,8 @@ import { TOOL_ICON } from '../../../core/preview-registry.js';
8
8
  const MEMORY_ICON = String.fromCodePoint(0xf02d);
9
9
  // trash — deletion has no tool of its own, so it borrows no TOOL_ICON entry.
10
10
  const DELETE_ICON = String.fromCodePoint(0xf1f8);
11
+ // info-circle — runtime context the fold hid, kept as its own evidence line.
12
+ const CARD_ICON = String.fromCodePoint(0xf05a);
11
13
  function count(value, singular, plural) {
12
14
  return `${value} ${value === 1 ? singular : plural}`;
13
15
  }
@@ -102,10 +104,25 @@ export class GroupRecapComponent extends Container {
102
104
  }
103
105
  this.addChild(new Text(stats.join(palette.muted(' · ')), 0, 0));
104
106
  }
107
+ this.addCardEvidence(activity, palette);
105
108
  for (const bullet of summary.bullets)
106
109
  this.addChild(new Text(palette.muted(` • ${bullet.trim()}`), 0, 0));
107
110
  this.addChild(new Spacer(1));
108
111
  }
112
+ /** Runtime cards the fold hid. Terse events are already compressed by count,
113
+ * so every card contributes exactly one row. */
114
+ addCardEvidence(activity, palette) {
115
+ for (const card of activity.cards) {
116
+ this.addChild(new Text(`${palette.info(CARD_ICON)} ${palette.muted(card.summary)}`, 0, 0));
117
+ }
118
+ for (const event of activity.events) {
119
+ const repeats = event.count > 1 ? ` ×${event.count}` : '';
120
+ this.addChild(new Text(`${palette.info(CARD_ICON)} ${palette.muted(`${event.text}${repeats}`)}`, 0, 0));
121
+ }
122
+ if (activity.eventsOmitted > 0) {
123
+ this.addChild(new Text(palette.muted(` ${count(activity.eventsOmitted, 'more runtime event', 'more runtime events')}`), 0, 0));
124
+ }
125
+ }
109
126
  addFile(file, palette) {
110
127
  const counts = file.hasKnownCounts
111
128
  ? `${palette.diffAdded(`+${file.added}`)}${file.kind === 'edit' ? ` ${palette.diffRemoved(`-${file.removed}`)}` : ''}`