colana 1.0.0-beta.39 → 1.0.0-beta.40

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "colana",
3
- "version": "1.0.0-beta.39",
3
+ "version": "1.0.0-beta.40",
4
4
  "description": "Agent-First. Multiplied. Multi-agent command center for AI coding agents.",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
package/public/app.js CHANGED
@@ -1512,19 +1512,51 @@ async function sendPersonalChatMessage() {
1512
1512
  if (textSpan) textSpan.remove();
1513
1513
 
1514
1514
  assistantBubble.className = 'chat-bubble error';
1515
- const errorText = err.fix ? `${err.message} ${err.fix}` : (err.message || 'Failed to get response');
1516
- assistantBubble.textContent = errorText;
1517
-
1518
- const catchSettingsLink = document.createElement('a');
1519
- catchSettingsLink.href = '#';
1520
- catchSettingsLink.className = 'chat-error-settings-link';
1521
- catchSettingsLink.textContent = 'Open Settings';
1522
- catchSettingsLink.addEventListener('click', (e) => {
1523
- e.preventDefault();
1524
- openSettingsModal();
1525
- });
1526
- assistantBubble.appendChild(document.createTextNode(' \u2014 '));
1527
- assistantBubble.appendChild(catchSettingsLink);
1515
+
1516
+ const isAuthError = err.code === 'GATEWAY_AUTH_FAILED' || err.code === 'GATEWAY_AUTH_MISSING';
1517
+
1518
+ if (isAuthError) {
1519
+ // Auth-specific error: show actionable message with "Add API Key" button
1520
+ const msgSpan = document.createElement('span');
1521
+ msgSpan.textContent = err.message || 'No valid API key found for your model provider.';
1522
+ assistantBubble.appendChild(msgSpan);
1523
+
1524
+ const addKeyBtn = document.createElement('button');
1525
+ addKeyBtn.className = 'chat-error-action-btn';
1526
+ addKeyBtn.textContent = 'Add API Key';
1527
+ addKeyBtn.addEventListener('click', (e) => {
1528
+ e.preventDefault();
1529
+ openSettingsModal();
1530
+ // Scroll to Personal AI Agents section after modal opens
1531
+ setTimeout(() => {
1532
+ const modelSection = document.getElementById('openclaw-model-config');
1533
+ if (modelSection) modelSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
1534
+ }, 300);
1535
+ });
1536
+ assistantBubble.appendChild(addKeyBtn);
1537
+
1538
+ if (err.fix) {
1539
+ const fixHint = document.createElement('div');
1540
+ fixHint.className = 'chat-error-fix-hint';
1541
+ fixHint.textContent = err.fix;
1542
+ assistantBubble.appendChild(fixHint);
1543
+ }
1544
+ } else {
1545
+ // Generic error: existing behavior
1546
+ const errorText = err.fix ? `${err.message} ${err.fix}` : (err.message || 'Failed to get response');
1547
+ assistantBubble.textContent = errorText;
1548
+
1549
+ const catchSettingsLink = document.createElement('a');
1550
+ catchSettingsLink.href = '#';
1551
+ catchSettingsLink.className = 'chat-error-settings-link';
1552
+ catchSettingsLink.textContent = 'Open Settings';
1553
+ catchSettingsLink.addEventListener('click', (e) => {
1554
+ e.preventDefault();
1555
+ openSettingsModal();
1556
+ });
1557
+ assistantBubble.appendChild(document.createTextNode(' \u2014 '));
1558
+ assistantBubble.appendChild(catchSettingsLink);
1559
+ }
1528
1560
 
1529
1561
  const time = document.createElement('span');
1530
1562
  time.className = 'chat-time';
@@ -7530,6 +7562,52 @@ async function loadOpenClawModelConfig() {
7530
7562
  setOpenClawModel(selectedModel, applyBtn);
7531
7563
  };
7532
7564
 
7565
+ // "Update API key" link — always visible for available models
7566
+ // Covers the case where OpenClaw CLI reports a model as available
7567
+ // but the gateway auth-profiles.json has a stale or missing key
7568
+ const updateKeyLink = document.createElement('a');
7569
+ updateKeyLink.href = '#';
7570
+ updateKeyLink.className = 'openclaw-update-key-link';
7571
+ updateKeyLink.textContent = 'Update API key for this provider';
7572
+ updateKeyLink.style.display = 'none';
7573
+ updateKeyLink.addEventListener('click', (e) => {
7574
+ e.preventDefault();
7575
+ const selectedModel = select.value;
7576
+ const info = modelAvailability[selectedModel];
7577
+ const authRowEl = document.getElementById('openclaw-auth-inline');
7578
+ const envVar = (info && providerEnvMap) ? providerEnvMap[info.provider] : null;
7579
+ if (envVar) {
7580
+ document.getElementById('openclaw-auth-label').textContent = `${envVar}:`;
7581
+ document.getElementById('openclaw-auth-key-input').value = '';
7582
+ document.getElementById('openclaw-auth-key-input').placeholder = `Enter ${info.provider} API key...`;
7583
+ }
7584
+ authRowEl.classList.add('visible');
7585
+ setTimeout(() => {
7586
+ authRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
7587
+ const keyInput = document.getElementById('openclaw-auth-key-input');
7588
+ if (keyInput) keyInput.focus();
7589
+ }, 50);
7590
+ });
7591
+ container.appendChild(updateKeyLink);
7592
+
7593
+ // Show/hide the update key link based on model selection
7594
+ const updateKeyLinkVisibility = () => {
7595
+ const selectedModel = select.value;
7596
+ const info = modelAvailability[selectedModel];
7597
+ // Show link when a model is selected and has a known provider with env var mapping,
7598
+ // but only when the auth row is NOT already mandatorily visible (i.e. model is available)
7599
+ const authRowEl = document.getElementById('openclaw-auth-inline');
7600
+ const authVisible = authRowEl && authRowEl.classList.contains('visible');
7601
+ if (info && info.available && providerEnvMap && providerEnvMap[info.provider]) {
7602
+ updateKeyLink.style.display = authVisible ? 'none' : '';
7603
+ } else {
7604
+ updateKeyLink.style.display = 'none';
7605
+ }
7606
+ };
7607
+ select.addEventListener('change', updateKeyLinkVisibility);
7608
+ // Run once on initial render
7609
+ setTimeout(updateKeyLinkVisibility, 0);
7610
+
7533
7611
  // Save & Apply — save key then set model
7534
7612
  authSaveBtn.onclick = async () => {
7535
7613
  const selectedModel = select.value;
package/public/styles.css CHANGED
@@ -7466,6 +7466,30 @@ select.form-input option {
7466
7466
  color: var(--accent-hover, #7c8aff);
7467
7467
  }
7468
7468
 
7469
+ .chat-error-action-btn {
7470
+ display: inline-block;
7471
+ margin: 8px 0 4px;
7472
+ padding: 5px 14px;
7473
+ background: var(--accent, #6366f1);
7474
+ color: #fff;
7475
+ border: none;
7476
+ border-radius: 6px;
7477
+ font-size: 0.82em;
7478
+ font-weight: 500;
7479
+ cursor: pointer;
7480
+ transition: background 0.15s;
7481
+ }
7482
+ .chat-error-action-btn:hover {
7483
+ background: var(--accent-hover, #7c8aff);
7484
+ }
7485
+
7486
+ .chat-error-fix-hint {
7487
+ margin-top: 4px;
7488
+ font-size: 0.78em;
7489
+ color: var(--text-muted, #94a3b8);
7490
+ font-style: italic;
7491
+ }
7492
+
7469
7493
  /* --- Notification Bubbles --- */
7470
7494
  .chat-bubble.notification {
7471
7495
  align-self: flex-start;
@@ -7831,6 +7855,21 @@ select.form-input option {
7831
7855
  font-family: inherit;
7832
7856
  }
7833
7857
 
7858
+ .openclaw-update-key-link {
7859
+ display: block;
7860
+ margin: 6px 0 10px;
7861
+ font-size: 0.75rem;
7862
+ color: var(--accent, #6366f1);
7863
+ cursor: pointer;
7864
+ text-decoration: none;
7865
+ opacity: 0.85;
7866
+ transition: opacity 0.15s;
7867
+ }
7868
+ .openclaw-update-key-link:hover {
7869
+ opacity: 1;
7870
+ text-decoration: underline;
7871
+ }
7872
+
7834
7873
  .openclaw-auth-hint {
7835
7874
  font-size: 0.72rem;
7836
7875
  color: var(--text-muted);
@@ -130,8 +130,27 @@ export function registerPersonalAgentRoutes(app, { sensitiveLimiter }) {
130
130
  console.error(`[personal-agent] Gateway ${gatewayRes.status}: ${errText.slice(0, 500)}`);
131
131
  }
132
132
  const status = gatewayRes.status;
133
+
134
+ // For 401, extract provider from current model for actionable guidance
135
+ if (status === 401) {
136
+ const currentModel = getOpenClawDefaultModel() || '';
137
+ const slash = currentModel.indexOf('/');
138
+ const provider = slash > 0 ? currentModel.substring(0, slash).toLowerCase() : null;
139
+ const providerLabel = provider || null;
140
+ const envVar = provider ? PROVIDER_ENV_MAP[provider] : null;
141
+ return res.status(401).json({
142
+ error: providerLabel
143
+ ? `Authentication failed — your ${providerLabel} API key may be missing or invalid.`
144
+ : 'Authentication failed — your API key may be missing or invalid.',
145
+ code: 'GATEWAY_AUTH_FAILED',
146
+ fix: envVar
147
+ ? `Add or update your ${envVar} in Settings > Personal AI Agents.`
148
+ : `Add your API key in Settings > Personal AI Agents.`,
149
+ provider: provider || null,
150
+ });
151
+ }
152
+
133
153
  const friendlyMessages = {
134
- 401: 'Authentication failed — check your API key configuration',
135
154
  403: 'Access denied by gateway',
136
155
  429: 'Rate limit exceeded — please try again shortly',
137
156
  503: 'Personal agent is temporarily unavailable',