@kodaris/krubble-app-components 1.0.116 → 1.0.118

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.
@@ -2481,8 +2481,7 @@ KRScreenNav.styles = i$6 `
2481
2481
 
2482
2482
  .nav {
2483
2483
  width: 200px;
2484
- background: white;
2485
- border-right: 1px solid #e5e7eb;
2484
+ background: #f3f7fcbf;
2486
2485
  display: flex;
2487
2486
  flex-direction: column;
2488
2487
  flex-shrink: 0;
@@ -5665,6 +5664,21 @@ let KRChatbot = class KRChatbot extends i$3 {
5665
5664
  this._reader = null;
5666
5665
  this._decoder = new TextDecoder();
5667
5666
  this._buffer = '';
5667
+ /**
5668
+ * True once the current turn ended cleanly (a `finished:true` chunk arrived).
5669
+ * Distinguishes a real completion from a dropped stream: if the SSE stream
5670
+ * closes (`result.done`) while this is still false, the turn is NOT over — the
5671
+ * connection dropped (5-min emitter timeout, tab was backgrounded, network
5672
+ * blip) — so we reconnect instead of unlocking the composer. Reset per turn.
5673
+ */
5674
+ this._turnFinishedCleanly = false;
5675
+ /**
5676
+ * Consecutive failed reconnect attempts for the current drop. Bounds the retry
5677
+ * loop so a genuinely dead turn (server gone) can't spin forever — after the
5678
+ * cap we give up and finish the turn. Reset when a reconnect succeeds or a new
5679
+ * turn starts.
5680
+ */
5681
+ this._reconnectAttempts = 0;
5668
5682
  this._userHasScrolledUp = false;
5669
5683
  this._isDragging = false;
5670
5684
  this._isResizing = false;
@@ -5851,11 +5865,26 @@ let KRChatbot = class KRChatbot extends i$3 {
5851
5865
  */
5852
5866
  this._drawerOpen = false;
5853
5867
  /**
5854
- * Which view the drawer shows — settings or debug. One drawer holds both; the
5855
- * header buttons switch this and the content/width swap inside. Never null, so
5856
- * the content stays visible while the drawer slides shut.
5868
+ * Which view the drawer shows — settings, debug, or a single tool call's full
5869
+ * detail. One drawer holds all three; the header buttons switch settings/debug
5870
+ * and clicking a tool line switches to `toolDetail`. The content/width swap
5871
+ * inside. Never null, so the content stays visible while the drawer slides shut.
5857
5872
  */
5858
5873
  this._drawerView = 'settings';
5874
+ /**
5875
+ * The tool call whose full input/output the `toolDetail` drawer view shows —
5876
+ * the clicked `tool_use` message and its paired `tool_result` message, stored
5877
+ * whole. The render reads `.data.*` off them (and formats for display) so we
5878
+ * don't duplicate the message shape here. Transient — set on click, never
5879
+ * persisted (its bytes can be large and the view is a peek). Null when none.
5880
+ */
5881
+ this._toolDetail = null;
5882
+ /** Which tool-detail block ('input' | 'output') just flashed "Copied" (null = none). */
5883
+ this._toolDetailCopied = null;
5884
+ /** Whether the tool-detail Input section is expanded. Both open on open. */
5885
+ this._toolDetailInputOpen = true;
5886
+ /** Whether the tool-detail Output section is expanded. */
5887
+ this._toolDetailOutputOpen = true;
5859
5888
  /** Current width of the drawer in px (user-resizable, persisted). */
5860
5889
  this._drawerWidth = 430;
5861
5890
  /** True while the user is dragging the drawer's resize handle. */
@@ -6109,6 +6138,20 @@ let KRChatbot = class KRChatbot extends i$3 {
6109
6138
  data,
6110
6139
  });
6111
6140
  }
6141
+ // Persisted reasoning row — the DB stores the model's thinking as its own
6142
+ // row with the text in `content` and type "reasoning". Live, reasoning
6143
+ // instead arrives as REASONING_PART deltas accumulated onto an assistant
6144
+ // bubble's `reasoning` property, which is the ONLY shape the render shows
6145
+ // (via msg.reasoning). Map the DB row into that same shape — an assistant
6146
+ // bubble carrying the text in `reasoning` — so reloaded/reconciled
6147
+ // reasoning renders identically to live instead of vanishing.
6148
+ if (msg.type === 'reasoning') {
6149
+ return Promise.resolve({
6150
+ type: 'assistant',
6151
+ content: '',
6152
+ reasoning: msg.content || '',
6153
+ });
6154
+ }
6112
6155
  // A persisted approve/deny record — content is {outcome, tools:[...]}.
6113
6156
  // Keep the type, parse content into `data`; the render reads msg.data.*.
6114
6157
  if (msg.type === 'action_decision') {
@@ -6151,6 +6194,16 @@ let KRChatbot = class KRChatbot extends i$3 {
6151
6194
  if (messages && messages.length) {
6152
6195
  this.messages = messages;
6153
6196
  }
6197
+ // A turn may still be running server-side for this conversation (the page was
6198
+ // reloaded mid-turn, or opened on another device while a turn streams). After
6199
+ // the transcript loads, try to attach to that live turn: _reconnectStream hits
6200
+ // the read-only attach endpoint — 200 resumes streaming (locks the composer +
6201
+ // shows the typing indicator so the user knows the AI is still working), 204
6202
+ // means nothing is running (stays idle). Only meaningful with a pinned
6203
+ // conversation and the built-in stream (a custom `send` owns its own turns).
6204
+ if (this.conversationId && !this._isStreaming) {
6205
+ this._reconnectStream();
6206
+ }
6154
6207
  }).catch(() => { });
6155
6208
  // Fetch the framework config on load (debugTelemetryEnabled, v2ChatEnabled,
6156
6209
  // registryServer). debugTelemetryEnabled gates the debug affordances and is
@@ -6374,6 +6427,12 @@ let KRChatbot = class KRChatbot extends i$3 {
6374
6427
  }
6375
6428
  _saveState() {
6376
6429
  try {
6430
+ // `toolDetail` is a transient peek, not a saved view — persist only the two
6431
+ // real preference views so a reload never reopens a stale tool.
6432
+ let persistedDrawerView = this._drawerView;
6433
+ if (persistedDrawerView === 'toolDetail') {
6434
+ persistedDrawerView = 'settings';
6435
+ }
6377
6436
  const state = {
6378
6437
  opened: String(this.opened),
6379
6438
  message: this._inputValue,
@@ -6382,7 +6441,7 @@ let KRChatbot = class KRChatbot extends i$3 {
6382
6441
  showToolInputs: String(this._showToolInputs),
6383
6442
  showConfirmationInput: String(this._showConfirmationInput),
6384
6443
  drawerOpen: String(this._drawerOpen),
6385
- drawerView: this._drawerView,
6444
+ drawerView: persistedDrawerView,
6386
6445
  drawerWidth: String(this._drawerWidth),
6387
6446
  };
6388
6447
  if (this._panelEl) {
@@ -7184,6 +7243,10 @@ let KRChatbot = class KRChatbot extends i$3 {
7184
7243
  content: '',
7185
7244
  }];
7186
7245
  this._isStreaming = true;
7246
+ // Fresh turn: not yet finished, no reconnects used. These gate the drop-vs-done
7247
+ // decision in _readStreamChunk when the stream closes.
7248
+ this._turnFinishedCleanly = false;
7249
+ this._reconnectAttempts = 0;
7187
7250
  // Scroll user's message to the top (Gemini-style). The runway (min-height =
7188
7251
  // viewport) goes on the agent-turn WRAPPER, not an individual message — the
7189
7252
  // whole turn (reasoning, tool lines, text, decision) streams inside that one
@@ -7394,6 +7457,10 @@ let KRChatbot = class KRChatbot extends i$3 {
7394
7457
  const btn = this._messagesEl?.querySelector(`.confirmation__btn[data-decision="${decision}"]`);
7395
7458
  btn?.showLoading();
7396
7459
  this._isStreaming = true;
7460
+ // Fresh turn (confirmation decision runs a model turn too): reset the
7461
+ // drop-vs-done gates used by _readStreamChunk.
7462
+ this._turnFinishedCleanly = false;
7463
+ this._reconnectAttempts = 0;
7397
7464
  this._userHasScrolledUp = false;
7398
7465
  this.send({
7399
7466
  message: '',
@@ -7525,6 +7592,162 @@ let KRChatbot = class KRChatbot extends i$3 {
7525
7592
  this._drawerOpen = false;
7526
7593
  this._saveState();
7527
7594
  }
7595
+ /**
7596
+ * Open the drawer to a single tool call's full input/output. Called when the
7597
+ * user clicks a tool-activity line in the transcript. Independent of the
7598
+ * `_showToolInputs` setting — this is the on-demand "show me everything for
7599
+ * THIS call" peek, so it works even when inline inputs are hidden. `result` is
7600
+ * the paired `tool_result` message data (may be absent if the tool hasn't
7601
+ * returned yet).
7602
+ */
7603
+ _openToolDetail(use) {
7604
+ // Pair the tool_use to the tool_result that immediately follows it (matched by
7605
+ // toolUseId) — the same pairing the transcript render uses for the ⎿ preview.
7606
+ const next = this.messages[this.messages.indexOf(use) + 1];
7607
+ this._toolDetail = {
7608
+ use,
7609
+ result: null,
7610
+ };
7611
+ if (next?.type === 'tool_result' && next.data?.toolUseId === use.data?.toolUseId) {
7612
+ this._toolDetail.result = next;
7613
+ }
7614
+ this._toolDetailCopied = null;
7615
+ this._toolDetailInputOpen = true;
7616
+ this._toolDetailOutputOpen = true;
7617
+ this._drawerView = 'toolDetail';
7618
+ this._drawerOpen = true;
7619
+ this._saveState();
7620
+ // Reset the panel scroll so a newly-opened tool starts at the top, rather
7621
+ // than inheriting the previous tool's scroll position. Wait for the render.
7622
+ this.updateComplete.then(() => {
7623
+ if (this._drawerBodyEl) {
7624
+ this._drawerBodyEl.scrollTop = 0;
7625
+ }
7626
+ });
7627
+ }
7628
+ /** Toggle the tool-detail Input section open/closed. */
7629
+ _handleToggleToolDetailInput() {
7630
+ this._toolDetailInputOpen = !this._toolDetailInputOpen;
7631
+ }
7632
+ /** Toggle the tool-detail Output section open/closed. */
7633
+ _handleToggleToolDetailOutput() {
7634
+ this._toolDetailOutputOpen = !this._toolDetailOutputOpen;
7635
+ }
7636
+ /**
7637
+ * Pretty-print a tool input/result for the detail view. The values arrive as
7638
+ * strings that usually hold JSON; reindent when they do, otherwise show as-is.
7639
+ * Never throws — falls back to the original text.
7640
+ *
7641
+ * A large/overflowing result is stashed server-side and returned as a preview
7642
+ * wrapped in `<<<uuid_BEGIN>>> … <<<uuid_END>>>` markers, followed by a paging
7643
+ * note. Two things make `JSON.parse` unusable here: the marker wrapper isn't
7644
+ * JSON, and the payload between the markers is a truncated SLICE of a bigger
7645
+ * blob (cut off mid-string), so it never parses on its own. So we reindent the
7646
+ * payload STRUCTURALLY (see `_reindentJson`) rather than parse it, and keep the
7647
+ * markers + note exactly where they were so it still reads as a paged response.
7648
+ */
7649
+ _formatToolJson(value) {
7650
+ if (value === undefined || value === null) {
7651
+ return '';
7652
+ }
7653
+ const raw = String(value);
7654
+ const marker = raw.match(/(<<<[0-9a-f-]+_BEGIN>>>\n?)([\s\S]*?)(\n?<<<[0-9a-f-]+_END>>>)/);
7655
+ if (marker && marker.index != null) {
7656
+ // Splice by index rather than String.replace — the reindented JSON can
7657
+ // contain `$` sequences (e.g. "$dtoRef") that replace() treats as special.
7658
+ const start = marker.index + marker[1].length;
7659
+ const end = start + marker[2].length;
7660
+ return raw.slice(0, start) + this._reindentJson(marker[2]) + raw.slice(end);
7661
+ }
7662
+ return this._reindentJson(raw);
7663
+ }
7664
+ /**
7665
+ * Reindent JSON-ish text for display. Tries a strict parse+stringify first (the
7666
+ * clean path for complete, valid JSON). If that fails — which it does for the
7667
+ * truncated slices returned by the stashed-response pager — it falls back to a
7668
+ * structural walk that inserts newlines/indentation around `{}[],:` while
7669
+ * tracking string state (so braces and commas INSIDE string values, and escaped
7670
+ * quotes, are left alone). This formats even incomplete JSON. Non-JSON text
7671
+ * (no leading `{`/`[`) is returned untouched.
7672
+ */
7673
+ _reindentJson(text) {
7674
+ const trimmed = text.trim();
7675
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
7676
+ return text;
7677
+ }
7678
+ try {
7679
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
7680
+ }
7681
+ catch {
7682
+ // Fall through to the tolerant structural reindent below.
7683
+ }
7684
+ let out = '';
7685
+ let depth = 0;
7686
+ let inString = false;
7687
+ let escaped = false;
7688
+ const pad = (level) => '\n' + ' '.repeat(Math.max(0, level));
7689
+ for (let i = 0; i < trimmed.length; i++) {
7690
+ const ch = trimmed[i];
7691
+ if (inString) {
7692
+ out += ch;
7693
+ if (escaped) {
7694
+ escaped = false;
7695
+ }
7696
+ else if (ch === '\\') {
7697
+ escaped = true;
7698
+ }
7699
+ else if (ch === '"') {
7700
+ inString = false;
7701
+ }
7702
+ continue;
7703
+ }
7704
+ if (ch === '"') {
7705
+ inString = true;
7706
+ out += ch;
7707
+ }
7708
+ else if (ch === '{' || ch === '[') {
7709
+ depth++;
7710
+ out += ch + pad(depth);
7711
+ }
7712
+ else if (ch === '}' || ch === ']') {
7713
+ depth--;
7714
+ out += pad(depth) + ch;
7715
+ }
7716
+ else if (ch === ',') {
7717
+ out += ch + pad(depth);
7718
+ }
7719
+ else if (ch === ':') {
7720
+ out += ': ';
7721
+ }
7722
+ else if (ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r') ;
7723
+ else {
7724
+ out += ch;
7725
+ }
7726
+ }
7727
+ return out;
7728
+ }
7729
+ /** Copy the tool-detail Input to the clipboard. Stops propagation so the click
7730
+ * doesn't also toggle the (parent) section header. */
7731
+ _handleCopyToolDetailInput(e) {
7732
+ e.stopPropagation();
7733
+ this._copyToolDetail('input', this._formatToolJson(this._toolDetail?.use.data?.toolInput));
7734
+ }
7735
+ /** Copy the tool-detail Output to the clipboard (see input handler). */
7736
+ _handleCopyToolDetailOutput(e) {
7737
+ e.stopPropagation();
7738
+ this._copyToolDetail('output', this._formatToolJson(this._toolDetail?.result?.data?.toolResultString));
7739
+ }
7740
+ /** Write one tool-detail block to the clipboard and flash "Copied" on it. */
7741
+ _copyToolDetail(block, content) {
7742
+ navigator.clipboard.writeText(content).then(() => {
7743
+ this._toolDetailCopied = block;
7744
+ setTimeout(() => {
7745
+ if (this._toolDetailCopied === block) {
7746
+ this._toolDetailCopied = null;
7747
+ }
7748
+ }, 2000);
7749
+ }).catch(() => { });
7750
+ }
7528
7751
  _handleDrawerResizeMouseDown(e) {
7529
7752
  this._isDrawerResizing = true;
7530
7753
  this._drawerResizeStartX = e.clientX;
@@ -7540,11 +7763,18 @@ let KRChatbot = class KRChatbot extends i$3 {
7540
7763
  }
7541
7764
  this._reader.read().then((result) => {
7542
7765
  if (result.done) {
7543
- // Fallback unlock: the stream closed without a `finished:true` chunk
7544
- // (network drop, server crash, the 5-min emitter timeout). Treat it as
7545
- // the turn ending so a dropped stream never wedges the composer — the
7546
- // server self-heals its turn lock and bounces any premature send.
7547
- this._finishStream();
7766
+ // The stream closed. Two very different cases:
7767
+ // 1. A `finished:true` chunk already arrived (_turnFinishedCleanly) — the
7768
+ // turn is genuinely over; finishing was already handled below. Nothing to do.
7769
+ // 2. No finished chunk yet — the connection DROPPED mid-turn (5-min emitter
7770
+ // timeout, tab backgrounded, network blip) but the server-side turn is
7771
+ // still running. Do NOT unlock the composer (that's the old bug that made
7772
+ // the input look ready while the AI was still working). Reconnect to the
7773
+ // live stream and keep receiving events.
7774
+ if (this._turnFinishedCleanly) {
7775
+ return;
7776
+ }
7777
+ this._reconnectStream();
7548
7778
  return;
7549
7779
  }
7550
7780
  this._buffer = this._buffer + this._decoder.decode(result.value, { stream: true });
@@ -7697,7 +7927,9 @@ let KRChatbot = class KRChatbot extends i$3 {
7697
7927
  }
7698
7928
  if (data.finished) {
7699
7929
  // Primary unlock signal: the turn is over and the server released
7700
- // the turn lock. The SSE stream closes right after this chunk.
7930
+ // the turn lock. The SSE stream closes right after this chunk. Mark
7931
+ // it clean so the imminent `result.done` doesn't trigger a reconnect.
7932
+ this._turnFinishedCleanly = true;
7701
7933
  this._finishStream();
7702
7934
  }
7703
7935
  }
@@ -7709,9 +7941,110 @@ let KRChatbot = class KRChatbot extends i$3 {
7709
7941
  }
7710
7942
  this._readStreamChunk();
7711
7943
  }).catch((err) => {
7712
- if (err.name !== 'AbortError') {
7713
- this._handleStreamError(err.message);
7944
+ if (err.name === 'AbortError') {
7945
+ return;
7946
+ }
7947
+ // A read error mid-turn is a drop, not a completion — reconnect (same as a
7948
+ // clean `done` without a finished chunk) unless the turn already finished.
7949
+ if (!this._turnFinishedCleanly) {
7950
+ this._reconnectStream();
7951
+ return;
7952
+ }
7953
+ this._handleStreamError(err.message);
7954
+ });
7955
+ }
7956
+ /**
7957
+ * Reconnect to an in-flight turn after the SSE stream dropped without finishing
7958
+ * (5-min emitter timeout, tab backgrounded, network blip). Hits the read-only
7959
+ * `/stream/attach/{conversationID}` endpoint, which re-attaches a fresh SSE
7960
+ * connection to the still-running server-side turn without starting anything new.
7961
+ *
7962
+ * - 200 with a body → resume reading; the turn's events keep flowing (events
7963
+ * emitted during the gap are not replayed, so the live text may jump forward —
7964
+ * the full transcript reconciles from the DB when the turn ends).
7965
+ * - 204 → no live turn: it already finished server-side while we were
7966
+ * disconnected. Reload the transcript so the final answer appears, then unlock.
7967
+ * - failure / retry cap hit → give up and finish so the composer never wedges.
7968
+ *
7969
+ * The composer stays locked (`_isStreaming` untouched) throughout, so the input
7970
+ * never looks ready while the AI is still working.
7971
+ */
7972
+ _reconnectStream() {
7973
+ // Without a pinned conversation there's nothing to re-attach to (a custom
7974
+ // `send` owns its own targeting). Fall back to finishing.
7975
+ if (!this.conversationId) {
7976
+ this._finishStream();
7977
+ return;
7978
+ }
7979
+ // Bound the retries so a genuinely dead turn can't loop forever.
7980
+ if (this._reconnectAttempts >= 5) {
7981
+ this._handleStreamError('Lost connection to the assistant. Please reload the conversation.');
7982
+ return;
7983
+ }
7984
+ this._reconnectAttempts = this._reconnectAttempts + 1;
7985
+ KRHttp.fetch({
7986
+ url: `/api/system/integration/aws/bedrock/stream/attach/${this.conversationId}`,
7987
+ method: 'GET',
7988
+ headers: { Accept: 'text/event-stream' },
7989
+ }).then((response) => {
7990
+ // 204 No Content: the turn finished server-side during the gap. Pull the
7991
+ // final transcript from the DB (via the same `load` callback the component
7992
+ // initializes with) so the answer that streamed during the disconnect
7993
+ // appears, then unlock.
7994
+ if (response.status === 204) {
7995
+ this._reconnectAttempts = 0;
7996
+ if (this.load) {
7997
+ this.load().then((messages) => {
7998
+ this.messages = messages;
7999
+ this.requestUpdate();
8000
+ }).catch(() => {
8001
+ // Reload failed — leave the transcript as-is; the user can refresh manually.
8002
+ });
8003
+ }
8004
+ this._finishStream();
8005
+ return;
7714
8006
  }
8007
+ if (!response.ok || !response.body) {
8008
+ // Transient failure — back off and try again on the same drop.
8009
+ setTimeout(() => this._reconnectStream(), 1000);
8010
+ return;
8011
+ }
8012
+ // Re-attached to the live turn: reset the retry counter and resume reading
8013
+ // into the same buffer/decoder from the fresh stream. Mark streaming so the
8014
+ // composer locks and the typing indicator shows — load-triggered attaches (a
8015
+ // page reloaded mid-turn) start with _isStreaming false, so this is what
8016
+ // surfaces "the AI is still working"; on a mid-session drop it's already true
8017
+ // (a no-op).
8018
+ this._reconnectAttempts = 0;
8019
+ this._isStreaming = true;
8020
+ this._reader = response.body.getReader();
8021
+ this._decoder = new TextDecoder();
8022
+ this._buffer = '';
8023
+ // Close the gap: events the turn emitted between the drop and this re-attach
8024
+ // aren't replayed on the live stream. Every COMPLETED iteration during the
8025
+ // gap was persisted (assistant text, tool_use/tool_result rows), so reload the
8026
+ // transcript from the DB to backfill them, THEN start reading live. Ordering
8027
+ // matters — the reader buffers incoming bytes until _readStreamChunk runs, so
8028
+ // starting live only after the async reload resolves prevents the reload from
8029
+ // clobbering freshly-streamed chunks. Only the current in-flight iteration's
8030
+ // partial text is missing from the reload; it streams live from here.
8031
+ if (this.load) {
8032
+ this.load().then((messages) => {
8033
+ this.messages = messages;
8034
+ this.requestUpdate();
8035
+ this._readStreamChunk();
8036
+ }).catch(() => {
8037
+ // Reload failed — go live anyway; the gap's completed iterations just
8038
+ // won't show until the next full reload, but the turn keeps streaming.
8039
+ this._readStreamChunk();
8040
+ });
8041
+ }
8042
+ else {
8043
+ this._readStreamChunk();
8044
+ }
8045
+ }).catch(() => {
8046
+ // Network error reaching the attach endpoint — back off and retry.
8047
+ setTimeout(() => this._reconnectStream(), 1000);
7715
8048
  });
7716
8049
  }
7717
8050
  /**
@@ -8131,6 +8464,16 @@ let KRChatbot = class KRChatbot extends i$3 {
8131
8464
  const isDebug = this._drawerView === 'debug'
8132
8465
  && this.variant === 'inline'
8133
8466
  && this._aiFrameworkConfig.debugTelemetryEnabled === true;
8467
+ let title = 'Settings';
8468
+ let body = this._renderSettingsContent();
8469
+ if (isDebug) {
8470
+ title = 'Debug';
8471
+ body = this._renderDebugContent();
8472
+ }
8473
+ else if (this._drawerView === 'toolDetail' && this._toolDetail) {
8474
+ title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
8475
+ body = this._renderToolDetailContent();
8476
+ }
8134
8477
  return b `
8135
8478
  <div
8136
8479
  class=${e({
@@ -8145,7 +8488,7 @@ let KRChatbot = class KRChatbot extends i$3 {
8145
8488
  @mousedown=${this._handleDrawerResizeMouseDown}
8146
8489
  ></div>
8147
8490
  <div class="drawer__header">
8148
- <span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
8491
+ <span class="drawer__title">${title}</span>
8149
8492
  <button
8150
8493
  class="header-action"
8151
8494
  @click=${this._handleCloseDrawer}
@@ -8168,7 +8511,7 @@ let KRChatbot = class KRChatbot extends i$3 {
8168
8511
  </button>
8169
8512
  </div>
8170
8513
  <div class="drawer__body">
8171
- ${isDebug ? this._renderDebugContent() : this._renderSettingsContent()}
8514
+ ${body}
8172
8515
  </div>
8173
8516
  </div>
8174
8517
  `;
@@ -8202,6 +8545,129 @@ let KRChatbot = class KRChatbot extends i$3 {
8202
8545
  </div>
8203
8546
  `;
8204
8547
  }
8548
+ /**
8549
+ * The Tool detail view's body — the full input and output for one tool call,
8550
+ * pretty-printed in scrollable code blocks with a per-block copy button. This
8551
+ * is the "occasionally see everything" path: the transcript stays terse, and
8552
+ * clicking a tool line drops its complete input/output here, uncapped (the
8553
+ * blocks scroll rather than truncating).
8554
+ */
8555
+ _renderToolDetailContent() {
8556
+ return b `
8557
+ <div class="tool-detail">
8558
+ <div class="tool-detail__section">
8559
+ <button
8560
+ type="button"
8561
+ class="tool-detail__section-head"
8562
+ aria-expanded=${this._toolDetailInputOpen}
8563
+ @click=${this._handleToggleToolDetailInput}
8564
+ >
8565
+ <span class=${e({
8566
+ 'tool-detail__caret': true,
8567
+ 'tool-detail__caret--open': this._toolDetailInputOpen,
8568
+ })}>›</span>
8569
+ <span class="tool-detail__section-title">Input</span>
8570
+ <span
8571
+ class="tool-detail__copy"
8572
+ role="button"
8573
+ @click=${this._handleCopyToolDetailInput}
8574
+ >
8575
+ ${this._toolDetailCopied === 'input'
8576
+ ? b `
8577
+ <svg
8578
+ viewBox="0 0 24 24"
8579
+ fill="none"
8580
+ stroke="currentColor"
8581
+ stroke-width="1.5"
8582
+ stroke-linecap="round"
8583
+ stroke-linejoin="round"
8584
+ >
8585
+ <path d="M20 6 9 17l-5-5" />
8586
+ </svg>
8587
+ `
8588
+ : b `
8589
+ <svg
8590
+ viewBox="0 0 24 24"
8591
+ fill="none"
8592
+ stroke="currentColor"
8593
+ stroke-width="1.5"
8594
+ stroke-linecap="round"
8595
+ stroke-linejoin="round"
8596
+ >
8597
+ <rect
8598
+ x="9"
8599
+ y="9"
8600
+ width="13"
8601
+ height="13"
8602
+ rx="2"
8603
+ />
8604
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
8605
+ </svg>
8606
+ `}
8607
+ </span>
8608
+ </button>
8609
+ ${this._toolDetailInputOpen
8610
+ ? b `<pre class="tool-detail__code">${this._formatToolJson(this._toolDetail.use.data?.toolInput) || '(no input)'}</pre>`
8611
+ : A}
8612
+ </div>
8613
+ <div class="tool-detail__section">
8614
+ <button
8615
+ type="button"
8616
+ class="tool-detail__section-head"
8617
+ aria-expanded=${this._toolDetailOutputOpen}
8618
+ @click=${this._handleToggleToolDetailOutput}
8619
+ >
8620
+ <span class=${e({
8621
+ 'tool-detail__caret': true,
8622
+ 'tool-detail__caret--open': this._toolDetailOutputOpen,
8623
+ })}>›</span>
8624
+ <span class="tool-detail__section-title">Output</span>
8625
+ <span
8626
+ class="tool-detail__copy"
8627
+ role="button"
8628
+ @click=${this._handleCopyToolDetailOutput}
8629
+ >
8630
+ ${this._toolDetailCopied === 'output'
8631
+ ? b `
8632
+ <svg
8633
+ viewBox="0 0 24 24"
8634
+ fill="none"
8635
+ stroke="currentColor"
8636
+ stroke-width="1.5"
8637
+ stroke-linecap="round"
8638
+ stroke-linejoin="round"
8639
+ >
8640
+ <path d="M20 6 9 17l-5-5" />
8641
+ </svg>
8642
+ `
8643
+ : b `
8644
+ <svg
8645
+ viewBox="0 0 24 24"
8646
+ fill="none"
8647
+ stroke="currentColor"
8648
+ stroke-width="1.5"
8649
+ stroke-linecap="round"
8650
+ stroke-linejoin="round"
8651
+ >
8652
+ <rect
8653
+ x="9"
8654
+ y="9"
8655
+ width="13"
8656
+ height="13"
8657
+ rx="2"
8658
+ />
8659
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
8660
+ </svg>
8661
+ `}
8662
+ </span>
8663
+ </button>
8664
+ ${this._toolDetailOutputOpen
8665
+ ? b `<pre class="tool-detail__code">${this._formatToolJson(this._toolDetail.result?.data?.toolResultString) || '(no output yet)'}</pre>`
8666
+ : A}
8667
+ </div>
8668
+ </div>
8669
+ `;
8670
+ }
8205
8671
  /** The Debug view's body — telemetry scopes and the per-iteration table. */
8206
8672
  _renderDebugContent() {
8207
8673
  return b `
@@ -8563,8 +9029,17 @@ let KRChatbot = class KRChatbot extends i$3 {
8563
9029
  catch {
8564
9030
  formatted = String(result.toolResultString);
8565
9031
  }
9032
+ // Cap the inline preview by BOTH lines and characters. Line-only
9033
+ // truncation blows up on results whose first line is huge (e.g. a
9034
+ // minified-JSON blob or a stashed-response marker with a 30k-char
9035
+ // opening line) — 3 lines then still dumps tens of thousands of
9036
+ // chars into the transcript. Char-cap first (content-blind, like
9037
+ // the backend chokepoint), then keep at most 3 lines of that.
8566
9038
  const lines = formatted.split('\n');
8567
9039
  resultPreview = lines.slice(0, 3).join('\n');
9040
+ if (resultPreview.length > 200) {
9041
+ resultPreview = `${resultPreview.slice(0, 200)}…`;
9042
+ }
8568
9043
  resultRemainder = Math.max(0, lines.length - 3);
8569
9044
  }
8570
9045
  }
@@ -8593,24 +9068,32 @@ let KRChatbot = class KRChatbot extends i$3 {
8593
9068
  })}>
8594
9069
  ${msg.type === 'tool_use' ? b `
8595
9070
  <div class="tool-activity-block">
8596
- <div class=${e({
9071
+ <button
9072
+ type="button"
9073
+ class=${e({
8597
9074
  'tool-activity': true,
8598
9075
  'tool-activity--error': result?.toolResultStatus === 'error',
8599
- })}>
9076
+ })}
9077
+ @click=${() => this._openToolDetail(msg)}
9078
+ >
8600
9079
  <span class="tool-activity__chevron">›</span>
8601
9080
  <span class="tool-activity__label">${msg.data?.displayName || msg.data?.toolName || 'Tool'}</span>
8602
9081
  ${inputSummary ? b `<span class="tool-activity__input">(${inputSummary})</span>` : A}
8603
- </div>
9082
+ </button>
8604
9083
  ${this._showToolInputs && result?.toolResultString ? b `
8605
- <div class=${e({
9084
+ <button
9085
+ type="button"
9086
+ class=${e({
8606
9087
  'tool-activity__result': true,
8607
9088
  'tool-activity__result--error': result.toolResultStatus === 'error',
8608
- })}>
9089
+ })}
9090
+ @click=${() => this._openToolDetail(msg)}
9091
+ >
8609
9092
  <span class="tool-activity__connector">⎿</span>
8610
9093
  <pre class="tool-activity__result-body">${resultPreview}${resultRemainder > 0
8611
9094
  ? b `\n<span class="tool-activity__more">… +${resultRemainder} lines</span>`
8612
9095
  : A}</pre>
8613
- </div>
9096
+ </button>
8614
9097
  ` : A}
8615
9098
  </div>
8616
9099
  ` : A}
@@ -9215,6 +9698,102 @@ KRChatbot.styles = i$6 `
9215
9698
  padding: 6px 0;
9216
9699
  }
9217
9700
 
9701
+ /* Tool detail view — the full input/output for one tool call. */
9702
+ .tool-detail {
9703
+ display: flex;
9704
+ flex-direction: column;
9705
+ gap: 24px;
9706
+ padding: 16px 20px;
9707
+ }
9708
+
9709
+ .tool-detail__section {
9710
+ display: flex;
9711
+ flex-direction: column;
9712
+ gap: 8px;
9713
+ min-width: 0;
9714
+ }
9715
+
9716
+ /* Clickable section header — toggles the code block below. Button chrome is
9717
+ stripped so it reads as a heading row. */
9718
+ .tool-detail__section-head {
9719
+ display: flex;
9720
+ align-items: center;
9721
+ gap: 8px;
9722
+ appearance: none;
9723
+ background: none;
9724
+ border: none;
9725
+ width: 100%;
9726
+ font-family: inherit;
9727
+ text-align: left;
9728
+ cursor: pointer;
9729
+ border-radius: 4px;
9730
+ }
9731
+
9732
+ .tool-detail__section-head:focus-visible {
9733
+ outline: 2px solid var(--kr-chatbot-primary);
9734
+ outline-offset: 2px;
9735
+ }
9736
+
9737
+ /* Disclosure caret — the same "›" used on tool lines; rotates down when open. */
9738
+ .tool-detail__caret {
9739
+ flex-shrink: 0;
9740
+ font-size: 16px;
9741
+ color: rgba(0, 0, 0, 0.55);
9742
+ transition: transform 0.15s ease;
9743
+ }
9744
+
9745
+ .tool-detail__caret--open {
9746
+ transform: rotate(90deg);
9747
+ }
9748
+
9749
+ .tool-detail__section-title {
9750
+ font-size: 12px;
9751
+ font-weight: 700;
9752
+ text-transform: uppercase;
9753
+ letter-spacing: 0.06em;
9754
+ color: #000000;
9755
+ }
9756
+
9757
+ /* Copy icon-button — same treatment as the per-message copy button. */
9758
+ .tool-detail__copy {
9759
+ margin-left: auto;
9760
+ display: inline-flex;
9761
+ align-items: center;
9762
+ justify-content: center;
9763
+ width: 26px;
9764
+ height: 26px;
9765
+ padding: 0;
9766
+ background: none;
9767
+ border: none;
9768
+ border-radius: 6px;
9769
+ color: #000000;
9770
+ cursor: pointer;
9771
+ transition: background-color 0.15s, color 0.15s;
9772
+ }
9773
+
9774
+ .tool-detail__copy:hover {
9775
+ background: rgba(0, 0, 0, 0.06);
9776
+ }
9777
+
9778
+ .tool-detail__copy svg {
9779
+ width: 14px;
9780
+ height: 14px;
9781
+ }
9782
+
9783
+ /* The code block — pretty-printed JSON, shown in full (no inner scroll). The
9784
+ drawer body is the only scroll container, so long inputs/outputs extend the
9785
+ page and you scroll the panel, not a nested box. */
9786
+ .tool-detail__code {
9787
+ margin: 0;
9788
+ padding: 0 18px;
9789
+ font-family: 'SF Mono', 'Fira Code', Monaco, monospace;
9790
+ font-size: 12px;
9791
+ line-height: 1.5;
9792
+ color: #000000;
9793
+ white-space: pre-wrap;
9794
+ word-break: break-word;
9795
+ }
9796
+
9218
9797
  /* Messages */
9219
9798
  /* Full-width scroll container — keeps the scrollbar at the panel edge. */
9220
9799
  .messages {
@@ -9311,6 +9890,9 @@ KRChatbot.styles = i$6 `
9311
9890
  /* Tool-activity line — "the assistant ran X" row in the transcript. Same text
9312
9891
  style as any other message (black, 14px); a leading › chevron marks it as a
9313
9892
  step (in the same text style, not an icon). */
9893
+ /* The tool-activity line and its result preview are <button>s (they open the
9894
+ tool-detail drawer on click) — strip the native button chrome so they read
9895
+ as plain transcript text, then re-add the layout. */
9314
9896
  .tool-activity {
9315
9897
  align-self: flex-start;
9316
9898
  display: flex;
@@ -9320,6 +9902,22 @@ KRChatbot.styles = i$6 `
9320
9902
  color: #000000;
9321
9903
  /* Keep the chevron + name + input on a single line; input truncates if long. */
9322
9904
  white-space: nowrap;
9905
+ /* Button reset */
9906
+ appearance: none;
9907
+ background: none;
9908
+ border: none;
9909
+ margin: 0;
9910
+ padding: 2px 4px;
9911
+ font-family: inherit;
9912
+ text-align: left;
9913
+ cursor: pointer;
9914
+ border-radius: 4px;
9915
+ max-width: 100%;
9916
+ }
9917
+
9918
+ .tool-activity:focus-visible {
9919
+ outline: 2px solid var(--kr-chatbot-primary);
9920
+ outline-offset: 1px;
9323
9921
  }
9324
9922
 
9325
9923
  .tool-activity--error {
@@ -9355,6 +9953,16 @@ KRChatbot.styles = i$6 `
9355
9953
  gap: 4px;
9356
9954
  }
9357
9955
 
9956
+ /* One hover for the whole tool block: hovering anywhere over the header or its
9957
+ result underlines the tool name, input, and result together as a single
9958
+ link, rather than lighting up header and result separately. */
9959
+ .tool-activity-block:hover .tool-activity__label,
9960
+ .tool-activity-block:hover .tool-activity__input,
9961
+ .tool-activity-block:hover .tool-activity__result-body {
9962
+ text-decoration: underline;
9963
+ text-decoration-color: rgba(0, 0, 0, 0.25);
9964
+ }
9965
+
9358
9966
  /* The result block indented under the activity line with a ⎿ tree connector,
9359
9967
  like Claude CLI's tool output preview. Uses the normal message text style
9360
9968
  (14px, black) — only the ⎿ connector is muted. */
@@ -9370,6 +9978,20 @@ KRChatbot.styles = i$6 `
9370
9978
  margin-left: 6px;
9371
9979
  font-size: 14px;
9372
9980
  color: #000000;
9981
+ /* Button reset — this preview row is also a click target for the detail view. */
9982
+ appearance: none;
9983
+ background: none;
9984
+ border: none;
9985
+ padding: 0;
9986
+ font-family: inherit;
9987
+ text-align: left;
9988
+ cursor: pointer;
9989
+ }
9990
+
9991
+ .tool-activity__result:focus-visible {
9992
+ outline: 2px solid var(--kr-chatbot-primary);
9993
+ outline-offset: 1px;
9994
+ border-radius: 4px;
9373
9995
  }
9374
9996
 
9375
9997
  .tool-activity__result--error {
@@ -10835,6 +11457,18 @@ __decorate([
10835
11457
  __decorate([
10836
11458
  r()
10837
11459
  ], KRChatbot.prototype, "_drawerView", void 0);
11460
+ __decorate([
11461
+ r()
11462
+ ], KRChatbot.prototype, "_toolDetail", void 0);
11463
+ __decorate([
11464
+ r()
11465
+ ], KRChatbot.prototype, "_toolDetailCopied", void 0);
11466
+ __decorate([
11467
+ r()
11468
+ ], KRChatbot.prototype, "_toolDetailInputOpen", void 0);
11469
+ __decorate([
11470
+ r()
11471
+ ], KRChatbot.prototype, "_toolDetailOutputOpen", void 0);
10838
11472
  __decorate([
10839
11473
  r()
10840
11474
  ], KRChatbot.prototype, "_drawerWidth", void 0);
@@ -10916,6 +11550,9 @@ __decorate([
10916
11550
  __decorate([
10917
11551
  e$3('.skill-menu')
10918
11552
  ], KRChatbot.prototype, "_skillMenuEl", void 0);
11553
+ __decorate([
11554
+ e$3('.drawer__body')
11555
+ ], KRChatbot.prototype, "_drawerBodyEl", void 0);
10919
11556
  KRChatbot = __decorate([
10920
11557
  t$2('kr-chatbot')
10921
11558
  ], KRChatbot);