@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
package/dist/index.js
CHANGED
|
@@ -982,6 +982,7 @@ var DEFAULT_UI_STRINGS = {
|
|
|
982
982
|
attachmentsStillUploading: "{n} file still uploading \u2014 it was not sent with this message and is still attached.",
|
|
983
983
|
notConnected: "This chat isn\u2019t connected to an agent, so the message wasn\u2019t sent.",
|
|
984
984
|
continueNeedsTurn: "Type the next turn in the composer first, then pick a run to continue.",
|
|
985
|
+
continueWhileRunning: "Wait for the current answer or stop it, then pick a run to continue.",
|
|
985
986
|
skillNeeds: "\u201C{title}\u201D needs {fields} \u2014 fill it in below, then send.",
|
|
986
987
|
message: "Message",
|
|
987
988
|
inputPlaceholder: "Ask anything\u2026",
|
|
@@ -1418,12 +1419,12 @@ var ToolCatalog = class {
|
|
|
1418
1419
|
},
|
|
1419
1420
|
required: ["question"]
|
|
1420
1421
|
},
|
|
1421
|
-
handler: (args) => this.#askUser(args)
|
|
1422
|
+
handler: (args, callId) => this.#askUser(args, callId)
|
|
1422
1423
|
}
|
|
1423
1424
|
];
|
|
1424
1425
|
}
|
|
1425
1426
|
/** Render the `ask_user` question card and resolve with the user's answer. */
|
|
1426
|
-
async #askUser(args) {
|
|
1427
|
+
async #askUser(args, callId) {
|
|
1427
1428
|
const question = typeof args["question"] === "string" ? args["question"] : "";
|
|
1428
1429
|
const request = { question };
|
|
1429
1430
|
const rawOptions = args["options"];
|
|
@@ -1435,19 +1436,59 @@ var ToolCatalog = class {
|
|
|
1435
1436
|
}
|
|
1436
1437
|
const signal = this.#host.decision.open();
|
|
1437
1438
|
this.#host.hidePending();
|
|
1438
|
-
const
|
|
1439
|
-
const answer = renderer !== null ? (
|
|
1440
|
-
// Called on the element, as `this.askUserRenderer(...)` always was.
|
|
1441
|
-
await renderer.call(this.#host.element, request, { signal })
|
|
1442
|
-
) : await requestQuestion(this.#host.ensureGroup(), request, {
|
|
1439
|
+
const builtIn = () => requestQuestion(this.#host.ensureGroup(), request, {
|
|
1443
1440
|
signal,
|
|
1444
1441
|
strings: this.#host.strings()
|
|
1445
1442
|
});
|
|
1443
|
+
const renderer = this.#host.askUserRenderer();
|
|
1444
|
+
let answer;
|
|
1445
|
+
if (renderer === null) {
|
|
1446
|
+
answer = await builtIn();
|
|
1447
|
+
} else {
|
|
1448
|
+
try {
|
|
1449
|
+
answer = await renderer.call(this.#host.element, request, { signal });
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1446
1454
|
this.#host.decision.close();
|
|
1447
1455
|
this.#host.updateEmptyState();
|
|
1448
1456
|
this.#host.follow();
|
|
1449
1457
|
return answer;
|
|
1450
1458
|
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Answer an `ask_user` call whose host renderer threw or rejected instead of
|
|
1461
|
+
* answering: put the question to the built-in card.
|
|
1462
|
+
*
|
|
1463
|
+
* The same answer `approvalRenderer` gets for the same failure, and for the
|
|
1464
|
+
* same reason: a renderer is presentation, not a policy. It decides how the
|
|
1465
|
+
* question looks, never whether the agent's question is put to the user.
|
|
1466
|
+
* Uncaught, the throw escaped the handler, so the call's card settled as an
|
|
1467
|
+
* error quoting the host's message, that message went on to the agent as the
|
|
1468
|
+
* tool result -- a detail of the host's page, never written for the model --
|
|
1469
|
+
* and the pending decision was never closed, so the next Stop, in whatever
|
|
1470
|
+
* round, aborted the signal of a wait that had already ended rather than
|
|
1471
|
+
* finding nothing open. The built-in card still asks,
|
|
1472
|
+
* and the run carries on as if no renderer had been set. Reported the way a
|
|
1473
|
+
* failed `render` is, and for the same reason: survived is not the same as
|
|
1474
|
+
* findable.
|
|
1475
|
+
*
|
|
1476
|
+
* Except when the wait was already abandoned. A renderer honouring its signal
|
|
1477
|
+
* rejects once a Stop fires it, which is the signal working rather than the
|
|
1478
|
+
* renderer failing, and a card drawn then would ask about a run the user just
|
|
1479
|
+
* ended. So it resolves with the empty answer the built-in card resolves with
|
|
1480
|
+
* on the same abort, and says nothing.
|
|
1481
|
+
*/
|
|
1482
|
+
#afterRendererFailed(error, signal, callId, builtIn) {
|
|
1483
|
+
if (signal.aborted) {
|
|
1484
|
+
return Promise.resolve("");
|
|
1485
|
+
}
|
|
1486
|
+
console.warn(
|
|
1487
|
+
`ag-ui-chat: askUserRenderer failed for tool call ${callId}, so the built-in question card asks instead`,
|
|
1488
|
+
error
|
|
1489
|
+
);
|
|
1490
|
+
return builtIn();
|
|
1491
|
+
}
|
|
1451
1492
|
};
|
|
1452
1493
|
|
|
1453
1494
|
// src/skills/skill_name_from.ts
|
|
@@ -2711,8 +2752,15 @@ var ComposerAttachments = class {
|
|
|
2711
2752
|
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
2712
2753
|
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
2713
2754
|
* stays hidden and the chat degrades to text-only.
|
|
2755
|
+
*
|
|
2756
|
+
* Called on every connect, so it starts by taking down the tray the last
|
|
2757
|
+
* connection mounted: that one was disposed when the element left, and the
|
|
2758
|
+
* attributes that decide whether there is a tray at all may have changed
|
|
2759
|
+
* since. The shell outlives a connection, so its listeners go under `signal`.
|
|
2714
2760
|
*/
|
|
2715
|
-
wire() {
|
|
2761
|
+
wire(signal) {
|
|
2762
|
+
this.#tray?.element.remove();
|
|
2763
|
+
this.#tray = null;
|
|
2716
2764
|
const url = this.#host.element.getAttribute("data-attachments-url");
|
|
2717
2765
|
const upload = this.#host.uploadHandler() ?? this.#defaultUploadHandler(url);
|
|
2718
2766
|
if (upload === null) {
|
|
@@ -2734,8 +2782,8 @@ var ComposerAttachments = class {
|
|
|
2734
2782
|
this.#host.slot.appendChild(this.#tray.element);
|
|
2735
2783
|
this.#host.fileInput.accept = accept;
|
|
2736
2784
|
this.#host.button.hidden = false;
|
|
2737
|
-
this.#enableDragAndDrop();
|
|
2738
|
-
this.#enablePaste(tray);
|
|
2785
|
+
this.#enableDragAndDrop(signal);
|
|
2786
|
+
this.#enablePaste(tray, signal);
|
|
2739
2787
|
}
|
|
2740
2788
|
/** The queueing behind `AgUiChat.attachFile`, whose doc is the contract. */
|
|
2741
2789
|
attach(file) {
|
|
@@ -2779,25 +2827,37 @@ var ComposerAttachments = class {
|
|
|
2779
2827
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
2780
2828
|
}
|
|
2781
2829
|
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
2782
|
-
#enableDragAndDrop() {
|
|
2830
|
+
#enableDragAndDrop(signal) {
|
|
2783
2831
|
const chat = this.#host.chat;
|
|
2784
|
-
chat.addEventListener(
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2832
|
+
chat.addEventListener(
|
|
2833
|
+
"dragover",
|
|
2834
|
+
(event) => {
|
|
2835
|
+
event.preventDefault();
|
|
2836
|
+
chat.classList.add("chat--dragover");
|
|
2837
|
+
},
|
|
2838
|
+
{ signal }
|
|
2839
|
+
);
|
|
2840
|
+
chat.addEventListener(
|
|
2841
|
+
"dragleave",
|
|
2842
|
+
() => {
|
|
2843
|
+
chat.classList.remove("chat--dragover");
|
|
2844
|
+
},
|
|
2845
|
+
{ signal }
|
|
2846
|
+
);
|
|
2847
|
+
chat.addEventListener(
|
|
2848
|
+
"drop",
|
|
2849
|
+
(event) => {
|
|
2850
|
+
event.preventDefault();
|
|
2851
|
+
chat.classList.remove("chat--dragover");
|
|
2852
|
+
const files = event.dataTransfer?.files;
|
|
2853
|
+
if (files !== void 0) {
|
|
2854
|
+
for (const file of Array.from(files)) {
|
|
2855
|
+
this.#tray?.add(file);
|
|
2856
|
+
}
|
|
2798
2857
|
}
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2858
|
+
},
|
|
2859
|
+
{ signal }
|
|
2860
|
+
);
|
|
2801
2861
|
}
|
|
2802
2862
|
/**
|
|
2803
2863
|
* Turn a very long text paste into an attachment instead of a wall of text.
|
|
@@ -2867,24 +2927,28 @@ var ComposerAttachments = class {
|
|
|
2867
2927
|
* clipboard, and swallowing the words someone meant to paste in order to
|
|
2868
2928
|
* attach a picture they did not is the worse of the two failures.
|
|
2869
2929
|
*/
|
|
2870
|
-
#enablePaste(tray) {
|
|
2871
|
-
this.#host.chat.addEventListener(
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2930
|
+
#enablePaste(tray, signal) {
|
|
2931
|
+
this.#host.chat.addEventListener(
|
|
2932
|
+
"paste",
|
|
2933
|
+
(event) => {
|
|
2934
|
+
const clipboard = event.clipboardData ?? null;
|
|
2935
|
+
if (clipboard === null) {
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
const files = Array.from(clipboard.files);
|
|
2939
|
+
if (files.length === 0) {
|
|
2940
|
+
this.#pasteLongTextAsFile(event, clipboard, tray);
|
|
2941
|
+
return;
|
|
2942
|
+
}
|
|
2943
|
+
if (clipboard.getData("text/plain") === "") {
|
|
2944
|
+
event.preventDefault();
|
|
2945
|
+
}
|
|
2946
|
+
for (const file of files) {
|
|
2947
|
+
this.#tray?.add(named(file));
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
{ signal }
|
|
2951
|
+
);
|
|
2888
2952
|
}
|
|
2889
2953
|
/** Tell the host what the tray now holds — see {@link ATTACHMENT_EVENT}. */
|
|
2890
2954
|
#dispatch(tray) {
|
|
@@ -3112,8 +3176,13 @@ var ComposerVoice = class {
|
|
|
3112
3176
|
* built-in POST endpoint. The control records via `MediaRecorder` and drops
|
|
3113
3177
|
* the transcript into the composer; with neither configured the mic stays
|
|
3114
3178
|
* hidden and the chat is text-only.
|
|
3179
|
+
*
|
|
3180
|
+
* Called on every connect, so it starts by taking down the mic the last
|
|
3181
|
+
* connection mounted, which was released when the element left.
|
|
3115
3182
|
*/
|
|
3116
3183
|
wire() {
|
|
3184
|
+
this.#voice?.element.remove();
|
|
3185
|
+
this.#voice = null;
|
|
3117
3186
|
const url = this.#host.element.getAttribute("data-transcribe-url");
|
|
3118
3187
|
const transcribe = this.#host.transcribeHandler() ?? this.#defaultTranscribeHandler(url);
|
|
3119
3188
|
if (transcribe === null) {
|
|
@@ -3622,26 +3691,39 @@ ${quoted}`;
|
|
|
3622
3691
|
this.#pageQuote?.detach();
|
|
3623
3692
|
this.#pageQuote = null;
|
|
3624
3693
|
}
|
|
3625
|
-
/**
|
|
3626
|
-
|
|
3694
|
+
/**
|
|
3695
|
+
* Build the offer and listen for settled selections in the transcript.
|
|
3696
|
+
*
|
|
3697
|
+
* The button and the transcript outlive a connection, so the listeners go
|
|
3698
|
+
* under `signal`, which the element aborts when it leaves the document.
|
|
3699
|
+
*/
|
|
3700
|
+
mount(signal) {
|
|
3627
3701
|
const button2 = this.button;
|
|
3628
3702
|
button2.className = "quote-selection";
|
|
3629
3703
|
button2.type = "button";
|
|
3630
3704
|
button2.setAttribute("part", "quote-selection");
|
|
3631
3705
|
button2.textContent = this.#host.strings().quoteSelection;
|
|
3632
3706
|
button2.hidden = true;
|
|
3633
|
-
button2.addEventListener(
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3707
|
+
button2.addEventListener(
|
|
3708
|
+
"mousedown",
|
|
3709
|
+
(event) => {
|
|
3710
|
+
event.preventDefault();
|
|
3711
|
+
},
|
|
3712
|
+
{ signal }
|
|
3713
|
+
);
|
|
3714
|
+
button2.addEventListener(
|
|
3715
|
+
"click",
|
|
3716
|
+
() => {
|
|
3717
|
+
this.#host.quote(this.#quoting);
|
|
3718
|
+
window.getSelection()?.removeAllRanges();
|
|
3719
|
+
this.#hide();
|
|
3720
|
+
},
|
|
3721
|
+
{ signal }
|
|
3722
|
+
);
|
|
3641
3723
|
const messages = this.#host.messages;
|
|
3642
|
-
messages.addEventListener("mouseup", (event) => this.#onSelectionSettled(event));
|
|
3643
|
-
messages.addEventListener("keyup", () => this.#onSelectionSettled());
|
|
3644
|
-
messages.addEventListener("mousedown", () => this.#hide());
|
|
3724
|
+
messages.addEventListener("mouseup", (event) => this.#onSelectionSettled(event), { signal });
|
|
3725
|
+
messages.addEventListener("keyup", () => this.#onSelectionSettled(), { signal });
|
|
3726
|
+
messages.addEventListener("mousedown", () => this.#hide(), { signal });
|
|
3645
3727
|
}
|
|
3646
3728
|
/** Whether the transcript offers to quote what the user selects. */
|
|
3647
3729
|
#enabled() {
|
|
@@ -4022,10 +4104,16 @@ var ConversationHistory = class {
|
|
|
4022
4104
|
/** The active thread's id. Empty until the element connects. */
|
|
4023
4105
|
#threadId = "";
|
|
4024
4106
|
/**
|
|
4025
|
-
* The
|
|
4026
|
-
*
|
|
4107
|
+
* The conversation as last written for the next client the element builds to
|
|
4108
|
+
* start from: what the last restore replayed, or what a checkpoint
|
|
4109
|
+
* continuation last saved after it. Emptied with the rest of the in-memory run.
|
|
4027
4110
|
*/
|
|
4028
4111
|
#restored = [];
|
|
4112
|
+
/**
|
|
4113
|
+
* Counts the conversations cleared away, so a continuation can tell whether
|
|
4114
|
+
* the one it continued is still on screen when it saves.
|
|
4115
|
+
*/
|
|
4116
|
+
#cleared = 0;
|
|
4029
4117
|
// Bumped on every rehydrate; a replay whose generation is stale (a newer
|
|
4030
4118
|
// thread switch started while it awaited a slow store) drops its result.
|
|
4031
4119
|
#generation = 0;
|
|
@@ -4043,10 +4131,14 @@ var ConversationHistory = class {
|
|
|
4043
4131
|
get threadId() {
|
|
4044
4132
|
return this.#threadId;
|
|
4045
4133
|
}
|
|
4046
|
-
/** The
|
|
4134
|
+
/** The conversation as last written, for seeding the next client. */
|
|
4047
4135
|
get restored() {
|
|
4048
4136
|
return this.#restored;
|
|
4049
4137
|
}
|
|
4138
|
+
/** The checkpoint continuation in flight, or `null` when none is running. */
|
|
4139
|
+
get continuation() {
|
|
4140
|
+
return this.#continuation;
|
|
4141
|
+
}
|
|
4050
4142
|
/** Point at the thread the store says is active. */
|
|
4051
4143
|
adoptActiveThread() {
|
|
4052
4144
|
this.#threadId = this.#host.conversationStore().threadId();
|
|
@@ -4071,6 +4163,7 @@ var ConversationHistory = class {
|
|
|
4071
4163
|
/** Forget the messages the last restore seeded, with the rest of the run. */
|
|
4072
4164
|
forgetRestored() {
|
|
4073
4165
|
this.#restored = [];
|
|
4166
|
+
this.#cleared += 1;
|
|
4074
4167
|
}
|
|
4075
4168
|
/** Delete the active thread if nothing was ever sent in it. */
|
|
4076
4169
|
reapUnsent() {
|
|
@@ -4156,43 +4249,76 @@ var ConversationHistory = class {
|
|
|
4156
4249
|
* Uses a short-lived agent pointed at the resume / fork endpoint and seeded
|
|
4157
4250
|
* with no history, because those endpoints supply the prior turns from the
|
|
4158
4251
|
* snapshot and re-sending them would duplicate. A separate agent makes that
|
|
4159
|
-
* structural
|
|
4160
|
-
* `run_id` the endpoints also require.
|
|
4252
|
+
* structural and mints the fresh `run_id` the endpoints also require.
|
|
4161
4253
|
*
|
|
4162
4254
|
* Built by the same construction as the conversation's own client, so the
|
|
4163
4255
|
* continuation streams into the same transcript the user is looking at and
|
|
4164
4256
|
* runs under the same state, tools and bounds. It is the run in flight while
|
|
4165
4257
|
* it lasts: {@link stopContinuation} is how the element's Stop reaches it.
|
|
4258
|
+
*
|
|
4259
|
+
* What it adds joins the conversation. Its saves write the conversation on
|
|
4260
|
+
* screen ahead of its exchange, and each one hands that whole list to the
|
|
4261
|
+
* next client the element builds, so the next ordinary message is sent with
|
|
4262
|
+
* the exchange it follows. The conversation's own client never held it, and
|
|
4263
|
+
* was sending without it.
|
|
4166
4264
|
*/
|
|
4167
4265
|
async continueRun(runId, verb) {
|
|
4168
4266
|
const index = this.runs();
|
|
4169
4267
|
if (index === null) {
|
|
4170
4268
|
return;
|
|
4171
4269
|
}
|
|
4270
|
+
if (this.#host.running() || this.#continuation !== null) {
|
|
4271
|
+
this.#refuse(this.#host.strings().continueWhileRunning);
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4172
4274
|
const content = this.#host.input.value.trim();
|
|
4173
4275
|
if (content === "") {
|
|
4174
|
-
this.#
|
|
4175
|
-
this.#host.hint.hidden = false;
|
|
4176
|
-
this.#host.input.focus();
|
|
4276
|
+
this.#refuse(this.#host.strings().continueNeedsTurn);
|
|
4177
4277
|
return;
|
|
4178
4278
|
}
|
|
4179
4279
|
this.#host.input.value = "";
|
|
4180
4280
|
this.#host.autoGrow();
|
|
4281
|
+
const cleared = this.#cleared;
|
|
4181
4282
|
const client = this.#host.buildClient({
|
|
4182
4283
|
endpoint: verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId),
|
|
4183
4284
|
// The seed the endpoints assume: nothing. The snapshot is the history.
|
|
4184
4285
|
initialMessages: [],
|
|
4185
|
-
//
|
|
4186
|
-
//
|
|
4187
|
-
// client
|
|
4188
|
-
|
|
4286
|
+
// What its saves write ahead of the exchange: the conversation on screen,
|
|
4287
|
+
// in the form the store holds it. From the conversation's own client when
|
|
4288
|
+
// there is one, because that client keeps how each call ended beside its
|
|
4289
|
+
// messages rather than on them; otherwise what the last restore or
|
|
4290
|
+
// continuation wrote, which is already in that form.
|
|
4291
|
+
//
|
|
4292
|
+
// All of it, even where this forks an earlier run and the server's
|
|
4293
|
+
// snapshot stops there: what is saved is what the screen shows.
|
|
4294
|
+
follows: this.#host.client()?.annotatedMessages ?? this.#restored,
|
|
4295
|
+
onSaved: (conversation) => {
|
|
4296
|
+
if (cleared !== this.#cleared) {
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4299
|
+
this.#restored = conversation;
|
|
4300
|
+
this.#host.releaseClient();
|
|
4301
|
+
}
|
|
4189
4302
|
});
|
|
4190
4303
|
this.#continuation = client;
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4304
|
+
try {
|
|
4305
|
+
await client.send(content);
|
|
4306
|
+
} finally {
|
|
4307
|
+
if (this.#continuation === client) {
|
|
4308
|
+
this.#continuation = null;
|
|
4309
|
+
this.#host.continuationEnded();
|
|
4310
|
+
}
|
|
4194
4311
|
}
|
|
4195
4312
|
}
|
|
4313
|
+
/**
|
|
4314
|
+
* Say at the composer why a picked run did not continue, and put the caret
|
|
4315
|
+
* there. The hint clears itself on the next keystroke.
|
|
4316
|
+
*/
|
|
4317
|
+
#refuse(reason) {
|
|
4318
|
+
this.#host.hint.textContent = reason;
|
|
4319
|
+
this.#host.hint.hidden = false;
|
|
4320
|
+
this.#host.input.focus();
|
|
4321
|
+
}
|
|
4196
4322
|
/**
|
|
4197
4323
|
* Restore the conversation from the store on mount, then — if a navigating
|
|
4198
4324
|
* tool reloaded the page mid-run — resume the loop by supplying that tool's
|
|
@@ -4784,6 +4910,7 @@ var STEP = 16;
|
|
|
4784
4910
|
var COARSE_STEP = 64;
|
|
4785
4911
|
function enableLauncherDrag(launcher, options) {
|
|
4786
4912
|
let suppressClick = false;
|
|
4913
|
+
const { signal } = options;
|
|
4787
4914
|
launcher.addEventListener(
|
|
4788
4915
|
"click",
|
|
4789
4916
|
(event) => {
|
|
@@ -4794,54 +4921,58 @@ function enableLauncherDrag(launcher, options) {
|
|
|
4794
4921
|
event.stopPropagation();
|
|
4795
4922
|
event.preventDefault();
|
|
4796
4923
|
},
|
|
4797
|
-
true
|
|
4924
|
+
{ capture: true, signal }
|
|
4798
4925
|
);
|
|
4799
|
-
launcher.addEventListener(
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
const start = options.rect();
|
|
4805
|
-
const originX = event.clientX;
|
|
4806
|
-
const originY = event.clientY;
|
|
4807
|
-
let dragging = false;
|
|
4808
|
-
const onMove = (move) => {
|
|
4809
|
-
const dx = move.clientX - originX;
|
|
4810
|
-
const dy = move.clientY - originY;
|
|
4811
|
-
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
4812
|
-
return;
|
|
4813
|
-
}
|
|
4814
|
-
dragging = true;
|
|
4815
|
-
launcher.setAttribute("data-dragging", "true");
|
|
4816
|
-
const next = clampLauncher(
|
|
4817
|
-
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
4818
|
-
options.viewport()
|
|
4819
|
-
);
|
|
4820
|
-
options.apply(next.left, next.top);
|
|
4821
|
-
};
|
|
4822
|
-
const onUp = (up) => {
|
|
4823
|
-
window.removeEventListener("pointermove", onMove);
|
|
4824
|
-
window.removeEventListener("pointerup", onUp);
|
|
4825
|
-
window.removeEventListener("pointercancel", onUp);
|
|
4826
|
-
if (!dragging) {
|
|
4926
|
+
launcher.addEventListener(
|
|
4927
|
+
"pointerdown",
|
|
4928
|
+
(event) => {
|
|
4929
|
+
suppressClick = false;
|
|
4930
|
+
if (!options.enabled()) {
|
|
4827
4931
|
return;
|
|
4828
4932
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
const
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4933
|
+
const start = options.rect();
|
|
4934
|
+
const originX = event.clientX;
|
|
4935
|
+
const originY = event.clientY;
|
|
4936
|
+
let dragging = false;
|
|
4937
|
+
const onMove = (move) => {
|
|
4938
|
+
const dx = move.clientX - originX;
|
|
4939
|
+
const dy = move.clientY - originY;
|
|
4940
|
+
if (!dragging && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
dragging = true;
|
|
4944
|
+
launcher.setAttribute("data-dragging", "true");
|
|
4945
|
+
const next = clampLauncher(
|
|
4946
|
+
{ ...start, left: start.left + dx, top: start.top + dy },
|
|
4947
|
+
options.viewport()
|
|
4948
|
+
);
|
|
4949
|
+
options.apply(next.left, next.top);
|
|
4950
|
+
};
|
|
4951
|
+
const onUp = (up) => {
|
|
4952
|
+
window.removeEventListener("pointermove", onMove);
|
|
4953
|
+
window.removeEventListener("pointerup", onUp);
|
|
4954
|
+
window.removeEventListener("pointercancel", onUp);
|
|
4955
|
+
if (!dragging) {
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
launcher.removeAttribute("data-dragging");
|
|
4959
|
+
suppressClick = true;
|
|
4960
|
+
const next = clampLauncher(
|
|
4961
|
+
{
|
|
4962
|
+
...start,
|
|
4963
|
+
left: start.left + (up.clientX - originX),
|
|
4964
|
+
top: start.top + (up.clientY - originY)
|
|
4965
|
+
},
|
|
4966
|
+
options.viewport()
|
|
4967
|
+
);
|
|
4968
|
+
options.commit(next.left, next.top);
|
|
4969
|
+
};
|
|
4970
|
+
window.addEventListener("pointermove", onMove);
|
|
4971
|
+
window.addEventListener("pointerup", onUp);
|
|
4972
|
+
window.addEventListener("pointercancel", onUp);
|
|
4973
|
+
},
|
|
4974
|
+
{ signal }
|
|
4975
|
+
);
|
|
4845
4976
|
let pending = null;
|
|
4846
4977
|
const settle = () => {
|
|
4847
4978
|
if (pending === null) {
|
|
@@ -4851,32 +4982,36 @@ function enableLauncherDrag(launcher, options) {
|
|
|
4851
4982
|
pending = null;
|
|
4852
4983
|
options.commit(left, top);
|
|
4853
4984
|
};
|
|
4854
|
-
launcher.addEventListener(
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
moved =
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4985
|
+
launcher.addEventListener(
|
|
4986
|
+
"keydown",
|
|
4987
|
+
(event) => {
|
|
4988
|
+
if (!options.enabled()) {
|
|
4989
|
+
return;
|
|
4990
|
+
}
|
|
4991
|
+
const step = event.shiftKey ? COARSE_STEP : STEP;
|
|
4992
|
+
const rect = options.rect();
|
|
4993
|
+
let moved = null;
|
|
4994
|
+
if (event.key === "ArrowLeft") {
|
|
4995
|
+
moved = { left: rect.left - step, top: rect.top };
|
|
4996
|
+
} else if (event.key === "ArrowRight") {
|
|
4997
|
+
moved = { left: rect.left + step, top: rect.top };
|
|
4998
|
+
} else if (event.key === "ArrowUp") {
|
|
4999
|
+
moved = { left: rect.left, top: rect.top - step };
|
|
5000
|
+
} else if (event.key === "ArrowDown") {
|
|
5001
|
+
moved = { left: rect.left, top: rect.top + step };
|
|
5002
|
+
}
|
|
5003
|
+
if (moved === null) {
|
|
5004
|
+
return;
|
|
5005
|
+
}
|
|
5006
|
+
event.preventDefault();
|
|
5007
|
+
const next = clampLauncher({ ...rect, ...moved }, options.viewport());
|
|
5008
|
+
options.apply(next.left, next.top);
|
|
5009
|
+
pending = next;
|
|
5010
|
+
},
|
|
5011
|
+
{ signal }
|
|
5012
|
+
);
|
|
5013
|
+
launcher.addEventListener("keyup", settle, { signal });
|
|
5014
|
+
launcher.addEventListener("blur", settle, { signal });
|
|
4880
5015
|
}
|
|
4881
5016
|
|
|
4882
5017
|
// src/ui/placement/place_widget.ts
|
|
@@ -5123,8 +5258,9 @@ var PanelPlacement = class {
|
|
|
5123
5258
|
* away and unclickable behind the open panel, so a drag there would move
|
|
5124
5259
|
* something nobody can see.
|
|
5125
5260
|
*/
|
|
5126
|
-
enableLauncherDrag() {
|
|
5261
|
+
enableLauncherDrag(signal) {
|
|
5127
5262
|
enableLauncherDrag(this.#host.launcher, {
|
|
5263
|
+
signal,
|
|
5128
5264
|
enabled: () => this.#host.collapsed() && this.#launcherDraggable(),
|
|
5129
5265
|
rect: () => this.#launcherBox(),
|
|
5130
5266
|
viewport: () => this.#viewport(),
|
|
@@ -5509,20 +5645,26 @@ var PanelPlacement = class {
|
|
|
5509
5645
|
if (visual === null || visual === void 0) {
|
|
5510
5646
|
return;
|
|
5511
5647
|
}
|
|
5512
|
-
|
|
5648
|
+
const layout = this.#screen().height;
|
|
5649
|
+
if (Math.abs(visual.height - layout) < 1) {
|
|
5513
5650
|
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-height");
|
|
5514
5651
|
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-bottom");
|
|
5652
|
+
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-top");
|
|
5515
5653
|
return;
|
|
5516
5654
|
}
|
|
5517
5655
|
this.#host.element.style.setProperty(
|
|
5518
5656
|
"--ag-ui-visual-viewport-height",
|
|
5519
5657
|
`${Math.round(visual.height)}px`
|
|
5520
5658
|
);
|
|
5521
|
-
const hidden =
|
|
5659
|
+
const hidden = layout - visual.height - visual.offsetTop;
|
|
5522
5660
|
this.#host.element.style.setProperty(
|
|
5523
5661
|
"--ag-ui-visual-viewport-inset-bottom",
|
|
5524
5662
|
`${Math.max(0, Math.round(hidden))}px`
|
|
5525
5663
|
);
|
|
5664
|
+
this.#host.element.style.setProperty(
|
|
5665
|
+
"--ag-ui-visual-viewport-inset-top",
|
|
5666
|
+
`${Math.max(0, Math.round(visual.offsetTop))}px`
|
|
5667
|
+
);
|
|
5526
5668
|
}
|
|
5527
5669
|
/**
|
|
5528
5670
|
* The launcher's box in viewport coordinates, with its transform divided out.
|
|
@@ -6665,7 +6807,24 @@ var STYLES = `
|
|
|
6665
6807
|
keyboard changes no viewport-percentage length -- not vh, not dvh, not svh
|
|
6666
6808
|
-- so a full-bleed panel on a phone has to be told the height rather than
|
|
6667
6809
|
deriving it. The value to publish there is the visual viewport's. */
|
|
6668
|
-
--_viewport-height: var(
|
|
6810
|
+
--_viewport-height: var(
|
|
6811
|
+
--ag-ui-viewport-height,
|
|
6812
|
+
calc(
|
|
6813
|
+
min(var(--_visual-viewport-inset-top) + var(--_visual-viewport-height), 100dvh - var(--_viewport-inset-bottom)) -
|
|
6814
|
+
var(--_viewport-inset-top) - var(--_keyboard-inset-top)
|
|
6815
|
+
)
|
|
6816
|
+
);
|
|
6817
|
+
/* That default is the host's box cut to the part of the screen the user can
|
|
6818
|
+
see: from the top the panel starts at, the host's reserved top moved down
|
|
6819
|
+
by the keyboard inset below, to whichever is further up, the bottom of the
|
|
6820
|
+
visible area or the host's reserved bottom. Where nothing has measured, the
|
|
6821
|
+
visible area is the layout viewport with no pan, and this is the host's
|
|
6822
|
+
box exactly.
|
|
6823
|
+
|
|
6824
|
+
Cut rather than replaced. A measured height used as the whole answer kept
|
|
6825
|
+
a bar the host reserved at the top in the position and out of the height,
|
|
6826
|
+
so the panel ran that far past the bottom of the visible area, with its
|
|
6827
|
+
composer behind the keyboard. */
|
|
6669
6828
|
/* The measured height of the part of the screen the user can actually see,
|
|
6670
6829
|
written by the element from the visual viewport and falling back to the
|
|
6671
6830
|
layout viewport where nothing has measured yet.
|
|
@@ -6678,11 +6837,15 @@ var STYLES = `
|
|
|
6678
6837
|
|
|
6679
6838
|
Separate from the token above so a host that states the usable height
|
|
6680
6839
|
outright still wins: the element writes this one inline, and an inline
|
|
6681
|
-
value would otherwise outrank the host's own rule.
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6840
|
+
value would otherwise outrank the host's own rule.
|
|
6841
|
+
|
|
6842
|
+
The fallback is dvh rather than vh because the browser's own bars do move
|
|
6843
|
+
it, where a keyboard does not. iOS Safari resolves vh to the viewport with
|
|
6844
|
+
its bars collapsed, so on a page that does not scroll them away a panel
|
|
6845
|
+
sized from it ran under the address bar by the bars' height -- 40px on
|
|
6846
|
+
the phone measured -- with a docked composer underneath. dvh is the
|
|
6847
|
+
viewport with the bars as they are. */
|
|
6848
|
+
--_visual-viewport-height: var(--ag-ui-visual-viewport-height, 100dvh);
|
|
6686
6849
|
/* How much of the layout viewport is hidden below the visible one, measured
|
|
6687
6850
|
and written by the element alongside the height above.
|
|
6688
6851
|
|
|
@@ -6697,6 +6860,24 @@ var STYLES = `
|
|
|
6697
6860
|
A host wanting no keyboard lift at all sets --ag-ui-keyboard-inset: 0px. */
|
|
6698
6861
|
--_keyboard-inset: var(--ag-ui-keyboard-inset, var(--_visual-viewport-inset-bottom));
|
|
6699
6862
|
--_visual-viewport-inset-bottom: var(--ag-ui-visual-viewport-inset-bottom, 0px);
|
|
6863
|
+
/* How far a panel anchored at the top moves down for a keyboard, below the
|
|
6864
|
+
top the host reserved. With the same two-token shape as the lift above;
|
|
6865
|
+
0px keeps the panel at the host's top.
|
|
6866
|
+
|
|
6867
|
+
To show the field being typed into, a browser pans the visible area down
|
|
6868
|
+
the layout viewport, and a fixed element stays against the layout top, so
|
|
6869
|
+
a panel that did not move showed only its lower part, from the pan down,
|
|
6870
|
+
with its header off the screen and an empty band under it. It moves by
|
|
6871
|
+
what the pan goes past the host's reserved top, not by the whole pan: the
|
|
6872
|
+
pan scrolls a bar reserved there away with the rest of the page rather
|
|
6873
|
+
than pushing it down, and adding the two put the panel a whole bar below
|
|
6874
|
+
the visible area. A corner panel anchored at the bottom does not use this,
|
|
6875
|
+
because the band below already accounts for the pan. */
|
|
6876
|
+
--_keyboard-inset-top: var(--ag-ui-keyboard-inset-top, max(0px, var(--_visual-viewport-inset-top) - var(--_viewport-inset-top)));
|
|
6877
|
+
/* How much of the layout viewport is hidden above the visible one: how far
|
|
6878
|
+
the browser panned. Measured and written by the element with the two
|
|
6879
|
+
above. */
|
|
6880
|
+
--_visual-viewport-inset-top: var(--ag-ui-visual-viewport-inset-top, 0px);
|
|
6700
6881
|
--_viewport-width: var(
|
|
6701
6882
|
--ag-ui-viewport-width,
|
|
6702
6883
|
calc(100vw - var(--_viewport-inset-left) - var(--_viewport-inset-right))
|
|
@@ -6833,7 +7014,7 @@ var STYLES = `
|
|
|
6833
7014
|
}
|
|
6834
7015
|
|
|
6835
7016
|
:host([placement="side"]) {
|
|
6836
|
-
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
7017
|
+
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
6837
7018
|
--_width: var(--ag-ui-width, 420px);
|
|
6838
7019
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
6839
7020
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
@@ -6843,7 +7024,8 @@ var STYLES = `
|
|
|
6843
7024
|
:host([placement="full"]) {
|
|
6844
7025
|
--_inset: var(
|
|
6845
7026
|
--ag-ui-inset,
|
|
6846
|
-
var(--_viewport-inset-top)
|
|
7027
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
7028
|
+
var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
6847
7029
|
);
|
|
6848
7030
|
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
6849
7031
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
@@ -6860,7 +7042,8 @@ var STYLES = `
|
|
|
6860
7042
|
:host([placement="page"]) {
|
|
6861
7043
|
--_inset: var(
|
|
6862
7044
|
--ag-ui-inset,
|
|
6863
|
-
var(--_viewport-inset-top)
|
|
7045
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
7046
|
+
var(--_viewport-inset-bottom) var(--_viewport-inset-left)
|
|
6864
7047
|
);
|
|
6865
7048
|
--_width: var(--ag-ui-width, var(--_viewport-width));
|
|
6866
7049
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
@@ -6926,7 +7109,7 @@ var STYLES = `
|
|
|
6926
7109
|
:host([placement=""]:not([data-small-viewport="off"])) {
|
|
6927
7110
|
--_inset: var(
|
|
6928
7111
|
--ag-ui-inset,
|
|
6929
|
-
var(--_viewport-inset-top) var(--_viewport-inset-right)
|
|
7112
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
|
|
6930
7113
|
calc(var(--_viewport-inset-bottom) + var(--_keyboard-inset))
|
|
6931
7114
|
var(--_viewport-inset-left)
|
|
6932
7115
|
);
|
|
@@ -6956,7 +7139,7 @@ var STYLES = `
|
|
|
6956
7139
|
--ag-ui-position: static (and place this element in your own layout) for a
|
|
6957
7140
|
host-managed push instead. */
|
|
6958
7141
|
:host([placement="sidebar"]) {
|
|
6959
|
-
--_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
7142
|
+
--_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
|
|
6960
7143
|
--_width: var(--ag-ui-width, 420px);
|
|
6961
7144
|
--_height: var(--ag-ui-height, var(--_viewport-height));
|
|
6962
7145
|
--_max-height: var(--ag-ui-max-height, var(--_viewport-height));
|
|
@@ -6965,7 +7148,11 @@ var STYLES = `
|
|
|
6965
7148
|
}
|
|
6966
7149
|
|
|
6967
7150
|
:host([placement="sidebar"][data-side="left"]) {
|
|
6968
|
-
--_inset: var(
|
|
7151
|
+
--_inset: var(
|
|
7152
|
+
--ag-ui-inset,
|
|
7153
|
+
calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) auto var(--_viewport-inset-bottom)
|
|
7154
|
+
var(--_viewport-inset-left)
|
|
7155
|
+
);
|
|
6969
7156
|
}
|
|
6970
7157
|
|
|
6971
7158
|
/* The docked panel is pinned to the edge it docks against rather than filling
|
|
@@ -7722,6 +7909,64 @@ var STYLES = `
|
|
|
7722
7909
|
border-block-start-color: transparent;
|
|
7723
7910
|
}
|
|
7724
7911
|
|
|
7912
|
+
/* On a phone the composer stays at the foot and the greeting takes the room
|
|
7913
|
+
above it, which is the other way round from everything above.
|
|
7914
|
+
|
|
7915
|
+
Centring is a shape for a screen with room to spare: the composer reads as
|
|
7916
|
+
the one thing on the page, and whichever way the rows grow there is space
|
|
7917
|
+
left on both sides of them. A phone has no space to spare, and the keyboard
|
|
7918
|
+
is what takes it -- opening one leaves a 426px-tall visible area with the
|
|
7919
|
+
composer halfway up it and an empty band underneath, which is the half of
|
|
7920
|
+
the screen a thumb is already resting on. Docked, the field sits directly
|
|
7921
|
+
over the keyboard, where every chat on the platform puts it, and the
|
|
7922
|
+
greeting keeps the rest.
|
|
7923
|
+
|
|
7924
|
+
Both rules restate the selector they override, with the breakpoint's own
|
|
7925
|
+
opt-out added: this is part of the shape a host keeps its desktop layout
|
|
7926
|
+
instead of, so it goes the same way as the rest of that shape. Restating
|
|
7927
|
+
rather than adding conditions to the originals keeps each of those rules
|
|
7928
|
+
readable as one decision, and source order settles the pair. */
|
|
7929
|
+
@media (max-width: 600px) {
|
|
7930
|
+
:host([data-empty]:not([data-restoring]):not([data-small-viewport="off"])) .chat::after {
|
|
7931
|
+
flex-grow: 0;
|
|
7932
|
+
}
|
|
7933
|
+
|
|
7934
|
+
/* The empty region fills the transcript instead of being centred in it as a
|
|
7935
|
+
block, so the two things in it can go to different places: the prompts the
|
|
7936
|
+
host offers to the foot, against the composer, and the greeting to the
|
|
7937
|
+
middle of what they leave.
|
|
7938
|
+
|
|
7939
|
+
A prompt chip is a way into the conversation. Against the field it starts,
|
|
7940
|
+
it reads as one; under the greeting halfway up the panel, with the field
|
|
7941
|
+
at the foot, it reads as decoration next to something else. Nothing here
|
|
7942
|
+
moves them on a screen where the composer is still centred: there the
|
|
7943
|
+
whole region hangs at the foot of the upper half, directly over the
|
|
7944
|
+
composer, and the prompts are already against it.
|
|
7945
|
+
|
|
7946
|
+
Auto block margins on the greeting are what centre it, and they take the
|
|
7947
|
+
free space whether or not there are prompts below to leave any, so a host
|
|
7948
|
+
that offers none gets a greeting in the middle of the transcript rather
|
|
7949
|
+
than one clinging to the composer.
|
|
7950
|
+
The region declares a display here, which outranks the rule collapsing a
|
|
7951
|
+
hidden one, so it says it is not hidden itself. Nothing sets the two
|
|
7952
|
+
apart today -- the transcript writes the hidden property and the host's
|
|
7953
|
+
data-empty from one expression -- and that is exactly why: a rule holding
|
|
7954
|
+
because of how a method in another file happens to be written is a rule
|
|
7955
|
+
holding by luck. */
|
|
7956
|
+
:host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]),
|
|
7957
|
+
:host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]) {
|
|
7958
|
+
margin: 0;
|
|
7959
|
+
flex: 1;
|
|
7960
|
+
display: flex;
|
|
7961
|
+
flex-direction: column;
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
:host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting,
|
|
7965
|
+
:host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting {
|
|
7966
|
+
margin-block: auto;
|
|
7967
|
+
}
|
|
7968
|
+
}
|
|
7969
|
+
|
|
7725
7970
|
/* Nor does the greeting paint while a restore is in flight. Hidden rather than
|
|
7726
7971
|
removed, so it keeps its place and nothing around it moves when it shows. */
|
|
7727
7972
|
:host([data-restoring]) .greeting {
|
|
@@ -8205,6 +8450,17 @@ var STYLES = `
|
|
|
8205
8450
|
gap: 3px;
|
|
8206
8451
|
}
|
|
8207
8452
|
|
|
8453
|
+
/* A call with no arguments drops the region rather than framing an empty
|
|
8454
|
+
object, and a settled card drops the result region until it has one -- both
|
|
8455
|
+
by setting the hidden property, and an author display beats the user-agent
|
|
8456
|
+
rule for that attribute. Without this the region kept laying out: 42px of
|
|
8457
|
+
card holding the ARGUMENTS heading over nothing, on every call the agent
|
|
8458
|
+
made with no arguments, in the display mode that shows arguments by
|
|
8459
|
+
default. */
|
|
8460
|
+
.tool-call-section[hidden] {
|
|
8461
|
+
display: none;
|
|
8462
|
+
}
|
|
8463
|
+
|
|
8208
8464
|
/* The heading that tells the two payloads apart. Without it the arguments and
|
|
8209
8465
|
the result were one run of text and a reader had to guess the boundary. */
|
|
8210
8466
|
.tool-call-section-label {
|
|
@@ -8875,8 +9131,8 @@ var STYLES = `
|
|
|
8875
9131
|
display: none;
|
|
8876
9132
|
}
|
|
8877
9133
|
|
|
8878
|
-
/* The mic button's mount point; filled only once
|
|
8879
|
-
control. */
|
|
9134
|
+
/* The mic button's mount point; filled only once ComposerVoice.wire mounts
|
|
9135
|
+
the control, which it skips unless transcription is configured. */
|
|
8880
9136
|
.voice-slot {
|
|
8881
9137
|
display: contents;
|
|
8882
9138
|
}
|
|
@@ -9553,6 +9809,15 @@ var STYLES = `
|
|
|
9553
9809
|
padding: 10px 12px;
|
|
9554
9810
|
}
|
|
9555
9811
|
|
|
9812
|
+
/* The row is hidden whenever the host offers no skills, which is most elements
|
|
9813
|
+
most of the time, and an author display beats the user-agent rule for the
|
|
9814
|
+
hidden attribute. Without this it kept its padding: 20px of panel between
|
|
9815
|
+
the transcript and the composer, under every placement, reading as the gap
|
|
9816
|
+
under whatever the transcript ends with. */
|
|
9817
|
+
.skill-chips[hidden] {
|
|
9818
|
+
display: none;
|
|
9819
|
+
}
|
|
9820
|
+
|
|
9556
9821
|
.skill-chip {
|
|
9557
9822
|
border: 1px solid var(--_border);
|
|
9558
9823
|
border-radius: 999px;
|
|
@@ -13846,12 +14111,17 @@ var Transcript = class {
|
|
|
13846
14111
|
/**
|
|
13847
14112
|
* Start following the foot of the list, and wire the jump button to it.
|
|
13848
14113
|
* Called while rendering rather than at construction: the viewport has to
|
|
13849
|
-
* exist and the observer has to have something to observe.
|
|
14114
|
+
* exist and the observer has to have something to observe. The button
|
|
14115
|
+
* outlives a connection, so its listener goes under `signal`.
|
|
13850
14116
|
*/
|
|
13851
|
-
mountScroller(jumpButton) {
|
|
13852
|
-
jumpButton.addEventListener(
|
|
13853
|
-
|
|
13854
|
-
|
|
14117
|
+
mountScroller(jumpButton, signal) {
|
|
14118
|
+
jumpButton.addEventListener(
|
|
14119
|
+
"click",
|
|
14120
|
+
() => {
|
|
14121
|
+
this.jump();
|
|
14122
|
+
},
|
|
14123
|
+
{ signal }
|
|
14124
|
+
);
|
|
13855
14125
|
this.#scroller = createStickToBottom({
|
|
13856
14126
|
viewport: this.#host.messages,
|
|
13857
14127
|
onMissedContent: (missed) => {
|
|
@@ -14533,20 +14803,31 @@ var AgUiClient = class {
|
|
|
14533
14803
|
* unannotated save quietly threw the annotations away.
|
|
14534
14804
|
*/
|
|
14535
14805
|
#persist() {
|
|
14806
|
+
this.#onPersist(this.annotatedMessages);
|
|
14807
|
+
}
|
|
14808
|
+
/**
|
|
14809
|
+
* The history in the form a save writes it: {@link messages}, with how each
|
|
14810
|
+
* tool call ended annotated onto its result -- exactly what
|
|
14811
|
+
* {@link AgUiClientConfig.onPersist} is handed.
|
|
14812
|
+
*
|
|
14813
|
+
* For writing the conversation somewhere other than a save. The element needs
|
|
14814
|
+
* it when a checkpoint continuation adds to a conversation this client holds:
|
|
14815
|
+
* the continuation's saves write the conversation ahead of the exchange, and
|
|
14816
|
+
* the bare {@link messages} would drop the annotations, so a declined card
|
|
14817
|
+
* turned green on the next reload.
|
|
14818
|
+
*/
|
|
14819
|
+
get annotatedMessages() {
|
|
14536
14820
|
const messages = this.#agent.messages;
|
|
14537
14821
|
if (this.#outcomes.size === 0) {
|
|
14538
|
-
|
|
14539
|
-
return;
|
|
14822
|
+
return messages;
|
|
14540
14823
|
}
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
})
|
|
14549
|
-
);
|
|
14824
|
+
return messages.map((message) => {
|
|
14825
|
+
if (message.role !== "tool") {
|
|
14826
|
+
return message;
|
|
14827
|
+
}
|
|
14828
|
+
const outcome = this.#outcomes.get(message.toolCallId);
|
|
14829
|
+
return outcome === void 0 ? message : { ...message, outcome };
|
|
14830
|
+
});
|
|
14550
14831
|
}
|
|
14551
14832
|
async #runLoop() {
|
|
14552
14833
|
let resume;
|
|
@@ -15771,6 +16052,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
15771
16052
|
* Optional full replacement for the `ask_user` question UI, resolving with
|
|
15772
16053
|
* the answer; the same seam as {@link approvalRenderer}, styled via `strings`
|
|
15773
16054
|
* and the `question*` `::part()`s when left unset. Requires {@link askUser}.
|
|
16055
|
+
* A renderer that throws or rejects hands the question to the built-in card,
|
|
16056
|
+
* as a failing {@link approvalRenderer} does, unless Stop already fired.
|
|
15774
16057
|
*/
|
|
15775
16058
|
askUserRenderer = null;
|
|
15776
16059
|
/**
|
|
@@ -16075,10 +16358,37 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16075
16358
|
#voice;
|
|
16076
16359
|
/** Whether the element is currently in the DOM; gates the connect-time warning. */
|
|
16077
16360
|
#connected = false;
|
|
16361
|
+
/**
|
|
16362
|
+
* Aborted when the element leaves the document, and replaced when it comes
|
|
16363
|
+
* back.
|
|
16364
|
+
*
|
|
16365
|
+
* Every listener connecting adds to an element that outlives the connection
|
|
16366
|
+
* -- the composer, Send, the launcher, the shell -- is added under this
|
|
16367
|
+
* signal. Connecting runs again on every insertion, so a listener added
|
|
16368
|
+
* without it is a second listener the next time: one click on Stop stopped
|
|
16369
|
+
* the run three times, and one press of the built-in theme toggle flipped it
|
|
16370
|
+
* twice, back to where it started.
|
|
16371
|
+
*/
|
|
16372
|
+
#connection = new AbortController();
|
|
16373
|
+
/**
|
|
16374
|
+
* Whether the element has connected before, which makes this connection a
|
|
16375
|
+
* re-insertion: there is a transcript on screen from last time, and the
|
|
16376
|
+
* history replay would draw the conversation a second time beneath it.
|
|
16377
|
+
*/
|
|
16378
|
+
#connectedBefore = false;
|
|
16379
|
+
/**
|
|
16380
|
+
* The remote store `data-threads-url` wrapped around the conversation store,
|
|
16381
|
+
* and the store inside it, so connecting again can wrap that store rather
|
|
16382
|
+
* than the wrapper.
|
|
16383
|
+
*/
|
|
16384
|
+
#threadStore = null;
|
|
16078
16385
|
#client = null;
|
|
16079
16386
|
// Seed for the next client. Once one exists it owns the live value (the
|
|
16080
16387
|
// agent applies STATE_SNAPSHOT / STATE_DELTA into it), so this is only the
|
|
16081
|
-
// starting point — `sharedState` reads through to the client when present
|
|
16388
|
+
// starting point — `sharedState` reads through to the client when present,
|
|
16389
|
+
// and to a running checkpoint continuation before that. Every client mirrors
|
|
16390
|
+
// its changes here, which is what the client built after a continuation is
|
|
16391
|
+
// seeded from.
|
|
16082
16392
|
#sharedState = {};
|
|
16083
16393
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
16084
16394
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
@@ -16285,7 +16595,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16285
16595
|
}
|
|
16286
16596
|
});
|
|
16287
16597
|
this.#checkpoints = new CheckpointMenu((runId, verb) => {
|
|
16288
|
-
void this.#history.continueRun(runId, verb)
|
|
16598
|
+
void this.#history.continueRun(runId, verb).catch((error) => {
|
|
16599
|
+
console.warn("<ag-ui-chat>: continuing a run failed", error);
|
|
16600
|
+
});
|
|
16289
16601
|
});
|
|
16290
16602
|
this.#history = new ConversationHistory({
|
|
16291
16603
|
element: this,
|
|
@@ -16305,8 +16617,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16305
16617
|
requestCredentials: () => this.#requestCredentials(),
|
|
16306
16618
|
appendMessage: (role, content) => this.appendMessage(role, content),
|
|
16307
16619
|
autoGrow: () => autoGrow(this.#input),
|
|
16620
|
+
continuationEnded: () => this.#flushQueued(),
|
|
16621
|
+
client: () => this.#client,
|
|
16308
16622
|
ensureClient: () => this.#ensureClient(),
|
|
16309
16623
|
buildClient: (seed) => this.#buildClient(seed),
|
|
16624
|
+
releaseClient: () => {
|
|
16625
|
+
this.#client = null;
|
|
16626
|
+
},
|
|
16627
|
+
running: () => this.#running,
|
|
16310
16628
|
cancelRun: () => this.#cancelRun(),
|
|
16311
16629
|
resetState: () => this.#resetState(),
|
|
16312
16630
|
setRunning: (running) => this.#setRunning(running)
|
|
@@ -16388,11 +16706,32 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16388
16706
|
* from {@link registerPageState}, which exposes host state as ordinary tools.
|
|
16389
16707
|
*/
|
|
16390
16708
|
get sharedState() {
|
|
16391
|
-
return this.#
|
|
16709
|
+
return this.#liveClient()?.state ?? this.#sharedState;
|
|
16392
16710
|
}
|
|
16393
16711
|
set sharedState(state) {
|
|
16394
16712
|
this.#sharedState = { ...state };
|
|
16395
|
-
this.#
|
|
16713
|
+
this.#liveClient()?.setState(this.#sharedState);
|
|
16714
|
+
}
|
|
16715
|
+
/**
|
|
16716
|
+
* The client holding the conversation's live shared state: a checkpoint
|
|
16717
|
+
* continuation while one runs, otherwise the conversation's own client, and
|
|
16718
|
+
* `null` before either exists.
|
|
16719
|
+
*
|
|
16720
|
+
* The continuation first, because the state it streams is applied to its own
|
|
16721
|
+
* agent. Reading the conversation's client alone returned what the state was
|
|
16722
|
+
* before the continuation began, for as long as that client lived.
|
|
16723
|
+
*
|
|
16724
|
+
* It now lives no longer than the continuation's first save, which hands the
|
|
16725
|
+
* conversation to the next client built, seeded from the mirror every client
|
|
16726
|
+
* writes -- so the next run sends what the continuation left. That release
|
|
16727
|
+
* alone makes the getter's fallback give the same answer, and no test can
|
|
16728
|
+
* tell the two apart; reading the continuation keeps the getter right
|
|
16729
|
+
* without depending on when the other client is let go. The setter has no
|
|
16730
|
+
* such fallback, and is held by "reads the shared state it changed while it
|
|
16731
|
+
* runs, not the conversation's" in `ag_ui_chat_checkpoints.test.ts`.
|
|
16732
|
+
*/
|
|
16733
|
+
#liveClient() {
|
|
16734
|
+
return this.#history.continuation ?? this.#client;
|
|
16396
16735
|
}
|
|
16397
16736
|
/** Bind a piece of host page state to `read_<name>` / `set_<name>` tools. */
|
|
16398
16737
|
registerPageState(binding) {
|
|
@@ -16569,6 +16908,11 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16569
16908
|
this.setAttribute("data-tool-display", value);
|
|
16570
16909
|
}
|
|
16571
16910
|
connectedCallback() {
|
|
16911
|
+
this.#connection = new AbortController();
|
|
16912
|
+
if (this.#connectedBefore) {
|
|
16913
|
+
this.#resetConversation();
|
|
16914
|
+
}
|
|
16915
|
+
this.#connectedBefore = true;
|
|
16572
16916
|
this.#storage.claim();
|
|
16573
16917
|
this.#placement.restoreSize();
|
|
16574
16918
|
requestAnimationFrame(() => {
|
|
@@ -16590,13 +16934,13 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16590
16934
|
}
|
|
16591
16935
|
this.#syncLauncher();
|
|
16592
16936
|
this.#skills.init();
|
|
16593
|
-
this.conversationStore = this.#storage.scopeStore(this
|
|
16937
|
+
this.conversationStore = this.#storage.scopeStore(this.#unwrappedStore(), this.userKey);
|
|
16594
16938
|
window.addEventListener("resize", this.#onViewportResize);
|
|
16595
16939
|
window.visualViewport?.addEventListener("resize", this.#onViewportResize);
|
|
16596
16940
|
window.visualViewport?.addEventListener("scroll", this.#onViewportResize);
|
|
16597
16941
|
this.#placement.publishVisualViewport();
|
|
16598
16942
|
this.#wireThreadStore();
|
|
16599
|
-
this.#attachments.wire();
|
|
16943
|
+
this.#attachments.wire(this.#connection.signal);
|
|
16600
16944
|
this.#voice.wire();
|
|
16601
16945
|
this.#history.adoptActiveThread();
|
|
16602
16946
|
queueMicrotask(() => this.#startup());
|
|
@@ -16661,6 +17005,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16661
17005
|
*/
|
|
16662
17006
|
disconnectedCallback() {
|
|
16663
17007
|
this.#connected = false;
|
|
17008
|
+
this.#connection.abort();
|
|
16664
17009
|
window.removeEventListener("resize", this.#onViewportResize);
|
|
16665
17010
|
window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
|
|
16666
17011
|
window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
|
|
@@ -16768,17 +17113,34 @@ var AgUiChat = class extends HTMLElement {
|
|
|
16768
17113
|
* thread id, the navigation checkpoint) keep their local store either way.
|
|
16769
17114
|
*/
|
|
16770
17115
|
#wireThreadStore() {
|
|
17116
|
+
this.#threadStore = null;
|
|
16771
17117
|
const url = this.getAttribute("data-threads-url");
|
|
16772
17118
|
if (url !== null) {
|
|
16773
|
-
this.conversationStore
|
|
17119
|
+
const inner = this.conversationStore;
|
|
17120
|
+
const remote = new RemoteConversationStore(
|
|
16774
17121
|
url,
|
|
16775
17122
|
() => this.#headersFor(url),
|
|
16776
|
-
|
|
17123
|
+
inner,
|
|
16777
17124
|
() => this.#requestCredentials(),
|
|
16778
17125
|
this.getAttribute("data-threads-cache") !== "false"
|
|
16779
17126
|
);
|
|
17127
|
+
this.#threadStore = { remote, inner };
|
|
17128
|
+
this.conversationStore = remote;
|
|
16780
17129
|
}
|
|
16781
17130
|
}
|
|
17131
|
+
/**
|
|
17132
|
+
* The conversation store without the remote this element wrapped it in.
|
|
17133
|
+
*
|
|
17134
|
+
* An element removed and inserted again connects again, and wrapping the
|
|
17135
|
+
* wrapper stacked a second remote on the first, so every rename and delete
|
|
17136
|
+
* reached the server twice. Unwrapping first also lets a `data-threads-url`
|
|
17137
|
+
* removed while detached take the remote away. A store the host assigned
|
|
17138
|
+
* since is theirs, and comes back as it is.
|
|
17139
|
+
*/
|
|
17140
|
+
#unwrappedStore() {
|
|
17141
|
+
const wrapped = this.#threadStore;
|
|
17142
|
+
return wrapped !== null && this.conversationStore === wrapped.remote ? wrapped.inner : this.conversationStore;
|
|
17143
|
+
}
|
|
16782
17144
|
/**
|
|
16783
17145
|
* Replace the host-supplied (client) skill catalog. Merged after the embedded
|
|
16784
17146
|
* and fetched skills (so a client skill overrides a same-named server one).
|
|
@@ -17020,12 +17382,22 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17020
17382
|
}
|
|
17021
17383
|
/** Drop the in-memory run + transcript, leaving the thread id untouched. */
|
|
17022
17384
|
#resetState() {
|
|
17385
|
+
this.#resetConversation();
|
|
17386
|
+
this.#sentDrafts.length = 0;
|
|
17387
|
+
this.#recallIndex = null;
|
|
17388
|
+
}
|
|
17389
|
+
/**
|
|
17390
|
+
* Drop the in-memory run and transcript, and nothing the user typed.
|
|
17391
|
+
*
|
|
17392
|
+
* Split from {@link #resetState} for re-insertion, which rebuilds the
|
|
17393
|
+
* conversation from history but is not a new one, so the composer's recall
|
|
17394
|
+
* history stays.
|
|
17395
|
+
*/
|
|
17396
|
+
#resetConversation() {
|
|
17023
17397
|
this.#client = null;
|
|
17024
17398
|
this.#runHandlers.detach();
|
|
17025
17399
|
this.#clearTranscript();
|
|
17026
17400
|
this.#history.forgetRestored();
|
|
17027
|
-
this.#sentDrafts.length = 0;
|
|
17028
|
-
this.#recallIndex = null;
|
|
17029
17401
|
}
|
|
17030
17402
|
/**
|
|
17031
17403
|
* Wipe the rendered transcript and everything that indexes into it.
|
|
@@ -17091,7 +17463,19 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17091
17463
|
appendMessage(role, content) {
|
|
17092
17464
|
return this.#transcript.append(role, content);
|
|
17093
17465
|
}
|
|
17466
|
+
/**
|
|
17467
|
+
* Build the chrome, from the attributes as they stand.
|
|
17468
|
+
*
|
|
17469
|
+
* Runs on every connect, not once, because removing and re-inserting the
|
|
17470
|
+
* element is the documented way to apply a connect-time attribute written
|
|
17471
|
+
* late. So it has to build the same element the second time as the first:
|
|
17472
|
+
* the elements made here are new each time, the long-lived ones it fills
|
|
17473
|
+
* are filled with `replaceChildren` rather than appended to, and every
|
|
17474
|
+
* listener on a long-lived one goes under the connection's signal. Appending
|
|
17475
|
+
* gave a re-inserted element two headers and two composers.
|
|
17476
|
+
*/
|
|
17094
17477
|
#render() {
|
|
17478
|
+
const { signal } = this.#connection;
|
|
17095
17479
|
this.#chat.className = "chat";
|
|
17096
17480
|
this.#chat.setAttribute("part", "panel");
|
|
17097
17481
|
const header = document.createElement("div");
|
|
@@ -17128,7 +17512,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17128
17512
|
this.#themeToggle.setAttribute("part", "header-button theme-toggle");
|
|
17129
17513
|
this.#themeToggle.title = this.#strings.toggleTheme;
|
|
17130
17514
|
this.#themeToggle.setAttribute("aria-label", this.#strings.toggleTheme);
|
|
17131
|
-
this.#themeToggle.addEventListener("click", () => this.toggleTheme());
|
|
17515
|
+
this.#themeToggle.addEventListener("click", () => this.toggleTheme(), { signal });
|
|
17132
17516
|
this.#syncThemeGlyph();
|
|
17133
17517
|
controls.append(this.#themeToggle);
|
|
17134
17518
|
}
|
|
@@ -17144,8 +17528,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17144
17528
|
this.#jumpButton.type = "button";
|
|
17145
17529
|
this.#jumpButton.setAttribute("part", "jump-latest");
|
|
17146
17530
|
this.#jumpButton.textContent = this.#strings.jumpToLatest;
|
|
17147
|
-
this.#excerpts.mount();
|
|
17148
|
-
this.#transcript.mountScroller(this.#jumpButton);
|
|
17531
|
+
this.#excerpts.mount(signal);
|
|
17532
|
+
this.#transcript.mountScroller(this.#jumpButton, signal);
|
|
17149
17533
|
this.#announcer.mount();
|
|
17150
17534
|
this.#emptyWrap.className = "empty";
|
|
17151
17535
|
this.#emptyWrap.setAttribute("part", "empty");
|
|
@@ -17157,7 +17541,6 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17157
17541
|
greetingSlot.append(this.#greetingText);
|
|
17158
17542
|
greeting.append(greetingSlot);
|
|
17159
17543
|
this.#syncGreeting();
|
|
17160
|
-
this.#emptyWrap.append(greeting);
|
|
17161
17544
|
const emptySlot = document.createElement("slot");
|
|
17162
17545
|
emptySlot.name = "empty";
|
|
17163
17546
|
const starters = renderStarterChips(this, this.#strings, (prompt) => {
|
|
@@ -17166,7 +17549,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17166
17549
|
if (starters !== null) {
|
|
17167
17550
|
emptySlot.append(starters);
|
|
17168
17551
|
}
|
|
17169
|
-
this.#emptyWrap.
|
|
17552
|
+
this.#emptyWrap.replaceChildren(greeting, emptySlot);
|
|
17170
17553
|
this.#queuedRow.className = "queued";
|
|
17171
17554
|
this.#queuedRow.setAttribute("part", "queued");
|
|
17172
17555
|
this.#queuedRow.setAttribute("role", "group");
|
|
@@ -17188,41 +17571,47 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17188
17571
|
this.#input.setAttribute("aria-label", this.#strings.message);
|
|
17189
17572
|
this.#input.rows = 1;
|
|
17190
17573
|
this.#input.placeholder = this.#strings.inputPlaceholder;
|
|
17191
|
-
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
17192
|
-
this.#input.addEventListener("input", () => this.#onInput());
|
|
17574
|
+
this.#input.addEventListener("keydown", (event) => this.#onKeydown(event), { signal });
|
|
17575
|
+
this.#input.addEventListener("input", () => this.#onInput(), { signal });
|
|
17193
17576
|
this.#send.className = "send";
|
|
17194
17577
|
this.#send.type = "button";
|
|
17195
17578
|
this.#send.setAttribute("part", "send");
|
|
17196
|
-
this.#send.
|
|
17579
|
+
this.#send.replaceChildren(
|
|
17197
17580
|
glyphSlot("icon-send", "send-send", ICON_SEND),
|
|
17198
17581
|
glyphSlot("icon-stop", "send-stop", ICON_STOP)
|
|
17199
17582
|
);
|
|
17200
17583
|
this.#send.title = this.#strings.send;
|
|
17201
17584
|
this.#send.setAttribute("aria-label", this.#strings.send);
|
|
17202
17585
|
this.#send.dataset["state"] = "idle";
|
|
17203
|
-
this.#send.addEventListener(
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17586
|
+
this.#send.addEventListener(
|
|
17587
|
+
"click",
|
|
17588
|
+
() => {
|
|
17589
|
+
if (this.#running) {
|
|
17590
|
+
this.#cancelRun();
|
|
17591
|
+
return;
|
|
17592
|
+
}
|
|
17593
|
+
void this.#submit();
|
|
17594
|
+
},
|
|
17595
|
+
{ signal }
|
|
17596
|
+
);
|
|
17210
17597
|
this.#composerHint.className = "skill-hint";
|
|
17211
17598
|
this.#composerHint.setAttribute("part", "skill-hint");
|
|
17212
17599
|
this.#composerHint.hidden = true;
|
|
17213
17600
|
this.#attachButton.className = "attach-btn";
|
|
17214
17601
|
this.#attachButton.type = "button";
|
|
17215
17602
|
this.#attachButton.setAttribute("part", "attach-button");
|
|
17216
|
-
this.#attachButton.
|
|
17603
|
+
this.#attachButton.replaceChildren(glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
|
|
17217
17604
|
this.#attachButton.title = this.#strings.attachFiles;
|
|
17218
17605
|
this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
|
|
17219
17606
|
this.#attachButton.hidden = true;
|
|
17220
|
-
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
17607
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click(), { signal });
|
|
17221
17608
|
this.#fileInput.className = "attach-input";
|
|
17222
17609
|
this.#fileInput.type = "file";
|
|
17223
17610
|
this.#fileInput.multiple = true;
|
|
17224
17611
|
this.#fileInput.hidden = true;
|
|
17225
|
-
this.#fileInput.addEventListener("change", () => this.#attachments.onFilesPicked()
|
|
17612
|
+
this.#fileInput.addEventListener("change", () => this.#attachments.onFilesPicked(), {
|
|
17613
|
+
signal
|
|
17614
|
+
});
|
|
17226
17615
|
this.#attachSlot.className = "attachment-slot";
|
|
17227
17616
|
this.#voiceSlot.className = "voice-slot";
|
|
17228
17617
|
const footer = document.createElement("slot");
|
|
@@ -17231,8 +17620,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17231
17620
|
composer.append(this.#input, tools);
|
|
17232
17621
|
inputRow.append(composer, this.#fileInput);
|
|
17233
17622
|
this.#messagesWrap.className = "messages-wrap";
|
|
17234
|
-
this.#messagesWrap.
|
|
17235
|
-
this.#chat.
|
|
17623
|
+
this.#messagesWrap.replaceChildren(this.#messages, this.#jumpButton, this.#excerpts.button);
|
|
17624
|
+
this.#chat.replaceChildren(
|
|
17236
17625
|
header,
|
|
17237
17626
|
this.#messagesWrap,
|
|
17238
17627
|
this.#skillsMenu.palette,
|
|
@@ -17245,16 +17634,20 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17245
17634
|
this.#drawer.element,
|
|
17246
17635
|
this.#checkpoints.element
|
|
17247
17636
|
);
|
|
17248
|
-
this.#chat.addEventListener(
|
|
17249
|
-
|
|
17250
|
-
|
|
17251
|
-
|
|
17252
|
-
|
|
17253
|
-
|
|
17254
|
-
|
|
17255
|
-
|
|
17256
|
-
|
|
17257
|
-
|
|
17637
|
+
this.#chat.addEventListener(
|
|
17638
|
+
"pointerdown",
|
|
17639
|
+
(event) => {
|
|
17640
|
+
if (!this.#checkpoints.open_) {
|
|
17641
|
+
return;
|
|
17642
|
+
}
|
|
17643
|
+
const path = event.composedPath();
|
|
17644
|
+
if (path.includes(this.#checkpoints.element) || path.includes(checkpoints)) {
|
|
17645
|
+
return;
|
|
17646
|
+
}
|
|
17647
|
+
this.#checkpoints.close();
|
|
17648
|
+
},
|
|
17649
|
+
{ signal }
|
|
17650
|
+
);
|
|
17258
17651
|
this.#launcher.className = "launcher";
|
|
17259
17652
|
this.#launcher.type = "button";
|
|
17260
17653
|
this.#launcher.setAttribute("part", "launcher");
|
|
@@ -17267,19 +17660,19 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17267
17660
|
this.#railLabel.setAttribute("part", "rail-label");
|
|
17268
17661
|
this.#railLabel.setAttribute("aria-hidden", "true");
|
|
17269
17662
|
this.#railLabel.textContent = this.getAttribute("title-text") ?? this.#strings.title;
|
|
17270
|
-
this.#launcher.
|
|
17663
|
+
this.#launcher.replaceChildren(
|
|
17271
17664
|
iconElement("launcher", "launcher-icon", ICON_LAUNCHER, readLauncherIconUrl(this)),
|
|
17272
17665
|
this.#railLabel,
|
|
17273
17666
|
this.#badge
|
|
17274
17667
|
);
|
|
17275
|
-
this.#launcher.addEventListener("click", () => this.setCollapsed(false));
|
|
17276
|
-
this.#placement.enableLauncherDrag();
|
|
17668
|
+
this.#launcher.addEventListener("click", () => this.setCollapsed(false), { signal });
|
|
17669
|
+
this.#placement.enableLauncherDrag(signal);
|
|
17277
17670
|
this.#placement.mountResizeGrips(this.#chat);
|
|
17278
17671
|
adoptStyles(this.#root);
|
|
17279
17672
|
const probe = this.#placement.probe;
|
|
17280
17673
|
probe.className = "viewport-probe";
|
|
17281
17674
|
probe.setAttribute("aria-hidden", "true");
|
|
17282
|
-
this.#root.
|
|
17675
|
+
this.#root.replaceChildren(probe, this.#announcer.region, this.#chat, this.#launcher);
|
|
17283
17676
|
}
|
|
17284
17677
|
/**
|
|
17285
17678
|
* Reflect the collapsed state and the unread count on the launcher.
|
|
@@ -17434,6 +17827,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17434
17827
|
* other shape would be a second sender racing the guard above.
|
|
17435
17828
|
*/
|
|
17436
17829
|
#flushQueued() {
|
|
17830
|
+
if (this.#running || this.#history.continuation !== null) {
|
|
17831
|
+
return;
|
|
17832
|
+
}
|
|
17437
17833
|
const next = this.#queued.shift();
|
|
17438
17834
|
this.#renderQueued();
|
|
17439
17835
|
if (next !== void 0) {
|
|
@@ -17471,7 +17867,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17471
17867
|
if (content === "" && attachments.length === 0) {
|
|
17472
17868
|
return;
|
|
17473
17869
|
}
|
|
17474
|
-
if (this.#running) {
|
|
17870
|
+
if (this.#running || this.#history.continuation !== null) {
|
|
17475
17871
|
if (content !== "") {
|
|
17476
17872
|
this.#queued.push(content);
|
|
17477
17873
|
this.#renderQueued();
|
|
@@ -17509,12 +17905,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17509
17905
|
* `attachments` are durable {@link AttachmentRef}s — what {@link attachFile}
|
|
17510
17906
|
* resolves to and what {@link ATTACHMENT_EVENT} reports.
|
|
17511
17907
|
*
|
|
17512
|
-
* No-ops on an empty message, and while a run
|
|
17513
|
-
*
|
|
17514
|
-
*
|
|
17908
|
+
* No-ops on an empty message, and while a run or a picked checkpoint's
|
|
17909
|
+
* continuation is in flight, since a second concurrent run would orphan the
|
|
17910
|
+
* first. Unlike the built-in Send it does not queue: it returns, and the
|
|
17911
|
+
* caller keeps what it tried to send. Nor does it consult the tray -- what
|
|
17912
|
+
* you pass is what is sent.
|
|
17515
17913
|
*/
|
|
17516
17914
|
async sendMessage(content, attachments = []) {
|
|
17517
|
-
if (this.#running || content === "" && attachments.length === 0) {
|
|
17915
|
+
if (this.#running || this.#history.continuation !== null || content === "" && attachments.length === 0) {
|
|
17518
17916
|
return;
|
|
17519
17917
|
}
|
|
17520
17918
|
if (this.#transcript.isEmpty()) {
|
|
@@ -17587,7 +17985,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17587
17985
|
this.#client = this.#buildClient({
|
|
17588
17986
|
endpoint: this.endpoint,
|
|
17589
17987
|
initialMessages: this.#history.restored,
|
|
17590
|
-
|
|
17988
|
+
// Its history is the whole conversation, so nothing goes ahead of it.
|
|
17989
|
+
follows: []
|
|
17591
17990
|
});
|
|
17592
17991
|
}
|
|
17593
17992
|
return this.#client;
|
|
@@ -17624,9 +18023,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
17624
18023
|
getContext: () => this.#dispatch.buildContext(),
|
|
17625
18024
|
executeTool: (call) => this.#dispatch.execute(call),
|
|
17626
18025
|
resolveInterrupts: (interrupts) => this.#dispatch.resolveInterrupts(interrupts),
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
18026
|
+
// Every client saves the whole conversation, because a store keeps one
|
|
18027
|
+
// list per thread. For the conversation's own client that is its
|
|
18028
|
+
// history; a continuation holds only what it adds, and writes the
|
|
18029
|
+
// conversation it continues ahead of that.
|
|
18030
|
+
onPersist: (messages) => {
|
|
18031
|
+
const conversation = [...seed.follows, ...messages];
|
|
18032
|
+
this.conversationStore.saveMessages(threadId, conversation);
|
|
18033
|
+
seed.onSaved?.(conversation);
|
|
18034
|
+
},
|
|
17630
18035
|
onStateChanged: (state) => this.#onSharedStateChanged(state),
|
|
17631
18036
|
connectionLostMessage: this.#strings.connectionLost,
|
|
17632
18037
|
unfinishedMessage: this.#strings.callNotFinished,
|
|
@@ -17789,7 +18194,7 @@ function setControlValue(el2, value) {
|
|
|
17789
18194
|
}
|
|
17790
18195
|
|
|
17791
18196
|
// src/version.ts
|
|
17792
|
-
var VERSION = "0.
|
|
18197
|
+
var VERSION = "0.40.0";
|
|
17793
18198
|
export {
|
|
17794
18199
|
ATTACHMENT_EVENT,
|
|
17795
18200
|
AgUiChat,
|