@artooi/ag-ui-web-component 0.24.0 → 0.25.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 +118 -1
- package/README.md +55 -36
- package/dist/ag-ui-web-component.bundle.js +173 -43
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +9 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +10 -0
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/index.js +271 -42
- package/dist/index.js.map +2 -2
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +26 -2
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +2 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +10 -0
- package/src/core/ag_ui_chat.ts +115 -41
- package/src/core/run_index.ts +10 -0
- package/src/ui/checkpoint_menu.ts +32 -7
- package/src/ui/styles.ts +140 -10
- package/src/ui/tool_call_card.ts +41 -3
- package/src/ui/ui_strings.ts +3 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -1596,6 +1596,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
1596
1596
|
* {@link toggleCollapsed} and {@link toggleTheme}.
|
|
1597
1597
|
*/
|
|
1598
1598
|
openThreads(): void {
|
|
1599
|
+
// Two overlapping surfaces, so opening one dismisses the other. Clicking away
|
|
1600
|
+
// already covers the built-in buttons, but a host driving its own chrome
|
|
1601
|
+
// through these methods raises no pointer event — and the drawer would then
|
|
1602
|
+
// open *underneath* a popover still floating over it.
|
|
1603
|
+
this.#checkpoints.close();
|
|
1599
1604
|
void this.#refreshDrawer();
|
|
1600
1605
|
this.#drawer.open();
|
|
1601
1606
|
}
|
|
@@ -1608,10 +1613,31 @@ export class AgUiChat extends HTMLElement {
|
|
|
1608
1613
|
* an empty panel.
|
|
1609
1614
|
*/
|
|
1610
1615
|
openCheckpoints(): void {
|
|
1616
|
+
// The other half of the pair — see `openThreads`.
|
|
1617
|
+
this.#drawer.close();
|
|
1611
1618
|
void this.#refreshCheckpoints();
|
|
1612
1619
|
this.#checkpoints.open();
|
|
1613
1620
|
}
|
|
1614
1621
|
|
|
1622
|
+
/** Close the checkpoints panel, if it is open. */
|
|
1623
|
+
closeCheckpoints(): void {
|
|
1624
|
+
this.#checkpoints.close();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* Open the checkpoints panel, or close it if it is already open — what the
|
|
1629
|
+
* built-in ⭯ button does, because a control that opens a panel is read as the
|
|
1630
|
+
* control that also dismisses it. {@link openCheckpoints} stays open-only for a
|
|
1631
|
+
* host that means exactly that.
|
|
1632
|
+
*/
|
|
1633
|
+
toggleCheckpoints(): void {
|
|
1634
|
+
if (this.#checkpoints.open_) {
|
|
1635
|
+
this.#checkpoints.close();
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
this.openCheckpoints();
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1615
1641
|
/**
|
|
1616
1642
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
1617
1643
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -1933,8 +1959,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
1933
1959
|
const history = this.#headerButton("history", this.#strings.chatHistory, "☰");
|
|
1934
1960
|
history.addEventListener("click", () => this.openThreads());
|
|
1935
1961
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1962
|
+
// ↺ rather than ⭯: the same idea in a glyph that has a font behind it in
|
|
1963
|
+
// every browser. The obscure one rendered as an unreadable mark at 14px, and a
|
|
1964
|
+
// header control nobody can name is one nobody presses.
|
|
1965
|
+
const checkpoints = this.#headerButton("checkpoints", this.#strings.checkpoints, "↺");
|
|
1966
|
+
checkpoints.addEventListener("click", () => this.toggleCheckpoints());
|
|
1938
1967
|
|
|
1939
1968
|
const newChat = this.#headerButton("new", this.#strings.newChat, "✚");
|
|
1940
1969
|
newChat.addEventListener("click", () => this.newChat());
|
|
@@ -2077,6 +2106,25 @@ export class AgUiChat extends HTMLElement {
|
|
|
2077
2106
|
this.#checkpoints.element,
|
|
2078
2107
|
);
|
|
2079
2108
|
|
|
2109
|
+
// Clicking away dismisses the checkpoints popover. Escape already did, and the
|
|
2110
|
+
// drawer has a backdrop that swallows the click — this popover has neither, so
|
|
2111
|
+
// it could only be closed by answering it.
|
|
2112
|
+
//
|
|
2113
|
+
// `pointerdown`, and the header button excluded: pointerdown runs *before* the
|
|
2114
|
+
// button's own click, so closing here and toggling there would land back open.
|
|
2115
|
+
// Composed path rather than `target`, because the event is retargeted at the
|
|
2116
|
+
// shadow boundary and every one of these nodes is inside it.
|
|
2117
|
+
this.#chat.addEventListener("pointerdown", (event) => {
|
|
2118
|
+
if (!this.#checkpoints.open_) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
const path = event.composedPath();
|
|
2122
|
+
if (path.includes(this.#checkpoints.element) || path.includes(checkpoints)) {
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
this.#checkpoints.close();
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2080
2128
|
// What a collapsed widget shrinks to: a round floating button, or the slim
|
|
2081
2129
|
// edge rail under `placement="sidebar"` — one element, shaped by CSS.
|
|
2082
2130
|
// A sibling of the panel, so it survives the panel being hidden.
|
|
@@ -2590,56 +2638,82 @@ export class AgUiChat extends HTMLElement {
|
|
|
2590
2638
|
* Render an approval card per server-side-tool interrupt and collect the
|
|
2591
2639
|
* user's decisions (approve → run it, deny → decline it).
|
|
2592
2640
|
*
|
|
2641
|
+
* **One card per gated call, in that call's own tool card, all at once.** A run
|
|
2642
|
+
* can defer several calls, and the wire answers each independently — so the UI
|
|
2643
|
+
* has to let a person answer each independently, which means saying which is
|
|
2644
|
+
* which. The prompt cannot: it comes from the tool's `x-confirm` and is
|
|
2645
|
+
* identical for every call of that tool. The tool card can, by position, and it
|
|
2646
|
+
* is already showing the arguments. Asking them serially was the other half of
|
|
2647
|
+
* the problem: the second question only appeared once the first was answered,
|
|
2648
|
+
* so a person could neither compare them nor tell that more were coming.
|
|
2649
|
+
*
|
|
2650
|
+
* Each gated card is marked `deferred` for the wait. That is not cosmetic — at
|
|
2651
|
+
* `pending` it read "running…" while the stream was over and the server idle.
|
|
2652
|
+
*
|
|
2593
2653
|
* The run is suspended on these cards. A Stop while any is open aborts the
|
|
2594
2654
|
* shared {@link #confirmAbort} controller, resolving every still-open card as
|
|
2595
2655
|
* denied. An approved tool runs on the follow-up resume run and streams its
|
|
2596
|
-
* result into the same
|
|
2597
|
-
* result will ever arrive.
|
|
2656
|
+
* result into the same card (returned to `pending`, since it now really is
|
|
2657
|
+
* running); a denied one settles here, as no result will ever arrive.
|
|
2598
2658
|
*/
|
|
2599
2659
|
async #resolveInterrupts(
|
|
2600
2660
|
interrupts: readonly Interrupt[],
|
|
2601
2661
|
): Promise<Record<string, InterruptResponse>> {
|
|
2602
|
-
const responses: Record<string, InterruptResponse> = {};
|
|
2603
2662
|
// One controller covers the whole batch: a single Stop denies all of them.
|
|
2604
2663
|
this.#confirmAbort = new AbortController();
|
|
2605
2664
|
this.#hidePending();
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
//
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2665
|
+
const signal = this.#confirmAbort.signal;
|
|
2666
|
+
const answered = await Promise.all(
|
|
2667
|
+
interrupts.map(async (interrupt) => {
|
|
2668
|
+
const card =
|
|
2669
|
+
interrupt.toolCallId !== undefined
|
|
2670
|
+
? this.#toolCards.get(interrupt.toolCallId)
|
|
2671
|
+
: undefined;
|
|
2672
|
+
const request: ApprovalRequest = {};
|
|
2673
|
+
const phrase = confirmPhrase(interrupt) ?? interrupt.message;
|
|
2674
|
+
if (phrase !== undefined) {
|
|
2675
|
+
request.message = phrase;
|
|
2676
|
+
}
|
|
2677
|
+
const toolName = card?.element.getAttribute("data-tool-name");
|
|
2678
|
+
if (toolName !== null && toolName !== undefined) {
|
|
2679
|
+
request.toolName = toolName;
|
|
2680
|
+
}
|
|
2681
|
+
card?.mark(TOOL_CALL_STATUS.DEFERRED);
|
|
2682
|
+
// A host-supplied renderer takes full control of the approval UI. The
|
|
2683
|
+
// built-in card renders into the gated call's own card, falling back to
|
|
2684
|
+
// the answer group when the interrupt names no call we hold one for.
|
|
2685
|
+
const approved =
|
|
2686
|
+
this.approvalRenderer !== null
|
|
2687
|
+
? await this.approvalRenderer(request, { signal })
|
|
2688
|
+
: await requestApproval(card?.approvalSlot ?? this.#ensureGroup(), request, {
|
|
2689
|
+
signal,
|
|
2690
|
+
strings: this.#strings,
|
|
2691
|
+
});
|
|
2692
|
+
// Same annotation as the client-side confirmation gate. Without it the
|
|
2693
|
+
// two gates read differently for the same act: a locally-confirmed call
|
|
2694
|
+
// said who let it through and a server-gated one said nothing, which is
|
|
2695
|
+
// backwards, since the server-side gate is the one guarding the tools
|
|
2696
|
+
// that actually run on the backend.
|
|
2697
|
+
card?.recordDecision(approved ? "approved" : "declined");
|
|
2698
|
+
if (approved) {
|
|
2699
|
+
card?.mark(TOOL_CALL_STATUS.PENDING);
|
|
2700
|
+
} else {
|
|
2701
|
+
// No TOOL_CALL_RESULT will stream for a denied tool — settle its card
|
|
2702
|
+
// now rather than leaving it hanging until the onSettled sweep.
|
|
2703
|
+
card?.settle(TOOL_CALL_STATUS.DECLINED, this.#strings.declinedAction);
|
|
2704
|
+
}
|
|
2705
|
+
return { id: interrupt.id, approved };
|
|
2706
|
+
}),
|
|
2707
|
+
);
|
|
2708
|
+
this.#updateEmptyState();
|
|
2709
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
2642
2710
|
this.#confirmAbort = null;
|
|
2711
|
+
const responses: Record<string, InterruptResponse> = {};
|
|
2712
|
+
for (const { id, approved } of answered) {
|
|
2713
|
+
responses[id] = approved
|
|
2714
|
+
? { status: "resolved", payload: { approved: true } }
|
|
2715
|
+
: { status: "cancelled" };
|
|
2716
|
+
}
|
|
2643
2717
|
return responses;
|
|
2644
2718
|
}
|
|
2645
2719
|
|
package/src/core/run_index.ts
CHANGED
|
@@ -8,6 +8,16 @@ export interface RunRow {
|
|
|
8
8
|
readonly started_at: string | null;
|
|
9
9
|
/** Whether the run has a snapshot to seed from — see {@link RunIndex}. */
|
|
10
10
|
readonly continuable: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The run's first user message, one line, already truncated by the server —
|
|
13
|
+
* the only field in a row a person recognises a conversation by.
|
|
14
|
+
*
|
|
15
|
+
* Optional because a server predating the field does not send it, and
|
|
16
|
+
* `null` where the run holds no words to show (seeded from history alone, or
|
|
17
|
+
* opened with an image and no caption). A row without one falls back to the
|
|
18
|
+
* time plus a short id, which is what every row used to be.
|
|
19
|
+
*/
|
|
20
|
+
readonly preview?: string | null;
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
/** Live header source, read per request so rotated tokens / CSRF reach the server. */
|
|
@@ -161,18 +161,43 @@ export class CheckpointMenu {
|
|
|
161
161
|
row.className = "checkpoint-row";
|
|
162
162
|
row.setAttribute("part", "checkpoint-row");
|
|
163
163
|
|
|
164
|
+
const preview =
|
|
165
|
+
run.preview !== undefined && run.preview !== null && run.preview !== "" ? run.preview : null;
|
|
166
|
+
const time =
|
|
167
|
+
run.started_at === null
|
|
168
|
+
? null
|
|
169
|
+
: relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
|
|
170
|
+
|
|
164
171
|
const label = document.createElement("span");
|
|
165
172
|
label.className = "checkpoint-label";
|
|
166
173
|
label.setAttribute("part", "checkpoint-label");
|
|
167
|
-
//
|
|
168
|
-
// the id
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
? run.run_id
|
|
172
|
-
: relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
|
|
173
|
-
label.title = run.run_id;
|
|
174
|
+
// What the run was about, if the server says. Otherwise the time, and only
|
|
175
|
+
// then the id — a person recognises the first, reads the second, and
|
|
176
|
+
// recognises nothing at all in the third.
|
|
177
|
+
label.textContent = preview ?? time ?? run.run_id;
|
|
174
178
|
row.append(label);
|
|
175
179
|
|
|
180
|
+
if (preview !== null && time !== null) {
|
|
181
|
+
// Demoted to a chip: with words in the label the time is no longer what
|
|
182
|
+
// identifies the run, but it still orders it.
|
|
183
|
+
const when = document.createElement("span");
|
|
184
|
+
when.className = "checkpoint-time";
|
|
185
|
+
when.setAttribute("part", "checkpoint-time");
|
|
186
|
+
when.textContent = time;
|
|
187
|
+
row.append(when);
|
|
188
|
+
} else if (run.started_at !== null) {
|
|
189
|
+
// No words to show, so the id has to do the identifying: two runs a few
|
|
190
|
+
// seconds apart both read "just now", and picking between them is picking
|
|
191
|
+
// blind. Shown rather than left in a tooltip, since a hover is not an
|
|
192
|
+
// identity either — and eight characters beats a full id on the row.
|
|
193
|
+
const short = document.createElement("span");
|
|
194
|
+
short.className = "checkpoint-id";
|
|
195
|
+
short.setAttribute("part", "checkpoint-id");
|
|
196
|
+
short.textContent = run.run_id.slice(0, 8);
|
|
197
|
+
short.title = run.run_id;
|
|
198
|
+
row.append(short);
|
|
199
|
+
}
|
|
200
|
+
|
|
176
201
|
if (run.parent_run_id !== null) {
|
|
177
202
|
// Lineage, so a branch doesn't read as a duplicate of its parent.
|
|
178
203
|
const branch = document.createElement("span");
|
package/src/ui/styles.ts
CHANGED
|
@@ -973,6 +973,13 @@ export const STYLES = `
|
|
|
973
973
|
to { transform: rotate(360deg); }
|
|
974
974
|
}
|
|
975
975
|
|
|
976
|
+
/* Deferred: no spinner, because nothing is spinning. A steady accent dot, since
|
|
977
|
+
the state is waiting-on-you rather than an outcome. */
|
|
978
|
+
.tool-call[data-status="deferred"] .tool-call-icon {
|
|
979
|
+
border-radius: 50%;
|
|
980
|
+
background: var(--_accent);
|
|
981
|
+
}
|
|
982
|
+
|
|
976
983
|
/* Settled: a themeable glyph coloured by outcome. */
|
|
977
984
|
.tool-call[data-status="done"] .tool-call-icon::before {
|
|
978
985
|
content: var(--_tool-icon-done);
|
|
@@ -1015,6 +1022,10 @@ export const STYLES = `
|
|
|
1015
1022
|
color: var(--_muted);
|
|
1016
1023
|
}
|
|
1017
1024
|
|
|
1025
|
+
.tool-call[data-status="deferred"] .tool-call-status {
|
|
1026
|
+
color: var(--_accent);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1018
1029
|
.tool-call[data-status="done"] .tool-call-status {
|
|
1019
1030
|
color: var(--_success);
|
|
1020
1031
|
}
|
|
@@ -1108,12 +1119,41 @@ export const STYLES = `
|
|
|
1108
1119
|
|
|
1109
1120
|
/* A pending card has no result yet, and in the modes where the arguments are
|
|
1110
1121
|
hidden too there is nothing behind the toggle. Hide the control rather than
|
|
1111
|
-
offer one that expands onto nothing.
|
|
1122
|
+
offer one that expands onto nothing. A deferred card is the same, and its
|
|
1123
|
+
arguments are shown unconditionally by the rules below. */
|
|
1112
1124
|
.tool-call[data-status="pending"] .tool-call-toggle,
|
|
1125
|
+
.tool-call[data-status="deferred"] .tool-call-toggle,
|
|
1113
1126
|
:host([data-tool-display="inline"]) .tool-call[data-status="pending"] .tool-call-toggle {
|
|
1114
1127
|
display: none;
|
|
1115
1128
|
}
|
|
1116
1129
|
|
|
1130
|
+
/* The approval prompt for a gated call, rendered inside that call's own card.
|
|
1131
|
+
Empty on every card nobody is being asked about, so it collapses instead of
|
|
1132
|
+
adding a gap to each one. */
|
|
1133
|
+
.tool-call-approval:empty {
|
|
1134
|
+
display: none;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.tool-call-approval {
|
|
1138
|
+
margin-top: 8px;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/* A card that is asking a question shows what it is asking about, in every
|
|
1142
|
+
display mode. Three gated calls of one tool ask the same words, so the
|
|
1143
|
+
arguments are the only thing telling them apart, and a density setting must
|
|
1144
|
+
not be able to hide the answer to "which one is this". */
|
|
1145
|
+
:host([data-tool-display="minimal"]) .tool-call[data-status="deferred"] .tool-call-body {
|
|
1146
|
+
display: flex;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/* The arguments region only, never every section: the result region carries the
|
|
1150
|
+
hidden attribute until a result exists, and a display value here overrides it,
|
|
1151
|
+
framing an empty RESULT heading under the question. */
|
|
1152
|
+
:host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args,
|
|
1153
|
+
:host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args {
|
|
1154
|
+
display: flex;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1117
1157
|
.tool-call-toggle {
|
|
1118
1158
|
align-self: flex-start;
|
|
1119
1159
|
border: none;
|
|
@@ -1333,6 +1373,20 @@ export const STYLES = `
|
|
|
1333
1373
|
opacity: 0.6;
|
|
1334
1374
|
}
|
|
1335
1375
|
|
|
1376
|
+
/* The same trap the attachment tray carries a note about, two rules along: an
|
|
1377
|
+
author display beats the UA stylesheet's rule for the hidden property, so a
|
|
1378
|
+
button the element has explicitly hidden keeps laying out and painting. The
|
|
1379
|
+
clip is hidden until a host supplies an upload handler or an attachments URL,
|
|
1380
|
+
and without this it is a visible control that cannot do anything.
|
|
1381
|
+
|
|
1382
|
+
The mic needs no such rule, and the asymmetry is worth knowing before adding
|
|
1383
|
+
one: it is not hidden when unconfigured, it is never built. The voice wiring
|
|
1384
|
+
returns before constructing the button, leaving only an empty voice slot that
|
|
1385
|
+
is display: contents. A hidden-state rule for the mic would match nothing. */
|
|
1386
|
+
.attach-btn[hidden] {
|
|
1387
|
+
display: none;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1336
1390
|
/* Send closes the row on the right: a circle, the only filled control in the
|
|
1337
1391
|
composer, so "the thing that acts" reads at a glance. */
|
|
1338
1392
|
.send {
|
|
@@ -1942,47 +1996,123 @@ export const STYLES = `
|
|
|
1942
1996
|
opacity: 0.7;
|
|
1943
1997
|
}
|
|
1944
1998
|
|
|
1999
|
+
/* A row is a label and two buttons, and nothing about the row itself is
|
|
2000
|
+
pressable. It used to light up on hover, which is the affordance of something
|
|
2001
|
+
clickable and made the buttons look like decoration on a clickable strip. The
|
|
2002
|
+
resting surface groups the row instead, so hover can mean what it says: only
|
|
2003
|
+
the buttons respond to it. */
|
|
2004
|
+
/* A row is a label and two buttons, and nothing about the row itself is
|
|
2005
|
+
pressable. It used to light up on hover, which is the affordance of something
|
|
2006
|
+
clickable and made the buttons look like decoration on a clickable strip. The
|
|
2007
|
+
resting surface groups the row instead, so hover can mean what it says: only
|
|
2008
|
+
the buttons respond to it.
|
|
2009
|
+
|
|
2010
|
+
It wraps for the same reason the tool-call head does. Every child but the label
|
|
2011
|
+
is fixed-width, so in a narrow panel the label is the only thing that can give
|
|
2012
|
+
-- and a flex-basis of zero lets it give everything. Adding the run id was
|
|
2013
|
+
enough to crush "just now" to zero pixels: present, correct, and invisible.
|
|
2014
|
+
Wrapping puts the buttons on their own line instead. */
|
|
1945
2015
|
.checkpoint-row {
|
|
1946
2016
|
display: flex;
|
|
2017
|
+
flex-wrap: wrap;
|
|
1947
2018
|
align-items: center;
|
|
1948
2019
|
gap: 0.5rem;
|
|
1949
|
-
padding: 0.
|
|
2020
|
+
padding: 0.3125rem 0.4375rem;
|
|
1950
2021
|
border-radius: 0.375rem;
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
.checkpoint-row:hover {
|
|
1954
2022
|
background: var(--_hover);
|
|
1955
2023
|
}
|
|
1956
2024
|
|
|
2025
|
+
/* Grows into spare room, and refuses to shrink past the shortest thing it ever
|
|
2026
|
+
says. A time is short and bounded, so there is no case for eliding it. */
|
|
1957
2027
|
.checkpoint-label {
|
|
1958
|
-
flex: 1;
|
|
2028
|
+
flex: 1 1 auto;
|
|
2029
|
+
min-width: 7ch;
|
|
1959
2030
|
font-size: 0.8125rem;
|
|
1960
2031
|
white-space: nowrap;
|
|
1961
2032
|
overflow: hidden;
|
|
1962
2033
|
text-overflow: ellipsis;
|
|
1963
2034
|
}
|
|
1964
2035
|
|
|
2036
|
+
/* When the label holds the run's first message, the time moves here: still worth
|
|
2037
|
+
showing, no longer what identifies the row. Muted and unshrinkable, so it does
|
|
2038
|
+
not compete with the words beside it. */
|
|
2039
|
+
.checkpoint-time {
|
|
2040
|
+
flex: 0 0 auto;
|
|
2041
|
+
font-size: 0.6875rem;
|
|
2042
|
+
opacity: 0.7;
|
|
2043
|
+
white-space: nowrap;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/* Enough of the run id to tell two runs apart when both say "just now". Muted
|
|
2047
|
+
and monospaced: it is a reference, not a name. */
|
|
2048
|
+
.checkpoint-id {
|
|
2049
|
+
flex: 0 0 auto;
|
|
2050
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2051
|
+
font-size: 0.6875rem;
|
|
2052
|
+
opacity: 0.55;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/* On the panel's own surface, not the row's: the row now paints the hover token
|
|
2056
|
+
itself, and a badge the same colour as what it sits on is not a badge. */
|
|
1965
2057
|
.checkpoint-branch {
|
|
1966
2058
|
font-size: 0.6875rem;
|
|
1967
2059
|
padding: 0 0.375rem;
|
|
1968
2060
|
border-radius: 999px;
|
|
1969
|
-
background: var(--
|
|
2061
|
+
background: var(--_assistant-bg);
|
|
1970
2062
|
opacity: 0.8;
|
|
1971
2063
|
}
|
|
1972
2064
|
|
|
2065
|
+
/* The two things in the row that actually do something, so they are the two
|
|
2066
|
+
things that look like it: a filled surface at rest rather than a transparent
|
|
2067
|
+
outline, which on top of the old row highlight was nearly invisible. */
|
|
1973
2068
|
.checkpoint-action {
|
|
1974
2069
|
font: inherit;
|
|
1975
2070
|
font-size: 0.75rem;
|
|
2071
|
+
line-height: 1.4;
|
|
1976
2072
|
cursor: pointer;
|
|
1977
|
-
padding: 0.
|
|
2073
|
+
padding: 0.1875rem 0.5625rem;
|
|
1978
2074
|
border: 1px solid var(--_border);
|
|
1979
2075
|
border-radius: 0.375rem;
|
|
1980
|
-
background:
|
|
2076
|
+
background: var(--_bg);
|
|
1981
2077
|
color: inherit;
|
|
2078
|
+
transition:
|
|
2079
|
+
background var(--_motion) var(--_ease),
|
|
2080
|
+
border-color var(--_motion) var(--_ease),
|
|
2081
|
+
transform var(--_motion) var(--_ease);
|
|
1982
2082
|
}
|
|
1983
2083
|
|
|
1984
|
-
|
|
2084
|
+
/* Resume is what a reader wants nine times in ten; fork is the deliberate choice
|
|
2085
|
+
beside it. Filled and outlined, the same pair the confirmation and approval
|
|
2086
|
+
cards already use for their primary and secondary action. */
|
|
2087
|
+
.checkpoint-resume {
|
|
2088
|
+
font-weight: 600;
|
|
2089
|
+
border-color: var(--_accent);
|
|
2090
|
+
background: var(--_accent);
|
|
2091
|
+
color: #ffffff;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
.checkpoint-fork:hover {
|
|
1985
2095
|
background: var(--_hover);
|
|
2096
|
+
border-color: var(--_accent);
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
/* The filled one cannot go lighter on hover without losing its contrast with the
|
|
2100
|
+
white label, so it dims instead. */
|
|
2101
|
+
.checkpoint-resume:hover {
|
|
2102
|
+
opacity: 0.88;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/* Pressed: a pixel down, so the click is felt as well as seen. */
|
|
2106
|
+
.checkpoint-action:active {
|
|
2107
|
+
transform: translateY(1px);
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/* Keyboard focus was invisible here, in a panel that traps focus and is reached
|
|
2111
|
+
by Tab -- so the one navigation path guaranteed to land on these buttons was
|
|
2112
|
+
the one with nothing to show for it. */
|
|
2113
|
+
.checkpoint-action:focus-visible {
|
|
2114
|
+
outline: 2px solid var(--_accent);
|
|
2115
|
+
outline-offset: 2px;
|
|
1986
2116
|
}
|
|
1987
2117
|
|
|
1988
2118
|
.drawer-backdrop {
|
package/src/ui/tool_call_card.ts
CHANGED
|
@@ -7,13 +7,17 @@ export type ToolCallStatus = (typeof TOOL_CALL_STATUS)[keyof typeof TOOL_CALL_ST
|
|
|
7
7
|
/** How much detail a card renders. */
|
|
8
8
|
export type ToolDisplayMode = (typeof TOOL_DISPLAY)[keyof typeof TOOL_DISPLAY];
|
|
9
9
|
|
|
10
|
-
/** The
|
|
11
|
-
export type
|
|
10
|
+
/** The two states a card can sit in before an outcome exists. */
|
|
11
|
+
export type UnsettledStatus = typeof TOOL_CALL_STATUS.PENDING | typeof TOOL_CALL_STATUS.DEFERRED;
|
|
12
|
+
|
|
13
|
+
/** The terminal states a card settles into (everything unsettled excluded). */
|
|
14
|
+
export type SettledStatus = Exclude<ToolCallStatus, UnsettledStatus>;
|
|
12
15
|
|
|
13
16
|
/** Short pill text shown for each status, drawn from the string table. */
|
|
14
17
|
function statusLabels(strings: UiStrings): Record<ToolCallStatus, string> {
|
|
15
18
|
return {
|
|
16
19
|
[TOOL_CALL_STATUS.PENDING]: strings.toolRunning,
|
|
20
|
+
[TOOL_CALL_STATUS.DEFERRED]: strings.toolDeferred,
|
|
17
21
|
[TOOL_CALL_STATUS.DONE]: strings.toolDone,
|
|
18
22
|
[TOOL_CALL_STATUS.ERROR]: strings.toolError,
|
|
19
23
|
[TOOL_CALL_STATUS.DECLINED]: strings.toolDeclined,
|
|
@@ -64,6 +68,21 @@ export class ToolCallCard {
|
|
|
64
68
|
/** The card's root element; append this into the message list. */
|
|
65
69
|
readonly element: HTMLDivElement;
|
|
66
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Where a question about *this* call renders — the approval prompt for a
|
|
73
|
+
* server-side tool the run deferred.
|
|
74
|
+
*
|
|
75
|
+
* It belongs to the card rather than to the transcript because a run can defer
|
|
76
|
+
* several calls at once, and a prompt written per *tool* ("Add this event to
|
|
77
|
+
* the board?") is identical for every one of them. Rendered into the answer
|
|
78
|
+
* group they were three anonymous copies of one question, below the three
|
|
79
|
+
* cards they gated; rendered here, position identifies them and the arguments
|
|
80
|
+
* are already on screen above the question.
|
|
81
|
+
*
|
|
82
|
+
* Empty until used, and hidden while empty by the shadow CSS.
|
|
83
|
+
*/
|
|
84
|
+
readonly approvalSlot: HTMLDivElement;
|
|
85
|
+
|
|
67
86
|
readonly #status: HTMLSpanElement;
|
|
68
87
|
readonly #decision: HTMLSpanElement;
|
|
69
88
|
readonly #toggle: HTMLButtonElement;
|
|
@@ -143,7 +162,26 @@ export class ToolCallCard {
|
|
|
143
162
|
body.setAttribute("part", "tool-card-body");
|
|
144
163
|
body.append(argsSection.root, resultSection.root);
|
|
145
164
|
|
|
146
|
-
this.
|
|
165
|
+
this.approvalSlot = document.createElement("div");
|
|
166
|
+
this.approvalSlot.className = "tool-call-approval";
|
|
167
|
+
this.approvalSlot.setAttribute("part", "tool-card-approval");
|
|
168
|
+
|
|
169
|
+
this.element.append(head, this.#toggle, body, this.approvalSlot);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Move between the two states that are not an outcome — `pending` (running)
|
|
174
|
+
* and `deferred` (gated, waiting on a person).
|
|
175
|
+
*
|
|
176
|
+
* Ignored once {@link settle} has run: a card that was declined must not be
|
|
177
|
+
* talked back into looking live by a late event.
|
|
178
|
+
*/
|
|
179
|
+
mark(status: UnsettledStatus): void {
|
|
180
|
+
if (this.#settled) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.element.setAttribute("data-status", status);
|
|
184
|
+
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
147
185
|
}
|
|
148
186
|
|
|
149
187
|
/**
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -84,6 +84,8 @@ export interface UiStrings {
|
|
|
84
84
|
// ── Tool-call card ──────────────────────────────────────────────────────────
|
|
85
85
|
/** Status pill while the call runs. */
|
|
86
86
|
toolRunning: string;
|
|
87
|
+
/** Status pill on a gated call the run deferred, waiting on a person. */
|
|
88
|
+
toolDeferred: string;
|
|
87
89
|
/** Status pill on success. */
|
|
88
90
|
toolDone: string;
|
|
89
91
|
/** Status pill on error. */
|
|
@@ -244,6 +246,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
244
246
|
transcriptionFailed: "Transcription failed",
|
|
245
247
|
|
|
246
248
|
toolRunning: "running…",
|
|
249
|
+
toolDeferred: "waiting for you",
|
|
247
250
|
toolDone: "✓ done",
|
|
248
251
|
toolError: "⚠ error",
|
|
249
252
|
toolDeclined: "⊘ declined",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.25.0";
|