@north-light/crouter 0.3.206 → 0.3.208
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.
- package/dist/api/dto/messages.d.ts +4 -3
- package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
- package/dist/clients/attach/render/card-presentation.d.ts +31 -0
- package/dist/clients/attach/render/card-presentation.js +163 -0
- package/dist/clients/attach/render/chat-view.d.ts +0 -2
- package/dist/clients/attach/render/chat-view.js +12 -23
- package/dist/clients/attach/render/context-message.d.ts +5 -9
- package/dist/clients/attach/render/context-message.js +20 -43
- package/dist/clients/attach/render/group-activity.d.ts +28 -4
- package/dist/clients/attach/render/group-activity.js +65 -11
- package/dist/clients/attach/render/group-recap.d.ts +3 -0
- package/dist/clients/attach/render/group-recap.js +17 -0
- package/dist/clients/attach/viewer.js +516 -516
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
- package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
- package/dist/core/__tests__/helpers/broker-clients.js +7 -2
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
- package/dist/core/canvas/paths.d.ts +2 -2
- package/dist/core/canvas/paths.js +2 -2
- package/dist/core/runtime/broker/fault-retry.js +2 -7
- package/dist/core/runtime/broker/frame-dispatch.js +1 -1
- package/dist/core/runtime/broker/tool-groups.js +7 -6
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/kickoff.js +4 -10
- package/dist/core/runtime/node-read.js +7 -1
- package/dist/core/runtime/situational-context.d.ts +0 -2
- package/dist/core/runtime/situational-context.js +3 -2
- package/dist/core/runtime/situational-live.js +1 -1
- package/dist/core/runtime/spawn.d.ts +4 -4
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/daemon/messaging/node-message.d.ts +4 -1
- package/dist/daemon/review/comment-notify.js +1 -8
- package/dist/daemon/review/deliver.js +1 -1
- package/dist/daemon/review/finish.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +4 -2
- package/dist/pi-extensions/canvas-review-boundary.js +1 -1
- package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
- package/dist/shared/generated-context.d.ts +0 -18
- package/dist/shared/generated-context.js +16 -120
- package/dist/shared/tool-groups.d.ts +1 -0
- package/dist/shared/tool-groups.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -22,9 +22,10 @@ export interface SendMessageRequest {
|
|
|
22
22
|
/** Clear a latched target's finalization latch before an immediate delivery
|
|
23
23
|
* or `--fresh` revive (`--reopen`). Immediate only. */
|
|
24
24
|
reopen?: boolean;
|
|
25
|
-
/** Hidden ambient context upserted onto the target's sidecar
|
|
26
|
-
*
|
|
27
|
-
* Immediate only; valid alone (no body).
|
|
25
|
+
/** Hidden ambient context upserted onto the target's sidecar as a
|
|
26
|
+
* `situational` card, never visible chat (`--situational-context`).
|
|
27
|
+
* Immediate only; valid alone (no body). Prose only — its body is NOT
|
|
28
|
+
* escaped; a caller with untrusted text uses `situational_card`. */
|
|
28
29
|
situational_context?: string;
|
|
29
30
|
/** A runtime card that REPLACES the target's situational sidecar, delivered
|
|
30
31
|
* ahead of the body in the same turn and re-stated by the session-start
|
|
@@ -70,7 +70,14 @@ test('a folded recap carries paths and counts, never diff or write payload bodie
|
|
|
70
70
|
bashMode: tone('bashMode'), bashModeAlt: tone('bashModeAlt'),
|
|
71
71
|
};
|
|
72
72
|
const recap = new GroupRecapComponent();
|
|
73
|
-
recap.setRecap({ bullets: ['finished'], filesDeleted: 2 }, 2, {
|
|
73
|
+
recap.setRecap({ bullets: ['finished'], filesDeleted: 2 }, 2, {
|
|
74
|
+
memories: [{ action: 'read', name: 'alpha', path: '/tmp/memories/alpha.md' }],
|
|
75
|
+
files: recorder.activity().files,
|
|
76
|
+
nodes: [],
|
|
77
|
+
cards: [],
|
|
78
|
+
events: [],
|
|
79
|
+
eventsOmitted: 0,
|
|
80
|
+
}, true, palette);
|
|
74
81
|
const text = recap.render(120).join('\n');
|
|
75
82
|
assert.match(text, /overwritten\.ts/, 'the recap still names what changed');
|
|
76
83
|
assert.doesNotMatch(text, /sensitive-old-diff|sensitive-new-diff|sensitive-write-payload/, 'payload bodies never leak into a recap');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type GeneratedCard } from '../../../shared/generated-context.js';
|
|
2
|
+
/** What the one global detail toggle reveals for this kind.
|
|
3
|
+
* `section` names an element in the body; everything outside it stays folded. */
|
|
4
|
+
export type CardExpandedPolicy = {
|
|
5
|
+
readonly disclose: 'never';
|
|
6
|
+
} | {
|
|
7
|
+
readonly disclose: 'payload';
|
|
8
|
+
} | {
|
|
9
|
+
readonly disclose: 'section';
|
|
10
|
+
readonly section: string;
|
|
11
|
+
};
|
|
12
|
+
/** What is left of the card once its turn folds into one recap line. A boundary
|
|
13
|
+
* kind never reaches this field — `isGroupBoundaryKind` removes it from the
|
|
14
|
+
* group first — so every case here is a member policy. */
|
|
15
|
+
export type CardGroupFoldedPolicy = 'omit' | 'event' | 'activity' | 'senders';
|
|
16
|
+
export interface CardPresentation {
|
|
17
|
+
/** The noun in `[crtr <subject>]`. Never a full label — the prefix is generated. */
|
|
18
|
+
readonly subject: string;
|
|
19
|
+
readonly summary: (card: GeneratedCard) => string;
|
|
20
|
+
readonly expanded: CardExpandedPolicy;
|
|
21
|
+
readonly groupFolded: CardGroupFoldedPolicy;
|
|
22
|
+
}
|
|
23
|
+
/** Every label in the transcript reads the same way; no definition stores one. */
|
|
24
|
+
export declare function cardLabel(subject: string): string;
|
|
25
|
+
/** The disclosure a card actually offers, given its policy and body. A kind
|
|
26
|
+
* whose section is missing from an old transcript offers nothing to expand. */
|
|
27
|
+
export declare function cardDisclosure(presentation: CardPresentation, body: string): string;
|
|
28
|
+
export declare function cardIsExpandable(presentation: CardPresentation, body: string): boolean;
|
|
29
|
+
/** The one lookup. Every display surface deciding how a card behaves resolves it
|
|
30
|
+
* here, so row, disclosure, and recap can never disagree about a kind. */
|
|
31
|
+
export declare function presentationFor(kind: string): CardPresentation;
|
|
@@ -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
|
|
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
|
|
1051
|
-
|
|
1052
|
-
//
|
|
1053
|
-
// call begins a
|
|
1054
|
-
|
|
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
|
-
|
|
1065
|
-
//
|
|
1066
|
-
|
|
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(
|
|
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
|
|
5
|
-
//
|
|
6
|
-
// user-message surface, regardless of the message's runtime role.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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(
|
|
22
|
+
constructor(card, presentation, expanded, dim, label) {
|
|
45
23
|
this.dim = dim;
|
|
46
24
|
this.label = label;
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
this.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
43
|
-
* the
|
|
44
|
-
|
|
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
|
-
/**
|
|
165
|
-
* the
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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}`)}` : ''}`
|