@kodaris/krubble-app-components 1.0.112 → 1.0.114
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/dist/chatbot.d.ts +247 -7
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +1594 -396
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +1597 -400
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +925 -409
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +1594 -397
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +905 -389
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/krubble-app.umd.js
CHANGED
|
@@ -5531,6 +5531,47 @@
|
|
|
5531
5531
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5532
5532
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
5533
5533
|
};
|
|
5534
|
+
/**
|
|
5535
|
+
* The action-confirmation decisions, in the left-to-right order they render and
|
|
5536
|
+
* the order the arrow keys walk. Yes is first so it's the default highlight.
|
|
5537
|
+
* Shared by the render and the keyboard handler so the two never drift.
|
|
5538
|
+
*/
|
|
5539
|
+
const CONFIRMATION_DECISIONS = [
|
|
5540
|
+
{
|
|
5541
|
+
decision: 'approve',
|
|
5542
|
+
label: 'Yes',
|
|
5543
|
+
},
|
|
5544
|
+
{
|
|
5545
|
+
decision: 'approveAlways',
|
|
5546
|
+
label: 'Always allow',
|
|
5547
|
+
},
|
|
5548
|
+
{
|
|
5549
|
+
decision: 'reject',
|
|
5550
|
+
label: 'No',
|
|
5551
|
+
},
|
|
5552
|
+
];
|
|
5553
|
+
/**
|
|
5554
|
+
* The modes as shown in the composer's mode selector, in menu order. `label` is
|
|
5555
|
+
* the short trigger text (Gemini-style inline label); `description` is the menu
|
|
5556
|
+
* sublabel. Written for a non-technical user — no "guarded tool" jargon.
|
|
5557
|
+
*/
|
|
5558
|
+
const CHATBOT_MODES = [
|
|
5559
|
+
{
|
|
5560
|
+
mode: 'NORMAL',
|
|
5561
|
+
label: 'Ask',
|
|
5562
|
+
description: 'Asks before making changes',
|
|
5563
|
+
},
|
|
5564
|
+
{
|
|
5565
|
+
mode: 'EDIT',
|
|
5566
|
+
label: 'Auto',
|
|
5567
|
+
description: 'Makes changes automatically',
|
|
5568
|
+
},
|
|
5569
|
+
{
|
|
5570
|
+
mode: 'PLAN',
|
|
5571
|
+
label: 'Plan',
|
|
5572
|
+
description: 'Suggests a plan, makes no changes',
|
|
5573
|
+
},
|
|
5574
|
+
];
|
|
5534
5575
|
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
5535
5576
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
5536
5577
|
/**
|
|
@@ -5565,9 +5606,20 @@
|
|
|
5565
5606
|
* - `TOOL_RESULT` — `{ type: "TOOL_RESULT", message: "tool_name", data: {...} }`
|
|
5566
5607
|
* - `ERROR` — `{ type: "ERROR", message: "Something went wrong" }`
|
|
5567
5608
|
* - `HEARTBEAT` — `{ type: "HEARTBEAT" }` (ignored)
|
|
5609
|
+
* - `ACTION_CONFIRMATION_REQUIRED` — a guarded tool paused for approval;
|
|
5610
|
+
* `{ type, data: { actions: [...] }, finished: true }` (Rail 3).
|
|
5568
5611
|
*
|
|
5569
5612
|
* The final event should include `finished: true` to signal stream completion.
|
|
5570
5613
|
*
|
|
5614
|
+
* ## Turn locking (when the composer is free to send)
|
|
5615
|
+
* `finished: true` is the single reliable "turn over, safe to send" signal — the
|
|
5616
|
+
* component unlocks the composer on it, and also on stream close/error as a
|
|
5617
|
+
* fallback so a dropped stream never wedges the UI. The one exception is
|
|
5618
|
+
* `ACTION_CONFIRMATION_REQUIRED`: it too carries `finished: true`, but the
|
|
5619
|
+
* conversation is now awaiting an approve/deny decision, so the composer stays
|
|
5620
|
+
* blocked and only a confirmation decision (not a new message) is a valid next
|
|
5621
|
+
* call.
|
|
5622
|
+
*
|
|
5571
5623
|
* ## Usage
|
|
5572
5624
|
* ```html
|
|
5573
5625
|
* <kr-chatbot
|
|
@@ -5594,6 +5646,25 @@
|
|
|
5594
5646
|
constructor() {
|
|
5595
5647
|
super(...arguments);
|
|
5596
5648
|
// --- Private properties ---
|
|
5649
|
+
/**
|
|
5650
|
+
* Internal toggle for how the action-confirmation prompt (Rail 3) is presented:
|
|
5651
|
+
* - `false`: the compact card that floats over the messages, pinned above the
|
|
5652
|
+
* composer — plain-language ask + Yes/Always/No only, no tool arguments.
|
|
5653
|
+
* - `true`: the prompt renders inline in the message stream (Claude-CLI style),
|
|
5654
|
+
* grows to any height (no inner scroll — the transcript scrolls), and offers a
|
|
5655
|
+
* "Show details" disclosure of the raw tool input for technical users.
|
|
5656
|
+
* Flip here to switch approaches; the floating path is unchanged when false.
|
|
5657
|
+
*/
|
|
5658
|
+
this._inlineConfirm = true;
|
|
5659
|
+
/**
|
|
5660
|
+
* Internal toggle for how the assistant's reasoning ("thinking") is presented:
|
|
5661
|
+
* - `false`: the current collapsible grey "Thinking" disclosure (kept so we can
|
|
5662
|
+
* revert).
|
|
5663
|
+
* - `true`: reasoning renders as a plain agentic message — same text style as a
|
|
5664
|
+
* normal assistant reply, no box, no collapse.
|
|
5665
|
+
* Flip here to switch approaches.
|
|
5666
|
+
*/
|
|
5667
|
+
this._reasoningAsMessage = true;
|
|
5597
5668
|
this._reader = null;
|
|
5598
5669
|
this._decoder = new TextDecoder();
|
|
5599
5670
|
this._buffer = '';
|
|
@@ -5737,6 +5808,32 @@
|
|
|
5737
5808
|
this._contextLimitReached = false;
|
|
5738
5809
|
/** Server-provided limit text (`CONVERSATION_LIMIT_REACHED`), if any. */
|
|
5739
5810
|
this._contextLimitMessage = '';
|
|
5811
|
+
/**
|
|
5812
|
+
* The guarded tool awaiting the user's decision, from the turn's
|
|
5813
|
+
* `ACTION_CONFIRMATION_REQUIRED` event (`data.actions[]`, of which we handle the
|
|
5814
|
+
* first — the framework confirms one guarded tool at a time). Non-empty means
|
|
5815
|
+
* the conversation is paused on a confirmation: the normal composer is blocked
|
|
5816
|
+
* (a new chat message would be rejected by the server with
|
|
5817
|
+
* `awaitingConfirmation`) and the approve/deny prompt is shown instead. Cleared
|
|
5818
|
+
* once a decision is sent.
|
|
5819
|
+
*
|
|
5820
|
+
* This is the one case where `finished:true` unlocks the turn lock but does
|
|
5821
|
+
* NOT free the composer — the only valid next call is a confirmation decision.
|
|
5822
|
+
*/
|
|
5823
|
+
this._pendingActions = [];
|
|
5824
|
+
/**
|
|
5825
|
+
* Keyboard-highlighted confirmation option, as an index into the decision list
|
|
5826
|
+
* (0 = Yes, 1 = Always allow, 2 = No). Yes is highlighted by default so Enter
|
|
5827
|
+
* approves. Arrow Left/Right move it; Enter activates it. -1 when no batch is
|
|
5828
|
+
* pending.
|
|
5829
|
+
*/
|
|
5830
|
+
this._confirmationHighlighted = -1;
|
|
5831
|
+
/**
|
|
5832
|
+
* True while a confirmation decision (approve/deny) is being sent and its
|
|
5833
|
+
* follow-up turn streams. Disables the approve/deny buttons so the batch can't
|
|
5834
|
+
* be resolved twice.
|
|
5835
|
+
*/
|
|
5836
|
+
this._confirmationInFlight = false;
|
|
5740
5837
|
/**
|
|
5741
5838
|
* INTERNAL-ONLY latest telemetry snapshot (last-value-wins). Populated from
|
|
5742
5839
|
* `TOKEN_USAGE` events during streaming and hydrated from the conversation
|
|
@@ -5751,11 +5848,20 @@
|
|
|
5751
5848
|
this._iterationLog = [];
|
|
5752
5849
|
/** Latest cache-checkpoint info keyed by iteration, for the verbose table. */
|
|
5753
5850
|
this._cacheCheckpoints = {};
|
|
5754
|
-
/**
|
|
5755
|
-
|
|
5756
|
-
|
|
5851
|
+
/**
|
|
5852
|
+
* Whether the side drawer is open (slid in). The drawer stays mounted and
|
|
5853
|
+
* animates via a CSS transform, so this drives the slide and the content push.
|
|
5854
|
+
*/
|
|
5855
|
+
this._drawerOpen = false;
|
|
5856
|
+
/**
|
|
5857
|
+
* Which view the drawer shows — settings or debug. One drawer holds both; the
|
|
5858
|
+
* header buttons switch this and the content/width swap inside. Never null, so
|
|
5859
|
+
* the content stays visible while the drawer slides shut.
|
|
5860
|
+
*/
|
|
5861
|
+
this._drawerView = 'settings';
|
|
5862
|
+
/** Current width of the drawer in px (user-resizable, persisted). */
|
|
5757
5863
|
this._debugDrawerWidth = 430;
|
|
5758
|
-
/** True while the user is dragging the
|
|
5864
|
+
/** True while the user is dragging the drawer's resize handle. */
|
|
5759
5865
|
this._isDebugResizing = false;
|
|
5760
5866
|
/** Files staged in the composer, before the message is sent. */
|
|
5761
5867
|
this._pendingConversationFiles = [];
|
|
@@ -5806,6 +5912,31 @@
|
|
|
5806
5912
|
this._skillBrowseQuery = '';
|
|
5807
5913
|
/** True while the mic is live and speech is being transcribed into the composer. */
|
|
5808
5914
|
this._isDictating = false;
|
|
5915
|
+
/**
|
|
5916
|
+
* Conversation posture for guarded tools (the composer's mode selector). Sent on
|
|
5917
|
+
* each turn's `mode` field and kept in sync with the server's `CONVERSATION_STATE`
|
|
5918
|
+
* event. Defaults to NORMAL (ask before each guarded tool).
|
|
5919
|
+
*/
|
|
5920
|
+
this._mode = 'NORMAL';
|
|
5921
|
+
/** Whether the composer's mode menu (Ask / Auto / Plan) is open. */
|
|
5922
|
+
this._modeMenuOpened = false;
|
|
5923
|
+
/**
|
|
5924
|
+
* Whether to show the tools the assistant ran, as activity lines in the
|
|
5925
|
+
* transcript. On by default; a user can hide them from the settings panel.
|
|
5926
|
+
* User-controlled in the settings panel; persisted in localStorage.
|
|
5927
|
+
*/
|
|
5928
|
+
this._showTools = true;
|
|
5929
|
+
/**
|
|
5930
|
+
* Whether to also show each tool's inputs and results under its line. Only
|
|
5931
|
+
* has an effect when `_showTools` is on. Off by default.
|
|
5932
|
+
*/
|
|
5933
|
+
this._showToolInputs = false;
|
|
5934
|
+
/**
|
|
5935
|
+
* Whether to show the raw tool input inside the confirmation card (the JSON the
|
|
5936
|
+
* model proposed for a guarded action). The plain-language ask + Yes/No always
|
|
5937
|
+
* show; this only reveals the technical detail. Off by default.
|
|
5938
|
+
*/
|
|
5939
|
+
this._showConfirmationInput = false;
|
|
5809
5940
|
/**
|
|
5810
5941
|
* Close the `/` skill menu when the click lands outside it.
|
|
5811
5942
|
*
|
|
@@ -5816,6 +5947,14 @@
|
|
|
5816
5947
|
* inside click would look like an outside one.
|
|
5817
5948
|
*/
|
|
5818
5949
|
this._handleDocumentClick = (e) => {
|
|
5950
|
+
// Close the mode menu on any click outside the mode selector (the trigger
|
|
5951
|
+
// owns its own toggle, so exclude the whole `.mode` block).
|
|
5952
|
+
if (this._modeMenuOpened) {
|
|
5953
|
+
const modePath = e.composedPath();
|
|
5954
|
+
if (!modePath.some((el) => el instanceof HTMLElement && el.classList.contains('mode'))) {
|
|
5955
|
+
this._modeMenuOpened = false;
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5819
5958
|
if (!this._skillMenuOpened) {
|
|
5820
5959
|
return;
|
|
5821
5960
|
}
|
|
@@ -5890,6 +6029,36 @@
|
|
|
5890
6029
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
5891
6030
|
this._saveState();
|
|
5892
6031
|
};
|
|
6032
|
+
/**
|
|
6033
|
+
* Keyboard control for the confirmation prompt. Bound at the document so it
|
|
6034
|
+
* works even though the composer (where focus usually sits) is blocked while a
|
|
6035
|
+
* batch is pending: Left/Right move the highlight across Yes / Always allow /
|
|
6036
|
+
* No, Enter activates the highlighted one. No-op when nothing is pending or a
|
|
6037
|
+
* decision is already in flight.
|
|
6038
|
+
*/
|
|
6039
|
+
this._handleDocumentKeydown = (e) => {
|
|
6040
|
+
if (!this._pendingActions.length || this._confirmationInFlight) {
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
if (e.key === 'ArrowRight') {
|
|
6044
|
+
e.preventDefault();
|
|
6045
|
+
this._confirmationHighlighted = (this._confirmationHighlighted + 1) % CONFIRMATION_DECISIONS.length;
|
|
6046
|
+
return;
|
|
6047
|
+
}
|
|
6048
|
+
if (e.key === 'ArrowLeft') {
|
|
6049
|
+
e.preventDefault();
|
|
6050
|
+
this._confirmationHighlighted =
|
|
6051
|
+
(this._confirmationHighlighted - 1 + CONFIRMATION_DECISIONS.length) % CONFIRMATION_DECISIONS.length;
|
|
6052
|
+
return;
|
|
6053
|
+
}
|
|
6054
|
+
if (e.key === 'Enter') {
|
|
6055
|
+
e.preventDefault();
|
|
6056
|
+
const option = CONFIRMATION_DECISIONS[this._confirmationHighlighted];
|
|
6057
|
+
if (option) {
|
|
6058
|
+
this._handleConfirmationDecision(option.decision);
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
};
|
|
5893
6062
|
this._handleDebugResizeMouseMove = (e) => {
|
|
5894
6063
|
if (!this._isDebugResizing) {
|
|
5895
6064
|
return;
|
|
@@ -5927,6 +6096,36 @@
|
|
|
5927
6096
|
return krubbleHttp.Http.fetch({ url: `/api/system/conversation/${this.conversationId}/messages` })
|
|
5928
6097
|
.then((r) => r.json())
|
|
5929
6098
|
.then((res) => Promise.all((res?.data || []).map((msg) => {
|
|
6099
|
+
// Persisted tool_use / tool_result rows — the message IS the row: keep
|
|
6100
|
+
// the type and parse content into `data`, the same {type, data} shape
|
|
6101
|
+
// the live stream pushes. The render reads data.* and pairs a result to
|
|
6102
|
+
// its use by data.toolUseId. No transform, so live and reload match.
|
|
6103
|
+
if (msg.type === 'tool_use' || msg.type === 'tool_result') {
|
|
6104
|
+
let data = {};
|
|
6105
|
+
try {
|
|
6106
|
+
data = JSON.parse(msg.content || '{}');
|
|
6107
|
+
}
|
|
6108
|
+
catch { /* malformed — render a bare line from what we have */ }
|
|
6109
|
+
return Promise.resolve({
|
|
6110
|
+
type: msg.type,
|
|
6111
|
+
content: '',
|
|
6112
|
+
data,
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
// A persisted approve/deny record — content is {outcome, tools:[...]}.
|
|
6116
|
+
// Keep the type, parse content into `data`; the render reads msg.data.*.
|
|
6117
|
+
if (msg.type === 'action_decision') {
|
|
6118
|
+
let data = {};
|
|
6119
|
+
try {
|
|
6120
|
+
data = JSON.parse(msg.content || '{}');
|
|
6121
|
+
}
|
|
6122
|
+
catch { /* malformed — render a bare line from what we have */ }
|
|
6123
|
+
return Promise.resolve({
|
|
6124
|
+
type: 'action_decision',
|
|
6125
|
+
content: '',
|
|
6126
|
+
data,
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
5930
6129
|
// Resolve this message's `conversationFileIDs` (a JSON array string of
|
|
5931
6130
|
// ids) into full file objects — each fetch response IS the
|
|
5932
6131
|
// KRChatbotConversationFile (id + niceName + mimeType), and niceName
|
|
@@ -5975,6 +6174,9 @@
|
|
|
5975
6174
|
}
|
|
5976
6175
|
this._restoreState();
|
|
5977
6176
|
document.addEventListener('click', this._handleDocumentClick);
|
|
6177
|
+
// Drive the confirmation prompt from the keyboard even when focus isn't in the
|
|
6178
|
+
// (blocked) composer: Left/Right move the highlight, Enter activates it.
|
|
6179
|
+
document.addEventListener('keydown', this._handleDocumentKeydown);
|
|
5978
6180
|
}
|
|
5979
6181
|
disconnectedCallback() {
|
|
5980
6182
|
super.disconnectedCallback();
|
|
@@ -5985,6 +6187,7 @@
|
|
|
5985
6187
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
5986
6188
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
5987
6189
|
document.removeEventListener('click', this._handleDocumentClick);
|
|
6190
|
+
document.removeEventListener('keydown', this._handleDocumentKeydown);
|
|
5988
6191
|
// Release the mic if the chatbot is torn down mid-dictation — otherwise the
|
|
5989
6192
|
// recognizer keeps the browser's recording indicator lit with nothing to
|
|
5990
6193
|
// transcribe into. abort() (not stop()) since the discarded text has nowhere to go.
|
|
@@ -6177,7 +6380,12 @@
|
|
|
6177
6380
|
const state = {
|
|
6178
6381
|
opened: String(this.opened),
|
|
6179
6382
|
message: this._inputValue,
|
|
6180
|
-
|
|
6383
|
+
mode: this._mode,
|
|
6384
|
+
showTools: String(this._showTools),
|
|
6385
|
+
showToolInputs: String(this._showToolInputs),
|
|
6386
|
+
showConfirmationInput: String(this._showConfirmationInput),
|
|
6387
|
+
drawerOpen: String(this._drawerOpen),
|
|
6388
|
+
drawerView: this._drawerView,
|
|
6181
6389
|
debugDrawerWidth: String(this._debugDrawerWidth),
|
|
6182
6390
|
};
|
|
6183
6391
|
if (this._panelEl) {
|
|
@@ -6218,7 +6426,16 @@
|
|
|
6218
6426
|
if (typeof state.message === 'string' && state.message.trim() !== '/') {
|
|
6219
6427
|
this._inputValue = state.message;
|
|
6220
6428
|
}
|
|
6221
|
-
|
|
6429
|
+
if (state.mode === 'NORMAL' || state.mode === 'EDIT' || state.mode === 'PLAN') {
|
|
6430
|
+
this._mode = state.mode;
|
|
6431
|
+
}
|
|
6432
|
+
this._showTools = state.showTools === 'true';
|
|
6433
|
+
this._showToolInputs = state.showToolInputs === 'true';
|
|
6434
|
+
this._showConfirmationInput = state.showConfirmationInput === 'true';
|
|
6435
|
+
this._drawerOpen = state.drawerOpen === 'true';
|
|
6436
|
+
if (state.drawerView === 'settings' || state.drawerView === 'debug') {
|
|
6437
|
+
this._drawerView = state.drawerView;
|
|
6438
|
+
}
|
|
6222
6439
|
const w = Number(state.debugDrawerWidth);
|
|
6223
6440
|
if (w >= 300) {
|
|
6224
6441
|
this._debugDrawerWidth = w;
|
|
@@ -6521,6 +6738,29 @@
|
|
|
6521
6738
|
this._skillMenuOpened = true;
|
|
6522
6739
|
this._searchInvocableSkills('');
|
|
6523
6740
|
}
|
|
6741
|
+
_handleModeToggle() {
|
|
6742
|
+
this._modeMenuOpened = !this._modeMenuOpened;
|
|
6743
|
+
}
|
|
6744
|
+
_handleModeSelect(mode) {
|
|
6745
|
+
this._mode = mode;
|
|
6746
|
+
this._modeMenuOpened = false;
|
|
6747
|
+
// Persist the choice so it survives a reload, alongside the other composer
|
|
6748
|
+
// state. The server is still the source of truth on the next turn's
|
|
6749
|
+
// CONVERSATION_STATE, but this keeps the selector from flashing the default.
|
|
6750
|
+
this._saveState();
|
|
6751
|
+
}
|
|
6752
|
+
_handleShowToolsChange(e) {
|
|
6753
|
+
this._showTools = e.target.checked;
|
|
6754
|
+
this._saveState();
|
|
6755
|
+
}
|
|
6756
|
+
_handleShowToolInputsChange(e) {
|
|
6757
|
+
this._showToolInputs = e.target.checked;
|
|
6758
|
+
this._saveState();
|
|
6759
|
+
}
|
|
6760
|
+
_handleShowConfirmationInputChange(e) {
|
|
6761
|
+
this._showConfirmationInput = e.target.checked;
|
|
6762
|
+
this._saveState();
|
|
6763
|
+
}
|
|
6524
6764
|
/**
|
|
6525
6765
|
* The mic button toggles dictation. Speech is transcribed straight into the
|
|
6526
6766
|
* composer as editable text — the user still reviews and presses send, so a
|
|
@@ -6859,6 +7099,13 @@
|
|
|
6859
7099
|
if (this._isStreaming) {
|
|
6860
7100
|
return;
|
|
6861
7101
|
}
|
|
7102
|
+
// A guarded-tool batch is awaiting a decision. The turn lock released with
|
|
7103
|
+
// its finished:true, but the server rejects a normal chat message with
|
|
7104
|
+
// awaitingConfirmation until it's resolved — so the composer stays blocked
|
|
7105
|
+
// and the approve/deny prompt owns the next call.
|
|
7106
|
+
if (this._pendingActions.length) {
|
|
7107
|
+
return;
|
|
7108
|
+
}
|
|
6862
7109
|
const messageText = this._inputValue.trim();
|
|
6863
7110
|
const pending = this._pendingConversationFiles;
|
|
6864
7111
|
// Allow sending an image with no text, but require at least one of them.
|
|
@@ -6940,22 +7187,25 @@
|
|
|
6940
7187
|
content: '',
|
|
6941
7188
|
}];
|
|
6942
7189
|
this._isStreaming = true;
|
|
6943
|
-
// Scroll user's message to the top (Gemini-style).
|
|
6944
|
-
//
|
|
6945
|
-
//
|
|
7190
|
+
// Scroll user's message to the top (Gemini-style). The runway (min-height =
|
|
7191
|
+
// viewport) goes on the agent-turn WRAPPER, not an individual message — the
|
|
7192
|
+
// whole turn (reasoning, tool lines, text, decision) streams inside that one
|
|
7193
|
+
// element, so nothing strands below it. Enough scroll room below the user
|
|
7194
|
+
// message to push it to the top.
|
|
6946
7195
|
this.updateComplete.then(() => {
|
|
6947
7196
|
if (!this._messagesEl) {
|
|
6948
7197
|
return;
|
|
6949
7198
|
}
|
|
6950
|
-
// Clear
|
|
6951
|
-
this._messagesEl.querySelectorAll('.
|
|
7199
|
+
// Clear runway from previous turns.
|
|
7200
|
+
this._messagesEl.querySelectorAll('.agent-turn').forEach((el) => {
|
|
6952
7201
|
el.style.minHeight = '';
|
|
6953
7202
|
});
|
|
6954
|
-
const
|
|
6955
|
-
const userMsgEl =
|
|
6956
|
-
const
|
|
6957
|
-
|
|
6958
|
-
|
|
7203
|
+
const userEls = this._messagesEl.querySelectorAll('.message--user');
|
|
7204
|
+
const userMsgEl = userEls[userEls.length - 1];
|
|
7205
|
+
const turnEls = this._messagesEl.querySelectorAll('.agent-turn');
|
|
7206
|
+
const turnEl = turnEls[turnEls.length - 1];
|
|
7207
|
+
if (turnEl) {
|
|
7208
|
+
turnEl.style.minHeight = this._messagesEl.clientHeight + 'px';
|
|
6959
7209
|
}
|
|
6960
7210
|
if (userMsgEl) {
|
|
6961
7211
|
this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
|
|
@@ -6965,6 +7215,22 @@
|
|
|
6965
7215
|
// the consumer hasn't provided their own `send`.
|
|
6966
7216
|
if (!this.send) {
|
|
6967
7217
|
this.send = (params) => {
|
|
7218
|
+
// Rail-3 confirmation decision: resolve a paused guarded-tool batch by
|
|
7219
|
+
// hitting the SAME streaming endpoint with `confirmation` set. There's no
|
|
7220
|
+
// userText/files on this path (the server ignores message text here), and
|
|
7221
|
+
// the conversation is always already pinned (a batch can't pause before
|
|
7222
|
+
// the conversation exists), so no upload and no createNew.
|
|
7223
|
+
if (params.confirmation) {
|
|
7224
|
+
return krubbleHttp.Http.fetch({
|
|
7225
|
+
url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
|
|
7226
|
+
method: 'POST',
|
|
7227
|
+
headers: { Accept: 'text/event-stream' },
|
|
7228
|
+
body: {
|
|
7229
|
+
confirmation: params.confirmation,
|
|
7230
|
+
conversationID: this.conversationId,
|
|
7231
|
+
},
|
|
7232
|
+
});
|
|
7233
|
+
}
|
|
6968
7234
|
// Images are upload-first: each file is stored as a conversation file
|
|
6969
7235
|
// (type LIBRARY — the format lives in mimeType), then the resulting
|
|
6970
7236
|
// conversationFileIDs are attached to the message via `fileIds` on the
|
|
@@ -7021,6 +7287,9 @@
|
|
|
7021
7287
|
const body = {
|
|
7022
7288
|
userText: params.message,
|
|
7023
7289
|
fileIds: files.map((f) => f.conversationFileID),
|
|
7290
|
+
// Posture for guarded tools this turn (Ask / Auto / Plan). The server
|
|
7291
|
+
// validates and persists it, and only re-sets it when it changes.
|
|
7292
|
+
mode: this._mode,
|
|
7024
7293
|
};
|
|
7025
7294
|
// Ground the model in where the user is and what they've highlighted.
|
|
7026
7295
|
// Only sent when present so an empty selection / failed capture doesn't
|
|
@@ -7104,6 +7373,67 @@
|
|
|
7104
7373
|
}
|
|
7105
7374
|
});
|
|
7106
7375
|
}
|
|
7376
|
+
/**
|
|
7377
|
+
* Resolve the paused guarded-tool batch (Rail 3). Hits the SAME streaming
|
|
7378
|
+
* endpoint as a normal turn but with a `confirmation` value instead of a
|
|
7379
|
+
* message; the server runs (approve/approveAlways) or denies (reject) the batch
|
|
7380
|
+
* and continues the turn, streaming the outcome into a fresh assistant bubble.
|
|
7381
|
+
*
|
|
7382
|
+
* The decision uses the same `send` callback so a custom `send` can own its own
|
|
7383
|
+
* targeting — it just receives `confirmation` set and no message.
|
|
7384
|
+
*/
|
|
7385
|
+
_handleConfirmationDecision(decision) {
|
|
7386
|
+
if (this._confirmationInFlight) {
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
this._confirmationInFlight = true;
|
|
7390
|
+
this._error = '';
|
|
7391
|
+
// The decision line is authored by the server (single source of truth): it
|
|
7392
|
+
// persists an `action_decision` row AND streams an `ACTION_DECISION` event
|
|
7393
|
+
// back. We keep the prompt in place — the clicked button shows a loading
|
|
7394
|
+
// spinner — until that event arrives, which is our ack that the server
|
|
7395
|
+
// accepted the decision. The event handler then drops the prompt, appends the
|
|
7396
|
+
// decision line, and the follow-up turn streams into a new assistant bubble.
|
|
7397
|
+
const btn = this._messagesEl?.querySelector(`.confirmation__btn[data-decision="${decision}"]`);
|
|
7398
|
+
btn?.showLoading();
|
|
7399
|
+
this._isStreaming = true;
|
|
7400
|
+
this._userHasScrolledUp = false;
|
|
7401
|
+
this.send({
|
|
7402
|
+
message: '',
|
|
7403
|
+
messages: this.messages,
|
|
7404
|
+
conversationFiles: [],
|
|
7405
|
+
pageContext: '',
|
|
7406
|
+
selectionContext: '',
|
|
7407
|
+
confirmation: decision,
|
|
7408
|
+
}).then((response) => {
|
|
7409
|
+
// Leave _confirmationInFlight true — the buttons stay loading until the
|
|
7410
|
+
// streamed ACTION_DECISION event resolves the prompt. Only clear it on an
|
|
7411
|
+
// error path, where no such event will arrive.
|
|
7412
|
+
if (!response.ok) {
|
|
7413
|
+
this._confirmationInFlight = false;
|
|
7414
|
+
btn?.reset();
|
|
7415
|
+
this._handleStreamError('Request failed: ' + response.status);
|
|
7416
|
+
return;
|
|
7417
|
+
}
|
|
7418
|
+
if (!response.body) {
|
|
7419
|
+
this._confirmationInFlight = false;
|
|
7420
|
+
btn?.reset();
|
|
7421
|
+
this._handleStreamError('No response body');
|
|
7422
|
+
return;
|
|
7423
|
+
}
|
|
7424
|
+
this._reader = response.body.getReader();
|
|
7425
|
+
this._decoder = new TextDecoder();
|
|
7426
|
+
this._buffer = '';
|
|
7427
|
+
this._readStreamChunk();
|
|
7428
|
+
}).catch((err) => {
|
|
7429
|
+
this._confirmationInFlight = false;
|
|
7430
|
+
btn?.reset();
|
|
7431
|
+
if (err.name !== 'AbortError') {
|
|
7432
|
+
this._handleStreamError(err.message);
|
|
7433
|
+
}
|
|
7434
|
+
});
|
|
7435
|
+
}
|
|
7436
|
+
/** Clear the in-flight confirmation flags (error paths, or once resolved). */
|
|
7107
7437
|
/**
|
|
7108
7438
|
* Capture ambient "where is the user / what are they looking at" context at
|
|
7109
7439
|
* send time, so the AI is grounded without the host passing anything in.
|
|
@@ -7177,8 +7507,26 @@
|
|
|
7177
7507
|
_handleContextWarningDismiss() {
|
|
7178
7508
|
this._contextWarning = '';
|
|
7179
7509
|
}
|
|
7180
|
-
|
|
7181
|
-
|
|
7510
|
+
/**
|
|
7511
|
+
* A header button either opens the drawer to its view, switches the view if the
|
|
7512
|
+
* drawer is already open on the other one, or closes the drawer if it's already
|
|
7513
|
+
* showing this view. `_drawerView` is left intact on close so the content stays
|
|
7514
|
+
* visible while the drawer slides shut.
|
|
7515
|
+
*/
|
|
7516
|
+
_toggleDrawer(view) {
|
|
7517
|
+
if (this._drawerOpen && this._drawerView === view) {
|
|
7518
|
+
this._drawerOpen = false;
|
|
7519
|
+
}
|
|
7520
|
+
else {
|
|
7521
|
+
this._drawerOpen = true;
|
|
7522
|
+
this._drawerView = view;
|
|
7523
|
+
}
|
|
7524
|
+
this._saveState();
|
|
7525
|
+
}
|
|
7526
|
+
/** Close the drawer. `_drawerView` is left intact so its content stays visible
|
|
7527
|
+
* while the drawer slides shut. */
|
|
7528
|
+
_handleCloseDrawer() {
|
|
7529
|
+
this._drawerOpen = false;
|
|
7182
7530
|
this._saveState();
|
|
7183
7531
|
}
|
|
7184
7532
|
_handleDebugResizeMouseDown(e) {
|
|
@@ -7196,8 +7544,11 @@
|
|
|
7196
7544
|
}
|
|
7197
7545
|
this._reader.read().then((result) => {
|
|
7198
7546
|
if (result.done) {
|
|
7199
|
-
|
|
7200
|
-
|
|
7547
|
+
// Fallback unlock: the stream closed without a `finished:true` chunk
|
|
7548
|
+
// (network drop, server crash, the 5-min emitter timeout). Treat it as
|
|
7549
|
+
// the turn ending so a dropped stream never wedges the composer — the
|
|
7550
|
+
// server self-heals its turn lock and bounces any premature send.
|
|
7551
|
+
this._finishStream();
|
|
7201
7552
|
return;
|
|
7202
7553
|
}
|
|
7203
7554
|
this._buffer = this._buffer + this._decoder.decode(result.value, { stream: true });
|
|
@@ -7231,28 +7582,54 @@
|
|
|
7231
7582
|
}));
|
|
7232
7583
|
if (data.type === 'REASONING_PART') {
|
|
7233
7584
|
if (data.message) {
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
this.requestUpdate();
|
|
7585
|
+
// Text after a tool line belongs in a NEW assistant bubble — the
|
|
7586
|
+
// last message may be a tool line, which text can't append to.
|
|
7587
|
+
// _ensureAssistantBubble opens a fresh bubble when needed.
|
|
7588
|
+
const target = this._ensureAssistantBubble();
|
|
7589
|
+
if (!target.reasoning) {
|
|
7590
|
+
target.reasoning = '';
|
|
7241
7591
|
}
|
|
7592
|
+
target.reasoning += String(data.message);
|
|
7593
|
+
this.requestUpdate();
|
|
7242
7594
|
}
|
|
7243
7595
|
}
|
|
7244
7596
|
else if (data.type === 'RESPONSE_PART') {
|
|
7245
7597
|
if (data.message) {
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
this.messages[lastIndex].content += String(data.message);
|
|
7249
|
-
this.requestUpdate();
|
|
7250
|
-
}
|
|
7598
|
+
this._ensureAssistantBubble().content += String(data.message);
|
|
7599
|
+
this.requestUpdate();
|
|
7251
7600
|
}
|
|
7252
7601
|
}
|
|
7602
|
+
else if (data.type === 'tool_use' || data.type === 'tool_result') {
|
|
7603
|
+
// Push the event straight into the transcript — the message IS the event
|
|
7604
|
+
// ({type, data}). The render reads msg.data.* and pairs a tool_result to
|
|
7605
|
+
// its tool_use by data.toolUseId. Same shape the DB persists and reload
|
|
7606
|
+
// replays, so live and reload share one representation — no transform.
|
|
7607
|
+
this.messages.push(data);
|
|
7608
|
+
this.requestUpdate();
|
|
7609
|
+
}
|
|
7610
|
+
else if (data.type === 'action_decision') {
|
|
7611
|
+
// The server accepted the confirmation decision and authored the record
|
|
7612
|
+
// (single source of truth). Its arrival is the ack: drop the prompt,
|
|
7613
|
+
// stop the buttons' loading, and push the event straight in — the render
|
|
7614
|
+
// reads msg.data.{outcome,tools}. Same type + shape the DB persists, so
|
|
7615
|
+
// live and reload share one representation.
|
|
7616
|
+
this._pendingActions = [];
|
|
7617
|
+
this._confirmationHighlighted = -1;
|
|
7618
|
+
this._confirmationInFlight = false;
|
|
7619
|
+
this.messages.push(data);
|
|
7620
|
+
this.requestUpdate();
|
|
7621
|
+
}
|
|
7253
7622
|
else if (data.type === 'ERROR') {
|
|
7254
7623
|
this._handleStreamError(String(data.message || 'Unknown error'));
|
|
7255
7624
|
}
|
|
7625
|
+
else if (data.type === 'CONVERSATION_STATE') {
|
|
7626
|
+
// Turn-start state sync: the server is the source of truth for the
|
|
7627
|
+
// conversation's mode (it may have decayed EDIT → NORMAL after idle).
|
|
7628
|
+
// Adopt it so the composer's selector reflects the real posture.
|
|
7629
|
+
if (data.data?.mode === 'NORMAL' || data.data?.mode === 'EDIT' || data.data?.mode === 'PLAN') {
|
|
7630
|
+
this._mode = data.data.mode;
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7256
7633
|
else if (data.type === 'CONVERSATION_TOKEN_WARNING') {
|
|
7257
7634
|
// User-facing: friendly "conversation is getting long" guidance.
|
|
7258
7635
|
// Text only, never sent to the model — render verbatim as a banner.
|
|
@@ -7263,16 +7640,14 @@
|
|
|
7263
7640
|
// (it was created with a truncated first-message placeholder that we
|
|
7264
7641
|
// deliberately never show). Adopt it as the header label and pass it
|
|
7265
7642
|
// up so consumers can update their own chrome, e.g. a breadcrumb.
|
|
7266
|
-
if (data.data) {
|
|
7267
|
-
const title = String(data.data.title
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
}));
|
|
7275
|
-
}
|
|
7643
|
+
if (data.data?.title) {
|
|
7644
|
+
const title = String(data.data.title);
|
|
7645
|
+
this.label = title;
|
|
7646
|
+
this.dispatchEvent(new CustomEvent('conversation-title', {
|
|
7647
|
+
detail: { title },
|
|
7648
|
+
bubbles: true,
|
|
7649
|
+
composed: true,
|
|
7650
|
+
}));
|
|
7276
7651
|
}
|
|
7277
7652
|
}
|
|
7278
7653
|
else if (data.type === 'CONVERSATION_LIMIT_REACHED') {
|
|
@@ -7282,6 +7657,20 @@
|
|
|
7282
7657
|
this._contextLimitMessage = String(data.message || '');
|
|
7283
7658
|
this._contextWarning = '';
|
|
7284
7659
|
}
|
|
7660
|
+
else if (data.type === 'ACTION_CONFIRMATION_REQUIRED') {
|
|
7661
|
+
// A guarded tool paused for approval (Rail 3). Carries finished:true
|
|
7662
|
+
// (handled below), so the turn lock releases — but the conversation
|
|
7663
|
+
// is now awaiting a decision, and a normal chat message would be
|
|
7664
|
+
// rejected with awaitingConfirmation. Capture the batch so the
|
|
7665
|
+
// composer stays blocked and the approve/deny prompt renders instead;
|
|
7666
|
+
// the next call must be a confirmation, not a userText.
|
|
7667
|
+
const actions = data.data?.actions;
|
|
7668
|
+
if (Array.isArray(actions) && actions.length) {
|
|
7669
|
+
this._pendingActions = actions;
|
|
7670
|
+
// Default the keyboard highlight to Yes so Enter approves.
|
|
7671
|
+
this._confirmationHighlighted = 0;
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7285
7674
|
else if (data.type === 'TOKEN_USAGE') {
|
|
7286
7675
|
// INTERNAL-ONLY debug telemetry. Update the panel snapshot and the
|
|
7287
7676
|
// per-iteration log; never render as a chat bubble.
|
|
@@ -7312,11 +7701,17 @@
|
|
|
7312
7701
|
}
|
|
7313
7702
|
}
|
|
7314
7703
|
if (data.finished) {
|
|
7315
|
-
|
|
7316
|
-
|
|
7704
|
+
// Primary unlock signal: the turn is over and the server released
|
|
7705
|
+
// the turn lock. The SSE stream closes right after this chunk.
|
|
7706
|
+
this._finishStream();
|
|
7317
7707
|
}
|
|
7318
7708
|
}
|
|
7319
7709
|
}
|
|
7710
|
+
// Stop reading once a terminal chunk ended the turn — the reader is nulled
|
|
7711
|
+
// by _finishStream and the stream is closing anyway.
|
|
7712
|
+
if (!this._reader) {
|
|
7713
|
+
return;
|
|
7714
|
+
}
|
|
7320
7715
|
this._readStreamChunk();
|
|
7321
7716
|
}).catch((err) => {
|
|
7322
7717
|
if (err.name !== 'AbortError') {
|
|
@@ -7324,6 +7719,61 @@
|
|
|
7324
7719
|
}
|
|
7325
7720
|
});
|
|
7326
7721
|
}
|
|
7722
|
+
/**
|
|
7723
|
+
* The assistant bubble streamed text should append to. Returns the last message
|
|
7724
|
+
* when it's already an assistant bubble; otherwise (the last message is a tool
|
|
7725
|
+
* line, or there's nothing yet) opens a fresh assistant bubble so post-tool text
|
|
7726
|
+
* doesn't get dropped or glued to a tool line.
|
|
7727
|
+
*/
|
|
7728
|
+
_ensureAssistantBubble() {
|
|
7729
|
+
const last = this.messages[this.messages.length - 1];
|
|
7730
|
+
if (last && last.type === 'assistant') {
|
|
7731
|
+
return last;
|
|
7732
|
+
}
|
|
7733
|
+
const bubble = {
|
|
7734
|
+
type: 'assistant',
|
|
7735
|
+
content: '',
|
|
7736
|
+
};
|
|
7737
|
+
this.messages = [...this.messages, bubble];
|
|
7738
|
+
return bubble;
|
|
7739
|
+
}
|
|
7740
|
+
/**
|
|
7741
|
+
* End the current turn: release the turn lock and the reader. Called on the
|
|
7742
|
+
* primary unlock signal (`finished:true`) and the fallback (stream close).
|
|
7743
|
+
*
|
|
7744
|
+
* A turn that paused for confirmation (`_pendingActions` set) leaves the
|
|
7745
|
+
* composer blocked even though the lock released — the approve/deny prompt
|
|
7746
|
+
* owns the next call. Its trailing empty assistant bubble is dropped so the
|
|
7747
|
+
* prompt doesn't sit under an empty message with a typing indicator.
|
|
7748
|
+
*/
|
|
7749
|
+
_finishStream() {
|
|
7750
|
+
this._isStreaming = false;
|
|
7751
|
+
this._reader = null;
|
|
7752
|
+
if (this._pendingActions.length) {
|
|
7753
|
+
const lastIndex = this.messages.length - 1;
|
|
7754
|
+
// Drop the trailing assistant bubble only when it's truly empty — no
|
|
7755
|
+
// streamed text AND no reasoning. A reasoning-only bubble (the model thought
|
|
7756
|
+
// out loud, then went straight to the guarded tool) must be kept, else its
|
|
7757
|
+
// "Thinking" trace is lost when the confirmation card renders.
|
|
7758
|
+
if (lastIndex >= 0
|
|
7759
|
+
&& this.messages[lastIndex].type === 'assistant'
|
|
7760
|
+
&& !this.messages[lastIndex].content
|
|
7761
|
+
&& !this.messages[lastIndex].reasoning) {
|
|
7762
|
+
this.messages = this.messages.slice(0, -1);
|
|
7763
|
+
}
|
|
7764
|
+
// Scroll the confirmation into view so the user actually sees the ask — an
|
|
7765
|
+
// agentic turn can stream many tool lines past the fold, leaving the prompt
|
|
7766
|
+
// below it. `block: 'end'` brings its bottom up to just above the composer.
|
|
7767
|
+
if (this._inlineConfirm) {
|
|
7768
|
+
this.updateComplete.then(() => {
|
|
7769
|
+
this._messagesEl?.querySelector('.confirmation')?.scrollIntoView({
|
|
7770
|
+
behavior: 'smooth',
|
|
7771
|
+
block: 'end',
|
|
7772
|
+
});
|
|
7773
|
+
});
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7327
7777
|
_handleStreamError(message) {
|
|
7328
7778
|
this._isStreaming = false;
|
|
7329
7779
|
this._error = message;
|
|
@@ -7340,6 +7790,126 @@
|
|
|
7340
7790
|
}
|
|
7341
7791
|
}
|
|
7342
7792
|
// --- Render ---
|
|
7793
|
+
/**
|
|
7794
|
+
* The composer's mode selector — a Gemini-style inline label + caret sitting
|
|
7795
|
+
* just before the mic, that opens a small menu (Ask / Auto / Plan). Inline text
|
|
7796
|
+
* rather than a pill so it doesn't grow the single-row composer's height. The
|
|
7797
|
+
* menu opens upward above the trigger.
|
|
7798
|
+
*/
|
|
7799
|
+
_renderModeSelector() {
|
|
7800
|
+
const active = CHATBOT_MODES.find((m) => m.mode === this._mode) || CHATBOT_MODES[0];
|
|
7801
|
+
return b `
|
|
7802
|
+
<div class="mode">
|
|
7803
|
+
${this._modeMenuOpened ? b `
|
|
7804
|
+
<div class="mode-menu">
|
|
7805
|
+
${CHATBOT_MODES.map((m) => b `
|
|
7806
|
+
<button
|
|
7807
|
+
class=${e({
|
|
7808
|
+
'mode-menu__item': true,
|
|
7809
|
+
'mode-menu__item--active': m.mode === this._mode,
|
|
7810
|
+
})}
|
|
7811
|
+
@click=${() => this._handleModeSelect(m.mode)}
|
|
7812
|
+
>
|
|
7813
|
+
<span class="mode-menu__label">${m.label}</span>
|
|
7814
|
+
<span class="mode-menu__desc">${m.description}</span>
|
|
7815
|
+
</button>
|
|
7816
|
+
`)}
|
|
7817
|
+
</div>
|
|
7818
|
+
` : A}
|
|
7819
|
+
<button
|
|
7820
|
+
class="mode-trigger"
|
|
7821
|
+
aria-haspopup="true"
|
|
7822
|
+
aria-expanded=${this._modeMenuOpened ? 'true' : 'false'}
|
|
7823
|
+
@click=${this._handleModeToggle}
|
|
7824
|
+
>
|
|
7825
|
+
<span class="mode-trigger__label">${active.label}</span>
|
|
7826
|
+
<svg
|
|
7827
|
+
class="mode-trigger__caret"
|
|
7828
|
+
viewBox="0 0 24 24"
|
|
7829
|
+
fill="none"
|
|
7830
|
+
stroke="currentColor"
|
|
7831
|
+
stroke-width="2"
|
|
7832
|
+
stroke-linecap="round"
|
|
7833
|
+
stroke-linejoin="round"
|
|
7834
|
+
aria-hidden="true"
|
|
7835
|
+
>
|
|
7836
|
+
<path d="M6 9l6 6 6-6" />
|
|
7837
|
+
</svg>
|
|
7838
|
+
</button>
|
|
7839
|
+
</div>
|
|
7840
|
+
`;
|
|
7841
|
+
}
|
|
7842
|
+
/**
|
|
7843
|
+
* The approve/deny card shown when guarded tools are paused (Rail 3). Floats
|
|
7844
|
+
* OVER the messages, anchored just above the composer — not part of the message
|
|
7845
|
+
* scroll — so it stays in view while the transcript scrolls behind it.
|
|
7846
|
+
*
|
|
7847
|
+
* Written for a non-technical user. A single action reads as one plain-language
|
|
7848
|
+
* ask (`Run "{tool}"?` + the model's `confirmationMessage`). When the model
|
|
7849
|
+
* batches several guarded tools, they're listed together under one header and
|
|
7850
|
+
* the user accepts or rejects them all at once — the backend decision is
|
|
7851
|
+
* all-or-nothing. Either way: NO tool code, NO raw arguments/JSON — those are
|
|
7852
|
+
* noise (or alarming) to a normal user.
|
|
7853
|
+
*/
|
|
7854
|
+
_renderConfirmation() {
|
|
7855
|
+
// Title: name the single tool, else a generic header for a batch.
|
|
7856
|
+
let title = 'Run this action?';
|
|
7857
|
+
if (this._pendingActions.length > 1) {
|
|
7858
|
+
title = 'These actions need your approval';
|
|
7859
|
+
}
|
|
7860
|
+
else if (this._pendingActions[0]) {
|
|
7861
|
+
title = `Run "${this._pendingActions[0].displayName || this._pendingActions[0].toolName}"?`;
|
|
7862
|
+
}
|
|
7863
|
+
return b `
|
|
7864
|
+
<div
|
|
7865
|
+
class=${e({
|
|
7866
|
+
'confirmation': true,
|
|
7867
|
+
'confirmation--inline': this._inlineConfirm,
|
|
7868
|
+
})}
|
|
7869
|
+
role="alertdialog"
|
|
7870
|
+
aria-label="Confirm before continuing"
|
|
7871
|
+
>
|
|
7872
|
+
<div class="confirmation__title">${title}</div>
|
|
7873
|
+
${this._pendingActions.length > 1
|
|
7874
|
+
? b `
|
|
7875
|
+
<ol class="confirmation__list">
|
|
7876
|
+
${this._pendingActions.map((action) => b `
|
|
7877
|
+
<li class="confirmation__list-item">
|
|
7878
|
+
<span class="confirmation__list-name">${action.displayName || action.toolName}</span>
|
|
7879
|
+
${action.confirmationMessage
|
|
7880
|
+
? b `<span class="confirmation__list-desc">${action.confirmationMessage}</span>`
|
|
7881
|
+
: A}
|
|
7882
|
+
${this._showConfirmationInput && this._inlineConfirm && action.input && Object.keys(action.input).length
|
|
7883
|
+
? b `<pre class="confirmation__details-input">${JSON.stringify(action.input, null, 2)}</pre>`
|
|
7884
|
+
: A}
|
|
7885
|
+
</li>
|
|
7886
|
+
`)}
|
|
7887
|
+
</ol>
|
|
7888
|
+
`
|
|
7889
|
+
: b `
|
|
7890
|
+
<p class="confirmation__message">${this._pendingActions[0]?.confirmationMessage || 'The assistant wants to take an action. Is that OK?'}</p>
|
|
7891
|
+
${this._showConfirmationInput && this._inlineConfirm && this._pendingActions[0]?.input && Object.keys(this._pendingActions[0].input).length
|
|
7892
|
+
? b `<pre class="confirmation__details-input">${JSON.stringify(this._pendingActions[0].input, null, 2)}</pre>`
|
|
7893
|
+
: A}
|
|
7894
|
+
`}
|
|
7895
|
+
<div class="confirmation__actions">
|
|
7896
|
+
${CONFIRMATION_DECISIONS.map((option, index) => b `
|
|
7897
|
+
<kr-button
|
|
7898
|
+
class=${e({
|
|
7899
|
+
'confirmation__btn': true,
|
|
7900
|
+
'confirmation__btn--highlighted': index === this._confirmationHighlighted,
|
|
7901
|
+
})}
|
|
7902
|
+
variant=${option.decision === 'approve' ? 'flat' : 'outline'}
|
|
7903
|
+
color=${option.decision === 'reject' ? 'secondary' : 'primary'}
|
|
7904
|
+
size="small"
|
|
7905
|
+
data-decision=${option.decision}
|
|
7906
|
+
@click=${() => this._handleConfirmationDecision(option.decision)}
|
|
7907
|
+
>${option.label}</kr-button>
|
|
7908
|
+
`)}
|
|
7909
|
+
</div>
|
|
7910
|
+
</div>
|
|
7911
|
+
`;
|
|
7912
|
+
}
|
|
7343
7913
|
/** The `/` skill menu shown above the composer while typing a slash command. */
|
|
7344
7914
|
_renderSkillMenu() {
|
|
7345
7915
|
const skills = this._invocableSkills;
|
|
@@ -7554,26 +8124,37 @@
|
|
|
7554
8124
|
* three scopes from the latest TOKEN_USAGE event — context gauge, this turn,
|
|
7555
8125
|
* lifetime — plus a verbose per-iteration table. Never customer-facing.
|
|
7556
8126
|
*/
|
|
7557
|
-
|
|
8127
|
+
/**
|
|
8128
|
+
* The side drawer — one sliding panel that holds both the Settings and Debug
|
|
8129
|
+
* views. The header buttons switch `_drawerView`; the title and body swap
|
|
8130
|
+
* inside. The panel keeps one consistent (resizable) width across both views —
|
|
8131
|
+
* switching views only changes the content, never the panel size.
|
|
8132
|
+
*/
|
|
8133
|
+
_renderDrawer() {
|
|
8134
|
+
// The debug view is gated on the server-side flag (inline variant + debug
|
|
8135
|
+
// telemetry enabled); if it's not enabled, the drawer only ever shows settings.
|
|
8136
|
+
const isDebug = this._drawerView === 'debug'
|
|
8137
|
+
&& this.variant === 'inline'
|
|
8138
|
+
&& this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
7558
8139
|
return b `
|
|
7559
8140
|
<div
|
|
7560
8141
|
class=${e({
|
|
7561
|
-
'
|
|
7562
|
-
'
|
|
7563
|
-
'
|
|
8142
|
+
'drawer': true,
|
|
8143
|
+
'drawer--open': this._drawerOpen,
|
|
8144
|
+
'drawer--resizing': this._isDebugResizing,
|
|
7564
8145
|
})}
|
|
7565
8146
|
style=${o({ width: `${this._debugDrawerWidth}px` })}
|
|
7566
8147
|
>
|
|
7567
8148
|
<div
|
|
7568
|
-
class="
|
|
8149
|
+
class="drawer__resize"
|
|
7569
8150
|
@mousedown=${this._handleDebugResizeMouseDown}
|
|
7570
8151
|
></div>
|
|
7571
|
-
<div class="
|
|
7572
|
-
<span class="
|
|
8152
|
+
<div class="drawer__header">
|
|
8153
|
+
<span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
|
|
7573
8154
|
<button
|
|
7574
8155
|
class="header-action"
|
|
7575
|
-
@click=${this.
|
|
7576
|
-
title="Close
|
|
8156
|
+
@click=${this._handleCloseDrawer}
|
|
8157
|
+
title="Close"
|
|
7577
8158
|
>
|
|
7578
8159
|
<svg
|
|
7579
8160
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -7591,105 +8172,163 @@
|
|
|
7591
8172
|
</svg>
|
|
7592
8173
|
</button>
|
|
7593
8174
|
</div>
|
|
7594
|
-
<div class="
|
|
7595
|
-
${this.
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
8175
|
+
<div class="drawer__body">
|
|
8176
|
+
${isDebug ? this._renderDebugContent() : this._renderSettingsContent()}
|
|
8177
|
+
</div>
|
|
8178
|
+
</div>
|
|
8179
|
+
`;
|
|
8180
|
+
}
|
|
8181
|
+
/** The Settings view's body — user preferences, grouped in sections. */
|
|
8182
|
+
_renderSettingsContent() {
|
|
8183
|
+
return b `
|
|
8184
|
+
<div class="settings-section">
|
|
8185
|
+
<div class="settings-section__title">Tools</div>
|
|
8186
|
+
<div class="settings-row">
|
|
8187
|
+
<kr-checkbox
|
|
8188
|
+
label="Show tools"
|
|
8189
|
+
.checked=${this._showTools}
|
|
8190
|
+
@change=${this._handleShowToolsChange}
|
|
8191
|
+
></kr-checkbox>
|
|
8192
|
+
</div>
|
|
8193
|
+
<div class="settings-row">
|
|
8194
|
+
<kr-checkbox
|
|
8195
|
+
label="Show tool inputs"
|
|
8196
|
+
.checked=${this._showToolInputs}
|
|
8197
|
+
@change=${this._handleShowToolInputsChange}
|
|
8198
|
+
></kr-checkbox>
|
|
8199
|
+
</div>
|
|
8200
|
+
<div class="settings-row">
|
|
8201
|
+
<kr-checkbox
|
|
8202
|
+
label="Show tool confirmation inputs"
|
|
8203
|
+
.checked=${this._showConfirmationInput}
|
|
8204
|
+
@change=${this._handleShowConfirmationInputChange}
|
|
8205
|
+
></kr-checkbox>
|
|
8206
|
+
</div>
|
|
8207
|
+
</div>
|
|
8208
|
+
`;
|
|
8209
|
+
}
|
|
8210
|
+
/** The Debug view's body — telemetry scopes and the per-iteration table. */
|
|
8211
|
+
_renderDebugContent() {
|
|
8212
|
+
return b `
|
|
8213
|
+
${this._telemetry ? b `
|
|
8214
|
+
<div class="debug-section">
|
|
8215
|
+
<div class="debug-section__label">Context</div>
|
|
8216
|
+
<div class="debug-gauge">
|
|
8217
|
+
<div class="debug-gauge__bar">
|
|
8218
|
+
<div
|
|
8219
|
+
class="debug-gauge__fill"
|
|
8220
|
+
style=${o({ width: `${Math.min(100, Math.max(0, this._telemetry.contextUsedPercent || 0))}%` })}
|
|
8221
|
+
></div>
|
|
7610
8222
|
</div>
|
|
8223
|
+
<span class="debug-gauge__text">
|
|
8224
|
+
${this._fmtTokens(this._telemetry.currentContextTokens)} / ${this._fmtTokens(this._telemetry.contextWindowTokens)}
|
|
8225
|
+
· ${this._telemetry.contextUsedPercent ?? 0}%
|
|
8226
|
+
</span>
|
|
8227
|
+
</div>
|
|
8228
|
+
</div>
|
|
7611
8229
|
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
8230
|
+
<div class="debug-section">
|
|
8231
|
+
<div class="debug-section__label">This turn</div>
|
|
8232
|
+
<div class="debug-metrics">
|
|
8233
|
+
<span class="debug-metrics__key">Input</span>
|
|
8234
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnInputTokens)}</span>
|
|
8235
|
+
<span class="debug-metrics__key">Output</span>
|
|
8236
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnOutputTokens)}</span>
|
|
8237
|
+
<span class="debug-metrics__key">Cache read</span>
|
|
8238
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnCacheReadTokens)}</span>
|
|
8239
|
+
<span class="debug-metrics__key">Hit rate</span>
|
|
8240
|
+
<span class="debug-metrics__val">${this._telemetry.turnHitRate ?? '—'}%</span>
|
|
8241
|
+
<span class="debug-metrics__key">Cost</span>
|
|
8242
|
+
<span class="debug-metrics__val debug-cost">${this._fmtCost(this._telemetry.turnCostUsd)}</span>
|
|
8243
|
+
</div>
|
|
8244
|
+
</div>
|
|
7627
8245
|
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
8246
|
+
<div class="debug-section">
|
|
8247
|
+
<div class="debug-section__label">Lifetime</div>
|
|
8248
|
+
<div class="debug-metrics">
|
|
8249
|
+
<span class="debug-metrics__key">Input</span>
|
|
8250
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationInputTokens)}</span>
|
|
8251
|
+
<span class="debug-metrics__key">Output</span>
|
|
8252
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationOutputTokens)}</span>
|
|
8253
|
+
<span class="debug-metrics__key">Cache read</span>
|
|
8254
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationCacheReadTokens)}</span>
|
|
8255
|
+
<span class="debug-metrics__key">Cache write</span>
|
|
8256
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationCacheWriteTokens)}</span>
|
|
8257
|
+
<span class="debug-metrics__key">Cost so far</span>
|
|
8258
|
+
<span class="debug-metrics__val debug-cost">${this._fmtCost(this._telemetry.conversationCostUsd)}</span>
|
|
8259
|
+
</div>
|
|
8260
|
+
</div>
|
|
7643
8261
|
` : b `
|
|
7644
8262
|
<div class="debug-empty">No telemetry yet — send a message to populate.</div>
|
|
7645
8263
|
`}
|
|
7646
8264
|
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
8265
|
+
${this._iterationLog.length ? b `
|
|
8266
|
+
<div class="debug-section">
|
|
8267
|
+
<div class="debug-section__label">Iterations (this turn)</div>
|
|
8268
|
+
<div class="debug-iterations-scroll">
|
|
8269
|
+
<table class="debug-iterations">
|
|
8270
|
+
<thead>
|
|
8271
|
+
<tr>
|
|
8272
|
+
<th>#</th>
|
|
8273
|
+
<th>In</th>
|
|
8274
|
+
<th>Out</th>
|
|
8275
|
+
<th>Cache R</th>
|
|
8276
|
+
<th>Cache W</th>
|
|
8277
|
+
<th>Hit</th>
|
|
8278
|
+
<th>Cost</th>
|
|
8279
|
+
<th>Ckpt</th>
|
|
8280
|
+
</tr>
|
|
8281
|
+
</thead>
|
|
8282
|
+
<tbody>
|
|
8283
|
+
${this._iterationLog.map((it) => {
|
|
7666
8284
|
const ckpt = this._cacheCheckpoints[it.iteration];
|
|
7667
8285
|
return b `
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
8286
|
+
<tr>
|
|
8287
|
+
<td>${it.iteration}</td>
|
|
8288
|
+
<td>${this._fmtTokens(it.iterationInput)}</td>
|
|
8289
|
+
<td>${this._fmtTokens(it.iterationOutput)}</td>
|
|
8290
|
+
<td>${this._fmtTokens(it.iterationCacheRead)}</td>
|
|
8291
|
+
<td>${this._fmtTokens(it.iterationCacheWrite)}</td>
|
|
8292
|
+
<td>${it.iterationHitRate ?? '—'}%</td>
|
|
8293
|
+
<td class="debug-cost">${this._fmtCost(it.iterationCostUsd)}</td>
|
|
8294
|
+
<td>${ckpt ? ckpt.mode : '—'}</td>
|
|
8295
|
+
</tr>
|
|
7678
8296
|
`;
|
|
7679
8297
|
})}
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
</div>
|
|
7684
|
-
` : A}
|
|
8298
|
+
</tbody>
|
|
8299
|
+
</table>
|
|
8300
|
+
</div>
|
|
7685
8301
|
</div>
|
|
7686
|
-
|
|
8302
|
+
` : A}
|
|
7687
8303
|
`;
|
|
7688
8304
|
}
|
|
7689
8305
|
render() {
|
|
7690
8306
|
// Debug affordances (drawer toggle, drawer, /test-* options) require the
|
|
7691
8307
|
// inline variant AND the server-side debug flag from aiFrameworkConfig.
|
|
7692
8308
|
const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
8309
|
+
// Push the content over by the open drawer's width so the drawer sits beside
|
|
8310
|
+
// the content rather than overlaying it. One consistent (resizable) width for
|
|
8311
|
+
// both views. Only pushes while the drawer is open.
|
|
8312
|
+
const contentPadding = {};
|
|
8313
|
+
if (this._drawerOpen) {
|
|
8314
|
+
contentPadding.paddingRight = `${this._debugDrawerWidth}px`;
|
|
8315
|
+
}
|
|
8316
|
+
// Group the transcript into runs so an agent turn (its reasoning/tool/text/
|
|
8317
|
+
// decision messages, which stream in as separate entries) renders inside ONE
|
|
8318
|
+
// wrapper. The scroll-to-top runway goes on that wrapper, so everything the
|
|
8319
|
+
// turn appends fills the SAME tall element instead of stacking as siblings
|
|
8320
|
+
// below it (which is what stranded tool lines below the fold). A user message
|
|
8321
|
+
// is its own group and is unaffected.
|
|
8322
|
+
const messageGroups = [];
|
|
8323
|
+
this.messages.forEach((msg) => {
|
|
8324
|
+
const lastGroup = messageGroups[messageGroups.length - 1];
|
|
8325
|
+
if (msg.type === 'user' || !lastGroup || lastGroup[0].type === 'user') {
|
|
8326
|
+
messageGroups.push([msg]);
|
|
8327
|
+
}
|
|
8328
|
+
else {
|
|
8329
|
+
lastGroup.push(msg);
|
|
8330
|
+
}
|
|
8331
|
+
});
|
|
7693
8332
|
return b `
|
|
7694
8333
|
${(this.hideToggle || this.variant === 'inline') ? A : b `
|
|
7695
8334
|
<button
|
|
@@ -7720,7 +8359,7 @@
|
|
|
7720
8359
|
'panel--opened': this.opened,
|
|
7721
8360
|
'panel--resizing-debug': this._isDebugResizing,
|
|
7722
8361
|
})}
|
|
7723
|
-
style=${o(
|
|
8362
|
+
style=${o(contentPadding)}
|
|
7724
8363
|
@dragenter=${this._handleDragEnter}
|
|
7725
8364
|
@dragover=${this._handleDragOver}
|
|
7726
8365
|
@dragleave=${this._handleDragLeave}
|
|
@@ -7793,13 +8432,35 @@
|
|
|
7793
8432
|
</div>
|
|
7794
8433
|
</div>
|
|
7795
8434
|
<div class="header-actions">
|
|
8435
|
+
<button
|
|
8436
|
+
class="header-action"
|
|
8437
|
+
@click=${() => this._toggleDrawer('settings')}
|
|
8438
|
+
title="Settings"
|
|
8439
|
+
>
|
|
8440
|
+
<svg
|
|
8441
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8442
|
+
fill="none"
|
|
8443
|
+
viewBox="0 0 24 24"
|
|
8444
|
+
stroke-width="1.5"
|
|
8445
|
+
stroke="currentColor"
|
|
8446
|
+
class="header-action-icon"
|
|
8447
|
+
>
|
|
8448
|
+
<path
|
|
8449
|
+
stroke-linecap="round"
|
|
8450
|
+
stroke-linejoin="round"
|
|
8451
|
+
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
|
8452
|
+
/>
|
|
8453
|
+
<path
|
|
8454
|
+
stroke-linecap="round"
|
|
8455
|
+
stroke-linejoin="round"
|
|
8456
|
+
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
8457
|
+
/>
|
|
8458
|
+
</svg>
|
|
8459
|
+
</button>
|
|
7796
8460
|
${isDebugEnabled ? b `
|
|
7797
8461
|
<button
|
|
7798
|
-
class
|
|
7799
|
-
|
|
7800
|
-
'header-action--active': this._debugPanelOpen,
|
|
7801
|
-
})}
|
|
7802
|
-
@click=${this._handleToggleDebugPanel}
|
|
8462
|
+
class="header-action"
|
|
8463
|
+
@click=${() => this._toggleDrawer('debug')}
|
|
7803
8464
|
title="Debug telemetry"
|
|
7804
8465
|
>
|
|
7805
8466
|
<svg
|
|
@@ -7850,139 +8511,302 @@
|
|
|
7850
8511
|
<span class="empty-text">What can I help you with?</span>
|
|
7851
8512
|
</div>
|
|
7852
8513
|
` : A}
|
|
7853
|
-
${
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
8514
|
+
${messageGroups.map((group) => {
|
|
8515
|
+
// A user group is a single message rendered bare. An agent group wraps
|
|
8516
|
+
// its items in one `.agent-turn` so the scroll-to-top runway sits on
|
|
8517
|
+
// that wrapper and the whole turn fills it (no stranded siblings).
|
|
8518
|
+
const items = group.map((msg) => {
|
|
8519
|
+
const index = this.messages.indexOf(msg);
|
|
8520
|
+
// Skip an empty assistant placeholder (no content, no reasoning) —
|
|
8521
|
+
// _send and _ensureAssistantBubble append these to stream into,
|
|
8522
|
+
// but between tool calls with no text they stay empty. An invisible
|
|
8523
|
+
// .message div still occupies a flex-gap slot, doubling the spacing
|
|
8524
|
+
// between tool lines. Keep it only while it's the actively-streaming
|
|
8525
|
+
// last message (it shows the typing dots).
|
|
8526
|
+
if (msg.type === 'assistant'
|
|
8527
|
+
&& !msg.content
|
|
8528
|
+
&& !msg.reasoning
|
|
8529
|
+
&& !(this._isStreaming && msg === this.messages[this.messages.length - 1])) {
|
|
8530
|
+
return A;
|
|
8531
|
+
}
|
|
8532
|
+
// Tool visibility is user-controlled ("Show tools" in the settings
|
|
8533
|
+
// panel). When off, drop the whole tool row so it doesn't leave an
|
|
8534
|
+
// empty flex-gap slot in the transcript.
|
|
8535
|
+
if ((msg.type === 'tool_use' || msg.type === 'tool_result') && !this._showTools) {
|
|
8536
|
+
return A;
|
|
8537
|
+
}
|
|
8538
|
+
// A tool_result renders nothing on its own — its tool_use line pulls
|
|
8539
|
+
// it in (matched by data.toolUseId) and renders it under the ⎿ cliff.
|
|
8540
|
+
if (msg.type === 'tool_result') {
|
|
8541
|
+
return A;
|
|
8542
|
+
}
|
|
8543
|
+
// A tool_use line (Claude-CLI style): header "› ToolName(input)" and,
|
|
8544
|
+
// when "Show tool inputs" is on, the result under a ⎿ cliff pulled
|
|
8545
|
+
// from the next tool_result with the same toolUseId. Precompute the
|
|
8546
|
+
// result and the two display strings (they need statements — JSON
|
|
8547
|
+
// reformat, truncation — that can't live in a template expression).
|
|
8548
|
+
let result;
|
|
8549
|
+
let inputSummary = '';
|
|
8550
|
+
let resultPreview = '';
|
|
8551
|
+
let resultRemainder = 0;
|
|
8552
|
+
if (msg.type === 'tool_use') {
|
|
8553
|
+
const next = this.messages[index + 1];
|
|
8554
|
+
if (next && next.type === 'tool_result' && next.data?.toolUseId === msg.data?.toolUseId) {
|
|
8555
|
+
result = next.data;
|
|
8556
|
+
}
|
|
8557
|
+
if (this._showToolInputs && msg.data?.toolInput) {
|
|
8558
|
+
inputSummary = String(msg.data.toolInput).replace(/\s+/g, ' ').trim();
|
|
8559
|
+
if (inputSummary.length > 80) {
|
|
8560
|
+
inputSummary = `${inputSummary.slice(0, 79)}…`;
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
if (this._showToolInputs && result?.toolResultString) {
|
|
8564
|
+
let formatted;
|
|
8565
|
+
try {
|
|
8566
|
+
formatted = JSON.stringify(JSON.parse(result.toolResultString), null, 2);
|
|
8567
|
+
}
|
|
8568
|
+
catch {
|
|
8569
|
+
formatted = String(result.toolResultString);
|
|
8570
|
+
}
|
|
8571
|
+
const lines = formatted.split('\n');
|
|
8572
|
+
resultPreview = lines.slice(0, 3).join('\n');
|
|
8573
|
+
resultRemainder = Math.max(0, lines.length - 3);
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8576
|
+
// A resolved-decision line (Rail 3) — reads "Approved Create AI Tool"
|
|
8577
|
+
// / "Denied …" / "Always allowed …". Precompute the verb and the
|
|
8578
|
+
// friendly tool names (an if/else and a map — not template exprs).
|
|
8579
|
+
let decisionVerb = 'Approved';
|
|
8580
|
+
let decisionTools = [];
|
|
8581
|
+
if (msg.type === 'action_decision') {
|
|
8582
|
+
if (msg.data?.outcome === 'denied') {
|
|
8583
|
+
decisionVerb = 'Denied';
|
|
8584
|
+
}
|
|
8585
|
+
else if (msg.data?.outcome === 'alwaysAllowed') {
|
|
8586
|
+
decisionVerb = 'Always allowed';
|
|
8587
|
+
}
|
|
8588
|
+
decisionTools = (msg.data?.tools || []).map((t) => t.displayName || t.toolName);
|
|
8589
|
+
}
|
|
8590
|
+
// A tool line and a decision line ARE assistant-side messages —
|
|
8591
|
+
// render them inside the same .message wrapper as everything else so
|
|
8592
|
+
// they share its spacing/structure, not as separate element types.
|
|
7881
8593
|
return b `
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
8594
|
+
<div class=${e({
|
|
8595
|
+
'message': true,
|
|
8596
|
+
'message--user': msg.type === 'user',
|
|
8597
|
+
'message--assistant': msg.type !== 'user',
|
|
8598
|
+
})}>
|
|
8599
|
+
${msg.type === 'tool_use' ? b `
|
|
8600
|
+
<div class="tool-activity-block">
|
|
8601
|
+
<div class=${e({
|
|
8602
|
+
'tool-activity': true,
|
|
8603
|
+
'tool-activity--error': result?.toolResultStatus === 'error',
|
|
8604
|
+
})}>
|
|
8605
|
+
<span class="tool-activity__chevron">›</span>
|
|
8606
|
+
<span class="tool-activity__label">${msg.data?.displayName || msg.data?.toolName || 'Tool'}</span>
|
|
8607
|
+
${inputSummary ? b `<span class="tool-activity__input">(${inputSummary})</span>` : A}
|
|
8608
|
+
</div>
|
|
8609
|
+
${this._showToolInputs && result?.toolResultString ? b `
|
|
8610
|
+
<div class=${e({
|
|
8611
|
+
'tool-activity__result': true,
|
|
8612
|
+
'tool-activity__result--error': result.toolResultStatus === 'error',
|
|
8613
|
+
})}>
|
|
8614
|
+
<span class="tool-activity__connector">⎿</span>
|
|
8615
|
+
<pre class="tool-activity__result-body">${resultPreview}${resultRemainder > 0
|
|
8616
|
+
? b `\n<span class="tool-activity__more">… +${resultRemainder} lines</span>`
|
|
8617
|
+
: A}</pre>
|
|
8618
|
+
</div>
|
|
8619
|
+
` : A}
|
|
8620
|
+
</div>
|
|
8621
|
+
` : A}
|
|
8622
|
+
${msg.type === 'action_decision' ? b `
|
|
8623
|
+
<div class=${e({
|
|
8624
|
+
'decision': true,
|
|
8625
|
+
'decision--denied': msg.data?.outcome === 'denied',
|
|
8626
|
+
})}>
|
|
8627
|
+
<span class="decision__icon">
|
|
8628
|
+
<svg
|
|
8629
|
+
viewBox="0 0 24 24"
|
|
8630
|
+
fill="none"
|
|
8631
|
+
stroke="currentColor"
|
|
8632
|
+
stroke-width="2"
|
|
8633
|
+
stroke-linecap="round"
|
|
8634
|
+
stroke-linejoin="round"
|
|
7887
8635
|
>
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
8636
|
+
${msg.data?.outcome === 'denied'
|
|
8637
|
+
? b `<path d="M18 6 6 18M6 6l12 12" />`
|
|
8638
|
+
: b `<path d="M20 6 9 17l-5-5" />`}
|
|
8639
|
+
</svg>
|
|
8640
|
+
</span>
|
|
8641
|
+
<span class="decision__label">${decisionVerb}${decisionTools.length ? ' ' + decisionTools.join(', ') : A}</span>
|
|
8642
|
+
</div>
|
|
8643
|
+
` : A}
|
|
8644
|
+
${msg.reasoning ? (this._reasoningAsMessage ? b `
|
|
8645
|
+
<div class="message-content">${msg.reasoning}</div>
|
|
8646
|
+
` : b `
|
|
8647
|
+
<details class="reasoning">
|
|
8648
|
+
<summary>Thinking</summary>
|
|
8649
|
+
<div class="reasoning-content">${msg.reasoning}</div>
|
|
8650
|
+
</details>
|
|
8651
|
+
`) : A}
|
|
8652
|
+
${msg.conversationFiles && msg.conversationFiles.length ? b `
|
|
8653
|
+
<div class="attachments">
|
|
8654
|
+
${msg.conversationFiles.map((file) => {
|
|
8655
|
+
const fileName = file.niceName || file.name || 'File';
|
|
8656
|
+
// A stored file is an image by its MIME when it has one, else
|
|
8657
|
+
// by its extension.
|
|
8658
|
+
let isImage = IMAGE_EXTENSIONS.includes(this._getFileExtension(fileName));
|
|
8659
|
+
if (file.mimeType) {
|
|
8660
|
+
isImage = file.mimeType.startsWith('image/');
|
|
8661
|
+
}
|
|
8662
|
+
// An image shows its thumbnail; everything else shows a
|
|
8663
|
+
// file-type tile. Both open the previewer on click — it renders
|
|
8664
|
+
// images, video, PDFs, and text inline, and shows a download
|
|
8665
|
+
// tile for the rest (Office formats), so a click always does
|
|
8666
|
+
// something useful.
|
|
8667
|
+
if (isImage) {
|
|
8668
|
+
return b `
|
|
8669
|
+
<button
|
|
8670
|
+
class="attachment"
|
|
8671
|
+
type="button"
|
|
8672
|
+
title=${file.niceName || 'View image'}
|
|
8673
|
+
@click=${() => this._handleFileOpen(file)}
|
|
8674
|
+
>
|
|
8675
|
+
<img
|
|
8676
|
+
src=${file.previewUrl || `${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${file.conversationFileID}/view`}
|
|
8677
|
+
alt=${file.niceName || ''}
|
|
8678
|
+
/>
|
|
8679
|
+
</button>
|
|
8680
|
+
`;
|
|
8681
|
+
}
|
|
8682
|
+
return b `
|
|
8683
|
+
<button
|
|
8684
|
+
class="attachment attachment--doc attachment--openable"
|
|
8685
|
+
type="button"
|
|
8686
|
+
title=${fileName}
|
|
8687
|
+
@click=${() => this._handleFileOpen(file)}
|
|
8688
|
+
>
|
|
8689
|
+
<span class="attachment__type">${this._getFileExtension(fileName) || 'File'}</span>
|
|
8690
|
+
<span class="attachment__name">${fileName}</span>
|
|
8691
|
+
</button>
|
|
8692
|
+
`;
|
|
8693
|
+
})}
|
|
8694
|
+
</div>
|
|
8695
|
+
` : A}
|
|
8696
|
+
${msg.content ? b `
|
|
8697
|
+
<div class="message-content">
|
|
8698
|
+
${msg.type === 'user'
|
|
8699
|
+
? this._renderUserContent(msg.content)
|
|
8700
|
+
: o$1(marked.parse(msg.content, { breaks: true }))}
|
|
8701
|
+
</div>
|
|
8702
|
+
` : A}
|
|
8703
|
+
${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? b `
|
|
8704
|
+
<div class="typing">
|
|
8705
|
+
<span></span>
|
|
8706
|
+
<span></span>
|
|
8707
|
+
<span></span>
|
|
8708
|
+
</div>
|
|
8709
|
+
` : A}
|
|
8710
|
+
${msg.content ? b `
|
|
8711
|
+
<button
|
|
8712
|
+
class="message-copy"
|
|
8713
|
+
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
8714
|
+
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
8715
|
+
>
|
|
8716
|
+
${this._copiedIndex === index ? b `
|
|
8717
|
+
<svg
|
|
8718
|
+
viewBox="0 0 24 24"
|
|
8719
|
+
fill="none"
|
|
8720
|
+
stroke="currentColor"
|
|
8721
|
+
stroke-width="1.5"
|
|
8722
|
+
stroke-linecap="round"
|
|
8723
|
+
stroke-linejoin="round"
|
|
8724
|
+
>
|
|
8725
|
+
<path d="M20 6 9 17l-5-5" />
|
|
8726
|
+
</svg>
|
|
8727
|
+
` : b `
|
|
8728
|
+
<svg
|
|
8729
|
+
viewBox="0 0 24 24"
|
|
8730
|
+
fill="none"
|
|
8731
|
+
stroke="currentColor"
|
|
8732
|
+
stroke-width="1.5"
|
|
8733
|
+
stroke-linecap="round"
|
|
8734
|
+
stroke-linejoin="round"
|
|
8735
|
+
>
|
|
8736
|
+
<rect
|
|
8737
|
+
x="9"
|
|
8738
|
+
y="9"
|
|
8739
|
+
width="13"
|
|
8740
|
+
height="13"
|
|
8741
|
+
rx="2"
|
|
8742
|
+
/>
|
|
8743
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
8744
|
+
</svg>
|
|
8745
|
+
`}
|
|
8746
|
+
</button>
|
|
8747
|
+
` : A}
|
|
8748
|
+
</div>
|
|
8749
|
+
`;
|
|
8750
|
+
});
|
|
8751
|
+
if (group[0].type === 'user') {
|
|
8752
|
+
return items;
|
|
8753
|
+
}
|
|
8754
|
+
// The inline confirmation is this turn's final step, so it renders
|
|
8755
|
+
// INSIDE the agent-turn wrapper (not as a sibling) — it sits right
|
|
8756
|
+
// after the last message and the runway falls below it naturally.
|
|
8757
|
+
// Only on the last group, which is the current (paused) turn.
|
|
8758
|
+
const isLastGroup = group === messageGroups[messageGroups.length - 1];
|
|
8759
|
+
const showConfirmation = isLastGroup && this._pendingActions.length && this._inlineConfirm;
|
|
8760
|
+
// Keep the "thinking" indicator visible while a tool is running or
|
|
8761
|
+
// the model is between steps with no text yet. The trailing empty
|
|
8762
|
+
// assistant bubble shows the dots itself; but once a tool line is
|
|
8763
|
+
// the last message, that bubble is gone — so render the dots here
|
|
8764
|
+
// at the end of the live turn instead. Not while paused for a
|
|
8765
|
+
// confirmation (the ask replaces the indicator).
|
|
8766
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
8767
|
+
const showTyping = isLastGroup
|
|
8768
|
+
&& this._isStreaming
|
|
8769
|
+
&& !this._pendingActions.length
|
|
8770
|
+
&& lastMsg?.type !== 'assistant';
|
|
7895
8771
|
return b `
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
<span
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
8772
|
+
<div class="agent-turn">
|
|
8773
|
+
${items}
|
|
8774
|
+
${showTyping ? b `
|
|
8775
|
+
<div class="typing">
|
|
8776
|
+
<span></span>
|
|
8777
|
+
<span></span>
|
|
8778
|
+
<span></span>
|
|
8779
|
+
</div>
|
|
8780
|
+
` : A}
|
|
8781
|
+
${showConfirmation ? this._renderConfirmation() : A}
|
|
8782
|
+
</div>
|
|
8783
|
+
`;
|
|
7906
8784
|
})}
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
</
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
class="message-copy"
|
|
7926
|
-
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
7927
|
-
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
7928
|
-
>
|
|
7929
|
-
${this._copiedIndex === index ? b `
|
|
7930
|
-
<svg
|
|
7931
|
-
viewBox="0 0 24 24"
|
|
7932
|
-
fill="none"
|
|
7933
|
-
stroke="currentColor"
|
|
7934
|
-
stroke-width="1.5"
|
|
7935
|
-
stroke-linecap="round"
|
|
7936
|
-
stroke-linejoin="round"
|
|
7937
|
-
>
|
|
7938
|
-
<path d="M20 6 9 17l-5-5" />
|
|
7939
|
-
</svg>
|
|
7940
|
-
` : b `
|
|
7941
|
-
<svg
|
|
7942
|
-
viewBox="0 0 24 24"
|
|
7943
|
-
fill="none"
|
|
7944
|
-
stroke="currentColor"
|
|
7945
|
-
stroke-width="1.5"
|
|
8785
|
+
</div>
|
|
8786
|
+
</div>
|
|
8787
|
+
|
|
8788
|
+
${this._pendingActions.length && !this._inlineConfirm ? this._renderConfirmation() : A}
|
|
8789
|
+
|
|
8790
|
+
${this._error ? b `
|
|
8791
|
+
<div class="error">
|
|
8792
|
+
<span>${this._error}</span>
|
|
8793
|
+
<button class="error-dismiss" @click=${this._handleErrorDismiss}>
|
|
8794
|
+
<svg
|
|
8795
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8796
|
+
fill="none"
|
|
8797
|
+
viewBox="0 0 24 24"
|
|
8798
|
+
stroke-width="2"
|
|
8799
|
+
stroke="currentColor"
|
|
8800
|
+
class="error-dismiss-icon"
|
|
8801
|
+
>
|
|
8802
|
+
<path
|
|
7946
8803
|
stroke-linecap="round"
|
|
7947
8804
|
stroke-linejoin="round"
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
y="9"
|
|
7952
|
-
width="13"
|
|
7953
|
-
height="13"
|
|
7954
|
-
rx="2"
|
|
7955
|
-
/>
|
|
7956
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
7957
|
-
</svg>
|
|
7958
|
-
`}
|
|
8805
|
+
d="M6 18 18 6M6 6l12 12"
|
|
8806
|
+
/>
|
|
8807
|
+
</svg>
|
|
7959
8808
|
</button>
|
|
7960
|
-
|
|
7961
|
-
</div>
|
|
7962
|
-
`)}
|
|
7963
|
-
</div>
|
|
7964
|
-
</div>
|
|
7965
|
-
|
|
7966
|
-
${this._error ? b `
|
|
7967
|
-
<div class="error">
|
|
7968
|
-
<span>${this._error}</span>
|
|
7969
|
-
<button class="error-dismiss" @click=${this._handleErrorDismiss}>
|
|
7970
|
-
<svg
|
|
7971
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
7972
|
-
fill="none"
|
|
7973
|
-
viewBox="0 0 24 24"
|
|
7974
|
-
stroke-width="2"
|
|
7975
|
-
stroke="currentColor"
|
|
7976
|
-
class="error-dismiss-icon"
|
|
7977
|
-
>
|
|
7978
|
-
<path
|
|
7979
|
-
stroke-linecap="round"
|
|
7980
|
-
stroke-linejoin="round"
|
|
7981
|
-
d="M6 18 18 6M6 6l12 12"
|
|
7982
|
-
/>
|
|
7983
|
-
</svg>
|
|
7984
|
-
</button>
|
|
7985
|
-
</div>
|
|
8809
|
+
</div>
|
|
7986
8810
|
` : this._contextLimitReached ? b `
|
|
7987
8811
|
<div class="context-limit" role="alert">
|
|
7988
8812
|
${this._contextLimitMessage
|
|
@@ -8014,68 +8838,68 @@
|
|
|
8014
8838
|
</div>
|
|
8015
8839
|
` : A}
|
|
8016
8840
|
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8841
|
+
<div class="footer">
|
|
8842
|
+
${this._pendingConversationFiles.length ? b `
|
|
8843
|
+
<div class="composer-attachments">
|
|
8844
|
+
${c(this._pendingConversationFiles, (p) => p.localId, (p) => b `
|
|
8845
|
+
<div
|
|
8846
|
+
class=${e({
|
|
8023
8847
|
chip: true,
|
|
8024
8848
|
'chip--doc': !p.previewUrl,
|
|
8025
8849
|
})}
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8850
|
+
title=${p.name}
|
|
8851
|
+
>
|
|
8852
|
+
<div
|
|
8853
|
+
class=${e({
|
|
8030
8854
|
'chip-body': true,
|
|
8031
8855
|
'chip-body--image': Boolean(p.previewUrl),
|
|
8032
8856
|
})}
|
|
8033
|
-
|
|
8034
|
-
|
|
8857
|
+
>
|
|
8858
|
+
${p.previewUrl
|
|
8035
8859
|
? b `<img src=${p.previewUrl} alt=${p.name} />`
|
|
8036
8860
|
: b `
|
|
8037
|
-
|
|
8038
|
-
|
|
8861
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
8862
|
+
<span class="chip-name">${p.name}</span>
|
|
8039
8863
|
`}
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8864
|
+
</div>
|
|
8865
|
+
<button
|
|
8866
|
+
class="chip-remove"
|
|
8867
|
+
title="Remove"
|
|
8868
|
+
@click=${() => this._handlePendingRemove(p.localId)}
|
|
8869
|
+
>
|
|
8870
|
+
<svg
|
|
8871
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8872
|
+
fill="none"
|
|
8873
|
+
viewBox="0 0 24 24"
|
|
8874
|
+
stroke-width="1.75"
|
|
8875
|
+
stroke="currentColor"
|
|
8876
|
+
>
|
|
8877
|
+
<path
|
|
8878
|
+
stroke-linecap="round"
|
|
8879
|
+
stroke-linejoin="round"
|
|
8880
|
+
d="M6 18 18 6M6 6l12 12"
|
|
8881
|
+
/>
|
|
8882
|
+
</svg>
|
|
8883
|
+
</button>
|
|
8884
|
+
</div>
|
|
8061
8885
|
`)}
|
|
8062
|
-
|
|
8886
|
+
</div>
|
|
8063
8887
|
` : A}
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8888
|
+
<input
|
|
8889
|
+
class="file-input"
|
|
8890
|
+
type="file"
|
|
8891
|
+
accept=${ACCEPTED_FILE_EXTENSIONS.map((ext) => '.' + ext).join(',')}
|
|
8892
|
+
multiple
|
|
8893
|
+
hidden
|
|
8894
|
+
@change=${this._handleFileInputChange}
|
|
8895
|
+
/>
|
|
8896
|
+
${this._skillMenuOpened ? this._renderSkillMenu() : A}
|
|
8897
|
+
<div class="input-wrapper">
|
|
8898
|
+
<button
|
|
8075
8899
|
class="input-plus"
|
|
8076
8900
|
@click=${this._handlePlusClick}
|
|
8077
|
-
|
|
8078
|
-
|
|
8901
|
+
>+</button>
|
|
8902
|
+
<textarea
|
|
8079
8903
|
class="input"
|
|
8080
8904
|
.value=${this._inputValue}
|
|
8081
8905
|
placeholder=${this.placeholder}
|
|
@@ -8083,31 +8907,32 @@
|
|
|
8083
8907
|
@input=${this._handleInputChange}
|
|
8084
8908
|
@keydown=${this._handleKeyDown}
|
|
8085
8909
|
@paste=${this._handlePaste}
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
${
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
<svg
|
|
8096
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8097
|
-
fill="currentColor"
|
|
8098
|
-
viewBox="0 0 24 24"
|
|
8099
|
-
class="mic-stop-icon"
|
|
8100
|
-
aria-hidden="true"
|
|
8910
|
+
></textarea>
|
|
8911
|
+
${this._renderModeSelector()}
|
|
8912
|
+
${window.SpeechRecognition || window.webkitSpeechRecognition ? b `
|
|
8913
|
+
${this._isDictating ? b `
|
|
8914
|
+
<button
|
|
8915
|
+
class="mic-stop"
|
|
8916
|
+
title="Stop dictation"
|
|
8917
|
+
aria-label="Stop dictation"
|
|
8918
|
+
@click=${this._handleMicClick}
|
|
8101
8919
|
>
|
|
8102
|
-
<
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8920
|
+
<svg
|
|
8921
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8922
|
+
fill="currentColor"
|
|
8923
|
+
viewBox="0 0 24 24"
|
|
8924
|
+
class="mic-stop-icon"
|
|
8925
|
+
aria-hidden="true"
|
|
8926
|
+
>
|
|
8927
|
+
<rect
|
|
8928
|
+
x="6"
|
|
8929
|
+
y="6"
|
|
8930
|
+
width="12"
|
|
8931
|
+
height="12"
|
|
8932
|
+
rx="2"
|
|
8933
|
+
/>
|
|
8934
|
+
</svg>
|
|
8935
|
+
</button>
|
|
8111
8936
|
` : b `
|
|
8112
8937
|
<button
|
|
8113
8938
|
class="mic"
|
|
@@ -8133,35 +8958,35 @@
|
|
|
8133
8958
|
</button>
|
|
8134
8959
|
`}
|
|
8135
8960
|
` : A}
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
>
|
|
8142
|
-
<svg
|
|
8143
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8144
|
-
fill="currentColor"
|
|
8145
|
-
viewBox="0 0 24 24"
|
|
8146
|
-
class="stop-icon"
|
|
8961
|
+
${this._isStreaming ? b `
|
|
8962
|
+
<button
|
|
8963
|
+
class="stop"
|
|
8964
|
+
@click=${this._handleStop}
|
|
8965
|
+
title="Stop generating"
|
|
8147
8966
|
>
|
|
8148
|
-
<
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8967
|
+
<svg
|
|
8968
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8969
|
+
fill="currentColor"
|
|
8970
|
+
viewBox="0 0 24 24"
|
|
8971
|
+
class="stop-icon"
|
|
8972
|
+
>
|
|
8973
|
+
<rect
|
|
8974
|
+
x="6"
|
|
8975
|
+
y="6"
|
|
8976
|
+
width="12"
|
|
8977
|
+
height="12"
|
|
8978
|
+
rx="2"
|
|
8979
|
+
/>
|
|
8980
|
+
</svg>
|
|
8981
|
+
</button>
|
|
8157
8982
|
` : b `
|
|
8158
8983
|
<button
|
|
8159
8984
|
class=${e({
|
|
8160
8985
|
'send': true,
|
|
8161
|
-
'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
|
|
8986
|
+
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length) || this._pendingActions.length > 0,
|
|
8162
8987
|
})}
|
|
8163
8988
|
@click=${this._handleSubmit}
|
|
8164
|
-
title
|
|
8989
|
+
title=${this._pendingActions.length ? 'Approve or deny the pending action first' : 'Send message'}
|
|
8165
8990
|
>
|
|
8166
8991
|
<svg
|
|
8167
8992
|
width="16"
|
|
@@ -8177,12 +9002,12 @@
|
|
|
8177
9002
|
</svg>
|
|
8178
9003
|
</button>
|
|
8179
9004
|
`}
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
9005
|
+
</div>
|
|
9006
|
+
</div>
|
|
9007
|
+
${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
|
|
9008
|
+
${this._renderDrawer()}
|
|
9009
|
+
</div>
|
|
9010
|
+
`;
|
|
8186
9011
|
}
|
|
8187
9012
|
};
|
|
8188
9013
|
exports.Chatbot.styles = i$6 `
|
|
@@ -8295,6 +9120,14 @@
|
|
|
8295
9120
|
box-shadow: none;
|
|
8296
9121
|
border: none;
|
|
8297
9122
|
animation: none;
|
|
9123
|
+
/* Reserve space for the drawer by animating padding-right in sync with the
|
|
9124
|
+
drawer's slide, so the chat column shrinks rather than being overlaid. */
|
|
9125
|
+
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
9126
|
+
}
|
|
9127
|
+
|
|
9128
|
+
/* Track the cursor 1:1 while dragging the drawer's resize handle. */
|
|
9129
|
+
:host([variant='inline']) .panel--resizing-debug {
|
|
9130
|
+
transition: none;
|
|
8298
9131
|
}
|
|
8299
9132
|
|
|
8300
9133
|
@keyframes chatbot-panel-in {
|
|
@@ -8362,16 +9195,31 @@
|
|
|
8362
9195
|
color: #000000;
|
|
8363
9196
|
}
|
|
8364
9197
|
|
|
8365
|
-
.header-action--active {
|
|
8366
|
-
background: rgba(0, 0, 0, 0.08);
|
|
8367
|
-
color: #000000;
|
|
8368
|
-
}
|
|
8369
|
-
|
|
8370
9198
|
.header-action-icon {
|
|
8371
9199
|
width: 20px;
|
|
8372
9200
|
height: 20px;
|
|
8373
9201
|
}
|
|
8374
9202
|
|
|
9203
|
+
.settings-section {
|
|
9204
|
+
padding: 14px 20px;
|
|
9205
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
9206
|
+
}
|
|
9207
|
+
|
|
9208
|
+
.settings-section:last-child {
|
|
9209
|
+
border-bottom: none;
|
|
9210
|
+
}
|
|
9211
|
+
|
|
9212
|
+
.settings-section__title {
|
|
9213
|
+
font-size: 14px;
|
|
9214
|
+
font-weight: 600;
|
|
9215
|
+
color: #000000;
|
|
9216
|
+
margin-bottom: 12px;
|
|
9217
|
+
}
|
|
9218
|
+
|
|
9219
|
+
.settings-row {
|
|
9220
|
+
padding: 6px 0;
|
|
9221
|
+
}
|
|
9222
|
+
|
|
8375
9223
|
/* Messages */
|
|
8376
9224
|
/* Full-width scroll container — keeps the scrollbar at the panel edge. */
|
|
8377
9225
|
.messages {
|
|
@@ -8452,6 +9300,138 @@
|
|
|
8452
9300
|
gap: 24px;
|
|
8453
9301
|
}
|
|
8454
9302
|
|
|
9303
|
+
/* An agent turn wraps all of its messages (reasoning, tool lines, text,
|
|
9304
|
+
decision) so the scroll-to-top runway can sit on the whole turn and content
|
|
9305
|
+
streams inside it instead of stacking as siblings below the runway. A flex
|
|
9306
|
+
column with the same 36px gap as .messages-inner, so item spacing is
|
|
9307
|
+
unchanged whether grouped or not. Full width so its children align exactly
|
|
9308
|
+
as before (assistant left, tool/decision left). */
|
|
9309
|
+
.agent-turn {
|
|
9310
|
+
display: flex;
|
|
9311
|
+
flex-direction: column;
|
|
9312
|
+
gap: 36px;
|
|
9313
|
+
width: 100%;
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9316
|
+
/* Tool-activity line — "the assistant ran X" row in the transcript. Same text
|
|
9317
|
+
style as any other message (black, 14px); a leading › chevron marks it as a
|
|
9318
|
+
step (in the same text style, not an icon). */
|
|
9319
|
+
.tool-activity {
|
|
9320
|
+
align-self: flex-start;
|
|
9321
|
+
display: flex;
|
|
9322
|
+
align-items: baseline;
|
|
9323
|
+
gap: 6px;
|
|
9324
|
+
font-size: 14px;
|
|
9325
|
+
color: #000000;
|
|
9326
|
+
/* Keep the chevron + name + input on a single line; input truncates if long. */
|
|
9327
|
+
white-space: nowrap;
|
|
9328
|
+
}
|
|
9329
|
+
|
|
9330
|
+
.tool-activity--error {
|
|
9331
|
+
color: var(--kr-chatbot-error-text);
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
/* Leading chevron — plain text in the same style as the name, so it reads as
|
|
9335
|
+
one label rather than an icon. */
|
|
9336
|
+
.tool-activity__chevron {
|
|
9337
|
+
flex-shrink: 0;
|
|
9338
|
+
}
|
|
9339
|
+
|
|
9340
|
+
/* The tool name never shrinks — it's the input that truncates if the header
|
|
9341
|
+
runs out of room. */
|
|
9342
|
+
.tool-activity__label {
|
|
9343
|
+
flex-shrink: 0;
|
|
9344
|
+
}
|
|
9345
|
+
|
|
9346
|
+
/* The tool input rendered inline in the header, Claude-CLI style:
|
|
9347
|
+
"Read(chatbot.ts)". Same color/font as the tool name — it reads as one
|
|
9348
|
+
label. Truncates with an ellipsis so the header stays on one line. */
|
|
9349
|
+
.tool-activity__input {
|
|
9350
|
+
overflow: hidden;
|
|
9351
|
+
text-overflow: ellipsis;
|
|
9352
|
+
min-width: 0;
|
|
9353
|
+
}
|
|
9354
|
+
|
|
9355
|
+
/* Header + result grouped as one unit so the parent .message--assistant 24px
|
|
9356
|
+
gap doesn't push them apart. Rows sit tight together inside. */
|
|
9357
|
+
.tool-activity-block {
|
|
9358
|
+
display: flex;
|
|
9359
|
+
flex-direction: column;
|
|
9360
|
+
gap: 4px;
|
|
9361
|
+
}
|
|
9362
|
+
|
|
9363
|
+
/* The result block indented under the activity line with a ⎿ tree connector,
|
|
9364
|
+
like Claude CLI's tool output preview. Uses the normal message text style
|
|
9365
|
+
(14px, black) — only the ⎿ connector is muted. */
|
|
9366
|
+
.tool-activity__result {
|
|
9367
|
+
align-self: flex-start;
|
|
9368
|
+
display: flex;
|
|
9369
|
+
gap: 2px;
|
|
9370
|
+
/* Align the ⎿ connector's visible stroke under the first character of the
|
|
9371
|
+
tool name. The name is preceded by the "› " chevron (chevron width + 6px
|
|
9372
|
+
gap ≈ 11px), and the ⎿ glyph carries ~5.6px more internal left bearing
|
|
9373
|
+
than the name's first letter — so net-shift the block right to land the
|
|
9374
|
+
stroke under the name. */
|
|
9375
|
+
margin-left: 6px;
|
|
9376
|
+
font-size: 14px;
|
|
9377
|
+
color: #000000;
|
|
9378
|
+
}
|
|
9379
|
+
|
|
9380
|
+
.tool-activity__result--error {
|
|
9381
|
+
color: var(--kr-chatbot-error-text);
|
|
9382
|
+
}
|
|
9383
|
+
|
|
9384
|
+
.tool-activity__connector {
|
|
9385
|
+
flex-shrink: 0;
|
|
9386
|
+
color: rgba(0, 0, 0, 0.55);
|
|
9387
|
+
user-select: none;
|
|
9388
|
+
/* The ⎿ arm sits near the baseline, ~6px below the result's first-character
|
|
9389
|
+
vertical center. Nudge the glyph up so the arm tip lines up with that center. */
|
|
9390
|
+
transform: translateY(-6px);
|
|
9391
|
+
}
|
|
9392
|
+
|
|
9393
|
+
.tool-activity__result-body {
|
|
9394
|
+
/* Small horizontal gap between the ⎿ arm and the result text so they don't
|
|
9395
|
+
collide (the ⎿ glyph's own arm already extends most of the way). */
|
|
9396
|
+
margin: 0 0 0 6px;
|
|
9397
|
+
white-space: pre-wrap;
|
|
9398
|
+
word-break: break-word;
|
|
9399
|
+
font: inherit;
|
|
9400
|
+
}
|
|
9401
|
+
|
|
9402
|
+
.tool-activity__more {
|
|
9403
|
+
color: rgba(0, 0, 0, 0.4);
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9406
|
+
/* Decision line — the persistent record of an approve/deny (Rail 3). Uses the
|
|
9407
|
+
normal message text style (14px, black); a check/deny icon marks it. */
|
|
9408
|
+
.decision {
|
|
9409
|
+
align-self: flex-start;
|
|
9410
|
+
display: flex;
|
|
9411
|
+
align-items: center;
|
|
9412
|
+
gap: 8px;
|
|
9413
|
+
font-size: 14px;
|
|
9414
|
+
color: #000000;
|
|
9415
|
+
}
|
|
9416
|
+
|
|
9417
|
+
.decision--denied {
|
|
9418
|
+
color: var(--kr-chatbot-error-text);
|
|
9419
|
+
}
|
|
9420
|
+
|
|
9421
|
+
.decision__icon {
|
|
9422
|
+
display: inline-flex;
|
|
9423
|
+
align-items: center;
|
|
9424
|
+
justify-content: center;
|
|
9425
|
+
width: 16px;
|
|
9426
|
+
height: 16px;
|
|
9427
|
+
flex-shrink: 0;
|
|
9428
|
+
}
|
|
9429
|
+
|
|
9430
|
+
.decision__icon svg {
|
|
9431
|
+
width: 15px;
|
|
9432
|
+
height: 15px;
|
|
9433
|
+
}
|
|
9434
|
+
|
|
8455
9435
|
/* anywhere, NOT break-word: both break a long unbreakable run (a pasted URL) for
|
|
8456
9436
|
layout, but only anywhere also shrinks the element's intrinsic min-content width.
|
|
8457
9437
|
With break-word the URL still reports its full unbroken width as the minimum,
|
|
@@ -8706,32 +9686,139 @@
|
|
|
8706
9686
|
flex-shrink: 0;
|
|
8707
9687
|
}
|
|
8708
9688
|
|
|
8709
|
-
/*
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
9689
|
+
/* Action-confirmation card (Rail 3). Floats OVER the messages, anchored just
|
|
9690
|
+
above the composer — not part of the message scroll — so it stays put and
|
|
9691
|
+
in view while the transcript scrolls behind it. Its width matches the
|
|
9692
|
+
message text column: .messages-inner is 768px max with 32px padding, so the
|
|
9693
|
+
text column is 768 - 64. Plain-language ask + Yes/No for a non-technical
|
|
9694
|
+
user; no tool codes, no raw arguments. */
|
|
9695
|
+
.confirmation {
|
|
9696
|
+
position: absolute;
|
|
9697
|
+
left: 32px;
|
|
9698
|
+
right: 32px;
|
|
9699
|
+
/* Sits above the footer/composer with a clear gap between the two. */
|
|
9700
|
+
bottom: 104px;
|
|
9701
|
+
z-index: 6;
|
|
9702
|
+
max-width: 720px;
|
|
9703
|
+
margin: 0 auto;
|
|
9704
|
+
padding: 24px;
|
|
9705
|
+
border: 1px solid #d8d8d8;
|
|
9706
|
+
border-radius: 8px;
|
|
9707
|
+
background: var(--kr-chatbot-bg);
|
|
9708
|
+
color: var(--kr-chatbot-text);
|
|
9709
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
9710
|
+
/* Never taller than the panel leaves room for — scroll the card itself if a
|
|
9711
|
+
batched multi-action ask is long, rather than covering the whole panel. */
|
|
9712
|
+
max-height: calc(100% - 176px);
|
|
9713
|
+
overflow-y: auto;
|
|
8723
9714
|
}
|
|
8724
9715
|
|
|
8725
|
-
/*
|
|
8726
|
-
|
|
8727
|
-
|
|
9716
|
+
/* Inline variant (Claude-CLI style): sits IN the message stream as just
|
|
9717
|
+
another assistant message — no card chrome (padding/background/border/
|
|
9718
|
+
radius/shadow), no float offsets, no height cap. It flows and scrolls with
|
|
9719
|
+
the transcript. */
|
|
9720
|
+
.confirmation--inline {
|
|
9721
|
+
position: static;
|
|
9722
|
+
left: auto;
|
|
9723
|
+
right: auto;
|
|
9724
|
+
bottom: auto;
|
|
9725
|
+
z-index: auto;
|
|
9726
|
+
max-width: none;
|
|
9727
|
+
margin: 0;
|
|
9728
|
+
max-height: none;
|
|
9729
|
+
overflow: visible;
|
|
9730
|
+
padding: 0;
|
|
9731
|
+
border: none;
|
|
9732
|
+
border-radius: 0;
|
|
9733
|
+
background: none;
|
|
9734
|
+
box-shadow: none;
|
|
9735
|
+
/* scrollIntoView({ block: 'end' }) aligns the card's bottom to the viewport
|
|
9736
|
+
bottom; this margin makes it stop short so the Yes/No buttons aren't flush
|
|
9737
|
+
against (or clipped by) the fold. */
|
|
9738
|
+
scroll-margin-bottom: 24px;
|
|
9739
|
+
}
|
|
9740
|
+
|
|
9741
|
+
/* The raw arguments, pretty-printed. Wraps rather than scrolling — the whole
|
|
9742
|
+
card grows with the transcript (the point of the inline variant). */
|
|
9743
|
+
.confirmation__details-input {
|
|
9744
|
+
margin: 8px 0 0;
|
|
9745
|
+
border-radius: 6px;
|
|
9746
|
+
color: var(--kr-chatbot-text);
|
|
9747
|
+
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
|
|
9748
|
+
font-size: 13px;
|
|
9749
|
+
line-height: 1.5;
|
|
9750
|
+
white-space: pre-wrap;
|
|
9751
|
+
overflow-wrap: anywhere;
|
|
9752
|
+
}
|
|
9753
|
+
|
|
9754
|
+
/* "Run {tool}?" heading — names the tool being run. */
|
|
9755
|
+
.confirmation__title {
|
|
9756
|
+
font-size: 16px;
|
|
9757
|
+
font-weight: 600;
|
|
9758
|
+
margin-bottom: 12px;
|
|
9759
|
+
}
|
|
9760
|
+
|
|
9761
|
+
.confirmation__message {
|
|
9762
|
+
margin: 0;
|
|
9763
|
+
font-size: 14px;
|
|
9764
|
+
line-height: 1.5;
|
|
9765
|
+
}
|
|
9766
|
+
|
|
9767
|
+
/* Batch list: one bullet per pending tool — its name, then the model's
|
|
9768
|
+
plain-language intent under it. */
|
|
9769
|
+
.confirmation__list {
|
|
9770
|
+
margin: 0;
|
|
9771
|
+
padding-left: 20px;
|
|
9772
|
+
font-size: 14px;
|
|
9773
|
+
line-height: 1.5;
|
|
8728
9774
|
}
|
|
8729
9775
|
|
|
8730
|
-
.
|
|
9776
|
+
.confirmation__list-item:not(:last-child) {
|
|
9777
|
+
margin-bottom: 8px;
|
|
9778
|
+
}
|
|
9779
|
+
|
|
9780
|
+
.confirmation__list-name {
|
|
9781
|
+
display: block;
|
|
9782
|
+
font-weight: 600;
|
|
9783
|
+
}
|
|
9784
|
+
|
|
9785
|
+
.confirmation__list-desc {
|
|
9786
|
+
display: block;
|
|
9787
|
+
}
|
|
9788
|
+
|
|
9789
|
+
/* Buttons in a row; wrap on a narrow panel rather than overflowing the card. */
|
|
9790
|
+
.confirmation__actions {
|
|
9791
|
+
display: flex;
|
|
9792
|
+
flex-wrap: wrap;
|
|
9793
|
+
align-items: center;
|
|
9794
|
+
gap: 10px;
|
|
9795
|
+
margin-top: 20px;
|
|
9796
|
+
}
|
|
9797
|
+
|
|
9798
|
+
/* Keyboard-highlighted decision — a focus ring around the button, drawn
|
|
9799
|
+
outside its border so it reads over both the flat (Yes) and outline
|
|
9800
|
+
variants without recoloring or reshaping the button. Don't set
|
|
9801
|
+
border-radius here: that's the button's own (its pill shape); outline
|
|
9802
|
+
follows it and doesn't affect the button's size. */
|
|
9803
|
+
.confirmation__btn--highlighted {
|
|
9804
|
+
outline: 2px solid var(--kr-chatbot-primary);
|
|
9805
|
+
outline-offset: 2px;
|
|
9806
|
+
}
|
|
9807
|
+
|
|
9808
|
+
/*
|
|
9809
|
+
* Side drawer, docked to the right of the inline chat, holding the Settings
|
|
9810
|
+
* and Debug views. The chat column shrinks (padding-right) to make room; the
|
|
9811
|
+
* drawer does not overlay. It stays mounted and slides in/out via a transform,
|
|
9812
|
+
* so opening and closing animate; switching views just swaps the content
|
|
9813
|
+
* inside (no re-slide, no width change). One consistent resizable width across
|
|
9814
|
+
* both views; the width is set inline from _debugDrawerWidth.
|
|
9815
|
+
*/
|
|
9816
|
+
.drawer {
|
|
8731
9817
|
position: absolute;
|
|
8732
9818
|
top: 0;
|
|
8733
9819
|
right: 0;
|
|
8734
9820
|
bottom: 0;
|
|
9821
|
+
max-width: 100%;
|
|
8735
9822
|
background: var(--kr-chatbot-bg);
|
|
8736
9823
|
display: flex;
|
|
8737
9824
|
flex-direction: column;
|
|
@@ -8743,17 +9830,17 @@
|
|
|
8743
9830
|
|
|
8744
9831
|
/* While dragging the resize handle, suppress the slide transition so the
|
|
8745
9832
|
drawer tracks the cursor 1:1. */
|
|
8746
|
-
.
|
|
9833
|
+
.drawer--resizing {
|
|
8747
9834
|
transition: none;
|
|
8748
9835
|
}
|
|
8749
9836
|
|
|
8750
|
-
.
|
|
9837
|
+
.drawer--open {
|
|
8751
9838
|
transform: translateX(0);
|
|
8752
9839
|
box-shadow: -4px 0 20px rgba(10, 26, 46, 0.07);
|
|
8753
9840
|
}
|
|
8754
9841
|
|
|
8755
9842
|
/* Left-edge grab handle for resizing the drawer. */
|
|
8756
|
-
.
|
|
9843
|
+
.drawer__resize {
|
|
8757
9844
|
position: absolute;
|
|
8758
9845
|
top: 0;
|
|
8759
9846
|
left: 0;
|
|
@@ -8763,7 +9850,7 @@
|
|
|
8763
9850
|
z-index: 10;
|
|
8764
9851
|
}
|
|
8765
9852
|
|
|
8766
|
-
.
|
|
9853
|
+
.drawer__header {
|
|
8767
9854
|
display: flex;
|
|
8768
9855
|
align-items: center;
|
|
8769
9856
|
justify-content: space-between;
|
|
@@ -8773,13 +9860,13 @@
|
|
|
8773
9860
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
8774
9861
|
}
|
|
8775
9862
|
|
|
8776
|
-
.
|
|
9863
|
+
.drawer__title {
|
|
8777
9864
|
font-size: 14px;
|
|
8778
9865
|
font-weight: 600;
|
|
8779
9866
|
color: #000000;
|
|
8780
9867
|
}
|
|
8781
9868
|
|
|
8782
|
-
.
|
|
9869
|
+
.drawer__body {
|
|
8783
9870
|
flex: 1;
|
|
8784
9871
|
overflow-y: auto;
|
|
8785
9872
|
overflow-x: hidden;
|
|
@@ -8988,6 +10075,89 @@
|
|
|
8988
10075
|
margin-right: 4px;
|
|
8989
10076
|
}
|
|
8990
10077
|
|
|
10078
|
+
/* Mode selector — Gemini-style inline label + caret before the mic. Anchors
|
|
10079
|
+
its upward menu; sized so it never grows the single-row composer height. */
|
|
10080
|
+
.mode {
|
|
10081
|
+
position: relative;
|
|
10082
|
+
flex-shrink: 0;
|
|
10083
|
+
display: flex;
|
|
10084
|
+
align-items: center;
|
|
10085
|
+
}
|
|
10086
|
+
|
|
10087
|
+
.mode-trigger {
|
|
10088
|
+
display: inline-flex;
|
|
10089
|
+
align-items: center;
|
|
10090
|
+
gap: 2px;
|
|
10091
|
+
height: 34px;
|
|
10092
|
+
padding: 0 8px;
|
|
10093
|
+
border: none;
|
|
10094
|
+
background: transparent;
|
|
10095
|
+
color: var(--kr-chatbot-text);
|
|
10096
|
+
border-radius: 17px;
|
|
10097
|
+
cursor: pointer;
|
|
10098
|
+
font: inherit;
|
|
10099
|
+
font-size: 14px;
|
|
10100
|
+
white-space: nowrap;
|
|
10101
|
+
transition: background 0.15s;
|
|
10102
|
+
}
|
|
10103
|
+
|
|
10104
|
+
.mode-trigger:hover {
|
|
10105
|
+
background: #f8f9fc;
|
|
10106
|
+
}
|
|
10107
|
+
|
|
10108
|
+
.mode-trigger__caret {
|
|
10109
|
+
width: 16px;
|
|
10110
|
+
height: 16px;
|
|
10111
|
+
flex-shrink: 0;
|
|
10112
|
+
}
|
|
10113
|
+
|
|
10114
|
+
/* Menu opens upward, anchored to the trigger's right edge. */
|
|
10115
|
+
.mode-menu {
|
|
10116
|
+
position: absolute;
|
|
10117
|
+
bottom: calc(100% + 8px);
|
|
10118
|
+
right: 0;
|
|
10119
|
+
min-width: 220px;
|
|
10120
|
+
background: white;
|
|
10121
|
+
border: 1px solid #b0b0b0;
|
|
10122
|
+
border-radius: 8px;
|
|
10123
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
10124
|
+
padding: 6px 0;
|
|
10125
|
+
z-index: 10000;
|
|
10126
|
+
display: flex;
|
|
10127
|
+
flex-direction: column;
|
|
10128
|
+
}
|
|
10129
|
+
|
|
10130
|
+
.mode-menu__item {
|
|
10131
|
+
display: flex;
|
|
10132
|
+
flex-direction: column;
|
|
10133
|
+
gap: 1px;
|
|
10134
|
+
width: 100%;
|
|
10135
|
+
padding: 8px 14px;
|
|
10136
|
+
background: none;
|
|
10137
|
+
border: none;
|
|
10138
|
+
text-align: left;
|
|
10139
|
+
cursor: pointer;
|
|
10140
|
+
font: inherit;
|
|
10141
|
+
color: var(--kr-chatbot-text);
|
|
10142
|
+
}
|
|
10143
|
+
|
|
10144
|
+
.mode-menu__item:hover {
|
|
10145
|
+
background: #f3f4f6;
|
|
10146
|
+
}
|
|
10147
|
+
|
|
10148
|
+
.mode-menu__item--active {
|
|
10149
|
+
background: #eef1f6;
|
|
10150
|
+
}
|
|
10151
|
+
|
|
10152
|
+
.mode-menu__label {
|
|
10153
|
+
font-size: 14px;
|
|
10154
|
+
font-weight: 500;
|
|
10155
|
+
}
|
|
10156
|
+
|
|
10157
|
+
.mode-menu__desc {
|
|
10158
|
+
font-size: 12px;
|
|
10159
|
+
}
|
|
10160
|
+
|
|
8991
10161
|
.mic {
|
|
8992
10162
|
width: 34px;
|
|
8993
10163
|
height: 34px;
|
|
@@ -9646,6 +10816,15 @@
|
|
|
9646
10816
|
__decorate([
|
|
9647
10817
|
r()
|
|
9648
10818
|
], exports.Chatbot.prototype, "_contextLimitMessage", void 0);
|
|
10819
|
+
__decorate([
|
|
10820
|
+
r()
|
|
10821
|
+
], exports.Chatbot.prototype, "_pendingActions", void 0);
|
|
10822
|
+
__decorate([
|
|
10823
|
+
r()
|
|
10824
|
+
], exports.Chatbot.prototype, "_confirmationHighlighted", void 0);
|
|
10825
|
+
__decorate([
|
|
10826
|
+
r()
|
|
10827
|
+
], exports.Chatbot.prototype, "_confirmationInFlight", void 0);
|
|
9649
10828
|
__decorate([
|
|
9650
10829
|
r()
|
|
9651
10830
|
], exports.Chatbot.prototype, "_telemetry", void 0);
|
|
@@ -9657,7 +10836,10 @@
|
|
|
9657
10836
|
], exports.Chatbot.prototype, "_cacheCheckpoints", void 0);
|
|
9658
10837
|
__decorate([
|
|
9659
10838
|
r()
|
|
9660
|
-
], exports.Chatbot.prototype, "
|
|
10839
|
+
], exports.Chatbot.prototype, "_drawerOpen", void 0);
|
|
10840
|
+
__decorate([
|
|
10841
|
+
r()
|
|
10842
|
+
], exports.Chatbot.prototype, "_drawerView", void 0);
|
|
9661
10843
|
__decorate([
|
|
9662
10844
|
r()
|
|
9663
10845
|
], exports.Chatbot.prototype, "_debugDrawerWidth", void 0);
|
|
@@ -9706,6 +10888,21 @@
|
|
|
9706
10888
|
__decorate([
|
|
9707
10889
|
r()
|
|
9708
10890
|
], exports.Chatbot.prototype, "_isDictating", void 0);
|
|
10891
|
+
__decorate([
|
|
10892
|
+
r()
|
|
10893
|
+
], exports.Chatbot.prototype, "_mode", void 0);
|
|
10894
|
+
__decorate([
|
|
10895
|
+
r()
|
|
10896
|
+
], exports.Chatbot.prototype, "_modeMenuOpened", void 0);
|
|
10897
|
+
__decorate([
|
|
10898
|
+
r()
|
|
10899
|
+
], exports.Chatbot.prototype, "_showTools", void 0);
|
|
10900
|
+
__decorate([
|
|
10901
|
+
r()
|
|
10902
|
+
], exports.Chatbot.prototype, "_showToolInputs", void 0);
|
|
10903
|
+
__decorate([
|
|
10904
|
+
r()
|
|
10905
|
+
], exports.Chatbot.prototype, "_showConfirmationInput", void 0);
|
|
9709
10906
|
__decorate([
|
|
9710
10907
|
e$3('.panel')
|
|
9711
10908
|
], exports.Chatbot.prototype, "_panelEl", void 0);
|