@kodaris/krubble-app-components 1.0.124 → 1.0.126

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
@@ -296,6 +296,14 @@ export class KRChatbot extends LitElement {
296
296
  this._inputValue = '';
297
297
  this._isStreaming = false;
298
298
  this._error = '';
299
+ /**
300
+ * The current conversation's title. In the floating widget it replaces the
301
+ * static header label so the user sees which conversation they're in. Set from
302
+ * the loaded/switched conversation and from the live `CONVERSATION_TITLE` event
303
+ * (the server generates a title at the end of a new conversation's first turn);
304
+ * empty for a brand-new conversation, where the header falls back to `label`.
305
+ */
306
+ this._conversationTitle = '';
299
307
  /**
300
308
  * User-facing context warning text (`CONVERSATION_TOKEN_WARNING`). Shown as a
301
309
  * dismissible banner in all variants. Carries only friendly guidance, no numbers.
@@ -355,12 +363,23 @@ export class KRChatbot extends LitElement {
355
363
  */
356
364
  this._drawerOpen = false;
357
365
  /**
358
- * Which view the drawer shows — settings, debug, or a single tool call's full
359
- * detail. One drawer holds all three; the header buttons switch settings/debug
360
- * and clicking a tool line switches to `toolDetail`. The content/width swap
361
- * inside. Never null, so the content stays visible while the drawer slides shut.
366
+ * Which view the drawer shows — settings, debug, history, or a single tool
367
+ * call's full detail. One drawer holds them all; header buttons switch
368
+ * settings/debug/history and clicking a tool line switches to `toolDetail`. The
369
+ * content/width swap inside. Never null, so the content stays visible while the
370
+ * drawer slides shut. `history` is floating-only (the previous-conversations
371
+ * picker); in floating the drawer fills the panel over the transcript.
362
372
  */
363
373
  this._drawerView = 'settings';
374
+ /**
375
+ * The user's previous conversations, loaded from `/conversation/list` when the
376
+ * history view opens. Each is the raw ConversationViewDto (we read conversationID,
377
+ * title, lastModified). Empty until the first open; reloaded on each open so a
378
+ * freshly-created or renamed conversation shows.
379
+ */
380
+ this._conversations = [];
381
+ /** True while the history list is being fetched (drives a loading state). */
382
+ this._conversationsLoading = false;
364
383
  /**
365
384
  * The tool call whose full input/output the `toolDetail` drawer view shows —
366
385
  * the clicked `tool_use` message and its paired `tool_result` message, stored
@@ -686,7 +705,54 @@ export class KRChatbot extends LitElement {
686
705
  })));
687
706
  };
688
707
  }
689
- this.load().then((messages) => {
708
+ // Tab-scoped resume (floating only): a reload should redisplay the exact
709
+ // conversation this tab was on, and a brand-new tab should reopen the user's
710
+ // most-recent one. The pinned id lives in sessionStorage (per-tab — unlike
711
+ // localStorage it isn't shared across tabs, so two tabs can sit on different
712
+ // conversations). We resolve the id FIRST, then run the built-in load so it
713
+ // rebuilds the right transcript. Skipped when the consumer owns targeting (an
714
+ // explicit `conversation-id`) or in inline mode (embedded as a page — the host
715
+ // route, not the tab, decides the conversation).
716
+ let resolveConversationId = Promise.resolve();
717
+ if (this.variant === 'floating' && this.conversationId == null) {
718
+ const stored = Number(sessionStorage.getItem('kr-chatbot-conversation-id'));
719
+ if (stored > 0) {
720
+ this.conversationId = stored;
721
+ // Fetch the conversation so the header shows its title on reload (the
722
+ // transcript load doesn't carry it). Best-effort — the header falls back
723
+ // to the default label if this fails.
724
+ resolveConversationId = KRHttp.fetch({ url: `/api/system/conversation/${stored}` })
725
+ .then((r) => r.json())
726
+ .then((res) => {
727
+ this._conversationTitle = res?.data?.title || '';
728
+ })
729
+ .catch(() => { });
730
+ }
731
+ else {
732
+ // Fresh tab, nothing pinned yet — resume the user's most recent conversation
733
+ // (404 when they have none, which leaves us blank for a new one).
734
+ resolveConversationId = KRHttp.fetch({ url: '/api/system/conversation/last' })
735
+ .then((r) => {
736
+ if (!r.ok) {
737
+ return;
738
+ }
739
+ return r.json().then((res) => {
740
+ const id = res?.data?.conversationID;
741
+ if (typeof id === 'number') {
742
+ this.conversationId = id;
743
+ this._conversationTitle = res?.data?.title || '';
744
+ this._persistConversationId();
745
+ }
746
+ });
747
+ })
748
+ .catch(() => { });
749
+ }
750
+ }
751
+ // `this.load` is always set by here (defaulted above when the consumer didn't
752
+ // supply one), so capture it for the async chain — TS can't narrow the nullable
753
+ // property across the promise boundary.
754
+ const load = this.load;
755
+ resolveConversationId.then(() => load()).then((messages) => {
690
756
  if (messages && messages.length) {
691
757
  this.messages = messages;
692
758
  }
@@ -928,12 +994,38 @@ export class KRChatbot extends LitElement {
928
994
  }
929
995
  this._saveState();
930
996
  }
997
+ /**
998
+ * Persist the pinned conversation id per-tab so a reload redisplays this exact
999
+ * conversation. sessionStorage (not localStorage) is deliberate: it's scoped to
1000
+ * the tab, so two tabs can sit on different conversations without clobbering
1001
+ * each other. Only meaningful for the floating widget's built-in flow — inline
1002
+ * embeds and custom-`load` consumers own their own targeting.
1003
+ */
1004
+ _persistConversationId() {
1005
+ // Floating-only: inline embeds (and custom-`load` consumers) own their own
1006
+ // targeting and never resume from this key, so they must not write it either —
1007
+ // otherwise an inline chat sharing the page/origin would clobber the floating
1008
+ // widget's pinned id.
1009
+ if (this.variant !== 'floating') {
1010
+ return;
1011
+ }
1012
+ try {
1013
+ if (this.conversationId != null) {
1014
+ sessionStorage.setItem('kr-chatbot-conversation-id', String(this.conversationId));
1015
+ }
1016
+ else {
1017
+ sessionStorage.removeItem('kr-chatbot-conversation-id');
1018
+ }
1019
+ }
1020
+ catch { /* sessionStorage not available */ }
1021
+ }
931
1022
  _saveState() {
932
1023
  try {
933
- // `toolDetail` is a transient peek, not a saved view — persist only the two
934
- // real preference views so a reload never reopens a stale tool.
1024
+ // `toolDetail` and `history` are transient views, not saved ones — persist
1025
+ // only the real preference views so a reload never reopens a stale tool or a
1026
+ // stale conversation list.
935
1027
  let persistedDrawerView = this._drawerView;
936
- if (persistedDrawerView === 'toolDetail') {
1028
+ if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
937
1029
  persistedDrawerView = 'settings';
938
1030
  }
939
1031
  const state = {
@@ -1938,6 +2030,8 @@ export class KRChatbot extends LitElement {
1938
2030
  const id = conversation?.conversationID;
1939
2031
  if (typeof id === 'number') {
1940
2032
  this.conversationId = id;
2033
+ // Pin it to this tab so a reload redisplays this new conversation.
2034
+ this._persistConversationId();
1941
2035
  // Announce the newly-created conversation so consumers can react
1942
2036
  // (e.g. update the route from /chat/new to /chat/{id}). The full
1943
2037
  // conversation object is passed, not just the id, so consumers
@@ -2135,9 +2229,111 @@ export class KRChatbot extends LitElement {
2135
2229
  else {
2136
2230
  this._drawerOpen = true;
2137
2231
  this._drawerView = view;
2232
+ // Opening the history view refetches the user's previous conversations
2233
+ // (most-recently modified first) so a just-created or renamed conversation
2234
+ // shows without a reload. Ownership is enforced server-side (the endpoint
2235
+ // scopes to the current administrator), so we only filter to active ones.
2236
+ if (view === 'history') {
2237
+ this._conversationsLoading = true;
2238
+ KRHttp.fetch({
2239
+ url: '/api/system/conversation/list',
2240
+ method: 'POST',
2241
+ body: {
2242
+ page: 0,
2243
+ size: 50,
2244
+ sortBy: 'lastModified',
2245
+ sortDirection: 'desc',
2246
+ filterFields: [{
2247
+ name: 'active',
2248
+ operation: 'IS',
2249
+ value: 'true',
2250
+ }],
2251
+ },
2252
+ })
2253
+ .then((r) => r.json())
2254
+ .then((res) => {
2255
+ this._conversations = res?.data?.content || [];
2256
+ })
2257
+ .catch(() => {
2258
+ this._conversations = [];
2259
+ })
2260
+ .then(() => {
2261
+ this._conversationsLoading = false;
2262
+ });
2263
+ }
2138
2264
  }
2139
2265
  this._saveState();
2140
2266
  }
2267
+ /**
2268
+ * Switch the floating widget to a previously-created conversation (clicked in the
2269
+ * history list): stop any live turn, reset the transient turn/context state, pin
2270
+ * the new id (persisted per-tab so a reload stays here), then rebuild its
2271
+ * transcript via the built-in `load`. The drawer closes so the user lands back
2272
+ * in the conversation.
2273
+ */
2274
+ _handleSwitchConversation(conversationID) {
2275
+ if (conversationID === this.conversationId) {
2276
+ this._handleCloseDrawer();
2277
+ return;
2278
+ }
2279
+ this._handleStop();
2280
+ this.conversationId = conversationID;
2281
+ // Adopt the picked conversation's title for the header (empty for an untitled
2282
+ // one, so the header falls back to the default label).
2283
+ this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
2284
+ this._persistConversationId();
2285
+ this._resetConversationState();
2286
+ this._drawerOpen = false;
2287
+ this._saveState();
2288
+ if (this.load) {
2289
+ this.load().then((messages) => {
2290
+ this.messages = messages || [];
2291
+ // A turn may still be streaming server-side for the conversation we just
2292
+ // opened (started in another tab/device) — attach to it if so.
2293
+ if (this.conversationId && !this._isStreaming) {
2294
+ this._reconnectStream();
2295
+ }
2296
+ }).catch(() => { });
2297
+ }
2298
+ }
2299
+ /**
2300
+ * Start a brand-new conversation (clicked in the history list): unpin the id (the
2301
+ * next turn sends `createNew` and the resolved id re-pins), clear the transcript
2302
+ * and transient state, and close the drawer back to an empty composer.
2303
+ */
2304
+ _handleNewConversation() {
2305
+ this._handleStop();
2306
+ this.conversationId = null;
2307
+ // New conversation has no title yet — the header shows the default label until
2308
+ // the server generates one at the end of the first turn.
2309
+ this._conversationTitle = '';
2310
+ this._persistConversationId();
2311
+ this.messages = [];
2312
+ this._resetConversationState();
2313
+ this._drawerOpen = false;
2314
+ this._saveState();
2315
+ }
2316
+ /**
2317
+ * Clear the transient, per-conversation state that must not bleed across a
2318
+ * conversation switch/new — pending confirmations, context banners, errors, the
2319
+ * reconnect counter, and staged composer files. The transcript itself is set by
2320
+ * the caller (loaded for a switch, emptied for new).
2321
+ */
2322
+ _resetConversationState() {
2323
+ this._pendingActions = [];
2324
+ this._inlineConfirm = true;
2325
+ this._contextWarning = '';
2326
+ this._contextLimitReached = false;
2327
+ this._contextLimitMessage = '';
2328
+ this._error = '';
2329
+ this._reconnectAttempts = 0;
2330
+ this._pendingConversationFiles.forEach((p) => {
2331
+ if (p.previewUrl) {
2332
+ URL.revokeObjectURL(p.previewUrl);
2333
+ }
2334
+ });
2335
+ this._pendingConversationFiles = [];
2336
+ }
2141
2337
  /** Close the drawer. `_drawerView` is left intact so its content stays visible
2142
2338
  * while the drawer slides shut. */
2143
2339
  _handleCloseDrawer() {
@@ -2421,7 +2617,7 @@ export class KRChatbot extends LitElement {
2421
2617
  // deliberately never show). Adopt it as the header label and pass it
2422
2618
  // up so consumers can update their own chrome, e.g. a breadcrumb.
2423
2619
  if (data.data?.title) {
2424
- this.label = data.data.title;
2620
+ this._conversationTitle = data.data.title;
2425
2621
  this.dispatchEvent(new CustomEvent('conversation-title', {
2426
2622
  detail: { title: data.data.title },
2427
2623
  bubbles: true,
@@ -3007,10 +3203,75 @@ export class KRChatbot extends LitElement {
3007
3203
  * lifetime — plus a verbose per-iteration table. Never customer-facing.
3008
3204
  */
3009
3205
  /**
3010
- * The side drawer — one sliding panel that holds both the Settings and Debug
3011
- * views. The header buttons switch `_drawerView`; the title and body swap
3012
- * inside. The panel keeps one consistent (resizable) width across both views —
3013
- * switching views only changes the content, never the panel size.
3206
+ * The History view's body — the user's previous conversations (most recent
3207
+ * first) plus a "New conversation" action. Clicking a row switches the widget to
3208
+ * that conversation; the current one is marked so the user sees where they are.
3209
+ * Floating-only (opened from the header history button).
3210
+ */
3211
+ _renderHistoryContent() {
3212
+ return html `
3213
+ <div class="history">
3214
+ ${this._conversationsLoading ? html `
3215
+ <div class="history__empty">Loading…</div>
3216
+ ` : this._conversations.length === 0 ? html `
3217
+ <div class="history__empty">No previous conversations</div>
3218
+ ` : html `
3219
+ <div class="history__list">
3220
+ ${this._conversations.map((conversation) => html `
3221
+ <button
3222
+ type="button"
3223
+ class=${classMap({
3224
+ 'history__item': true,
3225
+ 'history__item--active': conversation.conversationID === this.conversationId,
3226
+ })}
3227
+ @click=${() => this._handleSwitchConversation(conversation.conversationID)}
3228
+ >
3229
+ <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
3230
+ ${conversation.lastModified ? html `
3231
+ <span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
3232
+ ` : nothing}
3233
+ </button>
3234
+ `)}
3235
+ </div>
3236
+ `}
3237
+ </div>
3238
+ `;
3239
+ }
3240
+ /**
3241
+ * Format a conversation's lastModified for the history list: today shows the
3242
+ * time, this year shows month/day, older shows month/day/year. Falls back to the
3243
+ * raw value if it isn't a parseable date.
3244
+ */
3245
+ _renderConversationDate(lastModified) {
3246
+ const date = new Date(lastModified);
3247
+ if (isNaN(date.getTime())) {
3248
+ return lastModified;
3249
+ }
3250
+ const now = new Date();
3251
+ if (date.toDateString() === now.toDateString()) {
3252
+ return date.toLocaleTimeString('en-US', {
3253
+ hour: 'numeric',
3254
+ minute: '2-digit',
3255
+ });
3256
+ }
3257
+ if (date.getFullYear() === now.getFullYear()) {
3258
+ return date.toLocaleDateString('en-US', {
3259
+ month: 'short',
3260
+ day: 'numeric',
3261
+ });
3262
+ }
3263
+ return date.toLocaleDateString('en-US', {
3264
+ month: 'short',
3265
+ day: 'numeric',
3266
+ year: 'numeric',
3267
+ });
3268
+ }
3269
+ /**
3270
+ * The side drawer — one sliding panel that holds the Settings, Debug, History,
3271
+ * and tool-detail views. The header buttons switch `_drawerView`; the title and
3272
+ * body swap inside. In inline mode it slides in beside the transcript at a
3273
+ * consistent (resizable) width; in floating mode it fills the panel over the
3274
+ * transcript with a back button (the panel is too narrow to show both).
3014
3275
  */
3015
3276
  _renderDrawer() {
3016
3277
  // The debug view is gated on the server-side flag (inline variant + debug
@@ -3028,41 +3289,101 @@ export class KRChatbot extends LitElement {
3028
3289
  title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
3029
3290
  body = this._renderToolDetailContent();
3030
3291
  }
3292
+ else if (this._drawerView === 'history') {
3293
+ title = 'Conversations';
3294
+ body = this._renderHistoryContent();
3295
+ }
3296
+ // In the floating widget the drawer fills the panel over the transcript (a
3297
+ // full-panel view with a back button) rather than sliding in beside it — the
3298
+ // panel is too narrow to show both. Inline keeps the side-drawer behavior.
3299
+ const fullPanel = this.variant === 'floating';
3031
3300
  return html `
3032
3301
  <div
3033
3302
  class=${classMap({
3034
3303
  'drawer': true,
3035
3304
  'drawer--open': this._drawerOpen,
3036
3305
  'drawer--resizing': this._isDrawerResizing,
3306
+ 'drawer--full': fullPanel,
3037
3307
  })}
3038
- style=${styleMap({ width: `${this._drawerWidth}px` })}
3308
+ style=${styleMap(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
3039
3309
  >
3040
- <div
3041
- class="drawer__resize"
3042
- @mousedown=${this._handleDrawerResizeMouseDown}
3043
- ></div>
3310
+ ${fullPanel ? nothing : html `
3311
+ <div
3312
+ class="drawer__resize"
3313
+ @mousedown=${this._handleDrawerResizeMouseDown}
3314
+ ></div>
3315
+ `}
3044
3316
  <div class="drawer__header">
3317
+ ${fullPanel ? html `
3318
+ <button
3319
+ class="header-action"
3320
+ @click=${this._handleCloseDrawer}
3321
+ title="Back"
3322
+ >
3323
+ <svg
3324
+ xmlns="http://www.w3.org/2000/svg"
3325
+ fill="none"
3326
+ viewBox="0 0 24 24"
3327
+ stroke-width="2"
3328
+ stroke="currentColor"
3329
+ class="header-action-icon"
3330
+ >
3331
+ <path
3332
+ stroke-linecap="round"
3333
+ stroke-linejoin="round"
3334
+ d="M15.75 19.5 8.25 12l7.5-7.5"
3335
+ />
3336
+ </svg>
3337
+ </button>
3338
+ ` : nothing}
3045
3339
  <span class="drawer__title">${title}</span>
3046
- <button
3047
- class="header-action"
3048
- @click=${this._handleCloseDrawer}
3049
- title="Close"
3050
- >
3051
- <svg
3052
- xmlns="http://www.w3.org/2000/svg"
3053
- fill="none"
3054
- viewBox="0 0 24 24"
3055
- stroke-width="2"
3056
- stroke="currentColor"
3057
- class="header-action-icon"
3340
+ ${this._drawerView === 'history' ? html `
3341
+ <kr-button
3342
+ class="history__new"
3343
+ variant="flat"
3344
+ color="primary"
3345
+ size="small"
3346
+ @click=${this._handleNewConversation}
3058
3347
  >
3059
- <path
3060
- stroke-linecap="round"
3061
- stroke-linejoin="round"
3062
- d="M6 18 18 6M6 6l12 12"
3063
- />
3064
- </svg>
3065
- </button>
3348
+ <svg
3349
+ slot="icon"
3350
+ xmlns="http://www.w3.org/2000/svg"
3351
+ fill="none"
3352
+ viewBox="0 0 24 24"
3353
+ stroke-width="1.75"
3354
+ stroke="currentColor"
3355
+ >
3356
+ <path
3357
+ stroke-linecap="round"
3358
+ stroke-linejoin="round"
3359
+ d="M12 4.5v15m7.5-7.5h-15"
3360
+ />
3361
+ </svg>
3362
+ New conversation
3363
+ </kr-button>
3364
+ ` : nothing}
3365
+ ${fullPanel ? nothing : html `
3366
+ <button
3367
+ class="header-action"
3368
+ @click=${this._handleCloseDrawer}
3369
+ title="Close"
3370
+ >
3371
+ <svg
3372
+ xmlns="http://www.w3.org/2000/svg"
3373
+ fill="none"
3374
+ viewBox="0 0 24 24"
3375
+ stroke-width="2"
3376
+ stroke="currentColor"
3377
+ class="header-action-icon"
3378
+ >
3379
+ <path
3380
+ stroke-linecap="round"
3381
+ stroke-linejoin="round"
3382
+ d="M6 18 18 6M6 6l12 12"
3383
+ />
3384
+ </svg>
3385
+ </button>
3386
+ `}
3066
3387
  </div>
3067
3388
  <div class="drawer__body">
3068
3389
  ${body}
@@ -3323,9 +3644,10 @@ export class KRChatbot extends LitElement {
3323
3644
  const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
3324
3645
  // Push the content over by the open drawer's width so the drawer sits beside
3325
3646
  // the content rather than overlaying it. One consistent (resizable) width for
3326
- // both views. Only pushes while the drawer is open.
3647
+ // both views. Only pushes while the drawer is open, and only inline — the
3648
+ // floating drawer fills the panel over the transcript, so it must not push.
3327
3649
  const contentPadding = {};
3328
- if (this._drawerOpen) {
3650
+ if (this._drawerOpen && this.variant === 'inline') {
3329
3651
  contentPadding.paddingRight = `${this._drawerWidth}px`;
3330
3652
  }
3331
3653
  // Group the transcript into runs so an agent turn (its reasoning/tool/text/
@@ -3443,10 +3765,52 @@ export class KRChatbot extends LitElement {
3443
3765
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
3444
3766
  <div class="header-left">
3445
3767
  <div class="header-info">
3446
- <span class="header-title">${this.label}</span>
3768
+ <span class="header-title">${this._conversationTitle || this.label}</span>
3447
3769
  </div>
3448
3770
  </div>
3449
3771
  <div class="header-actions">
3772
+ ${this.variant === 'floating' ? html `
3773
+ <button
3774
+ class="header-action"
3775
+ @click=${this._handleNewConversation}
3776
+ title="New conversation"
3777
+ >
3778
+ <svg
3779
+ xmlns="http://www.w3.org/2000/svg"
3780
+ fill="none"
3781
+ viewBox="0 0 24 24"
3782
+ stroke-width="1.75"
3783
+ stroke="currentColor"
3784
+ class="header-action-icon"
3785
+ >
3786
+ <path
3787
+ stroke-linecap="round"
3788
+ stroke-linejoin="round"
3789
+ d="M12 4.5v15m7.5-7.5h-15"
3790
+ />
3791
+ </svg>
3792
+ </button>
3793
+ <button
3794
+ class="header-action"
3795
+ @click=${() => this._toggleDrawer('history')}
3796
+ title="Conversations"
3797
+ >
3798
+ <svg
3799
+ xmlns="http://www.w3.org/2000/svg"
3800
+ fill="none"
3801
+ viewBox="0 0 24 24"
3802
+ stroke-width="1.5"
3803
+ stroke="currentColor"
3804
+ class="header-action-icon"
3805
+ >
3806
+ <path
3807
+ stroke-linecap="round"
3808
+ stroke-linejoin="round"
3809
+ d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
3810
+ />
3811
+ </svg>
3812
+ </button>
3813
+ ` : nothing}
3450
3814
  <button
3451
3815
  class="header-action"
3452
3816
  @click=${() => this._toggleDrawer('settings')}
@@ -4201,22 +4565,28 @@ KRChatbot.styles = css `
4201
4565
  display: flex;
4202
4566
  align-items: center;
4203
4567
  gap: 10px;
4568
+ min-width: 0;
4204
4569
  }
4205
4570
 
4206
4571
  .header-info {
4207
4572
  display: flex;
4208
4573
  flex-direction: column;
4574
+ min-width: 0;
4209
4575
  }
4210
4576
 
4211
4577
  .header-title {
4212
4578
  font-size: 16px;
4213
4579
  font-weight: 600;
4580
+ white-space: nowrap;
4581
+ overflow: hidden;
4582
+ text-overflow: ellipsis;
4214
4583
  }
4215
4584
 
4216
4585
  .header-actions {
4217
4586
  display: flex;
4218
4587
  align-items: center;
4219
4588
  gap: 4px;
4589
+ flex-shrink: 0;
4220
4590
  }
4221
4591
 
4222
4592
  .header-action {
@@ -5071,6 +5441,13 @@ KRChatbot.styles = css `
5071
5441
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
5072
5442
  }
5073
5443
 
5444
+ /* Full-panel (floating) header: back button then title, left-aligned. */
5445
+ .drawer--full .drawer__header {
5446
+ justify-content: flex-start;
5447
+ gap: 8px;
5448
+ padding: 0 12px;
5449
+ }
5450
+
5074
5451
  .drawer__title {
5075
5452
  font-size: 14px;
5076
5453
  font-weight: 600;
@@ -5084,6 +5461,78 @@ KRChatbot.styles = css `
5084
5461
  padding: 4px 0;
5085
5462
  }
5086
5463
 
5464
+ /* Floating variant: the drawer fills the whole panel over the transcript (a
5465
+ full-panel view with a back button), rather than sliding in beside it. */
5466
+ .drawer--full {
5467
+ left: 0;
5468
+ width: 100%;
5469
+ max-width: 100%;
5470
+ z-index: 6;
5471
+ }
5472
+
5473
+ .drawer--full.drawer--open {
5474
+ box-shadow: none;
5475
+ }
5476
+
5477
+ /* History (previous conversations) view. */
5478
+ .history {
5479
+ display: flex;
5480
+ flex-direction: column;
5481
+ }
5482
+
5483
+ /* "New conversation" action in the Conversations header — pushed to the right. */
5484
+ .history__new {
5485
+ margin-left: auto;
5486
+ }
5487
+
5488
+ .history__empty {
5489
+ padding: 24px 20px;
5490
+ text-align: center;
5491
+ font-size: 13px;
5492
+ color: rgb(0 0 0 / 50%);
5493
+ }
5494
+
5495
+ .history__list {
5496
+ display: flex;
5497
+ flex-direction: column;
5498
+ padding: 4px 0;
5499
+ }
5500
+
5501
+ .history__item {
5502
+ display: flex;
5503
+ flex-direction: column;
5504
+ align-items: flex-start;
5505
+ gap: 5px;
5506
+ padding: 10px 20px;
5507
+ background: transparent;
5508
+ border: none;
5509
+ text-align: left;
5510
+ cursor: pointer;
5511
+ transition: background 0.15s;
5512
+ }
5513
+
5514
+ .history__item:hover {
5515
+ background: rgba(0, 0, 0, 0.04);
5516
+ }
5517
+
5518
+ .history__item--active {
5519
+ background: rgba(0, 0, 0, 0.06);
5520
+ }
5521
+
5522
+ .history__item-title {
5523
+ font-size: 14px;
5524
+ color: #000000;
5525
+ white-space: nowrap;
5526
+ overflow: hidden;
5527
+ text-overflow: ellipsis;
5528
+ max-width: 100%;
5529
+ }
5530
+
5531
+ .history__item-date {
5532
+ font-size: 12px;
5533
+ color: rgb(0 0 0 / 60%);
5534
+ }
5535
+
5087
5536
  .debug-section {
5088
5537
  padding: 14px 20px;
5089
5538
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
@@ -6018,6 +6467,9 @@ __decorate([
6018
6467
  __decorate([
6019
6468
  state()
6020
6469
  ], KRChatbot.prototype, "_error", void 0);
6470
+ __decorate([
6471
+ state()
6472
+ ], KRChatbot.prototype, "_conversationTitle", void 0);
6021
6473
  __decorate([
6022
6474
  state()
6023
6475
  ], KRChatbot.prototype, "_contextWarning", void 0);
@@ -6051,6 +6503,12 @@ __decorate([
6051
6503
  __decorate([
6052
6504
  state()
6053
6505
  ], KRChatbot.prototype, "_drawerView", void 0);
6506
+ __decorate([
6507
+ state()
6508
+ ], KRChatbot.prototype, "_conversations", void 0);
6509
+ __decorate([
6510
+ state()
6511
+ ], KRChatbot.prototype, "_conversationsLoading", void 0);
6054
6512
  __decorate([
6055
6513
  state()
6056
6514
  ], KRChatbot.prototype, "_toolDetail", void 0);