@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.
@@ -5624,8 +5624,9 @@
5624
5624
  * responses are dropped (mirrors the kr-combo-box `_requestId` pattern). Org and
5625
5625
  * global searches run concurrently into separate buffers, so each has its own.
5626
5626
  */
5627
- this._skillRequestId = 0;
5627
+ this._orgSkillRequestId = 0;
5628
5628
  this._globalSkillRequestId = 0;
5629
+ this._invocableSkillRequestId = 0;
5629
5630
  /**
5630
5631
  * Codes of global skills with an in-flight enable/disable request. The `+`
5631
5632
  * button reads this to show a pending state and ignore repeat clicks. Mutated
@@ -5734,24 +5735,31 @@
5734
5735
  this._isDraggingFile = false;
5735
5736
  /** Whether the `/` skill menu is open above the composer. */
5736
5737
  this._skillMenuOpened = false;
5737
- /** Current `/` query (text after the slash), sent to `_searchSkills`. */
5738
+ /** Current `/` query (text after the slash), sent to `_searchOrgSkills`. */
5738
5739
  this._skillQuery = '';
5739
5740
  /**
5740
- * Latest org skill search results (this instance's own skills). Backs the `/`
5741
- * menu and the browse dialog's "Your Org" tab.
5741
+ * Latest org skill search results (this instance's own skills). Backs the
5742
+ * browse dialog's "Your Org" tab only — global skills never appear here.
5742
5743
  */
5743
- this._skillResults = [];
5744
+ this._orgSkills = [];
5744
5745
  /**
5745
5746
  * Latest shared-Kodaris (global) skill results, for the browse dialog's
5746
- * "Kodaris" tab. Kept separate from `_skillResults` so switching tabs shows
5747
+ * "Kodaris" tab. Kept separate from `_orgSkills` so switching tabs shows
5747
5748
  * fresh results immediately instead of the other tab's stale list — the browse
5748
5749
  * search fills both buffers at once.
5749
5750
  */
5750
- this._globalSkillResults = [];
5751
+ this._globalSkills = [];
5752
+ /**
5753
+ * Latest results for the `/` composer menu: this instance's own org skills PLUS
5754
+ * the global skills installed on it (both are invocable by code). A separate
5755
+ * buffer from the two browse-tab lists so merging installed globals into the
5756
+ * menu never leaks them into the "Your Org" tab.
5757
+ */
5758
+ this._invocableSkills = [];
5751
5759
  /**
5752
5760
  * AI framework UI config flags fetched from `aiFrameworkConfig` on connect.
5753
5761
  * `debugTelemetryEnabled` (same server-side gate as TOKEN_USAGE emission) turns
5754
- * on the debug drawer toggle and the `/test-warning` / `/test-limit` composer
5762
+ * on the debug drawer toggle and the `/test-context-warning` / `/test-context-limit` composer
5755
5763
  * options — known up front, before any message.
5756
5764
  */
5757
5765
  this._aiFrameworkConfig = {};
@@ -5762,8 +5770,8 @@
5762
5770
  /** Whether the "Browse all skills" dialog is open. */
5763
5771
  this._skillBrowseOpened = false;
5764
5772
  /**
5765
- * Active tab in the browse dialog. `org` shows `_skillResults` (this instance's
5766
- * skills); `kodaris` shows `_globalSkillResults` (the shared global catalog).
5773
+ * Active tab in the browse dialog. `org` shows `_orgSkills` (this instance's
5774
+ * skills); `kodaris` shows `_globalSkills` (the shared global catalog).
5767
5775
  */
5768
5776
  this._skillBrowseSource = 'org';
5769
5777
  /** Search text in the browse dialog, routed to the active tab's search. */
@@ -6156,13 +6164,14 @@
6156
6164
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
6157
6165
  // Open the skill menu while the message is a `/` command being typed (a
6158
6166
  // leading slash with no space yet). The text after the slash is the search
6159
- // query, run server-side via `_searchSkills`.
6167
+ // query, run server-side via `_searchInvocableSkills`.
6168
+ // (org skills + installed globals — everything invocable by /code)
6160
6169
  const slashMatch = this._inputValue.match(/^\/(\S*)$/);
6161
6170
  if (slashMatch) {
6162
6171
  this._skillMenuOpened = true;
6163
6172
  this._skillQuery = slashMatch[1];
6164
6173
  this._skillHighlighted = 0;
6165
- this._searchSkills(this._skillQuery);
6174
+ this._searchInvocableSkills(this._skillQuery);
6166
6175
  }
6167
6176
  else {
6168
6177
  this._skillMenuOpened = false;
@@ -6178,7 +6187,7 @@
6178
6187
  if (debugActions) {
6179
6188
  actionCount = 4;
6180
6189
  }
6181
- const count = actionCount + this._skillResults.length;
6190
+ const count = actionCount + this._invocableSkills.length;
6182
6191
  if (e.key === 'ArrowDown') {
6183
6192
  e.preventDefault();
6184
6193
  this._skillHighlighted = (this._skillHighlighted + 1) % count;
@@ -6223,7 +6232,7 @@
6223
6232
  if (debugActions && index === 2) {
6224
6233
  this._skillMenuOpened = false;
6225
6234
  this._send({
6226
- message: '/test-warning',
6235
+ message: '/test-context-warning',
6227
6236
  internalFiles: [],
6228
6237
  });
6229
6238
  return;
@@ -6231,19 +6240,19 @@
6231
6240
  if (debugActions && index === 3) {
6232
6241
  this._skillMenuOpened = false;
6233
6242
  this._send({
6234
- message: '/test-limit',
6243
+ message: '/test-context-limit',
6235
6244
  internalFiles: [],
6236
6245
  });
6237
6246
  return;
6238
6247
  }
6239
- const skill = this._skillResults[index - actionCount];
6248
+ const skill = this._invocableSkills[index - actionCount];
6240
6249
  if (skill) {
6241
6250
  this._handleSkillSelect(skill);
6242
6251
  }
6243
6252
  }
6244
6253
  // --- Skills ---
6245
6254
  /**
6246
- * Search the Kodaris AI skills index and store the results in `_skillResults`.
6255
+ * Search the Kodaris AI skills index and store the results in `_orgSkills`.
6247
6256
  * The `/` menu and the browse dialog both call this; results are shared since
6248
6257
  * the two are never open at once. Mirrors the backend's own skill search (BM25
6249
6258
  * over name^3/description^2/instructions, active-only) so the menu ranks skills
@@ -6254,9 +6263,9 @@
6254
6263
  * that arrive out of order. `/aiskills/search` returns a raw OpenSearch response,
6255
6264
  * so we lift each hit's `_source` (already in KRChatbotSkill shape) out.
6256
6265
  */
6257
- _searchSkills(query) {
6258
- this._skillRequestId++;
6259
- const requestId = this._skillRequestId;
6266
+ _searchOrgSkills(query) {
6267
+ this._orgSkillRequestId++;
6268
+ const requestId = this._orgSkillRequestId;
6260
6269
  const trimmed = query.trim();
6261
6270
  // An empty query matches everything; otherwise substring-match the query
6262
6271
  // against name/code/description. We use case-insensitive wildcard queries on
@@ -6306,6 +6315,11 @@
6306
6315
  bool: {
6307
6316
  must: [must],
6308
6317
  filter: [{ term: { active: true } }],
6318
+ // The "Your Org" tab lists only this instance's own skills. Globally-
6319
+ // published skills belong to the "Kodaris" tab (fed by the registry),
6320
+ // so exclude them here. Documents without the field aren't matched by
6321
+ // the term, so ordinary org skills are kept.
6322
+ must_not: [{ term: { globalPublished: true } }],
6309
6323
  },
6310
6324
  },
6311
6325
  },
@@ -6313,17 +6327,106 @@
6313
6327
  .then((r) => r.json())
6314
6328
  .then((res) => {
6315
6329
  // Ignore a stale response superseded by a newer search.
6316
- if (requestId !== this._skillRequestId) {
6330
+ if (requestId !== this._orgSkillRequestId) {
6317
6331
  return;
6318
6332
  }
6319
- this._skillResults = (res?.data?.hits?.hits || []).map((hit) => hit._source);
6333
+ this._orgSkills = (res?.data?.hits?.hits || []).map((hit) => hit._source);
6320
6334
  })
6321
6335
  .catch((error) => {
6322
6336
  console.error('kr-chatbot: skill search failed', error);
6323
6337
  });
6324
6338
  }
6325
6339
  /**
6326
- * Search the shared Kodaris global-skill registry into `_globalSkillResults`
6340
+ * Search skills for the `/` composer menu into `_invocableSkills`: this instance's
6341
+ * own org skills PLUS the global skills installed on it, since both are
6342
+ * invocable by code. They come from two indexes (the local aiskills search and
6343
+ * the registry search filtered client-side to enabled), so we fire both and
6344
+ * concat — org skills first, then installed globals. Skill codes are unique
6345
+ * across the two, so no dedup is needed. Kept out of `_orgSkills` so
6346
+ * installed globals never leak into the browse dialog's "Your Org" tab.
6347
+ * (Cross-list relevance ranking is a later concern; for now org skills lead.)
6348
+ */
6349
+ _searchInvocableSkills(query) {
6350
+ this._invocableSkillRequestId++;
6351
+ const requestId = this._invocableSkillRequestId;
6352
+ // Same wildcard matching as _searchOrgSkills (see there for the rationale).
6353
+ let must = { match_all: {} };
6354
+ if (query.trim()) {
6355
+ must = {
6356
+ bool: {
6357
+ should: [
6358
+ {
6359
+ wildcard: {
6360
+ 'name.raw': {
6361
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
6362
+ case_insensitive: true,
6363
+ }
6364
+ }
6365
+ },
6366
+ {
6367
+ wildcard: {
6368
+ 'code.raw': {
6369
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
6370
+ case_insensitive: true,
6371
+ }
6372
+ }
6373
+ },
6374
+ {
6375
+ wildcard: {
6376
+ 'description.raw': {
6377
+ value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
6378
+ case_insensitive: true,
6379
+ }
6380
+ }
6381
+ },
6382
+ ],
6383
+ minimum_should_match: 1,
6384
+ },
6385
+ };
6386
+ }
6387
+ // Org skills — this instance's own catalog (globalPublished excluded).
6388
+ const orgSearch = krubbleHttp.Http.fetch({
6389
+ url: '/api/system/opensearch/aiskills/search',
6390
+ method: 'POST',
6391
+ body: {
6392
+ size: 100,
6393
+ query: {
6394
+ bool: {
6395
+ must: [must],
6396
+ filter: [{ term: { active: true } }],
6397
+ must_not: [{ term: { globalPublished: true } }],
6398
+ },
6399
+ },
6400
+ },
6401
+ })
6402
+ .then((r) => r.json())
6403
+ .then((res) => (res?.data?.hits?.hits || []).map((hit) => hit._source))
6404
+ .catch((error) => {
6405
+ console.error('kr-chatbot: invocable skill search failed', error);
6406
+ return [];
6407
+ });
6408
+ // Installed globals — the registry catalog, kept only where enabled=true.
6409
+ const globalSearch = krubbleHttp.Http.fetch({
6410
+ url: '/api/system/opensearch/aiskills/global/search',
6411
+ method: 'POST',
6412
+ body: { query: query.trim() },
6413
+ })
6414
+ .then((r) => r.json())
6415
+ .then((res) => (res?.data || []).filter((s) => !!s.enabled))
6416
+ .catch((error) => {
6417
+ console.error('kr-chatbot: installed global skill search failed', error);
6418
+ return [];
6419
+ });
6420
+ Promise.all([orgSearch, globalSearch]).then(([orgSkills, globalSkills]) => {
6421
+ // Ignore a stale response superseded by a newer search.
6422
+ if (requestId !== this._invocableSkillRequestId) {
6423
+ return;
6424
+ }
6425
+ this._invocableSkills = [...orgSkills, ...globalSkills];
6426
+ });
6427
+ }
6428
+ /**
6429
+ * Search the shared Kodaris global-skill registry into `_globalSkills`
6327
6430
  * (the browse dialog's "Kodaris" tab). Unlike `/aiskills/search`,
6328
6431
  * `/aiskills/global/search` returns a digested list
6329
6432
  * (`data: [{ code, name, description, enabled }]`) already in `KRChatbotSkill`
@@ -6344,7 +6447,7 @@
6344
6447
  if (requestId !== this._globalSkillRequestId) {
6345
6448
  return;
6346
6449
  }
6347
- this._globalSkillResults = (res?.data || []);
6450
+ this._globalSkills = (res?.data || []);
6348
6451
  })
6349
6452
  .catch((error) => {
6350
6453
  console.error('kr-chatbot: global skill search failed', error);
@@ -6356,13 +6459,16 @@
6356
6459
  * matching card. Ignores clicks while a request for the same code is pending.
6357
6460
  */
6358
6461
  _handleGlobalSkillEnable(skill, e) {
6359
- // The card body inserts the skill; the `+` button must not also do that.
6462
+ // The card body inserts the skill; the install button must not also do that.
6360
6463
  e.stopPropagation();
6361
6464
  if (this._globalSkillPending.has(skill.code)) {
6362
6465
  return;
6363
6466
  }
6467
+ // Drive the clicked button's own spinner (KRButton disables itself while
6468
+ // loading, so a second click can't fire until the request settles).
6469
+ const btn = e.currentTarget;
6364
6470
  this._globalSkillPending.add(skill.code);
6365
- this.requestUpdate();
6471
+ btn?.showLoading();
6366
6472
  krubbleHttp.Http.fetch({
6367
6473
  url: `/api/system/opensearch/aiskills/global/${encodeURIComponent(skill.code)}/enabled`,
6368
6474
  method: 'POST',
@@ -6370,16 +6476,18 @@
6370
6476
  })
6371
6477
  .then((r) => r.json())
6372
6478
  .then((res) => {
6373
- // Reflect the server's returned flag on the card.
6479
+ // Reflect the server's returned flag on the card. The button label/style
6480
+ // re-render from skill.enabled; reset() clears the spinner to idle.
6374
6481
  skill.enabled = res?.data?.enabled;
6482
+ btn?.reset();
6375
6483
  this.requestUpdate();
6376
6484
  })
6377
6485
  .catch((error) => {
6378
6486
  console.error('kr-chatbot: global skill enable failed', error);
6487
+ btn?.showError('Error');
6379
6488
  })
6380
6489
  .finally(() => {
6381
6490
  this._globalSkillPending.delete(skill.code);
6382
- this.requestUpdate();
6383
6491
  });
6384
6492
  }
6385
6493
  /** The `+` button toggles the skill/attachment menu open (showing all skills). */
@@ -6391,7 +6499,7 @@
6391
6499
  this._skillQuery = '';
6392
6500
  this._skillHighlighted = 0;
6393
6501
  this._skillMenuOpened = true;
6394
- this._searchSkills('');
6502
+ this._searchInvocableSkills('');
6395
6503
  }
6396
6504
  /** Insert `/code ` into the composer and focus it, ready for the message. */
6397
6505
  _handleSkillSelect(skill) {
@@ -6424,7 +6532,7 @@
6424
6532
  this._skillBrowseQuery = '';
6425
6533
  this._skillBrowseOpened = true;
6426
6534
  // Fill both tabs up front so switching between them is instant, not stale.
6427
- this._searchSkills('');
6535
+ this._searchOrgSkills('');
6428
6536
  this._searchGlobalSkills('');
6429
6537
  }
6430
6538
  _handleSkillBrowseClose() {
@@ -6433,7 +6541,7 @@
6433
6541
  _handleSkillBrowseSearch(e) {
6434
6542
  this._skillBrowseQuery = e.target.value;
6435
6543
  // Filter both catalogs so whichever tab the user switches to is already current.
6436
- this._searchSkills(this._skillBrowseQuery);
6544
+ this._searchOrgSkills(this._skillBrowseQuery);
6437
6545
  this._searchGlobalSkills(this._skillBrowseQuery);
6438
6546
  }
6439
6547
  _handleSkillBrowseTabChange(e) {
@@ -6878,7 +6986,7 @@
6878
6986
  // --- Render ---
6879
6987
  /** The `/` skill menu shown above the composer while typing a slash command. */
6880
6988
  _renderSkillMenu() {
6881
- const skills = this._skillResults;
6989
+ const skills = this._invocableSkills;
6882
6990
  // Debug test triggers are extra actions after Attach Files / Browse. The
6883
6991
  // flat keyboard index counts actions first, then skills, so the skills'
6884
6992
  // index offset is the number of actions shown.
@@ -6916,7 +7024,7 @@
6916
7024
  })}
6917
7025
  @click=${() => this._activateSkillMenuItem(2)}
6918
7026
  >
6919
- Test: context warning
7027
+ Test Context Warning
6920
7028
  </button>
6921
7029
  <button
6922
7030
  class=${e({
@@ -6925,7 +7033,7 @@
6925
7033
  })}
6926
7034
  @click=${() => this._activateSkillMenuItem(3)}
6927
7035
  >
6928
- Test: context limit
7036
+ Test Context Limit
6929
7037
  </button>
6930
7038
  ` : A}
6931
7039
  </div>
@@ -6985,69 +7093,71 @@
6985
7093
  /**
6986
7094
  * The card grid of skill search results for the browse dialog. Clicking a card
6987
7095
  * inserts `/code ` into the composer. In the shared-Kodaris (`global`) tab each
6988
- * card also gets a `+` install button that enables the skill on this instance
6989
- * (turning into a ✓ once installed).
7096
+ * card also gets a footer row with an Enabled toggle that installs/removes the
7097
+ * skill on this instance.
6990
7098
  */
6991
7099
  _renderSkillGrid(global = false) {
6992
- let skills = this._skillResults;
7100
+ let skills = this._orgSkills;
6993
7101
  if (global) {
6994
- skills = this._globalSkillResults;
7102
+ skills = this._globalSkills;
6995
7103
  }
6996
7104
  if (!skills.length) {
6997
7105
  return A;
6998
7106
  }
6999
7107
  return b `
7000
7108
  <div class="skill-browse__grid">
7001
- ${skills.map((skill) => b `
7002
- <div
7003
- class="skill-card"
7004
- role="button"
7005
- tabindex="0"
7006
- @click=${() => this._handleSkillSelect(skill)}
7007
- @keydown=${(e) => this._handleSkillCardKeydown(e, skill)}
7008
- >
7009
- <span class="skill-card__name">${skill.name}</span>
7010
- ${skill.description ? b `
7011
- <span class="skill-card__desc">${skill.description}</span>
7012
- ` : A}
7013
- ${global ? this._renderSkillInstall(skill) : A}
7014
- </div>
7015
- `)}
7109
+ ${skills.map((skill) => {
7110
+ // A global skill must be installed before it can be chosen — inserting
7111
+ // /code for a skill not enabled on this instance would just fail. So an
7112
+ // uninstalled global card does nothing on click; only its Install button
7113
+ // is interactive.
7114
+ const selectable = !global || !!skill.enabled;
7115
+ return b `
7116
+ <div
7117
+ class="skill-card"
7118
+ role=${selectable ? 'button' : A}
7119
+ tabindex=${selectable ? 0 : A}
7120
+ @click=${selectable ? () => this._handleSkillSelect(skill) : A}
7121
+ @keydown=${selectable ? (e) => this._handleSkillCardKeydown(e, skill) : A}
7122
+ >
7123
+ <span class="skill-card__name">${skill.name}</span>
7124
+ ${skill.description ? b `
7125
+ <span class="skill-card__desc">${skill.description}</span>
7126
+ ` : A}
7127
+ ${global ? this._renderSkillToggle(skill) : A}
7128
+ </div>
7129
+ `;
7130
+ })}
7016
7131
  </div>
7017
7132
  `;
7018
7133
  }
7134
+ /** Keep a click on the card footer from bubbling to the card's insert handler. */
7135
+ _stopFooterClick(e) {
7136
+ e.stopPropagation();
7137
+ }
7019
7138
  /**
7020
- * The install affordance on a global skill card: a `+` button that enables the
7021
- * skill on this instance, showing a ✓ once installed and a disabled state
7022
- * while the enable/disable request is in flight.
7139
+ * The install affordance on a global skill card: a footer row with an
7140
+ * install/remove button. The button owns its own spinner via KRButton's
7141
+ * showLoading()/showError() while the enable/disable request is in flight, so
7142
+ * the async call has clear feedback. Clicks are stopped from bubbling so the
7143
+ * button never also inserts the skill (the card body does that).
7023
7144
  */
7024
- _renderSkillInstall(skill) {
7145
+ _renderSkillToggle(skill) {
7146
+ const on = !!skill.enabled;
7025
7147
  return b `
7026
- <button
7027
- class=${e({
7028
- 'skill-card__install': true,
7029
- 'skill-card__install--installed': !!skill.enabled,
7030
- })}
7031
- ?disabled=${this._globalSkillPending.has(skill.code)}
7032
- title=${!!skill.enabled ? 'Installed — click to remove' : 'Install this skill'}
7033
- aria-label=${!!skill.enabled ? 'Remove skill' : 'Install skill'}
7034
- @click=${(e) => this._handleGlobalSkillEnable(skill, e)}
7148
+ <div
7149
+ class="skill-card__footer"
7150
+ @click=${this._stopFooterClick}
7035
7151
  >
7036
- <svg
7037
- xmlns="http://www.w3.org/2000/svg"
7038
- fill="none"
7039
- viewBox="0 0 24 24"
7040
- stroke-width="2"
7041
- stroke="currentColor"
7042
- class="skill-card__install-icon"
7043
- >
7044
- <path
7045
- stroke-linecap="round"
7046
- stroke-linejoin="round"
7047
- d=${!!skill.enabled ? 'm4.5 12.75 6 6 9-13.5' : 'M12 4.5v15m7.5-7.5h-15'}
7048
- />
7049
- </svg>
7050
- </button>
7152
+ <span class="skill-card__price">Free</span>
7153
+ <kr-button
7154
+ class="skill-card__install-btn"
7155
+ size="small"
7156
+ variant=${on ? 'outline' : 'flat'}
7157
+ color=${on ? 'secondary' : 'primary'}
7158
+ @click=${(e) => this._handleGlobalSkillEnable(skill, e)}
7159
+ >${on ? 'Uninstall' : 'Install'}</kr-button>
7160
+ </div>
7051
7161
  `;
7052
7162
  }
7053
7163
  /**
@@ -7508,9 +7618,7 @@
7508
7618
  </svg>
7509
7619
  </button>
7510
7620
  </div>
7511
- ` : A}
7512
-
7513
- ${this._contextLimitReached ? b `
7621
+ ` : this._contextLimitReached ? b `
7514
7622
  <div class="context-limit" role="alert">
7515
7623
  ${this._contextLimitMessage
7516
7624
  || 'This conversation has reached its limit. Please start a new one to continue.'}
@@ -8096,14 +8204,22 @@
8096
8204
 
8097
8205
  /* Context warning banner (user-facing, dismissible, non-blocking) */
8098
8206
  .context-warning {
8099
- padding: 8px 16px;
8207
+ /* Match the message text column: 768px cap minus its 32px side padding
8208
+ (704px), and viewport minus that padding on narrower screens. */
8209
+ width: 100%;
8210
+ max-width: 735px;
8211
+ margin: 8px auto 0;
8212
+ padding: 10px 14px;
8213
+ border: 1px solid #fde68a;
8214
+ border-radius: 8px;
8100
8215
  background: #fffbeb;
8101
8216
  color: #92400e;
8102
8217
  font-size: 13px;
8218
+ line-height: 1.4;
8103
8219
  display: flex;
8104
8220
  align-items: center;
8105
8221
  justify-content: space-between;
8106
- gap: 8px;
8222
+ gap: 10px;
8107
8223
  flex-shrink: 0;
8108
8224
  }
8109
8225
 
@@ -8116,6 +8232,11 @@
8116
8232
  display: flex;
8117
8233
  align-items: center;
8118
8234
  flex-shrink: 0;
8235
+ opacity: 0.7;
8236
+ }
8237
+
8238
+ .context-warning-dismiss:hover {
8239
+ opacity: 1;
8119
8240
  }
8120
8241
 
8121
8242
  .context-warning-dismiss-icon {
@@ -8125,11 +8246,18 @@
8125
8246
 
8126
8247
  /* Context-limit notice (user-facing, blocking — steers to a new conversation) */
8127
8248
  .context-limit {
8128
- padding: 10px 16px;
8249
+ /* Match the message text column: 768px cap minus its 32px side padding
8250
+ (704px), and viewport minus that padding on narrower screens. */
8251
+ width: 100%;
8252
+ max-width: 735px;
8253
+ margin: 8px auto 0;
8254
+ padding: 10px 14px;
8255
+ border: 1px solid #fecaca;
8256
+ border-radius: 8px;
8129
8257
  background: var(--kr-chatbot-error-bg);
8130
8258
  color: var(--kr-chatbot-error-text);
8131
8259
  font-size: 13px;
8132
- line-height: 1.5;
8260
+ line-height: 1.4;
8133
8261
  flex-shrink: 0;
8134
8262
  }
8135
8263
 
@@ -8657,7 +8785,7 @@
8657
8785
  height: 100%;
8658
8786
  overflow-y: auto;
8659
8787
  display: grid;
8660
- grid-template-columns: repeat(3, 1fr);
8788
+ grid-template-columns: repeat(2, 1fr);
8661
8789
  grid-auto-rows: min-content;
8662
8790
  align-content: start;
8663
8791
  gap: 12px;
@@ -8685,53 +8813,31 @@
8685
8813
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
8686
8814
  }
8687
8815
 
8688
- /* Install button on global skill cards. Absolutely positioned in the corner
8689
- so it sits above the card's own click target; leave room via the name's
8690
- padding-right below. */
8691
- .skill-card__install {
8692
- position: absolute;
8693
- top: 10px;
8694
- right: 10px;
8695
- display: inline-flex;
8816
+ /* Footer row on global skill cards: install button on the left, price on the
8817
+ right. margin-top:auto pins it to the bottom so cards in a row align their
8818
+ footers regardless of description length. Negative horizontal margins
8819
+ (matching the card's 14px padding) pull the top border out to the full
8820
+ card width; the content is re-padded horizontally to stay inset. */
8821
+ .skill-card__footer {
8822
+ display: flex;
8696
8823
  align-items: center;
8697
- justify-content: center;
8698
- width: 24px;
8699
- height: 24px;
8700
- padding: 0;
8701
- border: 1px solid #d1d5db;
8702
- border-radius: 6px;
8703
- background: white;
8704
- color: #374151;
8705
- cursor: pointer;
8706
- transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
8707
- }
8708
-
8709
- .skill-card__install:hover:not(:disabled) {
8710
- border-color: var(--kr-chatbot-primary);
8711
- color: var(--kr-chatbot-primary);
8712
- }
8713
-
8714
- .skill-card__install:disabled {
8715
- opacity: 0.5;
8716
- cursor: default;
8717
- }
8718
-
8719
- /* Installed state: filled check. */
8720
- .skill-card__install--installed {
8721
- background: var(--kr-chatbot-primary);
8722
- border-color: var(--kr-chatbot-primary);
8723
- color: white;
8824
+ justify-content: space-between;
8825
+ gap: 8px;
8826
+ margin-top: 8px;
8827
+ margin-left: -14px;
8828
+ margin-right: -14px;
8829
+ padding: 14px 14px 0;
8830
+ border-top: 1px solid #e5e7eb;
8724
8831
  }
8725
8832
 
8726
- .skill-card__install-icon {
8727
- width: 15px;
8728
- height: 15px;
8833
+ .skill-card__price {
8834
+ font-size: 13px;
8835
+ font-weight: 600;
8836
+ color: #000000;
8729
8837
  }
8730
8838
 
8731
8839
  .skill-card__name {
8732
8840
  font-weight: 600;
8733
- /* Leave room for the corner install button on global cards. */
8734
- padding-right: 28px;
8735
8841
  }
8736
8842
 
8737
8843
  .skill-card__desc {
@@ -8920,10 +9026,13 @@
8920
9026
  ], exports.Chatbot.prototype, "_skillQuery", void 0);
8921
9027
  __decorate([
8922
9028
  r()
8923
- ], exports.Chatbot.prototype, "_skillResults", void 0);
9029
+ ], exports.Chatbot.prototype, "_orgSkills", void 0);
9030
+ __decorate([
9031
+ r()
9032
+ ], exports.Chatbot.prototype, "_globalSkills", void 0);
8924
9033
  __decorate([
8925
9034
  r()
8926
- ], exports.Chatbot.prototype, "_globalSkillResults", void 0);
9035
+ ], exports.Chatbot.prototype, "_invocableSkills", void 0);
8927
9036
  __decorate([
8928
9037
  r()
8929
9038
  ], exports.Chatbot.prototype, "_aiFrameworkConfig", void 0);