@kodaris/krubble-app-components 1.0.79 → 1.0.81

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
@@ -85,8 +85,9 @@ let KRChatbot = class KRChatbot extends LitElement {
85
85
  * responses are dropped (mirrors the kr-combo-box `_requestId` pattern). Org and
86
86
  * global searches run concurrently into separate buffers, so each has its own.
87
87
  */
88
- this._skillRequestId = 0;
88
+ this._orgSkillRequestId = 0;
89
89
  this._globalSkillRequestId = 0;
90
+ this._invocableSkillRequestId = 0;
90
91
  /**
91
92
  * Codes of global skills with an in-flight enable/disable request. The `+`
92
93
  * button reads this to show a pending state and ignore repeat clicks. Mutated
@@ -195,24 +196,31 @@ let KRChatbot = class KRChatbot extends LitElement {
195
196
  this._isDraggingFile = false;
196
197
  /** Whether the `/` skill menu is open above the composer. */
197
198
  this._skillMenuOpened = false;
198
- /** Current `/` query (text after the slash), sent to `_searchSkills`. */
199
+ /** Current `/` query (text after the slash), sent to `_searchOrgSkills`. */
199
200
  this._skillQuery = '';
200
201
  /**
201
- * Latest org skill search results (this instance's own skills). Backs the `/`
202
- * menu and the browse dialog's "Your Org" tab.
202
+ * Latest org skill search results (this instance's own skills). Backs the
203
+ * browse dialog's "Your Org" tab only — global skills never appear here.
203
204
  */
204
- this._skillResults = [];
205
+ this._orgSkills = [];
205
206
  /**
206
207
  * Latest shared-Kodaris (global) skill results, for the browse dialog's
207
- * "Kodaris" tab. Kept separate from `_skillResults` so switching tabs shows
208
+ * "Kodaris" tab. Kept separate from `_orgSkills` so switching tabs shows
208
209
  * fresh results immediately instead of the other tab's stale list — the browse
209
210
  * search fills both buffers at once.
210
211
  */
211
- this._globalSkillResults = [];
212
+ this._globalSkills = [];
213
+ /**
214
+ * Latest results for the `/` composer menu: this instance's own org skills PLUS
215
+ * the global skills installed on it (both are invocable by code). A separate
216
+ * buffer from the two browse-tab lists so merging installed globals into the
217
+ * menu never leaks them into the "Your Org" tab.
218
+ */
219
+ this._invocableSkills = [];
212
220
  /**
213
221
  * AI framework UI config flags fetched from `aiFrameworkConfig` on connect.
214
222
  * `debugTelemetryEnabled` (same server-side gate as TOKEN_USAGE emission) turns
215
- * on the debug drawer toggle and the `/test-warning` / `/test-limit` composer
223
+ * on the debug drawer toggle and the `/test-context-warning` / `/test-context-limit` composer
216
224
  * options — known up front, before any message.
217
225
  */
218
226
  this._aiFrameworkConfig = {};
@@ -223,8 +231,8 @@ let KRChatbot = class KRChatbot extends LitElement {
223
231
  /** Whether the "Browse all skills" dialog is open. */
224
232
  this._skillBrowseOpened = false;
225
233
  /**
226
- * Active tab in the browse dialog. `org` shows `_skillResults` (this instance's
227
- * skills); `kodaris` shows `_globalSkillResults` (the shared global catalog).
234
+ * Active tab in the browse dialog. `org` shows `_orgSkills` (this instance's
235
+ * skills); `kodaris` shows `_globalSkills` (the shared global catalog).
228
236
  */
229
237
  this._skillBrowseSource = 'org';
230
238
  /** Search text in the browse dialog, routed to the active tab's search. */
@@ -617,13 +625,14 @@ let KRChatbot = class KRChatbot extends LitElement {
617
625
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
618
626
  // Open the skill menu while the message is a `/` command being typed (a
619
627
  // leading slash with no space yet). The text after the slash is the search
620
- // query, run server-side via `_searchSkills`.
628
+ // query, run server-side via `_searchInvocableSkills`.
629
+ // (org skills + installed globals — everything invocable by /code)
621
630
  const slashMatch = this._inputValue.match(/^\/(\S*)$/);
622
631
  if (slashMatch) {
623
632
  this._skillMenuOpened = true;
624
633
  this._skillQuery = slashMatch[1];
625
634
  this._skillHighlighted = 0;
626
- this._searchSkills(this._skillQuery);
635
+ this._searchInvocableSkills(this._skillQuery);
627
636
  }
628
637
  else {
629
638
  this._skillMenuOpened = false;
@@ -639,7 +648,7 @@ let KRChatbot = class KRChatbot extends LitElement {
639
648
  if (debugActions) {
640
649
  actionCount = 4;
641
650
  }
642
- const count = actionCount + this._skillResults.length;
651
+ const count = actionCount + this._invocableSkills.length;
643
652
  if (e.key === 'ArrowDown') {
644
653
  e.preventDefault();
645
654
  this._skillHighlighted = (this._skillHighlighted + 1) % count;
@@ -684,7 +693,7 @@ let KRChatbot = class KRChatbot extends LitElement {
684
693
  if (debugActions && index === 2) {
685
694
  this._skillMenuOpened = false;
686
695
  this._send({
687
- message: '/test-warning',
696
+ message: '/test-context-warning',
688
697
  internalFiles: [],
689
698
  });
690
699
  return;
@@ -692,19 +701,19 @@ let KRChatbot = class KRChatbot extends LitElement {
692
701
  if (debugActions && index === 3) {
693
702
  this._skillMenuOpened = false;
694
703
  this._send({
695
- message: '/test-limit',
704
+ message: '/test-context-limit',
696
705
  internalFiles: [],
697
706
  });
698
707
  return;
699
708
  }
700
- const skill = this._skillResults[index - actionCount];
709
+ const skill = this._invocableSkills[index - actionCount];
701
710
  if (skill) {
702
711
  this._handleSkillSelect(skill);
703
712
  }
704
713
  }
705
714
  // --- Skills ---
706
715
  /**
707
- * Search the Kodaris AI skills index and store the results in `_skillResults`.
716
+ * Search the Kodaris AI skills index and store the results in `_orgSkills`.
708
717
  * The `/` menu and the browse dialog both call this; results are shared since
709
718
  * the two are never open at once. Mirrors the backend's own skill search (BM25
710
719
  * over name^3/description^2/instructions, active-only) so the menu ranks skills
@@ -715,9 +724,9 @@ let KRChatbot = class KRChatbot extends LitElement {
715
724
  * that arrive out of order. `/aiskills/search` returns a raw OpenSearch response,
716
725
  * so we lift each hit's `_source` (already in KRChatbotSkill shape) out.
717
726
  */
718
- _searchSkills(query) {
719
- this._skillRequestId++;
720
- const requestId = this._skillRequestId;
727
+ _searchOrgSkills(query) {
728
+ this._orgSkillRequestId++;
729
+ const requestId = this._orgSkillRequestId;
721
730
  const trimmed = query.trim();
722
731
  // An empty query matches everything; otherwise substring-match the query
723
732
  // against name/code/description. We use case-insensitive wildcard queries on
@@ -767,6 +776,11 @@ let KRChatbot = class KRChatbot extends LitElement {
767
776
  bool: {
768
777
  must: [must],
769
778
  filter: [{ term: { active: true } }],
779
+ // The "Your Org" tab lists only this instance's own skills. Globally-
780
+ // published skills belong to the "Kodaris" tab (fed by the registry),
781
+ // so exclude them here. Documents without the field aren't matched by
782
+ // the term, so ordinary org skills are kept.
783
+ must_not: [{ term: { globalPublished: true } }],
770
784
  },
771
785
  },
772
786
  },
@@ -774,17 +788,106 @@ let KRChatbot = class KRChatbot extends LitElement {
774
788
  .then((r) => r.json())
775
789
  .then((res) => {
776
790
  // Ignore a stale response superseded by a newer search.
777
- if (requestId !== this._skillRequestId) {
791
+ if (requestId !== this._orgSkillRequestId) {
778
792
  return;
779
793
  }
780
- this._skillResults = (res?.data?.hits?.hits || []).map((hit) => hit._source);
794
+ this._orgSkills = (res?.data?.hits?.hits || []).map((hit) => hit._source);
781
795
  })
782
796
  .catch((error) => {
783
797
  console.error('kr-chatbot: skill search failed', error);
784
798
  });
785
799
  }
786
800
  /**
787
- * Search the shared Kodaris global-skill registry into `_globalSkillResults`
801
+ * Search skills for the `/` composer menu into `_invocableSkills`: this instance's
802
+ * own org skills PLUS the global skills installed on it, since both are
803
+ * invocable by code. They come from two indexes (the local aiskills search and
804
+ * the registry search filtered client-side to enabled), so we fire both and
805
+ * concat — org skills first, then installed globals. Skill codes are unique
806
+ * across the two, so no dedup is needed. Kept out of `_orgSkills` so
807
+ * installed globals never leak into the browse dialog's "Your Org" tab.
808
+ * (Cross-list relevance ranking is a later concern; for now org skills lead.)
809
+ */
810
+ _searchInvocableSkills(query) {
811
+ this._invocableSkillRequestId++;
812
+ const requestId = this._invocableSkillRequestId;
813
+ // Same wildcard matching as _searchOrgSkills (see there for the rationale).
814
+ let must = { match_all: {} };
815
+ if (query.trim()) {
816
+ must = {
817
+ bool: {
818
+ should: [
819
+ {
820
+ wildcard: {
821
+ 'name.raw': {
822
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
823
+ case_insensitive: true,
824
+ }
825
+ }
826
+ },
827
+ {
828
+ wildcard: {
829
+ 'code.raw': {
830
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
831
+ case_insensitive: true,
832
+ }
833
+ }
834
+ },
835
+ {
836
+ wildcard: {
837
+ 'description.raw': {
838
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
839
+ case_insensitive: true,
840
+ }
841
+ }
842
+ },
843
+ ],
844
+ minimum_should_match: 1,
845
+ },
846
+ };
847
+ }
848
+ // Org skills — this instance's own catalog (globalPublished excluded).
849
+ const orgSearch = KRHttp.fetch({
850
+ url: '/api/system/opensearch/aiskills/search',
851
+ method: 'POST',
852
+ body: {
853
+ size: 100,
854
+ query: {
855
+ bool: {
856
+ must: [must],
857
+ filter: [{ term: { active: true } }],
858
+ must_not: [{ term: { globalPublished: true } }],
859
+ },
860
+ },
861
+ },
862
+ })
863
+ .then((r) => r.json())
864
+ .then((res) => (res?.data?.hits?.hits || []).map((hit) => hit._source))
865
+ .catch((error) => {
866
+ console.error('kr-chatbot: invocable skill search failed', error);
867
+ return [];
868
+ });
869
+ // Installed globals — the registry catalog, kept only where enabled=true.
870
+ const globalSearch = KRHttp.fetch({
871
+ url: '/api/system/opensearch/aiskills/global/search',
872
+ method: 'POST',
873
+ body: { query: query.trim() },
874
+ })
875
+ .then((r) => r.json())
876
+ .then((res) => (res?.data || []).filter((s) => !!s.enabled))
877
+ .catch((error) => {
878
+ console.error('kr-chatbot: installed global skill search failed', error);
879
+ return [];
880
+ });
881
+ Promise.all([orgSearch, globalSearch]).then(([orgSkills, globalSkills]) => {
882
+ // Ignore a stale response superseded by a newer search.
883
+ if (requestId !== this._invocableSkillRequestId) {
884
+ return;
885
+ }
886
+ this._invocableSkills = [...orgSkills, ...globalSkills];
887
+ });
888
+ }
889
+ /**
890
+ * Search the shared Kodaris global-skill registry into `_globalSkills`
788
891
  * (the browse dialog's "Kodaris" tab). Unlike `/aiskills/search`,
789
892
  * `/aiskills/global/search` returns a digested list
790
893
  * (`data: [{ code, name, description, enabled }]`) already in `KRChatbotSkill`
@@ -805,7 +908,7 @@ let KRChatbot = class KRChatbot extends LitElement {
805
908
  if (requestId !== this._globalSkillRequestId) {
806
909
  return;
807
910
  }
808
- this._globalSkillResults = (res?.data || []);
911
+ this._globalSkills = (res?.data || []);
809
912
  })
810
913
  .catch((error) => {
811
914
  console.error('kr-chatbot: global skill search failed', error);
@@ -817,13 +920,16 @@ let KRChatbot = class KRChatbot extends LitElement {
817
920
  * matching card. Ignores clicks while a request for the same code is pending.
818
921
  */
819
922
  _handleGlobalSkillEnable(skill, e) {
820
- // The card body inserts the skill; the `+` button must not also do that.
923
+ // The card body inserts the skill; the install button must not also do that.
821
924
  e.stopPropagation();
822
925
  if (this._globalSkillPending.has(skill.code)) {
823
926
  return;
824
927
  }
928
+ // Drive the clicked button's own spinner (KRButton disables itself while
929
+ // loading, so a second click can't fire until the request settles).
930
+ const btn = e.currentTarget;
825
931
  this._globalSkillPending.add(skill.code);
826
- this.requestUpdate();
932
+ btn?.showLoading();
827
933
  KRHttp.fetch({
828
934
  url: `/api/system/opensearch/aiskills/global/${encodeURIComponent(skill.code)}/enabled`,
829
935
  method: 'POST',
@@ -831,16 +937,18 @@ let KRChatbot = class KRChatbot extends LitElement {
831
937
  })
832
938
  .then((r) => r.json())
833
939
  .then((res) => {
834
- // Reflect the server's returned flag on the card.
940
+ // Reflect the server's returned flag on the card. The button label/style
941
+ // re-render from skill.enabled; reset() clears the spinner to idle.
835
942
  skill.enabled = res?.data?.enabled;
943
+ btn?.reset();
836
944
  this.requestUpdate();
837
945
  })
838
946
  .catch((error) => {
839
947
  console.error('kr-chatbot: global skill enable failed', error);
948
+ btn?.showError('Error');
840
949
  })
841
950
  .finally(() => {
842
951
  this._globalSkillPending.delete(skill.code);
843
- this.requestUpdate();
844
952
  });
845
953
  }
846
954
  /** The `+` button toggles the skill/attachment menu open (showing all skills). */
@@ -852,7 +960,7 @@ let KRChatbot = class KRChatbot extends LitElement {
852
960
  this._skillQuery = '';
853
961
  this._skillHighlighted = 0;
854
962
  this._skillMenuOpened = true;
855
- this._searchSkills('');
963
+ this._searchInvocableSkills('');
856
964
  }
857
965
  /** Insert `/code ` into the composer and focus it, ready for the message. */
858
966
  _handleSkillSelect(skill) {
@@ -885,7 +993,7 @@ let KRChatbot = class KRChatbot extends LitElement {
885
993
  this._skillBrowseQuery = '';
886
994
  this._skillBrowseOpened = true;
887
995
  // Fill both tabs up front so switching between them is instant, not stale.
888
- this._searchSkills('');
996
+ this._searchOrgSkills('');
889
997
  this._searchGlobalSkills('');
890
998
  }
891
999
  _handleSkillBrowseClose() {
@@ -894,7 +1002,7 @@ let KRChatbot = class KRChatbot extends LitElement {
894
1002
  _handleSkillBrowseSearch(e) {
895
1003
  this._skillBrowseQuery = e.target.value;
896
1004
  // Filter both catalogs so whichever tab the user switches to is already current.
897
- this._searchSkills(this._skillBrowseQuery);
1005
+ this._searchOrgSkills(this._skillBrowseQuery);
898
1006
  this._searchGlobalSkills(this._skillBrowseQuery);
899
1007
  }
900
1008
  _handleSkillBrowseTabChange(e) {
@@ -1339,7 +1447,7 @@ let KRChatbot = class KRChatbot extends LitElement {
1339
1447
  // --- Render ---
1340
1448
  /** The `/` skill menu shown above the composer while typing a slash command. */
1341
1449
  _renderSkillMenu() {
1342
- const skills = this._skillResults;
1450
+ const skills = this._invocableSkills;
1343
1451
  // Debug test triggers are extra actions after Attach Files / Browse. The
1344
1452
  // flat keyboard index counts actions first, then skills, so the skills'
1345
1453
  // index offset is the number of actions shown.
@@ -1377,7 +1485,7 @@ let KRChatbot = class KRChatbot extends LitElement {
1377
1485
  })}
1378
1486
  @click=${() => this._activateSkillMenuItem(2)}
1379
1487
  >
1380
- Test: context warning
1488
+ Test Context Warning
1381
1489
  </button>
1382
1490
  <button
1383
1491
  class=${classMap({
@@ -1386,7 +1494,7 @@ let KRChatbot = class KRChatbot extends LitElement {
1386
1494
  })}
1387
1495
  @click=${() => this._activateSkillMenuItem(3)}
1388
1496
  >
1389
- Test: context limit
1497
+ Test Context Limit
1390
1498
  </button>
1391
1499
  ` : nothing}
1392
1500
  </div>
@@ -1446,69 +1554,71 @@ let KRChatbot = class KRChatbot extends LitElement {
1446
1554
  /**
1447
1555
  * The card grid of skill search results for the browse dialog. Clicking a card
1448
1556
  * inserts `/code ` into the composer. In the shared-Kodaris (`global`) tab each
1449
- * card also gets a `+` install button that enables the skill on this instance
1450
- * (turning into a ✓ once installed).
1557
+ * card also gets a footer row with an Enabled toggle that installs/removes the
1558
+ * skill on this instance.
1451
1559
  */
1452
1560
  _renderSkillGrid(global = false) {
1453
- let skills = this._skillResults;
1561
+ let skills = this._orgSkills;
1454
1562
  if (global) {
1455
- skills = this._globalSkillResults;
1563
+ skills = this._globalSkills;
1456
1564
  }
1457
1565
  if (!skills.length) {
1458
1566
  return nothing;
1459
1567
  }
1460
1568
  return html `
1461
1569
  <div class="skill-browse__grid">
1462
- ${skills.map((skill) => html `
1463
- <div
1464
- class="skill-card"
1465
- role="button"
1466
- tabindex="0"
1467
- @click=${() => this._handleSkillSelect(skill)}
1468
- @keydown=${(e) => this._handleSkillCardKeydown(e, skill)}
1469
- >
1470
- <span class="skill-card__name">${skill.name}</span>
1471
- ${skill.description ? html `
1472
- <span class="skill-card__desc">${skill.description}</span>
1473
- ` : nothing}
1474
- ${global ? this._renderSkillInstall(skill) : nothing}
1475
- </div>
1476
- `)}
1570
+ ${skills.map((skill) => {
1571
+ // A global skill must be installed before it can be chosen — inserting
1572
+ // /code for a skill not enabled on this instance would just fail. So an
1573
+ // uninstalled global card does nothing on click; only its Install button
1574
+ // is interactive.
1575
+ const selectable = !global || !!skill.enabled;
1576
+ return html `
1577
+ <div
1578
+ class="skill-card"
1579
+ role=${selectable ? 'button' : nothing}
1580
+ tabindex=${selectable ? 0 : nothing}
1581
+ @click=${selectable ? () => this._handleSkillSelect(skill) : nothing}
1582
+ @keydown=${selectable ? (e) => this._handleSkillCardKeydown(e, skill) : nothing}
1583
+ >
1584
+ <span class="skill-card__name">${skill.name}</span>
1585
+ ${skill.description ? html `
1586
+ <span class="skill-card__desc">${skill.description}</span>
1587
+ ` : nothing}
1588
+ ${global ? this._renderSkillToggle(skill) : nothing}
1589
+ </div>
1590
+ `;
1591
+ })}
1477
1592
  </div>
1478
1593
  `;
1479
1594
  }
1595
+ /** Keep a click on the card footer from bubbling to the card's insert handler. */
1596
+ _stopFooterClick(e) {
1597
+ e.stopPropagation();
1598
+ }
1480
1599
  /**
1481
- * The install affordance on a global skill card: a `+` button that enables the
1482
- * skill on this instance, showing a ✓ once installed and a disabled state
1483
- * while the enable/disable request is in flight.
1600
+ * The install affordance on a global skill card: a footer row with an
1601
+ * install/remove button. The button owns its own spinner via KRButton's
1602
+ * showLoading()/showError() while the enable/disable request is in flight, so
1603
+ * the async call has clear feedback. Clicks are stopped from bubbling so the
1604
+ * button never also inserts the skill (the card body does that).
1484
1605
  */
1485
- _renderSkillInstall(skill) {
1606
+ _renderSkillToggle(skill) {
1607
+ const on = !!skill.enabled;
1486
1608
  return html `
1487
- <button
1488
- class=${classMap({
1489
- 'skill-card__install': true,
1490
- 'skill-card__install--installed': !!skill.enabled,
1491
- })}
1492
- ?disabled=${this._globalSkillPending.has(skill.code)}
1493
- title=${!!skill.enabled ? 'Installed — click to remove' : 'Install this skill'}
1494
- aria-label=${!!skill.enabled ? 'Remove skill' : 'Install skill'}
1495
- @click=${(e) => this._handleGlobalSkillEnable(skill, e)}
1609
+ <div
1610
+ class="skill-card__footer"
1611
+ @click=${this._stopFooterClick}
1496
1612
  >
1497
- <svg
1498
- xmlns="http://www.w3.org/2000/svg"
1499
- fill="none"
1500
- viewBox="0 0 24 24"
1501
- stroke-width="2"
1502
- stroke="currentColor"
1503
- class="skill-card__install-icon"
1504
- >
1505
- <path
1506
- stroke-linecap="round"
1507
- stroke-linejoin="round"
1508
- d=${!!skill.enabled ? 'm4.5 12.75 6 6 9-13.5' : 'M12 4.5v15m7.5-7.5h-15'}
1509
- />
1510
- </svg>
1511
- </button>
1613
+ <span class="skill-card__price">Free</span>
1614
+ <kr-button
1615
+ class="skill-card__install-btn"
1616
+ size="small"
1617
+ variant=${on ? 'outline' : 'flat'}
1618
+ color=${on ? 'secondary' : 'primary'}
1619
+ @click=${(e) => this._handleGlobalSkillEnable(skill, e)}
1620
+ >${on ? 'Uninstall' : 'Install'}</kr-button>
1621
+ </div>
1512
1622
  `;
1513
1623
  }
1514
1624
  /**
@@ -1969,9 +2079,7 @@ let KRChatbot = class KRChatbot extends LitElement {
1969
2079
  </svg>
1970
2080
  </button>
1971
2081
  </div>
1972
- ` : nothing}
1973
-
1974
- ${this._contextLimitReached ? html `
2082
+ ` : this._contextLimitReached ? html `
1975
2083
  <div class="context-limit" role="alert">
1976
2084
  ${this._contextLimitMessage
1977
2085
  || 'This conversation has reached its limit. Please start a new one to continue.'}
@@ -2557,14 +2665,22 @@ KRChatbot.styles = css `
2557
2665
 
2558
2666
  /* Context warning banner (user-facing, dismissible, non-blocking) */
2559
2667
  .context-warning {
2560
- padding: 8px 16px;
2668
+ /* Match the message text column: 768px cap minus its 32px side padding
2669
+ (704px), and viewport minus that padding on narrower screens. */
2670
+ width: 100%;
2671
+ max-width: 735px;
2672
+ margin: 8px auto 0;
2673
+ padding: 10px 14px;
2674
+ border: 1px solid #fde68a;
2675
+ border-radius: 8px;
2561
2676
  background: #fffbeb;
2562
2677
  color: #92400e;
2563
2678
  font-size: 13px;
2679
+ line-height: 1.4;
2564
2680
  display: flex;
2565
2681
  align-items: center;
2566
2682
  justify-content: space-between;
2567
- gap: 8px;
2683
+ gap: 10px;
2568
2684
  flex-shrink: 0;
2569
2685
  }
2570
2686
 
@@ -2577,6 +2693,11 @@ KRChatbot.styles = css `
2577
2693
  display: flex;
2578
2694
  align-items: center;
2579
2695
  flex-shrink: 0;
2696
+ opacity: 0.7;
2697
+ }
2698
+
2699
+ .context-warning-dismiss:hover {
2700
+ opacity: 1;
2580
2701
  }
2581
2702
 
2582
2703
  .context-warning-dismiss-icon {
@@ -2586,11 +2707,18 @@ KRChatbot.styles = css `
2586
2707
 
2587
2708
  /* Context-limit notice (user-facing, blocking — steers to a new conversation) */
2588
2709
  .context-limit {
2589
- padding: 10px 16px;
2710
+ /* Match the message text column: 768px cap minus its 32px side padding
2711
+ (704px), and viewport minus that padding on narrower screens. */
2712
+ width: 100%;
2713
+ max-width: 735px;
2714
+ margin: 8px auto 0;
2715
+ padding: 10px 14px;
2716
+ border: 1px solid #fecaca;
2717
+ border-radius: 8px;
2590
2718
  background: var(--kr-chatbot-error-bg);
2591
2719
  color: var(--kr-chatbot-error-text);
2592
2720
  font-size: 13px;
2593
- line-height: 1.5;
2721
+ line-height: 1.4;
2594
2722
  flex-shrink: 0;
2595
2723
  }
2596
2724
 
@@ -3118,7 +3246,7 @@ KRChatbot.styles = css `
3118
3246
  height: 100%;
3119
3247
  overflow-y: auto;
3120
3248
  display: grid;
3121
- grid-template-columns: repeat(3, 1fr);
3249
+ grid-template-columns: repeat(2, 1fr);
3122
3250
  grid-auto-rows: min-content;
3123
3251
  align-content: start;
3124
3252
  gap: 12px;
@@ -3146,53 +3274,31 @@ KRChatbot.styles = css `
3146
3274
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
3147
3275
  }
3148
3276
 
3149
- /* Install button on global skill cards. Absolutely positioned in the corner
3150
- so it sits above the card's own click target; leave room via the name's
3151
- padding-right below. */
3152
- .skill-card__install {
3153
- position: absolute;
3154
- top: 10px;
3155
- right: 10px;
3156
- display: inline-flex;
3277
+ /* Footer row on global skill cards: install button on the left, price on the
3278
+ right. margin-top:auto pins it to the bottom so cards in a row align their
3279
+ footers regardless of description length. Negative horizontal margins
3280
+ (matching the card's 14px padding) pull the top border out to the full
3281
+ card width; the content is re-padded horizontally to stay inset. */
3282
+ .skill-card__footer {
3283
+ display: flex;
3157
3284
  align-items: center;
3158
- justify-content: center;
3159
- width: 24px;
3160
- height: 24px;
3161
- padding: 0;
3162
- border: 1px solid #d1d5db;
3163
- border-radius: 6px;
3164
- background: white;
3165
- color: #374151;
3166
- cursor: pointer;
3167
- transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
3168
- }
3169
-
3170
- .skill-card__install:hover:not(:disabled) {
3171
- border-color: var(--kr-chatbot-primary);
3172
- color: var(--kr-chatbot-primary);
3173
- }
3174
-
3175
- .skill-card__install:disabled {
3176
- opacity: 0.5;
3177
- cursor: default;
3178
- }
3179
-
3180
- /* Installed state: filled check. */
3181
- .skill-card__install--installed {
3182
- background: var(--kr-chatbot-primary);
3183
- border-color: var(--kr-chatbot-primary);
3184
- color: white;
3285
+ justify-content: space-between;
3286
+ gap: 8px;
3287
+ margin-top: 8px;
3288
+ margin-left: -14px;
3289
+ margin-right: -14px;
3290
+ padding: 14px 14px 0;
3291
+ border-top: 1px solid #e5e7eb;
3185
3292
  }
3186
3293
 
3187
- .skill-card__install-icon {
3188
- width: 15px;
3189
- height: 15px;
3294
+ .skill-card__price {
3295
+ font-size: 13px;
3296
+ font-weight: 600;
3297
+ color: #000000;
3190
3298
  }
3191
3299
 
3192
3300
  .skill-card__name {
3193
3301
  font-weight: 600;
3194
- /* Leave room for the corner install button on global cards. */
3195
- padding-right: 28px;
3196
3302
  }
3197
3303
 
3198
3304
  .skill-card__desc {
@@ -3381,10 +3487,13 @@ __decorate([
3381
3487
  ], KRChatbot.prototype, "_skillQuery", void 0);
3382
3488
  __decorate([
3383
3489
  state()
3384
- ], KRChatbot.prototype, "_skillResults", void 0);
3490
+ ], KRChatbot.prototype, "_orgSkills", void 0);
3491
+ __decorate([
3492
+ state()
3493
+ ], KRChatbot.prototype, "_globalSkills", void 0);
3385
3494
  __decorate([
3386
3495
  state()
3387
- ], KRChatbot.prototype, "_globalSkillResults", void 0);
3496
+ ], KRChatbot.prototype, "_invocableSkills", void 0);
3388
3497
  __decorate([
3389
3498
  state()
3390
3499
  ], KRChatbot.prototype, "_aiFrameworkConfig", void 0);