@artooi/ag-ui-web-component 0.39.0 → 0.40.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 +155 -1
- package/README.md +70 -12
- package/dist/ag-ui-web-component.bundle.js +168 -45
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/core/ag_ui_chat.d.ts +7 -3
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +12 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/client_seed.d.ts +14 -2
- package/dist/core/client_seed.d.ts.map +1 -1
- package/dist/index.js +636 -231
- package/dist/index.js.map +2 -2
- package/dist/tools/tool_catalog.d.ts.map +1 -1
- package/dist/ui/composer/composer_attachments.d.ts +6 -1
- package/dist/ui/composer/composer_attachments.d.ts.map +1 -1
- package/dist/ui/composer/composer_voice.d.ts +3 -0
- package/dist/ui/composer/composer_voice.d.ts.map +1 -1
- package/dist/ui/excerpts/transcript_quote_offer.d.ts +7 -2
- package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -1
- package/dist/ui/history/conversation_history.d.ts +29 -3
- package/dist/ui/history/conversation_history.d.ts.map +1 -1
- package/dist/ui/placement/launcher_drag.d.ts +6 -0
- package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
- package/dist/ui/placement/panel_placement.d.ts +1 -1
- package/dist/ui/placement/panel_placement.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/transcript/transcript.d.ts +3 -2
- package/dist/ui/transcript/transcript.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/core/ag_ui_chat.ts +217 -64
- package/src/core/agui_client.ts +26 -14
- package/src/core/client_seed.ts +14 -2
- package/src/tools/tool_catalog.ts +63 -12
- package/src/ui/composer/composer_attachments.ts +64 -41
- package/src/ui/composer/composer_voice.ts +5 -0
- package/src/ui/excerpts/transcript_quote_offer.ts +26 -13
- package/src/ui/history/conversation_history.ts +125 -17
- package/src/ui/placement/launcher_drag.ts +104 -89
- package/src/ui/placement/panel_placement.ts +27 -3
- package/src/ui/styles.ts +137 -14
- package/src/ui/transcript/transcript.ts +10 -5
- package/src/ui/ui_strings.ts +3 -0
- package/src/version.ts +1 -1
|
@@ -21,6 +21,12 @@ export interface LauncherDragOptions {
|
|
|
21
21
|
* per pointer move, and never per key repeat.
|
|
22
22
|
*/
|
|
23
23
|
readonly commit: (left: number, top: number) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Ends the launcher's own listeners when aborted. The launcher outlives the
|
|
26
|
+
* element's connection to the document and this is called on every connect,
|
|
27
|
+
* so without it a re-inserted element moved one arrow press two steps.
|
|
28
|
+
*/
|
|
29
|
+
readonly signal: AbortSignal;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -55,6 +61,7 @@ const COARSE_STEP = 64;
|
|
|
55
61
|
*/
|
|
56
62
|
export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragOptions): void {
|
|
57
63
|
let suppressClick = false;
|
|
64
|
+
const { signal } = options;
|
|
58
65
|
|
|
59
66
|
launcher.addEventListener(
|
|
60
67
|
"click",
|
|
@@ -70,72 +77,76 @@ export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragO
|
|
|
70
77
|
event.stopPropagation();
|
|
71
78
|
event.preventDefault();
|
|
72
79
|
},
|
|
73
|
-
true,
|
|
80
|
+
{ capture: true, signal },
|
|
74
81
|
);
|
|
75
82
|
|
|
76
|
-
launcher.addEventListener(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const start = options.rect();
|
|
84
|
-
const originX = event.clientX;
|
|
85
|
-
const originY = event.clientY;
|
|
86
|
-
let dragging = false;
|
|
87
|
-
|
|
88
|
-
const onMove = (move: PointerEvent): void => {
|
|
89
|
-
const dx = move.clientX - originX;
|
|
90
|
-
const dy = move.clientY - originY;
|
|
91
|
-
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
83
|
+
launcher.addEventListener(
|
|
84
|
+
"pointerdown",
|
|
85
|
+
(event: PointerEvent) => {
|
|
86
|
+
// Any suppression left armed by a drag that ended elsewhere dies here,
|
|
87
|
+
// before it can reach a click belonging to this new gesture.
|
|
88
|
+
suppressClick = false;
|
|
89
|
+
if (!options.enabled()) {
|
|
92
90
|
return;
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// travel would compound.
|
|
99
|
-
const next = clampLauncher(
|
|
100
|
-
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
101
|
-
options.viewport(),
|
|
102
|
-
);
|
|
103
|
-
options.apply(next.left, next.top);
|
|
104
|
-
};
|
|
92
|
+
const start = options.rect();
|
|
93
|
+
const originX = event.clientX;
|
|
94
|
+
const originY = event.clientY;
|
|
95
|
+
let dragging = false;
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
left: start.left +
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
options.
|
|
122
|
-
|
|
123
|
-
options.commit(next.left, next.top);
|
|
124
|
-
};
|
|
97
|
+
const onMove = (move: PointerEvent): void => {
|
|
98
|
+
const dx = move.clientX - originX;
|
|
99
|
+
const dy = move.clientY - originY;
|
|
100
|
+
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
dragging = true;
|
|
104
|
+
launcher.setAttribute("data-dragging", "true");
|
|
105
|
+
// Measured from the box the press started on, never from the live one:
|
|
106
|
+
// reading it each move would chase the launcher as it moves and the
|
|
107
|
+
// travel would compound.
|
|
108
|
+
const next = clampLauncher(
|
|
109
|
+
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
110
|
+
options.viewport(),
|
|
111
|
+
);
|
|
112
|
+
options.apply(next.left, next.top);
|
|
113
|
+
};
|
|
125
114
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
115
|
+
const onUp = (up: PointerEvent): void => {
|
|
116
|
+
window.removeEventListener("pointermove", onMove);
|
|
117
|
+
window.removeEventListener("pointerup", onUp);
|
|
118
|
+
window.removeEventListener("pointercancel", onUp);
|
|
119
|
+
if (!dragging) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
launcher.removeAttribute("data-dragging");
|
|
123
|
+
suppressClick = true;
|
|
124
|
+
const next = clampLauncher(
|
|
125
|
+
{
|
|
126
|
+
...start,
|
|
127
|
+
left: start.left + (up.clientX - originX),
|
|
128
|
+
top: start.top + (up.clientY - originY),
|
|
129
|
+
},
|
|
130
|
+
options.viewport(),
|
|
131
|
+
);
|
|
132
|
+
options.commit(next.left, next.top);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Listeners on `window`, not the launcher: a fast drag outruns the pointer
|
|
136
|
+
// and would otherwise strand it mid-move with no pointerup.
|
|
137
|
+
//
|
|
138
|
+
// pointercancel matters on touch, where it is routine rather than
|
|
139
|
+
// exceptional: the browser takes the pointer back for a scroll or a system
|
|
140
|
+
// gesture and never sends pointerup. Without this the move listeners stay
|
|
141
|
+
// attached and the drag stamp never clears, which leaves the launcher
|
|
142
|
+
// following a finger that has stopped and the element believing a gesture
|
|
143
|
+
// is still in flight.
|
|
144
|
+
window.addEventListener("pointermove", onMove);
|
|
145
|
+
window.addEventListener("pointerup", onUp);
|
|
146
|
+
window.addEventListener("pointercancel", onUp);
|
|
147
|
+
},
|
|
148
|
+
{ signal },
|
|
149
|
+
);
|
|
139
150
|
|
|
140
151
|
// The position this key gesture has applied but not yet persisted. The
|
|
141
152
|
// pointer path can commit inline because a drag has one unambiguous end; a
|
|
@@ -155,37 +166,41 @@ export function enableLauncherDrag(launcher: HTMLElement, options: LauncherDragO
|
|
|
155
166
|
// Keyboard parity: a pointer-only move is unreachable without a mouse, and
|
|
156
167
|
// the launcher has no equivalent control elsewhere. Arrow keys are free on a
|
|
157
168
|
// button, so Enter and Space still expand.
|
|
158
|
-
launcher.addEventListener(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
moved
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
169
|
+
launcher.addEventListener(
|
|
170
|
+
"keydown",
|
|
171
|
+
(event: KeyboardEvent) => {
|
|
172
|
+
if (!options.enabled()) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const step = event.shiftKey ? COARSE_STEP : STEP;
|
|
176
|
+
const rect = options.rect();
|
|
177
|
+
let moved: { left: number; top: number } | null = null;
|
|
178
|
+
if (event.key === "ArrowLeft") {
|
|
179
|
+
moved = { left: rect.left - step, top: rect.top };
|
|
180
|
+
} else if (event.key === "ArrowRight") {
|
|
181
|
+
moved = { left: rect.left + step, top: rect.top };
|
|
182
|
+
} else if (event.key === "ArrowUp") {
|
|
183
|
+
moved = { left: rect.left, top: rect.top - step };
|
|
184
|
+
} else if (event.key === "ArrowDown") {
|
|
185
|
+
moved = { left: rect.left, top: rect.top + step };
|
|
186
|
+
}
|
|
187
|
+
if (moved === null) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
const next = clampLauncher({ ...rect, ...moved }, options.viewport());
|
|
192
|
+
// Live feedback per key event, persistence only when the gesture ends: a
|
|
193
|
+
// held arrow repeats at the OS rate, and committing here would put that
|
|
194
|
+
// many storage writes behind a single press.
|
|
195
|
+
options.apply(next.left, next.top);
|
|
196
|
+
pending = next;
|
|
197
|
+
},
|
|
198
|
+
{ signal },
|
|
199
|
+
);
|
|
185
200
|
|
|
186
201
|
// The key coming up ends the gesture, mirroring `pointerup`. `blur` closes
|
|
187
202
|
// one whose keyup never arrives here -- focus moved on mid-press -- because a
|
|
188
203
|
// position applied but never committed is a move the host silently forgets.
|
|
189
|
-
launcher.addEventListener("keyup", settle);
|
|
190
|
-
launcher.addEventListener("blur", settle);
|
|
204
|
+
launcher.addEventListener("keyup", settle, { signal });
|
|
205
|
+
launcher.addEventListener("blur", settle, { signal });
|
|
191
206
|
}
|
|
@@ -149,8 +149,9 @@ export class PanelPlacement {
|
|
|
149
149
|
* away and unclickable behind the open panel, so a drag there would move
|
|
150
150
|
* something nobody can see.
|
|
151
151
|
*/
|
|
152
|
-
enableLauncherDrag(): void {
|
|
152
|
+
enableLauncherDrag(signal: AbortSignal): void {
|
|
153
153
|
enableLauncherDrag(this.#host.launcher, {
|
|
154
|
+
signal,
|
|
154
155
|
enabled: () => this.#host.collapsed() && this.#launcherDraggable(),
|
|
155
156
|
rect: () => this.#launcherBox(),
|
|
156
157
|
viewport: () => this.#viewport(),
|
|
@@ -652,9 +653,22 @@ export class PanelPlacement {
|
|
|
652
653
|
if (visual === null || visual === undefined) {
|
|
653
654
|
return;
|
|
654
655
|
}
|
|
655
|
-
|
|
656
|
+
// The layout viewport from the box a fixed element is laid out against,
|
|
657
|
+
// for the reasons #screen gives -- and one more that only a phone shows.
|
|
658
|
+
// While iOS Safari has the visible area panned down to show a focused
|
|
659
|
+
// field, innerHeight reads the layout height less the pan, and goes on
|
|
660
|
+
// reading it until a window scroll the element never hears about. Measured
|
|
661
|
+
// from that, the band below came out as nothing, and at the deepest pan the
|
|
662
|
+
// browser makes -- the one that shows a composer docked at the foot of the
|
|
663
|
+
// screen -- the shorter number equals the visible height, so the check
|
|
664
|
+
// below took every measurement back and put the panel at the layout top at
|
|
665
|
+
// full height, its header above the screen. clientHeight held the layout
|
|
666
|
+
// height throughout.
|
|
667
|
+
const layout = this.#screen().height;
|
|
668
|
+
if (Math.abs(visual.height - layout) < 1) {
|
|
656
669
|
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-height");
|
|
657
670
|
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-bottom");
|
|
671
|
+
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-top");
|
|
658
672
|
return;
|
|
659
673
|
}
|
|
660
674
|
this.#host.element.style.setProperty(
|
|
@@ -667,11 +681,21 @@ export class PanelPlacement {
|
|
|
667
681
|
// the launcher at that corner stay behind the keyboard until this lifts
|
|
668
682
|
// them. Never negative -- a visual viewport panned up past the layout one
|
|
669
683
|
// would otherwise pull the panel down off the screen.
|
|
670
|
-
const hidden =
|
|
684
|
+
const hidden = layout - visual.height - visual.offsetTop;
|
|
671
685
|
this.#host.element.style.setProperty(
|
|
672
686
|
"--ag-ui-visual-viewport-inset-bottom",
|
|
673
687
|
`${Math.max(0, Math.round(hidden))}px`,
|
|
674
688
|
);
|
|
689
|
+
// And what is hidden above it. To show a field the keyboard would cover, a
|
|
690
|
+
// browser pans the visual viewport down the layout one, and a fixed panel
|
|
691
|
+
// anchored at the top stays at the layout top: sized to the visible height,
|
|
692
|
+
// it then shows only its lower part, from the pan down, with its header off
|
|
693
|
+
// the screen and page background under it. Never negative, for the same
|
|
694
|
+
// reason as the band below.
|
|
695
|
+
this.#host.element.style.setProperty(
|
|
696
|
+
"--ag-ui-visual-viewport-inset-top",
|
|
697
|
+
`${Math.max(0, Math.round(visual.offsetTop))}px`,
|
|
698
|
+
);
|
|
675
699
|
}
|
|
676
700
|
|
|
677
701
|
/**
|
package/src/ui/styles.ts
CHANGED
|
@@ -180,7 +180,24 @@ export const STYLES = `
|
|
|
180
180
|
keyboard changes no viewport-percentage length -- not vh, not dvh, not svh
|
|
181
181
|
-- so a full-bleed panel on a phone has to be told the height rather than
|
|
182
182
|
deriving it. The value to publish there is the visual viewport's. */
|
|
183
|
-
--_viewport-height: var(
|
|
183
|
+
--_viewport-height: var(
|
|
184
|
+
--ag-ui-viewport-height,
|
|
185
|
+
calc(
|
|
186
|
+
min(var(--_visual-viewport-inset-top) + var(--_visual-viewport-height), 100dvh - var(--_viewport-inset-bottom)) -
|
|
187
|
+
var(--_viewport-inset-top) - var(--_keyboard-inset-top)
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
/* That default is the host's box cut to the part of the screen the user can
|
|
191
|
+
see: from the top the panel starts at, the host's reserved top moved down
|
|
192
|
+
by the keyboard inset below, to whichever is further up, the bottom of the
|
|
193
|
+
visible area or the host's reserved bottom. Where nothing has measured, the
|
|
194
|
+
visible area is the layout viewport with no pan, and this is the host's
|
|
195
|
+
box exactly.
|
|
196
|
+
|
|
197
|
+
Cut rather than replaced. A measured height used as the whole answer kept
|
|
198
|
+
a bar the host reserved at the top in the position and out of the height,
|
|
199
|
+
so the panel ran that far past the bottom of the visible area, with its
|
|
200
|
+
composer behind the keyboard. */
|
|
184
201
|
/* The measured height of the part of the screen the user can actually see,
|
|
185
202
|
written by the element from the visual viewport and falling back to the
|
|
186
203
|
layout viewport where nothing has measured yet.
|
|
@@ -193,11 +210,15 @@ export const STYLES = `
|
|
|
193
210
|
|
|
194
211
|
Separate from the token above so a host that states the usable height
|
|
195
212
|
outright still wins: the element writes this one inline, and an inline
|
|
196
|
-
value would otherwise outrank the host's own rule.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
value would otherwise outrank the host's own rule.
|
|
214
|
+
|
|
215
|
+
The fallback is dvh rather than vh because the browser's own bars do move
|
|
216
|
+
it, where a keyboard does not. iOS Safari resolves vh to the viewport with
|
|
217
|
+
its bars collapsed, so on a page that does not scroll them away a panel
|
|
218
|
+
sized from it ran under the address bar by the bars' height -- 40px on
|
|
219
|
+
the phone measured -- with a docked composer underneath. dvh is the
|
|
220
|
+
viewport with the bars as they are. */
|
|
221
|
+
--_visual-viewport-height: var(--ag-ui-visual-viewport-height, 100dvh);
|
|
201
222
|
/* How much of the layout viewport is hidden below the visible one, measured
|
|
202
223
|
and written by the element alongside the height above.
|
|
203
224
|
|
|
@@ -212,6 +233,24 @@ export const STYLES = `
|
|
|
212
233
|
A host wanting no keyboard lift at all sets --ag-ui-keyboard-inset: 0px. */
|
|
213
234
|
--_keyboard-inset: var(--ag-ui-keyboard-inset, var(--_visual-viewport-inset-bottom));
|
|
214
235
|
--_visual-viewport-inset-bottom: var(--ag-ui-visual-viewport-inset-bottom, 0px);
|
|
236
|
+
/* How far a panel anchored at the top moves down for a keyboard, below the
|
|
237
|
+
top the host reserved. With the same two-token shape as the lift above;
|
|
238
|
+
0px keeps the panel at the host's top.
|
|
239
|
+
|
|
240
|
+
To show the field being typed into, a browser pans the visible area down
|
|
241
|
+
the layout viewport, and a fixed element stays against the layout top, so
|
|
242
|
+
a panel that did not move showed only its lower part, from the pan down,
|
|
243
|
+
with its header off the screen and an empty band under it. It moves by
|
|
244
|
+
what the pan goes past the host's reserved top, not by the whole pan: the
|
|
245
|
+
pan scrolls a bar reserved there away with the rest of the page rather
|
|
246
|
+
than pushing it down, and adding the two put the panel a whole bar below
|
|
247
|
+
the visible area. A corner panel anchored at the bottom does not use this,
|
|
248
|
+
because the band below already accounts for the pan. */
|
|
249
|
+
--_keyboard-inset-top: var(--ag-ui-keyboard-inset-top, max(0px, var(--_visual-viewport-inset-top) - var(--_viewport-inset-top)));
|
|
250
|
+
/* How much of the layout viewport is hidden above the visible one: how far
|
|
251
|
+
the browser panned. Measured and written by the element with the two
|
|
252
|
+
above. */
|
|
253
|
+
--_visual-viewport-inset-top: var(--ag-ui-visual-viewport-inset-top, 0px);
|
|
215
254
|
--_viewport-width: var(
|
|
216
255
|
--ag-ui-viewport-width,
|
|
217
256
|
calc(100vw - var(--_viewport-inset-left) - var(--_viewport-inset-right))
|
|
@@ -348,7 +387,7 @@ export const STYLES = `
|
|
|
348
387
|
}
|
|
349
388
|
|
|
350
389
|
:host([placement="side"]) {
|
|
351
|
-
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
390
|
+
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
352
391
|
--_width: var(--ag-ui-width, 420px);
|
|
353
392
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
354
393
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
@@ -358,7 +397,8 @@ export const STYLES = `
|
|
|
358
397
|
:host([placement="full"]) {
|
|
359
398
|
--_inset: var(
|
|
360
399
|
--ag-ui-inset,
|
|
361
|
-
var(--_viewport-inset-top)
|
|
400
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
401
|
+
var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
362
402
|
);
|
|
363
403
|
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
364
404
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
@@ -375,7 +415,8 @@ export const STYLES = `
|
|
|
375
415
|
:host([placement="page"]) {
|
|
376
416
|
--_inset: var(
|
|
377
417
|
--ag-ui-inset,
|
|
378
|
-
var(--_viewport-inset-top)
|
|
418
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
419
|
+
var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
379
420
|
);
|
|
380
421
|
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
381
422
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
@@ -441,7 +482,7 @@ export const STYLES = `
|
|
|
441
482
|
:host([placement=""]:not([data-small-viewport="off"])) {
|
|
442
483
|
--_inset: var(
|
|
443
484
|
--ag-ui-inset,
|
|
444
|
-
var(--_viewport-inset-top) var(--_viewport-inset-right)
|
|
485
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
445
486
|
calc(var(--_viewport-inset-bottom) + var(--_keyboard-inset))
|
|
446
487
|
var(--_viewport-inset-left)
|
|
447
488
|
);
|
|
@@ -471,7 +512,7 @@ export const STYLES = `
|
|
|
471
512
|
--ag-ui-position: static (and place this element in your own layout) for a
|
|
472
513
|
host-managed push instead. */
|
|
473
514
|
:host([placement="sidebar"]) {
|
|
474
|
-
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
515
|
+
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
475
516
|
--_width: var(--ag-ui-width, 420px);
|
|
476
517
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
477
518
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
@@ -480,7 +521,11 @@ export const STYLES = `
|
|
|
480
521
|
}
|
|
481
522
|
|
|
482
523
|
:host([placement="sidebar"][data-side="left"]) {
|
|
483
|
-
--_inset: var(
|
|
524
|
+
--_inset: var(
|
|
525
|
+
--ag-ui-inset,
|
|
526
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) auto var(--_viewport-inset-bottom)
|
|
527
|
+
var(--_viewport-inset-left)
|
|
528
|
+
);
|
|
484
529
|
}
|
|
485
530
|
|
|
486
531
|
/* The docked panel is pinned to the edge it docks against rather than filling
|
|
@@ -1237,6 +1282,64 @@ export const STYLES = `
|
|
|
1237
1282
|
border-block-start-color: transparent;
|
|
1238
1283
|
}
|
|
1239
1284
|
|
|
1285
|
+
/* On a phone the composer stays at the foot and the greeting takes the room
|
|
1286
|
+
above it, which is the other way round from everything above.
|
|
1287
|
+
|
|
1288
|
+
Centring is a shape for a screen with room to spare: the composer reads as
|
|
1289
|
+
the one thing on the page, and whichever way the rows grow there is space
|
|
1290
|
+
left on both sides of them. A phone has no space to spare, and the keyboard
|
|
1291
|
+
is what takes it -- opening one leaves a 426px-tall visible area with the
|
|
1292
|
+
composer halfway up it and an empty band underneath, which is the half of
|
|
1293
|
+
the screen a thumb is already resting on. Docked, the field sits directly
|
|
1294
|
+
over the keyboard, where every chat on the platform puts it, and the
|
|
1295
|
+
greeting keeps the rest.
|
|
1296
|
+
|
|
1297
|
+
Both rules restate the selector they override, with the breakpoint's own
|
|
1298
|
+
opt-out added: this is part of the shape a host keeps its desktop layout
|
|
1299
|
+
instead of, so it goes the same way as the rest of that shape. Restating
|
|
1300
|
+
rather than adding conditions to the originals keeps each of those rules
|
|
1301
|
+
readable as one decision, and source order settles the pair. */
|
|
1302
|
+
@media (max-width: 600px) {
|
|
1303
|
+
:host([data-empty]:not([data-restoring]):not([data-small-viewport="off"])) .chat::after {
|
|
1304
|
+
flex-grow: 0;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/* The empty region fills the transcript instead of being centred in it as a
|
|
1308
|
+
block, so the two things in it can go to different places: the prompts the
|
|
1309
|
+
host offers to the foot, against the composer, and the greeting to the
|
|
1310
|
+
middle of what they leave.
|
|
1311
|
+
|
|
1312
|
+
A prompt chip is a way into the conversation. Against the field it starts,
|
|
1313
|
+
it reads as one; under the greeting halfway up the panel, with the field
|
|
1314
|
+
at the foot, it reads as decoration next to something else. Nothing here
|
|
1315
|
+
moves them on a screen where the composer is still centred: there the
|
|
1316
|
+
whole region hangs at the foot of the upper half, directly over the
|
|
1317
|
+
composer, and the prompts are already against it.
|
|
1318
|
+
|
|
1319
|
+
Auto block margins on the greeting are what centre it, and they take the
|
|
1320
|
+
free space whether or not there are prompts below to leave any, so a host
|
|
1321
|
+
that offers none gets a greeting in the middle of the transcript rather
|
|
1322
|
+
than one clinging to the composer.
|
|
1323
|
+
The region declares a display here, which outranks the rule collapsing a
|
|
1324
|
+
hidden one, so it says it is not hidden itself. Nothing sets the two
|
|
1325
|
+
apart today -- the transcript writes the hidden property and the host's
|
|
1326
|
+
data-empty from one expression -- and that is exactly why: a rule holding
|
|
1327
|
+
because of how a method in another file happens to be written is a rule
|
|
1328
|
+
holding by luck. */
|
|
1329
|
+
:host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]),
|
|
1330
|
+
:host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]) {
|
|
1331
|
+
margin: 0;
|
|
1332
|
+
flex: 1;
|
|
1333
|
+
display: flex;
|
|
1334
|
+
flex-direction: column;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
:host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting,
|
|
1338
|
+
:host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting {
|
|
1339
|
+
margin-block: auto;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1240
1343
|
/* Nor does the greeting paint while a restore is in flight. Hidden rather than
|
|
1241
1344
|
removed, so it keeps its place and nothing around it moves when it shows. */
|
|
1242
1345
|
:host([data-restoring]) .greeting {
|
|
@@ -1720,6 +1823,17 @@ export const STYLES = `
|
|
|
1720
1823
|
gap: 3px;
|
|
1721
1824
|
}
|
|
1722
1825
|
|
|
1826
|
+
/* A call with no arguments drops the region rather than framing an empty
|
|
1827
|
+
object, and a settled card drops the result region until it has one -- both
|
|
1828
|
+
by setting the hidden property, and an author display beats the user-agent
|
|
1829
|
+
rule for that attribute. Without this the region kept laying out: 42px of
|
|
1830
|
+
card holding the ARGUMENTS heading over nothing, on every call the agent
|
|
1831
|
+
made with no arguments, in the display mode that shows arguments by
|
|
1832
|
+
default. */
|
|
1833
|
+
.tool-call-section[hidden] {
|
|
1834
|
+
display: none;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1723
1837
|
/* The heading that tells the two payloads apart. Without it the arguments and
|
|
1724
1838
|
the result were one run of text and a reader had to guess the boundary. */
|
|
1725
1839
|
.tool-call-section-label {
|
|
@@ -2390,8 +2504,8 @@ export const STYLES = `
|
|
|
2390
2504
|
display: none;
|
|
2391
2505
|
}
|
|
2392
2506
|
|
|
2393
|
-
/* The mic button's mount point; filled only once
|
|
2394
|
-
control. */
|
|
2507
|
+
/* The mic button's mount point; filled only once ComposerVoice.wire mounts
|
|
2508
|
+
the control, which it skips unless transcription is configured. */
|
|
2395
2509
|
.voice-slot {
|
|
2396
2510
|
display: contents;
|
|
2397
2511
|
}
|
|
@@ -3068,6 +3182,15 @@ export const STYLES = `
|
|
|
3068
3182
|
padding: 10px 12px;
|
|
3069
3183
|
}
|
|
3070
3184
|
|
|
3185
|
+
/* The row is hidden whenever the host offers no skills, which is most elements
|
|
3186
|
+
most of the time, and an author display beats the user-agent rule for the
|
|
3187
|
+
hidden attribute. Without this it kept its padding: 20px of panel between
|
|
3188
|
+
the transcript and the composer, under every placement, reading as the gap
|
|
3189
|
+
under whatever the transcript ends with. */
|
|
3190
|
+
.skill-chips[hidden] {
|
|
3191
|
+
display: none;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3071
3194
|
.skill-chip {
|
|
3072
3195
|
border: 1px solid var(--_border);
|
|
3073
3196
|
border-radius: 999px;
|
|
@@ -82,12 +82,17 @@ export class Transcript {
|
|
|
82
82
|
/**
|
|
83
83
|
* Start following the foot of the list, and wire the jump button to it.
|
|
84
84
|
* Called while rendering rather than at construction: the viewport has to
|
|
85
|
-
* exist and the observer has to have something to observe.
|
|
85
|
+
* exist and the observer has to have something to observe. The button
|
|
86
|
+
* outlives a connection, so its listener goes under `signal`.
|
|
86
87
|
*/
|
|
87
|
-
mountScroller(jumpButton: HTMLButtonElement): void {
|
|
88
|
-
jumpButton.addEventListener(
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
mountScroller(jumpButton: HTMLButtonElement, signal: AbortSignal): void {
|
|
89
|
+
jumpButton.addEventListener(
|
|
90
|
+
"click",
|
|
91
|
+
() => {
|
|
92
|
+
this.jump();
|
|
93
|
+
},
|
|
94
|
+
{ signal },
|
|
95
|
+
);
|
|
91
96
|
this.#scroller = createStickToBottom({
|
|
92
97
|
viewport: this.#host.messages,
|
|
93
98
|
onMissedContent: (missed) => {
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface UiStrings {
|
|
|
118
118
|
notConnected: string;
|
|
119
119
|
/** Composer hint when a run continuation was picked with an empty composer. */
|
|
120
120
|
continueNeedsTurn: string;
|
|
121
|
+
/** Composer hint when a run continuation was picked while a run is in flight. */
|
|
122
|
+
continueWhileRunning: string;
|
|
121
123
|
|
|
122
124
|
// ── Composer ────────────────────────────────────────────────────────────────
|
|
123
125
|
/** `aria-label` of the message textarea. */
|
|
@@ -385,6 +387,7 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
385
387
|
"{n} file still uploading — it was not sent with this message and is still attached.",
|
|
386
388
|
notConnected: "This chat isn’t connected to an agent, so the message wasn’t sent.",
|
|
387
389
|
continueNeedsTurn: "Type the next turn in the composer first, then pick a run to continue.",
|
|
390
|
+
continueWhileRunning: "Wait for the current answer or stop it, then pick a run to continue.",
|
|
388
391
|
skillNeeds: "“{title}” needs {fields} — fill it in below, then send.",
|
|
389
392
|
|
|
390
393
|
message: "Message",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.40.0";
|