@unofficialbox/box-open-elements 0.19.0 → 0.20.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.
Files changed (79) hide show
  1. package/dist/components/collections/check-list.d.ts +10 -0
  2. package/dist/components/collections/check-list.js +8 -0
  3. package/dist/components/collections/document-list.d.ts +10 -0
  4. package/dist/components/collections/document-list.js +8 -0
  5. package/dist/components/collections/fact-list.d.ts +10 -0
  6. package/dist/components/collections/fact-list.js +8 -0
  7. package/dist/components/collections/result-blocks.d.ts +54 -0
  8. package/dist/components/collections/result-blocks.js +71 -0
  9. package/dist/components/feedback/progress-bar.d.ts +9 -0
  10. package/dist/components/feedback/progress-bar.js +29 -3
  11. package/dist/components/feedback/status-icon.d.ts +10 -0
  12. package/dist/components/feedback/status-icon.js +26 -0
  13. package/dist/element-maps.d.ts +39 -19
  14. package/dist/entries/agent-workspace.d.ts +1 -0
  15. package/dist/entries/agent-workspace.js +1 -0
  16. package/dist/entries/call-console.d.ts +1 -0
  17. package/dist/entries/call-console.js +1 -0
  18. package/dist/entries/check-list.d.ts +1 -0
  19. package/dist/entries/check-list.js +1 -0
  20. package/dist/entries/document-list.d.ts +1 -0
  21. package/dist/entries/document-list.js +1 -0
  22. package/dist/entries/fact-list.d.ts +1 -0
  23. package/dist/entries/fact-list.js +1 -0
  24. package/dist/entries/result-blocks.d.ts +1 -0
  25. package/dist/entries/result-blocks.js +1 -0
  26. package/dist/entries/run-summary.d.ts +1 -0
  27. package/dist/entries/run-summary.js +1 -0
  28. package/dist/entries/status-icon.d.ts +1 -0
  29. package/dist/entries/status-icon.js +1 -0
  30. package/dist/foundations/motion/behaviors.d.ts +8 -0
  31. package/dist/foundations/motion/behaviors.js +24 -0
  32. package/dist/foundations/motion/index.d.ts +2 -0
  33. package/dist/foundations/motion/index.js +2 -0
  34. package/dist/foundations/motion/scroll-pin.d.ts +19 -0
  35. package/dist/foundations/motion/scroll-pin.js +60 -0
  36. package/dist/foundations/motion/tokens.d.ts +2 -0
  37. package/dist/foundations/motion/tokens.js +2 -0
  38. package/dist/foundations/profiles/registry.d.ts +3 -0
  39. package/dist/foundations/profiles/registry.js +3 -0
  40. package/dist/foundations/profiles/types.d.ts +3 -0
  41. package/dist/foundations/status/index.d.ts +6 -0
  42. package/dist/foundations/status/index.js +33 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +10 -0
  45. package/dist/patterns/agent-chat/agent-chat.d.ts +9 -0
  46. package/dist/patterns/agent-chat/agent-chat.js +135 -21
  47. package/dist/patterns/agent-chat/controller.d.ts +3 -1
  48. package/dist/patterns/agent-chat/controller.js +92 -16
  49. package/dist/patterns/agent-chat/index.d.ts +1 -0
  50. package/dist/patterns/agent-chat/index.js +1 -0
  51. package/dist/patterns/agent-chat/ndjson.d.ts +5 -0
  52. package/dist/patterns/agent-chat/ndjson.js +41 -0
  53. package/dist/patterns/agent-chat/types.d.ts +44 -1
  54. package/dist/patterns/agent-workspace/agent-workspace.d.ts +28 -0
  55. package/dist/patterns/agent-workspace/agent-workspace.js +215 -0
  56. package/dist/patterns/agent-workspace/controller.d.ts +38 -0
  57. package/dist/patterns/agent-workspace/controller.js +65 -0
  58. package/dist/patterns/agent-workspace/index.d.ts +2 -0
  59. package/dist/patterns/agent-workspace/index.js +2 -0
  60. package/dist/patterns/call-console/call-console.d.ts +17 -0
  61. package/dist/patterns/call-console/call-console.js +107 -0
  62. package/dist/patterns/call-console/controller.d.ts +23 -0
  63. package/dist/patterns/call-console/controller.js +50 -0
  64. package/dist/patterns/call-console/http-transport.d.ts +3 -0
  65. package/dist/patterns/call-console/http-transport.js +26 -0
  66. package/dist/patterns/call-console/index.d.ts +4 -0
  67. package/dist/patterns/call-console/index.js +4 -0
  68. package/dist/patterns/call-console/types.d.ts +32 -0
  69. package/dist/patterns/call-console/types.js +8 -0
  70. package/dist/patterns/content-uploader/content-uploader.js +2 -1
  71. package/dist/patterns/run/index.d.ts +2 -0
  72. package/dist/patterns/run/index.js +2 -0
  73. package/dist/patterns/run/progress.d.ts +24 -0
  74. package/dist/patterns/run/progress.js +32 -0
  75. package/dist/patterns/run/run-summary.d.ts +21 -0
  76. package/dist/patterns/run/run-summary.js +82 -0
  77. package/dist/patterns/run/run-trace.d.ts +1 -0
  78. package/dist/patterns/run/run-trace.js +14 -3
  79. package/package.json +13 -1
@@ -0,0 +1,65 @@
1
+ export const conversationTitle = (body, max = 48) => {
2
+ const text = body.trim().replace(/\s+/g, " ");
3
+ if (text.length <= max)
4
+ return text || "New chat";
5
+ const cut = text.slice(0, max);
6
+ const space = cut.lastIndexOf(" ");
7
+ return `${space > 0 ? cut.slice(0, space) : cut}…`;
8
+ };
9
+ export function conversationDetails(state) {
10
+ const sources = new Map();
11
+ const approvals = [];
12
+ let context;
13
+ for (const message of state.messages) {
14
+ if (message.context)
15
+ context = message.context;
16
+ for (const citation of message.citations)
17
+ if (!sources.has(citation.id))
18
+ sources.set(citation.id, citation);
19
+ for (const block of message.blocks ?? [])
20
+ if (block.type === "documents")
21
+ for (const item of block.items) {
22
+ if (!sources.has(item.id))
23
+ sources.set(item.id, { id: item.id, label: item.name, ...(item.href ? { href: item.href } : {}) });
24
+ }
25
+ for (const proposal of message.proposals)
26
+ approvals.push({ messageId: message.id, proposal });
27
+ }
28
+ return { ...(context ? { context } : {}), approvals, sources: [...sources.values()] };
29
+ }
30
+ /** Owns independent sessions. Selecting a chat never disconnects another one. */
31
+ export class AgentWorkspaceController {
32
+ createSession;
33
+ chats = [];
34
+ activeId = null;
35
+ seq = 0;
36
+ listeners = new Set();
37
+ subscriptions = [];
38
+ constructor(createSession) {
39
+ this.createSession = createSession;
40
+ }
41
+ subscribe(listener) { this.listeners.add(listener); return () => this.listeners.delete(listener); }
42
+ emit() { this.listeners.forEach(fn => fn()); }
43
+ newChat() {
44
+ let chat = this.chats.find(c => c.controller.getState().messages.length === 0);
45
+ if (!chat) {
46
+ chat = { id: `chat-${++this.seq}`, controller: this.createSession() };
47
+ chat.controller.connect();
48
+ this.chats.push(chat);
49
+ this.subscriptions.push(chat.controller.subscribe("messagesChanged", () => this.emit()), chat.controller.subscribe("streamingChanged", () => this.emit()));
50
+ }
51
+ this.select(chat.id);
52
+ return chat;
53
+ }
54
+ select(id) { if (!this.chats.some(c => c.id === id))
55
+ return; this.activeId = id; this.emit(); }
56
+ get summaries() {
57
+ return this.chats.map(c => {
58
+ const state = c.controller.getState();
59
+ return { id: c.id, title: conversationTitle(state.messages.find(m => m.role === "user")?.body ?? ""),
60
+ status: state.messages.some(m => m.proposals.some(p => !p.decision)) ? "Needs your approval" : state.streaming ? "Working…" : "" };
61
+ });
62
+ }
63
+ get details() { const chat = this.chats.find(c => c.id === this.activeId); return chat ? conversationDetails(chat.controller.getState()) : { approvals: [], sources: [] }; }
64
+ destroy() { this.subscriptions.forEach(fn => fn()); this.subscriptions = []; this.chats.forEach(c => { c.controller.disconnect(); c.controller.destroy(); }); this.listeners.clear(); }
65
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./controller.js";
2
+ export * from "./agent-workspace.js";
@@ -0,0 +1,2 @@
1
+ export * from "./controller.js";
2
+ export * from "./agent-workspace.js";
@@ -0,0 +1,17 @@
1
+ import { BaseElement } from "../../core/element.js";
2
+ import type { CallConsoleController } from "./controller.js";
3
+ export declare function copyCallText(text: string, doc?: Document): Promise<boolean>;
4
+ export declare class CallConsole extends BaseElement {
5
+ static readonly tagName = "box-call-console";
6
+ private model;
7
+ private unsubscribe?;
8
+ private selected;
9
+ get callController(): CallConsoleController | null;
10
+ set callController(value: CallConsoleController | null);
11
+ private listen;
12
+ connectedCallback(): void;
13
+ disconnectedCallback(): void;
14
+ protected renderTemplate(): void;
15
+ protected setupListeners(): void;
16
+ protected update(): void;
17
+ }
@@ -0,0 +1,107 @@
1
+ import { BaseElement } from "../../core/element.js";
2
+ import { SplitView } from "../../components/layout/split-view.js";
3
+ import { boeEntrance, boeEntranceKeyframes, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
4
+ import { callFailed, callStatus, formatCallHttp } from "./types.js";
5
+ const DEFAULT_TAG_NAME = "box-call-console";
6
+ export async function copyCallText(text, doc = document) {
7
+ try {
8
+ await doc.defaultView?.navigator.clipboard.writeText(text);
9
+ if (doc.defaultView?.navigator.clipboard)
10
+ return true;
11
+ }
12
+ catch { }
13
+ const active = doc.activeElement;
14
+ const textarea = doc.createElement("textarea");
15
+ textarea.value = text;
16
+ textarea.style.cssText = "position:fixed;opacity:0";
17
+ doc.body.append(textarea);
18
+ textarea.select();
19
+ try {
20
+ return doc.execCommand?.("copy") ?? false;
21
+ }
22
+ finally {
23
+ textarea.remove();
24
+ active?.focus({ preventScroll: true });
25
+ }
26
+ }
27
+ export class CallConsole extends BaseElement {
28
+ static tagName = DEFAULT_TAG_NAME;
29
+ model = null;
30
+ unsubscribe;
31
+ selected = "";
32
+ get callController() { return this.model; }
33
+ set callController(value) { this.unsubscribe?.(); this.model = value; if (this.isConnected)
34
+ this.listen(); if (this.isRendered)
35
+ this.update(); }
36
+ listen() { this.unsubscribe?.(); this.unsubscribe = this.model?.subscribe(() => this.update()); }
37
+ connectedCallback() { super.connectedCallback(); this.listen(); }
38
+ disconnectedCallback() { this.unsubscribe?.(); }
39
+ renderTemplate() {
40
+ SplitView.register();
41
+ this.shadowRoot.innerHTML = `<style>:host{display:block;min-width:0;color:var(--boe-token-text-text,#222);font:inherit}:host([hidden]){display:none !important}
42
+ header{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center} pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;max-height:30rem;overflow:auto}
43
+ button,select{font:inherit;color:inherit;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:5px;padding:.4rem;cursor:pointer}
44
+ [part="call"]{width:100%;display:grid;grid-template-columns:5rem 5rem minmax(0,1fr) 4rem;text-align:left;gap:.5rem;margin:.4rem 0;${boeEntrance()}}
45
+ #list{container-type:inline-size} [part="call"] span{min-width:0;overflow-wrap:anywhere}
46
+ @container(max-width:420px){[part="call"]{grid-template-columns:minmax(0,1fr) auto}[part="call"] span:nth-child(even){text-align:right}}
47
+ [aria-current="true"]{background:var(--boe-token-surface-surface-secondary,#f5f5f5)} [part="skeleton"]{height:2rem;margin:.5rem;background:var(--boe-token-surface-surface-secondary,#eee)}
48
+ ${boeEntranceKeyframes}${boeReducedMotionPolicy}</style>
49
+ <header><span id="connection" role="status"></span><label>Service <select id="service"><option value="">All</option></select></label><label><input type="checkbox" id="errors"> Errors only</label><button id="clear">Clear</button><span id="copy-status" role="status"></span></header>
50
+ <box-split-view resizable ratio="0.4"><div slot="primary" id="list"></div><div><button data-copy="request">Copy request</button><button data-copy="response">Copy response</button><button data-copy="both">Copy both</button><h3>Request <button data-copy="request" aria-label="Copy request code">⧉</button></h3><pre id="request"></pre><h3>Response <button data-copy="response" aria-label="Copy response code">⧉</button></h3><pre id="response"></pre></div></box-split-view>`;
51
+ }
52
+ setupListeners() {
53
+ this.shadowRoot.querySelector("#service").addEventListener("change", () => this.update());
54
+ this.shadowRoot.querySelector("#errors").addEventListener("change", () => this.update());
55
+ this.shadowRoot.querySelector("#clear").addEventListener("click", () => { void this.model?.clear().catch(() => { this.shadowRoot.querySelector("#copy-status").textContent = "Unable to clear calls"; }); });
56
+ this.shadowRoot.querySelectorAll("[data-copy]").forEach(b => b.addEventListener("click", async () => {
57
+ const entry = this.model?.entries.find(e => e.id === this.selected);
58
+ if (!entry)
59
+ return;
60
+ const ok = await copyCallText(formatCallHttp(entry, b.dataset.copy));
61
+ this.shadowRoot.querySelector("#copy-status").textContent = ok ? "Copied" : "Copy unavailable; select the text to copy";
62
+ }));
63
+ }
64
+ update() {
65
+ const state = this.model?.connection ?? "connecting";
66
+ this.shadowRoot.querySelector("#connection").textContent = state === "unavailable" ? "Restart the agent: /calls is unavailable" : state;
67
+ const service = this.shadowRoot.querySelector("#service");
68
+ const services = [...new Set(this.model?.entries.map(e => e.service) ?? [])];
69
+ for (const name of services)
70
+ if (!Array.from(service.options).some(o => o.value === name)) {
71
+ const option = document.createElement("option");
72
+ option.value = name;
73
+ option.textContent = name;
74
+ service.append(option);
75
+ }
76
+ const errors = this.shadowRoot.querySelector("#errors").checked;
77
+ const entries = this.model?.entries.filter(e => (!service.value || e.service === service.value) && (!errors || callFailed(e))) ?? [];
78
+ if (!entries.some(e => e.id === this.selected))
79
+ this.selected = entries[0]?.id ?? "";
80
+ const list = this.shadowRoot.querySelector("#list");
81
+ const focused = this.shadowRoot.activeElement?.getAttribute("data-call-id");
82
+ list.querySelectorAll('[part="skeleton"]').forEach(el => el.remove());
83
+ const existing = new Map(Array.from(list.children).map(el => [el.getAttribute("data-call-id"), el]));
84
+ for (const entry of entries) {
85
+ let button = existing.get(entry.id);
86
+ existing.delete(entry.id);
87
+ if (!button) {
88
+ button = document.createElement("button");
89
+ button.setAttribute("part", "call");
90
+ button.dataset.callId = entry.id;
91
+ button.addEventListener("click", () => { this.selected = entry.id; this.update(); });
92
+ }
93
+ button.setAttribute("aria-current", String(entry.id === this.selected));
94
+ button.replaceChildren(...[callStatus(entry), entry.service, entry.summary, `${Math.round(entry.durationMs)} ms`].map(text => { const span = document.createElement("span"); span.textContent = text; return span; }));
95
+ list.append(button);
96
+ if (entry.id === focused)
97
+ button.focus({ preventScroll: true });
98
+ }
99
+ existing.forEach(el => el.remove());
100
+ if (!entries.length && state === "connecting")
101
+ list.innerHTML = '<div part="skeleton"></div><div part="skeleton"></div><div part="skeleton"></div>';
102
+ const entry = entries.find(e => e.id === this.selected);
103
+ this.shadowRoot.querySelector("#request").textContent = entry ? formatCallHttp(entry, "request") : "Select a call";
104
+ this.shadowRoot.querySelector("#response").textContent = entry ? formatCallHttp(entry, "response") : "";
105
+ }
106
+ }
107
+ CallConsole.register();
@@ -0,0 +1,23 @@
1
+ import type { CallConnection, CallEntry, CallEvent } from "./types.js";
2
+ export interface CallConsoleTransport {
3
+ snapshot(signal: AbortSignal): Promise<CallEntry[]>;
4
+ subscribe(onEvent: (event: CallEvent) => void, onConnection: (state: CallConnection) => void): () => void;
5
+ clear(): Promise<void>;
6
+ }
7
+ /** Transport injection keeps networking out of the component. */
8
+ export declare class CallConsoleController {
9
+ readonly transport: CallConsoleTransport;
10
+ entries: CallEntry[];
11
+ connection: CallConnection;
12
+ private listeners;
13
+ private abort?;
14
+ private close?;
15
+ constructor(transport: CallConsoleTransport);
16
+ subscribe(fn: () => void): () => void;
17
+ private emit;
18
+ apply(event: CallEvent): void;
19
+ connect(): Promise<void>;
20
+ clear(): Promise<void>;
21
+ disconnect(): void;
22
+ destroy(): void;
23
+ }
@@ -0,0 +1,50 @@
1
+ /** Transport injection keeps networking out of the component. */
2
+ export class CallConsoleController {
3
+ transport;
4
+ entries = [];
5
+ connection = "connecting";
6
+ listeners = new Set();
7
+ abort;
8
+ close;
9
+ constructor(transport) {
10
+ this.transport = transport;
11
+ }
12
+ subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
13
+ emit() { this.listeners.forEach(fn => fn()); }
14
+ apply(event) {
15
+ if (event.type === "snapshot")
16
+ this.entries = event.entries;
17
+ else if (event.type === "clear")
18
+ this.entries = [];
19
+ else
20
+ this.entries = this.entries.some(e => e.id === event.entry.id) ? this.entries.map(e => e.id === event.entry.id ? event.entry : e) : [event.entry, ...this.entries];
21
+ this.emit();
22
+ }
23
+ async connect() {
24
+ this.disconnect();
25
+ const abort = new AbortController();
26
+ this.abort = abort;
27
+ this.connection = "connecting";
28
+ this.emit();
29
+ try {
30
+ const entries = await this.transport.snapshot(abort.signal);
31
+ if (abort.signal.aborted)
32
+ return;
33
+ this.apply({ type: "snapshot", entries });
34
+ this.close = this.transport.subscribe(event => { if (!abort.signal.aborted)
35
+ this.apply(event); }, state => { if (!abort.signal.aborted) {
36
+ this.connection = state;
37
+ this.emit();
38
+ } });
39
+ }
40
+ catch (error) {
41
+ if (!abort.signal.aborted) {
42
+ this.connection = error instanceof Error && error.message === "404" ? "unavailable" : "reconnecting";
43
+ this.emit();
44
+ }
45
+ }
46
+ }
47
+ async clear() { await this.transport.clear(); this.apply({ type: "clear" }); }
48
+ disconnect() { this.abort?.abort(); this.close?.(); this.close = undefined; }
49
+ destroy() { this.disconnect(); this.listeners.clear(); }
50
+ }
@@ -0,0 +1,3 @@
1
+ import type { CallConsoleTransport } from "./controller.js";
2
+ /** Optional browser adapter for the documented snapshot/SSE/DELETE contract. */
3
+ export declare function createCallConsoleTransport(base?: string): CallConsoleTransport;
@@ -0,0 +1,26 @@
1
+ /** Optional browser adapter for the documented snapshot/SSE/DELETE contract. */
2
+ export function createCallConsoleTransport(base = "/calls") {
3
+ return {
4
+ async snapshot(signal) { const response = await fetch(base, { signal }); if (!response.ok)
5
+ throw new Error(String(response.status)); return await response.json(); },
6
+ subscribe(onEvent, onConnection) {
7
+ const events = new EventSource(`${base}/stream`);
8
+ events.onopen = () => onConnection("live");
9
+ events.onerror = () => onConnection("reconnecting");
10
+ for (const type of ["snapshot", "call", "clear"])
11
+ events.addEventListener(type, event => {
12
+ try {
13
+ const data = JSON.parse(event.data);
14
+ if (data.type === type)
15
+ onEvent(data);
16
+ }
17
+ catch {
18
+ onConnection("reconnecting");
19
+ }
20
+ });
21
+ return () => events.close();
22
+ },
23
+ async clear() { const response = await fetch(base, { method: "DELETE" }); if (!response.ok)
24
+ throw new Error(String(response.status)); },
25
+ };
26
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./controller.js";
3
+ export * from "./http-transport.js";
4
+ export * from "./call-console.js";
@@ -0,0 +1,4 @@
1
+ export * from "./types.js";
2
+ export * from "./controller.js";
3
+ export * from "./http-transport.js";
4
+ export * from "./call-console.js";
@@ -0,0 +1,32 @@
1
+ export interface CallEntry {
2
+ id: string;
3
+ startedAt: number;
4
+ durationMs: number;
5
+ pending: boolean;
6
+ service: string;
7
+ summary: string;
8
+ method: string;
9
+ url: string;
10
+ requestHeaders: Record<string, string>;
11
+ requestBody?: string;
12
+ status: number;
13
+ statusText: string;
14
+ responseHeaders: Record<string, string>;
15
+ responseBody?: string;
16
+ error?: string;
17
+ expected?: string;
18
+ rpcError?: string;
19
+ }
20
+ export type CallEvent = {
21
+ type: "snapshot";
22
+ entries: CallEntry[];
23
+ } | {
24
+ type: "call";
25
+ entry: CallEntry;
26
+ } | {
27
+ type: "clear";
28
+ };
29
+ export type CallConnection = "connecting" | "live" | "reconnecting" | "unavailable";
30
+ export declare const callFailed: (entry: CallEntry) => boolean;
31
+ export declare const callStatus: (entry: CallEntry) => string;
32
+ export declare const formatCallHttp: (entry: CallEntry, side?: "request" | "response" | "both") => string;
@@ -0,0 +1,8 @@
1
+ export const callFailed = (entry) => !entry.pending && Boolean(entry.error || entry.rpcError || (!entry.expected && (entry.status < 200 || entry.status >= 300)));
2
+ export const callStatus = (entry) => entry.pending ? "Pending" : entry.rpcError ? "Tool error" : entry.error ? "Failed" : entry.expected ? "Expected" : callFailed(entry) ? "Failed" : "Done";
3
+ export const formatCallHttp = (entry, side = "both") => {
4
+ const headers = (h) => Object.entries(h).map(([k, v]) => `${k}: ${v}`).join("\n");
5
+ const request = `${entry.method} ${entry.url}\n${headers(entry.requestHeaders)}\n\n${entry.requestBody ?? ""}`;
6
+ const response = `HTTP ${entry.status} ${entry.statusText}\n${headers(entry.responseHeaders)}\n\n${entry.responseBody ?? ""}${entry.rpcError || entry.error ? `\n${entry.rpcError ?? entry.error}` : ""}`;
7
+ return side === "request" ? request : side === "response" ? response : `${request}\n\n${response}`;
8
+ };
@@ -728,7 +728,8 @@ export class ContentUploader extends BaseElement {
728
728
  </div>
729
729
  <box-progress-bar
730
730
  part="row-progress"
731
- label="${escapeHtml(`${item.name} upload progress`)}"
731
+ hide-label
732
+ label="${escapeHtml(item.name)}"
732
733
  max="100"
733
734
  value="${String(Math.round(item.progress * 100))}"
734
735
  ></box-progress-bar>
@@ -1,2 +1,4 @@
1
1
  export * from "./types.js";
2
2
  export * from "./run-trace.js";
3
+ export * from "./progress.js";
4
+ export * from "./run-summary.js";
@@ -2,3 +2,5 @@
2
2
  export * from "./types.js";
3
3
  // Machine execution trace surface
4
4
  export * from "./run-trace.js";
5
+ export * from "./progress.js";
6
+ export * from "./run-summary.js";
@@ -0,0 +1,24 @@
1
+ import { type StatusKind } from "../../foundations/status/index.js";
2
+ import { type RunStep } from "./types.js";
3
+ export interface AgentTodo {
4
+ id: string;
5
+ content: string;
6
+ status: "pending" | "in_progress" | "completed" | "skipped";
7
+ }
8
+ export interface RunTurn {
9
+ steps: RunStep[];
10
+ todos: AgentTodo[];
11
+ startedAt: number;
12
+ endedAt?: number;
13
+ incomplete?: boolean;
14
+ }
15
+ export declare function formatElapsed(ms: number): string;
16
+ export declare function splitStepTitle(title: string): {
17
+ source?: string;
18
+ action: string;
19
+ };
20
+ export declare function progress(turn: RunTurn, failed?: boolean, now?: number, threshold?: number): {
21
+ kind: StatusKind;
22
+ label: string;
23
+ elapsed?: string;
24
+ };
@@ -0,0 +1,32 @@
1
+ import { toStatusKind } from "../../foundations/status/index.js";
2
+ import { resolveRunSteps } from "./types.js";
3
+ export function formatElapsed(ms) {
4
+ if (!Number.isFinite(ms) || ms < 0)
5
+ return "";
6
+ if (ms < 100)
7
+ return "<0.1 s";
8
+ if (ms < 10000)
9
+ return `${(ms / 1000).toFixed(1)} s`;
10
+ const seconds = Math.round(ms / 1000);
11
+ return seconds < 60 ? `${seconds} s` : `${Math.floor(seconds / 60)} min ${seconds % 60} s`;
12
+ }
13
+ export function splitStepTitle(title) {
14
+ const at = title.indexOf(" · ");
15
+ return at < 0 ? { action: title } : { source: title.slice(0, at), action: title.slice(at + 3) };
16
+ }
17
+ export function progress(turn, failed = false, now = Date.now(), threshold = 2000) {
18
+ const resolved = resolveRunSteps(turn.steps);
19
+ if (turn.endedAt === undefined) {
20
+ const step = [...resolved.steps].reverse().find(s => s.status === "running");
21
+ const label = turn.todos.find(t => t.status === "in_progress")?.content ?? (step ? splitStepTitle(step.step.title).action : "Thinking");
22
+ const elapsed = Math.max(0, now - turn.startedAt);
23
+ return { kind: "active", label: `${label}…`, ...(elapsed >= threshold ? { elapsed: `${Math.floor(elapsed / 1000)} s` } : {}) };
24
+ }
25
+ const took = formatElapsed(turn.endedAt - turn.startedAt);
26
+ if (failed || resolved.status === "failed")
27
+ return { kind: "failed", label: `Stopped after ${took}` };
28
+ if (turn.incomplete)
29
+ return { kind: "warning", label: `Cut short after ${took}` };
30
+ const warnings = resolved.steps.filter(s => toStatusKind(s.status) === "warning").length;
31
+ return { kind: warnings ? "warning" : "done", label: `Worked for ${took}${warnings ? ` · ${warnings} warning${warnings === 1 ? "" : "s"}` : ""}` };
32
+ }
@@ -0,0 +1,21 @@
1
+ import { BaseElement } from "../../core/element.js";
2
+ import { type RunTurn } from "./progress.js";
3
+ export declare class RunSummary extends BaseElement {
4
+ static readonly tagName = "box-run-summary";
5
+ static get observedAttributes(): string[];
6
+ private value;
7
+ private timer;
8
+ private planSignature;
9
+ private traceSignature;
10
+ get turn(): RunTurn;
11
+ set turn(turn: RunTurn);
12
+ get open(): boolean;
13
+ set open(value: boolean);
14
+ connectedCallback(): void;
15
+ disconnectedCallback(): void;
16
+ private syncTimer;
17
+ protected renderTemplate(): void;
18
+ protected setupListeners(): void;
19
+ private updateLabel;
20
+ protected update(): void;
21
+ }
@@ -0,0 +1,82 @@
1
+ import { BaseElement } from "../../core/element.js";
2
+ import { boeDisclosureStyles, boeEntranceKeyframes, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
3
+ import { StatusIcon } from "../../components/feedback/status-icon.js";
4
+ import { toStatusKind } from "../../foundations/status/index.js";
5
+ import { RunTrace } from "./run-trace.js";
6
+ import { progress } from "./progress.js";
7
+ const DEFAULT_TAG_NAME = "box-run-summary";
8
+ export class RunSummary extends BaseElement {
9
+ static tagName = DEFAULT_TAG_NAME;
10
+ static get observedAttributes() { return ["open", "failed", "elapsed-threshold"]; }
11
+ value = { steps: [], todos: [], startedAt: 0, endedAt: 0 };
12
+ timer;
13
+ planSignature = "";
14
+ traceSignature = "";
15
+ get turn() { return this.value; }
16
+ set turn(turn) { this.value = turn; if (this.isRendered)
17
+ this.update(); }
18
+ get open() { return this.hasAttribute("open"); }
19
+ set open(value) { this.toggleAttribute("open", value); }
20
+ connectedCallback() { super.connectedCallback(); this.syncTimer(); }
21
+ disconnectedCallback() { clearInterval(this.timer); this.timer = undefined; }
22
+ syncTimer() {
23
+ if (this.value.endedAt === undefined && this.isConnected && !this.timer)
24
+ this.timer = setInterval(() => this.updateLabel(), 1000);
25
+ if (this.value.endedAt !== undefined) {
26
+ clearInterval(this.timer);
27
+ this.timer = undefined;
28
+ }
29
+ }
30
+ renderTemplate() {
31
+ StatusIcon.register();
32
+ RunTrace.register();
33
+ this.shadowRoot.innerHTML = `<style>:host{display:block;font:inherit;color:var(--boe-token-text-text-secondary,#666)}:host([hidden]){display:none !important}
34
+ button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:.5rem 0;display:flex;gap:.5rem;align-items:center}
35
+ [part="clock"]{font-variant-numeric:tabular-nums} ul{padding:0;list-style:none} li{display:flex;gap:.5rem;margin:.5rem 0}
36
+ @keyframes boe-shimmer{50%{opacity:.6}} [data-running] [part="label"]{animation:boe-shimmer 1.4s ease-in-out infinite}
37
+ ${boeDisclosureStyles()}${boeEntranceKeyframes}${boeReducedMotionPolicy}</style>
38
+ <button part="trigger" aria-expanded="false" aria-controls="details"><box-status-icon></box-status-icon><span part="label" role="status"></span><span part="clock" aria-hidden="true"></span><span aria-hidden="true">›</span></button>
39
+ <div id="details" part="disclosure" class="boe-disclosure" inert><div><ul part="plan"></ul><box-run-trace></box-run-trace></div></div>`;
40
+ }
41
+ setupListeners() { this.shadowRoot.querySelector("button").addEventListener("click", () => { this.open = !this.open; }); }
42
+ updateLabel() {
43
+ const p = progress(this.value, this.hasAttribute("failed"), Date.now(), Number(this.getAttribute("elapsed-threshold") ?? 2000));
44
+ const label = this.shadowRoot.querySelector('[part="label"]');
45
+ if (label.textContent !== p.label)
46
+ label.textContent = p.label;
47
+ this.shadowRoot.querySelector('[part="clock"]').textContent = p.elapsed ?? "";
48
+ this.shadowRoot.querySelector("box-status-icon").kind = p.kind;
49
+ this.shadowRoot.querySelector("button").toggleAttribute("data-running", p.kind === "active");
50
+ this.shadowRoot.querySelector("button").setAttribute("aria-label", p.label);
51
+ }
52
+ update() {
53
+ if (!this.isRendered)
54
+ return;
55
+ this.updateLabel();
56
+ this.syncTimer();
57
+ this.shadowRoot.querySelector("button").setAttribute("aria-expanded", String(this.open));
58
+ const disclosure = this.shadowRoot.querySelector("#details");
59
+ disclosure.toggleAttribute("data-open", this.open);
60
+ disclosure.inert = !this.open;
61
+ disclosure.setAttribute("aria-hidden", String(!this.open));
62
+ const signature = JSON.stringify(this.value.todos);
63
+ if (signature !== this.planSignature) {
64
+ this.planSignature = signature;
65
+ const list = this.shadowRoot.querySelector("ul");
66
+ list.replaceChildren();
67
+ for (const todo of this.value.todos) {
68
+ const li = document.createElement("li");
69
+ const icon = new StatusIcon();
70
+ icon.kind = toStatusKind(todo.status);
71
+ li.append(icon, document.createTextNode(todo.content));
72
+ list.append(li);
73
+ }
74
+ }
75
+ const traceSignature = JSON.stringify(this.value.steps);
76
+ if (traceSignature !== this.traceSignature) {
77
+ this.traceSignature = traceSignature;
78
+ this.shadowRoot.querySelector("box-run-trace").steps = this.value.steps;
79
+ }
80
+ }
81
+ }
82
+ RunSummary.register();
@@ -25,6 +25,7 @@ export declare class RunTrace extends BaseElement {
25
25
  private stepsRaw;
26
26
  private stepsCache;
27
27
  private readonly expandedIds;
28
+ private readonly previousStatuses;
28
29
  get heading(): string;
29
30
  set heading(value: string);
30
31
  get steps(): RunStep[];
@@ -2,6 +2,8 @@ import { formatRunDuration, formatRunSummary, isRunStepRecord, resolveRunSteps,
2
2
  import { ProgressBar } from "../../components/feedback/progress-bar.js";
3
3
  import { BaseElement } from "../../core/index.js";
4
4
  import { boePanel } from "../../foundations/geometry/index.js";
5
+ import { boeStatusGlyph, boeStatusStyles, toStatusKind } from "../../foundations/status/index.js";
6
+ import { boeEntranceKeyframes, boeReducedMotionPolicy } from "../../foundations/motion/index.js";
5
7
  const DEFAULT_TAG_NAME = "box-run-trace";
6
8
  /** Escape a value for use inside a double-quoted attribute selector. */
7
9
  const cssAttrValue = (value) => value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
@@ -285,6 +287,7 @@ export class RunTrace extends BaseElement {
285
287
  stepsRaw = null;
286
288
  stepsCache = [];
287
289
  expandedIds = new Set();
290
+ previousStatuses = new Map();
288
291
  get heading() {
289
292
  return this.getAttribute("heading") ?? "Run";
290
293
  }
@@ -332,7 +335,11 @@ export class RunTrace extends BaseElement {
332
335
  if (!this.shadowRoot) {
333
336
  return;
334
337
  }
335
- this.shadowRoot.innerHTML = `<style>${elementStyles}</style><div part="host"></div>`;
338
+ this.shadowRoot.innerHTML = `<style>${elementStyles}
339
+ [part="step"] [part="marker"]{border:0;background:none;display:inline-flex}
340
+ ${boeEntranceKeyframes}${boeStatusStyles}
341
+ [data-settled] .boe-status[data-kind="done"], [data-settled] .boe-status[data-kind="done"] path { animation: none; }
342
+ ${boeReducedMotionPolicy}</style><div part="host"></div>`;
336
343
  this.hostEl = this.shadowRoot.querySelector('[part="host"]');
337
344
  }
338
345
  setupListeners() {
@@ -379,7 +386,7 @@ export class RunTrace extends BaseElement {
379
386
  <span part="child-label">${escapeHtml(child.label)}</span>
380
387
  ${clamped === null
381
388
  ? "<span></span>"
382
- : `<box-progress-bar label="${escapeHtml(child.label)} progress" value="${String(clamped)}"></box-progress-bar>`}
389
+ : `<box-progress-bar label="${escapeHtml(child.label)}" value="${String(clamped)}"></box-progress-bar>`}
383
390
  <span part="child-status">${RUN_STEP_STATUS_LABEL[status]}</span>
384
391
  </li>
385
392
  `;
@@ -395,6 +402,7 @@ export class RunTrace extends BaseElement {
395
402
  const rows = resolution.steps
396
403
  .map(entry => {
397
404
  const { step, status, position } = entry;
405
+ const settled = this.previousStatuses.get(step.id) === status;
398
406
  const duration = formatRunDuration(step.startedAt, step.finishedAt);
399
407
  // Slot-only detail counts: a host projecting logs for a step with no
400
408
  // JSON description still needs the toggle that reveals them.
@@ -412,7 +420,7 @@ export class RunTrace extends BaseElement {
412
420
  `;
413
421
  return `
414
422
  <li part="step" data-status="${status}" data-step-id="${escapeHtml(step.id)}" style="--run-tone:${STATUS_TONE[status]};">
415
- <span part="marker" aria-hidden="true"></span>
423
+ <span part="marker" aria-hidden="true" ${settled ? "data-settled" : ""}>${boeStatusGlyph(toStatusKind(status))}</span>
416
424
  <div part="step-body">
417
425
  <div part="topline">
418
426
  <span part="step-title">${escapeHtml(step.title)}</span>
@@ -430,6 +438,9 @@ export class RunTrace extends BaseElement {
430
438
  })
431
439
  .join("");
432
440
  const summary = formatRunSummary(resolution);
441
+ this.previousStatuses.clear();
442
+ for (const { step, status } of resolution.steps)
443
+ this.previousStatuses.set(step.id, status);
433
444
  this.hostEl.innerHTML = `
434
445
  <section part="panel" aria-label="${escapeHtml(this.heading)}">
435
446
  <div part="header">