@haikit/client 0.11.0 → 0.12.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/package.json +1 -1
- package/src/hai.css +20 -1
- package/src/index.d.ts +18 -1
- package/src/index.js +11 -0
- package/src/transcript.js +60 -3
package/package.json
CHANGED
package/src/hai.css
CHANGED
|
@@ -327,7 +327,26 @@
|
|
|
327
327
|
font: 12px var(--hai-mono);
|
|
328
328
|
color: var(--hai-dim);
|
|
329
329
|
}
|
|
330
|
-
.hai-tstatus {
|
|
330
|
+
.hai-tstatus {
|
|
331
|
+
max-width: 50%;
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
text-overflow: ellipsis;
|
|
334
|
+
color: var(--hai-dim);
|
|
335
|
+
white-space: nowrap;
|
|
336
|
+
font-variant-numeric: tabular-nums;
|
|
337
|
+
}
|
|
338
|
+
/* a running tool's progress: the message gives way before the count does */
|
|
339
|
+
.hai-tprogress { display: flex; min-width: 0; white-space: pre; }
|
|
340
|
+
.hai-tmessage { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
341
|
+
.hai-tcount { flex: none; }
|
|
342
|
+
/* …and a bar along the bottom edge of its row */
|
|
343
|
+
.hai-tool summary:has(.hai-tbar) { position: relative; }
|
|
344
|
+
.hai-tbar {
|
|
345
|
+
position: absolute;
|
|
346
|
+
inset: auto 0 0 0;
|
|
347
|
+
height: 2px;
|
|
348
|
+
background: linear-gradient(to right, var(--hai-accent) var(--hai-progress, 0%), transparent 0);
|
|
349
|
+
}
|
|
331
350
|
.hai-chevron { width: 14px; height: 14px; color: var(--hai-dim); transition: transform .15s; }
|
|
332
351
|
.hai-tool[open] .hai-chevron { transform: rotate(90deg); }
|
|
333
352
|
|
package/src/index.d.ts
CHANGED
|
@@ -12,11 +12,28 @@ export type ConversationStatus = "idle" | "streaming" | "awaiting";
|
|
|
12
12
|
export type SurfaceMode = "display" | "elicit";
|
|
13
13
|
export type SurfaceState = "live" | "frozen";
|
|
14
14
|
|
|
15
|
+
/** How far a running tool has got, as its `ctx.progress` calls said. */
|
|
16
|
+
export interface Progress {
|
|
17
|
+
message?: string;
|
|
18
|
+
done?: number;
|
|
19
|
+
total?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/** Blocks the server emits, plus the three the client synthesises locally. */
|
|
16
23
|
export type Block =
|
|
17
24
|
| { kind: "user"; id: string; text: string }
|
|
18
25
|
| { kind: "assistant"; id: string; text: string }
|
|
19
|
-
| {
|
|
26
|
+
| {
|
|
27
|
+
kind: "tool";
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
input: unknown;
|
|
31
|
+
status: string;
|
|
32
|
+
ms?: number;
|
|
33
|
+
result?: string;
|
|
34
|
+
/** What the tool's `ctx.progress` calls have said so far, merged. */
|
|
35
|
+
progress?: Progress;
|
|
36
|
+
}
|
|
20
37
|
| { kind: "interaction"; id: string; handle: string; label: string }
|
|
21
38
|
/** synthesised from `ui_open` — the slot a surface mounts into */
|
|
22
39
|
| { kind: "ui"; id: string; handle: string; toolId: string }
|
package/src/index.js
CHANGED
|
@@ -190,6 +190,17 @@ export function createChat({ endpoint = "/hai", registry }) {
|
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// A frame carries only the fields its call named, so it is merged: a
|
|
194
|
+
// tool says what it is doing once, then counts.
|
|
195
|
+
case "progress": {
|
|
196
|
+
const block = state.blocks.find((b) => b.id === event.toolId);
|
|
197
|
+
if (block?.kind === "tool") {
|
|
198
|
+
const { type, toolId, ...fields } = event;
|
|
199
|
+
block.progress = { ...block.progress, ...fields };
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
case "ui_open":
|
|
194
205
|
state.surfaces.set(event.handle, {
|
|
195
206
|
handle: event.handle,
|
package/src/transcript.js
CHANGED
|
@@ -46,8 +46,13 @@ export function renderTranscript(root, chat) {
|
|
|
46
46
|
el = renderBlock(block, chat);
|
|
47
47
|
// an expanded tool row stays expanded when its status changes
|
|
48
48
|
if (before?.el.open) el.open = true;
|
|
49
|
+
} else if (block.progress !== before.progress) {
|
|
50
|
+
// Progress changes the row in place. Rebuilt for every frame, a row a
|
|
51
|
+
// keyboard user is on would take their focus up to ten times a second.
|
|
52
|
+
const parts = rowParts.get(el);
|
|
53
|
+
if (parts) showStatus(parts, block, toolStatus(block, chat));
|
|
49
54
|
}
|
|
50
|
-
shown.set(block.id, { el, look });
|
|
55
|
+
shown.set(block.id, { el, look, progress: block.progress });
|
|
51
56
|
return el;
|
|
52
57
|
});
|
|
53
58
|
follow.shown = shown;
|
|
@@ -77,7 +82,8 @@ export function closeTranscript(root) {
|
|
|
77
82
|
function appearance(block, chat) {
|
|
78
83
|
// a surface shows a skeleton until its props arrive, and then itself for good
|
|
79
84
|
if (block.kind === "ui") return chat.state.surfaces.get(block.handle)?.props ? "mounted" : "loading";
|
|
80
|
-
|
|
85
|
+
// progress is left out: it changes the row in place (see renderTranscript)
|
|
86
|
+
if (block.kind === "tool") return JSON.stringify({ ...block, progress: undefined, status: toolStatus(block, chat) });
|
|
81
87
|
return JSON.stringify(block);
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -116,6 +122,46 @@ const STATUS_TEXT = {
|
|
|
116
122
|
error: (ms) => `failed · ${ms} ms`,
|
|
117
123
|
};
|
|
118
124
|
|
|
125
|
+
/** "2/4", or "2" when the tool hasn't said how many in all. */
|
|
126
|
+
function progressCount({ done, total }) {
|
|
127
|
+
return total !== undefined ? `${done ?? 0}/${total}` : done !== undefined ? String(done) : "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A tool row's element → its status text and progress bar, which progress changes in place. */
|
|
131
|
+
const rowParts = new WeakMap();
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fill in what a tool row says of its call. While it runs, that is its
|
|
135
|
+
* progress once it has reported some: the message gives way first on a narrow
|
|
136
|
+
* row, so the count stays readable, and the bar fills once there is a total.
|
|
137
|
+
*/
|
|
138
|
+
function showStatus({ status: text, bar }, block, status) {
|
|
139
|
+
const progress = status === "running" ? block.progress : undefined;
|
|
140
|
+
const message = progress?.message ?? "";
|
|
141
|
+
const count = progress ? progressCount(progress) : "";
|
|
142
|
+
if (message || count) {
|
|
143
|
+
text.className = "hai-tstatus hai-tprogress";
|
|
144
|
+
const parts = [];
|
|
145
|
+
if (message) parts.push(h("span", "hai-tmessage", message));
|
|
146
|
+
if (message && count) parts.push(h("span", null, " · "));
|
|
147
|
+
if (count) parts.push(h("span", "hai-tcount", count));
|
|
148
|
+
text.replaceChildren(...parts);
|
|
149
|
+
} else {
|
|
150
|
+
text.className = "hai-tstatus";
|
|
151
|
+
text.textContent = (STATUS_TEXT[status] ?? ((ms) => `${ms} ms`))(block.ms);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!bar) return;
|
|
155
|
+
const total = progress?.total;
|
|
156
|
+
bar.hidden = !total;
|
|
157
|
+
if (!total) return;
|
|
158
|
+
const done = Math.min(progress.done ?? 0, total);
|
|
159
|
+
bar.setAttribute("aria-valuemax", String(total));
|
|
160
|
+
bar.setAttribute("aria-valuenow", String(done));
|
|
161
|
+
bar.setAttribute("aria-valuetext", [message, `${done} of ${total}`].filter(Boolean).join(" · "));
|
|
162
|
+
bar.style.setProperty("--hai-progress", `${(done / total) * 100}%`);
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
function statusMark(status) {
|
|
120
166
|
const mark = h("span", "hai-ticon");
|
|
121
167
|
if (status === "running") mark.append(h("span", "hai-spinner"));
|
|
@@ -283,13 +329,24 @@ function renderBlock(block, chat) {
|
|
|
283
329
|
const status = toolStatus(block, chat);
|
|
284
330
|
const el = h("details", `hai-block hai-tool hai-status-${status}`);
|
|
285
331
|
const summary = h("summary");
|
|
332
|
+
const parts = { status: h("span", "hai-tstatus"), bar: null };
|
|
286
333
|
summary.append(
|
|
287
334
|
statusMark(status),
|
|
288
335
|
h("span", "hai-tname", block.name),
|
|
289
336
|
h("span", "hai-targs", JSON.stringify(block.input)),
|
|
290
|
-
|
|
337
|
+
parts.status,
|
|
291
338
|
icon("chevron", "hai-icon hai-chevron"),
|
|
292
339
|
);
|
|
340
|
+
// a thin bar along the row's bottom edge, for progress with a total
|
|
341
|
+
if (status === "running") {
|
|
342
|
+
parts.bar = h("span", "hai-tbar");
|
|
343
|
+
parts.bar.setAttribute("role", "progressbar");
|
|
344
|
+
parts.bar.setAttribute("aria-label", `${block.name} progress`);
|
|
345
|
+
parts.bar.setAttribute("aria-valuemin", "0");
|
|
346
|
+
summary.append(parts.bar);
|
|
347
|
+
}
|
|
348
|
+
rowParts.set(el, parts);
|
|
349
|
+
showStatus(parts, block, status);
|
|
293
350
|
el.append(summary);
|
|
294
351
|
const detail = h("div", "hai-tool-detail");
|
|
295
352
|
detail.append(
|