@nanobpm/nano-workforce 0.193.2 → 0.194.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 +6 -0
- package/app/agentic/cockpit/agent-history-render.test.ts +24 -0
- package/app/agentic/cockpit/agent-history-render.ts +18 -4
- package/app/agentic/cockpit/agent-history-view.test.ts +16 -0
- package/app/agentic/cockpit/agent-history-view.ts +17 -0
- package/app/agentic/cockpit/mount.test.ts +12 -0
- package/package.json +1 -1
- package/pages/cockpit/cockpit.css +17 -1
- package/pages/cockpit/mount.js +27 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.194.0](https://github.com/nanobpm/nano-workforce/compare/v0.193.2...v0.194.0) (2026-09-30)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **cockpit:** collapse agent-history turns behind a timestamped heading ([#832](https://github.com/nanobpm/nano-workforce/issues/832)) ([812907d](https://github.com/nanobpm/nano-workforce/commit/812907d13c49dc8a008bb8682d7d58f04c8ba323)), closes [#831](https://github.com/nanobpm/nano-workforce/issues/831)
|
|
6
|
+
|
|
1
7
|
## [0.193.2](https://github.com/nanobpm/nano-workforce/compare/v0.193.1...v0.193.2) (2026-09-29)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -63,6 +63,8 @@ test("renderAgentHistory renders turns with role, text, tool calls and per-turn
|
|
|
63
63
|
text: "on it",
|
|
64
64
|
toolCalls: [{ toolCallId: "t-1", toolName: "grep", elementId: "tool" }],
|
|
65
65
|
metrics: "12 in \u00b7 4 out \u00b7 900ms",
|
|
66
|
+
producedAt: "2024-05-01T12:34:56.789Z",
|
|
67
|
+
time: "12:34:56",
|
|
66
68
|
},
|
|
67
69
|
],
|
|
68
70
|
};
|
|
@@ -78,6 +80,28 @@ test("renderAgentHistory renders turns with role, text, tool calls and per-turn
|
|
|
78
80
|
assert.equal(host.byClass("cockpit-agent-turn-metrics")[0]?.text(), "12 in \u00b7 4 out \u00b7 900ms");
|
|
79
81
|
});
|
|
80
82
|
|
|
83
|
+
test("renderAgentHistory renders each turn collapsed: a <details> whose <summary> heading carries the timestamp (#831)", () => {
|
|
84
|
+
const host = new FakeElement("div");
|
|
85
|
+
renderAgentHistory(host, doc, {
|
|
86
|
+
agentInstanceKey: "ai-1",
|
|
87
|
+
count: 1,
|
|
88
|
+
turns: [{ historyItemKey: "h-1", loopIteration: 1, role: "ASSISTANT", text: "on it", toolCalls: [], producedAt: "2024-05-01T12:34:56.789Z", time: "12:34:56" }],
|
|
89
|
+
});
|
|
90
|
+
const turn = host.byClass("cockpit-agent-turn")[0];
|
|
91
|
+
assert.equal(turn?.tagName, "details");
|
|
92
|
+
// Collapsed by default: no `open` attribute — the native <summary> toggles it on click.
|
|
93
|
+
assert.equal(turn?.getAttribute("open"), undefined);
|
|
94
|
+
const heading = host.byClass("cockpit-agent-turn-meta")[0];
|
|
95
|
+
assert.equal(heading?.tagName, "summary");
|
|
96
|
+
const time = host.byClass("cockpit-agent-turn-time")[0];
|
|
97
|
+
assert.equal(time?.tagName, "time");
|
|
98
|
+
assert.equal(time?.text(), "12:34:56");
|
|
99
|
+
assert.equal(time?.getAttribute("datetime"), "2024-05-01T12:34:56.789Z");
|
|
100
|
+
// The details body sits inside the collapsible turn, not in the heading.
|
|
101
|
+
const body = host.byClass("cockpit-agent-turn-body")[0];
|
|
102
|
+
assert.equal(body?.byClass("cockpit-agent-turn-text")[0]?.text(), "on it");
|
|
103
|
+
});
|
|
104
|
+
|
|
81
105
|
test("renderAgentHistory renders an empty history state", () => {
|
|
82
106
|
const host = new FakeElement("div");
|
|
83
107
|
renderAgentHistory(host, doc, { agentInstanceKey: "ai-9", count: 0, turns: [] });
|
|
@@ -106,19 +106,32 @@ export function renderAgentSessions(
|
|
|
106
106
|
return { root };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* One turn as a native collapsible: a `<details>` (collapsed by default — no `open`) whose `<summary>`
|
|
111
|
+
* heading (role, iteration, timestamp, metrics) toggles the turn's details body on click (#831).
|
|
112
|
+
*/
|
|
109
113
|
function turnBlock(doc: DocumentLike, t: AgentTurnView): ElementLike {
|
|
110
|
-
const block = el(doc, "
|
|
114
|
+
const block = el(doc, "details", "cockpit-agent-turn");
|
|
111
115
|
block.setAttribute("data-history-item-key", t.historyItemKey);
|
|
112
116
|
block.setAttribute("data-role", t.role);
|
|
113
117
|
block.setAttribute("data-loop-iteration", String(t.loopIteration));
|
|
114
118
|
|
|
115
|
-
const meta = el(doc, "
|
|
119
|
+
const meta = el(doc, "summary", "cockpit-agent-turn-meta");
|
|
116
120
|
meta.appendChild(el(doc, "span", "cockpit-agent-turn-role", t.role));
|
|
117
121
|
meta.appendChild(el(doc, "span", "cockpit-agent-turn-iter", `#${t.loopIteration}`));
|
|
122
|
+
if (t.time !== undefined) {
|
|
123
|
+
const time = el(doc, "time", "cockpit-agent-turn-time", t.time);
|
|
124
|
+
if (t.producedAt !== undefined) {
|
|
125
|
+
time.setAttribute("datetime", t.producedAt);
|
|
126
|
+
time.setAttribute("title", t.producedAt);
|
|
127
|
+
}
|
|
128
|
+
meta.appendChild(time);
|
|
129
|
+
}
|
|
118
130
|
if (t.metrics !== undefined) meta.appendChild(el(doc, "span", "cockpit-agent-turn-metrics", t.metrics));
|
|
119
131
|
block.appendChild(meta);
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
const body = el(doc, "div", "cockpit-agent-turn-body");
|
|
134
|
+
if (t.text !== "") body.appendChild(el(doc, "pre", "cockpit-agent-turn-text", t.text));
|
|
122
135
|
|
|
123
136
|
if (t.toolCalls.length > 0) {
|
|
124
137
|
const tools = el(doc, "ul", "cockpit-agent-turn-tools");
|
|
@@ -127,8 +140,9 @@ function turnBlock(doc: DocumentLike, t: AgentTurnView): ElementLike {
|
|
|
127
140
|
li.setAttribute("data-tool-call-id", call.toolCallId);
|
|
128
141
|
tools.appendChild(li);
|
|
129
142
|
}
|
|
130
|
-
|
|
143
|
+
body.appendChild(tools);
|
|
131
144
|
}
|
|
145
|
+
block.appendChild(body);
|
|
132
146
|
return block;
|
|
133
147
|
}
|
|
134
148
|
|
|
@@ -96,6 +96,22 @@ test("agentHistoryView projects turns in transport order with text, tool calls a
|
|
|
96
96
|
assert.equal(view.turns[1]?.metrics, "12 in \u00b7 4 out \u00b7 900ms");
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("agentHistoryView carries each turn's producedAt timestamp (absent when unreported)", () => {
|
|
100
|
+
const view = agentHistoryView({
|
|
101
|
+
agentInstanceKey: "ai-1",
|
|
102
|
+
count: 2,
|
|
103
|
+
records: [
|
|
104
|
+
{ historyItemKey: "h-0", agentInstanceKey: "ai-1", loopIteration: 0, role: "USER", commitStatus: "COMMITTED", content: [], toolCalls: [], producedAt: "2024-05-01T12:34:56.789Z" },
|
|
105
|
+
{ historyItemKey: "h-1", agentInstanceKey: "ai-1", loopIteration: 1, role: "ASSISTANT", commitStatus: "COMMITTED", content: [], toolCalls: [] },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
assert.equal(view.turns[0]?.producedAt, "2024-05-01T12:34:56.789Z");
|
|
109
|
+
// UTC: the heading is locale-free, so the expected value is fixed regardless of the runner's timezone.
|
|
110
|
+
assert.equal(view.turns[0]?.time, "12:34:56");
|
|
111
|
+
assert.equal(view.turns[1]?.producedAt, undefined);
|
|
112
|
+
assert.equal(view.turns[1]?.time, undefined);
|
|
113
|
+
});
|
|
114
|
+
|
|
99
115
|
test("agentHistoryView read-as-absence: an empty history yields zero turns", () => {
|
|
100
116
|
const view = agentHistoryView({ agentInstanceKey: "ai-9", count: 0, records: [] });
|
|
101
117
|
assert.equal(view.count, 0);
|
|
@@ -63,6 +63,10 @@ export interface AgentTurnView {
|
|
|
63
63
|
readonly toolCalls: readonly AgentTurnToolCallView[];
|
|
64
64
|
/** A compact per-turn token/duration rollup, when metrics exist. */
|
|
65
65
|
readonly metrics?: string;
|
|
66
|
+
/** When the turn was produced (ISO-8601), when the engine reports it. */
|
|
67
|
+
readonly producedAt?: string;
|
|
68
|
+
/** The turn's compact heading timestamp (`HH:MM:SS`, UTC), when `producedAt` parses. */
|
|
69
|
+
readonly time?: string;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
/** The full renderable history for one agent instance: its rolled-up header + ordered turns. */
|
|
@@ -113,6 +117,16 @@ function turnMetrics(r: AgentHistoryRecord): string | undefined {
|
|
|
113
117
|
return dur !== undefined ? `${base} \u00b7 ${dur}` : base;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/** A compact `HH:MM:SS` heading timestamp in UTC for an ISO instant; undefined when unparseable. */
|
|
121
|
+
export function turnTime(iso: string | undefined): string | undefined {
|
|
122
|
+
if (iso === undefined || iso === "") return undefined;
|
|
123
|
+
const ms = Date.parse(iso);
|
|
124
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
125
|
+
const d = new Date(ms);
|
|
126
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
127
|
+
return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
/** The textual content of a turn: TEXT blocks joined in order (non-textual/empty blocks dropped). */
|
|
117
131
|
function turnText(r: AgentHistoryRecord): string {
|
|
118
132
|
const texts: string[] = [];
|
|
@@ -156,6 +170,7 @@ export function agentSessionsView(report: AgentInstanceListReport): AgentSession
|
|
|
156
170
|
/** Project one engine {@link AgentHistoryRecord} onto a renderable {@link AgentTurnView}. */
|
|
157
171
|
export function agentTurnView(r: AgentHistoryRecord): AgentTurnView {
|
|
158
172
|
const metrics = turnMetrics(r);
|
|
173
|
+
const time = turnTime(r.producedAt);
|
|
159
174
|
return {
|
|
160
175
|
historyItemKey: r.historyItemKey,
|
|
161
176
|
loopIteration: r.loopIteration,
|
|
@@ -167,6 +182,8 @@ export function agentTurnView(r: AgentHistoryRecord): AgentTurnView {
|
|
|
167
182
|
...(c.elementId !== undefined && c.elementId !== "" ? { elementId: c.elementId } : {}),
|
|
168
183
|
})),
|
|
169
184
|
...(metrics !== undefined ? { metrics } : {}),
|
|
185
|
+
...(r.producedAt !== undefined && r.producedAt !== "" ? { producedAt: r.producedAt } : {}),
|
|
186
|
+
...(time !== undefined ? { time } : {}),
|
|
170
187
|
};
|
|
171
188
|
}
|
|
172
189
|
|
|
@@ -303,6 +303,7 @@ test("agent-history panel renders the engine run list and a selected run's turns
|
|
|
303
303
|
content: [{ contentType: "TEXT", text: "did the thing" }],
|
|
304
304
|
toolCalls: [{ toolCallId: "t-1", toolName: "grep", elementId: "tool", arguments: {} }],
|
|
305
305
|
metrics: { inputTokens: 12, outputTokens: 4, reasoningTokenCount: 0, cacheCreationTokenCount: 0, cacheReadTokenCount: 0, durationMs: 900 },
|
|
306
|
+
producedAt: "2024-05-01T12:34:56.789Z",
|
|
306
307
|
},
|
|
307
308
|
],
|
|
308
309
|
};
|
|
@@ -334,6 +335,17 @@ test("agent-history panel renders the engine run list and a selected run's turns
|
|
|
334
335
|
assert(transcript != null, "the selected run's history rendered");
|
|
335
336
|
assert((transcript?.textContent ?? "").includes("did the thing"), "the turn text rendered");
|
|
336
337
|
assert((transcript?.textContent ?? "").includes("grep"), "the tool call rendered");
|
|
338
|
+
// #831 — each turn renders as a collapsed native <details> whose <summary> heading carries the
|
|
339
|
+
// UTC timestamp. Assert the browser twin mirrors the typed renderer so mount.js can't regress.
|
|
340
|
+
const turn = transcript?.querySelector(".cockpit-agent-turn");
|
|
341
|
+
assertEquals(turn?.tagName?.toLowerCase(), "details", "the turn is a <details>");
|
|
342
|
+
assertEquals(turn?.hasAttribute("open"), false, "collapsed by default: no open attribute");
|
|
343
|
+
const summary = turn?.querySelector(".cockpit-agent-turn-meta");
|
|
344
|
+
assertEquals(summary?.tagName?.toLowerCase(), "summary", "the heading is a <summary>");
|
|
345
|
+
const time = turn?.querySelector(".cockpit-agent-turn-time");
|
|
346
|
+
assertEquals(time?.tagName?.toLowerCase(), "time", "the timestamp is a <time>");
|
|
347
|
+
assertEquals(time?.textContent, "12:34:56", "the heading shows the UTC HH:MM:SS");
|
|
348
|
+
assertEquals(time?.getAttribute("datetime"), "2024-05-01T12:34:56.789Z", "the <time> carries the ISO instant");
|
|
337
349
|
} finally {
|
|
338
350
|
handle.dispose();
|
|
339
351
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.194.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -316,9 +316,24 @@
|
|
|
316
316
|
display: flex;
|
|
317
317
|
gap: 8px;
|
|
318
318
|
align-items: baseline;
|
|
319
|
-
|
|
319
|
+
cursor: pointer;
|
|
320
|
+
list-style: none;
|
|
321
|
+
user-select: none;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.cockpit-agent-turn-meta::-webkit-details-marker { display: none; }
|
|
325
|
+
|
|
326
|
+
.cockpit-agent-turn-meta::before {
|
|
327
|
+
content: "\25B8";
|
|
328
|
+
color: var(--cockpit-muted);
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
width: 10px;
|
|
320
331
|
}
|
|
321
332
|
|
|
333
|
+
.cockpit-agent-turn[open] > .cockpit-agent-turn-meta::before { content: "\25BE"; }
|
|
334
|
+
|
|
335
|
+
.cockpit-agent-turn-body { margin-top: 4px; padding-left: 18px; }
|
|
336
|
+
|
|
322
337
|
.cockpit-agent-turn-role {
|
|
323
338
|
font-size: 11px;
|
|
324
339
|
text-transform: uppercase;
|
|
@@ -327,6 +342,7 @@
|
|
|
327
342
|
}
|
|
328
343
|
|
|
329
344
|
.cockpit-agent-turn-iter,
|
|
345
|
+
.cockpit-agent-turn-time,
|
|
330
346
|
.cockpit-agent-turn-metrics {
|
|
331
347
|
font-size: 11px;
|
|
332
348
|
color: var(--cockpit-muted);
|
package/pages/cockpit/mount.js
CHANGED
|
@@ -574,6 +574,15 @@ function turnMetrics(m) {
|
|
|
574
574
|
return dur != null ? `${base} \u00b7 ${dur}` : base;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
function turnTime(iso) {
|
|
578
|
+
if (iso == null || iso === "") return undefined;
|
|
579
|
+
const ms = Date.parse(iso);
|
|
580
|
+
if (!Number.isFinite(ms)) return undefined;
|
|
581
|
+
const d = new Date(ms);
|
|
582
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
583
|
+
return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
577
586
|
function agentHistoryView(report) {
|
|
578
587
|
const turns = (report.records ?? []).map((r) => ({
|
|
579
588
|
historyItemKey: r.historyItemKey,
|
|
@@ -582,6 +591,8 @@ function agentHistoryView(report) {
|
|
|
582
591
|
text: turnText(r),
|
|
583
592
|
toolCalls: (r.toolCalls ?? []).map((c) => ({ toolCallId: c.toolCallId, toolName: c.toolName, elementId: c.elementId })),
|
|
584
593
|
metrics: turnMetrics(r.metrics),
|
|
594
|
+
producedAt: r.producedAt != null && r.producedAt !== "" ? r.producedAt : undefined,
|
|
595
|
+
time: turnTime(r.producedAt),
|
|
585
596
|
}));
|
|
586
597
|
return {
|
|
587
598
|
agentInstanceKey: report.agentInstanceKey,
|
|
@@ -639,17 +650,28 @@ function renderAgentSessions(host, doc, view, onSelect, activeInstanceKey) {
|
|
|
639
650
|
host.appendChild(root);
|
|
640
651
|
}
|
|
641
652
|
|
|
653
|
+
// One turn as a native collapsible <details> (collapsed by default); its <summary> heading (role,
|
|
654
|
+
// iteration, timestamp, metrics) toggles the details body on click (#831).
|
|
642
655
|
function agentTurnBlock(doc, t) {
|
|
643
|
-
const block = el(doc, "
|
|
656
|
+
const block = el(doc, "details", "cockpit-agent-turn");
|
|
644
657
|
block.setAttribute("data-history-item-key", t.historyItemKey);
|
|
645
658
|
block.setAttribute("data-role", t.role);
|
|
646
659
|
block.setAttribute("data-loop-iteration", String(t.loopIteration));
|
|
647
|
-
const meta = el(doc, "
|
|
660
|
+
const meta = el(doc, "summary", "cockpit-agent-turn-meta");
|
|
648
661
|
meta.appendChild(el(doc, "span", "cockpit-agent-turn-role", t.role));
|
|
649
662
|
meta.appendChild(el(doc, "span", "cockpit-agent-turn-iter", `#${t.loopIteration}`));
|
|
663
|
+
if (t.time != null) {
|
|
664
|
+
const time = el(doc, "time", "cockpit-agent-turn-time", t.time);
|
|
665
|
+
if (t.producedAt != null) {
|
|
666
|
+
time.setAttribute("datetime", t.producedAt);
|
|
667
|
+
time.setAttribute("title", t.producedAt);
|
|
668
|
+
}
|
|
669
|
+
meta.appendChild(time);
|
|
670
|
+
}
|
|
650
671
|
if (t.metrics != null) meta.appendChild(el(doc, "span", "cockpit-agent-turn-metrics", t.metrics));
|
|
651
672
|
block.appendChild(meta);
|
|
652
|
-
|
|
673
|
+
const body = el(doc, "div", "cockpit-agent-turn-body");
|
|
674
|
+
if (t.text !== "") body.appendChild(el(doc, "pre", "cockpit-agent-turn-text", t.text));
|
|
653
675
|
if (t.toolCalls.length > 0) {
|
|
654
676
|
const tools = el(doc, "ul", "cockpit-agent-turn-tools");
|
|
655
677
|
for (const call of t.toolCalls) {
|
|
@@ -657,8 +679,9 @@ function agentTurnBlock(doc, t) {
|
|
|
657
679
|
li.setAttribute("data-tool-call-id", call.toolCallId);
|
|
658
680
|
tools.appendChild(li);
|
|
659
681
|
}
|
|
660
|
-
|
|
682
|
+
body.appendChild(tools);
|
|
661
683
|
}
|
|
684
|
+
block.appendChild(body);
|
|
662
685
|
return block;
|
|
663
686
|
}
|
|
664
687
|
|