@kodaris/krubble-app-components 1.0.113 → 1.0.115
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 +142 -22
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +1320 -459
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +1332 -472
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +859 -428
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +1318 -458
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +833 -402
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/krubble-app.umd.js
CHANGED
|
@@ -5550,6 +5550,28 @@
|
|
|
5550
5550
|
label: 'No',
|
|
5551
5551
|
},
|
|
5552
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
|
+
];
|
|
5553
5575
|
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
5554
5576
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
5555
5577
|
/**
|
|
@@ -5624,6 +5646,25 @@
|
|
|
5624
5646
|
constructor() {
|
|
5625
5647
|
super(...arguments);
|
|
5626
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;
|
|
5627
5668
|
this._reader = null;
|
|
5628
5669
|
this._decoder = new TextDecoder();
|
|
5629
5670
|
this._buffer = '';
|
|
@@ -5638,10 +5679,10 @@
|
|
|
5638
5679
|
this._dragStartTop = 0;
|
|
5639
5680
|
this._resizeStartW = 0;
|
|
5640
5681
|
this._resizeStartH = 0;
|
|
5641
|
-
/** Cursor X at
|
|
5642
|
-
this.
|
|
5643
|
-
/**
|
|
5644
|
-
this.
|
|
5682
|
+
/** Cursor X at drawer resize start. */
|
|
5683
|
+
this._drawerResizeStartX = 0;
|
|
5684
|
+
/** Drawer width at resize start. */
|
|
5685
|
+
this._drawerResizeStartW = 0;
|
|
5645
5686
|
/** Counter for depth-aware dragenter/dragleave tracking. */
|
|
5646
5687
|
this._dragDepth = 0;
|
|
5647
5688
|
/** Monotonic id source for pending attachments. */
|
|
@@ -5807,12 +5848,21 @@
|
|
|
5807
5848
|
this._iterationLog = [];
|
|
5808
5849
|
/** Latest cache-checkpoint info keyed by iteration, for the verbose table. */
|
|
5809
5850
|
this._cacheCheckpoints = {};
|
|
5810
|
-
/**
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
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). */
|
|
5863
|
+
this._drawerWidth = 430;
|
|
5864
|
+
/** True while the user is dragging the drawer's resize handle. */
|
|
5865
|
+
this._isDrawerResizing = false;
|
|
5816
5866
|
/** Files staged in the composer, before the message is sent. */
|
|
5817
5867
|
this._pendingConversationFiles = [];
|
|
5818
5868
|
/** True while a file is being dragged over the panel (shows the drop overlay). */
|
|
@@ -5862,6 +5912,31 @@
|
|
|
5862
5912
|
this._skillBrowseQuery = '';
|
|
5863
5913
|
/** True while the mic is live and speech is being transcribed into the composer. */
|
|
5864
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;
|
|
5865
5940
|
/**
|
|
5866
5941
|
* Close the `/` skill menu when the click lands outside it.
|
|
5867
5942
|
*
|
|
@@ -5872,6 +5947,14 @@
|
|
|
5872
5947
|
* inside click would look like an outside one.
|
|
5873
5948
|
*/
|
|
5874
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
|
+
}
|
|
5875
5958
|
if (!this._skillMenuOpened) {
|
|
5876
5959
|
return;
|
|
5877
5960
|
}
|
|
@@ -5976,23 +6059,23 @@
|
|
|
5976
6059
|
}
|
|
5977
6060
|
}
|
|
5978
6061
|
};
|
|
5979
|
-
this.
|
|
5980
|
-
if (!this.
|
|
6062
|
+
this._handleDrawerResizeMouseMove = (e) => {
|
|
6063
|
+
if (!this._isDrawerResizing) {
|
|
5981
6064
|
return;
|
|
5982
6065
|
}
|
|
5983
6066
|
// Handle is on the drawer's left edge; dragging left (negative dx) widens it.
|
|
5984
|
-
const dx = e.clientX - this.
|
|
6067
|
+
const dx = e.clientX - this._drawerResizeStartX;
|
|
5985
6068
|
const minW = 300;
|
|
5986
6069
|
// Never let the chat column shrink below 300px — cap the drawer at the
|
|
5987
6070
|
// container width minus that reserved chat width.
|
|
5988
6071
|
const minChatWidth = 300;
|
|
5989
6072
|
const maxW = Math.max(minW, this.offsetWidth - minChatWidth);
|
|
5990
|
-
this.
|
|
6073
|
+
this._drawerWidth = Math.max(minW, Math.min(this._drawerResizeStartW - dx, maxW));
|
|
5991
6074
|
};
|
|
5992
|
-
this.
|
|
5993
|
-
this.
|
|
5994
|
-
document.removeEventListener('mousemove', this.
|
|
5995
|
-
document.removeEventListener('mouseup', this.
|
|
6075
|
+
this._handleDrawerResizeMouseUp = () => {
|
|
6076
|
+
this._isDrawerResizing = false;
|
|
6077
|
+
document.removeEventListener('mousemove', this._handleDrawerResizeMouseMove);
|
|
6078
|
+
document.removeEventListener('mouseup', this._handleDrawerResizeMouseUp);
|
|
5996
6079
|
this._saveState();
|
|
5997
6080
|
};
|
|
5998
6081
|
}
|
|
@@ -6013,6 +6096,36 @@
|
|
|
6013
6096
|
return krubbleHttp.Http.fetch({ url: `/api/system/conversation/${this.conversationId}/messages` })
|
|
6014
6097
|
.then((r) => r.json())
|
|
6015
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
|
+
}
|
|
6016
6129
|
// Resolve this message's `conversationFileIDs` (a JSON array string of
|
|
6017
6130
|
// ids) into full file objects — each fetch response IS the
|
|
6018
6131
|
// KRChatbotConversationFile (id + niceName + mimeType), and niceName
|
|
@@ -6267,8 +6380,13 @@
|
|
|
6267
6380
|
const state = {
|
|
6268
6381
|
opened: String(this.opened),
|
|
6269
6382
|
message: this._inputValue,
|
|
6270
|
-
|
|
6271
|
-
|
|
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,
|
|
6389
|
+
drawerWidth: String(this._drawerWidth),
|
|
6272
6390
|
};
|
|
6273
6391
|
if (this._panelEl) {
|
|
6274
6392
|
const s = this._panelEl.style;
|
|
@@ -6308,10 +6426,19 @@
|
|
|
6308
6426
|
if (typeof state.message === 'string' && state.message.trim() !== '/') {
|
|
6309
6427
|
this._inputValue = state.message;
|
|
6310
6428
|
}
|
|
6311
|
-
|
|
6312
|
-
|
|
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
|
+
}
|
|
6439
|
+
const w = Number(state.drawerWidth);
|
|
6313
6440
|
if (w >= 300) {
|
|
6314
|
-
this.
|
|
6441
|
+
this._drawerWidth = w;
|
|
6315
6442
|
}
|
|
6316
6443
|
// The floating widget's open/layout state doesn't apply to inline mode.
|
|
6317
6444
|
if (this.variant !== 'inline' && state.opened === 'true') {
|
|
@@ -6611,6 +6738,29 @@
|
|
|
6611
6738
|
this._skillMenuOpened = true;
|
|
6612
6739
|
this._searchInvocableSkills('');
|
|
6613
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
|
+
}
|
|
6614
6764
|
/**
|
|
6615
6765
|
* The mic button toggles dictation. Speech is transcribed straight into the
|
|
6616
6766
|
* composer as editable text — the user still reviews and presses send, so a
|
|
@@ -7037,22 +7187,25 @@
|
|
|
7037
7187
|
content: '',
|
|
7038
7188
|
}];
|
|
7039
7189
|
this._isStreaming = true;
|
|
7040
|
-
// Scroll user's message to the top (Gemini-style).
|
|
7041
|
-
//
|
|
7042
|
-
//
|
|
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.
|
|
7043
7195
|
this.updateComplete.then(() => {
|
|
7044
7196
|
if (!this._messagesEl) {
|
|
7045
7197
|
return;
|
|
7046
7198
|
}
|
|
7047
|
-
// Clear
|
|
7048
|
-
this._messagesEl.querySelectorAll('.
|
|
7199
|
+
// Clear runway from previous turns.
|
|
7200
|
+
this._messagesEl.querySelectorAll('.agent-turn').forEach((el) => {
|
|
7049
7201
|
el.style.minHeight = '';
|
|
7050
7202
|
});
|
|
7051
|
-
const
|
|
7052
|
-
const userMsgEl =
|
|
7053
|
-
const
|
|
7054
|
-
|
|
7055
|
-
|
|
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';
|
|
7056
7209
|
}
|
|
7057
7210
|
if (userMsgEl) {
|
|
7058
7211
|
this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
|
|
@@ -7134,6 +7287,9 @@
|
|
|
7134
7287
|
const body = {
|
|
7135
7288
|
userText: params.message,
|
|
7136
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,
|
|
7137
7293
|
};
|
|
7138
7294
|
// Ground the model in where the user is and what they've highlighted.
|
|
7139
7295
|
// Only sent when present so an empty selection / failed capture doesn't
|
|
@@ -7232,30 +7388,36 @@
|
|
|
7232
7388
|
}
|
|
7233
7389
|
this._confirmationInFlight = true;
|
|
7234
7390
|
this._error = '';
|
|
7235
|
-
// The
|
|
7236
|
-
//
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
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();
|
|
7243
7399
|
this._isStreaming = true;
|
|
7244
7400
|
this._userHasScrolledUp = false;
|
|
7245
7401
|
this.send({
|
|
7246
7402
|
message: '',
|
|
7247
|
-
messages: this.messages
|
|
7403
|
+
messages: this.messages,
|
|
7248
7404
|
conversationFiles: [],
|
|
7249
7405
|
pageContext: '',
|
|
7250
7406
|
selectionContext: '',
|
|
7251
7407
|
confirmation: decision,
|
|
7252
7408
|
}).then((response) => {
|
|
7253
|
-
|
|
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.
|
|
7254
7412
|
if (!response.ok) {
|
|
7413
|
+
this._confirmationInFlight = false;
|
|
7414
|
+
btn?.reset();
|
|
7255
7415
|
this._handleStreamError('Request failed: ' + response.status);
|
|
7256
7416
|
return;
|
|
7257
7417
|
}
|
|
7258
7418
|
if (!response.body) {
|
|
7419
|
+
this._confirmationInFlight = false;
|
|
7420
|
+
btn?.reset();
|
|
7259
7421
|
this._handleStreamError('No response body');
|
|
7260
7422
|
return;
|
|
7261
7423
|
}
|
|
@@ -7265,11 +7427,13 @@
|
|
|
7265
7427
|
this._readStreamChunk();
|
|
7266
7428
|
}).catch((err) => {
|
|
7267
7429
|
this._confirmationInFlight = false;
|
|
7430
|
+
btn?.reset();
|
|
7268
7431
|
if (err.name !== 'AbortError') {
|
|
7269
7432
|
this._handleStreamError(err.message);
|
|
7270
7433
|
}
|
|
7271
7434
|
});
|
|
7272
7435
|
}
|
|
7436
|
+
/** Clear the in-flight confirmation flags (error paths, or once resolved). */
|
|
7273
7437
|
/**
|
|
7274
7438
|
* Capture ambient "where is the user / what are they looking at" context at
|
|
7275
7439
|
* send time, so the AI is grounded without the host passing anything in.
|
|
@@ -7343,18 +7507,36 @@
|
|
|
7343
7507
|
_handleContextWarningDismiss() {
|
|
7344
7508
|
this._contextWarning = '';
|
|
7345
7509
|
}
|
|
7346
|
-
|
|
7347
|
-
|
|
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;
|
|
7348
7530
|
this._saveState();
|
|
7349
7531
|
}
|
|
7350
|
-
|
|
7351
|
-
this.
|
|
7352
|
-
this.
|
|
7353
|
-
this.
|
|
7532
|
+
_handleDrawerResizeMouseDown(e) {
|
|
7533
|
+
this._isDrawerResizing = true;
|
|
7534
|
+
this._drawerResizeStartX = e.clientX;
|
|
7535
|
+
this._drawerResizeStartW = this._drawerWidth;
|
|
7354
7536
|
e.preventDefault();
|
|
7355
7537
|
e.stopPropagation();
|
|
7356
|
-
document.addEventListener('mousemove', this.
|
|
7357
|
-
document.addEventListener('mouseup', this.
|
|
7538
|
+
document.addEventListener('mousemove', this._handleDrawerResizeMouseMove);
|
|
7539
|
+
document.addEventListener('mouseup', this._handleDrawerResizeMouseUp);
|
|
7358
7540
|
}
|
|
7359
7541
|
_readStreamChunk() {
|
|
7360
7542
|
if (!this._reader) {
|
|
@@ -7400,28 +7582,54 @@
|
|
|
7400
7582
|
}));
|
|
7401
7583
|
if (data.type === 'REASONING_PART') {
|
|
7402
7584
|
if (data.message) {
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
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 = '';
|
|
7410
7591
|
}
|
|
7592
|
+
target.reasoning += String(data.message);
|
|
7593
|
+
this.requestUpdate();
|
|
7411
7594
|
}
|
|
7412
7595
|
}
|
|
7413
7596
|
else if (data.type === 'RESPONSE_PART') {
|
|
7414
7597
|
if (data.message) {
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
this.messages[lastIndex].content += String(data.message);
|
|
7418
|
-
this.requestUpdate();
|
|
7419
|
-
}
|
|
7598
|
+
this._ensureAssistantBubble().content += String(data.message);
|
|
7599
|
+
this.requestUpdate();
|
|
7420
7600
|
}
|
|
7421
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
|
+
}
|
|
7422
7622
|
else if (data.type === 'ERROR') {
|
|
7423
7623
|
this._handleStreamError(String(data.message || 'Unknown error'));
|
|
7424
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
|
+
}
|
|
7425
7633
|
else if (data.type === 'CONVERSATION_TOKEN_WARNING') {
|
|
7426
7634
|
// User-facing: friendly "conversation is getting long" guidance.
|
|
7427
7635
|
// Text only, never sent to the model — render verbatim as a banner.
|
|
@@ -7432,16 +7640,14 @@
|
|
|
7432
7640
|
// (it was created with a truncated first-message placeholder that we
|
|
7433
7641
|
// deliberately never show). Adopt it as the header label and pass it
|
|
7434
7642
|
// up so consumers can update their own chrome, e.g. a breadcrumb.
|
|
7435
|
-
if (data.data) {
|
|
7436
|
-
const title = String(data.data.title
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
}));
|
|
7444
|
-
}
|
|
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
|
+
}));
|
|
7445
7651
|
}
|
|
7446
7652
|
}
|
|
7447
7653
|
else if (data.type === 'CONVERSATION_LIMIT_REACHED') {
|
|
@@ -7513,6 +7719,24 @@
|
|
|
7513
7719
|
}
|
|
7514
7720
|
});
|
|
7515
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
|
+
}
|
|
7516
7740
|
/**
|
|
7517
7741
|
* End the current turn: release the turn lock and the reader. Called on the
|
|
7518
7742
|
* primary unlock signal (`finished:true`) and the fallback (stream close).
|
|
@@ -7537,6 +7761,17 @@
|
|
|
7537
7761
|
&& !this.messages[lastIndex].reasoning) {
|
|
7538
7762
|
this.messages = this.messages.slice(0, -1);
|
|
7539
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
|
+
}
|
|
7540
7775
|
}
|
|
7541
7776
|
}
|
|
7542
7777
|
_handleStreamError(message) {
|
|
@@ -7556,33 +7791,107 @@
|
|
|
7556
7791
|
}
|
|
7557
7792
|
// --- Render ---
|
|
7558
7793
|
/**
|
|
7559
|
-
* The
|
|
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
|
|
7560
7844
|
* OVER the messages, anchored just above the composer — not part of the message
|
|
7561
7845
|
* scroll — so it stays in view while the transcript scrolls behind it.
|
|
7562
7846
|
*
|
|
7563
|
-
* Written for a non-technical user
|
|
7564
|
-
*
|
|
7565
|
-
*
|
|
7566
|
-
*
|
|
7567
|
-
*
|
|
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.
|
|
7568
7853
|
*/
|
|
7569
7854
|
_renderConfirmation() {
|
|
7570
|
-
//
|
|
7571
|
-
// (and only) pending action.
|
|
7572
|
-
const action = this._pendingActions[0];
|
|
7573
|
-
// Title names the tool being run ("Run {tool}?").
|
|
7855
|
+
// Title: name the single tool, else a generic header for a batch.
|
|
7574
7856
|
let title = 'Run this action?';
|
|
7575
|
-
if (
|
|
7576
|
-
title =
|
|
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}"?`;
|
|
7577
7862
|
}
|
|
7578
7863
|
return b `
|
|
7579
7864
|
<div
|
|
7580
|
-
class
|
|
7865
|
+
class=${e({
|
|
7866
|
+
'confirmation': true,
|
|
7867
|
+
'confirmation--inline': this._inlineConfirm,
|
|
7868
|
+
})}
|
|
7581
7869
|
role="alertdialog"
|
|
7582
7870
|
aria-label="Confirm before continuing"
|
|
7583
7871
|
>
|
|
7584
7872
|
<div class="confirmation__title">${title}</div>
|
|
7585
|
-
|
|
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
|
+
`}
|
|
7586
7895
|
<div class="confirmation__actions">
|
|
7587
7896
|
${CONFIRMATION_DECISIONS.map((option, index) => b `
|
|
7588
7897
|
<kr-button
|
|
@@ -7593,7 +7902,7 @@
|
|
|
7593
7902
|
variant=${option.decision === 'approve' ? 'flat' : 'outline'}
|
|
7594
7903
|
color=${option.decision === 'reject' ? 'secondary' : 'primary'}
|
|
7595
7904
|
size="small"
|
|
7596
|
-
|
|
7905
|
+
data-decision=${option.decision}
|
|
7597
7906
|
@click=${() => this._handleConfirmationDecision(option.decision)}
|
|
7598
7907
|
>${option.label}</kr-button>
|
|
7599
7908
|
`)}
|
|
@@ -7815,26 +8124,37 @@
|
|
|
7815
8124
|
* three scopes from the latest TOKEN_USAGE event — context gauge, this turn,
|
|
7816
8125
|
* lifetime — plus a verbose per-iteration table. Never customer-facing.
|
|
7817
8126
|
*/
|
|
7818
|
-
|
|
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;
|
|
7819
8139
|
return b `
|
|
7820
8140
|
<div
|
|
7821
8141
|
class=${e({
|
|
7822
|
-
'
|
|
7823
|
-
'
|
|
7824
|
-
'
|
|
8142
|
+
'drawer': true,
|
|
8143
|
+
'drawer--open': this._drawerOpen,
|
|
8144
|
+
'drawer--resizing': this._isDrawerResizing,
|
|
7825
8145
|
})}
|
|
7826
|
-
style=${o({ width: `${this.
|
|
8146
|
+
style=${o({ width: `${this._drawerWidth}px` })}
|
|
7827
8147
|
>
|
|
7828
8148
|
<div
|
|
7829
|
-
class="
|
|
7830
|
-
@mousedown=${this.
|
|
8149
|
+
class="drawer__resize"
|
|
8150
|
+
@mousedown=${this._handleDrawerResizeMouseDown}
|
|
7831
8151
|
></div>
|
|
7832
|
-
<div class="
|
|
7833
|
-
<span class="
|
|
8152
|
+
<div class="drawer__header">
|
|
8153
|
+
<span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
|
|
7834
8154
|
<button
|
|
7835
8155
|
class="header-action"
|
|
7836
|
-
@click=${this.
|
|
7837
|
-
title="Close
|
|
8156
|
+
@click=${this._handleCloseDrawer}
|
|
8157
|
+
title="Close"
|
|
7838
8158
|
>
|
|
7839
8159
|
<svg
|
|
7840
8160
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -7852,105 +8172,163 @@
|
|
|
7852
8172
|
</svg>
|
|
7853
8173
|
</button>
|
|
7854
8174
|
</div>
|
|
7855
|
-
<div class="
|
|
7856
|
-
${this.
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
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>
|
|
7871
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>
|
|
7872
8229
|
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
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>
|
|
7888
8245
|
|
|
7889
|
-
|
|
7890
|
-
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
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>
|
|
7904
8261
|
` : b `
|
|
7905
8262
|
<div class="debug-empty">No telemetry yet — send a message to populate.</div>
|
|
7906
8263
|
`}
|
|
7907
8264
|
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
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) => {
|
|
7927
8284
|
const ckpt = this._cacheCheckpoints[it.iteration];
|
|
7928
8285
|
return b `
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
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>
|
|
7939
8296
|
`;
|
|
7940
8297
|
})}
|
|
7941
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
</div>
|
|
7945
|
-
` : A}
|
|
8298
|
+
</tbody>
|
|
8299
|
+
</table>
|
|
8300
|
+
</div>
|
|
7946
8301
|
</div>
|
|
7947
|
-
|
|
8302
|
+
` : A}
|
|
7948
8303
|
`;
|
|
7949
8304
|
}
|
|
7950
8305
|
render() {
|
|
7951
8306
|
// Debug affordances (drawer toggle, drawer, /test-* options) require the
|
|
7952
8307
|
// inline variant AND the server-side debug flag from aiFrameworkConfig.
|
|
7953
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._drawerWidth}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
|
+
});
|
|
7954
8332
|
return b `
|
|
7955
8333
|
${(this.hideToggle || this.variant === 'inline') ? A : b `
|
|
7956
8334
|
<button
|
|
@@ -7979,9 +8357,9 @@
|
|
|
7979
8357
|
class=${e({
|
|
7980
8358
|
'panel': true,
|
|
7981
8359
|
'panel--opened': this.opened,
|
|
7982
|
-
'panel--resizing-
|
|
8360
|
+
'panel--resizing-drawer': this._isDrawerResizing,
|
|
7983
8361
|
})}
|
|
7984
|
-
style=${o(
|
|
8362
|
+
style=${o(contentPadding)}
|
|
7985
8363
|
@dragenter=${this._handleDragEnter}
|
|
7986
8364
|
@dragover=${this._handleDragOver}
|
|
7987
8365
|
@dragleave=${this._handleDragLeave}
|
|
@@ -8054,13 +8432,35 @@
|
|
|
8054
8432
|
</div>
|
|
8055
8433
|
</div>
|
|
8056
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>
|
|
8057
8460
|
${isDebugEnabled ? b `
|
|
8058
8461
|
<button
|
|
8059
|
-
class
|
|
8060
|
-
|
|
8061
|
-
'header-action--active': this._debugPanelOpen,
|
|
8062
|
-
})}
|
|
8063
|
-
@click=${this._handleToggleDebugPanel}
|
|
8462
|
+
class="header-action"
|
|
8463
|
+
@click=${() => this._toggleDrawer('debug')}
|
|
8064
8464
|
title="Debug telemetry"
|
|
8065
8465
|
>
|
|
8066
8466
|
<svg
|
|
@@ -8111,141 +8511,302 @@
|
|
|
8111
8511
|
<span class="empty-text">What can I help you with?</span>
|
|
8112
8512
|
</div>
|
|
8113
8513
|
` : A}
|
|
8114
|
-
${
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
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.
|
|
8142
8593
|
return b `
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
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"
|
|
8635
|
+
>
|
|
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"
|
|
8148
8724
|
>
|
|
8149
|
-
<
|
|
8150
|
-
|
|
8151
|
-
|
|
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"
|
|
8152
8742
|
/>
|
|
8153
|
-
|
|
8154
|
-
|
|
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;
|
|
8155
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';
|
|
8156
8771
|
return b `
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
<span
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
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
|
+
`;
|
|
8167
8784
|
})}
|
|
8168
|
-
</div>
|
|
8169
|
-
` : A}
|
|
8170
|
-
${msg.content ? b `
|
|
8171
|
-
<div class="message-content">
|
|
8172
|
-
${msg.type === 'user'
|
|
8173
|
-
? this._renderUserContent(msg.content)
|
|
8174
|
-
: o$1(marked.parse(msg.content, { breaks: true }))}
|
|
8175
|
-
</div>
|
|
8176
|
-
` : A}
|
|
8177
|
-
${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? b `
|
|
8178
|
-
<div class="typing">
|
|
8179
|
-
<span></span>
|
|
8180
|
-
<span></span>
|
|
8181
|
-
<span></span>
|
|
8182
|
-
</div>
|
|
8183
|
-
` : A}
|
|
8184
|
-
${msg.content ? b `
|
|
8185
|
-
<button
|
|
8186
|
-
class="message-copy"
|
|
8187
|
-
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
8188
|
-
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
8189
|
-
>
|
|
8190
|
-
${this._copiedIndex === index ? b `
|
|
8191
|
-
<svg
|
|
8192
|
-
viewBox="0 0 24 24"
|
|
8193
|
-
fill="none"
|
|
8194
|
-
stroke="currentColor"
|
|
8195
|
-
stroke-width="1.5"
|
|
8196
|
-
stroke-linecap="round"
|
|
8197
|
-
stroke-linejoin="round"
|
|
8198
|
-
>
|
|
8199
|
-
<path d="M20 6 9 17l-5-5" />
|
|
8200
|
-
</svg>
|
|
8201
|
-
` : b `
|
|
8202
|
-
<svg
|
|
8203
|
-
viewBox="0 0 24 24"
|
|
8204
|
-
fill="none"
|
|
8205
|
-
stroke="currentColor"
|
|
8206
|
-
stroke-width="1.5"
|
|
8207
|
-
stroke-linecap="round"
|
|
8208
|
-
stroke-linejoin="round"
|
|
8209
|
-
>
|
|
8210
|
-
<rect
|
|
8211
|
-
x="9"
|
|
8212
|
-
y="9"
|
|
8213
|
-
width="13"
|
|
8214
|
-
height="13"
|
|
8215
|
-
rx="2"
|
|
8216
|
-
/>
|
|
8217
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
8218
|
-
</svg>
|
|
8219
|
-
`}
|
|
8220
|
-
</button>
|
|
8221
|
-
` : A}
|
|
8222
8785
|
</div>
|
|
8223
|
-
|
|
8224
|
-
</div>
|
|
8225
|
-
</div>
|
|
8786
|
+
</div>
|
|
8226
8787
|
|
|
8227
|
-
|
|
8788
|
+
${this._pendingActions.length && !this._inlineConfirm ? this._renderConfirmation() : A}
|
|
8228
8789
|
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
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
|
|
8803
|
+
stroke-linecap="round"
|
|
8804
|
+
stroke-linejoin="round"
|
|
8805
|
+
d="M6 18 18 6M6 6l12 12"
|
|
8806
|
+
/>
|
|
8807
|
+
</svg>
|
|
8808
|
+
</button>
|
|
8809
|
+
</div>
|
|
8249
8810
|
` : this._contextLimitReached ? b `
|
|
8250
8811
|
<div class="context-limit" role="alert">
|
|
8251
8812
|
${this._contextLimitMessage
|
|
@@ -8277,68 +8838,68 @@
|
|
|
8277
8838
|
</div>
|
|
8278
8839
|
` : A}
|
|
8279
8840
|
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
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({
|
|
8286
8847
|
chip: true,
|
|
8287
8848
|
'chip--doc': !p.previewUrl,
|
|
8288
8849
|
})}
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8292
|
-
|
|
8850
|
+
title=${p.name}
|
|
8851
|
+
>
|
|
8852
|
+
<div
|
|
8853
|
+
class=${e({
|
|
8293
8854
|
'chip-body': true,
|
|
8294
8855
|
'chip-body--image': Boolean(p.previewUrl),
|
|
8295
8856
|
})}
|
|
8296
|
-
|
|
8297
|
-
|
|
8857
|
+
>
|
|
8858
|
+
${p.previewUrl
|
|
8298
8859
|
? b `<img src=${p.previewUrl} alt=${p.name} />`
|
|
8299
8860
|
: b `
|
|
8300
|
-
|
|
8301
|
-
|
|
8861
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
8862
|
+
<span class="chip-name">${p.name}</span>
|
|
8302
8863
|
`}
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
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>
|
|
8324
8885
|
`)}
|
|
8325
|
-
|
|
8886
|
+
</div>
|
|
8326
8887
|
` : A}
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
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
|
|
8338
8899
|
class="input-plus"
|
|
8339
8900
|
@click=${this._handlePlusClick}
|
|
8340
|
-
|
|
8341
|
-
|
|
8901
|
+
>+</button>
|
|
8902
|
+
<textarea
|
|
8342
8903
|
class="input"
|
|
8343
8904
|
.value=${this._inputValue}
|
|
8344
8905
|
placeholder=${this.placeholder}
|
|
@@ -8346,31 +8907,32 @@
|
|
|
8346
8907
|
@input=${this._handleInputChange}
|
|
8347
8908
|
@keydown=${this._handleKeyDown}
|
|
8348
8909
|
@paste=${this._handlePaste}
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
${
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
<svg
|
|
8359
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8360
|
-
fill="currentColor"
|
|
8361
|
-
viewBox="0 0 24 24"
|
|
8362
|
-
class="mic-stop-icon"
|
|
8363
|
-
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}
|
|
8364
8919
|
>
|
|
8365
|
-
<
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
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>
|
|
8374
8936
|
` : b `
|
|
8375
8937
|
<button
|
|
8376
8938
|
class="mic"
|
|
@@ -8396,35 +8958,32 @@
|
|
|
8396
8958
|
</button>
|
|
8397
8959
|
`}
|
|
8398
8960
|
` : A}
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
>
|
|
8405
|
-
<svg
|
|
8406
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
8407
|
-
fill="currentColor"
|
|
8408
|
-
viewBox="0 0 24 24"
|
|
8409
|
-
class="stop-icon"
|
|
8961
|
+
${this._isStreaming ? b `
|
|
8962
|
+
<button
|
|
8963
|
+
class="stop"
|
|
8964
|
+
@click=${this._handleStop}
|
|
8965
|
+
title="Stop generating"
|
|
8410
8966
|
>
|
|
8411
|
-
<
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
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>
|
|
8420
8982
|
` : b `
|
|
8421
8983
|
<button
|
|
8422
8984
|
class=${e({
|
|
8423
8985
|
'send': true,
|
|
8424
|
-
|
|
8425
|
-
// normal message would be rejected until it's resolved.
|
|
8426
|
-
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length)
|
|
8427
|
-
|| this._pendingActions.length > 0,
|
|
8986
|
+
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length) || this._pendingActions.length > 0,
|
|
8428
8987
|
})}
|
|
8429
8988
|
@click=${this._handleSubmit}
|
|
8430
8989
|
title=${this._pendingActions.length ? 'Approve or deny the pending action first' : 'Send message'}
|
|
@@ -8443,12 +9002,12 @@
|
|
|
8443
9002
|
</svg>
|
|
8444
9003
|
</button>
|
|
8445
9004
|
`}
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
9005
|
+
</div>
|
|
9006
|
+
</div>
|
|
9007
|
+
${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
|
|
9008
|
+
${this._renderDrawer()}
|
|
9009
|
+
</div>
|
|
9010
|
+
`;
|
|
8452
9011
|
}
|
|
8453
9012
|
};
|
|
8454
9013
|
exports.Chatbot.styles = i$6 `
|
|
@@ -8561,6 +9120,14 @@
|
|
|
8561
9120
|
box-shadow: none;
|
|
8562
9121
|
border: none;
|
|
8563
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-drawer {
|
|
9130
|
+
transition: none;
|
|
8564
9131
|
}
|
|
8565
9132
|
|
|
8566
9133
|
@keyframes chatbot-panel-in {
|
|
@@ -8628,16 +9195,31 @@
|
|
|
8628
9195
|
color: #000000;
|
|
8629
9196
|
}
|
|
8630
9197
|
|
|
8631
|
-
.header-action--active {
|
|
8632
|
-
background: rgba(0, 0, 0, 0.08);
|
|
8633
|
-
color: #000000;
|
|
8634
|
-
}
|
|
8635
|
-
|
|
8636
9198
|
.header-action-icon {
|
|
8637
9199
|
width: 20px;
|
|
8638
9200
|
height: 20px;
|
|
8639
9201
|
}
|
|
8640
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
|
+
|
|
8641
9223
|
/* Messages */
|
|
8642
9224
|
/* Full-width scroll container — keeps the scrollbar at the panel edge. */
|
|
8643
9225
|
.messages {
|
|
@@ -8718,6 +9300,138 @@
|
|
|
8718
9300
|
gap: 24px;
|
|
8719
9301
|
}
|
|
8720
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
|
+
|
|
8721
9435
|
/* anywhere, NOT break-word: both break a long unbreakable run (a pasted URL) for
|
|
8722
9436
|
layout, but only anywhere also shrinks the element's intrinsic min-content width.
|
|
8723
9437
|
With break-word the URL still reports its full unbroken width as the minimum,
|
|
@@ -8999,11 +9713,49 @@
|
|
|
8999
9713
|
overflow-y: auto;
|
|
9000
9714
|
}
|
|
9001
9715
|
|
|
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
|
+
|
|
9002
9754
|
/* "Run {tool}?" heading — names the tool being run. */
|
|
9003
9755
|
.confirmation__title {
|
|
9004
9756
|
font-size: 16px;
|
|
9005
9757
|
font-weight: 600;
|
|
9006
|
-
margin-bottom:
|
|
9758
|
+
margin-bottom: 12px;
|
|
9007
9759
|
}
|
|
9008
9760
|
|
|
9009
9761
|
.confirmation__message {
|
|
@@ -9012,8 +9764,26 @@
|
|
|
9012
9764
|
line-height: 1.5;
|
|
9013
9765
|
}
|
|
9014
9766
|
|
|
9015
|
-
|
|
9016
|
-
|
|
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;
|
|
9774
|
+
}
|
|
9775
|
+
|
|
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;
|
|
9017
9787
|
}
|
|
9018
9788
|
|
|
9019
9789
|
/* Buttons in a row; wrap on a narrow panel rather than overflowing the card. */
|
|
@@ -9021,45 +9791,34 @@
|
|
|
9021
9791
|
display: flex;
|
|
9022
9792
|
flex-wrap: wrap;
|
|
9023
9793
|
align-items: center;
|
|
9024
|
-
gap:
|
|
9025
|
-
margin-top:
|
|
9794
|
+
gap: 10px;
|
|
9795
|
+
margin-top: 20px;
|
|
9026
9796
|
}
|
|
9027
9797
|
|
|
9028
|
-
/* Keyboard-highlighted decision — a focus ring around the button
|
|
9798
|
+
/* Keyboard-highlighted decision — a focus ring around the button, drawn
|
|
9029
9799
|
outside its border so it reads over both the flat (Yes) and outline
|
|
9030
|
-
variants without recoloring the button
|
|
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. */
|
|
9031
9803
|
.confirmation__btn--highlighted {
|
|
9032
|
-
border-radius: 8px;
|
|
9033
9804
|
outline: 2px solid var(--kr-chatbot-primary);
|
|
9034
9805
|
outline-offset: 2px;
|
|
9035
9806
|
}
|
|
9036
9807
|
|
|
9037
9808
|
/*
|
|
9038
|
-
*
|
|
9039
|
-
*
|
|
9040
|
-
* overlay.
|
|
9041
|
-
*
|
|
9042
|
-
*
|
|
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 _drawerWidth.
|
|
9043
9815
|
*/
|
|
9044
|
-
|
|
9045
|
-
* The chat column reserves space for the drawer by animating padding-right,
|
|
9046
|
-
* so opening the drawer shrinks the chat (header/messages/composer) rather
|
|
9047
|
-
* than overlaying it. Only applies in inline mode.
|
|
9048
|
-
*/
|
|
9049
|
-
:host([variant='inline']) .panel {
|
|
9050
|
-
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
9051
|
-
}
|
|
9052
|
-
|
|
9053
|
-
/* Track the cursor 1:1 while dragging the drawer's resize handle. */
|
|
9054
|
-
:host([variant='inline']) .panel--resizing-debug {
|
|
9055
|
-
transition: none;
|
|
9056
|
-
}
|
|
9057
|
-
|
|
9058
|
-
.debug-drawer {
|
|
9816
|
+
.drawer {
|
|
9059
9817
|
position: absolute;
|
|
9060
9818
|
top: 0;
|
|
9061
9819
|
right: 0;
|
|
9062
9820
|
bottom: 0;
|
|
9821
|
+
max-width: 100%;
|
|
9063
9822
|
background: var(--kr-chatbot-bg);
|
|
9064
9823
|
display: flex;
|
|
9065
9824
|
flex-direction: column;
|
|
@@ -9071,17 +9830,17 @@
|
|
|
9071
9830
|
|
|
9072
9831
|
/* While dragging the resize handle, suppress the slide transition so the
|
|
9073
9832
|
drawer tracks the cursor 1:1. */
|
|
9074
|
-
.
|
|
9833
|
+
.drawer--resizing {
|
|
9075
9834
|
transition: none;
|
|
9076
9835
|
}
|
|
9077
9836
|
|
|
9078
|
-
.
|
|
9837
|
+
.drawer--open {
|
|
9079
9838
|
transform: translateX(0);
|
|
9080
9839
|
box-shadow: -4px 0 20px rgba(10, 26, 46, 0.07);
|
|
9081
9840
|
}
|
|
9082
9841
|
|
|
9083
9842
|
/* Left-edge grab handle for resizing the drawer. */
|
|
9084
|
-
.
|
|
9843
|
+
.drawer__resize {
|
|
9085
9844
|
position: absolute;
|
|
9086
9845
|
top: 0;
|
|
9087
9846
|
left: 0;
|
|
@@ -9091,7 +9850,7 @@
|
|
|
9091
9850
|
z-index: 10;
|
|
9092
9851
|
}
|
|
9093
9852
|
|
|
9094
|
-
.
|
|
9853
|
+
.drawer__header {
|
|
9095
9854
|
display: flex;
|
|
9096
9855
|
align-items: center;
|
|
9097
9856
|
justify-content: space-between;
|
|
@@ -9101,13 +9860,13 @@
|
|
|
9101
9860
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
9102
9861
|
}
|
|
9103
9862
|
|
|
9104
|
-
.
|
|
9863
|
+
.drawer__title {
|
|
9105
9864
|
font-size: 14px;
|
|
9106
9865
|
font-weight: 600;
|
|
9107
9866
|
color: #000000;
|
|
9108
9867
|
}
|
|
9109
9868
|
|
|
9110
|
-
.
|
|
9869
|
+
.drawer__body {
|
|
9111
9870
|
flex: 1;
|
|
9112
9871
|
overflow-y: auto;
|
|
9113
9872
|
overflow-x: hidden;
|
|
@@ -9316,6 +10075,89 @@
|
|
|
9316
10075
|
margin-right: 4px;
|
|
9317
10076
|
}
|
|
9318
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
|
+
|
|
9319
10161
|
.mic {
|
|
9320
10162
|
width: 34px;
|
|
9321
10163
|
height: 34px;
|
|
@@ -9994,13 +10836,16 @@
|
|
|
9994
10836
|
], exports.Chatbot.prototype, "_cacheCheckpoints", void 0);
|
|
9995
10837
|
__decorate([
|
|
9996
10838
|
r()
|
|
9997
|
-
], exports.Chatbot.prototype, "
|
|
10839
|
+
], exports.Chatbot.prototype, "_drawerOpen", void 0);
|
|
9998
10840
|
__decorate([
|
|
9999
10841
|
r()
|
|
10000
|
-
], exports.Chatbot.prototype, "
|
|
10842
|
+
], exports.Chatbot.prototype, "_drawerView", void 0);
|
|
10001
10843
|
__decorate([
|
|
10002
10844
|
r()
|
|
10003
|
-
], exports.Chatbot.prototype, "
|
|
10845
|
+
], exports.Chatbot.prototype, "_drawerWidth", void 0);
|
|
10846
|
+
__decorate([
|
|
10847
|
+
r()
|
|
10848
|
+
], exports.Chatbot.prototype, "_isDrawerResizing", void 0);
|
|
10004
10849
|
__decorate([
|
|
10005
10850
|
r()
|
|
10006
10851
|
], exports.Chatbot.prototype, "_pendingConversationFiles", void 0);
|
|
@@ -10043,6 +10888,21 @@
|
|
|
10043
10888
|
__decorate([
|
|
10044
10889
|
r()
|
|
10045
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);
|
|
10046
10906
|
__decorate([
|
|
10047
10907
|
e$3('.panel')
|
|
10048
10908
|
], exports.Chatbot.prototype, "_panelEl", void 0);
|