@north-light/crouter 0.3.204 → 0.3.206
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/broker.d.ts +2 -0
- package/dist/api/dto/messages.d.ts +22 -1
- package/dist/api/dto/nodes.d.ts +8 -5
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +54 -19
- package/dist/clients/attach/__tests__/page-block.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/page-block.test.js +54 -0
- package/dist/clients/attach/render/chat-view.d.ts +3 -0
- package/dist/clients/attach/render/chat-view.js +56 -27
- package/dist/clients/attach/render/context-message.d.ts +6 -0
- package/dist/clients/attach/render/context-message.js +21 -3
- package/dist/clients/attach/render/group-activity.d.ts +5 -3
- package/dist/clients/attach/render/group-activity.js +10 -6
- package/dist/clients/attach/render/page-block.js +11 -11
- package/dist/clients/attach/viewer.js +717 -703
- package/dist/clients/conversation/projection.js +7 -37
- package/dist/commands/node/create.js +1 -1
- package/dist/commands/profile/project.js +1 -1
- package/dist/commands/profile/show.js +1 -1
- package/dist/commands/profile.js +1 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +90 -23
- package/dist/core/__tests__/context-intro.test.js +7 -8
- package/dist/core/__tests__/human-deliver.test.js +32 -4
- package/dist/core/__tests__/kickoff.test.js +6 -6
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +5 -2
- package/dist/core/__tests__/serial/deferred-no-wake.test.js +7 -4
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +4 -1
- package/dist/core/__tests__/serial/revive.test.js +8 -3
- package/dist/core/__tests__/session-cycles.test.js +10 -6
- package/dist/core/canvas/render-source.js +3 -0
- package/dist/core/feed/feed.d.ts +7 -1
- package/dist/core/feed/feed.js +8 -7
- package/dist/core/feed/inbox.d.ts +16 -12
- package/dist/core/feed/inbox.js +56 -56
- package/dist/core/human/page-eval.d.ts +9 -4
- package/dist/core/human/page-eval.js +3 -1
- package/dist/core/human/page-markdown.d.ts +3 -0
- package/dist/core/human/page-markdown.js +295 -0
- package/dist/core/human/page.js +4 -0
- package/dist/core/runtime/bearings.js +5 -4
- package/dist/core/runtime/broker/client-registry.d.ts +1 -0
- package/dist/core/runtime/broker/client-registry.js +1 -0
- package/dist/core/runtime/broker/fault-retry.js +8 -3
- package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +13 -2
- package/dist/core/runtime/broker/inbox.d.ts +20 -2
- package/dist/core/runtime/broker/inbox.js +68 -25
- package/dist/core/runtime/broker/passive.d.ts +1 -0
- package/dist/core/runtime/broker/tool-groups.js +3 -2
- package/dist/core/runtime/broker-extension-render.d.ts +3 -0
- package/dist/core/runtime/broker-extension-render.js +3 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker-protocol.js +11 -0
- package/dist/core/runtime/broker.d.ts +2 -2
- package/dist/core/runtime/broker.js +31 -6
- package/dist/core/runtime/deliver-live.js +2 -1
- package/dist/core/runtime/interactive-deliver.d.ts +3 -2
- package/dist/core/runtime/interactive-deliver.js +4 -3
- package/dist/core/runtime/kickoff.d.ts +3 -4
- package/dist/core/runtime/kickoff.js +27 -16
- package/dist/core/runtime/node-read.d.ts +2 -0
- package/dist/core/runtime/node-read.js +11 -3
- package/dist/core/runtime/session-cycles.d.ts +6 -1
- package/dist/core/runtime/session-cycles.js +17 -11
- package/dist/core/runtime/session-visibility.d.ts +7 -12
- package/dist/core/runtime/session-visibility.js +7 -13
- package/dist/core/runtime/situational-context.d.ts +14 -5
- package/dist/core/runtime/situational-context.js +39 -21
- package/dist/core/runtime/situational-live.d.ts +2 -2
- package/dist/core/runtime/situational-live.js +17 -16
- package/dist/core/runtime/spawn.js +4 -3
- package/dist/core/runtime/stop-guard.js +8 -4
- package/dist/core/runtime/warm-pool.d.ts +1 -1
- package/dist/core/runtime/warm-pool.js +6 -9
- package/dist/daemon/api/handlers/messages.js +101 -17
- package/dist/daemon/api/handlers/nodes.js +12 -8
- package/dist/daemon/human/finish.js +44 -4
- package/dist/daemon/messaging/node-message.d.ts +5 -0
- package/dist/daemon/messaging/node-message.js +6 -1
- package/dist/daemon/review/comment-notify.js +8 -0
- package/dist/daemon/review/deliver.js +2 -2
- package/dist/daemon/review/finish.js +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +96 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +2 -2
- package/dist/pi-extensions/broker-local.d.ts +0 -2
- package/dist/pi-extensions/broker-local.js +0 -2
- package/dist/pi-extensions/canvas-context-intro.js +9 -8
- package/dist/pi-extensions/canvas-goal-capture.js +10 -6
- package/dist/pi-extensions/canvas-inbox-watcher.js +116 -69
- package/dist/pi-extensions/canvas-passive-context.d.ts +10 -0
- package/dist/pi-extensions/canvas-passive-context.js +23 -3
- package/dist/pi-extensions/canvas-review-boundary.d.ts +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +11 -3
- package/dist/pi-extensions/canvas-stophook.js +3 -3
- package/dist/shared/__tests__/generated-context-grammar.test.d.ts +1 -0
- package/dist/shared/__tests__/generated-context-grammar.test.js +85 -0
- package/dist/shared/generated-context.d.ts +68 -36
- package/dist/shared/generated-context.js +300 -174
- package/dist/shared/tool-groups.js +3 -2
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/postinstall.mjs +9 -0
package/dist/api/dto/broker.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { InboxTierDTO, IsoTime, NodeIdDTO } from './common.js';
|
|
2
|
+
/** One runtime card supplied by a caller: the runtime renders and escapes it
|
|
3
|
+
* exactly once, so a caller cannot emit a malformed card or smuggle markup
|
|
4
|
+
* into one. `kind` must be namespaced (contain `:`) — bare kinds are
|
|
5
|
+
* crouter's own closed vocabulary. */
|
|
6
|
+
export interface RuntimeCardRequest {
|
|
7
|
+
kind: string;
|
|
8
|
+
facts?: Record<string, string | number>;
|
|
9
|
+
/** Data, not markup. */
|
|
10
|
+
body?: string;
|
|
11
|
+
}
|
|
2
12
|
/** `POST /v1/nodes/{id}/messages` body. */
|
|
3
13
|
export interface SendMessageRequest {
|
|
4
14
|
body: string;
|
|
@@ -16,6 +26,14 @@ export interface SendMessageRequest {
|
|
|
16
26
|
* a `<situational-context>` block, never visible chat (`--situational-context`).
|
|
17
27
|
* Immediate only; valid alone (no body). */
|
|
18
28
|
situational_context?: string;
|
|
29
|
+
/** A runtime card that REPLACES the target's situational sidecar, delivered
|
|
30
|
+
* ahead of the body in the same turn and re-stated by the session-start
|
|
31
|
+
* bearings after a context refresh. Mutually exclusive with
|
|
32
|
+
* `situational_context`; valid alone (no body) on the durable path. */
|
|
33
|
+
situational_card?: RuntimeCardRequest;
|
|
34
|
+
/** One-shot runtime cards for this turn only — never persisted. Delivered in
|
|
35
|
+
* array order, after `situational_card` and ahead of the body. */
|
|
36
|
+
context_cards?: RuntimeCardRequest[];
|
|
19
37
|
/** Raw JSON-schema string granting a one-off `submit` tool before delivery
|
|
20
38
|
* (`--output-schema`). Immediate only. */
|
|
21
39
|
output_schema?: string;
|
|
@@ -24,7 +42,10 @@ export interface SendMessageRequest {
|
|
|
24
42
|
* instead of the durable inbox; a dormant or mid-revive target falls back to
|
|
25
43
|
* the durable inbox + revive (watcher delivers post-boot). Plain immediate
|
|
26
44
|
* body only — rejected with fresh/reopen/situational_context/
|
|
27
|
-
* output_schema or tier 'deferred'.
|
|
45
|
+
* output_schema or tier 'deferred'. Runtime cards ARE accepted: a
|
|
46
|
+
* card-bearing send is an ordinary human send that happens to carry context,
|
|
47
|
+
* and the live deliver frame places the cards ahead of the body in one turn.
|
|
48
|
+
* Absent → durable inbox (unchanged). */
|
|
28
49
|
delivery?: 'interactive';
|
|
29
50
|
}
|
|
30
51
|
/** Result of an immediate message send. */
|
package/dist/api/dto/nodes.d.ts
CHANGED
|
@@ -5,12 +5,13 @@ export interface CreateNodeRequest {
|
|
|
5
5
|
prompt?: string;
|
|
6
6
|
profile?: string;
|
|
7
7
|
mode?: ModeDTO;
|
|
8
|
-
/** The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
/** The directory the create came from. It is what the daemon selects a
|
|
9
|
+
* profile from when the request neither names one nor inherits one from a
|
|
10
|
+
* parent, and — for a node with no `pin_cwd` and no spawner to inherit from —
|
|
11
|
+
* where the node lands. */
|
|
11
12
|
cwd?: string;
|
|
12
|
-
/** Pin the node to this exact directory, overriding the
|
|
13
|
-
*
|
|
13
|
+
/** Pin the node to this exact directory, overriding both the spawner's
|
|
14
|
+
* directory and the launch cwd. See `crtr node new --cwd`. */
|
|
14
15
|
pin_cwd?: string;
|
|
15
16
|
/** Display name (tmux window + resume picker). Defaults to the kind. */
|
|
16
17
|
name?: string;
|
|
@@ -187,6 +188,8 @@ export interface NodeMessagesPageDTO {
|
|
|
187
188
|
node_id: NodeIdDTO;
|
|
188
189
|
/** pi AgentMessage[] JSON, chronological ascending within this page. */
|
|
189
190
|
messages: unknown[];
|
|
191
|
+
/** Stable session-entry ids aligned 1:1 with `messages`. */
|
|
192
|
+
message_ids?: string[];
|
|
190
193
|
/** Opaque cursor toward older messages; null at the start of the session. */
|
|
191
194
|
next_cursor: Cursor | null;
|
|
192
195
|
captured_at: IsoTime;
|
package/dist/api/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { CrtrClientOptions } from './client.js';
|
|
|
3
3
|
export { ApiError, isErrorBody } from './errors.js';
|
|
4
4
|
export type { ErrorBody } from './errors.js';
|
|
5
5
|
export { API_VERSION, routes } from './routes.js';
|
|
6
|
+
export * from '../shared/generated-context.js';
|
|
6
7
|
export * from './dto/common.js';
|
|
7
8
|
export * from './dto/health.js';
|
|
8
9
|
export * from './dto/nodes.js';
|
package/dist/api/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export { CrtrClient } from './client.js';
|
|
5
5
|
export { ApiError, isErrorBody } from './errors.js';
|
|
6
6
|
export { API_VERSION, routes } from './routes.js';
|
|
7
|
+
export * from '../shared/generated-context.js';
|
|
7
8
|
export * from './dto/common.js';
|
|
8
9
|
export * from './dto/health.js';
|
|
9
10
|
export * from './dto/nodes.js';
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
// Regression coverage for the attach viewer's display-only folding of
|
|
2
|
-
// crouter-generated context.
|
|
2
|
+
// crouter-generated context. Three user-visible hazards are pinned here:
|
|
3
3
|
//
|
|
4
4
|
// 1. A folded row must never leak its body, and Ctrl+O must restore the full
|
|
5
5
|
// agent-visible content — the fold is display-only, so nothing is lost.
|
|
6
6
|
// 2. A human message that merely reads like runtime recovery prose must NOT
|
|
7
7
|
// be classified as generated context; folding the user's own message away
|
|
8
8
|
// is the hazard that misclassification produces.
|
|
9
|
+
// 3. An answer the person typed in their inbox renders as the person
|
|
10
|
+
// speaking, never as a runtime card.
|
|
9
11
|
import assert from 'node:assert/strict';
|
|
10
12
|
import test from 'node:test';
|
|
11
13
|
import { Container } from '@earendil-works/pi-tui';
|
|
12
14
|
import { applyTheme } from '../config.js';
|
|
13
15
|
import { ChatView } from '../render/chat-view.js';
|
|
14
|
-
import { CONTEXT_INTRO_CUSTOM_TYPE, CONTEXT_NUDGE_CUSTOM_TYPE,
|
|
16
|
+
import { CONTEXT_INTRO_CUSTOM_TYPE, CONTEXT_NUDGE_CUSTOM_TYPE, formatCard, formatInboxCard, formatModelFallbackRecovery, parseCard, } from '../../../shared/generated-context.js';
|
|
15
17
|
applyTheme();
|
|
18
|
+
function chatView(container) {
|
|
19
|
+
const tui = { requestRender() { }, terminal: { rows: 40 } };
|
|
20
|
+
return new ChatView(tui, container, { toolOutputExpanded: false });
|
|
21
|
+
}
|
|
16
22
|
test('collapsed generated context never leaks its body; Ctrl+O restores it', async () => {
|
|
17
23
|
const container = new Container();
|
|
18
|
-
const
|
|
19
|
-
const view = new ChatView(tui, container, { toolOutputExpanded: false });
|
|
24
|
+
const view = chatView(container);
|
|
20
25
|
const internals = view;
|
|
21
26
|
internals.summarizeToolCalls = true;
|
|
22
27
|
internals.foldSettledTools = true;
|
|
@@ -27,41 +32,71 @@ test('collapsed generated context never leaks its body; Ctrl+O restores it', asy
|
|
|
27
32
|
role: 'custom',
|
|
28
33
|
customType: CONTEXT_INTRO_CUSTOM_TYPE,
|
|
29
34
|
display: true,
|
|
30
|
-
content: '<auto-loaded-context>\nloaded INDEX.md content\n</auto-loaded-context>',
|
|
35
|
+
content: formatCard('bearings', {}, '<auto-loaded-context>\nloaded INDEX.md content\n</auto-loaded-context>'),
|
|
31
36
|
},
|
|
32
|
-
{ role: 'user', content:
|
|
33
|
-
{ role: 'user', content:
|
|
37
|
+
{ role: 'user', content: formatCard('revive', {}, '<yield-message>resume from this note</yield-message>') },
|
|
38
|
+
{ role: 'user', content: formatCard('restart-continuation', {}, 'private restart continuation') },
|
|
39
|
+
// Pre-envelope custom message from an old session: customType still classifies.
|
|
34
40
|
{
|
|
35
41
|
role: 'custom',
|
|
36
42
|
customType: CONTEXT_NUDGE_CUSTOM_TYPE,
|
|
37
43
|
display: true,
|
|
38
44
|
content: '[crtr] Context ~150k. private context reminder',
|
|
39
45
|
},
|
|
40
|
-
{
|
|
41
|
-
|
|
46
|
+
{
|
|
47
|
+
role: 'user',
|
|
48
|
+
content: formatInboxCard([
|
|
49
|
+
{ id: 'child-node-123', name: 'child', entries: [{ kind: 'update', body: 'private inbox detail' }] },
|
|
50
|
+
]),
|
|
51
|
+
},
|
|
52
|
+
{ role: 'user', content: formatCard('review-approval', {}, 'The human reviewed and approved private-review.md.') },
|
|
42
53
|
],
|
|
43
54
|
stats: {},
|
|
44
55
|
state: { isStreaming: false },
|
|
45
56
|
});
|
|
46
|
-
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|
|
|
57
|
+
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|private restart continuation|private context reminder|private inbox detail|private-review\.md/);
|
|
47
58
|
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: false });
|
|
48
59
|
assert.equal(internals.toolOutputExpanded, true);
|
|
49
60
|
const expanded = container.render(80).join('\n');
|
|
50
61
|
assert.match(expanded, /loaded INDEX\.md content/);
|
|
51
62
|
assert.match(expanded, /resume from this note/);
|
|
52
|
-
assert.doesNotMatch(expanded, /
|
|
63
|
+
assert.doesNotMatch(expanded, /private restart continuation|private context reminder/);
|
|
53
64
|
assert.match(expanded, /private inbox detail/, 'an expanded inbox card shows the actual delivered update');
|
|
54
65
|
assert.match(expanded, /private-review\.md/, 'an expanded review card shows the approval');
|
|
55
66
|
assert.deepEqual(view.toggleToolsExpanded(), { kind: 'summaries', shown: true });
|
|
56
67
|
assert.equal(internals.toolOutputExpanded, false);
|
|
57
|
-
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|
|
|
68
|
+
assert.doesNotMatch(container.render(80).join('\n'), /loaded INDEX\.md content|resume from this note|private restart continuation|private context reminder|private inbox detail/);
|
|
58
69
|
});
|
|
59
70
|
test('a human message that reads like runtime recovery is not folded as generated context', () => {
|
|
60
|
-
assert.equal(
|
|
61
|
-
// The producer
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
assert.equal(
|
|
66
|
-
assert.equal(
|
|
71
|
+
assert.equal(parseCard({ role: 'user', content: 'continue' }), null, 'a human one-word continue is not mistaken for runtime recovery');
|
|
72
|
+
// The producer's envelope, not the prose, is what classifies a message. Strip
|
|
73
|
+
// the envelope off a real recovery body and it must read as a human message.
|
|
74
|
+
const guidance = formatModelFallbackRecovery('custom(provider)/model', 'new/model:high', 'not-found');
|
|
75
|
+
const recovery = formatCard('recovery', { reason: 'model-fallback' }, guidance);
|
|
76
|
+
assert.equal(parseCard({ role: 'user', content: recovery })?.kind, 'recovery');
|
|
77
|
+
assert.equal(parseCard({ role: 'user', content: guidance }), null, 'a human message matching the recovery prose lacks the envelope');
|
|
78
|
+
assert.equal(parseCard({ role: 'user', content: 'The human reviewed and approved private-review.md.' }), null, 'a human message matching the approval prose lacks the envelope');
|
|
79
|
+
});
|
|
80
|
+
test("a person's inbox answer renders as their own message, not a runtime card", async () => {
|
|
81
|
+
const container = new Container();
|
|
82
|
+
const view = chatView(container);
|
|
83
|
+
await view.applySnapshot({
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
role: 'user',
|
|
87
|
+
content: formatInboxCard([
|
|
88
|
+
{
|
|
89
|
+
id: 'human-ticket-node',
|
|
90
|
+
name: 'human interaction',
|
|
91
|
+
entries: [{ kind: 'final', disposition: 'human-answer', body: 'ship it on friday' }],
|
|
92
|
+
},
|
|
93
|
+
]),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
stats: {},
|
|
97
|
+
state: { isStreaming: false },
|
|
98
|
+
});
|
|
99
|
+
const rendered = container.render(80).join('\n');
|
|
100
|
+
assert.match(rendered, /ship it on friday/, "the person's words show without expanding anything");
|
|
101
|
+
assert.doesNotMatch(rendered, /crtr inbox|ctrl\+o/, 'an answer-only digest gets no card');
|
|
67
102
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, utimesSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { atomicWriteJson, pageManifestPath, pagePath } from '../../../core/human/convention.js';
|
|
7
|
+
import { derivePageManifestFromDocument } from '../../../core/human/page.js';
|
|
8
|
+
import { sanitize } from '../../inbox/tui/ansi.js';
|
|
9
|
+
import { PageBlockComponent } from '../render/page-block.js';
|
|
10
|
+
function proposal(intro) {
|
|
11
|
+
return `const DOCS = ['Local stack', 'Guest home', 'Data and dependencies', 'Troubleshooting'];
|
|
12
|
+
function Proposal({ docs }) {
|
|
13
|
+
return (
|
|
14
|
+
<Card>
|
|
15
|
+
<p>Move the <code>{docs.length}</code> operational areas behind focused pointers.</p>
|
|
16
|
+
<ul>{docs.map((doc) => <li key={doc}><strong>{doc}</strong> reference</li>)}</ul>
|
|
17
|
+
</Card>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
export default function DevMemorySplit() {
|
|
21
|
+
return (
|
|
22
|
+
<Page title="Northlight dev memory" subtitle="Choose the documentation shape">
|
|
23
|
+
<p>${intro}</p>
|
|
24
|
+
<Proposal docs={DOCS} />
|
|
25
|
+
<UserQuestion id="decision" label="Approve the split?" body="The current file is 4,189 words." mode="single" options={[{ id: 'approve', label: 'Approve' }, { id: 'keep', label: 'Keep one file' }]} />
|
|
26
|
+
</Page>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
test('an inline page renders dynamic display JSX beside its manifest question and refreshes source-only revisions', () => {
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-page-block-'));
|
|
33
|
+
const source = proposal('The current guide mixes routing and operational detail.');
|
|
34
|
+
const manifest = derivePageManifestFromDocument(source);
|
|
35
|
+
writeFileSync(pagePath(dir), source);
|
|
36
|
+
atomicWriteJson(pageManifestPath(dir), manifest);
|
|
37
|
+
const block = new PageBlockComponent('dev-memory', dir, () => 'open it');
|
|
38
|
+
const rendered = sanitize(block.render(88).join('\n'));
|
|
39
|
+
assert.match(rendered, /The current guide mixes routing and operational detail\./);
|
|
40
|
+
assert.match(rendered, /Move the 4 operational areas behind focused pointers\./);
|
|
41
|
+
assert.match(rendered, /Local stack.*reference/);
|
|
42
|
+
assert.match(rendered, /Troubleshooting.*reference/);
|
|
43
|
+
assert.match(rendered, /Approve the split\?/);
|
|
44
|
+
assert.match(rendered, /The current file is 4,189 words\./);
|
|
45
|
+
assert.match(rendered, /Keep one file/);
|
|
46
|
+
const revised = proposal('The replacement keeps the intent router short.');
|
|
47
|
+
writeFileSync(pagePath(dir), revised);
|
|
48
|
+
const future = new Date(Date.now() + 2_000);
|
|
49
|
+
utimesSync(pagePath(dir), future, future);
|
|
50
|
+
assert.equal(block.refresh(), true);
|
|
51
|
+
const refreshed = sanitize(block.render(88).join('\n'));
|
|
52
|
+
assert.match(refreshed, /The replacement keeps the intent router short\./);
|
|
53
|
+
assert.doesNotMatch(refreshed, /mixes routing and operational detail/);
|
|
54
|
+
});
|
|
@@ -245,6 +245,9 @@ export declare class ChatView {
|
|
|
245
245
|
private appendAssistantSeparator;
|
|
246
246
|
private appendAssistantPart;
|
|
247
247
|
private appendUserPart;
|
|
248
|
+
/** An answer the person typed in their inbox renders exactly like a message
|
|
249
|
+
* they typed here, and closes the run around it the same way. */
|
|
250
|
+
private appendHumanAnswer;
|
|
248
251
|
/** Only the full-fidelity trailing region gets summary groups. Condensed
|
|
249
252
|
* history intentionally drops tool traffic, so grouping there would create
|
|
250
253
|
* phantom summary rows and cross the live/condensed boundary. */
|
|
@@ -23,11 +23,11 @@
|
|
|
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 } from './context-message.js';
|
|
26
|
+
import { ContextMessageComponent, contextCardBody, expandsWithTools } from './context-message.js';
|
|
27
27
|
import { detectInlinePageResult, PageBlockComponent } from './page-block.js';
|
|
28
28
|
import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashToolDefinition, } from './crtr-output.js';
|
|
29
29
|
import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE, endedByAbort } from '../../../core/runtime/session-cycles.js';
|
|
30
|
-
import {
|
|
30
|
+
import { parseCard } from '../../../shared/generated-context.js';
|
|
31
31
|
import { assistantVisibleText, isTrueUserMessage, } from '../../../shared/tool-groups.js';
|
|
32
32
|
import { transformDiagramFences } from './diagram.js';
|
|
33
33
|
import { styleAttachAgentMarkdown, styleAttachMarkdownSource, styleAttachMessageMarkdown, styleAttachSummaryMarkdown, } from './markdown-source.js';
|
|
@@ -167,6 +167,9 @@ function stripBackground(line) {
|
|
|
167
167
|
return kept.length > 0 ? `\x1b[${kept.join(';')}m` : '';
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
|
+
/** Sender sections of an inbox card body; `parseCard` already read their entries.
|
|
171
|
+
* Whatever remains is other content riding the same digest. */
|
|
172
|
+
const INBOX_SENDER_SECTIONS = /<from\b[^>]*>[\s\S]*?<\/from>/g;
|
|
170
173
|
/** A tool component that can fold itself down to its CALL. pi's `ToolExecutionComponent`
|
|
171
174
|
* only knows expanded/collapsed, so each crtr-owned definition receives a local fold
|
|
172
175
|
* controller and captures the compact call component it renders. Folded render paints
|
|
@@ -1024,33 +1027,49 @@ export class ChatView {
|
|
|
1024
1027
|
addMessageToChat(message) {
|
|
1025
1028
|
// Crouter-generated context retains its runtime role (user vs custom) so
|
|
1026
1029
|
// delivery to the model is unchanged, but every attached view presents it
|
|
1027
|
-
// through one compact disclosure contract. The
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
1030
|
-
const
|
|
1031
|
-
if (
|
|
1030
|
+
// through one compact disclosure contract. The producer's envelope is what
|
|
1031
|
+
// classifies, so an ordinary user prompt cannot acquire this treatment and
|
|
1032
|
+
// saved transcripts gain it too.
|
|
1033
|
+
const card = parseCard(message);
|
|
1034
|
+
if (card !== null &&
|
|
1032
1035
|
(message.role !== 'custom' || message.display === true)) {
|
|
1033
|
-
this.pushTranscriptText(
|
|
1034
|
-
|
|
1035
|
-
//
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
//
|
|
1050
|
-
//
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1036
|
+
this.pushTranscriptText(card.body);
|
|
1037
|
+
// A person's inbox answer is the person speaking: their words leave the
|
|
1038
|
+
// card and render as their own message. A digest carrying nothing else
|
|
1039
|
+
// gets no card at all — but a passive backlog folded into the same body
|
|
1040
|
+
// is content, so the residue check keeps it from vanishing with the card.
|
|
1041
|
+
const answers = card.senders
|
|
1042
|
+
.flatMap((sender) => sender.entries)
|
|
1043
|
+
.filter((entry) => entry.disposition === 'human-answer')
|
|
1044
|
+
.map((entry) => entry.body.trim())
|
|
1045
|
+
.filter((text) => text !== '');
|
|
1046
|
+
const onlyHumanWords = answers.length > 0
|
|
1047
|
+
&& card.body.replace(INBOX_SENDER_SECTIONS, '').trim() === ''
|
|
1048
|
+
&& card.senders.every((sender) => sender.entries.every((entry) => entry.disposition === 'human-answer'));
|
|
1049
|
+
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') {
|
|
1055
|
+
this.closeOpenGroup();
|
|
1056
|
+
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
|
+
}
|
|
1063
|
+
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);
|
|
1069
|
+
}
|
|
1053
1070
|
}
|
|
1071
|
+
for (const answer of answers)
|
|
1072
|
+
this.appendHumanAnswer(answer);
|
|
1054
1073
|
return;
|
|
1055
1074
|
}
|
|
1056
1075
|
switch (message.role) {
|
|
@@ -1193,6 +1212,16 @@ export class ChatView {
|
|
|
1193
1212
|
else
|
|
1194
1213
|
this.append(component);
|
|
1195
1214
|
}
|
|
1215
|
+
/** An answer the person typed in their inbox renders exactly like a message
|
|
1216
|
+
* they typed here, and closes the run around it the same way. */
|
|
1217
|
+
appendHumanAnswer(text) {
|
|
1218
|
+
this.closeOpenGroup();
|
|
1219
|
+
if (this.chatChildCount() > 0)
|
|
1220
|
+
this.append(new Spacer(1));
|
|
1221
|
+
const component = new UserMessageComponent(text, attachMarkdownTheme(), undefined, this.markdownTransformers);
|
|
1222
|
+
this.append(component);
|
|
1223
|
+
this.registerCopyBlock(component, 'user', text);
|
|
1224
|
+
}
|
|
1196
1225
|
/** Only the full-fidelity trailing region gets summary groups. Condensed
|
|
1197
1226
|
* history intentionally drops tool traffic, so grouping there would create
|
|
1198
1227
|
* phantom summary rows and cross the live/condensed boundary. */
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type Component } from '@earendil-works/pi-tui';
|
|
2
|
+
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;
|
|
2
8
|
export declare class ContextMessageComponent implements Component {
|
|
3
9
|
private readonly dim;
|
|
4
10
|
private readonly label;
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// receives: collapsed → a padded dim line; expanded (Ctrl+O) → the ordinary
|
|
6
6
|
// user-message surface, regardless of the message's runtime role. Selected
|
|
7
7
|
// cards implement `setExpanded` for the global tool-display cycle; other cards
|
|
8
|
-
// remain folded while tool output is shown. Classification
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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.
|
|
11
11
|
import { UserMessageComponent } from '@earendil-works/pi-coding-agent';
|
|
12
12
|
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
13
13
|
import { attachMarkdownTheme } from '../config.js';
|
|
@@ -15,6 +15,24 @@ import { styleAttachMarkdownSource } from './markdown-source.js';
|
|
|
15
15
|
/** Default label/hint for the `crtr-context` bearings block. */
|
|
16
16
|
const LABEL = 'crtr context';
|
|
17
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
|
+
}
|
|
18
36
|
export class ContextMessageComponent {
|
|
19
37
|
dim;
|
|
20
38
|
label;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeneratedCard } from '../../../shared/generated-context.js';
|
|
1
2
|
export type FileActivity = {
|
|
2
3
|
key: string;
|
|
3
4
|
displayPath: string;
|
|
@@ -38,9 +39,10 @@ export declare class GroupActivityRecorder {
|
|
|
38
39
|
/** `roster` is the ChatView-wide node id → display name record: spawns fill
|
|
39
40
|
* it, and a later group's finish reads the name a sibling group recorded. */
|
|
40
41
|
constructor(cwd: string, roster?: Map<string, string>);
|
|
41
|
-
/** Record the nodes an inbox digest reports finished. The
|
|
42
|
-
*
|
|
43
|
-
|
|
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;
|
|
44
46
|
noteCall(id: string, name: string, args: unknown): void;
|
|
45
47
|
settle(id: string, result: unknown, isError: boolean): void;
|
|
46
48
|
resolveWithoutResult(id: string): void;
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// calls into the exact evidence already present in the session transcript.
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
-
import { inboxFinishedSenders } from '../../../shared/generated-context.js';
|
|
7
6
|
import { countDisplayDiffLines } from './edit-diff.js';
|
|
8
7
|
const EMPTY_ACTIVITY = Object.freeze({
|
|
9
8
|
files: Object.freeze([]),
|
|
@@ -162,11 +161,16 @@ export class GroupActivityRecorder {
|
|
|
162
161
|
this.cwd = cwd;
|
|
163
162
|
this.roster = roster;
|
|
164
163
|
}
|
|
165
|
-
/** Record the nodes an inbox digest reports finished. The
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 });
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
noteCall(id, name, args) {
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// result text, so `--replace` revisions and the answer that settles a page both
|
|
12
12
|
// land on the already-placed block when `refresh()` is driven by the viewer's
|
|
13
13
|
// inbox-activity watcher.
|
|
14
|
-
import { statSync } from 'node:fs';
|
|
14
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
15
|
+
import { projectPageDisplayMarkdown } from '../../../core/human/page-markdown.js';
|
|
15
16
|
import { parsePage } from '../../../core/human/page.js';
|
|
16
|
-
import { pageManifestPath, responsePath } from '../../../core/human/convention.js';
|
|
17
|
+
import { pageManifestPath, pagePath, responsePath } from '../../../core/human/convention.js';
|
|
17
18
|
import { pageTicketState, readTicketResult } from '../../../core/human/tickets.js';
|
|
18
19
|
import { renderMarkdownBlockAwareLines } from '../../../core/termrender/termrender.js';
|
|
19
20
|
import { describeSlot, tableMarkdown, terminalAnswerable } from '../../inbox/tui/slots.js';
|
|
@@ -101,7 +102,7 @@ export class PageBlockComponent {
|
|
|
101
102
|
this.cache = undefined;
|
|
102
103
|
}
|
|
103
104
|
diskStamp() {
|
|
104
|
-
return `${mtimeOf(pageManifestPath(this.dir))}|${mtimeOf(responsePath(this.dir))}`;
|
|
105
|
+
return `${mtimeOf(pageManifestPath(this.dir))}|${mtimeOf(pagePath(this.dir, 'jsx'))}|${mtimeOf(pagePath(this.dir, 'html'))}|${mtimeOf(responsePath(this.dir))}`;
|
|
105
106
|
}
|
|
106
107
|
reload() {
|
|
107
108
|
this.stamp = this.diskStamp();
|
|
@@ -109,10 +110,14 @@ export class PageBlockComponent {
|
|
|
109
110
|
this.cache = undefined;
|
|
110
111
|
try {
|
|
111
112
|
const manifest = parsePage(this.dir);
|
|
113
|
+
const displayMarkdown = manifest.dialect === 'jsx'
|
|
114
|
+
? projectPageDisplayMarkdown(readFileSync(pagePath(this.dir, 'jsx'), 'utf8'), manifest)
|
|
115
|
+
: '';
|
|
112
116
|
const state = pageTicketState(this.dir);
|
|
113
117
|
const digest = state === 'resolved' ? readTicketResult(this.dir) : null;
|
|
114
118
|
this.snapshot = {
|
|
115
119
|
manifest,
|
|
120
|
+
displayMarkdown,
|
|
116
121
|
state,
|
|
117
122
|
...(digest?.kind === 'page' ? { answerDigest: digest.summary.replace(/\s+/g, ' ').trim() } : {}),
|
|
118
123
|
};
|
|
@@ -134,7 +139,7 @@ export class PageBlockComponent {
|
|
|
134
139
|
for (const line of wrap(sanitize(snapshot.manifest.subtitle), maxW))
|
|
135
140
|
out.push(` ${DIM}${line}${RESET}`);
|
|
136
141
|
}
|
|
137
|
-
const body = this.bodyLines(snapshot.manifest, maxW, Math.max(20, width - 4));
|
|
142
|
+
const body = this.bodyLines(snapshot.manifest, snapshot.displayMarkdown, maxW, Math.max(20, width - 4));
|
|
138
143
|
if (body.length > 0) {
|
|
139
144
|
out.push('');
|
|
140
145
|
const shown = this.expanded || body.length <= FOLDED_BODY_LINES ? body : body.slice(0, FOLDED_BODY_LINES);
|
|
@@ -149,16 +154,11 @@ export class PageBlockComponent {
|
|
|
149
154
|
out.push('');
|
|
150
155
|
return out;
|
|
151
156
|
}
|
|
152
|
-
bodyLines(manifest, maxW, paneW) {
|
|
157
|
+
bodyLines(manifest, displayMarkdown, maxW, paneW) {
|
|
153
158
|
const md = (text) => renderMarkdownBlockAwareLines(text, maxW, paneW);
|
|
154
159
|
if (manifest.dialect === 'html')
|
|
155
160
|
return [`${DIM}HTML page — opens in a browser surface${RESET}`];
|
|
156
|
-
|
|
157
|
-
// not manifest slots, so a page built only from them keeps nothing here a
|
|
158
|
-
// terminal could draw. Say so rather than paint a title over an empty body.
|
|
159
|
-
if (manifest.slots.length === 0)
|
|
160
|
-
return [`${DIM}display-only page — its content renders in Northlight${RESET}`];
|
|
161
|
-
const lines = [];
|
|
161
|
+
const lines = displayMarkdown === '' ? [] : md(displayMarkdown);
|
|
162
162
|
let priorStep = -1;
|
|
163
163
|
for (const slot of manifest.slots) {
|
|
164
164
|
if (manifest.steps > 1 && slot.step !== priorStep) {
|