@kodaris/krubble-app-components 1.0.116 → 1.0.117

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.js CHANGED
@@ -339,11 +339,26 @@ let KRChatbot = class KRChatbot extends LitElement {
339
339
  */
340
340
  this._drawerOpen = false;
341
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.
342
+ * Which view the drawer shows — settings, debug, or a single tool call's full
343
+ * detail. One drawer holds all three; the header buttons switch settings/debug
344
+ * and clicking a tool line switches to `toolDetail`. The content/width swap
345
+ * inside. Never null, so the content stays visible while the drawer slides shut.
345
346
  */
346
347
  this._drawerView = 'settings';
348
+ /**
349
+ * The tool call whose full input/output the `toolDetail` drawer view shows —
350
+ * the clicked `tool_use` message and its paired `tool_result` message, stored
351
+ * whole. The render reads `.data.*` off them (and formats for display) so we
352
+ * don't duplicate the message shape here. Transient — set on click, never
353
+ * persisted (its bytes can be large and the view is a peek). Null when none.
354
+ */
355
+ this._toolDetail = null;
356
+ /** Which tool-detail block ('input' | 'output') just flashed "Copied" (null = none). */
357
+ this._toolDetailCopied = null;
358
+ /** Whether the tool-detail Input section is expanded. Both open on open. */
359
+ this._toolDetailInputOpen = true;
360
+ /** Whether the tool-detail Output section is expanded. */
361
+ this._toolDetailOutputOpen = true;
347
362
  /** Current width of the drawer in px (user-resizable, persisted). */
348
363
  this._drawerWidth = 430;
349
364
  /** True while the user is dragging the drawer's resize handle. */
@@ -862,6 +877,12 @@ let KRChatbot = class KRChatbot extends LitElement {
862
877
  }
863
878
  _saveState() {
864
879
  try {
880
+ // `toolDetail` is a transient peek, not a saved view — persist only the two
881
+ // real preference views so a reload never reopens a stale tool.
882
+ let persistedDrawerView = this._drawerView;
883
+ if (persistedDrawerView === 'toolDetail') {
884
+ persistedDrawerView = 'settings';
885
+ }
865
886
  const state = {
866
887
  opened: String(this.opened),
867
888
  message: this._inputValue,
@@ -870,7 +891,7 @@ let KRChatbot = class KRChatbot extends LitElement {
870
891
  showToolInputs: String(this._showToolInputs),
871
892
  showConfirmationInput: String(this._showConfirmationInput),
872
893
  drawerOpen: String(this._drawerOpen),
873
- drawerView: this._drawerView,
894
+ drawerView: persistedDrawerView,
874
895
  drawerWidth: String(this._drawerWidth),
875
896
  };
876
897
  if (this._panelEl) {
@@ -2013,6 +2034,164 @@ let KRChatbot = class KRChatbot extends LitElement {
2013
2034
  this._drawerOpen = false;
2014
2035
  this._saveState();
2015
2036
  }
2037
+ /**
2038
+ * Open the drawer to a single tool call's full input/output. Called when the
2039
+ * user clicks a tool-activity line in the transcript. Independent of the
2040
+ * `_showToolInputs` setting — this is the on-demand "show me everything for
2041
+ * THIS call" peek, so it works even when inline inputs are hidden. `result` is
2042
+ * the paired `tool_result` message data (may be absent if the tool hasn't
2043
+ * returned yet).
2044
+ */
2045
+ _openToolDetail(use) {
2046
+ // Pair the tool_use to the tool_result that immediately follows it (matched by
2047
+ // toolUseId) — the same pairing the transcript render uses for the ⎿ preview.
2048
+ const next = this.messages[this.messages.indexOf(use) + 1];
2049
+ this._toolDetail = {
2050
+ use,
2051
+ result: null,
2052
+ };
2053
+ if (next?.type === 'tool_result' && next.data?.toolUseId === use.data?.toolUseId) {
2054
+ this._toolDetail.result = next;
2055
+ }
2056
+ this._toolDetailCopied = null;
2057
+ this._toolDetailInputOpen = true;
2058
+ this._toolDetailOutputOpen = true;
2059
+ this._drawerView = 'toolDetail';
2060
+ this._drawerOpen = true;
2061
+ this._saveState();
2062
+ // Reset the panel scroll so a newly-opened tool starts at the top, rather
2063
+ // than inheriting the previous tool's scroll position. Wait for the render.
2064
+ this.updateComplete.then(() => {
2065
+ if (this._drawerBodyEl) {
2066
+ this._drawerBodyEl.scrollTop = 0;
2067
+ }
2068
+ });
2069
+ }
2070
+ /** Toggle the tool-detail Input section open/closed. */
2071
+ _handleToggleToolDetailInput() {
2072
+ this._toolDetailInputOpen = !this._toolDetailInputOpen;
2073
+ }
2074
+ /** Toggle the tool-detail Output section open/closed. */
2075
+ _handleToggleToolDetailOutput() {
2076
+ this._toolDetailOutputOpen = !this._toolDetailOutputOpen;
2077
+ }
2078
+ /**
2079
+ * Pretty-print a tool input/result for the detail view. The values arrive as
2080
+ * strings that usually hold JSON; reindent when they do, otherwise show as-is.
2081
+ * Never throws — falls back to the original text.
2082
+ *
2083
+ * A large/overflowing result is stashed server-side and returned as a preview
2084
+ * wrapped in `<<<uuid_BEGIN>>> … <<<uuid_END>>>` markers, followed by a paging
2085
+ * note. Two things make `JSON.parse` unusable here: the marker wrapper isn't
2086
+ * JSON, and the payload between the markers is a truncated SLICE of a bigger
2087
+ * blob (cut off mid-string), so it never parses on its own. So we reindent the
2088
+ * payload STRUCTURALLY (see `_reindentJson`) rather than parse it, and keep the
2089
+ * markers + note exactly where they were so it still reads as a paged response.
2090
+ */
2091
+ _formatToolJson(value) {
2092
+ if (value === undefined || value === null) {
2093
+ return '';
2094
+ }
2095
+ const raw = String(value);
2096
+ const marker = raw.match(/(<<<[0-9a-f-]+_BEGIN>>>\n?)([\s\S]*?)(\n?<<<[0-9a-f-]+_END>>>)/);
2097
+ if (marker && marker.index != null) {
2098
+ // Splice by index rather than String.replace — the reindented JSON can
2099
+ // contain `$` sequences (e.g. "$dtoRef") that replace() treats as special.
2100
+ const start = marker.index + marker[1].length;
2101
+ const end = start + marker[2].length;
2102
+ return raw.slice(0, start) + this._reindentJson(marker[2]) + raw.slice(end);
2103
+ }
2104
+ return this._reindentJson(raw);
2105
+ }
2106
+ /**
2107
+ * Reindent JSON-ish text for display. Tries a strict parse+stringify first (the
2108
+ * clean path for complete, valid JSON). If that fails — which it does for the
2109
+ * truncated slices returned by the stashed-response pager — it falls back to a
2110
+ * structural walk that inserts newlines/indentation around `{}[],:` while
2111
+ * tracking string state (so braces and commas INSIDE string values, and escaped
2112
+ * quotes, are left alone). This formats even incomplete JSON. Non-JSON text
2113
+ * (no leading `{`/`[`) is returned untouched.
2114
+ */
2115
+ _reindentJson(text) {
2116
+ const trimmed = text.trim();
2117
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
2118
+ return text;
2119
+ }
2120
+ try {
2121
+ return JSON.stringify(JSON.parse(trimmed), null, 2);
2122
+ }
2123
+ catch {
2124
+ // Fall through to the tolerant structural reindent below.
2125
+ }
2126
+ let out = '';
2127
+ let depth = 0;
2128
+ let inString = false;
2129
+ let escaped = false;
2130
+ const pad = (level) => '\n' + ' '.repeat(Math.max(0, level));
2131
+ for (let i = 0; i < trimmed.length; i++) {
2132
+ const ch = trimmed[i];
2133
+ if (inString) {
2134
+ out += ch;
2135
+ if (escaped) {
2136
+ escaped = false;
2137
+ }
2138
+ else if (ch === '\\') {
2139
+ escaped = true;
2140
+ }
2141
+ else if (ch === '"') {
2142
+ inString = false;
2143
+ }
2144
+ continue;
2145
+ }
2146
+ if (ch === '"') {
2147
+ inString = true;
2148
+ out += ch;
2149
+ }
2150
+ else if (ch === '{' || ch === '[') {
2151
+ depth++;
2152
+ out += ch + pad(depth);
2153
+ }
2154
+ else if (ch === '}' || ch === ']') {
2155
+ depth--;
2156
+ out += pad(depth) + ch;
2157
+ }
2158
+ else if (ch === ',') {
2159
+ out += ch + pad(depth);
2160
+ }
2161
+ else if (ch === ':') {
2162
+ out += ': ';
2163
+ }
2164
+ else if (ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r') {
2165
+ // Collapse the source's own whitespace between tokens — our pad() owns layout.
2166
+ }
2167
+ else {
2168
+ out += ch;
2169
+ }
2170
+ }
2171
+ return out;
2172
+ }
2173
+ /** Copy the tool-detail Input to the clipboard. Stops propagation so the click
2174
+ * doesn't also toggle the (parent) section header. */
2175
+ _handleCopyToolDetailInput(e) {
2176
+ e.stopPropagation();
2177
+ this._copyToolDetail('input', this._formatToolJson(this._toolDetail?.use.data?.toolInput));
2178
+ }
2179
+ /** Copy the tool-detail Output to the clipboard (see input handler). */
2180
+ _handleCopyToolDetailOutput(e) {
2181
+ e.stopPropagation();
2182
+ this._copyToolDetail('output', this._formatToolJson(this._toolDetail?.result?.data?.toolResultString));
2183
+ }
2184
+ /** Write one tool-detail block to the clipboard and flash "Copied" on it. */
2185
+ _copyToolDetail(block, content) {
2186
+ navigator.clipboard.writeText(content).then(() => {
2187
+ this._toolDetailCopied = block;
2188
+ setTimeout(() => {
2189
+ if (this._toolDetailCopied === block) {
2190
+ this._toolDetailCopied = null;
2191
+ }
2192
+ }, 2000);
2193
+ }).catch(() => { });
2194
+ }
2016
2195
  _handleDrawerResizeMouseDown(e) {
2017
2196
  this._isDrawerResizing = true;
2018
2197
  this._drawerResizeStartX = e.clientX;
@@ -2619,6 +2798,16 @@ let KRChatbot = class KRChatbot extends LitElement {
2619
2798
  const isDebug = this._drawerView === 'debug'
2620
2799
  && this.variant === 'inline'
2621
2800
  && this._aiFrameworkConfig.debugTelemetryEnabled === true;
2801
+ let title = 'Settings';
2802
+ let body = this._renderSettingsContent();
2803
+ if (isDebug) {
2804
+ title = 'Debug';
2805
+ body = this._renderDebugContent();
2806
+ }
2807
+ else if (this._drawerView === 'toolDetail' && this._toolDetail) {
2808
+ title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
2809
+ body = this._renderToolDetailContent();
2810
+ }
2622
2811
  return html `
2623
2812
  <div
2624
2813
  class=${classMap({
@@ -2633,7 +2822,7 @@ let KRChatbot = class KRChatbot extends LitElement {
2633
2822
  @mousedown=${this._handleDrawerResizeMouseDown}
2634
2823
  ></div>
2635
2824
  <div class="drawer__header">
2636
- <span class="drawer__title">${isDebug ? 'Debug' : 'Settings'}</span>
2825
+ <span class="drawer__title">${title}</span>
2637
2826
  <button
2638
2827
  class="header-action"
2639
2828
  @click=${this._handleCloseDrawer}
@@ -2656,7 +2845,7 @@ let KRChatbot = class KRChatbot extends LitElement {
2656
2845
  </button>
2657
2846
  </div>
2658
2847
  <div class="drawer__body">
2659
- ${isDebug ? this._renderDebugContent() : this._renderSettingsContent()}
2848
+ ${body}
2660
2849
  </div>
2661
2850
  </div>
2662
2851
  `;
@@ -2690,6 +2879,129 @@ let KRChatbot = class KRChatbot extends LitElement {
2690
2879
  </div>
2691
2880
  `;
2692
2881
  }
2882
+ /**
2883
+ * The Tool detail view's body — the full input and output for one tool call,
2884
+ * pretty-printed in scrollable code blocks with a per-block copy button. This
2885
+ * is the "occasionally see everything" path: the transcript stays terse, and
2886
+ * clicking a tool line drops its complete input/output here, uncapped (the
2887
+ * blocks scroll rather than truncating).
2888
+ */
2889
+ _renderToolDetailContent() {
2890
+ return html `
2891
+ <div class="tool-detail">
2892
+ <div class="tool-detail__section">
2893
+ <button
2894
+ type="button"
2895
+ class="tool-detail__section-head"
2896
+ aria-expanded=${this._toolDetailInputOpen}
2897
+ @click=${this._handleToggleToolDetailInput}
2898
+ >
2899
+ <span class=${classMap({
2900
+ 'tool-detail__caret': true,
2901
+ 'tool-detail__caret--open': this._toolDetailInputOpen,
2902
+ })}>›</span>
2903
+ <span class="tool-detail__section-title">Input</span>
2904
+ <span
2905
+ class="tool-detail__copy"
2906
+ role="button"
2907
+ @click=${this._handleCopyToolDetailInput}
2908
+ >
2909
+ ${this._toolDetailCopied === 'input'
2910
+ ? html `
2911
+ <svg
2912
+ viewBox="0 0 24 24"
2913
+ fill="none"
2914
+ stroke="currentColor"
2915
+ stroke-width="1.5"
2916
+ stroke-linecap="round"
2917
+ stroke-linejoin="round"
2918
+ >
2919
+ <path d="M20 6 9 17l-5-5" />
2920
+ </svg>
2921
+ `
2922
+ : html `
2923
+ <svg
2924
+ viewBox="0 0 24 24"
2925
+ fill="none"
2926
+ stroke="currentColor"
2927
+ stroke-width="1.5"
2928
+ stroke-linecap="round"
2929
+ stroke-linejoin="round"
2930
+ >
2931
+ <rect
2932
+ x="9"
2933
+ y="9"
2934
+ width="13"
2935
+ height="13"
2936
+ rx="2"
2937
+ />
2938
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
2939
+ </svg>
2940
+ `}
2941
+ </span>
2942
+ </button>
2943
+ ${this._toolDetailInputOpen
2944
+ ? html `<pre class="tool-detail__code">${this._formatToolJson(this._toolDetail.use.data?.toolInput) || '(no input)'}</pre>`
2945
+ : nothing}
2946
+ </div>
2947
+ <div class="tool-detail__section">
2948
+ <button
2949
+ type="button"
2950
+ class="tool-detail__section-head"
2951
+ aria-expanded=${this._toolDetailOutputOpen}
2952
+ @click=${this._handleToggleToolDetailOutput}
2953
+ >
2954
+ <span class=${classMap({
2955
+ 'tool-detail__caret': true,
2956
+ 'tool-detail__caret--open': this._toolDetailOutputOpen,
2957
+ })}>›</span>
2958
+ <span class="tool-detail__section-title">Output</span>
2959
+ <span
2960
+ class="tool-detail__copy"
2961
+ role="button"
2962
+ @click=${this._handleCopyToolDetailOutput}
2963
+ >
2964
+ ${this._toolDetailCopied === 'output'
2965
+ ? html `
2966
+ <svg
2967
+ viewBox="0 0 24 24"
2968
+ fill="none"
2969
+ stroke="currentColor"
2970
+ stroke-width="1.5"
2971
+ stroke-linecap="round"
2972
+ stroke-linejoin="round"
2973
+ >
2974
+ <path d="M20 6 9 17l-5-5" />
2975
+ </svg>
2976
+ `
2977
+ : html `
2978
+ <svg
2979
+ viewBox="0 0 24 24"
2980
+ fill="none"
2981
+ stroke="currentColor"
2982
+ stroke-width="1.5"
2983
+ stroke-linecap="round"
2984
+ stroke-linejoin="round"
2985
+ >
2986
+ <rect
2987
+ x="9"
2988
+ y="9"
2989
+ width="13"
2990
+ height="13"
2991
+ rx="2"
2992
+ />
2993
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
2994
+ </svg>
2995
+ `}
2996
+ </span>
2997
+ </button>
2998
+ ${this._toolDetailOutputOpen
2999
+ ? html `<pre class="tool-detail__code">${this._formatToolJson(this._toolDetail.result?.data?.toolResultString) || '(no output yet)'}</pre>`
3000
+ : nothing}
3001
+ </div>
3002
+ </div>
3003
+ `;
3004
+ }
2693
3005
  /** The Debug view's body — telemetry scopes and the per-iteration table. */
2694
3006
  _renderDebugContent() {
2695
3007
  return html `
@@ -3051,8 +3363,17 @@ let KRChatbot = class KRChatbot extends LitElement {
3051
3363
  catch {
3052
3364
  formatted = String(result.toolResultString);
3053
3365
  }
3366
+ // Cap the inline preview by BOTH lines and characters. Line-only
3367
+ // truncation blows up on results whose first line is huge (e.g. a
3368
+ // minified-JSON blob or a stashed-response marker with a 30k-char
3369
+ // opening line) — 3 lines then still dumps tens of thousands of
3370
+ // chars into the transcript. Char-cap first (content-blind, like
3371
+ // the backend chokepoint), then keep at most 3 lines of that.
3054
3372
  const lines = formatted.split('\n');
3055
3373
  resultPreview = lines.slice(0, 3).join('\n');
3374
+ if (resultPreview.length > 200) {
3375
+ resultPreview = `${resultPreview.slice(0, 200)}…`;
3376
+ }
3056
3377
  resultRemainder = Math.max(0, lines.length - 3);
3057
3378
  }
3058
3379
  }
@@ -3081,24 +3402,32 @@ let KRChatbot = class KRChatbot extends LitElement {
3081
3402
  })}>
3082
3403
  ${msg.type === 'tool_use' ? html `
3083
3404
  <div class="tool-activity-block">
3084
- <div class=${classMap({
3405
+ <button
3406
+ type="button"
3407
+ class=${classMap({
3085
3408
  'tool-activity': true,
3086
3409
  'tool-activity--error': result?.toolResultStatus === 'error',
3087
- })}>
3410
+ })}
3411
+ @click=${() => this._openToolDetail(msg)}
3412
+ >
3088
3413
  <span class="tool-activity__chevron">›</span>
3089
3414
  <span class="tool-activity__label">${msg.data?.displayName || msg.data?.toolName || 'Tool'}</span>
3090
3415
  ${inputSummary ? html `<span class="tool-activity__input">(${inputSummary})</span>` : nothing}
3091
- </div>
3416
+ </button>
3092
3417
  ${this._showToolInputs && result?.toolResultString ? html `
3093
- <div class=${classMap({
3418
+ <button
3419
+ type="button"
3420
+ class=${classMap({
3094
3421
  'tool-activity__result': true,
3095
3422
  'tool-activity__result--error': result.toolResultStatus === 'error',
3096
- })}>
3423
+ })}
3424
+ @click=${() => this._openToolDetail(msg)}
3425
+ >
3097
3426
  <span class="tool-activity__connector">⎿</span>
3098
3427
  <pre class="tool-activity__result-body">${resultPreview}${resultRemainder > 0
3099
3428
  ? html `\n<span class="tool-activity__more">… +${resultRemainder} lines</span>`
3100
3429
  : nothing}</pre>
3101
- </div>
3430
+ </button>
3102
3431
  ` : nothing}
3103
3432
  </div>
3104
3433
  ` : nothing}
@@ -3703,6 +4032,102 @@ KRChatbot.styles = css `
3703
4032
  padding: 6px 0;
3704
4033
  }
3705
4034
 
4035
+ /* Tool detail view — the full input/output for one tool call. */
4036
+ .tool-detail {
4037
+ display: flex;
4038
+ flex-direction: column;
4039
+ gap: 24px;
4040
+ padding: 16px 20px;
4041
+ }
4042
+
4043
+ .tool-detail__section {
4044
+ display: flex;
4045
+ flex-direction: column;
4046
+ gap: 8px;
4047
+ min-width: 0;
4048
+ }
4049
+
4050
+ /* Clickable section header — toggles the code block below. Button chrome is
4051
+ stripped so it reads as a heading row. */
4052
+ .tool-detail__section-head {
4053
+ display: flex;
4054
+ align-items: center;
4055
+ gap: 8px;
4056
+ appearance: none;
4057
+ background: none;
4058
+ border: none;
4059
+ width: 100%;
4060
+ font-family: inherit;
4061
+ text-align: left;
4062
+ cursor: pointer;
4063
+ border-radius: 4px;
4064
+ }
4065
+
4066
+ .tool-detail__section-head:focus-visible {
4067
+ outline: 2px solid var(--kr-chatbot-primary);
4068
+ outline-offset: 2px;
4069
+ }
4070
+
4071
+ /* Disclosure caret — the same "›" used on tool lines; rotates down when open. */
4072
+ .tool-detail__caret {
4073
+ flex-shrink: 0;
4074
+ font-size: 16px;
4075
+ color: rgba(0, 0, 0, 0.55);
4076
+ transition: transform 0.15s ease;
4077
+ }
4078
+
4079
+ .tool-detail__caret--open {
4080
+ transform: rotate(90deg);
4081
+ }
4082
+
4083
+ .tool-detail__section-title {
4084
+ font-size: 12px;
4085
+ font-weight: 700;
4086
+ text-transform: uppercase;
4087
+ letter-spacing: 0.06em;
4088
+ color: #000000;
4089
+ }
4090
+
4091
+ /* Copy icon-button — same treatment as the per-message copy button. */
4092
+ .tool-detail__copy {
4093
+ margin-left: auto;
4094
+ display: inline-flex;
4095
+ align-items: center;
4096
+ justify-content: center;
4097
+ width: 26px;
4098
+ height: 26px;
4099
+ padding: 0;
4100
+ background: none;
4101
+ border: none;
4102
+ border-radius: 6px;
4103
+ color: #000000;
4104
+ cursor: pointer;
4105
+ transition: background-color 0.15s, color 0.15s;
4106
+ }
4107
+
4108
+ .tool-detail__copy:hover {
4109
+ background: rgba(0, 0, 0, 0.06);
4110
+ }
4111
+
4112
+ .tool-detail__copy svg {
4113
+ width: 14px;
4114
+ height: 14px;
4115
+ }
4116
+
4117
+ /* The code block — pretty-printed JSON, shown in full (no inner scroll). The
4118
+ drawer body is the only scroll container, so long inputs/outputs extend the
4119
+ page and you scroll the panel, not a nested box. */
4120
+ .tool-detail__code {
4121
+ margin: 0;
4122
+ padding: 0 18px;
4123
+ font-family: 'SF Mono', 'Fira Code', Monaco, monospace;
4124
+ font-size: 12px;
4125
+ line-height: 1.5;
4126
+ color: #000000;
4127
+ white-space: pre-wrap;
4128
+ word-break: break-word;
4129
+ }
4130
+
3706
4131
  /* Messages */
3707
4132
  /* Full-width scroll container — keeps the scrollbar at the panel edge. */
3708
4133
  .messages {
@@ -3799,6 +4224,9 @@ KRChatbot.styles = css `
3799
4224
  /* Tool-activity line — "the assistant ran X" row in the transcript. Same text
3800
4225
  style as any other message (black, 14px); a leading › chevron marks it as a
3801
4226
  step (in the same text style, not an icon). */
4227
+ /* The tool-activity line and its result preview are <button>s (they open the
4228
+ tool-detail drawer on click) — strip the native button chrome so they read
4229
+ as plain transcript text, then re-add the layout. */
3802
4230
  .tool-activity {
3803
4231
  align-self: flex-start;
3804
4232
  display: flex;
@@ -3808,6 +4236,22 @@ KRChatbot.styles = css `
3808
4236
  color: #000000;
3809
4237
  /* Keep the chevron + name + input on a single line; input truncates if long. */
3810
4238
  white-space: nowrap;
4239
+ /* Button reset */
4240
+ appearance: none;
4241
+ background: none;
4242
+ border: none;
4243
+ margin: 0;
4244
+ padding: 2px 4px;
4245
+ font-family: inherit;
4246
+ text-align: left;
4247
+ cursor: pointer;
4248
+ border-radius: 4px;
4249
+ max-width: 100%;
4250
+ }
4251
+
4252
+ .tool-activity:focus-visible {
4253
+ outline: 2px solid var(--kr-chatbot-primary);
4254
+ outline-offset: 1px;
3811
4255
  }
3812
4256
 
3813
4257
  .tool-activity--error {
@@ -3843,6 +4287,16 @@ KRChatbot.styles = css `
3843
4287
  gap: 4px;
3844
4288
  }
3845
4289
 
4290
+ /* One hover for the whole tool block: hovering anywhere over the header or its
4291
+ result underlines the tool name, input, and result together as a single
4292
+ link, rather than lighting up header and result separately. */
4293
+ .tool-activity-block:hover .tool-activity__label,
4294
+ .tool-activity-block:hover .tool-activity__input,
4295
+ .tool-activity-block:hover .tool-activity__result-body {
4296
+ text-decoration: underline;
4297
+ text-decoration-color: rgba(0, 0, 0, 0.25);
4298
+ }
4299
+
3846
4300
  /* The result block indented under the activity line with a ⎿ tree connector,
3847
4301
  like Claude CLI's tool output preview. Uses the normal message text style
3848
4302
  (14px, black) — only the ⎿ connector is muted. */
@@ -3858,6 +4312,20 @@ KRChatbot.styles = css `
3858
4312
  margin-left: 6px;
3859
4313
  font-size: 14px;
3860
4314
  color: #000000;
4315
+ /* Button reset — this preview row is also a click target for the detail view. */
4316
+ appearance: none;
4317
+ background: none;
4318
+ border: none;
4319
+ padding: 0;
4320
+ font-family: inherit;
4321
+ text-align: left;
4322
+ cursor: pointer;
4323
+ }
4324
+
4325
+ .tool-activity__result:focus-visible {
4326
+ outline: 2px solid var(--kr-chatbot-primary);
4327
+ outline-offset: 1px;
4328
+ border-radius: 4px;
3861
4329
  }
3862
4330
 
3863
4331
  .tool-activity__result--error {
@@ -5323,6 +5791,18 @@ __decorate([
5323
5791
  __decorate([
5324
5792
  state()
5325
5793
  ], KRChatbot.prototype, "_drawerView", void 0);
5794
+ __decorate([
5795
+ state()
5796
+ ], KRChatbot.prototype, "_toolDetail", void 0);
5797
+ __decorate([
5798
+ state()
5799
+ ], KRChatbot.prototype, "_toolDetailCopied", void 0);
5800
+ __decorate([
5801
+ state()
5802
+ ], KRChatbot.prototype, "_toolDetailInputOpen", void 0);
5803
+ __decorate([
5804
+ state()
5805
+ ], KRChatbot.prototype, "_toolDetailOutputOpen", void 0);
5326
5806
  __decorate([
5327
5807
  state()
5328
5808
  ], KRChatbot.prototype, "_drawerWidth", void 0);
@@ -5404,6 +5884,9 @@ __decorate([
5404
5884
  __decorate([
5405
5885
  query('.skill-menu')
5406
5886
  ], KRChatbot.prototype, "_skillMenuEl", void 0);
5887
+ __decorate([
5888
+ query('.drawer__body')
5889
+ ], KRChatbot.prototype, "_drawerBodyEl", void 0);
5407
5890
  KRChatbot = __decorate([
5408
5891
  customElement('kr-chatbot')
5409
5892
  ], KRChatbot);