@kodaris/krubble-app-components 1.0.123 → 1.0.125

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
@@ -202,6 +202,7 @@ export class KRChatbot extends LitElement {
202
202
  this._orgSkillRequestId = 0;
203
203
  this._globalSkillRequestId = 0;
204
204
  this._invocableSkillRequestId = 0;
205
+ this._suggestedActionRequestId = 0;
205
206
  /**
206
207
  * Codes of global skills with an in-flight enable/disable request. The `+`
207
208
  * button reads this to show a pending state and ignore repeat clicks. Mutated
@@ -295,6 +296,14 @@ export class KRChatbot extends LitElement {
295
296
  this._inputValue = '';
296
297
  this._isStreaming = false;
297
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 = '';
298
307
  /**
299
308
  * User-facing context warning text (`CONVERSATION_TOKEN_WARNING`). Shown as a
300
309
  * dismissible banner in all variants. Carries only friendly guidance, no numbers.
@@ -354,12 +363,23 @@ export class KRChatbot extends LitElement {
354
363
  */
355
364
  this._drawerOpen = false;
356
365
  /**
357
- * Which view the drawer shows — settings, debug, or a single tool call's full
358
- * detail. One drawer holds all three; the header buttons switch settings/debug
359
- * and clicking a tool line switches to `toolDetail`. The content/width swap
360
- * 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.
361
372
  */
362
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;
363
383
  /**
364
384
  * The tool call whose full input/output the `toolDetail` drawer view shows —
365
385
  * the clicked `tool_use` message and its paired `tool_result` message, stored
@@ -405,6 +425,12 @@ export class KRChatbot extends LitElement {
405
425
  * so installed globals in the menu never leak into the "Your Org" tab.
406
426
  */
407
427
  this._invocableSkills = [];
428
+ /**
429
+ * Context-aware action chips for the empty state, from `/conversation/suggestedActions`.
430
+ * Loaded fresh every time the panel opens on an empty conversation (see `_loadSuggestedActions`);
431
+ * cleared when the first message is sent so they don't linger over a live conversation.
432
+ */
433
+ this._suggestedActions = [];
408
434
  /**
409
435
  * AI framework UI config flags fetched from `aiFrameworkConfig` on connect.
410
436
  * `debugTelemetryEnabled` (same server-side gate as TOKEN_USAGE emission) turns
@@ -597,6 +623,11 @@ export class KRChatbot extends LitElement {
597
623
  // --- Lifecycle ---
598
624
  connectedCallback() {
599
625
  super.connectedCallback();
626
+ // Capture the consumer's targeting BEFORE we default `load` below: a custom
627
+ // `load` or an explicitly-set `conversation-id` means the consumer owns which
628
+ // conversation shows, so the tab-scoped resume must not override it.
629
+ const usesBuiltInLoad = !this.load;
630
+ const consumerSetConversationId = this.conversationId != null;
600
631
  // Default to loading a conversation's messages when the consumer hasn't
601
632
  // provided their own `load`. With `conversationId` set we rebuild that
602
633
  // conversation; with none set the user wants a NEW conversation, so we start
@@ -679,7 +710,54 @@ export class KRChatbot extends LitElement {
679
710
  })));
680
711
  };
681
712
  }
682
- this.load().then((messages) => {
713
+ // Tab-scoped resume (floating only): a reload should redisplay the exact
714
+ // conversation this tab was on, and a brand-new tab should reopen the user's
715
+ // most-recent one. The pinned id lives in sessionStorage (per-tab — unlike
716
+ // localStorage it isn't shared across tabs, so two tabs can sit on different
717
+ // conversations). We resolve the id FIRST, then run the built-in load so it
718
+ // rebuilds the right transcript. Skipped when the consumer owns targeting (a
719
+ // custom `load` or an explicit `conversation-id`) or in inline mode (embedded
720
+ // as a page — the host route, not the tab, decides the conversation).
721
+ const resolveConversationId = () => {
722
+ if (this.variant !== 'floating' || !usesBuiltInLoad || consumerSetConversationId) {
723
+ return Promise.resolve();
724
+ }
725
+ const stored = Number(sessionStorage.getItem('kr-chatbot-conversation-id'));
726
+ if (stored > 0) {
727
+ this.conversationId = stored;
728
+ // Fetch the conversation so the header shows its title on reload (the
729
+ // transcript load doesn't carry it). Best-effort — the header falls back
730
+ // to the default label if this fails.
731
+ return KRHttp.fetch({ url: `/api/system/conversation/${stored}` })
732
+ .then((r) => r.json())
733
+ .then((res) => {
734
+ this._conversationTitle = res?.data?.title || '';
735
+ })
736
+ .catch(() => { });
737
+ }
738
+ // Fresh tab, nothing pinned yet — resume the user's most recent conversation
739
+ // (404 when they have none, which leaves us blank for a new one).
740
+ return KRHttp.fetch({ url: '/api/system/conversation/last' })
741
+ .then((r) => {
742
+ if (!r.ok) {
743
+ return;
744
+ }
745
+ return r.json().then((res) => {
746
+ const id = res?.data?.conversationID;
747
+ if (typeof id === 'number') {
748
+ this.conversationId = id;
749
+ this._conversationTitle = res?.data?.title || '';
750
+ this._persistConversationId();
751
+ }
752
+ });
753
+ })
754
+ .catch(() => { });
755
+ };
756
+ // `this.load` is always set by here (defaulted above when the consumer didn't
757
+ // supply one), so capture it for the async chain — TS can't narrow the nullable
758
+ // property across the promise boundary.
759
+ const load = this.load;
760
+ resolveConversationId().then(() => load()).then((messages) => {
683
761
  if (messages && messages.length) {
684
762
  this.messages = messages;
685
763
  }
@@ -759,6 +837,13 @@ export class KRChatbot extends LitElement {
759
837
  }
760
838
  }, 200);
761
839
  }
840
+ // Load context-aware action chips every time the panel opens on an empty conversation. Fires for
841
+ // the floating variant on toggle-open and for the inline variant on its first render (which sets
842
+ // `opened` true at connect). Only when empty — a resumed conversation shows its transcript, not chips.
843
+ // The request-id guard inside handles a stale reply if the user closes/reopens quickly.
844
+ if (changed.has('opened') && this.opened && this.messages.length === 0) {
845
+ this._loadSuggestedActions();
846
+ }
762
847
  // Position the fixed skill menu above the composer, following the krubble
763
848
  // select-field pattern (fixed element positioned from the trigger's rect).
764
849
  if (this._skillMenuOpened) {
@@ -914,12 +999,31 @@ export class KRChatbot extends LitElement {
914
999
  }
915
1000
  this._saveState();
916
1001
  }
1002
+ /**
1003
+ * Persist the pinned conversation id per-tab so a reload redisplays this exact
1004
+ * conversation. sessionStorage (not localStorage) is deliberate: it's scoped to
1005
+ * the tab, so two tabs can sit on different conversations without clobbering
1006
+ * each other. Only meaningful for the floating widget's built-in flow — inline
1007
+ * embeds and custom-`load` consumers own their own targeting.
1008
+ */
1009
+ _persistConversationId() {
1010
+ try {
1011
+ if (this.conversationId != null) {
1012
+ sessionStorage.setItem('kr-chatbot-conversation-id', String(this.conversationId));
1013
+ }
1014
+ else {
1015
+ sessionStorage.removeItem('kr-chatbot-conversation-id');
1016
+ }
1017
+ }
1018
+ catch { /* sessionStorage not available */ }
1019
+ }
917
1020
  _saveState() {
918
1021
  try {
919
- // `toolDetail` is a transient peek, not a saved view — persist only the two
920
- // real preference views so a reload never reopens a stale tool.
1022
+ // `toolDetail` and `history` are transient views, not saved ones — persist
1023
+ // only the real preference views so a reload never reopens a stale tool or a
1024
+ // stale conversation list.
921
1025
  let persistedDrawerView = this._drawerView;
922
- if (persistedDrawerView === 'toolDetail') {
1026
+ if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
923
1027
  persistedDrawerView = 'settings';
924
1028
  }
925
1029
  const state = {
@@ -1191,6 +1295,41 @@ export class KRChatbot extends LitElement {
1191
1295
  console.error('kr-chatbot: invocable skill search failed', error);
1192
1296
  });
1193
1297
  }
1298
+ /**
1299
+ * Load the empty-state action chips from `/conversation/suggestedActions`, passing the user's
1300
+ * current page/selection context (captured live, same as a send). The server picks a few skills
1301
+ * the user can run that fit where they are and writes a label for each. Called every time the
1302
+ * panel opens on an empty conversation (per product decision — context can change between opens).
1303
+ *
1304
+ * AI-only, no fallback: any failure, or v2 chat being off (server returns `[]`), just leaves the
1305
+ * empty state with no chips. A request-id guard drops a response superseded by a newer open, so a
1306
+ * slow reply from a previous open can't land after the user has started typing.
1307
+ */
1308
+ _loadSuggestedActions() {
1309
+ this._suggestedActionRequestId++;
1310
+ const requestId = this._suggestedActionRequestId;
1311
+ const context = this._captureContext();
1312
+ KRHttp.fetch({
1313
+ url: '/api/system/conversation/suggestedActions',
1314
+ method: 'POST',
1315
+ body: {
1316
+ pageContext: context.pageContext,
1317
+ selectionContext: context.selectionContext,
1318
+ },
1319
+ })
1320
+ .then((r) => r.json())
1321
+ .then((res) => {
1322
+ // Ignore a stale response from a prior open, and never overwrite chips once a conversation
1323
+ // has started (the user opened, the reply was slow, they typed and sent in the meantime).
1324
+ if (requestId !== this._suggestedActionRequestId || this.messages.length > 0) {
1325
+ return;
1326
+ }
1327
+ this._suggestedActions = (res?.data || []);
1328
+ })
1329
+ .catch((error) => {
1330
+ console.error('kr-chatbot: suggested actions load failed', error);
1331
+ });
1332
+ }
1194
1333
  /**
1195
1334
  * Load the browse dialog's "Kodaris" tab into `_globalSkills` — the whole
1196
1335
  * published Kodaris catalog for discovery, installed or not, runnable by this
@@ -1434,6 +1573,17 @@ export class KRChatbot extends LitElement {
1434
1573
  this._handleSkillSelect(skill);
1435
1574
  }
1436
1575
  }
1576
+ /**
1577
+ * Click an empty-state suggestion chip: prefill `/code ` into the composer, same as picking the
1578
+ * skill from the `/` menu, and focus so the user can add details before sending. No permission
1579
+ * guards here (unlike `_handleSkillSelect`) — a suggested action's code always comes from the
1580
+ * server's role-filtered runnable set, so it's already known to be invokable by this user.
1581
+ */
1582
+ _handleSuggestedActionClick(action) {
1583
+ this._inputValue = `/${action.code} `;
1584
+ this._saveState();
1585
+ this._inputEl?.focus();
1586
+ }
1437
1587
  /** Copy a message's text to the clipboard and briefly flash "Copied". */
1438
1588
  _handleCopyMessage(content, index) {
1439
1589
  navigator.clipboard.writeText(content).then(() => {
@@ -1716,6 +1866,9 @@ export class KRChatbot extends LitElement {
1716
1866
  }
1717
1867
  this.messages = [...this.messages, userMessage];
1718
1868
  this._inputValue = '';
1869
+ // The conversation has started — drop the empty-state chips so they don't linger above the
1870
+ // transcript. A newer suggestedActions reply is also ignored now (its guard checks messages.length).
1871
+ this._suggestedActions = [];
1719
1872
  this._saveState();
1720
1873
  this._error = '';
1721
1874
  this._userHasScrolledUp = false;
@@ -1875,6 +2028,8 @@ export class KRChatbot extends LitElement {
1875
2028
  const id = conversation?.conversationID;
1876
2029
  if (typeof id === 'number') {
1877
2030
  this.conversationId = id;
2031
+ // Pin it to this tab so a reload redisplays this new conversation.
2032
+ this._persistConversationId();
1878
2033
  // Announce the newly-created conversation so consumers can react
1879
2034
  // (e.g. update the route from /chat/new to /chat/{id}). The full
1880
2035
  // conversation object is passed, not just the id, so consumers
@@ -2072,9 +2227,111 @@ export class KRChatbot extends LitElement {
2072
2227
  else {
2073
2228
  this._drawerOpen = true;
2074
2229
  this._drawerView = view;
2230
+ // Opening the history view refetches the user's previous conversations
2231
+ // (most-recently modified first) so a just-created or renamed conversation
2232
+ // shows without a reload. Ownership is enforced server-side (the endpoint
2233
+ // scopes to the current administrator), so we only filter to active ones.
2234
+ if (view === 'history') {
2235
+ this._conversationsLoading = true;
2236
+ KRHttp.fetch({
2237
+ url: '/api/system/conversation/list',
2238
+ method: 'POST',
2239
+ body: {
2240
+ page: 0,
2241
+ size: 50,
2242
+ sortBy: 'lastModified',
2243
+ sortDirection: 'desc',
2244
+ filterFields: [{
2245
+ name: 'active',
2246
+ operation: 'IS',
2247
+ value: 'true',
2248
+ }],
2249
+ },
2250
+ })
2251
+ .then((r) => r.json())
2252
+ .then((res) => {
2253
+ this._conversations = res?.data?.content || [];
2254
+ })
2255
+ .catch(() => {
2256
+ this._conversations = [];
2257
+ })
2258
+ .then(() => {
2259
+ this._conversationsLoading = false;
2260
+ });
2261
+ }
2075
2262
  }
2076
2263
  this._saveState();
2077
2264
  }
2265
+ /**
2266
+ * Switch the floating widget to a previously-created conversation (clicked in the
2267
+ * history list): stop any live turn, reset the transient turn/context state, pin
2268
+ * the new id (persisted per-tab so a reload stays here), then rebuild its
2269
+ * transcript via the built-in `load`. The drawer closes so the user lands back
2270
+ * in the conversation.
2271
+ */
2272
+ _handleSwitchConversation(conversationID) {
2273
+ if (conversationID === this.conversationId) {
2274
+ this._handleCloseDrawer();
2275
+ return;
2276
+ }
2277
+ this._handleStop();
2278
+ this.conversationId = conversationID;
2279
+ // Adopt the picked conversation's title for the header (empty for an untitled
2280
+ // one, so the header falls back to the default label).
2281
+ this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
2282
+ this._persistConversationId();
2283
+ this._resetConversationState();
2284
+ this._drawerOpen = false;
2285
+ this._saveState();
2286
+ if (this.load) {
2287
+ this.load().then((messages) => {
2288
+ this.messages = messages || [];
2289
+ // A turn may still be streaming server-side for the conversation we just
2290
+ // opened (started in another tab/device) — attach to it if so.
2291
+ if (this.conversationId && !this._isStreaming) {
2292
+ this._reconnectStream();
2293
+ }
2294
+ }).catch(() => { });
2295
+ }
2296
+ }
2297
+ /**
2298
+ * Start a brand-new conversation (clicked in the history list): unpin the id (the
2299
+ * next turn sends `createNew` and the resolved id re-pins), clear the transcript
2300
+ * and transient state, and close the drawer back to an empty composer.
2301
+ */
2302
+ _handleNewConversation() {
2303
+ this._handleStop();
2304
+ this.conversationId = null;
2305
+ // New conversation has no title yet — the header shows the default label until
2306
+ // the server generates one at the end of the first turn.
2307
+ this._conversationTitle = '';
2308
+ this._persistConversationId();
2309
+ this.messages = [];
2310
+ this._resetConversationState();
2311
+ this._drawerOpen = false;
2312
+ this._saveState();
2313
+ }
2314
+ /**
2315
+ * Clear the transient, per-conversation state that must not bleed across a
2316
+ * conversation switch/new — pending confirmations, context banners, errors, the
2317
+ * reconnect counter, and staged composer files. The transcript itself is set by
2318
+ * the caller (loaded for a switch, emptied for new).
2319
+ */
2320
+ _resetConversationState() {
2321
+ this._pendingActions = [];
2322
+ this._inlineConfirm = true;
2323
+ this._contextWarning = '';
2324
+ this._contextLimitReached = false;
2325
+ this._contextLimitMessage = '';
2326
+ this._error = '';
2327
+ this._reconnectAttempts = 0;
2328
+ this._pendingConversationFiles.forEach((p) => {
2329
+ if (p.previewUrl) {
2330
+ URL.revokeObjectURL(p.previewUrl);
2331
+ }
2332
+ });
2333
+ this._pendingConversationFiles = [];
2334
+ }
2078
2335
  /** Close the drawer. `_drawerView` is left intact so its content stays visible
2079
2336
  * while the drawer slides shut. */
2080
2337
  _handleCloseDrawer() {
@@ -2358,7 +2615,7 @@ export class KRChatbot extends LitElement {
2358
2615
  // deliberately never show). Adopt it as the header label and pass it
2359
2616
  // up so consumers can update their own chrome, e.g. a breadcrumb.
2360
2617
  if (data.data?.title) {
2361
- this.label = data.data.title;
2618
+ this._conversationTitle = data.data.title;
2362
2619
  this.dispatchEvent(new CustomEvent('conversation-title', {
2363
2620
  detail: { title: data.data.title },
2364
2621
  bubbles: true,
@@ -2944,10 +3201,75 @@ export class KRChatbot extends LitElement {
2944
3201
  * lifetime — plus a verbose per-iteration table. Never customer-facing.
2945
3202
  */
2946
3203
  /**
2947
- * The side drawer — one sliding panel that holds both the Settings and Debug
2948
- * views. The header buttons switch `_drawerView`; the title and body swap
2949
- * inside. The panel keeps one consistent (resizable) width across both views —
2950
- * switching views only changes the content, never the panel size.
3204
+ * The History view's body — the user's previous conversations (most recent
3205
+ * first) plus a "New conversation" action. Clicking a row switches the widget to
3206
+ * that conversation; the current one is marked so the user sees where they are.
3207
+ * Floating-only (opened from the header history button).
3208
+ */
3209
+ _renderHistoryContent() {
3210
+ return html `
3211
+ <div class="history">
3212
+ ${this._conversationsLoading ? html `
3213
+ <div class="history__empty">Loading…</div>
3214
+ ` : this._conversations.length === 0 ? html `
3215
+ <div class="history__empty">No previous conversations</div>
3216
+ ` : html `
3217
+ <div class="history__list">
3218
+ ${this._conversations.map((conversation) => html `
3219
+ <button
3220
+ type="button"
3221
+ class=${classMap({
3222
+ 'history__item': true,
3223
+ 'history__item--active': conversation.conversationID === this.conversationId,
3224
+ })}
3225
+ @click=${() => this._handleSwitchConversation(conversation.conversationID)}
3226
+ >
3227
+ <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
3228
+ ${conversation.lastModified ? html `
3229
+ <span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
3230
+ ` : nothing}
3231
+ </button>
3232
+ `)}
3233
+ </div>
3234
+ `}
3235
+ </div>
3236
+ `;
3237
+ }
3238
+ /**
3239
+ * Format a conversation's lastModified for the history list: today shows the
3240
+ * time, this year shows month/day, older shows month/day/year. Falls back to the
3241
+ * raw value if it isn't a parseable date.
3242
+ */
3243
+ _renderConversationDate(lastModified) {
3244
+ const date = new Date(lastModified);
3245
+ if (isNaN(date.getTime())) {
3246
+ return lastModified;
3247
+ }
3248
+ const now = new Date();
3249
+ if (date.toDateString() === now.toDateString()) {
3250
+ return date.toLocaleTimeString('en-US', {
3251
+ hour: 'numeric',
3252
+ minute: '2-digit',
3253
+ });
3254
+ }
3255
+ if (date.getFullYear() === now.getFullYear()) {
3256
+ return date.toLocaleDateString('en-US', {
3257
+ month: 'short',
3258
+ day: 'numeric',
3259
+ });
3260
+ }
3261
+ return date.toLocaleDateString('en-US', {
3262
+ month: 'short',
3263
+ day: 'numeric',
3264
+ year: 'numeric',
3265
+ });
3266
+ }
3267
+ /**
3268
+ * The side drawer — one sliding panel that holds the Settings, Debug, History,
3269
+ * and tool-detail views. The header buttons switch `_drawerView`; the title and
3270
+ * body swap inside. In inline mode it slides in beside the transcript at a
3271
+ * consistent (resizable) width; in floating mode it fills the panel over the
3272
+ * transcript with a back button (the panel is too narrow to show both).
2951
3273
  */
2952
3274
  _renderDrawer() {
2953
3275
  // The debug view is gated on the server-side flag (inline variant + debug
@@ -2965,41 +3287,101 @@ export class KRChatbot extends LitElement {
2965
3287
  title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
2966
3288
  body = this._renderToolDetailContent();
2967
3289
  }
3290
+ else if (this._drawerView === 'history') {
3291
+ title = 'Conversations';
3292
+ body = this._renderHistoryContent();
3293
+ }
3294
+ // In the floating widget the drawer fills the panel over the transcript (a
3295
+ // full-panel view with a back button) rather than sliding in beside it — the
3296
+ // panel is too narrow to show both. Inline keeps the side-drawer behavior.
3297
+ const fullPanel = this.variant === 'floating';
2968
3298
  return html `
2969
3299
  <div
2970
3300
  class=${classMap({
2971
3301
  'drawer': true,
2972
3302
  'drawer--open': this._drawerOpen,
2973
3303
  'drawer--resizing': this._isDrawerResizing,
3304
+ 'drawer--full': fullPanel,
2974
3305
  })}
2975
- style=${styleMap({ width: `${this._drawerWidth}px` })}
3306
+ style=${styleMap(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
2976
3307
  >
2977
- <div
2978
- class="drawer__resize"
2979
- @mousedown=${this._handleDrawerResizeMouseDown}
2980
- ></div>
3308
+ ${fullPanel ? nothing : html `
3309
+ <div
3310
+ class="drawer__resize"
3311
+ @mousedown=${this._handleDrawerResizeMouseDown}
3312
+ ></div>
3313
+ `}
2981
3314
  <div class="drawer__header">
3315
+ ${fullPanel ? html `
3316
+ <button
3317
+ class="header-action"
3318
+ @click=${this._handleCloseDrawer}
3319
+ title="Back"
3320
+ >
3321
+ <svg
3322
+ xmlns="http://www.w3.org/2000/svg"
3323
+ fill="none"
3324
+ viewBox="0 0 24 24"
3325
+ stroke-width="2"
3326
+ stroke="currentColor"
3327
+ class="header-action-icon"
3328
+ >
3329
+ <path
3330
+ stroke-linecap="round"
3331
+ stroke-linejoin="round"
3332
+ d="M15.75 19.5 8.25 12l7.5-7.5"
3333
+ />
3334
+ </svg>
3335
+ </button>
3336
+ ` : nothing}
2982
3337
  <span class="drawer__title">${title}</span>
2983
- <button
2984
- class="header-action"
2985
- @click=${this._handleCloseDrawer}
2986
- title="Close"
2987
- >
2988
- <svg
2989
- xmlns="http://www.w3.org/2000/svg"
2990
- fill="none"
2991
- viewBox="0 0 24 24"
2992
- stroke-width="2"
2993
- stroke="currentColor"
2994
- class="header-action-icon"
3338
+ ${this._drawerView === 'history' ? html `
3339
+ <kr-button
3340
+ class="history__new"
3341
+ variant="flat"
3342
+ color="primary"
3343
+ size="small"
3344
+ @click=${this._handleNewConversation}
2995
3345
  >
2996
- <path
2997
- stroke-linecap="round"
2998
- stroke-linejoin="round"
2999
- d="M6 18 18 6M6 6l12 12"
3000
- />
3001
- </svg>
3002
- </button>
3346
+ <svg
3347
+ slot="icon"
3348
+ xmlns="http://www.w3.org/2000/svg"
3349
+ fill="none"
3350
+ viewBox="0 0 24 24"
3351
+ stroke-width="1.75"
3352
+ stroke="currentColor"
3353
+ >
3354
+ <path
3355
+ stroke-linecap="round"
3356
+ stroke-linejoin="round"
3357
+ d="M12 4.5v15m7.5-7.5h-15"
3358
+ />
3359
+ </svg>
3360
+ New conversation
3361
+ </kr-button>
3362
+ ` : nothing}
3363
+ ${fullPanel ? nothing : html `
3364
+ <button
3365
+ class="header-action"
3366
+ @click=${this._handleCloseDrawer}
3367
+ title="Close"
3368
+ >
3369
+ <svg
3370
+ xmlns="http://www.w3.org/2000/svg"
3371
+ fill="none"
3372
+ viewBox="0 0 24 24"
3373
+ stroke-width="2"
3374
+ stroke="currentColor"
3375
+ class="header-action-icon"
3376
+ >
3377
+ <path
3378
+ stroke-linecap="round"
3379
+ stroke-linejoin="round"
3380
+ d="M6 18 18 6M6 6l12 12"
3381
+ />
3382
+ </svg>
3383
+ </button>
3384
+ `}
3003
3385
  </div>
3004
3386
  <div class="drawer__body">
3005
3387
  ${body}
@@ -3260,9 +3642,10 @@ export class KRChatbot extends LitElement {
3260
3642
  const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
3261
3643
  // Push the content over by the open drawer's width so the drawer sits beside
3262
3644
  // the content rather than overlaying it. One consistent (resizable) width for
3263
- // both views. Only pushes while the drawer is open.
3645
+ // both views. Only pushes while the drawer is open, and only inline — the
3646
+ // floating drawer fills the panel over the transcript, so it must not push.
3264
3647
  const contentPadding = {};
3265
- if (this._drawerOpen) {
3648
+ if (this._drawerOpen && this.variant === 'inline') {
3266
3649
  contentPadding.paddingRight = `${this._drawerWidth}px`;
3267
3650
  }
3268
3651
  // Group the transcript into runs so an agent turn (its reasoning/tool/text/
@@ -3380,10 +3763,52 @@ export class KRChatbot extends LitElement {
3380
3763
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
3381
3764
  <div class="header-left">
3382
3765
  <div class="header-info">
3383
- <span class="header-title">${this.label}</span>
3766
+ <span class="header-title">${this._conversationTitle || this.label}</span>
3384
3767
  </div>
3385
3768
  </div>
3386
3769
  <div class="header-actions">
3770
+ ${this.variant === 'floating' ? html `
3771
+ <button
3772
+ class="header-action"
3773
+ @click=${this._handleNewConversation}
3774
+ title="New conversation"
3775
+ >
3776
+ <svg
3777
+ xmlns="http://www.w3.org/2000/svg"
3778
+ fill="none"
3779
+ viewBox="0 0 24 24"
3780
+ stroke-width="1.75"
3781
+ stroke="currentColor"
3782
+ class="header-action-icon"
3783
+ >
3784
+ <path
3785
+ stroke-linecap="round"
3786
+ stroke-linejoin="round"
3787
+ d="M12 4.5v15m7.5-7.5h-15"
3788
+ />
3789
+ </svg>
3790
+ </button>
3791
+ <button
3792
+ class="header-action"
3793
+ @click=${() => this._toggleDrawer('history')}
3794
+ title="Conversations"
3795
+ >
3796
+ <svg
3797
+ xmlns="http://www.w3.org/2000/svg"
3798
+ fill="none"
3799
+ viewBox="0 0 24 24"
3800
+ stroke-width="1.5"
3801
+ stroke="currentColor"
3802
+ class="header-action-icon"
3803
+ >
3804
+ <path
3805
+ stroke-linecap="round"
3806
+ stroke-linejoin="round"
3807
+ d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
3808
+ />
3809
+ </svg>
3810
+ </button>
3811
+ ` : nothing}
3387
3812
  <button
3388
3813
  class="header-action"
3389
3814
  @click=${() => this._toggleDrawer('settings')}
@@ -3461,6 +3886,17 @@ export class KRChatbot extends LitElement {
3461
3886
  ${this.messages.length === 0 ? html `
3462
3887
  <div class="empty">
3463
3888
  <span class="empty-text">What can I help you with?</span>
3889
+ ${this._suggestedActions.length ? html `
3890
+ <div class="empty-suggestions">
3891
+ ${this._suggestedActions.map((action) => html `
3892
+ <button
3893
+ type="button"
3894
+ class="empty-suggestions__chip"
3895
+ @click=${() => this._handleSuggestedActionClick(action)}
3896
+ >${action.label}</button>
3897
+ `)}
3898
+ </div>
3899
+ ` : nothing}
3464
3900
  </div>
3465
3901
  ` : nothing}
3466
3902
  ${messageGroups.map((group) => {
@@ -4127,22 +4563,28 @@ KRChatbot.styles = css `
4127
4563
  display: flex;
4128
4564
  align-items: center;
4129
4565
  gap: 10px;
4566
+ min-width: 0;
4130
4567
  }
4131
4568
 
4132
4569
  .header-info {
4133
4570
  display: flex;
4134
4571
  flex-direction: column;
4572
+ min-width: 0;
4135
4573
  }
4136
4574
 
4137
4575
  .header-title {
4138
4576
  font-size: 16px;
4139
4577
  font-weight: 600;
4578
+ white-space: nowrap;
4579
+ overflow: hidden;
4580
+ text-overflow: ellipsis;
4140
4581
  }
4141
4582
 
4142
4583
  .header-actions {
4143
4584
  display: flex;
4144
4585
  align-items: center;
4145
4586
  gap: 4px;
4587
+ flex-shrink: 0;
4146
4588
  }
4147
4589
 
4148
4590
  .header-action {
@@ -4316,7 +4758,7 @@ KRChatbot.styles = css `
4316
4758
  align-items: center;
4317
4759
  justify-content: center;
4318
4760
  color: var(--kr-chatbot-text-secondary);
4319
- gap: 8px;
4761
+ gap: 16px;
4320
4762
  }
4321
4763
 
4322
4764
  .empty-text {
@@ -4324,6 +4766,35 @@ KRChatbot.styles = css `
4324
4766
  color: rgb(0 0 0 / 80%);
4325
4767
  }
4326
4768
 
4769
+ /* Context-aware action chips under the empty-state prompt. A row of pills the user can click to
4770
+ prefill a skill — capped small server-side so this reads as a few suggestions, not a menu. */
4771
+ .empty-suggestions {
4772
+ display: flex;
4773
+ flex-wrap: wrap;
4774
+ justify-content: center;
4775
+ gap: 10px;
4776
+ max-width: 420px;
4777
+ margin-top: 4px;
4778
+ }
4779
+
4780
+ .empty-suggestions__chip {
4781
+ padding: 8px 12px;
4782
+ background: rgb(0 0 0 / 6%);
4783
+ border: none;
4784
+ border-radius: 16px;
4785
+ color: #000000;
4786
+ font: inherit;
4787
+ font-size: 13px;
4788
+ line-height: 1.2;
4789
+ cursor: pointer;
4790
+ white-space: nowrap;
4791
+ }
4792
+
4793
+ .empty-suggestions__chip:hover {
4794
+ background: rgb(0 0 0 / 10%);
4795
+ color: #000000;
4796
+ }
4797
+
4327
4798
  .message {
4328
4799
  position: relative;
4329
4800
  display: flex;
@@ -4968,6 +5439,13 @@ KRChatbot.styles = css `
4968
5439
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
4969
5440
  }
4970
5441
 
5442
+ /* Full-panel (floating) header: back button then title, left-aligned. */
5443
+ .drawer--full .drawer__header {
5444
+ justify-content: flex-start;
5445
+ gap: 8px;
5446
+ padding: 0 12px;
5447
+ }
5448
+
4971
5449
  .drawer__title {
4972
5450
  font-size: 14px;
4973
5451
  font-weight: 600;
@@ -4981,6 +5459,78 @@ KRChatbot.styles = css `
4981
5459
  padding: 4px 0;
4982
5460
  }
4983
5461
 
5462
+ /* Floating variant: the drawer fills the whole panel over the transcript (a
5463
+ full-panel view with a back button), rather than sliding in beside it. */
5464
+ .drawer--full {
5465
+ left: 0;
5466
+ width: 100%;
5467
+ max-width: 100%;
5468
+ z-index: 6;
5469
+ }
5470
+
5471
+ .drawer--full.drawer--open {
5472
+ box-shadow: none;
5473
+ }
5474
+
5475
+ /* History (previous conversations) view. */
5476
+ .history {
5477
+ display: flex;
5478
+ flex-direction: column;
5479
+ }
5480
+
5481
+ /* "New conversation" action in the Conversations header — pushed to the right. */
5482
+ .history__new {
5483
+ margin-left: auto;
5484
+ }
5485
+
5486
+ .history__empty {
5487
+ padding: 24px 20px;
5488
+ text-align: center;
5489
+ font-size: 13px;
5490
+ color: rgb(0 0 0 / 50%);
5491
+ }
5492
+
5493
+ .history__list {
5494
+ display: flex;
5495
+ flex-direction: column;
5496
+ padding: 4px 0;
5497
+ }
5498
+
5499
+ .history__item {
5500
+ display: flex;
5501
+ flex-direction: column;
5502
+ align-items: flex-start;
5503
+ gap: 5px;
5504
+ padding: 10px 20px;
5505
+ background: transparent;
5506
+ border: none;
5507
+ text-align: left;
5508
+ cursor: pointer;
5509
+ transition: background 0.15s;
5510
+ }
5511
+
5512
+ .history__item:hover {
5513
+ background: rgba(0, 0, 0, 0.04);
5514
+ }
5515
+
5516
+ .history__item--active {
5517
+ background: rgba(0, 0, 0, 0.06);
5518
+ }
5519
+
5520
+ .history__item-title {
5521
+ font-size: 14px;
5522
+ color: #000000;
5523
+ white-space: nowrap;
5524
+ overflow: hidden;
5525
+ text-overflow: ellipsis;
5526
+ max-width: 100%;
5527
+ }
5528
+
5529
+ .history__item-date {
5530
+ font-size: 12px;
5531
+ color: rgb(0 0 0 / 60%);
5532
+ }
5533
+
4984
5534
  .debug-section {
4985
5535
  padding: 14px 20px;
4986
5536
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
@@ -5915,6 +6465,9 @@ __decorate([
5915
6465
  __decorate([
5916
6466
  state()
5917
6467
  ], KRChatbot.prototype, "_error", void 0);
6468
+ __decorate([
6469
+ state()
6470
+ ], KRChatbot.prototype, "_conversationTitle", void 0);
5918
6471
  __decorate([
5919
6472
  state()
5920
6473
  ], KRChatbot.prototype, "_contextWarning", void 0);
@@ -5948,6 +6501,12 @@ __decorate([
5948
6501
  __decorate([
5949
6502
  state()
5950
6503
  ], KRChatbot.prototype, "_drawerView", void 0);
6504
+ __decorate([
6505
+ state()
6506
+ ], KRChatbot.prototype, "_conversations", void 0);
6507
+ __decorate([
6508
+ state()
6509
+ ], KRChatbot.prototype, "_conversationsLoading", void 0);
5951
6510
  __decorate([
5952
6511
  state()
5953
6512
  ], KRChatbot.prototype, "_toolDetail", void 0);
@@ -5987,6 +6546,9 @@ __decorate([
5987
6546
  __decorate([
5988
6547
  state()
5989
6548
  ], KRChatbot.prototype, "_invocableSkills", void 0);
6549
+ __decorate([
6550
+ state()
6551
+ ], KRChatbot.prototype, "_suggestedActions", void 0);
5990
6552
  __decorate([
5991
6553
  state()
5992
6554
  ], KRChatbot.prototype, "_aiFrameworkConfig", void 0);