@kodaris/krubble-app-components 1.0.112 → 1.0.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chatbot.d.ts +247 -7
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +1594 -396
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +1597 -400
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +925 -409
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +1594 -397
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +905 -389
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -12,9 +12,51 @@ 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';
|
|
19
|
+
/**
|
|
20
|
+
* The action-confirmation decisions, in the left-to-right order they render and
|
|
21
|
+
* the order the arrow keys walk. Yes is first so it's the default highlight.
|
|
22
|
+
* Shared by the render and the keyboard handler so the two never drift.
|
|
23
|
+
*/
|
|
24
|
+
const CONFIRMATION_DECISIONS = [
|
|
25
|
+
{
|
|
26
|
+
decision: 'approve',
|
|
27
|
+
label: 'Yes',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
decision: 'approveAlways',
|
|
31
|
+
label: 'Always allow',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
decision: 'reject',
|
|
35
|
+
label: 'No',
|
|
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
|
+
];
|
|
18
60
|
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
19
61
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
20
62
|
/**
|
|
@@ -49,9 +91,20 @@ const ACCEPTED_FILE_EXTENSIONS = [
|
|
|
49
91
|
* - `TOOL_RESULT` — `{ type: "TOOL_RESULT", message: "tool_name", data: {...} }`
|
|
50
92
|
* - `ERROR` — `{ type: "ERROR", message: "Something went wrong" }`
|
|
51
93
|
* - `HEARTBEAT` — `{ type: "HEARTBEAT" }` (ignored)
|
|
94
|
+
* - `ACTION_CONFIRMATION_REQUIRED` — a guarded tool paused for approval;
|
|
95
|
+
* `{ type, data: { actions: [...] }, finished: true }` (Rail 3).
|
|
52
96
|
*
|
|
53
97
|
* The final event should include `finished: true` to signal stream completion.
|
|
54
98
|
*
|
|
99
|
+
* ## Turn locking (when the composer is free to send)
|
|
100
|
+
* `finished: true` is the single reliable "turn over, safe to send" signal — the
|
|
101
|
+
* component unlocks the composer on it, and also on stream close/error as a
|
|
102
|
+
* fallback so a dropped stream never wedges the UI. The one exception is
|
|
103
|
+
* `ACTION_CONFIRMATION_REQUIRED`: it too carries `finished: true`, but the
|
|
104
|
+
* conversation is now awaiting an approve/deny decision, so the composer stays
|
|
105
|
+
* blocked and only a confirmation decision (not a new message) is a valid next
|
|
106
|
+
* call.
|
|
107
|
+
*
|
|
55
108
|
* ## Usage
|
|
56
109
|
* ```html
|
|
57
110
|
* <kr-chatbot
|
|
@@ -78,6 +131,25 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
78
131
|
constructor() {
|
|
79
132
|
super(...arguments);
|
|
80
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;
|
|
81
153
|
this._reader = null;
|
|
82
154
|
this._decoder = new TextDecoder();
|
|
83
155
|
this._buffer = '';
|
|
@@ -221,6 +293,32 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
221
293
|
this._contextLimitReached = false;
|
|
222
294
|
/** Server-provided limit text (`CONVERSATION_LIMIT_REACHED`), if any. */
|
|
223
295
|
this._contextLimitMessage = '';
|
|
296
|
+
/**
|
|
297
|
+
* The guarded tool awaiting the user's decision, from the turn's
|
|
298
|
+
* `ACTION_CONFIRMATION_REQUIRED` event (`data.actions[]`, of which we handle the
|
|
299
|
+
* first — the framework confirms one guarded tool at a time). Non-empty means
|
|
300
|
+
* the conversation is paused on a confirmation: the normal composer is blocked
|
|
301
|
+
* (a new chat message would be rejected by the server with
|
|
302
|
+
* `awaitingConfirmation`) and the approve/deny prompt is shown instead. Cleared
|
|
303
|
+
* once a decision is sent.
|
|
304
|
+
*
|
|
305
|
+
* This is the one case where `finished:true` unlocks the turn lock but does
|
|
306
|
+
* NOT free the composer — the only valid next call is a confirmation decision.
|
|
307
|
+
*/
|
|
308
|
+
this._pendingActions = [];
|
|
309
|
+
/**
|
|
310
|
+
* Keyboard-highlighted confirmation option, as an index into the decision list
|
|
311
|
+
* (0 = Yes, 1 = Always allow, 2 = No). Yes is highlighted by default so Enter
|
|
312
|
+
* approves. Arrow Left/Right move it; Enter activates it. -1 when no batch is
|
|
313
|
+
* pending.
|
|
314
|
+
*/
|
|
315
|
+
this._confirmationHighlighted = -1;
|
|
316
|
+
/**
|
|
317
|
+
* True while a confirmation decision (approve/deny) is being sent and its
|
|
318
|
+
* follow-up turn streams. Disables the approve/deny buttons so the batch can't
|
|
319
|
+
* be resolved twice.
|
|
320
|
+
*/
|
|
321
|
+
this._confirmationInFlight = false;
|
|
224
322
|
/**
|
|
225
323
|
* INTERNAL-ONLY latest telemetry snapshot (last-value-wins). Populated from
|
|
226
324
|
* `TOKEN_USAGE` events during streaming and hydrated from the conversation
|
|
@@ -235,11 +333,20 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
235
333
|
this._iterationLog = [];
|
|
236
334
|
/** Latest cache-checkpoint info keyed by iteration, for the verbose table. */
|
|
237
335
|
this._cacheCheckpoints = {};
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
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). */
|
|
241
348
|
this._debugDrawerWidth = 430;
|
|
242
|
-
/** True while the user is dragging the
|
|
349
|
+
/** True while the user is dragging the drawer's resize handle. */
|
|
243
350
|
this._isDebugResizing = false;
|
|
244
351
|
/** Files staged in the composer, before the message is sent. */
|
|
245
352
|
this._pendingConversationFiles = [];
|
|
@@ -290,6 +397,31 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
290
397
|
this._skillBrowseQuery = '';
|
|
291
398
|
/** True while the mic is live and speech is being transcribed into the composer. */
|
|
292
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;
|
|
293
425
|
/**
|
|
294
426
|
* Close the `/` skill menu when the click lands outside it.
|
|
295
427
|
*
|
|
@@ -300,6 +432,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
300
432
|
* inside click would look like an outside one.
|
|
301
433
|
*/
|
|
302
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
|
+
}
|
|
303
443
|
if (!this._skillMenuOpened) {
|
|
304
444
|
return;
|
|
305
445
|
}
|
|
@@ -374,6 +514,36 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
374
514
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
375
515
|
this._saveState();
|
|
376
516
|
};
|
|
517
|
+
/**
|
|
518
|
+
* Keyboard control for the confirmation prompt. Bound at the document so it
|
|
519
|
+
* works even though the composer (where focus usually sits) is blocked while a
|
|
520
|
+
* batch is pending: Left/Right move the highlight across Yes / Always allow /
|
|
521
|
+
* No, Enter activates the highlighted one. No-op when nothing is pending or a
|
|
522
|
+
* decision is already in flight.
|
|
523
|
+
*/
|
|
524
|
+
this._handleDocumentKeydown = (e) => {
|
|
525
|
+
if (!this._pendingActions.length || this._confirmationInFlight) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (e.key === 'ArrowRight') {
|
|
529
|
+
e.preventDefault();
|
|
530
|
+
this._confirmationHighlighted = (this._confirmationHighlighted + 1) % CONFIRMATION_DECISIONS.length;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (e.key === 'ArrowLeft') {
|
|
534
|
+
e.preventDefault();
|
|
535
|
+
this._confirmationHighlighted =
|
|
536
|
+
(this._confirmationHighlighted - 1 + CONFIRMATION_DECISIONS.length) % CONFIRMATION_DECISIONS.length;
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (e.key === 'Enter') {
|
|
540
|
+
e.preventDefault();
|
|
541
|
+
const option = CONFIRMATION_DECISIONS[this._confirmationHighlighted];
|
|
542
|
+
if (option) {
|
|
543
|
+
this._handleConfirmationDecision(option.decision);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
};
|
|
377
547
|
this._handleDebugResizeMouseMove = (e) => {
|
|
378
548
|
if (!this._isDebugResizing) {
|
|
379
549
|
return;
|
|
@@ -411,6 +581,36 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
411
581
|
return KRHttp.fetch({ url: `/api/system/conversation/${this.conversationId}/messages` })
|
|
412
582
|
.then((r) => r.json())
|
|
413
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
|
+
}
|
|
414
614
|
// Resolve this message's `conversationFileIDs` (a JSON array string of
|
|
415
615
|
// ids) into full file objects — each fetch response IS the
|
|
416
616
|
// KRChatbotConversationFile (id + niceName + mimeType), and niceName
|
|
@@ -459,6 +659,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
459
659
|
}
|
|
460
660
|
this._restoreState();
|
|
461
661
|
document.addEventListener('click', this._handleDocumentClick);
|
|
662
|
+
// Drive the confirmation prompt from the keyboard even when focus isn't in the
|
|
663
|
+
// (blocked) composer: Left/Right move the highlight, Enter activates it.
|
|
664
|
+
document.addEventListener('keydown', this._handleDocumentKeydown);
|
|
462
665
|
}
|
|
463
666
|
disconnectedCallback() {
|
|
464
667
|
super.disconnectedCallback();
|
|
@@ -469,6 +672,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
469
672
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
470
673
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
471
674
|
document.removeEventListener('click', this._handleDocumentClick);
|
|
675
|
+
document.removeEventListener('keydown', this._handleDocumentKeydown);
|
|
472
676
|
// Release the mic if the chatbot is torn down mid-dictation — otherwise the
|
|
473
677
|
// recognizer keeps the browser's recording indicator lit with nothing to
|
|
474
678
|
// transcribe into. abort() (not stop()) since the discarded text has nowhere to go.
|
|
@@ -661,7 +865,12 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
661
865
|
const state = {
|
|
662
866
|
opened: String(this.opened),
|
|
663
867
|
message: this._inputValue,
|
|
664
|
-
|
|
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,
|
|
665
874
|
debugDrawerWidth: String(this._debugDrawerWidth),
|
|
666
875
|
};
|
|
667
876
|
if (this._panelEl) {
|
|
@@ -702,7 +911,16 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
702
911
|
if (typeof state.message === 'string' && state.message.trim() !== '/') {
|
|
703
912
|
this._inputValue = state.message;
|
|
704
913
|
}
|
|
705
|
-
|
|
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
|
+
}
|
|
706
924
|
const w = Number(state.debugDrawerWidth);
|
|
707
925
|
if (w >= 300) {
|
|
708
926
|
this._debugDrawerWidth = w;
|
|
@@ -1005,6 +1223,29 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1005
1223
|
this._skillMenuOpened = true;
|
|
1006
1224
|
this._searchInvocableSkills('');
|
|
1007
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
|
+
}
|
|
1008
1249
|
/**
|
|
1009
1250
|
* The mic button toggles dictation. Speech is transcribed straight into the
|
|
1010
1251
|
* composer as editable text — the user still reviews and presses send, so a
|
|
@@ -1343,6 +1584,13 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1343
1584
|
if (this._isStreaming) {
|
|
1344
1585
|
return;
|
|
1345
1586
|
}
|
|
1587
|
+
// A guarded-tool batch is awaiting a decision. The turn lock released with
|
|
1588
|
+
// its finished:true, but the server rejects a normal chat message with
|
|
1589
|
+
// awaitingConfirmation until it's resolved — so the composer stays blocked
|
|
1590
|
+
// and the approve/deny prompt owns the next call.
|
|
1591
|
+
if (this._pendingActions.length) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1346
1594
|
const messageText = this._inputValue.trim();
|
|
1347
1595
|
const pending = this._pendingConversationFiles;
|
|
1348
1596
|
// Allow sending an image with no text, but require at least one of them.
|
|
@@ -1424,22 +1672,25 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1424
1672
|
content: '',
|
|
1425
1673
|
}];
|
|
1426
1674
|
this._isStreaming = true;
|
|
1427
|
-
// Scroll user's message to the top (Gemini-style).
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
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.
|
|
1430
1680
|
this.updateComplete.then(() => {
|
|
1431
1681
|
if (!this._messagesEl) {
|
|
1432
1682
|
return;
|
|
1433
1683
|
}
|
|
1434
|
-
// Clear
|
|
1435
|
-
this._messagesEl.querySelectorAll('.
|
|
1684
|
+
// Clear runway from previous turns.
|
|
1685
|
+
this._messagesEl.querySelectorAll('.agent-turn').forEach((el) => {
|
|
1436
1686
|
el.style.minHeight = '';
|
|
1437
1687
|
});
|
|
1438
|
-
const
|
|
1439
|
-
const userMsgEl =
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
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';
|
|
1443
1694
|
}
|
|
1444
1695
|
if (userMsgEl) {
|
|
1445
1696
|
this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
|
|
@@ -1449,6 +1700,22 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1449
1700
|
// the consumer hasn't provided their own `send`.
|
|
1450
1701
|
if (!this.send) {
|
|
1451
1702
|
this.send = (params) => {
|
|
1703
|
+
// Rail-3 confirmation decision: resolve a paused guarded-tool batch by
|
|
1704
|
+
// hitting the SAME streaming endpoint with `confirmation` set. There's no
|
|
1705
|
+
// userText/files on this path (the server ignores message text here), and
|
|
1706
|
+
// the conversation is always already pinned (a batch can't pause before
|
|
1707
|
+
// the conversation exists), so no upload and no createNew.
|
|
1708
|
+
if (params.confirmation) {
|
|
1709
|
+
return KRHttp.fetch({
|
|
1710
|
+
url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
|
|
1711
|
+
method: 'POST',
|
|
1712
|
+
headers: { Accept: 'text/event-stream' },
|
|
1713
|
+
body: {
|
|
1714
|
+
confirmation: params.confirmation,
|
|
1715
|
+
conversationID: this.conversationId,
|
|
1716
|
+
},
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1452
1719
|
// Images are upload-first: each file is stored as a conversation file
|
|
1453
1720
|
// (type LIBRARY — the format lives in mimeType), then the resulting
|
|
1454
1721
|
// conversationFileIDs are attached to the message via `fileIds` on the
|
|
@@ -1505,6 +1772,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1505
1772
|
const body = {
|
|
1506
1773
|
userText: params.message,
|
|
1507
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,
|
|
1508
1778
|
};
|
|
1509
1779
|
// Ground the model in where the user is and what they've highlighted.
|
|
1510
1780
|
// Only sent when present so an empty selection / failed capture doesn't
|
|
@@ -1588,6 +1858,67 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1588
1858
|
}
|
|
1589
1859
|
});
|
|
1590
1860
|
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Resolve the paused guarded-tool batch (Rail 3). Hits the SAME streaming
|
|
1863
|
+
* endpoint as a normal turn but with a `confirmation` value instead of a
|
|
1864
|
+
* message; the server runs (approve/approveAlways) or denies (reject) the batch
|
|
1865
|
+
* and continues the turn, streaming the outcome into a fresh assistant bubble.
|
|
1866
|
+
*
|
|
1867
|
+
* The decision uses the same `send` callback so a custom `send` can own its own
|
|
1868
|
+
* targeting — it just receives `confirmation` set and no message.
|
|
1869
|
+
*/
|
|
1870
|
+
_handleConfirmationDecision(decision) {
|
|
1871
|
+
if (this._confirmationInFlight) {
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this._confirmationInFlight = true;
|
|
1875
|
+
this._error = '';
|
|
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();
|
|
1884
|
+
this._isStreaming = true;
|
|
1885
|
+
this._userHasScrolledUp = false;
|
|
1886
|
+
this.send({
|
|
1887
|
+
message: '',
|
|
1888
|
+
messages: this.messages,
|
|
1889
|
+
conversationFiles: [],
|
|
1890
|
+
pageContext: '',
|
|
1891
|
+
selectionContext: '',
|
|
1892
|
+
confirmation: decision,
|
|
1893
|
+
}).then((response) => {
|
|
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.
|
|
1897
|
+
if (!response.ok) {
|
|
1898
|
+
this._confirmationInFlight = false;
|
|
1899
|
+
btn?.reset();
|
|
1900
|
+
this._handleStreamError('Request failed: ' + response.status);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
if (!response.body) {
|
|
1904
|
+
this._confirmationInFlight = false;
|
|
1905
|
+
btn?.reset();
|
|
1906
|
+
this._handleStreamError('No response body');
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
this._reader = response.body.getReader();
|
|
1910
|
+
this._decoder = new TextDecoder();
|
|
1911
|
+
this._buffer = '';
|
|
1912
|
+
this._readStreamChunk();
|
|
1913
|
+
}).catch((err) => {
|
|
1914
|
+
this._confirmationInFlight = false;
|
|
1915
|
+
btn?.reset();
|
|
1916
|
+
if (err.name !== 'AbortError') {
|
|
1917
|
+
this._handleStreamError(err.message);
|
|
1918
|
+
}
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
/** Clear the in-flight confirmation flags (error paths, or once resolved). */
|
|
1591
1922
|
/**
|
|
1592
1923
|
* Capture ambient "where is the user / what are they looking at" context at
|
|
1593
1924
|
* send time, so the AI is grounded without the host passing anything in.
|
|
@@ -1661,8 +1992,26 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1661
1992
|
_handleContextWarningDismiss() {
|
|
1662
1993
|
this._contextWarning = '';
|
|
1663
1994
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
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;
|
|
1666
2015
|
this._saveState();
|
|
1667
2016
|
}
|
|
1668
2017
|
_handleDebugResizeMouseDown(e) {
|
|
@@ -1680,8 +2029,11 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1680
2029
|
}
|
|
1681
2030
|
this._reader.read().then((result) => {
|
|
1682
2031
|
if (result.done) {
|
|
1683
|
-
|
|
1684
|
-
|
|
2032
|
+
// Fallback unlock: the stream closed without a `finished:true` chunk
|
|
2033
|
+
// (network drop, server crash, the 5-min emitter timeout). Treat it as
|
|
2034
|
+
// the turn ending so a dropped stream never wedges the composer — the
|
|
2035
|
+
// server self-heals its turn lock and bounces any premature send.
|
|
2036
|
+
this._finishStream();
|
|
1685
2037
|
return;
|
|
1686
2038
|
}
|
|
1687
2039
|
this._buffer = this._buffer + this._decoder.decode(result.value, { stream: true });
|
|
@@ -1715,28 +2067,54 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1715
2067
|
}));
|
|
1716
2068
|
if (data.type === 'REASONING_PART') {
|
|
1717
2069
|
if (data.message) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
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 = '';
|
|
1725
2076
|
}
|
|
2077
|
+
target.reasoning += String(data.message);
|
|
2078
|
+
this.requestUpdate();
|
|
1726
2079
|
}
|
|
1727
2080
|
}
|
|
1728
2081
|
else if (data.type === 'RESPONSE_PART') {
|
|
1729
2082
|
if (data.message) {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
this.messages[lastIndex].content += String(data.message);
|
|
1733
|
-
this.requestUpdate();
|
|
1734
|
-
}
|
|
2083
|
+
this._ensureAssistantBubble().content += String(data.message);
|
|
2084
|
+
this.requestUpdate();
|
|
1735
2085
|
}
|
|
1736
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
|
+
}
|
|
1737
2107
|
else if (data.type === 'ERROR') {
|
|
1738
2108
|
this._handleStreamError(String(data.message || 'Unknown error'));
|
|
1739
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
|
+
}
|
|
1740
2118
|
else if (data.type === 'CONVERSATION_TOKEN_WARNING') {
|
|
1741
2119
|
// User-facing: friendly "conversation is getting long" guidance.
|
|
1742
2120
|
// Text only, never sent to the model — render verbatim as a banner.
|
|
@@ -1747,16 +2125,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1747
2125
|
// (it was created with a truncated first-message placeholder that we
|
|
1748
2126
|
// deliberately never show). Adopt it as the header label and pass it
|
|
1749
2127
|
// up so consumers can update their own chrome, e.g. a breadcrumb.
|
|
1750
|
-
if (data.data) {
|
|
1751
|
-
const title = String(data.data.title
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}));
|
|
1759
|
-
}
|
|
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
|
+
}));
|
|
1760
2136
|
}
|
|
1761
2137
|
}
|
|
1762
2138
|
else if (data.type === 'CONVERSATION_LIMIT_REACHED') {
|
|
@@ -1766,6 +2142,20 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1766
2142
|
this._contextLimitMessage = String(data.message || '');
|
|
1767
2143
|
this._contextWarning = '';
|
|
1768
2144
|
}
|
|
2145
|
+
else if (data.type === 'ACTION_CONFIRMATION_REQUIRED') {
|
|
2146
|
+
// A guarded tool paused for approval (Rail 3). Carries finished:true
|
|
2147
|
+
// (handled below), so the turn lock releases — but the conversation
|
|
2148
|
+
// is now awaiting a decision, and a normal chat message would be
|
|
2149
|
+
// rejected with awaitingConfirmation. Capture the batch so the
|
|
2150
|
+
// composer stays blocked and the approve/deny prompt renders instead;
|
|
2151
|
+
// the next call must be a confirmation, not a userText.
|
|
2152
|
+
const actions = data.data?.actions;
|
|
2153
|
+
if (Array.isArray(actions) && actions.length) {
|
|
2154
|
+
this._pendingActions = actions;
|
|
2155
|
+
// Default the keyboard highlight to Yes so Enter approves.
|
|
2156
|
+
this._confirmationHighlighted = 0;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
1769
2159
|
else if (data.type === 'TOKEN_USAGE') {
|
|
1770
2160
|
// INTERNAL-ONLY debug telemetry. Update the panel snapshot and the
|
|
1771
2161
|
// per-iteration log; never render as a chat bubble.
|
|
@@ -1796,11 +2186,17 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1796
2186
|
}
|
|
1797
2187
|
}
|
|
1798
2188
|
if (data.finished) {
|
|
1799
|
-
|
|
1800
|
-
|
|
2189
|
+
// Primary unlock signal: the turn is over and the server released
|
|
2190
|
+
// the turn lock. The SSE stream closes right after this chunk.
|
|
2191
|
+
this._finishStream();
|
|
1801
2192
|
}
|
|
1802
2193
|
}
|
|
1803
2194
|
}
|
|
2195
|
+
// Stop reading once a terminal chunk ended the turn — the reader is nulled
|
|
2196
|
+
// by _finishStream and the stream is closing anyway.
|
|
2197
|
+
if (!this._reader) {
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
1804
2200
|
this._readStreamChunk();
|
|
1805
2201
|
}).catch((err) => {
|
|
1806
2202
|
if (err.name !== 'AbortError') {
|
|
@@ -1808,6 +2204,61 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1808
2204
|
}
|
|
1809
2205
|
});
|
|
1810
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
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* End the current turn: release the turn lock and the reader. Called on the
|
|
2227
|
+
* primary unlock signal (`finished:true`) and the fallback (stream close).
|
|
2228
|
+
*
|
|
2229
|
+
* A turn that paused for confirmation (`_pendingActions` set) leaves the
|
|
2230
|
+
* composer blocked even though the lock released — the approve/deny prompt
|
|
2231
|
+
* owns the next call. Its trailing empty assistant bubble is dropped so the
|
|
2232
|
+
* prompt doesn't sit under an empty message with a typing indicator.
|
|
2233
|
+
*/
|
|
2234
|
+
_finishStream() {
|
|
2235
|
+
this._isStreaming = false;
|
|
2236
|
+
this._reader = null;
|
|
2237
|
+
if (this._pendingActions.length) {
|
|
2238
|
+
const lastIndex = this.messages.length - 1;
|
|
2239
|
+
// Drop the trailing assistant bubble only when it's truly empty — no
|
|
2240
|
+
// streamed text AND no reasoning. A reasoning-only bubble (the model thought
|
|
2241
|
+
// out loud, then went straight to the guarded tool) must be kept, else its
|
|
2242
|
+
// "Thinking" trace is lost when the confirmation card renders.
|
|
2243
|
+
if (lastIndex >= 0
|
|
2244
|
+
&& this.messages[lastIndex].type === 'assistant'
|
|
2245
|
+
&& !this.messages[lastIndex].content
|
|
2246
|
+
&& !this.messages[lastIndex].reasoning) {
|
|
2247
|
+
this.messages = this.messages.slice(0, -1);
|
|
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
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
1811
2262
|
_handleStreamError(message) {
|
|
1812
2263
|
this._isStreaming = false;
|
|
1813
2264
|
this._error = message;
|
|
@@ -1824,6 +2275,126 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1824
2275
|
}
|
|
1825
2276
|
}
|
|
1826
2277
|
// --- Render ---
|
|
2278
|
+
/**
|
|
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
|
|
2329
|
+
* OVER the messages, anchored just above the composer — not part of the message
|
|
2330
|
+
* scroll — so it stays in view while the transcript scrolls behind it.
|
|
2331
|
+
*
|
|
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.
|
|
2338
|
+
*/
|
|
2339
|
+
_renderConfirmation() {
|
|
2340
|
+
// Title: name the single tool, else a generic header for a batch.
|
|
2341
|
+
let title = 'Run this action?';
|
|
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}"?`;
|
|
2347
|
+
}
|
|
2348
|
+
return html `
|
|
2349
|
+
<div
|
|
2350
|
+
class=${classMap({
|
|
2351
|
+
'confirmation': true,
|
|
2352
|
+
'confirmation--inline': this._inlineConfirm,
|
|
2353
|
+
})}
|
|
2354
|
+
role="alertdialog"
|
|
2355
|
+
aria-label="Confirm before continuing"
|
|
2356
|
+
>
|
|
2357
|
+
<div class="confirmation__title">${title}</div>
|
|
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
|
+
`}
|
|
2380
|
+
<div class="confirmation__actions">
|
|
2381
|
+
${CONFIRMATION_DECISIONS.map((option, index) => html `
|
|
2382
|
+
<kr-button
|
|
2383
|
+
class=${classMap({
|
|
2384
|
+
'confirmation__btn': true,
|
|
2385
|
+
'confirmation__btn--highlighted': index === this._confirmationHighlighted,
|
|
2386
|
+
})}
|
|
2387
|
+
variant=${option.decision === 'approve' ? 'flat' : 'outline'}
|
|
2388
|
+
color=${option.decision === 'reject' ? 'secondary' : 'primary'}
|
|
2389
|
+
size="small"
|
|
2390
|
+
data-decision=${option.decision}
|
|
2391
|
+
@click=${() => this._handleConfirmationDecision(option.decision)}
|
|
2392
|
+
>${option.label}</kr-button>
|
|
2393
|
+
`)}
|
|
2394
|
+
</div>
|
|
2395
|
+
</div>
|
|
2396
|
+
`;
|
|
2397
|
+
}
|
|
1827
2398
|
/** The `/` skill menu shown above the composer while typing a slash command. */
|
|
1828
2399
|
_renderSkillMenu() {
|
|
1829
2400
|
const skills = this._invocableSkills;
|
|
@@ -2038,26 +2609,37 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2038
2609
|
* three scopes from the latest TOKEN_USAGE event — context gauge, this turn,
|
|
2039
2610
|
* lifetime — plus a verbose per-iteration table. Never customer-facing.
|
|
2040
2611
|
*/
|
|
2041
|
-
|
|
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;
|
|
2042
2624
|
return html `
|
|
2043
2625
|
<div
|
|
2044
2626
|
class=${classMap({
|
|
2045
|
-
'
|
|
2046
|
-
'
|
|
2047
|
-
'
|
|
2627
|
+
'drawer': true,
|
|
2628
|
+
'drawer--open': this._drawerOpen,
|
|
2629
|
+
'drawer--resizing': this._isDebugResizing,
|
|
2048
2630
|
})}
|
|
2049
2631
|
style=${styleMap({ width: `${this._debugDrawerWidth}px` })}
|
|
2050
2632
|
>
|
|
2051
2633
|
<div
|
|
2052
|
-
class="
|
|
2634
|
+
class="drawer__resize"
|
|
2053
2635
|
@mousedown=${this._handleDebugResizeMouseDown}
|
|
2054
2636
|
></div>
|
|
2055
|
-
<div class="
|
|
2056
|
-
<span class="
|
|
2637
|
+
<div class="drawer__header">
|
|
2638
|
+
<span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
|
|
2057
2639
|
<button
|
|
2058
2640
|
class="header-action"
|
|
2059
|
-
@click=${this.
|
|
2060
|
-
title="Close
|
|
2641
|
+
@click=${this._handleCloseDrawer}
|
|
2642
|
+
title="Close"
|
|
2061
2643
|
>
|
|
2062
2644
|
<svg
|
|
2063
2645
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -2075,105 +2657,163 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2075
2657
|
</svg>
|
|
2076
2658
|
</button>
|
|
2077
2659
|
</div>
|
|
2078
|
-
<div class="
|
|
2079
|
-
${this.
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
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>
|
|
2094
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>
|
|
2095
2714
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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>
|
|
2111
2730
|
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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>
|
|
2127
2746
|
` : html `
|
|
2128
2747
|
<div class="debug-empty">No telemetry yet — send a message to populate.</div>
|
|
2129
2748
|
`}
|
|
2130
2749
|
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
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) => {
|
|
2150
2769
|
const ckpt = this._cacheCheckpoints[it.iteration];
|
|
2151
2770
|
return html `
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
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>
|
|
2162
2781
|
`;
|
|
2163
2782
|
})}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
</div>
|
|
2168
|
-
` : nothing}
|
|
2783
|
+
</tbody>
|
|
2784
|
+
</table>
|
|
2785
|
+
</div>
|
|
2169
2786
|
</div>
|
|
2170
|
-
|
|
2787
|
+
` : nothing}
|
|
2171
2788
|
`;
|
|
2172
2789
|
}
|
|
2173
2790
|
render() {
|
|
2174
2791
|
// Debug affordances (drawer toggle, drawer, /test-* options) require the
|
|
2175
2792
|
// inline variant AND the server-side debug flag from aiFrameworkConfig.
|
|
2176
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
|
+
});
|
|
2177
2817
|
return html `
|
|
2178
2818
|
${(this.hideToggle || this.variant === 'inline') ? nothing : html `
|
|
2179
2819
|
<button
|
|
@@ -2204,7 +2844,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2204
2844
|
'panel--opened': this.opened,
|
|
2205
2845
|
'panel--resizing-debug': this._isDebugResizing,
|
|
2206
2846
|
})}
|
|
2207
|
-
style=${styleMap(
|
|
2847
|
+
style=${styleMap(contentPadding)}
|
|
2208
2848
|
@dragenter=${this._handleDragEnter}
|
|
2209
2849
|
@dragover=${this._handleDragOver}
|
|
2210
2850
|
@dragleave=${this._handleDragLeave}
|
|
@@ -2277,13 +2917,35 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2277
2917
|
</div>
|
|
2278
2918
|
</div>
|
|
2279
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>
|
|
2280
2945
|
${isDebugEnabled ? html `
|
|
2281
2946
|
<button
|
|
2282
|
-
class
|
|
2283
|
-
|
|
2284
|
-
'header-action--active': this._debugPanelOpen,
|
|
2285
|
-
})}
|
|
2286
|
-
@click=${this._handleToggleDebugPanel}
|
|
2947
|
+
class="header-action"
|
|
2948
|
+
@click=${() => this._toggleDrawer('debug')}
|
|
2287
2949
|
title="Debug telemetry"
|
|
2288
2950
|
>
|
|
2289
2951
|
<svg
|
|
@@ -2334,139 +2996,302 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2334
2996
|
<span class="empty-text">What can I help you with?</span>
|
|
2335
2997
|
</div>
|
|
2336
2998
|
` : nothing}
|
|
2337
|
-
${
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
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.
|
|
2365
3078
|
return html `
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
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"
|
|
2371
3120
|
>
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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"
|
|
3209
|
+
>
|
|
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"
|
|
2375
3227
|
/>
|
|
2376
|
-
|
|
2377
|
-
|
|
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;
|
|
2378
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';
|
|
2379
3256
|
return html `
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
<span
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
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
|
+
`;
|
|
2390
3269
|
})}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
</
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
class="message-copy"
|
|
2410
|
-
title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
|
|
2411
|
-
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
2412
|
-
>
|
|
2413
|
-
${this._copiedIndex === index ? html `
|
|
2414
|
-
<svg
|
|
2415
|
-
viewBox="0 0 24 24"
|
|
2416
|
-
fill="none"
|
|
2417
|
-
stroke="currentColor"
|
|
2418
|
-
stroke-width="1.5"
|
|
2419
|
-
stroke-linecap="round"
|
|
2420
|
-
stroke-linejoin="round"
|
|
2421
|
-
>
|
|
2422
|
-
<path d="M20 6 9 17l-5-5" />
|
|
2423
|
-
</svg>
|
|
2424
|
-
` : html `
|
|
2425
|
-
<svg
|
|
2426
|
-
viewBox="0 0 24 24"
|
|
2427
|
-
fill="none"
|
|
2428
|
-
stroke="currentColor"
|
|
2429
|
-
stroke-width="1.5"
|
|
3270
|
+
</div>
|
|
3271
|
+
</div>
|
|
3272
|
+
|
|
3273
|
+
${this._pendingActions.length && !this._inlineConfirm ? this._renderConfirmation() : nothing}
|
|
3274
|
+
|
|
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
|
|
2430
3288
|
stroke-linecap="round"
|
|
2431
3289
|
stroke-linejoin="round"
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
y="9"
|
|
2436
|
-
width="13"
|
|
2437
|
-
height="13"
|
|
2438
|
-
rx="2"
|
|
2439
|
-
/>
|
|
2440
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
2441
|
-
</svg>
|
|
2442
|
-
`}
|
|
3290
|
+
d="M6 18 18 6M6 6l12 12"
|
|
3291
|
+
/>
|
|
3292
|
+
</svg>
|
|
2443
3293
|
</button>
|
|
2444
|
-
|
|
2445
|
-
</div>
|
|
2446
|
-
`)}
|
|
2447
|
-
</div>
|
|
2448
|
-
</div>
|
|
2449
|
-
|
|
2450
|
-
${this._error ? html `
|
|
2451
|
-
<div class="error">
|
|
2452
|
-
<span>${this._error}</span>
|
|
2453
|
-
<button class="error-dismiss" @click=${this._handleErrorDismiss}>
|
|
2454
|
-
<svg
|
|
2455
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
2456
|
-
fill="none"
|
|
2457
|
-
viewBox="0 0 24 24"
|
|
2458
|
-
stroke-width="2"
|
|
2459
|
-
stroke="currentColor"
|
|
2460
|
-
class="error-dismiss-icon"
|
|
2461
|
-
>
|
|
2462
|
-
<path
|
|
2463
|
-
stroke-linecap="round"
|
|
2464
|
-
stroke-linejoin="round"
|
|
2465
|
-
d="M6 18 18 6M6 6l12 12"
|
|
2466
|
-
/>
|
|
2467
|
-
</svg>
|
|
2468
|
-
</button>
|
|
2469
|
-
</div>
|
|
3294
|
+
</div>
|
|
2470
3295
|
` : this._contextLimitReached ? html `
|
|
2471
3296
|
<div class="context-limit" role="alert">
|
|
2472
3297
|
${this._contextLimitMessage
|
|
@@ -2498,68 +3323,68 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2498
3323
|
</div>
|
|
2499
3324
|
` : nothing}
|
|
2500
3325
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
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({
|
|
2507
3332
|
chip: true,
|
|
2508
3333
|
'chip--doc': !p.previewUrl,
|
|
2509
3334
|
})}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
3335
|
+
title=${p.name}
|
|
3336
|
+
>
|
|
3337
|
+
<div
|
|
3338
|
+
class=${classMap({
|
|
2514
3339
|
'chip-body': true,
|
|
2515
3340
|
'chip-body--image': Boolean(p.previewUrl),
|
|
2516
3341
|
})}
|
|
2517
|
-
|
|
2518
|
-
|
|
3342
|
+
>
|
|
3343
|
+
${p.previewUrl
|
|
2519
3344
|
? html `<img src=${p.previewUrl} alt=${p.name} />`
|
|
2520
3345
|
: html `
|
|
2521
|
-
|
|
2522
|
-
|
|
3346
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
3347
|
+
<span class="chip-name">${p.name}</span>
|
|
2523
3348
|
`}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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>
|
|
2545
3370
|
`)}
|
|
2546
|
-
|
|
3371
|
+
</div>
|
|
2547
3372
|
` : nothing}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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
|
|
2559
3384
|
class="input-plus"
|
|
2560
3385
|
@click=${this._handlePlusClick}
|
|
2561
|
-
|
|
2562
|
-
|
|
3386
|
+
>+</button>
|
|
3387
|
+
<textarea
|
|
2563
3388
|
class="input"
|
|
2564
3389
|
.value=${this._inputValue}
|
|
2565
3390
|
placeholder=${this.placeholder}
|
|
@@ -2567,31 +3392,32 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2567
3392
|
@input=${this._handleInputChange}
|
|
2568
3393
|
@keydown=${this._handleKeyDown}
|
|
2569
3394
|
@paste=${this._handlePaste}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
${
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
<svg
|
|
2580
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
2581
|
-
fill="currentColor"
|
|
2582
|
-
viewBox="0 0 24 24"
|
|
2583
|
-
class="mic-stop-icon"
|
|
2584
|
-
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}
|
|
2585
3404
|
>
|
|
2586
|
-
<
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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>
|
|
2595
3421
|
` : html `
|
|
2596
3422
|
<button
|
|
2597
3423
|
class="mic"
|
|
@@ -2617,35 +3443,35 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2617
3443
|
</button>
|
|
2618
3444
|
`}
|
|
2619
3445
|
` : nothing}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
>
|
|
2626
|
-
<svg
|
|
2627
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
2628
|
-
fill="currentColor"
|
|
2629
|
-
viewBox="0 0 24 24"
|
|
2630
|
-
class="stop-icon"
|
|
3446
|
+
${this._isStreaming ? html `
|
|
3447
|
+
<button
|
|
3448
|
+
class="stop"
|
|
3449
|
+
@click=${this._handleStop}
|
|
3450
|
+
title="Stop generating"
|
|
2631
3451
|
>
|
|
2632
|
-
<
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
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>
|
|
2641
3467
|
` : html `
|
|
2642
3468
|
<button
|
|
2643
3469
|
class=${classMap({
|
|
2644
3470
|
'send': true,
|
|
2645
|
-
'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
|
|
3471
|
+
'send--disabled': (!this._inputValue.trim() && !this._pendingConversationFiles.length) || this._pendingActions.length > 0,
|
|
2646
3472
|
})}
|
|
2647
3473
|
@click=${this._handleSubmit}
|
|
2648
|
-
title
|
|
3474
|
+
title=${this._pendingActions.length ? 'Approve or deny the pending action first' : 'Send message'}
|
|
2649
3475
|
>
|
|
2650
3476
|
<svg
|
|
2651
3477
|
width="16"
|
|
@@ -2661,12 +3487,12 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2661
3487
|
</svg>
|
|
2662
3488
|
</button>
|
|
2663
3489
|
`}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3490
|
+
</div>
|
|
3491
|
+
</div>
|
|
3492
|
+
${this._skillBrowseOpened ? this._renderSkillBrowse() : nothing}
|
|
3493
|
+
${this._renderDrawer()}
|
|
3494
|
+
</div>
|
|
3495
|
+
`;
|
|
2670
3496
|
}
|
|
2671
3497
|
};
|
|
2672
3498
|
KRChatbot.styles = css `
|
|
@@ -2779,6 +3605,14 @@ KRChatbot.styles = css `
|
|
|
2779
3605
|
box-shadow: none;
|
|
2780
3606
|
border: none;
|
|
2781
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;
|
|
2782
3616
|
}
|
|
2783
3617
|
|
|
2784
3618
|
@keyframes chatbot-panel-in {
|
|
@@ -2846,16 +3680,31 @@ KRChatbot.styles = css `
|
|
|
2846
3680
|
color: #000000;
|
|
2847
3681
|
}
|
|
2848
3682
|
|
|
2849
|
-
.header-action--active {
|
|
2850
|
-
background: rgba(0, 0, 0, 0.08);
|
|
2851
|
-
color: #000000;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
3683
|
.header-action-icon {
|
|
2855
3684
|
width: 20px;
|
|
2856
3685
|
height: 20px;
|
|
2857
3686
|
}
|
|
2858
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
|
+
|
|
2859
3708
|
/* Messages */
|
|
2860
3709
|
/* Full-width scroll container — keeps the scrollbar at the panel edge. */
|
|
2861
3710
|
.messages {
|
|
@@ -2936,6 +3785,138 @@ KRChatbot.styles = css `
|
|
|
2936
3785
|
gap: 24px;
|
|
2937
3786
|
}
|
|
2938
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
|
+
|
|
2939
3920
|
/* anywhere, NOT break-word: both break a long unbreakable run (a pasted URL) for
|
|
2940
3921
|
layout, but only anywhere also shrinks the element's intrinsic min-content width.
|
|
2941
3922
|
With break-word the URL still reports its full unbroken width as the minimum,
|
|
@@ -3190,32 +4171,139 @@ KRChatbot.styles = css `
|
|
|
3190
4171
|
flex-shrink: 0;
|
|
3191
4172
|
}
|
|
3192
4173
|
|
|
3193
|
-
/*
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
4174
|
+
/* Action-confirmation card (Rail 3). Floats OVER the messages, anchored just
|
|
4175
|
+
above the composer — not part of the message scroll — so it stays put and
|
|
4176
|
+
in view while the transcript scrolls behind it. Its width matches the
|
|
4177
|
+
message text column: .messages-inner is 768px max with 32px padding, so the
|
|
4178
|
+
text column is 768 - 64. Plain-language ask + Yes/No for a non-technical
|
|
4179
|
+
user; no tool codes, no raw arguments. */
|
|
4180
|
+
.confirmation {
|
|
4181
|
+
position: absolute;
|
|
4182
|
+
left: 32px;
|
|
4183
|
+
right: 32px;
|
|
4184
|
+
/* Sits above the footer/composer with a clear gap between the two. */
|
|
4185
|
+
bottom: 104px;
|
|
4186
|
+
z-index: 6;
|
|
4187
|
+
max-width: 720px;
|
|
4188
|
+
margin: 0 auto;
|
|
4189
|
+
padding: 24px;
|
|
4190
|
+
border: 1px solid #d8d8d8;
|
|
4191
|
+
border-radius: 8px;
|
|
4192
|
+
background: var(--kr-chatbot-bg);
|
|
4193
|
+
color: var(--kr-chatbot-text);
|
|
4194
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
4195
|
+
/* Never taller than the panel leaves room for — scroll the card itself if a
|
|
4196
|
+
batched multi-action ask is long, rather than covering the whole panel. */
|
|
4197
|
+
max-height: calc(100% - 176px);
|
|
4198
|
+
overflow-y: auto;
|
|
3207
4199
|
}
|
|
3208
4200
|
|
|
3209
|
-
/*
|
|
3210
|
-
|
|
3211
|
-
|
|
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
|
+
|
|
4239
|
+
/* "Run {tool}?" heading — names the tool being run. */
|
|
4240
|
+
.confirmation__title {
|
|
4241
|
+
font-size: 16px;
|
|
4242
|
+
font-weight: 600;
|
|
4243
|
+
margin-bottom: 12px;
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
.confirmation__message {
|
|
4247
|
+
margin: 0;
|
|
4248
|
+
font-size: 14px;
|
|
4249
|
+
line-height: 1.5;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
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;
|
|
3212
4263
|
}
|
|
3213
4264
|
|
|
3214
|
-
.
|
|
4265
|
+
.confirmation__list-name {
|
|
4266
|
+
display: block;
|
|
4267
|
+
font-weight: 600;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
.confirmation__list-desc {
|
|
4271
|
+
display: block;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
/* Buttons in a row; wrap on a narrow panel rather than overflowing the card. */
|
|
4275
|
+
.confirmation__actions {
|
|
4276
|
+
display: flex;
|
|
4277
|
+
flex-wrap: wrap;
|
|
4278
|
+
align-items: center;
|
|
4279
|
+
gap: 10px;
|
|
4280
|
+
margin-top: 20px;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
/* Keyboard-highlighted decision — a focus ring around the button, drawn
|
|
4284
|
+
outside its border so it reads over both the flat (Yes) and outline
|
|
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. */
|
|
4288
|
+
.confirmation__btn--highlighted {
|
|
4289
|
+
outline: 2px solid var(--kr-chatbot-primary);
|
|
4290
|
+
outline-offset: 2px;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
/*
|
|
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.
|
|
4300
|
+
*/
|
|
4301
|
+
.drawer {
|
|
3215
4302
|
position: absolute;
|
|
3216
4303
|
top: 0;
|
|
3217
4304
|
right: 0;
|
|
3218
4305
|
bottom: 0;
|
|
4306
|
+
max-width: 100%;
|
|
3219
4307
|
background: var(--kr-chatbot-bg);
|
|
3220
4308
|
display: flex;
|
|
3221
4309
|
flex-direction: column;
|
|
@@ -3227,17 +4315,17 @@ KRChatbot.styles = css `
|
|
|
3227
4315
|
|
|
3228
4316
|
/* While dragging the resize handle, suppress the slide transition so the
|
|
3229
4317
|
drawer tracks the cursor 1:1. */
|
|
3230
|
-
.
|
|
4318
|
+
.drawer--resizing {
|
|
3231
4319
|
transition: none;
|
|
3232
4320
|
}
|
|
3233
4321
|
|
|
3234
|
-
.
|
|
4322
|
+
.drawer--open {
|
|
3235
4323
|
transform: translateX(0);
|
|
3236
4324
|
box-shadow: -4px 0 20px rgba(10, 26, 46, 0.07);
|
|
3237
4325
|
}
|
|
3238
4326
|
|
|
3239
4327
|
/* Left-edge grab handle for resizing the drawer. */
|
|
3240
|
-
.
|
|
4328
|
+
.drawer__resize {
|
|
3241
4329
|
position: absolute;
|
|
3242
4330
|
top: 0;
|
|
3243
4331
|
left: 0;
|
|
@@ -3247,7 +4335,7 @@ KRChatbot.styles = css `
|
|
|
3247
4335
|
z-index: 10;
|
|
3248
4336
|
}
|
|
3249
4337
|
|
|
3250
|
-
.
|
|
4338
|
+
.drawer__header {
|
|
3251
4339
|
display: flex;
|
|
3252
4340
|
align-items: center;
|
|
3253
4341
|
justify-content: space-between;
|
|
@@ -3257,13 +4345,13 @@ KRChatbot.styles = css `
|
|
|
3257
4345
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
|
3258
4346
|
}
|
|
3259
4347
|
|
|
3260
|
-
.
|
|
4348
|
+
.drawer__title {
|
|
3261
4349
|
font-size: 14px;
|
|
3262
4350
|
font-weight: 600;
|
|
3263
4351
|
color: #000000;
|
|
3264
4352
|
}
|
|
3265
4353
|
|
|
3266
|
-
.
|
|
4354
|
+
.drawer__body {
|
|
3267
4355
|
flex: 1;
|
|
3268
4356
|
overflow-y: auto;
|
|
3269
4357
|
overflow-x: hidden;
|
|
@@ -3472,6 +4560,89 @@ KRChatbot.styles = css `
|
|
|
3472
4560
|
margin-right: 4px;
|
|
3473
4561
|
}
|
|
3474
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
|
+
|
|
3475
4646
|
.mic {
|
|
3476
4647
|
width: 34px;
|
|
3477
4648
|
height: 34px;
|
|
@@ -4130,6 +5301,15 @@ __decorate([
|
|
|
4130
5301
|
__decorate([
|
|
4131
5302
|
state()
|
|
4132
5303
|
], KRChatbot.prototype, "_contextLimitMessage", void 0);
|
|
5304
|
+
__decorate([
|
|
5305
|
+
state()
|
|
5306
|
+
], KRChatbot.prototype, "_pendingActions", void 0);
|
|
5307
|
+
__decorate([
|
|
5308
|
+
state()
|
|
5309
|
+
], KRChatbot.prototype, "_confirmationHighlighted", void 0);
|
|
5310
|
+
__decorate([
|
|
5311
|
+
state()
|
|
5312
|
+
], KRChatbot.prototype, "_confirmationInFlight", void 0);
|
|
4133
5313
|
__decorate([
|
|
4134
5314
|
state()
|
|
4135
5315
|
], KRChatbot.prototype, "_telemetry", void 0);
|
|
@@ -4141,7 +5321,10 @@ __decorate([
|
|
|
4141
5321
|
], KRChatbot.prototype, "_cacheCheckpoints", void 0);
|
|
4142
5322
|
__decorate([
|
|
4143
5323
|
state()
|
|
4144
|
-
], KRChatbot.prototype, "
|
|
5324
|
+
], KRChatbot.prototype, "_drawerOpen", void 0);
|
|
5325
|
+
__decorate([
|
|
5326
|
+
state()
|
|
5327
|
+
], KRChatbot.prototype, "_drawerView", void 0);
|
|
4145
5328
|
__decorate([
|
|
4146
5329
|
state()
|
|
4147
5330
|
], KRChatbot.prototype, "_debugDrawerWidth", void 0);
|
|
@@ -4190,6 +5373,21 @@ __decorate([
|
|
|
4190
5373
|
__decorate([
|
|
4191
5374
|
state()
|
|
4192
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);
|
|
4193
5391
|
__decorate([
|
|
4194
5392
|
query('.panel')
|
|
4195
5393
|
], KRChatbot.prototype, "_panelEl", void 0);
|