@artooi/ag-ui-web-component 0.9.0 → 0.11.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 +104 -7
- package/README.md +89 -7
- package/dist/ag-ui-web-component.bundle.js +168 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +39 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +28 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +5 -0
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +8 -0
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +8 -2
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +887 -128
- package/dist/index.js.map +4 -4
- package/dist/ui/approval_card.d.ts +51 -0
- package/dist/ui/approval_card.d.ts.map +1 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +7 -1
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/question_card.d.ts +52 -0
- package/dist/ui/question_card.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/skills_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/thoughts_block.d.ts +2 -2
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +16 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +9 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/ag_ui_chat.ts +251 -14
- package/src/core/agui_client.ts +95 -9
- package/src/core/attachment.ts +21 -1
- package/src/core/conversation_store.ts +84 -18
- package/src/core/remote_conversation_store.ts +24 -3
- package/src/core/upload_attachment.ts +8 -1
- package/src/index.ts +14 -0
- package/src/ui/approval_card.ts +119 -0
- package/src/ui/attachment_chips.ts +5 -0
- package/src/ui/attachment_tray.ts +50 -5
- package/src/ui/question_card.ts +216 -0
- package/src/ui/relative_time.ts +8 -3
- package/src/ui/skills_menu.ts +6 -0
- package/src/ui/styles.ts +130 -9
- package/src/ui/thoughts_block.ts +3 -2
- package/src/ui/thread_drawer.ts +94 -9
- package/src/ui/tool_call_card.ts +6 -0
- package/src/ui/ui_strings.ts +30 -0
- package/src/ui/voice_input.ts +21 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
2
|
+
|
|
3
|
+
/** What the inline question card asks (the `ask_user` frontend tool's args). */
|
|
4
|
+
export interface QuestionRequest {
|
|
5
|
+
/** The question shown to the user. */
|
|
6
|
+
question: string;
|
|
7
|
+
/**
|
|
8
|
+
* Preset choices rendered as radios. When empty/omitted the card is a plain
|
|
9
|
+
* free-text prompt.
|
|
10
|
+
*/
|
|
11
|
+
options?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Whether the user may type a custom answer. With `options`, adds an "other"
|
|
14
|
+
* radio revealing a text field; without options the card is free-text anyway.
|
|
15
|
+
*/
|
|
16
|
+
allowCustom?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Options for {@link requestQuestion}. */
|
|
20
|
+
export interface QuestionOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Aborting this signal resolves the card with an empty answer (fields
|
|
23
|
+
* disabled) — the hook a Stop control uses to dismiss an open question when
|
|
24
|
+
* the user cancels the whole run.
|
|
25
|
+
*/
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
/** Localized strings; defaults to the English {@link DEFAULT_UI_STRINGS}. */
|
|
28
|
+
strings?: UiStrings;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A fully custom renderer for the `ask_user` question, set via
|
|
33
|
+
* `AgUiChat.askUserRenderer`. Receives the parsed {@link QuestionRequest} and an
|
|
34
|
+
* `AbortSignal` that fires when the run is stopped, and resolves with the user's
|
|
35
|
+
* answer (an empty string signals "no answer", e.g. on abort). When provided it
|
|
36
|
+
* **replaces** the built-in {@link requestQuestion} card entirely — the host owns
|
|
37
|
+
* the DOM, so it can render a native modal, a framework component, or anything
|
|
38
|
+
* else. See the `strings` / `::part()` seams for styling the built-in card
|
|
39
|
+
* instead of replacing it.
|
|
40
|
+
*/
|
|
41
|
+
export type QuestionRenderer = (
|
|
42
|
+
request: QuestionRequest,
|
|
43
|
+
options: { signal: AbortSignal },
|
|
44
|
+
) => Promise<string>;
|
|
45
|
+
|
|
46
|
+
/** A single custom-answer text field. */
|
|
47
|
+
function answerInput(placeholder: string): HTMLInputElement {
|
|
48
|
+
const input = document.createElement("input");
|
|
49
|
+
input.type = "text";
|
|
50
|
+
input.className = "question-input";
|
|
51
|
+
input.setAttribute("part", "question-input");
|
|
52
|
+
input.placeholder = placeholder;
|
|
53
|
+
return input;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Append an inline **question** card to ``host`` and resolve with the user's
|
|
58
|
+
* answer — the browser half of the built-in `ask_user` frontend tool.
|
|
59
|
+
*
|
|
60
|
+
* Unlike the confirmation/approval cards (which resolve a yes/no), this collects
|
|
61
|
+
* a typed answer: a radio pick from ``options``, or free text (when
|
|
62
|
+
* ``allowCustom`` or no ``options`` are given). The card stays in the transcript
|
|
63
|
+
* as a resolved record (controls disabled, `data-resolved` set) rather than
|
|
64
|
+
* vanishing. A Stop while it is open resolves it with an empty string.
|
|
65
|
+
*/
|
|
66
|
+
export function requestQuestion(
|
|
67
|
+
host: Node & ParentNode,
|
|
68
|
+
request: QuestionRequest,
|
|
69
|
+
options: QuestionOptions = {},
|
|
70
|
+
): Promise<string> {
|
|
71
|
+
const strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
72
|
+
const choices = request.options ?? [];
|
|
73
|
+
const hasChoices = choices.length > 0;
|
|
74
|
+
// Free text is offered when there are no preset choices, or when custom
|
|
75
|
+
// answers are explicitly allowed alongside them (via an "other" radio).
|
|
76
|
+
const allowsText = !hasChoices || request.allowCustom === true;
|
|
77
|
+
|
|
78
|
+
return new Promise<string>((resolve) => {
|
|
79
|
+
const card = document.createElement("div");
|
|
80
|
+
card.className = "question";
|
|
81
|
+
card.setAttribute("part", "question");
|
|
82
|
+
card.setAttribute("role", "group");
|
|
83
|
+
card.setAttribute("aria-label", strings.askUserAction);
|
|
84
|
+
|
|
85
|
+
const body = document.createElement("div");
|
|
86
|
+
body.className = "question-body";
|
|
87
|
+
body.setAttribute("part", "question-body");
|
|
88
|
+
body.textContent = request.question;
|
|
89
|
+
|
|
90
|
+
const form = document.createElement("div");
|
|
91
|
+
form.className = "question-options";
|
|
92
|
+
form.setAttribute("part", "question-options");
|
|
93
|
+
|
|
94
|
+
// `name` scopes the radio group to this card so multiple open cards don't
|
|
95
|
+
// interfere; a per-card token keeps it unique without module state.
|
|
96
|
+
const group = `q-${choices.length}-${request.question.length}`;
|
|
97
|
+
const radios: HTMLInputElement[] = [];
|
|
98
|
+
for (const choice of choices) {
|
|
99
|
+
const label = document.createElement("label");
|
|
100
|
+
label.className = "question-choice";
|
|
101
|
+
label.setAttribute("part", "question-choice");
|
|
102
|
+
const radio = document.createElement("input");
|
|
103
|
+
radio.type = "radio";
|
|
104
|
+
radio.name = group;
|
|
105
|
+
radio.value = choice;
|
|
106
|
+
radio.setAttribute("part", "question-radio");
|
|
107
|
+
const text = document.createElement("span");
|
|
108
|
+
text.setAttribute("part", "question-choice-text");
|
|
109
|
+
text.textContent = choice;
|
|
110
|
+
label.append(radio, text);
|
|
111
|
+
form.appendChild(label);
|
|
112
|
+
radios.push(radio);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The "other" radio (only alongside choices) toggles the free-text field.
|
|
116
|
+
let otherRadio: HTMLInputElement | null = null;
|
|
117
|
+
let input: HTMLInputElement | null = null;
|
|
118
|
+
if (allowsText) {
|
|
119
|
+
input = answerInput(strings.answerPlaceholder);
|
|
120
|
+
if (hasChoices) {
|
|
121
|
+
const label = document.createElement("label");
|
|
122
|
+
label.className = "question-choice";
|
|
123
|
+
label.setAttribute("part", "question-choice");
|
|
124
|
+
otherRadio = document.createElement("input");
|
|
125
|
+
otherRadio.type = "radio";
|
|
126
|
+
otherRadio.name = group;
|
|
127
|
+
otherRadio.value = "";
|
|
128
|
+
otherRadio.setAttribute("part", "question-radio");
|
|
129
|
+
const text = document.createElement("span");
|
|
130
|
+
text.setAttribute("part", "question-choice-text");
|
|
131
|
+
text.textContent = strings.otherOption;
|
|
132
|
+
label.append(otherRadio, text);
|
|
133
|
+
form.appendChild(label);
|
|
134
|
+
input.disabled = true;
|
|
135
|
+
}
|
|
136
|
+
form.appendChild(input);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const actions = document.createElement("div");
|
|
140
|
+
actions.className = "question-actions";
|
|
141
|
+
actions.setAttribute("part", "question-actions");
|
|
142
|
+
const submit = document.createElement("button");
|
|
143
|
+
submit.type = "button";
|
|
144
|
+
submit.className = "question-btn";
|
|
145
|
+
submit.setAttribute("part", "question-button");
|
|
146
|
+
submit.textContent = strings.submit;
|
|
147
|
+
actions.appendChild(submit);
|
|
148
|
+
|
|
149
|
+
let settled = false;
|
|
150
|
+
const answerFor = (): string | null => {
|
|
151
|
+
const picked = radios.find((r) => r.checked);
|
|
152
|
+
if (picked !== undefined) {
|
|
153
|
+
return picked.value;
|
|
154
|
+
}
|
|
155
|
+
if (input !== null && (otherRadio === null || otherRadio.checked)) {
|
|
156
|
+
const typed = input.value.trim();
|
|
157
|
+
return typed === "" ? null : typed;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
const refresh = (): void => {
|
|
162
|
+
if (input !== null && otherRadio !== null) {
|
|
163
|
+
input.disabled = !otherRadio.checked;
|
|
164
|
+
}
|
|
165
|
+
submit.disabled = answerFor() === null;
|
|
166
|
+
};
|
|
167
|
+
const close = (answer: string): void => {
|
|
168
|
+
if (settled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
settled = true;
|
|
172
|
+
submit.disabled = true;
|
|
173
|
+
for (const radio of radios) {
|
|
174
|
+
radio.disabled = true;
|
|
175
|
+
}
|
|
176
|
+
if (otherRadio !== null) {
|
|
177
|
+
otherRadio.disabled = true;
|
|
178
|
+
}
|
|
179
|
+
if (input !== null) {
|
|
180
|
+
input.disabled = true;
|
|
181
|
+
}
|
|
182
|
+
card.setAttribute("data-resolved", answer === "" ? "cancelled" : "answered");
|
|
183
|
+
resolve(answer);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
for (const radio of [...radios, ...(otherRadio !== null ? [otherRadio] : [])]) {
|
|
187
|
+
radio.addEventListener("change", refresh);
|
|
188
|
+
}
|
|
189
|
+
input?.addEventListener("input", refresh);
|
|
190
|
+
input?.addEventListener("keydown", (event) => {
|
|
191
|
+
if (event.key === "Enter") {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
const answer = answerFor();
|
|
194
|
+
if (answer !== null) {
|
|
195
|
+
close(answer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
submit.addEventListener("click", () => {
|
|
200
|
+
const answer = answerFor();
|
|
201
|
+
if (answer !== null) {
|
|
202
|
+
close(answer);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
options.signal?.addEventListener("abort", () => close(""), { once: true });
|
|
206
|
+
|
|
207
|
+
card.append(body, form, actions);
|
|
208
|
+
host.appendChild(card);
|
|
209
|
+
if (options.signal?.aborted === true) {
|
|
210
|
+
close("");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
refresh();
|
|
214
|
+
(hasChoices ? radios[0] : input)?.focus();
|
|
215
|
+
});
|
|
216
|
+
}
|
package/src/ui/relative_time.ts
CHANGED
|
@@ -6,15 +6,20 @@ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
|
6
6
|
*
|
|
7
7
|
* `now` is injectable so callers (and tests) can pin the reference point; it
|
|
8
8
|
* defaults to the current time. A timestamp in the future (clock skew) reads as
|
|
9
|
-
* `"just now"`.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* `"just now"`. A non-finite timestamp — an unparseable or missing `updated_at`
|
|
10
|
+
* that arrived as `NaN` — has no meaningful age, so it falls back to `justNow`
|
|
11
|
+
* rather than rendering `"NaNw ago"` or `"~2950w ago"`. The unit words come from
|
|
12
|
+
* {@link UiStrings} (the `{n}` token is filled in here) so a localized host
|
|
13
|
+
* translates them; the bucketing stays integer-rounded and locale-neutral.
|
|
12
14
|
*/
|
|
13
15
|
export function relativeTime(
|
|
14
16
|
timestamp: number,
|
|
15
17
|
now: number = Date.now(),
|
|
16
18
|
strings: UiStrings = DEFAULT_UI_STRINGS,
|
|
17
19
|
): string {
|
|
20
|
+
if (!Number.isFinite(timestamp)) {
|
|
21
|
+
return strings.justNow;
|
|
22
|
+
}
|
|
18
23
|
const seconds = Math.round((now - timestamp) / 1000);
|
|
19
24
|
if (seconds < 60) {
|
|
20
25
|
return strings.justNow;
|
package/src/ui/skills_menu.ts
CHANGED
|
@@ -24,9 +24,11 @@ export class SkillsMenu {
|
|
|
24
24
|
this.#onPick = onPick;
|
|
25
25
|
this.chips = document.createElement("div");
|
|
26
26
|
this.chips.className = "skill-chips";
|
|
27
|
+
this.chips.setAttribute("part", "skill-chips");
|
|
27
28
|
this.chips.hidden = true;
|
|
28
29
|
this.palette = document.createElement("div");
|
|
29
30
|
this.palette.className = "skill-palette";
|
|
31
|
+
this.palette.setAttribute("part", "skill-palette");
|
|
30
32
|
this.palette.setAttribute("role", "listbox");
|
|
31
33
|
this.palette.hidden = true;
|
|
32
34
|
}
|
|
@@ -137,6 +139,7 @@ export class SkillsMenu {
|
|
|
137
139
|
const button = document.createElement("button");
|
|
138
140
|
button.type = "button";
|
|
139
141
|
button.className = "skill-chip";
|
|
142
|
+
button.setAttribute("part", "skill-chip");
|
|
140
143
|
button.textContent = skill.title;
|
|
141
144
|
button.addEventListener("click", () => this.#pick(skill));
|
|
142
145
|
this.chips.appendChild(button);
|
|
@@ -149,17 +152,20 @@ export class SkillsMenu {
|
|
|
149
152
|
const item = document.createElement("button");
|
|
150
153
|
item.type = "button";
|
|
151
154
|
item.className = "skill-item";
|
|
155
|
+
item.setAttribute("part", "skill-item");
|
|
152
156
|
item.setAttribute("role", "option");
|
|
153
157
|
item.setAttribute("aria-selected", index === this.#activeIndex ? "true" : "false");
|
|
154
158
|
|
|
155
159
|
const title = document.createElement("span");
|
|
156
160
|
title.className = "skill-item-title";
|
|
161
|
+
title.setAttribute("part", "skill-item-title");
|
|
157
162
|
title.textContent = skill.title;
|
|
158
163
|
item.appendChild(title);
|
|
159
164
|
|
|
160
165
|
if (skill.description !== undefined) {
|
|
161
166
|
const desc = document.createElement("span");
|
|
162
167
|
desc.className = "skill-item-desc";
|
|
168
|
+
desc.setAttribute("part", "skill-item-desc");
|
|
163
169
|
desc.textContent = skill.description;
|
|
164
170
|
item.appendChild(desc);
|
|
165
171
|
}
|
package/src/ui/styles.ts
CHANGED
|
@@ -146,7 +146,7 @@ export const STYLES = `
|
|
|
146
146
|
--ag-ui-radius: 0;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/* Page
|
|
149
|
+
/* Page: full-bleed background with a centred reading column. Unlike
|
|
150
150
|
"full" (edge-to-edge, left-aligned messages) the content sits in a column
|
|
151
151
|
capped at --ag-ui-content-max-width. The column is produced by symmetric auto
|
|
152
152
|
padding on the scroll area + composer (no per-row wrapper), so user pills
|
|
@@ -188,7 +188,7 @@ export const STYLES = `
|
|
|
188
188
|
max-width: 100%;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/* Sidebar
|
|
191
|
+
/* Sidebar: a full-height docked panel that slides open/closed and
|
|
192
192
|
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
193
193
|
default; data-side="left" docks it left. Overlay by default — set
|
|
194
194
|
--ag-ui-position: static (and place this element in your own layout) for a
|
|
@@ -298,7 +298,7 @@ export const STYLES = `
|
|
|
298
298
|
white-space: nowrap;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
-
/* Header / launcher icon holder
|
|
301
|
+
/* Header / launcher icon holder: a slot, with a data-icon-url <img>
|
|
302
302
|
fallback, sized via --ag-ui-icon-size. */
|
|
303
303
|
.icon-holder {
|
|
304
304
|
display: inline-flex;
|
|
@@ -372,7 +372,7 @@ export const STYLES = `
|
|
|
372
372
|
gap: var(--ag-ui-space);
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
/* Empty-state region (
|
|
375
|
+
/* Empty-state region (slot): centred while it's the only thing in the
|
|
376
376
|
list, hidden as soon as a message, card, or pending indicator renders. */
|
|
377
377
|
.empty {
|
|
378
378
|
margin: auto;
|
|
@@ -384,7 +384,7 @@ export const STYLES = `
|
|
|
384
384
|
display: none;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
/* ── Answer group / well
|
|
387
|
+
/* ── Answer group / well ─────────────────────────────────────────
|
|
388
388
|
One .answer per assistant turn wraps the streamed text, its tool cards,
|
|
389
389
|
and the pending indicator so a whole answer reads (and can be boxed) as one
|
|
390
390
|
unit. A flex column on the message-list gap, stretched to the list width so
|
|
@@ -553,7 +553,7 @@ export const STYLES = `
|
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
/* ── Thoughts region
|
|
556
|
+
/* ── Thoughts region ────────────────────────────────────────────
|
|
557
557
|
A muted, collapsible chain-of-thought at the top of the answer group: open
|
|
558
558
|
while the model reasons, folded once the answer text starts. */
|
|
559
559
|
.thoughts {
|
|
@@ -646,7 +646,7 @@ export const STYLES = `
|
|
|
646
646
|
word-break: break-word;
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
-
/* Leading status icon
|
|
649
|
+
/* Leading status icon. Empty in the DOM — the glyph/spinner is drawn
|
|
650
650
|
here from the card's data-status, so it stays themeable. */
|
|
651
651
|
.tool-call-icon {
|
|
652
652
|
flex: none;
|
|
@@ -694,7 +694,7 @@ export const STYLES = `
|
|
|
694
694
|
}
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
/* Inline display mode
|
|
697
|
+
/* Inline display mode: the lightest card — drop the box chrome so the
|
|
698
698
|
status row reads as one line of the answer; the result toggle still expands
|
|
699
699
|
below it. */
|
|
700
700
|
.tool-call[data-display="inline"] {
|
|
@@ -824,7 +824,7 @@ export const STYLES = `
|
|
|
824
824
|
display: none;
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
-
/* The 🎤 mic button
|
|
827
|
+
/* The 🎤 mic button; shown only once #wireVoice mounts it. */
|
|
828
828
|
.voice-slot {
|
|
829
829
|
display: contents;
|
|
830
830
|
}
|
|
@@ -1028,6 +1028,127 @@ export const STYLES = `
|
|
|
1028
1028
|
color: #ffffff;
|
|
1029
1029
|
}
|
|
1030
1030
|
|
|
1031
|
+
/* Approval card — the server-side-tool gate (approve/deny an interrupt). */
|
|
1032
|
+
.approval {
|
|
1033
|
+
align-self: stretch;
|
|
1034
|
+
box-sizing: border-box;
|
|
1035
|
+
display: flex;
|
|
1036
|
+
flex-direction: column;
|
|
1037
|
+
gap: 8px;
|
|
1038
|
+
padding: 12px;
|
|
1039
|
+
background: var(--ag-ui-bg);
|
|
1040
|
+
border: 1px solid var(--ag-ui-accent);
|
|
1041
|
+
border-radius: 10px;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.approval[data-resolved] {
|
|
1045
|
+
opacity: 0.7;
|
|
1046
|
+
border-color: var(--ag-ui-border);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.approval-body {
|
|
1050
|
+
font-weight: 600;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.approval-actions {
|
|
1054
|
+
display: flex;
|
|
1055
|
+
gap: 8px;
|
|
1056
|
+
justify-content: flex-end;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.approval-btn {
|
|
1060
|
+
border: 1px solid var(--ag-ui-border);
|
|
1061
|
+
border-radius: 8px;
|
|
1062
|
+
padding: 8px 14px;
|
|
1063
|
+
font: inherit;
|
|
1064
|
+
font-weight: 600;
|
|
1065
|
+
cursor: pointer;
|
|
1066
|
+
background: var(--ag-ui-bg);
|
|
1067
|
+
color: var(--ag-ui-fg);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.approval-btn:disabled {
|
|
1071
|
+
cursor: default;
|
|
1072
|
+
opacity: 0.6;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.approval-btn--approve {
|
|
1076
|
+
border-color: var(--ag-ui-accent);
|
|
1077
|
+
background: var(--ag-ui-accent);
|
|
1078
|
+
color: #ffffff;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/* Question card — the built-in ask_user prompt (radios and/or free text). */
|
|
1082
|
+
.question {
|
|
1083
|
+
align-self: stretch;
|
|
1084
|
+
box-sizing: border-box;
|
|
1085
|
+
display: flex;
|
|
1086
|
+
flex-direction: column;
|
|
1087
|
+
gap: 8px;
|
|
1088
|
+
padding: 12px;
|
|
1089
|
+
background: var(--ag-ui-bg);
|
|
1090
|
+
border: 1px solid var(--ag-ui-accent);
|
|
1091
|
+
border-radius: 10px;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.question[data-resolved] {
|
|
1095
|
+
opacity: 0.7;
|
|
1096
|
+
border-color: var(--ag-ui-border);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.question-body {
|
|
1100
|
+
font-weight: 600;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.question-options {
|
|
1104
|
+
display: flex;
|
|
1105
|
+
flex-direction: column;
|
|
1106
|
+
gap: 6px;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.question-choice {
|
|
1110
|
+
display: flex;
|
|
1111
|
+
align-items: center;
|
|
1112
|
+
gap: 8px;
|
|
1113
|
+
cursor: pointer;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.question-input {
|
|
1117
|
+
box-sizing: border-box;
|
|
1118
|
+
width: 100%;
|
|
1119
|
+
padding: 8px 10px;
|
|
1120
|
+
font: inherit;
|
|
1121
|
+
color: var(--ag-ui-fg);
|
|
1122
|
+
background: var(--ag-ui-bg);
|
|
1123
|
+
border: 1px solid var(--ag-ui-border);
|
|
1124
|
+
border-radius: 8px;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
.question-input:disabled {
|
|
1128
|
+
opacity: 0.6;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.question-actions {
|
|
1132
|
+
display: flex;
|
|
1133
|
+
justify-content: flex-end;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.question-btn {
|
|
1137
|
+
border: 1px solid var(--ag-ui-accent);
|
|
1138
|
+
border-radius: 8px;
|
|
1139
|
+
padding: 8px 14px;
|
|
1140
|
+
font: inherit;
|
|
1141
|
+
font-weight: 600;
|
|
1142
|
+
cursor: pointer;
|
|
1143
|
+
background: var(--ag-ui-accent);
|
|
1144
|
+
color: #ffffff;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
.question-btn:disabled {
|
|
1148
|
+
cursor: default;
|
|
1149
|
+
opacity: 0.6;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1031
1152
|
/* Skills — chips row + the /-command palette, above the input. */
|
|
1032
1153
|
.skill-chips {
|
|
1033
1154
|
display: flex;
|
package/src/ui/thoughts_block.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A muted, collapsible "thinking" region for a reasoning model's streamed
|
|
5
|
-
* chain-of-thought
|
|
5
|
+
* chain-of-thought.
|
|
6
6
|
*
|
|
7
|
-
* Lives at the top of the current answer group (the
|
|
7
|
+
* Lives at the top of the current answer group (the turn container): it
|
|
8
8
|
* opens expanded while the model reasons — {@link stream} replaces its body with
|
|
9
9
|
* the running reasoning buffer — and {@link collapse} folds it away once the
|
|
10
10
|
* answer's first text token arrives, so the thoughts don't crowd the answer.
|
|
@@ -42,6 +42,7 @@ export class ThoughtsBlock {
|
|
|
42
42
|
|
|
43
43
|
this.#label = document.createElement("span");
|
|
44
44
|
this.#label.className = "thoughts-label";
|
|
45
|
+
this.#label.setAttribute("part", "thoughts-label");
|
|
45
46
|
this.#label.textContent = strings.thinking;
|
|
46
47
|
this.#toggle.append(this.#label);
|
|
47
48
|
|