@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.
@@ -2926,6 +2926,7 @@
2926
2926
  this._orgSkillRequestId = 0;
2927
2927
  this._globalSkillRequestId = 0;
2928
2928
  this._invocableSkillRequestId = 0;
2929
+ this._suggestedActionRequestId = 0;
2929
2930
  /**
2930
2931
  * Codes of global skills with an in-flight enable/disable request. The `+`
2931
2932
  * button reads this to show a pending state and ignore repeat clicks. Mutated
@@ -3019,6 +3020,14 @@
3019
3020
  this._inputValue = '';
3020
3021
  this._isStreaming = false;
3021
3022
  this._error = '';
3023
+ /**
3024
+ * The current conversation's title. In the floating widget it replaces the
3025
+ * static header label so the user sees which conversation they're in. Set from
3026
+ * the loaded/switched conversation and from the live `CONVERSATION_TITLE` event
3027
+ * (the server generates a title at the end of a new conversation's first turn);
3028
+ * empty for a brand-new conversation, where the header falls back to `label`.
3029
+ */
3030
+ this._conversationTitle = '';
3022
3031
  /**
3023
3032
  * User-facing context warning text (`CONVERSATION_TOKEN_WARNING`). Shown as a
3024
3033
  * dismissible banner in all variants. Carries only friendly guidance, no numbers.
@@ -3078,12 +3087,23 @@
3078
3087
  */
3079
3088
  this._drawerOpen = false;
3080
3089
  /**
3081
- * Which view the drawer shows — settings, debug, or a single tool call's full
3082
- * detail. One drawer holds all three; the header buttons switch settings/debug
3083
- * and clicking a tool line switches to `toolDetail`. The content/width swap
3084
- * inside. Never null, so the content stays visible while the drawer slides shut.
3090
+ * Which view the drawer shows — settings, debug, history, or a single tool
3091
+ * call's full detail. One drawer holds them all; header buttons switch
3092
+ * settings/debug/history and clicking a tool line switches to `toolDetail`. The
3093
+ * content/width swap inside. Never null, so the content stays visible while the
3094
+ * drawer slides shut. `history` is floating-only (the previous-conversations
3095
+ * picker); in floating the drawer fills the panel over the transcript.
3085
3096
  */
3086
3097
  this._drawerView = 'settings';
3098
+ /**
3099
+ * The user's previous conversations, loaded from `/conversation/list` when the
3100
+ * history view opens. Each is the raw ConversationViewDto (we read conversationID,
3101
+ * title, lastModified). Empty until the first open; reloaded on each open so a
3102
+ * freshly-created or renamed conversation shows.
3103
+ */
3104
+ this._conversations = [];
3105
+ /** True while the history list is being fetched (drives a loading state). */
3106
+ this._conversationsLoading = false;
3087
3107
  /**
3088
3108
  * The tool call whose full input/output the `toolDetail` drawer view shows —
3089
3109
  * the clicked `tool_use` message and its paired `tool_result` message, stored
@@ -3129,6 +3149,12 @@
3129
3149
  * so installed globals in the menu never leak into the "Your Org" tab.
3130
3150
  */
3131
3151
  this._invocableSkills = [];
3152
+ /**
3153
+ * Context-aware action chips for the empty state, from `/conversation/suggestedActions`.
3154
+ * Loaded fresh every time the panel opens on an empty conversation (see `_loadSuggestedActions`);
3155
+ * cleared when the first message is sent so they don't linger over a live conversation.
3156
+ */
3157
+ this._suggestedActions = [];
3132
3158
  /**
3133
3159
  * AI framework UI config flags fetched from `aiFrameworkConfig` on connect.
3134
3160
  * `debugTelemetryEnabled` (same server-side gate as TOKEN_USAGE emission) turns
@@ -3321,6 +3347,11 @@
3321
3347
  // --- Lifecycle ---
3322
3348
  connectedCallback() {
3323
3349
  super.connectedCallback();
3350
+ // Capture the consumer's targeting BEFORE we default `load` below: a custom
3351
+ // `load` or an explicitly-set `conversation-id` means the consumer owns which
3352
+ // conversation shows, so the tab-scoped resume must not override it.
3353
+ const usesBuiltInLoad = !this.load;
3354
+ const consumerSetConversationId = this.conversationId != null;
3324
3355
  // Default to loading a conversation's messages when the consumer hasn't
3325
3356
  // provided their own `load`. With `conversationId` set we rebuild that
3326
3357
  // conversation; with none set the user wants a NEW conversation, so we start
@@ -3403,7 +3434,54 @@
3403
3434
  })));
3404
3435
  };
3405
3436
  }
3406
- this.load().then((messages) => {
3437
+ // Tab-scoped resume (floating only): a reload should redisplay the exact
3438
+ // conversation this tab was on, and a brand-new tab should reopen the user's
3439
+ // most-recent one. The pinned id lives in sessionStorage (per-tab — unlike
3440
+ // localStorage it isn't shared across tabs, so two tabs can sit on different
3441
+ // conversations). We resolve the id FIRST, then run the built-in load so it
3442
+ // rebuilds the right transcript. Skipped when the consumer owns targeting (a
3443
+ // custom `load` or an explicit `conversation-id`) or in inline mode (embedded
3444
+ // as a page — the host route, not the tab, decides the conversation).
3445
+ const resolveConversationId = () => {
3446
+ if (this.variant !== 'floating' || !usesBuiltInLoad || consumerSetConversationId) {
3447
+ return Promise.resolve();
3448
+ }
3449
+ const stored = Number(sessionStorage.getItem('kr-chatbot-conversation-id'));
3450
+ if (stored > 0) {
3451
+ this.conversationId = stored;
3452
+ // Fetch the conversation so the header shows its title on reload (the
3453
+ // transcript load doesn't carry it). Best-effort — the header falls back
3454
+ // to the default label if this fails.
3455
+ return krubbleHttp.Http.fetch({ url: `/api/system/conversation/${stored}` })
3456
+ .then((r) => r.json())
3457
+ .then((res) => {
3458
+ this._conversationTitle = res?.data?.title || '';
3459
+ })
3460
+ .catch(() => { });
3461
+ }
3462
+ // Fresh tab, nothing pinned yet — resume the user's most recent conversation
3463
+ // (404 when they have none, which leaves us blank for a new one).
3464
+ return krubbleHttp.Http.fetch({ url: '/api/system/conversation/last' })
3465
+ .then((r) => {
3466
+ if (!r.ok) {
3467
+ return;
3468
+ }
3469
+ return r.json().then((res) => {
3470
+ const id = res?.data?.conversationID;
3471
+ if (typeof id === 'number') {
3472
+ this.conversationId = id;
3473
+ this._conversationTitle = res?.data?.title || '';
3474
+ this._persistConversationId();
3475
+ }
3476
+ });
3477
+ })
3478
+ .catch(() => { });
3479
+ };
3480
+ // `this.load` is always set by here (defaulted above when the consumer didn't
3481
+ // supply one), so capture it for the async chain — TS can't narrow the nullable
3482
+ // property across the promise boundary.
3483
+ const load = this.load;
3484
+ resolveConversationId().then(() => load()).then((messages) => {
3407
3485
  if (messages && messages.length) {
3408
3486
  this.messages = messages;
3409
3487
  }
@@ -3483,6 +3561,13 @@
3483
3561
  }
3484
3562
  }, 200);
3485
3563
  }
3564
+ // Load context-aware action chips every time the panel opens on an empty conversation. Fires for
3565
+ // the floating variant on toggle-open and for the inline variant on its first render (which sets
3566
+ // `opened` true at connect). Only when empty — a resumed conversation shows its transcript, not chips.
3567
+ // The request-id guard inside handles a stale reply if the user closes/reopens quickly.
3568
+ if (changed.has('opened') && this.opened && this.messages.length === 0) {
3569
+ this._loadSuggestedActions();
3570
+ }
3486
3571
  // Position the fixed skill menu above the composer, following the krubble
3487
3572
  // select-field pattern (fixed element positioned from the trigger's rect).
3488
3573
  if (this._skillMenuOpened) {
@@ -3638,12 +3723,31 @@
3638
3723
  }
3639
3724
  this._saveState();
3640
3725
  }
3726
+ /**
3727
+ * Persist the pinned conversation id per-tab so a reload redisplays this exact
3728
+ * conversation. sessionStorage (not localStorage) is deliberate: it's scoped to
3729
+ * the tab, so two tabs can sit on different conversations without clobbering
3730
+ * each other. Only meaningful for the floating widget's built-in flow — inline
3731
+ * embeds and custom-`load` consumers own their own targeting.
3732
+ */
3733
+ _persistConversationId() {
3734
+ try {
3735
+ if (this.conversationId != null) {
3736
+ sessionStorage.setItem('kr-chatbot-conversation-id', String(this.conversationId));
3737
+ }
3738
+ else {
3739
+ sessionStorage.removeItem('kr-chatbot-conversation-id');
3740
+ }
3741
+ }
3742
+ catch { /* sessionStorage not available */ }
3743
+ }
3641
3744
  _saveState() {
3642
3745
  try {
3643
- // `toolDetail` is a transient peek, not a saved view — persist only the two
3644
- // real preference views so a reload never reopens a stale tool.
3746
+ // `toolDetail` and `history` are transient views, not saved ones — persist
3747
+ // only the real preference views so a reload never reopens a stale tool or a
3748
+ // stale conversation list.
3645
3749
  let persistedDrawerView = this._drawerView;
3646
- if (persistedDrawerView === 'toolDetail') {
3750
+ if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
3647
3751
  persistedDrawerView = 'settings';
3648
3752
  }
3649
3753
  const state = {
@@ -3915,6 +4019,41 @@
3915
4019
  console.error('kr-chatbot: invocable skill search failed', error);
3916
4020
  });
3917
4021
  }
4022
+ /**
4023
+ * Load the empty-state action chips from `/conversation/suggestedActions`, passing the user's
4024
+ * current page/selection context (captured live, same as a send). The server picks a few skills
4025
+ * the user can run that fit where they are and writes a label for each. Called every time the
4026
+ * panel opens on an empty conversation (per product decision — context can change between opens).
4027
+ *
4028
+ * AI-only, no fallback: any failure, or v2 chat being off (server returns `[]`), just leaves the
4029
+ * empty state with no chips. A request-id guard drops a response superseded by a newer open, so a
4030
+ * slow reply from a previous open can't land after the user has started typing.
4031
+ */
4032
+ _loadSuggestedActions() {
4033
+ this._suggestedActionRequestId++;
4034
+ const requestId = this._suggestedActionRequestId;
4035
+ const context = this._captureContext();
4036
+ krubbleHttp.Http.fetch({
4037
+ url: '/api/system/conversation/suggestedActions',
4038
+ method: 'POST',
4039
+ body: {
4040
+ pageContext: context.pageContext,
4041
+ selectionContext: context.selectionContext,
4042
+ },
4043
+ })
4044
+ .then((r) => r.json())
4045
+ .then((res) => {
4046
+ // Ignore a stale response from a prior open, and never overwrite chips once a conversation
4047
+ // has started (the user opened, the reply was slow, they typed and sent in the meantime).
4048
+ if (requestId !== this._suggestedActionRequestId || this.messages.length > 0) {
4049
+ return;
4050
+ }
4051
+ this._suggestedActions = (res?.data || []);
4052
+ })
4053
+ .catch((error) => {
4054
+ console.error('kr-chatbot: suggested actions load failed', error);
4055
+ });
4056
+ }
3918
4057
  /**
3919
4058
  * Load the browse dialog's "Kodaris" tab into `_globalSkills` — the whole
3920
4059
  * published Kodaris catalog for discovery, installed or not, runnable by this
@@ -4158,6 +4297,17 @@
4158
4297
  this._handleSkillSelect(skill);
4159
4298
  }
4160
4299
  }
4300
+ /**
4301
+ * Click an empty-state suggestion chip: prefill `/code ` into the composer, same as picking the
4302
+ * skill from the `/` menu, and focus so the user can add details before sending. No permission
4303
+ * guards here (unlike `_handleSkillSelect`) — a suggested action's code always comes from the
4304
+ * server's role-filtered runnable set, so it's already known to be invokable by this user.
4305
+ */
4306
+ _handleSuggestedActionClick(action) {
4307
+ this._inputValue = `/${action.code} `;
4308
+ this._saveState();
4309
+ this._inputEl?.focus();
4310
+ }
4161
4311
  /** Copy a message's text to the clipboard and briefly flash "Copied". */
4162
4312
  _handleCopyMessage(content, index) {
4163
4313
  navigator.clipboard.writeText(content).then(() => {
@@ -4440,6 +4590,9 @@
4440
4590
  }
4441
4591
  this.messages = [...this.messages, userMessage];
4442
4592
  this._inputValue = '';
4593
+ // The conversation has started — drop the empty-state chips so they don't linger above the
4594
+ // transcript. A newer suggestedActions reply is also ignored now (its guard checks messages.length).
4595
+ this._suggestedActions = [];
4443
4596
  this._saveState();
4444
4597
  this._error = '';
4445
4598
  this._userHasScrolledUp = false;
@@ -4599,6 +4752,8 @@
4599
4752
  const id = conversation?.conversationID;
4600
4753
  if (typeof id === 'number') {
4601
4754
  this.conversationId = id;
4755
+ // Pin it to this tab so a reload redisplays this new conversation.
4756
+ this._persistConversationId();
4602
4757
  // Announce the newly-created conversation so consumers can react
4603
4758
  // (e.g. update the route from /chat/new to /chat/{id}). The full
4604
4759
  // conversation object is passed, not just the id, so consumers
@@ -4796,9 +4951,111 @@
4796
4951
  else {
4797
4952
  this._drawerOpen = true;
4798
4953
  this._drawerView = view;
4954
+ // Opening the history view refetches the user's previous conversations
4955
+ // (most-recently modified first) so a just-created or renamed conversation
4956
+ // shows without a reload. Ownership is enforced server-side (the endpoint
4957
+ // scopes to the current administrator), so we only filter to active ones.
4958
+ if (view === 'history') {
4959
+ this._conversationsLoading = true;
4960
+ krubbleHttp.Http.fetch({
4961
+ url: '/api/system/conversation/list',
4962
+ method: 'POST',
4963
+ body: {
4964
+ page: 0,
4965
+ size: 50,
4966
+ sortBy: 'lastModified',
4967
+ sortDirection: 'desc',
4968
+ filterFields: [{
4969
+ name: 'active',
4970
+ operation: 'IS',
4971
+ value: 'true',
4972
+ }],
4973
+ },
4974
+ })
4975
+ .then((r) => r.json())
4976
+ .then((res) => {
4977
+ this._conversations = res?.data?.content || [];
4978
+ })
4979
+ .catch(() => {
4980
+ this._conversations = [];
4981
+ })
4982
+ .then(() => {
4983
+ this._conversationsLoading = false;
4984
+ });
4985
+ }
4799
4986
  }
4800
4987
  this._saveState();
4801
4988
  }
4989
+ /**
4990
+ * Switch the floating widget to a previously-created conversation (clicked in the
4991
+ * history list): stop any live turn, reset the transient turn/context state, pin
4992
+ * the new id (persisted per-tab so a reload stays here), then rebuild its
4993
+ * transcript via the built-in `load`. The drawer closes so the user lands back
4994
+ * in the conversation.
4995
+ */
4996
+ _handleSwitchConversation(conversationID) {
4997
+ if (conversationID === this.conversationId) {
4998
+ this._handleCloseDrawer();
4999
+ return;
5000
+ }
5001
+ this._handleStop();
5002
+ this.conversationId = conversationID;
5003
+ // Adopt the picked conversation's title for the header (empty for an untitled
5004
+ // one, so the header falls back to the default label).
5005
+ this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
5006
+ this._persistConversationId();
5007
+ this._resetConversationState();
5008
+ this._drawerOpen = false;
5009
+ this._saveState();
5010
+ if (this.load) {
5011
+ this.load().then((messages) => {
5012
+ this.messages = messages || [];
5013
+ // A turn may still be streaming server-side for the conversation we just
5014
+ // opened (started in another tab/device) — attach to it if so.
5015
+ if (this.conversationId && !this._isStreaming) {
5016
+ this._reconnectStream();
5017
+ }
5018
+ }).catch(() => { });
5019
+ }
5020
+ }
5021
+ /**
5022
+ * Start a brand-new conversation (clicked in the history list): unpin the id (the
5023
+ * next turn sends `createNew` and the resolved id re-pins), clear the transcript
5024
+ * and transient state, and close the drawer back to an empty composer.
5025
+ */
5026
+ _handleNewConversation() {
5027
+ this._handleStop();
5028
+ this.conversationId = null;
5029
+ // New conversation has no title yet — the header shows the default label until
5030
+ // the server generates one at the end of the first turn.
5031
+ this._conversationTitle = '';
5032
+ this._persistConversationId();
5033
+ this.messages = [];
5034
+ this._resetConversationState();
5035
+ this._drawerOpen = false;
5036
+ this._saveState();
5037
+ }
5038
+ /**
5039
+ * Clear the transient, per-conversation state that must not bleed across a
5040
+ * conversation switch/new — pending confirmations, context banners, errors, the
5041
+ * reconnect counter, and staged composer files. The transcript itself is set by
5042
+ * the caller (loaded for a switch, emptied for new).
5043
+ */
5044
+ _resetConversationState() {
5045
+ this._pendingActions = [];
5046
+ this._inlineConfirm = true;
5047
+ this._contextWarning = '';
5048
+ this._contextLimitReached = false;
5049
+ this._contextLimitMessage = '';
5050
+ this._error = '';
5051
+ this._reconnectAttempts = 0;
5052
+ this._pendingConversationFiles.forEach((p) => {
5053
+ if (p.previewUrl) {
5054
+ URL.revokeObjectURL(p.previewUrl);
5055
+ }
5056
+ });
5057
+ this._pendingConversationFiles = [];
5058
+ }
4802
5059
  /** Close the drawer. `_drawerView` is left intact so its content stays visible
4803
5060
  * while the drawer slides shut. */
4804
5061
  _handleCloseDrawer() {
@@ -5080,7 +5337,7 @@
5080
5337
  // deliberately never show). Adopt it as the header label and pass it
5081
5338
  // up so consumers can update their own chrome, e.g. a breadcrumb.
5082
5339
  if (data.data?.title) {
5083
- this.label = data.data.title;
5340
+ this._conversationTitle = data.data.title;
5084
5341
  this.dispatchEvent(new CustomEvent('conversation-title', {
5085
5342
  detail: { title: data.data.title },
5086
5343
  bubbles: true,
@@ -5666,10 +5923,75 @@
5666
5923
  * lifetime — plus a verbose per-iteration table. Never customer-facing.
5667
5924
  */
5668
5925
  /**
5669
- * The side drawer — one sliding panel that holds both the Settings and Debug
5670
- * views. The header buttons switch `_drawerView`; the title and body swap
5671
- * inside. The panel keeps one consistent (resizable) width across both views —
5672
- * switching views only changes the content, never the panel size.
5926
+ * The History view's body — the user's previous conversations (most recent
5927
+ * first) plus a "New conversation" action. Clicking a row switches the widget to
5928
+ * that conversation; the current one is marked so the user sees where they are.
5929
+ * Floating-only (opened from the header history button).
5930
+ */
5931
+ _renderHistoryContent() {
5932
+ return b `
5933
+ <div class="history">
5934
+ ${this._conversationsLoading ? b `
5935
+ <div class="history__empty">Loading…</div>
5936
+ ` : this._conversations.length === 0 ? b `
5937
+ <div class="history__empty">No previous conversations</div>
5938
+ ` : b `
5939
+ <div class="history__list">
5940
+ ${this._conversations.map((conversation) => b `
5941
+ <button
5942
+ type="button"
5943
+ class=${e({
5944
+ 'history__item': true,
5945
+ 'history__item--active': conversation.conversationID === this.conversationId,
5946
+ })}
5947
+ @click=${() => this._handleSwitchConversation(conversation.conversationID)}
5948
+ >
5949
+ <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
5950
+ ${conversation.lastModified ? b `
5951
+ <span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
5952
+ ` : A}
5953
+ </button>
5954
+ `)}
5955
+ </div>
5956
+ `}
5957
+ </div>
5958
+ `;
5959
+ }
5960
+ /**
5961
+ * Format a conversation's lastModified for the history list: today shows the
5962
+ * time, this year shows month/day, older shows month/day/year. Falls back to the
5963
+ * raw value if it isn't a parseable date.
5964
+ */
5965
+ _renderConversationDate(lastModified) {
5966
+ const date = new Date(lastModified);
5967
+ if (isNaN(date.getTime())) {
5968
+ return lastModified;
5969
+ }
5970
+ const now = new Date();
5971
+ if (date.toDateString() === now.toDateString()) {
5972
+ return date.toLocaleTimeString('en-US', {
5973
+ hour: 'numeric',
5974
+ minute: '2-digit',
5975
+ });
5976
+ }
5977
+ if (date.getFullYear() === now.getFullYear()) {
5978
+ return date.toLocaleDateString('en-US', {
5979
+ month: 'short',
5980
+ day: 'numeric',
5981
+ });
5982
+ }
5983
+ return date.toLocaleDateString('en-US', {
5984
+ month: 'short',
5985
+ day: 'numeric',
5986
+ year: 'numeric',
5987
+ });
5988
+ }
5989
+ /**
5990
+ * The side drawer — one sliding panel that holds the Settings, Debug, History,
5991
+ * and tool-detail views. The header buttons switch `_drawerView`; the title and
5992
+ * body swap inside. In inline mode it slides in beside the transcript at a
5993
+ * consistent (resizable) width; in floating mode it fills the panel over the
5994
+ * transcript with a back button (the panel is too narrow to show both).
5673
5995
  */
5674
5996
  _renderDrawer() {
5675
5997
  // The debug view is gated on the server-side flag (inline variant + debug
@@ -5687,41 +6009,101 @@
5687
6009
  title = this._toolDetail.use.data?.displayName || this._toolDetail.use.data?.toolName || 'Tool';
5688
6010
  body = this._renderToolDetailContent();
5689
6011
  }
6012
+ else if (this._drawerView === 'history') {
6013
+ title = 'Conversations';
6014
+ body = this._renderHistoryContent();
6015
+ }
6016
+ // In the floating widget the drawer fills the panel over the transcript (a
6017
+ // full-panel view with a back button) rather than sliding in beside it — the
6018
+ // panel is too narrow to show both. Inline keeps the side-drawer behavior.
6019
+ const fullPanel = this.variant === 'floating';
5690
6020
  return b `
5691
6021
  <div
5692
6022
  class=${e({
5693
6023
  'drawer': true,
5694
6024
  'drawer--open': this._drawerOpen,
5695
6025
  'drawer--resizing': this._isDrawerResizing,
6026
+ 'drawer--full': fullPanel,
5696
6027
  })}
5697
- style=${o({ width: `${this._drawerWidth}px` })}
6028
+ style=${o(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
5698
6029
  >
5699
- <div
5700
- class="drawer__resize"
5701
- @mousedown=${this._handleDrawerResizeMouseDown}
5702
- ></div>
6030
+ ${fullPanel ? A : b `
6031
+ <div
6032
+ class="drawer__resize"
6033
+ @mousedown=${this._handleDrawerResizeMouseDown}
6034
+ ></div>
6035
+ `}
5703
6036
  <div class="drawer__header">
6037
+ ${fullPanel ? b `
6038
+ <button
6039
+ class="header-action"
6040
+ @click=${this._handleCloseDrawer}
6041
+ title="Back"
6042
+ >
6043
+ <svg
6044
+ xmlns="http://www.w3.org/2000/svg"
6045
+ fill="none"
6046
+ viewBox="0 0 24 24"
6047
+ stroke-width="2"
6048
+ stroke="currentColor"
6049
+ class="header-action-icon"
6050
+ >
6051
+ <path
6052
+ stroke-linecap="round"
6053
+ stroke-linejoin="round"
6054
+ d="M15.75 19.5 8.25 12l7.5-7.5"
6055
+ />
6056
+ </svg>
6057
+ </button>
6058
+ ` : A}
5704
6059
  <span class="drawer__title">${title}</span>
5705
- <button
5706
- class="header-action"
5707
- @click=${this._handleCloseDrawer}
5708
- title="Close"
5709
- >
5710
- <svg
5711
- xmlns="http://www.w3.org/2000/svg"
5712
- fill="none"
5713
- viewBox="0 0 24 24"
5714
- stroke-width="2"
5715
- stroke="currentColor"
5716
- class="header-action-icon"
6060
+ ${this._drawerView === 'history' ? b `
6061
+ <kr-button
6062
+ class="history__new"
6063
+ variant="flat"
6064
+ color="primary"
6065
+ size="small"
6066
+ @click=${this._handleNewConversation}
5717
6067
  >
5718
- <path
5719
- stroke-linecap="round"
5720
- stroke-linejoin="round"
5721
- d="M6 18 18 6M6 6l12 12"
5722
- />
5723
- </svg>
5724
- </button>
6068
+ <svg
6069
+ slot="icon"
6070
+ xmlns="http://www.w3.org/2000/svg"
6071
+ fill="none"
6072
+ viewBox="0 0 24 24"
6073
+ stroke-width="1.75"
6074
+ stroke="currentColor"
6075
+ >
6076
+ <path
6077
+ stroke-linecap="round"
6078
+ stroke-linejoin="round"
6079
+ d="M12 4.5v15m7.5-7.5h-15"
6080
+ />
6081
+ </svg>
6082
+ New conversation
6083
+ </kr-button>
6084
+ ` : A}
6085
+ ${fullPanel ? A : b `
6086
+ <button
6087
+ class="header-action"
6088
+ @click=${this._handleCloseDrawer}
6089
+ title="Close"
6090
+ >
6091
+ <svg
6092
+ xmlns="http://www.w3.org/2000/svg"
6093
+ fill="none"
6094
+ viewBox="0 0 24 24"
6095
+ stroke-width="2"
6096
+ stroke="currentColor"
6097
+ class="header-action-icon"
6098
+ >
6099
+ <path
6100
+ stroke-linecap="round"
6101
+ stroke-linejoin="round"
6102
+ d="M6 18 18 6M6 6l12 12"
6103
+ />
6104
+ </svg>
6105
+ </button>
6106
+ `}
5725
6107
  </div>
5726
6108
  <div class="drawer__body">
5727
6109
  ${body}
@@ -5982,9 +6364,10 @@
5982
6364
  const isDebugEnabled = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
5983
6365
  // Push the content over by the open drawer's width so the drawer sits beside
5984
6366
  // the content rather than overlaying it. One consistent (resizable) width for
5985
- // both views. Only pushes while the drawer is open.
6367
+ // both views. Only pushes while the drawer is open, and only inline — the
6368
+ // floating drawer fills the panel over the transcript, so it must not push.
5986
6369
  const contentPadding = {};
5987
- if (this._drawerOpen) {
6370
+ if (this._drawerOpen && this.variant === 'inline') {
5988
6371
  contentPadding.paddingRight = `${this._drawerWidth}px`;
5989
6372
  }
5990
6373
  // Group the transcript into runs so an agent turn (its reasoning/tool/text/
@@ -6102,10 +6485,52 @@
6102
6485
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
6103
6486
  <div class="header-left">
6104
6487
  <div class="header-info">
6105
- <span class="header-title">${this.label}</span>
6488
+ <span class="header-title">${this._conversationTitle || this.label}</span>
6106
6489
  </div>
6107
6490
  </div>
6108
6491
  <div class="header-actions">
6492
+ ${this.variant === 'floating' ? b `
6493
+ <button
6494
+ class="header-action"
6495
+ @click=${this._handleNewConversation}
6496
+ title="New conversation"
6497
+ >
6498
+ <svg
6499
+ xmlns="http://www.w3.org/2000/svg"
6500
+ fill="none"
6501
+ viewBox="0 0 24 24"
6502
+ stroke-width="1.75"
6503
+ stroke="currentColor"
6504
+ class="header-action-icon"
6505
+ >
6506
+ <path
6507
+ stroke-linecap="round"
6508
+ stroke-linejoin="round"
6509
+ d="M12 4.5v15m7.5-7.5h-15"
6510
+ />
6511
+ </svg>
6512
+ </button>
6513
+ <button
6514
+ class="header-action"
6515
+ @click=${() => this._toggleDrawer('history')}
6516
+ title="Conversations"
6517
+ >
6518
+ <svg
6519
+ xmlns="http://www.w3.org/2000/svg"
6520
+ fill="none"
6521
+ viewBox="0 0 24 24"
6522
+ stroke-width="1.5"
6523
+ stroke="currentColor"
6524
+ class="header-action-icon"
6525
+ >
6526
+ <path
6527
+ stroke-linecap="round"
6528
+ stroke-linejoin="round"
6529
+ d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
6530
+ />
6531
+ </svg>
6532
+ </button>
6533
+ ` : A}
6109
6534
  <button
6110
6535
  class="header-action"
6111
6536
  @click=${() => this._toggleDrawer('settings')}
@@ -6183,6 +6608,17 @@
6183
6608
  ${this.messages.length === 0 ? b `
6184
6609
  <div class="empty">
6185
6610
  <span class="empty-text">What can I help you with?</span>
6611
+ ${this._suggestedActions.length ? b `
6612
+ <div class="empty-suggestions">
6613
+ ${this._suggestedActions.map((action) => b `
6614
+ <button
6615
+ type="button"
6616
+ class="empty-suggestions__chip"
6617
+ @click=${() => this._handleSuggestedActionClick(action)}
6618
+ >${action.label}</button>
6619
+ `)}
6620
+ </div>
6621
+ ` : A}
6186
6622
  </div>
6187
6623
  ` : A}
6188
6624
  ${messageGroups.map((group) => {
@@ -6849,22 +7285,28 @@
6849
7285
  display: flex;
6850
7286
  align-items: center;
6851
7287
  gap: 10px;
7288
+ min-width: 0;
6852
7289
  }
6853
7290
 
6854
7291
  .header-info {
6855
7292
  display: flex;
6856
7293
  flex-direction: column;
7294
+ min-width: 0;
6857
7295
  }
6858
7296
 
6859
7297
  .header-title {
6860
7298
  font-size: 16px;
6861
7299
  font-weight: 600;
7300
+ white-space: nowrap;
7301
+ overflow: hidden;
7302
+ text-overflow: ellipsis;
6862
7303
  }
6863
7304
 
6864
7305
  .header-actions {
6865
7306
  display: flex;
6866
7307
  align-items: center;
6867
7308
  gap: 4px;
7309
+ flex-shrink: 0;
6868
7310
  }
6869
7311
 
6870
7312
  .header-action {
@@ -7038,7 +7480,7 @@
7038
7480
  align-items: center;
7039
7481
  justify-content: center;
7040
7482
  color: var(--kr-chatbot-text-secondary);
7041
- gap: 8px;
7483
+ gap: 16px;
7042
7484
  }
7043
7485
 
7044
7486
  .empty-text {
@@ -7046,6 +7488,35 @@
7046
7488
  color: rgb(0 0 0 / 80%);
7047
7489
  }
7048
7490
 
7491
+ /* Context-aware action chips under the empty-state prompt. A row of pills the user can click to
7492
+ prefill a skill — capped small server-side so this reads as a few suggestions, not a menu. */
7493
+ .empty-suggestions {
7494
+ display: flex;
7495
+ flex-wrap: wrap;
7496
+ justify-content: center;
7497
+ gap: 10px;
7498
+ max-width: 420px;
7499
+ margin-top: 4px;
7500
+ }
7501
+
7502
+ .empty-suggestions__chip {
7503
+ padding: 8px 12px;
7504
+ background: rgb(0 0 0 / 6%);
7505
+ border: none;
7506
+ border-radius: 16px;
7507
+ color: #000000;
7508
+ font: inherit;
7509
+ font-size: 13px;
7510
+ line-height: 1.2;
7511
+ cursor: pointer;
7512
+ white-space: nowrap;
7513
+ }
7514
+
7515
+ .empty-suggestions__chip:hover {
7516
+ background: rgb(0 0 0 / 10%);
7517
+ color: #000000;
7518
+ }
7519
+
7049
7520
  .message {
7050
7521
  position: relative;
7051
7522
  display: flex;
@@ -7690,6 +8161,13 @@
7690
8161
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
7691
8162
  }
7692
8163
 
8164
+ /* Full-panel (floating) header: back button then title, left-aligned. */
8165
+ .drawer--full .drawer__header {
8166
+ justify-content: flex-start;
8167
+ gap: 8px;
8168
+ padding: 0 12px;
8169
+ }
8170
+
7693
8171
  .drawer__title {
7694
8172
  font-size: 14px;
7695
8173
  font-weight: 600;
@@ -7703,6 +8181,78 @@
7703
8181
  padding: 4px 0;
7704
8182
  }
7705
8183
 
8184
+ /* Floating variant: the drawer fills the whole panel over the transcript (a
8185
+ full-panel view with a back button), rather than sliding in beside it. */
8186
+ .drawer--full {
8187
+ left: 0;
8188
+ width: 100%;
8189
+ max-width: 100%;
8190
+ z-index: 6;
8191
+ }
8192
+
8193
+ .drawer--full.drawer--open {
8194
+ box-shadow: none;
8195
+ }
8196
+
8197
+ /* History (previous conversations) view. */
8198
+ .history {
8199
+ display: flex;
8200
+ flex-direction: column;
8201
+ }
8202
+
8203
+ /* "New conversation" action in the Conversations header — pushed to the right. */
8204
+ .history__new {
8205
+ margin-left: auto;
8206
+ }
8207
+
8208
+ .history__empty {
8209
+ padding: 24px 20px;
8210
+ text-align: center;
8211
+ font-size: 13px;
8212
+ color: rgb(0 0 0 / 50%);
8213
+ }
8214
+
8215
+ .history__list {
8216
+ display: flex;
8217
+ flex-direction: column;
8218
+ padding: 4px 0;
8219
+ }
8220
+
8221
+ .history__item {
8222
+ display: flex;
8223
+ flex-direction: column;
8224
+ align-items: flex-start;
8225
+ gap: 5px;
8226
+ padding: 10px 20px;
8227
+ background: transparent;
8228
+ border: none;
8229
+ text-align: left;
8230
+ cursor: pointer;
8231
+ transition: background 0.15s;
8232
+ }
8233
+
8234
+ .history__item:hover {
8235
+ background: rgba(0, 0, 0, 0.04);
8236
+ }
8237
+
8238
+ .history__item--active {
8239
+ background: rgba(0, 0, 0, 0.06);
8240
+ }
8241
+
8242
+ .history__item-title {
8243
+ font-size: 14px;
8244
+ color: #000000;
8245
+ white-space: nowrap;
8246
+ overflow: hidden;
8247
+ text-overflow: ellipsis;
8248
+ max-width: 100%;
8249
+ }
8250
+
8251
+ .history__item-date {
8252
+ font-size: 12px;
8253
+ color: rgb(0 0 0 / 60%);
8254
+ }
8255
+
7706
8256
  .debug-section {
7707
8257
  padding: 14px 20px;
7708
8258
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
@@ -8637,6 +9187,9 @@
8637
9187
  __decorate$4([
8638
9188
  r()
8639
9189
  ], KRChatbot.prototype, "_error", void 0);
9190
+ __decorate$4([
9191
+ r()
9192
+ ], KRChatbot.prototype, "_conversationTitle", void 0);
8640
9193
  __decorate$4([
8641
9194
  r()
8642
9195
  ], KRChatbot.prototype, "_contextWarning", void 0);
@@ -8670,6 +9223,12 @@
8670
9223
  __decorate$4([
8671
9224
  r()
8672
9225
  ], KRChatbot.prototype, "_drawerView", void 0);
9226
+ __decorate$4([
9227
+ r()
9228
+ ], KRChatbot.prototype, "_conversations", void 0);
9229
+ __decorate$4([
9230
+ r()
9231
+ ], KRChatbot.prototype, "_conversationsLoading", void 0);
8673
9232
  __decorate$4([
8674
9233
  r()
8675
9234
  ], KRChatbot.prototype, "_toolDetail", void 0);
@@ -8709,6 +9268,9 @@
8709
9268
  __decorate$4([
8710
9269
  r()
8711
9270
  ], KRChatbot.prototype, "_invocableSkills", void 0);
9271
+ __decorate$4([
9272
+ r()
9273
+ ], KRChatbot.prototype, "_suggestedActions", void 0);
8712
9274
  __decorate$4([
8713
9275
  r()
8714
9276
  ], KRChatbot.prototype, "_aiFrameworkConfig", void 0);