@nanobpm/nano-workforce 0.185.0 → 0.186.0
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/CHANGELOG.md +12 -0
- package/app/agentic/cockpit/cockpit-browser-bundle.test.ts +14 -5
- package/app/agentic/cockpit/transcript-derive.display.test.ts +231 -0
- package/app/agentic/cockpit/transcript-derive.ts +297 -63
- package/app/agentic/transcript-display.ts +17 -0
- package/app/readiness.test.ts +51 -0
- package/app/readiness.ts +55 -12
- package/package.json +2 -2
- package/pages/cockpit/generated/transcript-derive.js +213 -50
- package/pages/cockpit/generated/transcript-display.js +316 -0
- package/pages/cockpit/generated/transcript-events.js +35 -1
- package/pages/cockpit/mount.js +83 -29
- package/scripts/build-cockpit-browser.ts +11 -1
|
@@ -75,6 +75,12 @@ function num(body, key) {
|
|
|
75
75
|
const v = body[key];
|
|
76
76
|
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
77
77
|
}
|
|
78
|
+
/** Read a strictly-boolean field, or `undefined` when absent. A present-but-non-boolean value returns
|
|
79
|
+
* `undefined` too, so the caller can reject a malformed envelope rather than silently coercing it. */
|
|
80
|
+
function bool(body, key) {
|
|
81
|
+
const v = body[key];
|
|
82
|
+
return typeof v === "boolean" ? v : undefined;
|
|
83
|
+
}
|
|
78
84
|
const ROLES = ["assistant", "user", "system", "tool"];
|
|
79
85
|
/** Narrow an arbitrary string to a known {@link TranscriptRole}, defaulting to `assistant`. */
|
|
80
86
|
function toRole(value) {
|
|
@@ -123,7 +129,35 @@ export const CORE_TRANSCRIPT_VOCAB = Object.freeze(Object.assign(Object.create(n
|
|
|
123
129
|
if (text === undefined)
|
|
124
130
|
return undefined;
|
|
125
131
|
const roleRaw = str(body, "role");
|
|
126
|
-
|
|
132
|
+
// The additive display metadata (all optional). Mirror the permission decoder's discipline: a
|
|
133
|
+
// present-but-malformed optional field REJECTS the whole envelope (→ raw stream-chunk) rather than
|
|
134
|
+
// being silently dropped, so a decoded event never diverges from the on-wire JSON.
|
|
135
|
+
const messageId = str(body, "messageId");
|
|
136
|
+
if (body.messageId !== undefined && messageId === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
let mode;
|
|
139
|
+
if (body.mode !== undefined) {
|
|
140
|
+
const modeRaw = str(body, "mode");
|
|
141
|
+
if (modeRaw !== "delta" && modeRaw !== "snapshot")
|
|
142
|
+
return undefined;
|
|
143
|
+
mode = modeRaw;
|
|
144
|
+
}
|
|
145
|
+
const start = bool(body, "start");
|
|
146
|
+
if (body.start !== undefined && start === undefined)
|
|
147
|
+
return undefined;
|
|
148
|
+
const final = bool(body, "final");
|
|
149
|
+
if (body.final !== undefined && final === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
return {
|
|
152
|
+
kind: "message",
|
|
153
|
+
offset,
|
|
154
|
+
role: toRole(roleRaw),
|
|
155
|
+
text,
|
|
156
|
+
...(messageId !== undefined ? { messageId } : {}),
|
|
157
|
+
...(mode !== undefined ? { mode } : {}),
|
|
158
|
+
...(start !== undefined ? { start } : {}),
|
|
159
|
+
...(final !== undefined ? { final } : {}),
|
|
160
|
+
};
|
|
127
161
|
},
|
|
128
162
|
"tool-call": (body, offset) => {
|
|
129
163
|
const name = str(body, "name");
|
package/pages/cockpit/mount.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// (`app/agentic/cockpit/transcript-derive.ts` + `app/agentic/transcript-events.ts`) that a drift-guard
|
|
26
26
|
// test keeps byte-identical to its source.
|
|
27
27
|
import { RelayChannelClient, TerminalSession } from "@nanobpm/agentic/cockpit";
|
|
28
|
-
import {
|
|
28
|
+
import { createIncrementalTranscript } from "./generated/transcript-derive.js";
|
|
29
29
|
|
|
30
30
|
const DEFAULT_REFRESH_MS = 2000;
|
|
31
31
|
const DEFAULT_STALE_AFTER_MS = 15_000;
|
|
@@ -390,42 +390,96 @@ function renderTranscripts(host, doc, view, onReplay, activeStream, title = "Pas
|
|
|
390
390
|
host.appendChild(root);
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
// ── transcript rendering (#660)
|
|
393
|
+
// ── transcript rendering (#660, #757) ───────────────────────────────────────────────────────────
|
|
394
394
|
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
395
|
+
// An INCREMENTAL rendered-transcript sink over `host`: it feeds each relay `{offset, chunk}` into the
|
|
396
|
+
// shared {@link createIncrementalTranscript} projection — the SAME ordered-display fold the typed core
|
|
397
|
+
// uses (#757) — so the operator sees ONE growing text block per logical message with tool/diff/permission
|
|
398
|
+
// activity interleaved in chronological order, never one bordered card per streamed delta and never the
|
|
399
|
+
// raw `nwfTranscriptEvent` envelopes. The renderer patches only the ONE touched block's DOM node per
|
|
400
|
+
// chunk (append a new block, or grow/settle an existing one) instead of rebuilding the whole tree, so the
|
|
401
|
+
// operator's text selection and expansion survive live streaming.
|
|
402
|
+
//
|
|
403
|
+
// Redraws are BATCHED to an animation frame when the host provides one (bounded scheduling: many deltas
|
|
404
|
+
// arriving in one frame apply together, once), falling back to a synchronous flush where no rAF exists
|
|
405
|
+
// (Node/tests). Scroll is AUTO-FOLLOWED only when the operator is already at the tail — captured before
|
|
406
|
+
// each flush mutates the DOM — so reading back through history is not yanked to the bottom by new output.
|
|
407
|
+
// The derive/render logic is NOT re-copied here (that hand-copy was the #660 drift); this only schedules
|
|
408
|
+
// and collects chunks and hands them to the imported projection.
|
|
402
409
|
function transcriptSink(host, stream, opts = {}) {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
+
const doc = host.ownerDocument;
|
|
411
|
+
const win = doc?.defaultView ?? (typeof window !== "undefined" ? window : undefined);
|
|
412
|
+
const raf = win && typeof win.requestAnimationFrame === "function" ? win.requestAnimationFrame.bind(win) : undefined;
|
|
413
|
+
const caf = win && typeof win.cancelAnimationFrame === "function" ? win.cancelAnimationFrame.bind(win) : undefined;
|
|
414
|
+
|
|
415
|
+
let renderer = createIncrementalTranscript(host, doc, stream, opts);
|
|
416
|
+
const pending = []; // buffered {offset, chunk} awaiting the next flush
|
|
417
|
+
let gapPending = false; // a retention gap to open before the next flush's chunks
|
|
418
|
+
let frame = 0; // scheduled animation-frame handle (0 = none)
|
|
419
|
+
let disposed = false;
|
|
420
|
+
|
|
421
|
+
// Is the operator following the tail (so new output should auto-scroll)? Only meaningful with real
|
|
422
|
+
// layout; a host without numeric scroll metrics (the Node fake DOM) is treated as following.
|
|
423
|
+
function atTail() {
|
|
424
|
+
const sh = host.scrollHeight;
|
|
425
|
+
const st = host.scrollTop;
|
|
426
|
+
const ch = host.clientHeight;
|
|
427
|
+
if (typeof sh !== "number" || typeof st !== "number" || typeof ch !== "number") return true;
|
|
428
|
+
return st + ch >= sh - 4;
|
|
429
|
+
}
|
|
430
|
+
function followTail() {
|
|
431
|
+
if (typeof host.scrollHeight === "number") host.scrollTop = host.scrollHeight;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function flush() {
|
|
435
|
+
frame = 0;
|
|
436
|
+
if (disposed) return;
|
|
437
|
+
// Capture follow-state BEFORE mutating so only an operator already at the tail is auto-scrolled.
|
|
438
|
+
const follow = atTail();
|
|
439
|
+
if (gapPending) {
|
|
440
|
+
renderer.noteGap();
|
|
441
|
+
gapPending = false;
|
|
442
|
+
}
|
|
443
|
+
const batch = pending.splice(0, pending.length).sort((a, b) => a.offset - b.offset);
|
|
444
|
+
for (const entry of batch) renderer.applyChunk(entry);
|
|
445
|
+
if (follow) followTail();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function schedule() {
|
|
449
|
+
if (disposed || frame !== 0) return;
|
|
450
|
+
if (raf) frame = raf(flush);
|
|
451
|
+
else flush(); // no animation-frame scheduler (Node/tests) — flush synchronously so the DOM is current
|
|
452
|
+
}
|
|
453
|
+
|
|
410
454
|
return {
|
|
411
|
-
/** Fold in the resume ack
|
|
455
|
+
/** Fold in the resume ack: a gap flag opens a visible retention break before the following chunks. */
|
|
412
456
|
ack: (message) => {
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
draw();
|
|
457
|
+
if (message != null && message.gap === true) gapPending = true;
|
|
458
|
+
schedule();
|
|
416
459
|
},
|
|
417
|
-
/**
|
|
460
|
+
/** Buffer one relay data chunk (deduped by offset inside the idempotent projection) and schedule a flush. */
|
|
418
461
|
add: (offset, chunk) => {
|
|
419
|
-
|
|
420
|
-
|
|
462
|
+
pending.push({ offset, chunk });
|
|
463
|
+
schedule();
|
|
421
464
|
},
|
|
422
|
-
/** Render a whole fetched transcript page in one pass (static replay). */
|
|
465
|
+
/** Render a whole fetched transcript page in one pass (static replay) — rebuild from a fresh projection. */
|
|
423
466
|
replace: (data) => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
467
|
+
if (frame !== 0 && caf) caf(frame);
|
|
468
|
+
frame = 0;
|
|
469
|
+
pending.length = 0;
|
|
470
|
+
gapPending = false;
|
|
471
|
+
renderer = createIncrementalTranscript(host, doc, data.stream ?? stream, opts);
|
|
472
|
+
if (data.gap) renderer.noteGap();
|
|
473
|
+
const entries = [...(data.entries ?? [])].sort((a, b) => a.offset - b.offset);
|
|
474
|
+
for (const entry of entries) renderer.applyChunk(entry);
|
|
475
|
+
followTail();
|
|
476
|
+
},
|
|
477
|
+
/** Tear down the scheduler when the terminal region is claimed by another session (or disposed). */
|
|
478
|
+
dispose: () => {
|
|
479
|
+
disposed = true;
|
|
480
|
+
if (frame !== 0 && caf) caf(frame);
|
|
481
|
+
frame = 0;
|
|
482
|
+
pending.length = 0;
|
|
429
483
|
},
|
|
430
484
|
};
|
|
431
485
|
}
|
|
@@ -40,12 +40,22 @@ interface BundleModule {
|
|
|
40
40
|
// self-contained ESM (`dist/transcript/events.js` has no runtime imports), the SAME source of truth the
|
|
41
41
|
// Node core re-exports via the `transcript-events.ts` barrel. This keeps "one grammar, no drift surface"
|
|
42
42
|
// true across BOTH hosts: the cockpit renders from agentic's grammar, never a hand-rolled second copy.
|
|
43
|
+
// The ordered DISPLAY projection (agentic #566) is a THIRD browser-safe agentic module
|
|
44
|
+
// (`dist/transcript/display.js`, self-contained — its only import is `import type` from events.ts,
|
|
45
|
+
// erased on transpile). The cockpit's derive+render now folds through it (one growing block per message,
|
|
46
|
+
// chronological interleave), so it joins the bundle beside the events grammar. `transcript-derive.ts`
|
|
47
|
+
// imports the display fold from the `../transcript-display.ts` barrel, rewritten here to the generated
|
|
48
|
+
// display sibling — the SAME source of truth the Node core re-exports.
|
|
43
49
|
const MODULES: readonly BundleModule[] = [
|
|
44
50
|
{ src: "node_modules/@nanobpm/agentic/dist/transcript/events.js", out: "pages/cockpit/generated/transcript-events.js" },
|
|
51
|
+
{ src: "node_modules/@nanobpm/agentic/dist/transcript/display.js", out: "pages/cockpit/generated/transcript-display.js" },
|
|
45
52
|
{
|
|
46
53
|
src: "app/agentic/cockpit/transcript-derive.ts",
|
|
47
54
|
out: "pages/cockpit/generated/transcript-derive.js",
|
|
48
|
-
rewrites: [
|
|
55
|
+
rewrites: [
|
|
56
|
+
["../transcript-events.ts", "./transcript-events.js"],
|
|
57
|
+
["../transcript-display.ts", "./transcript-display.js"],
|
|
58
|
+
],
|
|
49
59
|
},
|
|
50
60
|
];
|
|
51
61
|
|