@kodaris/krubble-app-components 1.0.113 → 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 +133 -13
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +1292 -431
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +1304 -444
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +856 -425
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +1290 -430
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +836 -405
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -12,7 +12,8 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
|
12
12
|
import { repeat } from 'lit/directives/repeat.js';
|
|
13
13
|
import { marked } from 'marked';
|
|
14
14
|
// Importing these named exports also registers their custom elements
|
|
15
|
-
// (kr-dialog, kr-tab-group, kr-tab, kr-text-field
|
|
15
|
+
// (kr-dialog, kr-tab-group, kr-tab, kr-text-field for the browse dialog;
|
|
16
|
+
// kr-checkbox for the settings panel's Tools toggles).
|
|
16
17
|
import { KRFilePreview, KRSnackbar } from '@kodaris/krubble-components';
|
|
17
18
|
import { KRHttp } from '@kodaris/krubble-http';
|
|
18
19
|
/**
|
|
@@ -34,6 +35,28 @@ const CONFIRMATION_DECISIONS = [
|
|
|
34
35
|
label: 'No',
|
|
35
36
|
},
|
|
36
37
|
];
|
|
38
|
+
/**
|
|
39
|
+
* The modes as shown in the composer's mode selector, in menu order. `label` is
|
|
40
|
+
* the short trigger text (Gemini-style inline label); `description` is the menu
|
|
41
|
+
* sublabel. Written for a non-technical user — no "guarded tool" jargon.
|
|
42
|
+
*/
|
|
43
|
+
const CHATBOT_MODES = [
|
|
44
|
+
{
|
|
45
|
+
mode: 'NORMAL',
|
|
46
|
+
label: 'Ask',
|
|
47
|
+
description: 'Asks before making changes',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
mode: 'EDIT',
|
|
51
|
+
label: 'Auto',
|
|
52
|
+
description: 'Makes changes automatically',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
mode: 'PLAN',
|
|
56
|
+
label: 'Plan',
|
|
57
|
+
description: 'Suggests a plan, makes no changes',
|
|
58
|
+
},
|
|
59
|
+
];
|
|
37
60
|
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
38
61
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
39
62
|
/**
|
|
@@ -108,6 +131,25 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
108
131
|
constructor() {
|
|
109
132
|
super(...arguments);
|
|
110
133
|
// --- Private properties ---
|
|
134
|
+
/**
|
|
135
|
+
* Internal toggle for how the action-confirmation prompt (Rail 3) is presented:
|
|
136
|
+
* - `false`: the compact card that floats over the messages, pinned above the
|
|
137
|
+
* composer — plain-language ask + Yes/Always/No only, no tool arguments.
|
|
138
|
+
* - `true`: the prompt renders inline in the message stream (Claude-CLI style),
|
|
139
|
+
* grows to any height (no inner scroll — the transcript scrolls), and offers a
|
|
140
|
+
* "Show details" disclosure of the raw tool input for technical users.
|
|
141
|
+
* Flip here to switch approaches; the floating path is unchanged when false.
|
|
142
|
+
*/
|
|
143
|
+
this._inlineConfirm = true;
|
|
144
|
+
/**
|
|
145
|
+
* Internal toggle for how the assistant's reasoning ("thinking") is presented:
|
|
146
|
+
* - `false`: the current collapsible grey "Thinking" disclosure (kept so we can
|
|
147
|
+
* revert).
|
|
148
|
+
* - `true`: reasoning renders as a plain agentic message — same text style as a
|
|
149
|
+
* normal assistant reply, no box, no collapse.
|
|
150
|
+
* Flip here to switch approaches.
|
|
151
|
+
*/
|
|
152
|
+
this._reasoningAsMessage = true;
|
|
111
153
|
this._reader = null;
|
|
112
154
|
this._decoder = new TextDecoder();
|
|
113
155
|
this._buffer = '';
|
|
@@ -291,11 +333,20 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
291
333
|
this._iterationLog = [];
|
|
292
334
|
/** Latest cache-checkpoint info keyed by iteration, for the verbose table. */
|
|
293
335
|
this._cacheCheckpoints = {};
|
|
294
|
-
/**
|
|
295
|
-
|
|
296
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Whether the side drawer is open (slid in). The drawer stays mounted and
|
|
338
|
+
* animates via a CSS transform, so this drives the slide and the content push.
|
|
339
|
+
*/
|
|
340
|
+
this._drawerOpen = false;
|
|
341
|
+
/**
|
|
342
|
+
* Which view the drawer shows — settings or debug. One drawer holds both; the
|
|
343
|
+
* header buttons switch this and the content/width swap inside. Never null, so
|
|
344
|
+
* the content stays visible while the drawer slides shut.
|
|
345
|
+
*/
|
|
346
|
+
this._drawerView = 'settings';
|
|
347
|
+
/** Current width of the drawer in px (user-resizable, persisted). */
|
|
297
348
|
this._debugDrawerWidth = 430;
|
|
298
|
-
/** True while the user is dragging the
|
|
349
|
+
/** True while the user is dragging the drawer's resize handle. */
|
|
299
350
|
this._isDebugResizing = false;
|
|
300
351
|
/** Files staged in the composer, before the message is sent. */
|
|
301
352
|
this._pendingConversationFiles = [];
|
|
@@ -346,6 +397,31 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
346
397
|
this._skillBrowseQuery = '';
|
|
347
398
|
/** True while the mic is live and speech is being transcribed into the composer. */
|
|
348
399
|
this._isDictating = false;
|
|
400
|
+
/**
|
|
401
|
+
* Conversation posture for guarded tools (the composer's mode selector). Sent on
|
|
402
|
+
* each turn's `mode` field and kept in sync with the server's `CONVERSATION_STATE`
|
|
403
|
+
* event. Defaults to NORMAL (ask before each guarded tool).
|
|
404
|
+
*/
|
|
405
|
+
this._mode = 'NORMAL';
|
|
406
|
+
/** Whether the composer's mode menu (Ask / Auto / Plan) is open. */
|
|
407
|
+
this._modeMenuOpened = false;
|
|
408
|
+
/**
|
|
409
|
+
* Whether to show the tools the assistant ran, as activity lines in the
|
|
410
|
+
* transcript. On by default; a user can hide them from the settings panel.
|
|
411
|
+
* User-controlled in the settings panel; persisted in localStorage.
|
|
412
|
+
*/
|
|
413
|
+
this._showTools = true;
|
|
414
|
+
/**
|
|
415
|
+
* Whether to also show each tool's inputs and results under its line. Only
|
|
416
|
+
* has an effect when `_showTools` is on. Off by default.
|
|
417
|
+
*/
|
|
418
|
+
this._showToolInputs = false;
|
|
419
|
+
/**
|
|
420
|
+
* Whether to show the raw tool input inside the confirmation card (the JSON the
|
|
421
|
+
* model proposed for a guarded action). The plain-language ask + Yes/No always
|
|
422
|
+
* show; this only reveals the technical detail. Off by default.
|
|
423
|
+
*/
|
|
424
|
+
this._showConfirmationInput = false;
|
|
349
425
|
/**
|
|
350
426
|
* Close the `/` skill menu when the click lands outside it.
|
|
351
427
|
*
|
|
@@ -356,6 +432,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
356
432
|
* inside click would look like an outside one.
|
|
357
433
|
*/
|
|
358
434
|
this._handleDocumentClick = (e) => {
|
|
435
|
+
// Close the mode menu on any click outside the mode selector (the trigger
|
|
436
|
+
// owns its own toggle, so exclude the whole `.mode` block).
|
|
437
|
+
if (this._modeMenuOpened) {
|
|
438
|
+
const modePath = e.composedPath();
|
|
439
|
+
if (!modePath.some((el) => el instanceof HTMLElement && el.classList.contains('mode'))) {
|
|
440
|
+
this._modeMenuOpened = false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
359
443
|
if (!this._skillMenuOpened) {
|
|
360
444
|
return;
|
|
361
445
|
}
|
|
@@ -497,6 +581,36 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
497
581
|
return KRHttp.fetch({ url: `/api/system/conversation/${this.conversationId}/messages` })
|
|
498
582
|
.then((r) => r.json())
|
|
499
583
|
.then((res) => Promise.all((res?.data || []).map((msg) => {
|
|
584
|
+
// Persisted tool_use / tool_result rows — the message IS the row: keep
|
|
585
|
+
// the type and parse content into `data`, the same {type, data} shape
|
|
586
|
+
// the live stream pushes. The render reads data.* and pairs a result to
|
|
587
|
+
// its use by data.toolUseId. No transform, so live and reload match.
|
|
588
|
+
if (msg.type === 'tool_use' || msg.type === 'tool_result') {
|
|
589
|
+
let data = {};
|
|
590
|
+
try {
|
|
591
|
+
data = JSON.parse(msg.content || '{}');
|
|
592
|
+
}
|
|
593
|
+
catch { /* malformed — render a bare line from what we have */ }
|
|
594
|
+
return Promise.resolve({
|
|
595
|
+
type: msg.type,
|
|
596
|
+
content: '',
|
|
597
|
+
data,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
// A persisted approve/deny record — content is {outcome, tools:[...]}.
|
|
601
|
+
// Keep the type, parse content into `data`; the render reads msg.data.*.
|
|
602
|
+
if (msg.type === 'action_decision') {
|
|
603
|
+
let data = {};
|
|
604
|
+
try {
|
|
605
|
+
data = JSON.parse(msg.content || '{}');
|
|
606
|
+
}
|
|
607
|
+
catch { /* malformed — render a bare line from what we have */ }
|
|
608
|
+
return Promise.resolve({
|
|
609
|
+
type: 'action_decision',
|
|
610
|
+
content: '',
|
|
611
|
+
data,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
500
614
|
// Resolve this message's `conversationFileIDs` (a JSON array string of
|
|
501
615
|
// ids) into full file objects — each fetch response IS the
|
|
502
616
|
// KRChatbotConversationFile (id + niceName + mimeType), and niceName
|
|
@@ -751,7 +865,12 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
751
865
|
const state = {
|
|
752
866
|
opened: String(this.opened),
|
|
753
867
|
message: this._inputValue,
|
|
754
|
-
|
|
868
|
+
mode: this._mode,
|
|
869
|
+
showTools: String(this._showTools),
|
|
870
|
+
showToolInputs: String(this._showToolInputs),
|
|
871
|
+
showConfirmationInput: String(this._showConfirmationInput),
|
|
872
|
+
drawerOpen: String(this._drawerOpen),
|
|
873
|
+
drawerView: this._drawerView,
|
|
755
874
|
debugDrawerWidth: String(this._debugDrawerWidth),
|
|
756
875
|
};
|
|
757
876
|
if (this._panelEl) {
|
|
@@ -792,7 +911,16 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
792
911
|
if (typeof state.message === 'string' && state.message.trim() !== '/') {
|
|
793
912
|
this._inputValue = state.message;
|
|
794
913
|
}
|
|
795
|
-
|
|
914
|
+
if (state.mode === 'NORMAL' || state.mode === 'EDIT' || state.mode === 'PLAN') {
|
|
915
|
+
this._mode = state.mode;
|
|
916
|
+
}
|
|
917
|
+
this._showTools = state.showTools === 'true';
|
|
918
|
+
this._showToolInputs = state.showToolInputs === 'true';
|
|
919
|
+
this._showConfirmationInput = state.showConfirmationInput === 'true';
|
|
920
|
+
this._drawerOpen = state.drawerOpen === 'true';
|
|
921
|
+
if (state.drawerView === 'settings' || state.drawerView === 'debug') {
|
|
922
|
+
this._drawerView = state.drawerView;
|
|
923
|
+
}
|
|
796
924
|
const w = Number(state.debugDrawerWidth);
|
|
797
925
|
if (w >= 300) {
|
|
798
926
|
this._debugDrawerWidth = w;
|
|
@@ -1095,6 +1223,29 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1095
1223
|
this._skillMenuOpened = true;
|
|
1096
1224
|
this._searchInvocableSkills('');
|
|
1097
1225
|
}
|
|
1226
|
+
_handleModeToggle() {
|
|
1227
|
+
this._modeMenuOpened = !this._modeMenuOpened;
|
|
1228
|
+
}
|
|
1229
|
+
_handleModeSelect(mode) {
|
|
1230
|
+
this._mode = mode;
|
|
1231
|
+
this._modeMenuOpened = false;
|
|
1232
|
+
// Persist the choice so it survives a reload, alongside the other composer
|
|
1233
|
+
// state. The server is still the source of truth on the next turn's
|
|
1234
|
+
// CONVERSATION_STATE, but this keeps the selector from flashing the default.
|
|
1235
|
+
this._saveState();
|
|
1236
|
+
}
|
|
1237
|
+
_handleShowToolsChange(e) {
|
|
1238
|
+
this._showTools = e.target.checked;
|
|
1239
|
+
this._saveState();
|
|
1240
|
+
}
|
|
1241
|
+
_handleShowToolInputsChange(e) {
|
|
1242
|
+
this._showToolInputs = e.target.checked;
|
|
1243
|
+
this._saveState();
|
|
1244
|
+
}
|
|
1245
|
+
_handleShowConfirmationInputChange(e) {
|
|
1246
|
+
this._showConfirmationInput = e.target.checked;
|
|
1247
|
+
this._saveState();
|
|
1248
|
+
}
|
|
1098
1249
|
/**
|
|
1099
1250
|
* The mic button toggles dictation. Speech is transcribed straight into the
|
|
1100
1251
|
* composer as editable text — the user still reviews and presses send, so a
|
|
@@ -1521,22 +1672,25 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1521
1672
|
content: '',
|
|
1522
1673
|
}];
|
|
1523
1674
|
this._isStreaming = true;
|
|
1524
|
-
// Scroll user's message to the top (Gemini-style).
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1675
|
+
// Scroll user's message to the top (Gemini-style). The runway (min-height =
|
|
1676
|
+
// viewport) goes on the agent-turn WRAPPER, not an individual message — the
|
|
1677
|
+
// whole turn (reasoning, tool lines, text, decision) streams inside that one
|
|
1678
|
+
// element, so nothing strands below it. Enough scroll room below the user
|
|
1679
|
+
// message to push it to the top.
|
|
1527
1680
|
this.updateComplete.then(() => {
|
|
1528
1681
|
if (!this._messagesEl) {
|
|
1529
1682
|
return;
|
|
1530
1683
|
}
|
|
1531
|
-
// Clear
|
|
1532
|
-
this._messagesEl.querySelectorAll('.
|
|
1684
|
+
// Clear runway from previous turns.
|
|
1685
|
+
this._messagesEl.querySelectorAll('.agent-turn').forEach((el) => {
|
|
1533
1686
|
el.style.minHeight = '';
|
|
1534
1687
|
});
|
|
1535
|
-
const
|
|
1536
|
-
const userMsgEl =
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1688
|
+
const userEls = this._messagesEl.querySelectorAll('.message--user');
|
|
1689
|
+
const userMsgEl = userEls[userEls.length - 1];
|
|
1690
|
+
const turnEls = this._messagesEl.querySelectorAll('.agent-turn');
|
|
1691
|
+
const turnEl = turnEls[turnEls.length - 1];
|
|
1692
|
+
if (turnEl) {
|
|
1693
|
+
turnEl.style.minHeight = this._messagesEl.clientHeight + 'px';
|
|
1540
1694
|
}
|
|
1541
1695
|
if (userMsgEl) {
|
|
1542
1696
|
this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
|
|
@@ -1618,6 +1772,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1618
1772
|
const body = {
|
|
1619
1773
|
userText: params.message,
|
|
1620
1774
|
fileIds: files.map((f) => f.conversationFileID),
|
|
1775
|
+
// Posture for guarded tools this turn (Ask / Auto / Plan). The server
|
|
1776
|
+
// validates and persists it, and only re-sets it when it changes.
|
|
1777
|
+
mode: this._mode,
|
|
1621
1778
|
};
|
|
1622
1779
|
// Ground the model in where the user is and what they've highlighted.
|
|
1623
1780
|
// Only sent when present so an empty selection / failed capture doesn't
|
|
@@ -1716,30 +1873,36 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1716
1873
|
}
|
|
1717
1874
|
this._confirmationInFlight = true;
|
|
1718
1875
|
this._error = '';
|
|
1719
|
-
// The
|
|
1720
|
-
//
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1876
|
+
// The decision line is authored by the server (single source of truth): it
|
|
1877
|
+
// persists an `action_decision` row AND streams an `ACTION_DECISION` event
|
|
1878
|
+
// back. We keep the prompt in place — the clicked button shows a loading
|
|
1879
|
+
// spinner — until that event arrives, which is our ack that the server
|
|
1880
|
+
// accepted the decision. The event handler then drops the prompt, appends the
|
|
1881
|
+
// decision line, and the follow-up turn streams into a new assistant bubble.
|
|
1882
|
+
const btn = this._messagesEl?.querySelector(`.confirmation__btn[data-decision="${decision}"]`);
|
|
1883
|
+
btn?.showLoading();
|
|
1727
1884
|
this._isStreaming = true;
|
|
1728
1885
|
this._userHasScrolledUp = false;
|
|
1729
1886
|
this.send({
|
|
1730
1887
|
message: '',
|
|
1731
|
-
messages: this.messages
|
|
1888
|
+
messages: this.messages,
|
|
1732
1889
|
conversationFiles: [],
|
|
1733
1890
|
pageContext: '',
|
|
1734
1891
|
selectionContext: '',
|
|
1735
1892
|
confirmation: decision,
|
|
1736
1893
|
}).then((response) => {
|
|
1737
|
-
|
|
1894
|
+
// Leave _confirmationInFlight true — the buttons stay loading until the
|
|
1895
|
+
// streamed ACTION_DECISION event resolves the prompt. Only clear it on an
|
|
1896
|
+
// error path, where no such event will arrive.
|
|
1738
1897
|
if (!response.ok) {
|
|
1898
|
+
this._confirmationInFlight = false;
|
|
1899
|
+
btn?.reset();
|
|
1739
1900
|
this._handleStreamError('Request failed: ' + response.status);
|
|
1740
1901
|
return;
|
|
1741
1902
|
}
|
|
1742
1903
|
if (!response.body) {
|
|
1904
|
+
this._confirmationInFlight = false;
|
|
1905
|
+
btn?.reset();
|
|
1743
1906
|
this._handleStreamError('No response body');
|
|
1744
1907
|
return;
|
|
1745
1908
|
}
|
|
@@ -1749,11 +1912,13 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1749
1912
|
this._readStreamChunk();
|
|
1750
1913
|
}).catch((err) => {
|
|
1751
1914
|
this._confirmationInFlight = false;
|
|
1915
|
+
btn?.reset();
|
|
1752
1916
|
if (err.name !== 'AbortError') {
|
|
1753
1917
|
this._handleStreamError(err.message);
|
|
1754
1918
|
}
|
|
1755
1919
|
});
|
|
1756
1920
|
}
|
|
1921
|
+
/** Clear the in-flight confirmation flags (error paths, or once resolved). */
|
|
1757
1922
|
/**
|
|
1758
1923
|
* Capture ambient "where is the user / what are they looking at" context at
|
|
1759
1924
|
* send time, so the AI is grounded without the host passing anything in.
|
|
@@ -1827,8 +1992,26 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1827
1992
|
_handleContextWarningDismiss() {
|
|
1828
1993
|
this._contextWarning = '';
|
|
1829
1994
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1995
|
+
/**
|
|
1996
|
+
* A header button either opens the drawer to its view, switches the view if the
|
|
1997
|
+
* drawer is already open on the other one, or closes the drawer if it's already
|
|
1998
|
+
* showing this view. `_drawerView` is left intact on close so the content stays
|
|
1999
|
+
* visible while the drawer slides shut.
|
|
2000
|
+
*/
|
|
2001
|
+
_toggleDrawer(view) {
|
|
2002
|
+
if (this._drawerOpen && this._drawerView === view) {
|
|
2003
|
+
this._drawerOpen = false;
|
|
2004
|
+
}
|
|
2005
|
+
else {
|
|
2006
|
+
this._drawerOpen = true;
|
|
2007
|
+
this._drawerView = view;
|
|
2008
|
+
}
|
|
2009
|
+
this._saveState();
|
|
2010
|
+
}
|
|
2011
|
+
/** Close the drawer. `_drawerView` is left intact so its content stays visible
|
|
2012
|
+
* while the drawer slides shut. */
|
|
2013
|
+
_handleCloseDrawer() {
|
|
2014
|
+
this._drawerOpen = false;
|
|
1832
2015
|
this._saveState();
|
|
1833
2016
|
}
|
|
1834
2017
|
_handleDebugResizeMouseDown(e) {
|
|
@@ -1884,28 +2067,54 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1884
2067
|
}));
|
|
1885
2068
|
if (data.type === 'REASONING_PART') {
|
|
1886
2069
|
if (data.message) {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
this.requestUpdate();
|
|
2070
|
+
// Text after a tool line belongs in a NEW assistant bubble — the
|
|
2071
|
+
// last message may be a tool line, which text can't append to.
|
|
2072
|
+
// _ensureAssistantBubble opens a fresh bubble when needed.
|
|
2073
|
+
const target = this._ensureAssistantBubble();
|
|
2074
|
+
if (!target.reasoning) {
|
|
2075
|
+
target.reasoning = '';
|
|
1894
2076
|
}
|
|
2077
|
+
target.reasoning += String(data.message);
|
|
2078
|
+
this.requestUpdate();
|
|
1895
2079
|
}
|
|
1896
2080
|
}
|
|
1897
2081
|
else if (data.type === 'RESPONSE_PART') {
|
|
1898
2082
|
if (data.message) {
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
this.messages[lastIndex].content += String(data.message);
|
|
1902
|
-
this.requestUpdate();
|
|
1903
|
-
}
|
|
2083
|
+
this._ensureAssistantBubble().content += String(data.message);
|
|
2084
|
+
this.requestUpdate();
|
|
1904
2085
|
}
|
|
1905
2086
|
}
|
|
2087
|
+
else if (data.type === 'tool_use' || data.type === 'tool_result') {
|
|
2088
|
+
// Push the event straight into the transcript — the message IS the event
|
|
2089
|
+
// ({type, data}). The render reads msg.data.* and pairs a tool_result to
|
|
2090
|
+
// its tool_use by data.toolUseId. Same shape the DB persists and reload
|
|
2091
|
+
// replays, so live and reload share one representation — no transform.
|
|
2092
|
+
this.messages.push(data);
|
|
2093
|
+
this.requestUpdate();
|
|
2094
|
+
}
|
|
2095
|
+
else if (data.type === 'action_decision') {
|
|
2096
|
+
// The server accepted the confirmation decision and authored the record
|
|
2097
|
+
// (single source of truth). Its arrival is the ack: drop the prompt,
|
|
2098
|
+
// stop the buttons' loading, and push the event straight in — the render
|
|
2099
|
+
// reads msg.data.{outcome,tools}. Same type + shape the DB persists, so
|
|
2100
|
+
// live and reload share one representation.
|
|
2101
|
+
this._pendingActions = [];
|
|
2102
|
+
this._confirmationHighlighted = -1;
|
|
2103
|
+
this._confirmationInFlight = false;
|
|
2104
|
+
this.messages.push(data);
|
|
2105
|
+
this.requestUpdate();
|
|
2106
|
+
}
|
|
1906
2107
|
else if (data.type === 'ERROR') {
|
|
1907
2108
|
this._handleStreamError(String(data.message || 'Unknown error'));
|
|
1908
2109
|
}
|
|
2110
|
+
else if (data.type === 'CONVERSATION_STATE') {
|
|
2111
|
+
// Turn-start state sync: the server is the source of truth for the
|
|
2112
|
+
// conversation's mode (it may have decayed EDIT → NORMAL after idle).
|
|
2113
|
+
// Adopt it so the composer's selector reflects the real posture.
|
|
2114
|
+
if (data.data?.mode === 'NORMAL' || data.data?.mode === 'EDIT' || data.data?.mode === 'PLAN') {
|
|
2115
|
+
this._mode = data.data.mode;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
1909
2118
|
else if (data.type === 'CONVERSATION_TOKEN_WARNING') {
|
|
1910
2119
|
// User-facing: friendly "conversation is getting long" guidance.
|
|
1911
2120
|
// Text only, never sent to the model — render verbatim as a banner.
|
|
@@ -1916,16 +2125,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1916
2125
|
// (it was created with a truncated first-message placeholder that we
|
|
1917
2126
|
// deliberately never show). Adopt it as the header label and pass it
|
|
1918
2127
|
// up so consumers can update their own chrome, e.g. a breadcrumb.
|
|
1919
|
-
if (data.data) {
|
|
1920
|
-
const title = String(data.data.title
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
}));
|
|
1928
|
-
}
|
|
2128
|
+
if (data.data?.title) {
|
|
2129
|
+
const title = String(data.data.title);
|
|
2130
|
+
this.label = title;
|
|
2131
|
+
this.dispatchEvent(new CustomEvent('conversation-title', {
|
|
2132
|
+
detail: { title },
|
|
2133
|
+
bubbles: true,
|
|
2134
|
+
composed: true,
|
|
2135
|
+
}));
|
|
1929
2136
|
}
|
|
1930
2137
|
}
|
|
1931
2138
|
else if (data.type === 'CONVERSATION_LIMIT_REACHED') {
|
|
@@ -1997,6 +2204,24 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1997
2204
|
}
|
|
1998
2205
|
});
|
|
1999
2206
|
}
|
|
2207
|
+
/**
|
|
2208
|
+
* The assistant bubble streamed text should append to. Returns the last message
|
|
2209
|
+
* when it's already an assistant bubble; otherwise (the last message is a tool
|
|
2210
|
+
* line, or there's nothing yet) opens a fresh assistant bubble so post-tool text
|
|
2211
|
+
* doesn't get dropped or glued to a tool line.
|
|
2212
|
+
*/
|
|
2213
|
+
_ensureAssistantBubble() {
|
|
2214
|
+
const last = this.messages[this.messages.length - 1];
|
|
2215
|
+
if (last && last.type === 'assistant') {
|
|
2216
|
+
return last;
|
|
2217
|
+
}
|
|
2218
|
+
const bubble = {
|
|
2219
|
+
type: 'assistant',
|
|
2220
|
+
content: '',
|
|
2221
|
+
};
|
|
2222
|
+
this.messages = [...this.messages, bubble];
|
|
2223
|
+
return bubble;
|
|
2224
|
+
}
|
|
2000
2225
|
/**
|
|
2001
2226
|
* End the current turn: release the turn lock and the reader. Called on the
|
|
2002
2227
|
* primary unlock signal (`finished:true`) and the fallback (stream close).
|
|
@@ -2021,6 +2246,17 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2021
2246
|
&& !this.messages[lastIndex].reasoning) {
|
|
2022
2247
|
this.messages = this.messages.slice(0, -1);
|
|
2023
2248
|
}
|
|
2249
|
+
// Scroll the confirmation into view so the user actually sees the ask — an
|
|
2250
|
+
// agentic turn can stream many tool lines past the fold, leaving the prompt
|
|
2251
|
+
// below it. `block: 'end'` brings its bottom up to just above the composer.
|
|
2252
|
+
if (this._inlineConfirm) {
|
|
2253
|
+
this.updateComplete.then(() => {
|
|
2254
|
+
this._messagesEl?.querySelector('.confirmation')?.scrollIntoView({
|
|
2255
|
+
behavior: 'smooth',
|
|
2256
|
+
block: 'end',
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2024
2260
|
}
|
|
2025
2261
|
}
|
|
2026
2262
|
_handleStreamError(message) {
|
|
@@ -2040,33 +2276,107 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2040
2276
|
}
|
|
2041
2277
|
// --- Render ---
|
|
2042
2278
|
/**
|
|
2043
|
-
* The
|
|
2279
|
+
* The composer's mode selector — a Gemini-style inline label + caret sitting
|
|
2280
|
+
* just before the mic, that opens a small menu (Ask / Auto / Plan). Inline text
|
|
2281
|
+
* rather than a pill so it doesn't grow the single-row composer's height. The
|
|
2282
|
+
* menu opens upward above the trigger.
|
|
2283
|
+
*/
|
|
2284
|
+
_renderModeSelector() {
|
|
2285
|
+
const active = CHATBOT_MODES.find((m) => m.mode === this._mode) || CHATBOT_MODES[0];
|
|
2286
|
+
return html `
|
|
2287
|
+
<div class="mode">
|
|
2288
|
+
${this._modeMenuOpened ? html `
|
|
2289
|
+
<div class="mode-menu">
|
|
2290
|
+
${CHATBOT_MODES.map((m) => html `
|
|
2291
|
+
<button
|
|
2292
|
+
class=${classMap({
|
|
2293
|
+
'mode-menu__item': true,
|
|
2294
|
+
'mode-menu__item--active': m.mode === this._mode,
|
|
2295
|
+
})}
|
|
2296
|
+
@click=${() => this._handleModeSelect(m.mode)}
|
|
2297
|
+
>
|
|
2298
|
+
<span class="mode-menu__label">${m.label}</span>
|
|
2299
|
+
<span class="mode-menu__desc">${m.description}</span>
|
|
2300
|
+
</button>
|
|
2301
|
+
`)}
|
|
2302
|
+
</div>
|
|
2303
|
+
` : nothing}
|
|
2304
|
+
<button
|
|
2305
|
+
class="mode-trigger"
|
|
2306
|
+
aria-haspopup="true"
|
|
2307
|
+
aria-expanded=${this._modeMenuOpened ? 'true' : 'false'}
|
|
2308
|
+
@click=${this._handleModeToggle}
|
|
2309
|
+
>
|
|
2310
|
+
<span class="mode-trigger__label">${active.label}</span>
|
|
2311
|
+
<svg
|
|
2312
|
+
class="mode-trigger__caret"
|
|
2313
|
+
viewBox="0 0 24 24"
|
|
2314
|
+
fill="none"
|
|
2315
|
+
stroke="currentColor"
|
|
2316
|
+
stroke-width="2"
|
|
2317
|
+
stroke-linecap="round"
|
|
2318
|
+
stroke-linejoin="round"
|
|
2319
|
+
aria-hidden="true"
|
|
2320
|
+
>
|
|
2321
|
+
<path d="M6 9l6 6 6-6" />
|
|
2322
|
+
</svg>
|
|
2323
|
+
</button>
|
|
2324
|
+
</div>
|
|
2325
|
+
`;
|
|
2326
|
+
}
|
|
2327
|
+
/**
|
|
2328
|
+
* The approve/deny card shown when guarded tools are paused (Rail 3). Floats
|
|
2044
2329
|
* OVER the messages, anchored just above the composer — not part of the message
|
|
2045
2330
|
* scroll — so it stays in view while the transcript scrolls behind it.
|
|
2046
2331
|
*
|
|
2047
|
-
* Written for a non-technical user
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2051
|
-
*
|
|
2332
|
+
* Written for a non-technical user. A single action reads as one plain-language
|
|
2333
|
+
* ask (`Run "{tool}"?` + the model's `confirmationMessage`). When the model
|
|
2334
|
+
* batches several guarded tools, they're listed together under one header and
|
|
2335
|
+
* the user accepts or rejects them all at once — the backend decision is
|
|
2336
|
+
* all-or-nothing. Either way: NO tool code, NO raw arguments/JSON — those are
|
|
2337
|
+
* noise (or alarming) to a normal user.
|
|
2052
2338
|
*/
|
|
2053
2339
|
_renderConfirmation() {
|
|
2054
|
-
//
|
|
2055
|
-
// (and only) pending action.
|
|
2056
|
-
const action = this._pendingActions[0];
|
|
2057
|
-
// Title names the tool being run ("Run {tool}?").
|
|
2340
|
+
// Title: name the single tool, else a generic header for a batch.
|
|
2058
2341
|
let title = 'Run this action?';
|
|
2059
|
-
if (
|
|
2060
|
-
title =
|
|
2342
|
+
if (this._pendingActions.length > 1) {
|
|
2343
|
+
title = 'These actions need your approval';
|
|
2344
|
+
}
|
|
2345
|
+
else if (this._pendingActions[0]) {
|
|
2346
|
+
title = `Run "${this._pendingActions[0].displayName || this._pendingActions[0].toolName}"?`;
|
|
2061
2347
|
}
|
|
2062
2348
|
return html `
|
|
2063
2349
|
<div
|
|
2064
|
-
class
|
|
2350
|
+
class=${classMap({
|
|
2351
|
+
'confirmation': true,
|
|
2352
|
+
'confirmation--inline': this._inlineConfirm,
|
|
2353
|
+
})}
|
|
2065
2354
|
role="alertdialog"
|
|
2066
2355
|
aria-label="Confirm before continuing"
|
|
2067
2356
|
>
|
|
2068
2357
|
<div class="confirmation__title">${title}</div>
|
|
2069
|
-
|
|
2358
|
+
${this._pendingActions.length > 1
|
|
2359
|
+
? html `
|
|
2360
|
+
<ol class="confirmation__list">
|
|
2361
|
+
${this._pendingActions.map((action) => html `
|
|
2362
|
+
<li class="confirmation__list-item">
|
|
2363
|
+
<span class="confirmation__list-name">${action.displayName || action.toolName}</span>
|
|
2364
|
+
${action.confirmationMessage
|
|
2365
|
+
? html `<span class="confirmation__list-desc">${action.confirmationMessage}</span>`
|
|
2366
|
+
: nothing}
|
|
2367
|
+
${this._showConfirmationInput && this._inlineConfirm && action.input && Object.keys(action.input).length
|
|
2368
|
+
? html `<pre class="confirmation__details-input">${JSON.stringify(action.input, null, 2)}</pre>`
|
|
2369
|
+
: nothing}
|
|
2370
|
+
</li>
|
|
2371
|
+
`)}
|
|
2372
|
+
</ol>
|
|
2373
|
+
`
|
|
2374
|
+
: html `
|
|
2375
|
+
<p class="confirmation__message">${this._pendingActions[0]?.confirmationMessage || 'The assistant wants to take an action. Is that OK?'}</p>
|
|
2376
|
+
${this._showConfirmationInput && this._inlineConfirm && this._pendingActions[0]?.input && Object.keys(this._pendingActions[0].input).length
|
|
2377
|
+
? html `<pre class="confirmation__details-input">${JSON.stringify(this._pendingActions[0].input, null, 2)}</pre>`
|
|
2378
|
+
: nothing}
|
|
2379
|
+
`}
|
|
2070
2380
|
<div class="confirmation__actions">
|
|
2071
2381
|
${CONFIRMATION_DECISIONS.map((option, index) => html `
|
|
2072
2382
|
<kr-button
|
|
@@ -2077,7 +2387,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2077
2387
|
variant=${option.decision === 'approve' ? 'flat' : 'outline'}
|
|
2078
2388
|
color=${option.decision === 'reject' ? 'secondary' : 'primary'}
|
|
2079
2389
|
size="small"
|
|
2080
|
-
|
|
2390
|
+
data-decision=${option.decision}
|
|
2081
2391
|
@click=${() => this._handleConfirmationDecision(option.decision)}
|
|
2082
2392
|
>${option.label}</kr-button>
|
|
2083
2393
|
`)}
|
|
@@ -2299,26 +2609,37 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2299
2609
|
* three scopes from the latest TOKEN_USAGE event — context gauge, this turn,
|
|
2300
2610
|
* lifetime — plus a verbose per-iteration table. Never customer-facing.
|
|
2301
2611
|
*/
|
|
2302
|
-
|
|
2612
|
+
/**
|
|
2613
|
+
* The side drawer — one sliding panel that holds both the Settings and Debug
|
|
2614
|
+
* views. The header buttons switch `_drawerView`; the title and body swap
|
|
2615
|
+
* inside. The panel keeps one consistent (resizable) width across both views —
|
|
2616
|
+
* switching views only changes the content, never the panel size.
|
|
2617
|
+
*/
|
|
2618
|
+
_renderDrawer() {
|
|
2619
|
+
// The debug view is gated on the server-side flag (inline variant + debug
|
|
2620
|
+
// telemetry enabled); if it's not enabled, the drawer only ever shows settings.
|
|
2621
|
+
const isDebug = this._drawerView === 'debug'
|
|
2622
|
+
&& this.variant === 'inline'
|
|
2623
|
+
&& this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
2303
2624
|
return html `
|
|
2304
2625
|
<div
|
|
2305
2626
|
class=${classMap({
|
|
2306
|
-
'
|
|
2307
|
-
'
|
|
2308
|
-
'
|
|
2627
|
+
'drawer': true,
|
|
2628
|
+
'drawer--open': this._drawerOpen,
|
|
2629
|
+
'drawer--resizing': this._isDebugResizing,
|
|
2309
2630
|
})}
|
|
2310
2631
|
style=${styleMap({ width: `${this._debugDrawerWidth}px` })}
|
|
2311
2632
|
>
|
|
2312
2633
|
<div
|
|
2313
|
-
class="
|
|
2634
|
+
class="drawer__resize"
|
|
2314
2635
|
@mousedown=${this._handleDebugResizeMouseDown}
|
|
2315
2636
|
></div>
|
|
2316
|
-
<div class="
|
|
2317
|
-
<span class="
|
|
2637
|
+
<div class="drawer__header">
|
|
2638
|
+
<span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
|
|
2318
2639
|
<button
|
|
2319
2640
|
class="header-action"
|
|
2320
|
-
@click=${this.
|
|
2321
|
-
title="Close
|
|
2641
|
+
@click=${this._handleCloseDrawer}
|
|
2642
|
+
title="Close"
|
|
2322
2643
|
>
|
|
2323
2644
|
<svg
|
|
2324
2645
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -2336,105 +2657,163 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2336
2657
|
</svg>
|
|
2337
2658
|
</button>
|
|
2338
2659
|
</div>
|
|
2339
|
-
<div class="
|
|
2340
|
-
${this.
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2660
|
+
<div class="drawer__body">
|
|
2661
|
+
${isDebug ? this._renderDebugContent() : this._renderSettingsContent()}
|
|
2662
|
+
</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
`;
|
|
2665
|
+
}
|
|
2666
|
+
/** The Settings view's body — user preferences, grouped in sections. */
|
|
2667
|
+
_renderSettingsContent() {
|
|
2668
|
+
return html `
|
|
2669
|
+
<div class="settings-section">
|
|
2670
|
+
<div class="settings-section__title">Tools</div>
|
|
2671
|
+
<div class="settings-row">
|
|
2672
|
+
<kr-checkbox
|
|
2673
|
+
label="Show tools"
|
|
2674
|
+
.checked=${this._showTools}
|
|
2675
|
+
@change=${this._handleShowToolsChange}
|
|
2676
|
+
></kr-checkbox>
|
|
2677
|
+
</div>
|
|
2678
|
+
<div class="settings-row">
|
|
2679
|
+
<kr-checkbox
|
|
2680
|
+
label="Show tool inputs"
|
|
2681
|
+
.checked=${this._showToolInputs}
|
|
2682
|
+
@change=${this._handleShowToolInputsChange}
|
|
2683
|
+
></kr-checkbox>
|
|
2684
|
+
</div>
|
|
2685
|
+
<div class="settings-row">
|
|
2686
|
+
<kr-checkbox
|
|
2687
|
+
label="Show tool confirmation inputs"
|
|
2688
|
+
.checked=${this._showConfirmationInput}
|
|
2689
|
+
@change=${this._handleShowConfirmationInputChange}
|
|
2690
|
+
></kr-checkbox>
|
|
2691
|
+
</div>
|
|
2692
|
+
</div>
|
|
2693
|
+
`;
|
|
2694
|
+
}
|
|
2695
|
+
/** The Debug view's body — telemetry scopes and the per-iteration table. */
|
|
2696
|
+
_renderDebugContent() {
|
|
2697
|
+
return html `
|
|
2698
|
+
${this._telemetry ? html `
|
|
2699
|
+
<div class="debug-section">
|
|
2700
|
+
<div class="debug-section__label">Context</div>
|
|
2701
|
+
<div class="debug-gauge">
|
|
2702
|
+
<div class="debug-gauge__bar">
|
|
2703
|
+
<div
|
|
2704
|
+
class="debug-gauge__fill"
|
|
2705
|
+
style=${styleMap({ width: `${Math.min(100, Math.max(0, this._telemetry.contextUsedPercent || 0))}%` })}
|
|
2706
|
+
></div>
|
|
2355
2707
|
</div>
|
|
2708
|
+
<span class="debug-gauge__text">
|
|
2709
|
+
${this._fmtTokens(this._telemetry.currentContextTokens)} / ${this._fmtTokens(this._telemetry.contextWindowTokens)}
|
|
2710
|
+
· ${this._telemetry.contextUsedPercent ?? 0}%
|
|
2711
|
+
</span>
|
|
2712
|
+
</div>
|
|
2713
|
+
</div>
|
|
2356
2714
|
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2715
|
+
<div class="debug-section">
|
|
2716
|
+
<div class="debug-section__label">This turn</div>
|
|
2717
|
+
<div class="debug-metrics">
|
|
2718
|
+
<span class="debug-metrics__key">Input</span>
|
|
2719
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnInputTokens)}</span>
|
|
2720
|
+
<span class="debug-metrics__key">Output</span>
|
|
2721
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnOutputTokens)}</span>
|
|
2722
|
+
<span class="debug-metrics__key">Cache read</span>
|
|
2723
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.turnCacheReadTokens)}</span>
|
|
2724
|
+
<span class="debug-metrics__key">Hit rate</span>
|
|
2725
|
+
<span class="debug-metrics__val">${this._telemetry.turnHitRate ?? '—'}%</span>
|
|
2726
|
+
<span class="debug-metrics__key">Cost</span>
|
|
2727
|
+
<span class="debug-metrics__val debug-cost">${this._fmtCost(this._telemetry.turnCostUsd)}</span>
|
|
2728
|
+
</div>
|
|
2729
|
+
</div>
|
|
2372
2730
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2731
|
+
<div class="debug-section">
|
|
2732
|
+
<div class="debug-section__label">Lifetime</div>
|
|
2733
|
+
<div class="debug-metrics">
|
|
2734
|
+
<span class="debug-metrics__key">Input</span>
|
|
2735
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationInputTokens)}</span>
|
|
2736
|
+
<span class="debug-metrics__key">Output</span>
|
|
2737
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationOutputTokens)}</span>
|
|
2738
|
+
<span class="debug-metrics__key">Cache read</span>
|
|
2739
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationCacheReadTokens)}</span>
|
|
2740
|
+
<span class="debug-metrics__key">Cache write</span>
|
|
2741
|
+
<span class="debug-metrics__val">${this._fmtTokens(this._telemetry.conversationCacheWriteTokens)}</span>
|
|
2742
|
+
<span class="debug-metrics__key">Cost so far</span>
|
|
2743
|
+
<span class="debug-metrics__val debug-cost">${this._fmtCost(this._telemetry.conversationCostUsd)}</span>
|
|
2744
|
+
</div>
|
|
2745
|
+
</div>
|
|
2388
2746
|
` : html `
|
|
2389
2747
|
<div class="debug-empty">No telemetry yet — send a message to populate.</div>
|
|
2390
2748
|
`}
|
|
2391
2749
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2750
|
+
${this._iterationLog.length ? html `
|
|
2751
|
+
<div class="debug-section">
|
|
2752
|
+
<div class="debug-section__label">Iterations (this turn)</div>
|
|
2753
|
+
<div class="debug-iterations-scroll">
|
|
2754
|
+
<table class="debug-iterations">
|
|
2755
|
+
<thead>
|
|
2756
|
+
<tr>
|
|
2757
|
+
<th>#</th>
|
|
2758
|
+
<th>In</th>
|
|
2759
|
+
<th>Out</th>
|
|
2760
|
+
<th>Cache R</th>
|
|
2761
|
+
<th>Cache W</th>
|
|
2762
|
+
<th>Hit</th>
|
|
2763
|
+
<th>Cost</th>
|
|
2764
|
+
<th>Ckpt</th>
|
|
2765
|
+
</tr>
|
|
2766
|
+
</thead>
|
|
2767
|
+
<tbody>
|
|
2768
|
+
${this._iterationLog.map((it) => {
|
|
2411
2769
|
const ckpt = this._cacheCheckpoints[it.iteration];
|
|
2412
2770
|
return html `
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2771
|
+
<tr>
|
|
2772
|
+
<td>${it.iteration}</td>
|
|
2773
|
+
<td>${this._fmtTokens(it.iterationInput)}</td>
|
|
2774
|
+
<td>${this._fmtTokens(it.iterationOutput)}</td>
|
|
2775
|
+
<td>${this._fmtTokens(it.iterationCacheRead)}</td>
|
|
2776
|
+
<td>${this._fmtTokens(it.iterationCacheWrite)}</td>
|
|
2777
|
+
<td>${it.iterationHitRate ?? '—'}%</td>
|
|
2778
|
+
<td class="debug-cost">${this._fmtCost(it.iterationCostUsd)}</td>
|
|
2779
|
+
<td>${ckpt ? ckpt.mode : '—'}</td>
|
|
2780
|
+
</tr>
|
|
2423
2781
|
`;
|
|
2424
2782
|
})}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
</div>
|
|
2429
|
-
` : nothing}
|
|
2783
|
+
</tbody>
|
|
2784
|
+
</table>
|
|
2785
|
+
</div>
|
|
2430
2786
|
</div>
|
|
2431
|
-
|
|
2787
|
+
` : nothing}
|
|
2432
2788
|
`;
|
|
2433
2789
|
}
|
|
2434
2790
|
render() {
|
|
2435
2791
|
// Debug affordances (drawer toggle, drawer, /test-* options) require the
|
|
2436
2792
|
// inline variant AND the server-side debug flag from aiFrameworkConfig.
|
|
2437
2793
|
const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
2794
|
+
// Push the content over by the open drawer's width so the drawer sits beside
|
|
2795
|
+
// the content rather than overlaying it. One consistent (resizable) width for
|
|
2796
|
+
// both views. Only pushes while the drawer is open.
|
|
2797
|
+
const contentPadding = {};
|
|
2798
|
+
if (this._drawerOpen) {
|
|
2799
|
+
contentPadding.paddingRight = `${this._debugDrawerWidth}px`;
|
|
2800
|
+
}
|
|
2801
|
+
// Group the transcript into runs so an agent turn (its reasoning/tool/text/
|
|
2802
|
+
// decision messages, which stream in as separate entries) renders inside ONE
|
|
2803
|
+
// wrapper. The scroll-to-top runway goes on that wrapper, so everything the
|
|
2804
|
+
// turn appends fills the SAME tall element instead of stacking as siblings
|
|
2805
|
+
// below it (which is what stranded tool lines below the fold). A user message
|
|
2806
|
+
// is its own group and is unaffected.
|
|
2807
|
+
const messageGroups = [];
|
|
2808
|
+
this.messages.forEach((msg) => {
|
|
2809
|
+
const lastGroup = messageGroups[messageGroups.length - 1];
|
|
2810
|
+
if (msg.type === 'user' || !lastGroup || lastGroup[0].type === 'user') {
|
|
2811
|
+
messageGroups.push([msg]);
|
|
2812
|
+
}
|
|
2813
|
+
else {
|
|
2814
|
+
lastGroup.push(msg);
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2438
2817
|
return html `
|
|
2439
2818
|
${(this.hideToggle || this.variant === 'inline') ? nothing : html `
|
|
2440
2819
|
<button
|
|
@@ -2465,7 +2844,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2465
2844
|
'panel--opened': this.opened,
|
|
2466
2845
|
'panel--resizing-debug': this._isDebugResizing,
|
|
2467
2846
|
})}
|
|
2468
|
-
style=${styleMap(
|
|
2847
|
+
style=${styleMap(contentPadding)}
|
|
2469
2848
|
@dragenter=${this._handleDragEnter}
|
|
2470
2849
|
@dragover=${this._handleDragOver}
|
|
2471
2850
|
@dragleave=${this._handleDragLeave}
|
|
@@ -2538,13 +2917,35 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2538
2917
|
</div>
|
|
2539
2918
|
</div>
|
|
2540
2919
|
<div class="header-actions">
|
|
2920
|
+
<button
|
|
2921
|
+
class="header-action"
|
|
2922
|
+
@click=${() => this._toggleDrawer('settings')}
|
|
2923
|
+
title="Settings"
|
|
2924
|
+
>
|
|
2925
|
+
<svg
|
|
2926
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
2927
|
+
fill="none"
|
|
2928
|
+
viewBox="0 0 24 24"
|
|
2929
|
+
stroke-width="1.5"
|
|
2930
|
+
stroke="currentColor"
|
|
2931
|
+
class="header-action-icon"
|
|
2932
|
+
>
|
|
2933
|
+
<path
|
|
2934
|
+
stroke-linecap="round"
|
|
2935
|
+
stroke-linejoin="round"
|
|
2936
|
+
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"
|
|
2937
|
+
/>
|
|
2938
|
+
<path
|
|
2939
|
+
stroke-linecap="round"
|
|
2940
|
+
stroke-linejoin="round"
|
|
2941
|
+
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
2942
|
+
/>
|
|
2943
|
+
</svg>
|
|
2944
|
+
</button>
|
|
2541
2945
|
${isDebugEnabled ? html `
|
|
2542
2946
|
<button
|
|
2543
|
-
class
|
|
2544
|
-
|
|
2545
|
-
'header-action--active': this._debugPanelOpen,
|
|
2546
|
-
})}
|
|
2547
|
-
@click=${this._handleToggleDebugPanel}
|
|
2947
|
+
class="header-action"
|
|
2948
|
+
@click=${() => this._toggleDrawer('debug')}
|
|
2548
2949
|
title="Debug telemetry"
|
|
2549
2950
|
>
|
|
2550
2951
|
<svg
|
|
@@ -2595,141 +2996,302 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2595
2996
|
<span class="empty-text">What can I help you with?</span>
|
|
2596
2997
|
</div>
|
|
2597
2998
|
` : nothing}
|
|
2598
|
-
${
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2999
|
+
${messageGroups.map((group) => {
|
|
3000
|
+
// A user group is a single message rendered bare. An agent group wraps
|
|
3001
|
+
// its items in one `.agent-turn` so the scroll-to-top runway sits on
|
|
3002
|
+
// that wrapper and the whole turn fills it (no stranded siblings).
|
|
3003
|
+
const items = group.map((msg) => {
|
|
3004
|
+
const index = this.messages.indexOf(msg);
|
|
3005
|
+
// Skip an empty assistant placeholder (no content, no reasoning) —
|
|
3006
|
+
// _send and _ensureAssistantBubble append these to stream into,
|
|
3007
|
+
// but between tool calls with no text they stay empty. An invisible
|
|
3008
|
+
// .message div still occupies a flex-gap slot, doubling the spacing
|
|
3009
|
+
// between tool lines. Keep it only while it's the actively-streaming
|
|
3010
|
+
// last message (it shows the typing dots).
|
|
3011
|
+
if (msg.type === 'assistant'
|
|
3012
|
+
&& !msg.content
|
|
3013
|
+
&& !msg.reasoning
|
|
3014
|
+
&& !(this._isStreaming && msg === this.messages[this.messages.length - 1])) {
|
|
3015
|
+
return nothing;
|
|
3016
|
+
}
|
|
3017
|
+
// Tool visibility is user-controlled ("Show tools" in the settings
|
|
3018
|
+
// panel). When off, drop the whole tool row so it doesn't leave an
|
|
3019
|
+
// empty flex-gap slot in the transcript.
|
|
3020
|
+
if ((msg.type === 'tool_use' || msg.type === 'tool_result') && !this._showTools) {
|
|
3021
|
+
return nothing;
|
|
3022
|
+
}
|
|
3023
|
+
// A tool_result renders nothing on its own — its tool_use line pulls
|
|
3024
|
+
// it in (matched by data.toolUseId) and renders it under the ⎿ cliff.
|
|
3025
|
+
if (msg.type === 'tool_result') {
|
|
3026
|
+
return nothing;
|
|
3027
|
+
}
|
|
3028
|
+
// A tool_use line (Claude-CLI style): header "› ToolName(input)" and,
|
|
3029
|
+
// when "Show tool inputs" is on, the result under a ⎿ cliff pulled
|
|
3030
|
+
// from the next tool_result with the same toolUseId. Precompute the
|
|
3031
|
+
// result and the two display strings (they need statements — JSON
|
|
3032
|
+
// reformat, truncation — that can't live in a template expression).
|
|
3033
|
+
let result;
|
|
3034
|
+
let inputSummary = '';
|
|
3035
|
+
let resultPreview = '';
|
|
3036
|
+
let resultRemainder = 0;
|
|
3037
|
+
if (msg.type === 'tool_use') {
|
|
3038
|
+
const next = this.messages[index + 1];
|
|
3039
|
+
if (next && next.type === 'tool_result' && next.data?.toolUseId === msg.data?.toolUseId) {
|
|
3040
|
+
result = next.data;
|
|
3041
|
+
}
|
|
3042
|
+
if (this._showToolInputs && msg.data?.toolInput) {
|
|
3043
|
+
inputSummary = String(msg.data.toolInput).replace(/\s+/g, ' ').trim();
|
|
3044
|
+
if (inputSummary.length > 80) {
|
|
3045
|
+
inputSummary = `${inputSummary.slice(0, 79)}…`;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (this._showToolInputs && result?.toolResultString) {
|
|
3049
|
+
let formatted;
|
|
3050
|
+
try {
|
|
3051
|
+
formatted = JSON.stringify(JSON.parse(result.toolResultString), null, 2);
|
|
3052
|
+
}
|
|
3053
|
+
catch {
|
|
3054
|
+
formatted = String(result.toolResultString);
|
|
3055
|
+
}
|
|
3056
|
+
const lines = formatted.split('\n');
|
|
3057
|
+
resultPreview = lines.slice(0, 3).join('\n');
|
|
3058
|
+
resultRemainder = Math.max(0, lines.length - 3);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
// A resolved-decision line (Rail 3) — reads "Approved Create AI Tool"
|
|
3062
|
+
// / "Denied …" / "Always allowed …". Precompute the verb and the
|
|
3063
|
+
// friendly tool names (an if/else and a map — not template exprs).
|
|
3064
|
+
let decisionVerb = 'Approved';
|
|
3065
|
+
let decisionTools = [];
|
|
3066
|
+
if (msg.type === 'action_decision') {
|
|
3067
|
+
if (msg.data?.outcome === 'denied') {
|
|
3068
|
+
decisionVerb = 'Denied';
|
|
3069
|
+
}
|
|
3070
|
+
else if (msg.data?.outcome === 'alwaysAllowed') {
|
|
3071
|
+
decisionVerb = 'Always allowed';
|
|
3072
|
+
}
|
|
3073
|
+
decisionTools = (msg.data?.tools || []).map((t) => t.displayName || t.toolName);
|
|
3074
|
+
}
|
|
3075
|
+
// A tool line and a decision line ARE assistant-side messages —
|
|
3076
|
+
// render them inside the same .message wrapper as everything else so
|
|
3077
|
+
// they share its spacing/structure, not as separate element types.
|
|
2626
3078
|
return html `
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
3079
|
+
<div class=${classMap({
|
|
3080
|
+
'message': true,
|
|
3081
|
+
'message--user': msg.type === 'user',
|
|
3082
|
+
'message--assistant': msg.type !== 'user',
|
|
3083
|
+
})}>
|
|
3084
|
+
${msg.type === 'tool_use' ? html `
|
|
3085
|
+
<div class="tool-activity-block">
|
|
3086
|
+
<div class=${classMap({
|
|
3087
|
+
'tool-activity': true,
|
|
3088
|
+
'tool-activity--error': result?.toolResultStatus === 'error',
|
|
3089
|
+
})}>
|
|
3090
|
+
<span class="tool-activity__chevron">›</span>
|
|
3091
|
+
<span class="tool-activity__label">${msg.data?.displayName || msg.data?.toolName || 'Tool'}</span>
|
|
3092
|
+
${inputSummary ? html `<span class="tool-activity__input">(${inputSummary})</span>` : nothing}
|
|
3093
|
+
</div>
|
|
3094
|
+
${this._showToolInputs && result?.toolResultString ? html `
|
|
3095
|
+
<div class=${classMap({
|
|
3096
|
+
'tool-activity__result': true,
|
|
3097
|
+
'tool-activity__result--error': result.toolResultStatus === 'error',
|
|
3098
|
+
})}>
|
|
3099
|
+
<span class="tool-activity__connector">⎿</span>
|
|
3100
|
+
<pre class="tool-activity__result-body">${resultPreview}${resultRemainder > 0
|
|
3101
|
+
? html `\n<span class="tool-activity__more">… +${resultRemainder} lines</span>`
|
|
3102
|
+
: nothing}</pre>
|
|
3103
|
+
</div>
|
|
3104
|
+
` : nothing}
|
|
3105
|
+
</div>
|
|
3106
|
+
` : nothing}
|
|
3107
|
+
${msg.type === 'action_decision' ? html `
|
|
3108
|
+
<div class=${classMap({
|
|
3109
|
+
'decision': true,
|
|
3110
|
+
'decision--denied': msg.data?.outcome === 'denied',
|
|
3111
|
+
})}>
|
|
3112
|
+
<span class="decision__icon">
|
|
3113
|
+
<svg
|
|
3114
|
+
viewBox="0 0 24 24"
|
|
3115
|
+
fill="none"
|
|
3116
|
+
stroke="currentColor"
|
|
3117
|
+
stroke-width="2"
|
|
3118
|
+
stroke-linecap="round"
|
|
3119
|
+
stroke-linejoin="round"
|
|
3120
|
+
>
|
|
3121
|
+
${msg.data?.outcome === 'denied'
|
|
3122
|
+
? html `<path d="M18 6 6 18M6 6l12 12" />`
|
|
3123
|
+
: html `<path d="M20 6 9 17l-5-5" />`}
|
|
3124
|
+
</svg>
|
|
3125
|
+
</span>
|
|
3126
|
+
<span class="decision__label">${decisionVerb}${decisionTools.length ? ' ' + decisionTools.join(', ') : nothing}</span>
|
|
3127
|
+
</div>
|
|
3128
|
+
` : nothing}
|
|
3129
|
+
${msg.reasoning ? (this._reasoningAsMessage ? html `
|
|
3130
|
+
<div class="message-content">${msg.reasoning}</div>
|
|
3131
|
+
` : html `
|
|
3132
|
+
<details class="reasoning">
|
|
3133
|
+
<summary>Thinking</summary>
|
|
3134
|
+
<div class="reasoning-content">${msg.reasoning}</div>
|
|
3135
|
+
</details>
|
|
3136
|
+
`) : nothing}
|
|
3137
|
+
${msg.conversationFiles && msg.conversationFiles.length ? html `
|
|
3138
|
+
<div class="attachments">
|
|
3139
|
+
${msg.conversationFiles.map((file) => {
|
|
3140
|
+
const fileName = file.niceName || file.name || 'File';
|
|
3141
|
+
// A stored file is an image by its MIME when it has one, else
|
|
3142
|
+
// by its extension.
|
|
3143
|
+
let isImage = IMAGE_EXTENSIONS.includes(this._getFileExtension(fileName));
|
|
3144
|
+
if (file.mimeType) {
|
|
3145
|
+
isImage = file.mimeType.startsWith('image/');
|
|
3146
|
+
}
|
|
3147
|
+
// An image shows its thumbnail; everything else shows a
|
|
3148
|
+
// file-type tile. Both open the previewer on click — it renders
|
|
3149
|
+
// images, video, PDFs, and text inline, and shows a download
|
|
3150
|
+
// tile for the rest (Office formats), so a click always does
|
|
3151
|
+
// something useful.
|
|
3152
|
+
if (isImage) {
|
|
3153
|
+
return html `
|
|
3154
|
+
<button
|
|
3155
|
+
class="attachment"
|
|
3156
|
+
type="button"
|
|
3157
|
+
title=${file.niceName || 'View image'}
|
|
3158
|
+
@click=${() => this._handleFileOpen(file)}
|
|
3159
|
+
>
|
|
3160
|
+
<img
|
|
3161
|
+
src=${file.previewUrl || `${KRHttp.getBaseUrl()}/api/system/conversationFile/${file.conversationFileID}/view`}
|
|
3162
|
+
alt=${file.niceName || ''}
|
|
3163
|
+
/>
|
|
3164
|
+
</button>
|
|
3165
|
+
`;
|
|
3166
|
+
}
|
|
3167
|
+
return html `
|
|
3168
|
+
<button
|
|
3169
|
+
class="attachment attachment--doc attachment--openable"
|
|
3170
|
+
type="button"
|
|
3171
|
+
title=${fileName}
|
|
3172
|
+
@click=${() => this._handleFileOpen(file)}
|
|
3173
|
+
>
|
|
3174
|
+
<span class="attachment__type">${this._getFileExtension(fileName) || 'File'}</span>
|
|
3175
|
+
<span class="attachment__name">${fileName}</span>
|
|
3176
|
+
</button>
|
|
3177
|
+
`;
|
|
3178
|
+
})}
|
|
3179
|
+
</div>
|
|
3180
|
+
` : nothing}
|
|
3181
|
+
${msg.content ? html `
|
|
3182
|
+
<div class="message-content">
|
|
3183
|
+
${msg.type === 'user'
|
|
3184
|
+
? this._renderUserContent(msg.content)
|
|
3185
|
+
: unsafeHTML(marked.parse(msg.content, { breaks: true }))}
|
|
3186
|
+
</div>
|
|
3187
|
+
` : nothing}
|
|
3188
|
+
${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? html `
|
|
3189
|
+
<div class="typing">
|
|
3190
|
+
<span></span>
|
|
3191
|
+
<span></span>
|
|
3192
|
+
<span></span>
|
|
3193
|
+
</div>
|
|
3194
|
+
` : nothing}
|
|
3195
|
+
${msg.content ? html `
|
|
3196
|
+
<button
|
|
3197
|
+
class="message-copy"
|
|
3198
|
+
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
3199
|
+
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
3200
|
+
>
|
|
3201
|
+
${this._copiedIndex === index ? html `
|
|
3202
|
+
<svg
|
|
3203
|
+
viewBox="0 0 24 24"
|
|
3204
|
+
fill="none"
|
|
3205
|
+
stroke="currentColor"
|
|
3206
|
+
stroke-width="1.5"
|
|
3207
|
+
stroke-linecap="round"
|
|
3208
|
+
stroke-linejoin="round"
|
|
2632
3209
|
>
|
|
2633
|
-
<
|
|
2634
|
-
|
|
2635
|
-
|
|
3210
|
+
<path d="M20 6 9 17l-5-5" />
|
|
3211
|
+
</svg>
|
|
3212
|
+
` : html `
|
|
3213
|
+
<svg
|
|
3214
|
+
viewBox="0 0 24 24"
|
|
3215
|
+
fill="none"
|
|
3216
|
+
stroke="currentColor"
|
|
3217
|
+
stroke-width="1.5"
|
|
3218
|
+
stroke-linecap="round"
|
|
3219
|
+
stroke-linejoin="round"
|
|
3220
|
+
>
|
|
3221
|
+
<rect
|
|
3222
|
+
x="9"
|
|
3223
|
+
y="9"
|
|
3224
|
+
width="13"
|
|
3225
|
+
height="13"
|
|
3226
|
+
rx="2"
|
|
2636
3227
|
/>
|
|
2637
|
-
|
|
2638
|
-
|
|
3228
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
3229
|
+
</svg>
|
|
3230
|
+
`}
|
|
3231
|
+
</button>
|
|
3232
|
+
` : nothing}
|
|
3233
|
+
</div>
|
|
3234
|
+
`;
|
|
3235
|
+
});
|
|
3236
|
+
if (group[0].type === 'user') {
|
|
3237
|
+
return items;
|
|
2639
3238
|
}
|
|
3239
|
+
// The inline confirmation is this turn's final step, so it renders
|
|
3240
|
+
// INSIDE the agent-turn wrapper (not as a sibling) — it sits right
|
|
3241
|
+
// after the last message and the runway falls below it naturally.
|
|
3242
|
+
// Only on the last group, which is the current (paused) turn.
|
|
3243
|
+
const isLastGroup = group === messageGroups[messageGroups.length - 1];
|
|
3244
|
+
const showConfirmation = isLastGroup && this._pendingActions.length && this._inlineConfirm;
|
|
3245
|
+
// Keep the "thinking" indicator visible while a tool is running or
|
|
3246
|
+
// the model is between steps with no text yet. The trailing empty
|
|
3247
|
+
// assistant bubble shows the dots itself; but once a tool line is
|
|
3248
|
+
// the last message, that bubble is gone — so render the dots here
|
|
3249
|
+
// at the end of the live turn instead. Not while paused for a
|
|
3250
|
+
// confirmation (the ask replaces the indicator).
|
|
3251
|
+
const lastMsg = this.messages[this.messages.length - 1];
|
|
3252
|
+
const showTyping = isLastGroup
|
|
3253
|
+
&& this._isStreaming
|
|
3254
|
+
&& !this._pendingActions.length
|
|
3255
|
+
&& lastMsg?.type !== 'assistant';
|
|
2640
3256
|
return html `
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
<span
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3257
|
+
<div class="agent-turn">
|
|
3258
|
+
${items}
|
|
3259
|
+
${showTyping ? html `
|
|
3260
|
+
<div class="typing">
|
|
3261
|
+
<span></span>
|
|
3262
|
+
<span></span>
|
|
3263
|
+
<span></span>
|
|
3264
|
+
</div>
|
|
3265
|
+
` : nothing}
|
|
3266
|
+
${showConfirmation ? this._renderConfirmation() : nothing}
|
|
3267
|
+
</div>
|
|
3268
|
+
`;
|
|
2651
3269
|
})}
|
|
2652
|
-
</div>
|
|
2653
|
-
` : nothing}
|
|
2654
|
-
${msg.content ? html `
|
|
2655
|
-
<div class="message-content">
|
|
2656
|
-
${msg.type === 'user'
|
|
2657
|
-
? this._renderUserContent(msg.content)
|
|
2658
|
-
: unsafeHTML(marked.parse(msg.content, { breaks: true }))}
|
|
2659
|
-
</div>
|
|
2660
|
-
` : nothing}
|
|
2661
|
-
${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? html `
|
|
2662
|
-
<div class="typing">
|
|
2663
|
-
<span></span>
|
|
2664
|
-
<span></span>
|
|
2665
|
-
<span></span>
|
|
2666
|
-
</div>
|
|
2667
|
-
` : nothing}
|
|
2668
|
-
${msg.content ? html `
|
|
2669
|
-
<button
|
|
2670
|
-
class="message-copy"
|
|
2671
|
-
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
2672
|
-
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
2673
|
-
>
|
|
2674
|
-
${this._copiedIndex === index ? html `
|
|
2675
|
-
<svg
|
|
2676
|
-
viewBox="0 0 24 24"
|
|
2677
|
-
fill="none"
|
|
2678
|
-
stroke="currentColor"
|
|
2679
|
-
stroke-width="1.5"
|
|
2680
|
-
stroke-linecap="round"
|
|
2681
|
-
stroke-linejoin="round"
|
|
2682
|
-
>
|
|
2683
|
-
<path d="M20 6 9 17l-5-5" />
|
|
2684
|
-
</svg>
|
|
2685
|
-
` : html `
|
|
2686
|
-
<svg
|
|
2687
|
-
viewBox="0 0 24 24"
|
|
2688
|
-
fill="none"
|
|
2689
|
-
stroke="currentColor"
|
|
2690
|
-
stroke-width="1.5"
|
|
2691
|
-
stroke-linecap="round"
|
|
2692
|
-
stroke-linejoin="round"
|
|
2693
|
-
>
|
|
2694
|
-
<rect
|
|
2695
|
-
x="9"
|
|
2696
|
-
y="9"
|
|
2697
|
-
width="13"
|
|
2698
|
-
height="13"
|
|
2699
|
-
rx="2"
|
|
2700
|
-
/>
|
|
2701
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
2702
|
-
</svg>
|
|
2703
|
-
`}
|
|
2704
|
-
</button>
|
|
2705
|
-
` : nothing}
|
|
2706
3270
|
</div>
|
|
2707
|
-
|
|
2708
|
-
</div>
|
|
2709
|
-
</div>
|
|
3271
|
+
</div>
|
|
2710
3272
|
|
|
2711
|
-
|
|
3273
|
+
${this._pendingActions.length && !this._inlineConfirm ? this._renderConfirmation() : nothing}
|
|
2712
3274
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3275
|
+
${this._error ? html `
|
|
3276
|
+
<div class="error">
|
|
3277
|
+
<span>${this._error}</span>
|
|
3278
|
+
<button class="error-dismiss" @click=${this._handleErrorDismiss}>
|
|
3279
|
+
<svg
|
|
3280
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3281
|
+
fill="none"
|
|
3282
|
+
viewBox="0 0 24 24"
|
|
3283
|
+
stroke-width="2"
|
|
3284
|
+
stroke="currentColor"
|
|
3285
|
+
class="error-dismiss-icon"
|
|
3286
|
+
>
|
|
3287
|
+
<path
|
|
3288
|
+
stroke-linecap="round"
|
|
3289
|
+
stroke-linejoin="round"
|
|
3290
|
+
d="M6 18 18 6M6 6l12 12"
|
|
3291
|
+
/>
|
|
3292
|
+
</svg>
|
|
3293
|
+
</button>
|
|
3294
|
+
</div>
|
|
2733
3295
|
` : this._contextLimitReached ? html `
|
|
2734
3296
|
<div class="context-limit" role="alert">
|
|
2735
3297
|
${this._contextLimitMessage
|
|
@@ -2761,68 +3323,68 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2761
3323
|
</div>
|
|
2762
3324
|
` : nothing}
|
|
2763
3325
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3326
|
+
<div class="footer">
|
|
3327
|
+
${this._pendingConversationFiles.length ? html `
|
|
3328
|
+
<div class="composer-attachments">
|
|
3329
|
+
${repeat(this._pendingConversationFiles, (p) => p.localId, (p) => html `
|
|
3330
|
+
<div
|
|
3331
|
+
class=${classMap({
|
|
2770
3332
|
chip: true,
|
|
2771
3333
|
'chip--doc': !p.previewUrl,
|
|
2772
3334
|
})}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3335
|
+
title=${p.name}
|
|
3336
|
+
>
|
|
3337
|
+
<div
|
|
3338
|
+
class=${classMap({
|
|
2777
3339
|
'chip-body': true,
|
|
2778
3340
|
'chip-body--image': Boolean(p.previewUrl),
|
|
2779
3341
|
})}
|
|
2780
|
-
|
|
2781
|
-
|
|
3342
|
+
>
|
|
3343
|
+
${p.previewUrl
|
|
2782
3344
|
? html `<img src=${p.previewUrl} alt=${p.name} />`
|
|
2783
3345
|
: html `
|
|
2784
|
-
|
|
2785
|
-
|
|
3346
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
3347
|
+
<span class="chip-name">${p.name}</span>
|
|
2786
3348
|
`}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
3349
|
+
</div>
|
|
3350
|
+
<button
|
|
3351
|
+
class="chip-remove"
|
|
3352
|
+
title="Remove"
|
|
3353
|
+
@click=${() => this._handlePendingRemove(p.localId)}
|
|
3354
|
+
>
|
|
3355
|
+
<svg
|
|
3356
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3357
|
+
fill="none"
|
|
3358
|
+
viewBox="0 0 24 24"
|
|
3359
|
+
stroke-width="1.75"
|
|
3360
|
+
stroke="currentColor"
|
|
3361
|
+
>
|
|
3362
|
+
<path
|
|
3363
|
+
stroke-linecap="round"
|
|
3364
|
+
stroke-linejoin="round"
|
|
3365
|
+
d="M6 18 18 6M6 6l12 12"
|
|
3366
|
+
/>
|
|
3367
|
+
</svg>
|
|
3368
|
+
</button>
|
|
3369
|
+
</div>
|
|
2808
3370
|
`)}
|
|
2809
|
-
|
|
3371
|
+
</div>
|
|
2810
3372
|
` : nothing}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3373
|
+
<input
|
|
3374
|
+
class="file-input"
|
|
3375
|
+
type="file"
|
|
3376
|
+
accept=${ACCEPTED_FILE_EXTENSIONS.map((ext) => '.' + ext).join(',')}
|
|
3377
|
+
multiple
|
|
3378
|
+
hidden
|
|
3379
|
+
@change=${this._handleFileInputChange}
|
|
3380
|
+
/>
|
|
3381
|
+
${this._skillMenuOpened ? this._renderSkillMenu() : nothing}
|
|
3382
|
+
<div class="input-wrapper">
|
|
3383
|
+
<button
|
|
2822
3384
|
class="input-plus"
|
|
2823
3385
|
@click=${this._handlePlusClick}
|
|
2824
|
-
|
|
2825
|
-
|
|
3386
|
+
>+</button>
|
|
3387
|
+
<textarea
|
|
2826
3388
|
class="input"
|
|
2827
3389
|
.value=${this._inputValue}
|
|
2828
3390
|
placeholder=${this.placeholder}
|
|
@@ -2830,31 +3392,32 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2830
3392
|
@input=${this._handleInputChange}
|
|
2831
3393
|
@keydown=${this._handleKeyDown}
|
|
2832
3394
|
@paste=${this._handlePaste}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
${
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
<svg
|
|
2843
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
2844
|
-
fill="currentColor"
|
|
2845
|
-
viewBox="0 0 24 24"
|
|
2846
|
-
class="mic-stop-icon"
|
|
2847
|
-
aria-hidden="true"
|
|
3395
|
+
></textarea>
|
|
3396
|
+
${this._renderModeSelector()}
|
|
3397
|
+
${window.SpeechRecognition || window.webkitSpeechRecognition ? html `
|
|
3398
|
+
${this._isDictating ? html `
|
|
3399
|
+
<button
|
|
3400
|
+
class="mic-stop"
|
|
3401
|
+
title="Stop dictation"
|
|
3402
|
+
aria-label="Stop dictation"
|
|
3403
|
+
@click=${this._handleMicClick}
|
|
2848
3404
|
>
|
|
2849
|
-
<
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3405
|
+
<svg
|
|
3406
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3407
|
+
fill="currentColor"
|
|
3408
|
+
viewBox="0 0 24 24"
|
|
3409
|
+
class="mic-stop-icon"
|
|
3410
|
+
aria-hidden="true"
|
|
3411
|
+
>
|
|
3412
|
+
<rect
|
|
3413
|
+
x="6"
|
|
3414
|
+
y="6"
|
|
3415
|
+
width="12"
|
|
3416
|
+
height="12"
|
|
3417
|
+
rx="2"
|
|
3418
|
+
/>
|
|
3419
|
+
</svg>
|
|
3420
|
+
</button>
|
|
2858
3421
|
` : html `
|
|
2859
3422
|
<button
|
|
2860
3423
|
class="mic"
|
|
@@ -2880,35 +3443,32 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2880
3443
|
</button>
|
|
2881
3444
|
`}
|
|
2882
3445
|
` : nothing}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
>
|
|
2889
|
-
<svg
|
|
2890
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
2891
|
-
fill="currentColor"
|
|
2892
|
-
viewBox="0 0 24 24"
|
|
2893
|
-
class="stop-icon"
|
|
3446
|
+
${this._isStreaming ? html `
|
|
3447
|
+
<button
|
|
3448
|
+
class="stop"
|
|
3449
|
+
@click=${this._handleStop}
|
|
3450
|
+
title="Stop generating"
|
|
2894
3451
|
>
|
|
2895
|
-
<
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3452
|
+
<svg
|
|
3453
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3454
|
+
fill="currentColor"
|
|
3455
|
+
viewBox="0 0 24 24"
|
|
3456
|
+
class="stop-icon"
|
|
3457
|
+
>
|
|
3458
|
+
<rect
|
|
3459
|
+
x="6"
|
|
3460
|
+
y="6"
|
|
3461
|
+
width="12"
|
|
3462
|
+
height="12"
|
|
3463
|
+
rx="2"
|
|
3464
|
+
/>
|
|
3465
|
+
</svg>
|
|
3466
|
+
</button>
|
|
2904
3467
|
` : html `
|
|
2905
3468
|
<button
|
|
2906
3469
|
class=${classMap({
|
|
2907
3470
|
'send': true,
|
|
2908
|
-
|
|
2909
|
-
// normal message would be rejected until it's resolved.
|
|
2910
|
-
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length)
|
|
2911
|
-
|| this._pendingActions.length > 0,
|
|
3471
|
+
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length) || this._pendingActions.length > 0,
|
|
2912
3472
|
})}
|
|
2913
3473
|
@click=${this._handleSubmit}
|
|
2914
3474
|
title=${this._pendingActions.length ? 'Approve or deny the pending action first' : 'Send message'}
|
|
@@ -2927,12 +3487,12 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2927
3487
|
</svg>
|
|
2928
3488
|
</button>
|
|
2929
3489
|
`}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3490
|
+
</div>
|
|
3491
|
+
</div>
|
|
3492
|
+
${this._skillBrowseOpened ? this._renderSkillBrowse() : nothing}
|
|
3493
|
+
${this._renderDrawer()}
|
|
3494
|
+
</div>
|
|
3495
|
+
`;
|
|
2936
3496
|
}
|
|
2937
3497
|
};
|
|
2938
3498
|
KRChatbot.styles = css `
|
|
@@ -3045,6 +3605,14 @@ KRChatbot.styles = css `
|
|
|
3045
3605
|
box-shadow: none;
|
|
3046
3606
|
border: none;
|
|
3047
3607
|
animation: none;
|
|
3608
|
+
/* Reserve space for the drawer by animating padding-right in sync with the
|
|
3609
|
+
drawer's slide, so the chat column shrinks rather than being overlaid. */
|
|
3610
|
+
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/* Track the cursor 1:1 while dragging the drawer's resize handle. */
|
|
3614
|
+
:host([variant='inline']) .panel--resizing-debug {
|
|
3615
|
+
transition: none;
|
|
3048
3616
|
}
|
|
3049
3617
|
|
|
3050
3618
|
@keyframes chatbot-panel-in {
|
|
@@ -3112,16 +3680,31 @@ KRChatbot.styles = css `
|
|
|
3112
3680
|
color: #000000;
|
|
3113
3681
|
}
|
|
3114
3682
|
|
|
3115
|
-
.header-action--active {
|
|
3116
|
-
background: rgba(0, 0, 0, 0.08);
|
|
3117
|
-
color: #000000;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
3683
|
.header-action-icon {
|
|
3121
3684
|
width: 20px;
|
|
3122
3685
|
height: 20px;
|
|
3123
3686
|
}
|
|
3124
3687
|
|
|
3688
|
+
.settings-section {
|
|
3689
|
+
padding: 14px 20px;
|
|
3690
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
.settings-section:last-child {
|
|
3694
|
+
border-bottom: none;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
.settings-section__title {
|
|
3698
|
+
font-size: 14px;
|
|
3699
|
+
font-weight: 600;
|
|
3700
|
+
color: #000000;
|
|
3701
|
+
margin-bottom: 12px;
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
.settings-row {
|
|
3705
|
+
padding: 6px 0;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3125
3708
|
/* Messages */
|
|
3126
3709
|
/* Full-width scroll container — keeps the scrollbar at the panel edge. */
|
|
3127
3710
|
.messages {
|
|
@@ -3202,6 +3785,138 @@ KRChatbot.styles = css `
|
|
|
3202
3785
|
gap: 24px;
|
|
3203
3786
|
}
|
|
3204
3787
|
|
|
3788
|
+
/* An agent turn wraps all of its messages (reasoning, tool lines, text,
|
|
3789
|
+
decision) so the scroll-to-top runway can sit on the whole turn and content
|
|
3790
|
+
streams inside it instead of stacking as siblings below the runway. A flex
|
|
3791
|
+
column with the same 36px gap as .messages-inner, so item spacing is
|
|
3792
|
+
unchanged whether grouped or not. Full width so its children align exactly
|
|
3793
|
+
as before (assistant left, tool/decision left). */
|
|
3794
|
+
.agent-turn {
|
|
3795
|
+
display: flex;
|
|
3796
|
+
flex-direction: column;
|
|
3797
|
+
gap: 36px;
|
|
3798
|
+
width: 100%;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
/* Tool-activity line — "the assistant ran X" row in the transcript. Same text
|
|
3802
|
+
style as any other message (black, 14px); a leading › chevron marks it as a
|
|
3803
|
+
step (in the same text style, not an icon). */
|
|
3804
|
+
.tool-activity {
|
|
3805
|
+
align-self: flex-start;
|
|
3806
|
+
display: flex;
|
|
3807
|
+
align-items: baseline;
|
|
3808
|
+
gap: 6px;
|
|
3809
|
+
font-size: 14px;
|
|
3810
|
+
color: #000000;
|
|
3811
|
+
/* Keep the chevron + name + input on a single line; input truncates if long. */
|
|
3812
|
+
white-space: nowrap;
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
.tool-activity--error {
|
|
3816
|
+
color: var(--kr-chatbot-error-text);
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
/* Leading chevron — plain text in the same style as the name, so it reads as
|
|
3820
|
+
one label rather than an icon. */
|
|
3821
|
+
.tool-activity__chevron {
|
|
3822
|
+
flex-shrink: 0;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
/* The tool name never shrinks — it's the input that truncates if the header
|
|
3826
|
+
runs out of room. */
|
|
3827
|
+
.tool-activity__label {
|
|
3828
|
+
flex-shrink: 0;
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/* The tool input rendered inline in the header, Claude-CLI style:
|
|
3832
|
+
"Read(chatbot.ts)". Same color/font as the tool name — it reads as one
|
|
3833
|
+
label. Truncates with an ellipsis so the header stays on one line. */
|
|
3834
|
+
.tool-activity__input {
|
|
3835
|
+
overflow: hidden;
|
|
3836
|
+
text-overflow: ellipsis;
|
|
3837
|
+
min-width: 0;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
/* Header + result grouped as one unit so the parent .message--assistant 24px
|
|
3841
|
+
gap doesn't push them apart. Rows sit tight together inside. */
|
|
3842
|
+
.tool-activity-block {
|
|
3843
|
+
display: flex;
|
|
3844
|
+
flex-direction: column;
|
|
3845
|
+
gap: 4px;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
/* The result block indented under the activity line with a ⎿ tree connector,
|
|
3849
|
+
like Claude CLI's tool output preview. Uses the normal message text style
|
|
3850
|
+
(14px, black) — only the ⎿ connector is muted. */
|
|
3851
|
+
.tool-activity__result {
|
|
3852
|
+
align-self: flex-start;
|
|
3853
|
+
display: flex;
|
|
3854
|
+
gap: 2px;
|
|
3855
|
+
/* Align the ⎿ connector's visible stroke under the first character of the
|
|
3856
|
+
tool name. The name is preceded by the "› " chevron (chevron width + 6px
|
|
3857
|
+
gap ≈ 11px), and the ⎿ glyph carries ~5.6px more internal left bearing
|
|
3858
|
+
than the name's first letter — so net-shift the block right to land the
|
|
3859
|
+
stroke under the name. */
|
|
3860
|
+
margin-left: 6px;
|
|
3861
|
+
font-size: 14px;
|
|
3862
|
+
color: #000000;
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
.tool-activity__result--error {
|
|
3866
|
+
color: var(--kr-chatbot-error-text);
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
.tool-activity__connector {
|
|
3870
|
+
flex-shrink: 0;
|
|
3871
|
+
color: rgba(0, 0, 0, 0.55);
|
|
3872
|
+
user-select: none;
|
|
3873
|
+
/* The ⎿ arm sits near the baseline, ~6px below the result's first-character
|
|
3874
|
+
vertical center. Nudge the glyph up so the arm tip lines up with that center. */
|
|
3875
|
+
transform: translateY(-6px);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
.tool-activity__result-body {
|
|
3879
|
+
/* Small horizontal gap between the ⎿ arm and the result text so they don't
|
|
3880
|
+
collide (the ⎿ glyph's own arm already extends most of the way). */
|
|
3881
|
+
margin: 0 0 0 6px;
|
|
3882
|
+
white-space: pre-wrap;
|
|
3883
|
+
word-break: break-word;
|
|
3884
|
+
font: inherit;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
.tool-activity__more {
|
|
3888
|
+
color: rgba(0, 0, 0, 0.4);
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
/* Decision line — the persistent record of an approve/deny (Rail 3). Uses the
|
|
3892
|
+
normal message text style (14px, black); a check/deny icon marks it. */
|
|
3893
|
+
.decision {
|
|
3894
|
+
align-self: flex-start;
|
|
3895
|
+
display: flex;
|
|
3896
|
+
align-items: center;
|
|
3897
|
+
gap: 8px;
|
|
3898
|
+
font-size: 14px;
|
|
3899
|
+
color: #000000;
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
.decision--denied {
|
|
3903
|
+
color: var(--kr-chatbot-error-text);
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
.decision__icon {
|
|
3907
|
+
display: inline-flex;
|
|
3908
|
+
align-items: center;
|
|
3909
|
+
justify-content: center;
|
|
3910
|
+
width: 16px;
|
|
3911
|
+
height: 16px;
|
|
3912
|
+
flex-shrink: 0;
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
.decision__icon svg {
|
|
3916
|
+
width: 15px;
|
|
3917
|
+
height: 15px;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3205
3920
|
/* anywhere, NOT break-word: both break a long unbreakable run (a pasted URL) for
|
|
3206
3921
|
layout, but only anywhere also shrinks the element's intrinsic min-content width.
|
|
3207
3922
|
With break-word the URL still reports its full unbroken width as the minimum,
|
|
@@ -3483,11 +4198,49 @@ KRChatbot.styles = css `
|
|
|
3483
4198
|
overflow-y: auto;
|
|
3484
4199
|
}
|
|
3485
4200
|
|
|
4201
|
+
/* Inline variant (Claude-CLI style): sits IN the message stream as just
|
|
4202
|
+
another assistant message — no card chrome (padding/background/border/
|
|
4203
|
+
radius/shadow), no float offsets, no height cap. It flows and scrolls with
|
|
4204
|
+
the transcript. */
|
|
4205
|
+
.confirmation--inline {
|
|
4206
|
+
position: static;
|
|
4207
|
+
left: auto;
|
|
4208
|
+
right: auto;
|
|
4209
|
+
bottom: auto;
|
|
4210
|
+
z-index: auto;
|
|
4211
|
+
max-width: none;
|
|
4212
|
+
margin: 0;
|
|
4213
|
+
max-height: none;
|
|
4214
|
+
overflow: visible;
|
|
4215
|
+
padding: 0;
|
|
4216
|
+
border: none;
|
|
4217
|
+
border-radius: 0;
|
|
4218
|
+
background: none;
|
|
4219
|
+
box-shadow: none;
|
|
4220
|
+
/* scrollIntoView({ block: 'end' }) aligns the card's bottom to the viewport
|
|
4221
|
+
bottom; this margin makes it stop short so the Yes/No buttons aren't flush
|
|
4222
|
+
against (or clipped by) the fold. */
|
|
4223
|
+
scroll-margin-bottom: 24px;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
/* The raw arguments, pretty-printed. Wraps rather than scrolling — the whole
|
|
4227
|
+
card grows with the transcript (the point of the inline variant). */
|
|
4228
|
+
.confirmation__details-input {
|
|
4229
|
+
margin: 8px 0 0;
|
|
4230
|
+
border-radius: 6px;
|
|
4231
|
+
color: var(--kr-chatbot-text);
|
|
4232
|
+
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
|
|
4233
|
+
font-size: 13px;
|
|
4234
|
+
line-height: 1.5;
|
|
4235
|
+
white-space: pre-wrap;
|
|
4236
|
+
overflow-wrap: anywhere;
|
|
4237
|
+
}
|
|
4238
|
+
|
|
3486
4239
|
/* "Run {tool}?" heading — names the tool being run. */
|
|
3487
4240
|
.confirmation__title {
|
|
3488
4241
|
font-size: 16px;
|
|
3489
4242
|
font-weight: 600;
|
|
3490
|
-
margin-bottom:
|
|
4243
|
+
margin-bottom: 12px;
|
|
3491
4244
|
}
|
|
3492
4245
|
|
|
3493
4246
|
.confirmation__message {
|
|
@@ -3496,8 +4249,26 @@ KRChatbot.styles = css `
|
|
|
3496
4249
|
line-height: 1.5;
|
|
3497
4250
|
}
|
|
3498
4251
|
|
|
3499
|
-
|
|
3500
|
-
|
|
4252
|
+
/* Batch list: one bullet per pending tool — its name, then the model's
|
|
4253
|
+
plain-language intent under it. */
|
|
4254
|
+
.confirmation__list {
|
|
4255
|
+
margin: 0;
|
|
4256
|
+
padding-left: 20px;
|
|
4257
|
+
font-size: 14px;
|
|
4258
|
+
line-height: 1.5;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
.confirmation__list-item:not(:last-child) {
|
|
4262
|
+
margin-bottom: 8px;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
.confirmation__list-name {
|
|
4266
|
+
display: block;
|
|
4267
|
+
font-weight: 600;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
.confirmation__list-desc {
|
|
4271
|
+
display: block;
|
|
3501
4272
|
}
|
|
3502
4273
|
|
|
3503
4274
|
/* Buttons in a row; wrap on a narrow panel rather than overflowing the card. */
|
|
@@ -3505,45 +4276,34 @@ KRChatbot.styles = css `
|
|
|
3505
4276
|
display: flex;
|
|
3506
4277
|
flex-wrap: wrap;
|
|
3507
4278
|
align-items: center;
|
|
3508
|
-
gap:
|
|
3509
|
-
margin-top:
|
|
4279
|
+
gap: 10px;
|
|
4280
|
+
margin-top: 20px;
|
|
3510
4281
|
}
|
|
3511
4282
|
|
|
3512
|
-
/* Keyboard-highlighted decision — a focus ring around the button
|
|
4283
|
+
/* Keyboard-highlighted decision — a focus ring around the button, drawn
|
|
3513
4284
|
outside its border so it reads over both the flat (Yes) and outline
|
|
3514
|
-
variants without recoloring the button
|
|
4285
|
+
variants without recoloring or reshaping the button. Don't set
|
|
4286
|
+
border-radius here: that's the button's own (its pill shape); outline
|
|
4287
|
+
follows it and doesn't affect the button's size. */
|
|
3515
4288
|
.confirmation__btn--highlighted {
|
|
3516
|
-
border-radius: 8px;
|
|
3517
4289
|
outline: 2px solid var(--kr-chatbot-primary);
|
|
3518
4290
|
outline-offset: 2px;
|
|
3519
4291
|
}
|
|
3520
4292
|
|
|
3521
4293
|
/*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
* overlay.
|
|
3525
|
-
*
|
|
3526
|
-
*
|
|
4294
|
+
* Side drawer, docked to the right of the inline chat, holding the Settings
|
|
4295
|
+
* and Debug views. The chat column shrinks (padding-right) to make room; the
|
|
4296
|
+
* drawer does not overlay. It stays mounted and slides in/out via a transform,
|
|
4297
|
+
* so opening and closing animate; switching views just swaps the content
|
|
4298
|
+
* inside (no re-slide, no width change). One consistent resizable width across
|
|
4299
|
+
* both views; the width is set inline from _debugDrawerWidth.
|
|
3527
4300
|
*/
|
|
3528
|
-
|
|
3529
|
-
* The chat column reserves space for the drawer by animating padding-right,
|
|
3530
|
-
* so opening the drawer shrinks the chat (header/messages/composer) rather
|
|
3531
|
-
* than overlaying it. Only applies in inline mode.
|
|
3532
|
-
*/
|
|
3533
|
-
:host([variant='inline']) .panel {
|
|
3534
|
-
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
/* Track the cursor 1:1 while dragging the drawer's resize handle. */
|
|
3538
|
-
:host([variant='inline']) .panel--resizing-debug {
|
|
3539
|
-
transition: none;
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
.debug-drawer {
|
|
4301
|
+
.drawer {
|
|
3543
4302
|
position: absolute;
|
|
3544
4303
|
top: 0;
|
|
3545
4304
|
right: 0;
|
|
3546
4305
|
bottom: 0;
|
|
4306
|
+
max-width: 100%;
|
|
3547
4307
|
background: var(--kr-chatbot-bg);
|
|
3548
4308
|
display: flex;
|
|
3549
4309
|
flex-direction: column;
|
|
@@ -3555,17 +4315,17 @@ KRChatbot.styles = css `
|
|
|
3555
4315
|
|
|
3556
4316
|
/* While dragging the resize handle, suppress the slide transition so the
|
|
3557
4317
|
drawer tracks the cursor 1:1. */
|
|
3558
|
-
.
|
|
4318
|
+
.drawer--resizing {
|
|
3559
4319
|
transition: none;
|
|
3560
4320
|
}
|
|
3561
4321
|
|
|
3562
|
-
.
|
|
4322
|
+
.drawer--open {
|
|
3563
4323
|
transform: translateX(0);
|
|
3564
4324
|
box-shadow: -4px 0 20px rgba(10, 26, 46, 0.07);
|
|
3565
4325
|
}
|
|
3566
4326
|
|
|
3567
4327
|
/* Left-edge grab handle for resizing the drawer. */
|
|
3568
|
-
.
|
|
4328
|
+
.drawer__resize {
|
|
3569
4329
|
position: absolute;
|
|
3570
4330
|
top: 0;
|
|
3571
4331
|
left: 0;
|
|
@@ -3575,7 +4335,7 @@ KRChatbot.styles = css `
|
|
|
3575
4335
|
z-index: 10;
|
|
3576
4336
|
}
|
|
3577
4337
|
|
|
3578
|
-
.
|
|
4338
|
+
.drawer__header {
|
|
3579
4339
|
display: flex;
|
|
3580
4340
|
align-items: center;
|
|
3581
4341
|
justify-content: space-between;
|
|
@@ -3585,13 +4345,13 @@ KRChatbot.styles = css `
|
|
|
3585
4345
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
3586
4346
|
}
|
|
3587
4347
|
|
|
3588
|
-
.
|
|
4348
|
+
.drawer__title {
|
|
3589
4349
|
font-size: 14px;
|
|
3590
4350
|
font-weight: 600;
|
|
3591
4351
|
color: #000000;
|
|
3592
4352
|
}
|
|
3593
4353
|
|
|
3594
|
-
.
|
|
4354
|
+
.drawer__body {
|
|
3595
4355
|
flex: 1;
|
|
3596
4356
|
overflow-y: auto;
|
|
3597
4357
|
overflow-x: hidden;
|
|
@@ -3800,6 +4560,89 @@ KRChatbot.styles = css `
|
|
|
3800
4560
|
margin-right: 4px;
|
|
3801
4561
|
}
|
|
3802
4562
|
|
|
4563
|
+
/* Mode selector — Gemini-style inline label + caret before the mic. Anchors
|
|
4564
|
+
its upward menu; sized so it never grows the single-row composer height. */
|
|
4565
|
+
.mode {
|
|
4566
|
+
position: relative;
|
|
4567
|
+
flex-shrink: 0;
|
|
4568
|
+
display: flex;
|
|
4569
|
+
align-items: center;
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
.mode-trigger {
|
|
4573
|
+
display: inline-flex;
|
|
4574
|
+
align-items: center;
|
|
4575
|
+
gap: 2px;
|
|
4576
|
+
height: 34px;
|
|
4577
|
+
padding: 0 8px;
|
|
4578
|
+
border: none;
|
|
4579
|
+
background: transparent;
|
|
4580
|
+
color: var(--kr-chatbot-text);
|
|
4581
|
+
border-radius: 17px;
|
|
4582
|
+
cursor: pointer;
|
|
4583
|
+
font: inherit;
|
|
4584
|
+
font-size: 14px;
|
|
4585
|
+
white-space: nowrap;
|
|
4586
|
+
transition: background 0.15s;
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
.mode-trigger:hover {
|
|
4590
|
+
background: #f8f9fc;
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
.mode-trigger__caret {
|
|
4594
|
+
width: 16px;
|
|
4595
|
+
height: 16px;
|
|
4596
|
+
flex-shrink: 0;
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
/* Menu opens upward, anchored to the trigger's right edge. */
|
|
4600
|
+
.mode-menu {
|
|
4601
|
+
position: absolute;
|
|
4602
|
+
bottom: calc(100% + 8px);
|
|
4603
|
+
right: 0;
|
|
4604
|
+
min-width: 220px;
|
|
4605
|
+
background: white;
|
|
4606
|
+
border: 1px solid #b0b0b0;
|
|
4607
|
+
border-radius: 8px;
|
|
4608
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
4609
|
+
padding: 6px 0;
|
|
4610
|
+
z-index: 10000;
|
|
4611
|
+
display: flex;
|
|
4612
|
+
flex-direction: column;
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
.mode-menu__item {
|
|
4616
|
+
display: flex;
|
|
4617
|
+
flex-direction: column;
|
|
4618
|
+
gap: 1px;
|
|
4619
|
+
width: 100%;
|
|
4620
|
+
padding: 8px 14px;
|
|
4621
|
+
background: none;
|
|
4622
|
+
border: none;
|
|
4623
|
+
text-align: left;
|
|
4624
|
+
cursor: pointer;
|
|
4625
|
+
font: inherit;
|
|
4626
|
+
color: var(--kr-chatbot-text);
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
.mode-menu__item:hover {
|
|
4630
|
+
background: #f3f4f6;
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
.mode-menu__item--active {
|
|
4634
|
+
background: #eef1f6;
|
|
4635
|
+
}
|
|
4636
|
+
|
|
4637
|
+
.mode-menu__label {
|
|
4638
|
+
font-size: 14px;
|
|
4639
|
+
font-weight: 500;
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
.mode-menu__desc {
|
|
4643
|
+
font-size: 12px;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
3803
4646
|
.mic {
|
|
3804
4647
|
width: 34px;
|
|
3805
4648
|
height: 34px;
|
|
@@ -4478,7 +5321,10 @@ __decorate([
|
|
|
4478
5321
|
], KRChatbot.prototype, "_cacheCheckpoints", void 0);
|
|
4479
5322
|
__decorate([
|
|
4480
5323
|
state()
|
|
4481
|
-
], KRChatbot.prototype, "
|
|
5324
|
+
], KRChatbot.prototype, "_drawerOpen", void 0);
|
|
5325
|
+
__decorate([
|
|
5326
|
+
state()
|
|
5327
|
+
], KRChatbot.prototype, "_drawerView", void 0);
|
|
4482
5328
|
__decorate([
|
|
4483
5329
|
state()
|
|
4484
5330
|
], KRChatbot.prototype, "_debugDrawerWidth", void 0);
|
|
@@ -4527,6 +5373,21 @@ __decorate([
|
|
|
4527
5373
|
__decorate([
|
|
4528
5374
|
state()
|
|
4529
5375
|
], KRChatbot.prototype, "_isDictating", void 0);
|
|
5376
|
+
__decorate([
|
|
5377
|
+
state()
|
|
5378
|
+
], KRChatbot.prototype, "_mode", void 0);
|
|
5379
|
+
__decorate([
|
|
5380
|
+
state()
|
|
5381
|
+
], KRChatbot.prototype, "_modeMenuOpened", void 0);
|
|
5382
|
+
__decorate([
|
|
5383
|
+
state()
|
|
5384
|
+
], KRChatbot.prototype, "_showTools", void 0);
|
|
5385
|
+
__decorate([
|
|
5386
|
+
state()
|
|
5387
|
+
], KRChatbot.prototype, "_showToolInputs", void 0);
|
|
5388
|
+
__decorate([
|
|
5389
|
+
state()
|
|
5390
|
+
], KRChatbot.prototype, "_showConfirmationInput", void 0);
|
|
4530
5391
|
__decorate([
|
|
4531
5392
|
query('.panel')
|
|
4532
5393
|
], KRChatbot.prototype, "_panelEl", void 0);
|