create-walle 0.9.21 → 0.9.22

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.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/package.json +2 -2
  3. package/template/claude-task-manager/api-prompts.js +13 -0
  4. package/template/claude-task-manager/api-reviews.js +5 -2
  5. package/template/claude-task-manager/db.js +348 -15
  6. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  7. package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
  8. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  9. package/template/claude-task-manager/git-utils.js +146 -17
  10. package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
  11. package/template/claude-task-manager/lib/auth-rules.js +3 -0
  12. package/template/claude-task-manager/lib/document-review.js +33 -2
  13. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
  14. package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
  15. package/template/claude-task-manager/lib/restart-guard.js +68 -0
  16. package/template/claude-task-manager/lib/session-standup.js +36 -13
  17. package/template/claude-task-manager/lib/session-stream.js +11 -4
  18. package/template/claude-task-manager/lib/transport-security.js +50 -0
  19. package/template/claude-task-manager/lib/walle-transcript.js +16 -0
  20. package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
  21. package/template/claude-task-manager/public/css/reviews.css +10 -0
  22. package/template/claude-task-manager/public/css/setup.css +13 -0
  23. package/template/claude-task-manager/public/css/walle.css +145 -0
  24. package/template/claude-task-manager/public/index.html +539 -44
  25. package/template/claude-task-manager/public/ipad.html +363 -0
  26. package/template/claude-task-manager/public/js/document-review-links.js +196 -0
  27. package/template/claude-task-manager/public/js/message-renderer.js +14 -3
  28. package/template/claude-task-manager/public/js/reviews.js +30 -6
  29. package/template/claude-task-manager/public/js/setup.js +42 -2
  30. package/template/claude-task-manager/public/js/stream-view.js +20 -1
  31. package/template/claude-task-manager/public/js/walle.js +314 -18
  32. package/template/claude-task-manager/public/m/app.css +789 -11
  33. package/template/claude-task-manager/public/m/app.js +1070 -67
  34. package/template/claude-task-manager/public/m/claim.html +9 -2
  35. package/template/claude-task-manager/public/m/index.html +17 -10
  36. package/template/claude-task-manager/public/m/sw.js +1 -1
  37. package/template/claude-task-manager/server.js +365 -95
  38. package/template/claude-task-manager/session-integrity.js +4 -0
  39. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
  40. package/template/package.json +1 -1
  41. package/template/wall-e/api-walle.js +19 -1
  42. package/template/wall-e/brain.js +152 -6
  43. package/template/wall-e/chat.js +85 -0
  44. package/template/wall-e/coding-orchestrator.js +106 -12
  45. package/template/wall-e/http/model-admin.js +131 -0
  46. package/template/wall-e/lib/service-health.js +194 -0
  47. package/template/wall-e/llm/anthropic.js +7 -0
  48. package/template/wall-e/llm/client.js +46 -12
  49. package/template/wall-e/llm/openai.js +17 -2
  50. package/template/wall-e/llm/portkey-sync.js +201 -0
  51. package/template/wall-e/server.js +13 -0
  52. package/template/website/index.html +10 -10
@@ -2508,7 +2508,7 @@ function _microsoftProbeNoteText(probe, ms) {
2508
2508
  return 'The Mac-side check reached CTM. If the phone still fails, refresh the phone page and check the CTM traffic list below.';
2509
2509
  }
2510
2510
  if (probe.blocked_by_tunnel_auth || probe.diagnosis === 'tunnel_auth_required') {
2511
- return 'The tunnel is private. This Mac-side check is stopped by Microsoft because it is not a signed-in browser session; your phone should sign in with the same Microsoft/GitHub account, then CTM will load.';
2511
+ return 'The tunnel is private. This Mac-side check is stopped by Microsoft because it is not a signed-in browser session; your phone should sign in with the same Microsoft/GitHub account, then CTM will load. If the browser is stuck on the wrong account, use Different Account above and sign in with GitHub.';
2512
2512
  }
2513
2513
  return probe.message || 'Use Recover Now if the tunnel process should be running, then check the URL again.';
2514
2514
  }
@@ -2544,6 +2544,7 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
2544
2544
  var mobileUrl = document.getElementById('setup-ms-mobile-url');
2545
2545
  var inspectUrl = document.getElementById('setup-ms-inspect-url');
2546
2546
  var setupBtn = document.getElementById('setup-ms-setup');
2547
+ var switchLoginBtn = document.getElementById('setup-ms-switch-login');
2547
2548
  var stopBtn = document.getElementById('setup-ms-stop');
2548
2549
  var installCommand = document.getElementById('setup-ms-install-command');
2549
2550
  var loginCommand = document.getElementById('setup-ms-login-command');
@@ -2620,6 +2621,12 @@ function _renderMicrosoftDevTunnelSetup(ms, d) {
2620
2621
  : (_microsoftSetupInFlight ? 'Working...' : (!installed ? 'Set Up' : (!signedIn ? 'Open Sign-In' : 'Start Tunnel')));
2621
2622
  setupBtn.title = ready ? 'Microsoft tunnel phone access is ready to use.' : '';
2622
2623
  }
2624
+ if (switchLoginBtn) {
2625
+ switchLoginBtn.style.display = installed && signedIn ? '' : 'none';
2626
+ switchLoginBtn.disabled = _microsoftSetupInFlight;
2627
+ switchLoginBtn.textContent = 'Use Different Account';
2628
+ switchLoginBtn.title = 'Sign out of the current Dev Tunnels account and start GitHub device sign-in.';
2629
+ }
2623
2630
  if (stopBtn) {
2624
2631
  stopBtn.disabled = !ready;
2625
2632
  }
@@ -3253,7 +3260,7 @@ async function setupMicrosoftTunnel() {
3253
3260
  if (err) err.style.display = 'none';
3254
3261
  if (btn) { btn.disabled = true; btn.textContent = 'Working...'; }
3255
3262
  try {
3256
- var d = _lastNetworkSettings || await loadNetworkSettings() || {};
3263
+ var d = await loadNetworkSettings() || _lastNetworkSettings || {};
3257
3264
  var ms = d.microsoft_dev_tunnel || {};
3258
3265
  if (_microsoftTunnelReady(d)) {
3259
3266
  _setMicrosoftActionStatus('Microsoft tunnel is already ready.', 'ok');
@@ -3312,6 +3319,38 @@ async function startMicrosoftTunnelLogin(deviceCode) {
3312
3319
  }
3313
3320
  }
3314
3321
 
3322
+ async function switchMicrosoftTunnelLogin() {
3323
+ var err = document.getElementById('setup-network-err');
3324
+ var btn = document.getElementById('setup-ms-switch-login');
3325
+ if (err) err.style.display = 'none';
3326
+ if (btn) { btn.disabled = true; btn.textContent = 'Signing out...'; }
3327
+ _setMicrosoftActionStatus('Signing out of the current Dev Tunnels account...', '');
3328
+ try {
3329
+ var logoutRes = await fetch('/api/setup/network/microsoft-dev-tunnel/logout', {
3330
+ method: 'POST',
3331
+ headers: { 'Content-Type': 'application/json' },
3332
+ body: '{}',
3333
+ });
3334
+ var logoutData = await logoutRes.json();
3335
+ if (!logoutRes.ok || !logoutData.ok) throw new Error(logoutData.error || 'Could not sign out of Dev Tunnels');
3336
+ if (_lastNetworkSettings) {
3337
+ _lastNetworkSettings.microsoft_dev_tunnel = logoutData.setup || _lastNetworkSettings.microsoft_dev_tunnel || {};
3338
+ }
3339
+ _renderMicrosoftDevTunnelSetup((_lastNetworkSettings || {}).microsoft_dev_tunnel || {}, _lastNetworkSettings || {});
3340
+ _setMicrosoftActionStatus('Signed out. Starting GitHub sign-in now...', '');
3341
+ if (btn) btn.textContent = 'Opening GitHub...';
3342
+ var loginData = await _postMicrosoftTunnelLogin(true);
3343
+ _renderMicrosoftProgress(loginData.progress || {});
3344
+ _setMicrosoftActionStatus('GitHub sign-in started. Enter the displayed code on the GitHub page, then click Set Up again after it finishes.', 'ok');
3345
+ setupToast('GitHub sign-in started');
3346
+ } catch (e) {
3347
+ if (err) { err.textContent = e.message; err.style.display = 'block'; }
3348
+ _setMicrosoftActionStatus(e.message || 'Could not switch Dev Tunnels account', 'error');
3349
+ } finally {
3350
+ if (btn) { btn.disabled = false; btn.textContent = 'Use Different Account'; }
3351
+ }
3352
+ }
3353
+
3315
3354
  async function startMicrosoftTunnel() {
3316
3355
  var err = document.getElementById('setup-network-err');
3317
3356
  var btn = document.getElementById('setup-ms-setup');
@@ -5263,6 +5302,7 @@ SETUP.setupMicrosoftTunnel = setupMicrosoftTunnel;
5263
5302
  SETUP.startMicrosoftTunnel = startMicrosoftTunnel;
5264
5303
  SETUP.stopMicrosoftTunnel = stopMicrosoftTunnel;
5265
5304
  SETUP.startMicrosoftTunnelLogin = startMicrosoftTunnelLogin;
5305
+ SETUP.switchMicrosoftTunnelLogin = switchMicrosoftTunnelLogin;
5266
5306
  SETUP.toggleMicrosoftKeepAwake = toggleMicrosoftKeepAwake;
5267
5307
  SETUP.recoverMicrosoftTunnel = recoverMicrosoftTunnel;
5268
5308
  SETUP.probeMicrosoftTunnel = probeMicrosoftTunnel;
@@ -496,6 +496,16 @@ function renderConversationEvent(evt) {
496
496
  return MR.renderConversationEvent(evt);
497
497
  }
498
498
 
499
+ function _linkConversationDocumentReferences(container, root) {
500
+ if (!container || !root || !window.CTMDocLinks || typeof window.CTMDocLinks.linkifyElement !== 'function') return;
501
+ try {
502
+ const sessionId = container.dataset?.sessionId || '';
503
+ window.CTMDocLinks.linkifyElement(root, window.CTMDocLinks.contextForSession(sessionId));
504
+ } catch (e) {
505
+ console.warn('[stream-view] document linkification failed:', e && e.message ? e.message : e);
506
+ }
507
+ }
508
+
499
509
  // When the new element AND the last existing child are compatible collapsible
500
510
  // operational rows (`.conv-tool-group` or `.conv-warning-group`), fold the new
501
511
  // one into the previous one's items list and bump the count + last-time.
@@ -718,6 +728,7 @@ function _appendConversationEventToTurns(container, evt, opts) {
718
728
  const turn = MR.createConversationTurn(evt, { expanded: !!opts.expandPrompt });
719
729
  _removeConversationState(container);
720
730
  container.appendChild(turn);
731
+ _linkConversationDocumentReferences(container, turn);
721
732
  if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
722
733
  return turn;
723
734
  }
@@ -730,7 +741,9 @@ function _appendConversationEventToTurns(container, evt, opts) {
730
741
  if (!body) return null;
731
742
  _removePromptTurnEmpty(body);
732
743
  if (evt.data?.parentUuid) _assignConversationParentUuid(el, evt.data.parentUuid);
733
- if (!_mergeConsecutiveToolGroups(body, el)) body.appendChild(el);
744
+ const merged = _mergeConsecutiveToolGroups(body, el);
745
+ if (!merged) body.appendChild(el);
746
+ _linkConversationDocumentReferences(container, merged ? body : el);
734
747
  if (typeof MR.refreshPromptTurnMeta === 'function') MR.refreshPromptTurnMeta(turn);
735
748
  return el;
736
749
  }
@@ -754,6 +767,7 @@ function populateConversationView(container, events) {
754
767
  if (_mergeConsecutiveToolGroups(container, el)) continue;
755
768
  container.appendChild(el);
756
769
  }
770
+ _linkConversationDocumentReferences(container, container);
757
771
  if (_isPromptTurnContainer(container)) _refreshLatestPromptTurnStatus(container.dataset?.sessionId || '', container);
758
772
  container.scrollTop = container.scrollHeight;
759
773
  }
@@ -1364,6 +1378,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1364
1378
  : renderConversationEvent(msg);
1365
1379
  if (newEl) {
1366
1380
  _assignConversationParentUuid(newEl, parentUuid);
1381
+ _linkConversationDocumentReferences(container, newEl);
1367
1382
  _replaceConversationParentEvent(existing, newEl);
1368
1383
  } else {
1369
1384
  _replaceConversationParentEvent(existing, null); // Event became empty after update
@@ -1385,6 +1400,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1385
1400
  : renderConversationEvent(msg);
1386
1401
  if (newEl) {
1387
1402
  _assignConversationParentUuid(newEl, parentUuid);
1403
+ _linkConversationDocumentReferences(container, newEl);
1388
1404
  _replaceConversationParentEvent(existing, newEl);
1389
1405
  }
1390
1406
  refreshLatestPromptStatus();
@@ -1399,6 +1415,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1399
1415
  : renderConversationEvent(msg);
1400
1416
  if (newEl) {
1401
1417
  _assignConversationParentUuid(newEl, parentUuid);
1418
+ _linkConversationDocumentReferences(container, newEl);
1402
1419
  _replaceConversationParentEvent(existing, newEl);
1403
1420
  seen.add(parentUuid);
1404
1421
  }
@@ -1425,6 +1442,7 @@ function _applyStreamEvent(sessionId, container, msg) {
1425
1442
  } else {
1426
1443
  container.appendChild(el);
1427
1444
  }
1445
+ _linkConversationDocumentReferences(container, container);
1428
1446
  // Phase 4 reconciliation: respect the global "Hide tool calls" toggle
1429
1447
  // for newly-streamed tool-only rows so the user's preference applies
1430
1448
  // immediately to live updates, not just to the next full re-render.
@@ -1711,6 +1729,7 @@ async function _loadOlder(sessionId, convView) {
1711
1729
  while (pageHost.firstChild) frag.appendChild(pageHost.firstChild);
1712
1730
  // Prepend below the bar so the bar stays at the very top.
1713
1731
  convView.insertBefore(frag, bar.nextSibling);
1732
+ _linkConversationDocumentReferences(convView, convView);
1714
1733
  const newScrollHeight = convView.scrollHeight;
1715
1734
  convView.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight);
1716
1735
  // Merge new parentUuids into the dedup set so live events for primed
@@ -8,6 +8,7 @@ let _brainReloadTimer = null;
8
8
  let _providerStatus = null;
9
9
  let _providerPollTimer = null;
10
10
  let _cachedActiveModel = null; // { model, provider } from /api/setup/status
11
+ let _cachedSetupStatus = null;
11
12
  let _activeProviderIssue = null;
12
13
 
13
14
  const WalleCore = window.WalleCore || {};
@@ -1927,6 +1928,7 @@ WE.renderChat = function() {
1927
1928
  // Fetch active model for badge (once, then cached)
1928
1929
  if (!_cachedActiveModel) {
1929
1930
  fetch('/api/setup/status').then(function(r) { return r.json(); }).then(function(d) {
1931
+ _cachedSetupStatus = d || null;
1930
1932
  if (d.walle_model || d.walle_provider) {
1931
1933
  _cachedActiveModel = { model: d.walle_model || '', provider: d.walle_provider || '' };
1932
1934
  var badge = document.querySelector('.we-model-badge');
@@ -6464,22 +6466,15 @@ checkProviderStatus();
6464
6466
 
6465
6467
  var _alertsPollTimer = null;
6466
6468
  var _serviceAlerts = [];
6469
+ var _serviceHealth = null;
6467
6470
  var _updatePromptShownVersions = {};
6468
6471
 
6469
6472
  function checkServiceAlerts() {
6470
6473
  api('/alerts').then(function(d) {
6471
6474
  var alerts = d.alerts || [];
6472
6475
  _serviceAlerts = alerts;
6473
- var providerIssue = null;
6474
- for (var i = 0; i < alerts.length; i++) {
6475
- var issue = _normalizeProviderIssue(alerts[i]);
6476
- if (issue) providerIssue = issue;
6477
- }
6478
- if (providerIssue) {
6479
- _activeProviderIssue = providerIssue;
6480
- if (currentView === 'chat') renderChatUI();
6481
- }
6482
- renderServiceAlerts(alerts);
6476
+ _serviceHealth = d.summary || buildClientServiceHealth(alerts);
6477
+ renderServiceAlerts(alerts, _serviceHealth);
6483
6478
  maybeShowUpdateWizard(alerts);
6484
6479
  clearTimeout(_alertsPollTimer);
6485
6480
  _alertsPollTimer = setTimeout(checkServiceAlerts, 60000);
@@ -6489,6 +6484,131 @@ function checkServiceAlerts() {
6489
6484
  });
6490
6485
  }
6491
6486
 
6487
+ function alertCreatedAtMs(alert) {
6488
+ var t = Date.parse(alert && alert.created_at || '');
6489
+ return Number.isFinite(t) ? t : 0;
6490
+ }
6491
+
6492
+ function serviceAlertSkillName(alert) {
6493
+ if (!alert) return '';
6494
+ if (alert.skill) return String(alert.skill);
6495
+ var service = String(alert.service || '');
6496
+ if (service && service !== 'ai_provider' && service !== 'system') return service;
6497
+ var msg = String(alert.message || '');
6498
+ var quoted = msg.match(/Skill\s+"([^"]+)"/i);
6499
+ if (quoted) return quoted[1];
6500
+ var named = msg.match(/Skill\s+([A-Za-z0-9_.-]+)/i);
6501
+ return named ? named[1] : '';
6502
+ }
6503
+
6504
+ function serviceAlertProviderIssueType(issue) {
6505
+ return String(issue && issue.type || '').toLowerCase();
6506
+ }
6507
+
6508
+ function serviceAlertIsStickyProviderIssue(issue) {
6509
+ var text = [
6510
+ serviceAlertProviderIssueType(issue),
6511
+ issue && issue.title,
6512
+ issue && issue.message,
6513
+ ].filter(Boolean).join(' ').toLowerCase();
6514
+ return /auth|quota|billing|credit|insufficient|invalid|forbidden|unauthorized|permission/.test(text);
6515
+ }
6516
+
6517
+ function serviceAlertMatchesActiveProvider(issue) {
6518
+ if (!issue) return false;
6519
+ var active = _cachedActiveModel || {};
6520
+ var activeProvider = String(active.provider || '').toLowerCase();
6521
+ var activeModel = String(active.model || '').toLowerCase();
6522
+ var provider = String(issue.provider || '').toLowerCase();
6523
+ var model = String(issue.model || '').toLowerCase();
6524
+ if (activeProvider && provider && activeProvider === provider) return true;
6525
+ if (activeModel && model && activeModel === model) return true;
6526
+ return !activeProvider && !activeModel;
6527
+ }
6528
+
6529
+ function serviceAlertIsCurrentProviderIssue(issue) {
6530
+ if (!serviceAlertMatchesActiveProvider(issue)) return false;
6531
+ if (serviceAlertIsStickyProviderIssue(issue)) return true;
6532
+ var created = Date.parse(issue.createdAt || issue.created_at || '');
6533
+ if (!Number.isFinite(created)) return true;
6534
+ return Date.now() - created <= 6 * 60 * 60 * 1000;
6535
+ }
6536
+
6537
+ function publicServiceAlert(alert) {
6538
+ return {
6539
+ id: alert.id || '',
6540
+ service: alert.service || '',
6541
+ type: alert.type || '',
6542
+ title: alert.title || '',
6543
+ message: alert.message || '',
6544
+ severity: alert.severity || '',
6545
+ action: alert.action || '',
6546
+ action_label: alert.action_label || '',
6547
+ action_url: alert.action_url || '',
6548
+ created_at: alert.created_at || '',
6549
+ skill: serviceAlertSkillName(alert),
6550
+ };
6551
+ }
6552
+
6553
+ function summarizeProviderIssueForHealth(issue) {
6554
+ return {
6555
+ id: issue.id || '',
6556
+ type: issue.type || '',
6557
+ severity: issue.severity || 'error',
6558
+ title: issue.title || 'AI provider issue',
6559
+ message: issue.message || '',
6560
+ provider: issue.provider || '',
6561
+ model: issue.model || '',
6562
+ status: issue.status || '',
6563
+ action_url: issue.actionUrl || issue.action_url || '/setup.html',
6564
+ action_label: issue.actionLabel || issue.action_label || 'Open Setup',
6565
+ created_at: issue.createdAt || issue.created_at || '',
6566
+ };
6567
+ }
6568
+
6569
+ function buildClientServiceHealth(alerts) {
6570
+ alerts = Array.isArray(alerts) ? alerts : [];
6571
+ var providerIssues = alerts.map(_normalizeProviderIssue).filter(Boolean).sort(function(a, b) {
6572
+ return Date.parse(b.createdAt || b.created_at || '') - Date.parse(a.createdAt || a.created_at || '');
6573
+ });
6574
+ var currentIssue = null;
6575
+ for (var i = 0; i < providerIssues.length; i++) {
6576
+ if (serviceAlertIsCurrentProviderIssue(providerIssues[i])) {
6577
+ currentIssue = providerIssues[i];
6578
+ break;
6579
+ }
6580
+ }
6581
+ var disabled = alerts.filter(function(a) { return String(a && a.type || '').toLowerCase() === 'skill_disabled'; }).map(publicServiceAlert);
6582
+ var integrations = alerts.filter(function(a) {
6583
+ return !_normalizeProviderIssue(a) && String(a && a.type || '').toLowerCase() !== 'skill_disabled' && isSessionsIntegrationAlert(a);
6584
+ }).map(publicServiceAlert);
6585
+ var systems = alerts.filter(function(a) {
6586
+ return !_normalizeProviderIssue(a) && String(a && a.type || '').toLowerCase() !== 'skill_disabled' && !isSessionsIntegrationAlert(a);
6587
+ }).map(publicServiceAlert);
6588
+ var history = providerIssues.filter(function(issue) {
6589
+ return !currentIssue || issue.id !== currentIssue.id;
6590
+ }).map(summarizeProviderIssueForHealth);
6591
+ var level = currentIssue ? (currentIssue.severity === 'warning' ? 'warning' : 'error') : ((disabled.length || integrations.length) ? 'warning' : ((history.length || systems.length) ? 'info' : 'ok'));
6592
+ return {
6593
+ level: level,
6594
+ title: currentIssue ? currentIssue.title : (level === 'ok' ? 'Wall-E services healthy' : (level === 'info' ? 'Older service history' : 'Background services need review')),
6595
+ message: currentIssue ? currentIssue.message : (level === 'ok' ? 'No current service blocker.' : 'Review provider history, disabled skills, or integrations below.'),
6596
+ current_issue: currentIssue ? summarizeProviderIssueForHealth(currentIssue) : null,
6597
+ disabled_skills: disabled,
6598
+ integration_alerts: integrations,
6599
+ provider_history: history,
6600
+ system_alerts: systems,
6601
+ counts: {
6602
+ total: alerts.length,
6603
+ current: currentIssue ? 1 : 0,
6604
+ disabled_skills: disabled.length,
6605
+ integration: integrations.length,
6606
+ provider_history: history.length,
6607
+ system: systems.length,
6608
+ },
6609
+ };
6610
+ }
6611
+
6492
6612
  function serviceAlertPresentation(alert) {
6493
6613
  var providerIssue = _normalizeProviderIssue(alert);
6494
6614
  var kind = providerIssue ? 'provider' : (alert.type === 'auth_expired' ? 'error' : alert.type === 'update_available' ? 'info' : 'warning');
@@ -6507,7 +6627,14 @@ function safeServiceAlertId(alert) {
6507
6627
  function serviceAlertActionHtml(alert, safeId) {
6508
6628
  var actionBtn = '';
6509
6629
  var actionLabel = esc(alert.action_label || 'Fix');
6510
- if (alert.action === 'repair_slack_owner') {
6630
+ var providerIssue = _normalizeProviderIssue(alert);
6631
+ if (providerIssue) {
6632
+ actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._testProviderFromAlert(\'' + safeId + '\')">Test</button>'
6633
+ + ' <button class="we-service-alert-action" type="button" onclick="navTo(\'setup\')">Setup</button>';
6634
+ } else if (String(alert.type || '').toLowerCase() === 'skill_disabled') {
6635
+ actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._reviewSkillAlert(\'' + safeId + '\')">Review</button>'
6636
+ + ' <button class="we-service-alert-action" type="button" onclick="WE._enableSkillFromAlert(\'' + safeId + '\')">Re-enable</button>';
6637
+ } else if (alert.action === 'repair_slack_owner') {
6511
6638
  var actionName = esc(alert.action || 'custom').replace(/'/g, '&#39;');
6512
6639
  actionBtn = ' <button class="we-service-alert-action" type="button" onclick="WE._runAlertAction(\'' + safeId + '\', \'' + actionName + '\')">' + actionLabel + '</button>';
6513
6640
  } else if (alert.action === 'gws_reauth') {
@@ -6530,6 +6657,96 @@ function serviceAlertItemHtml(alert) {
6530
6657
  + dismissBtn + '</div>';
6531
6658
  }
6532
6659
 
6660
+ function serviceAlertById(alertId) {
6661
+ for (var i = 0; i < _serviceAlerts.length; i++) {
6662
+ if (String(_serviceAlerts[i].id) === String(alertId)) return _serviceAlerts[i];
6663
+ }
6664
+ return null;
6665
+ }
6666
+
6667
+ function serviceAlertSummaryMeta(health) {
6668
+ var counts = (health && health.counts) || {};
6669
+ var parts = [];
6670
+ if (health && health.current_issue) parts.push('current blocker');
6671
+ if (counts.disabled_skills) parts.push(counts.disabled_skills + ' disabled skill' + (counts.disabled_skills === 1 ? '' : 's'));
6672
+ if (counts.integration) parts.push(counts.integration + ' integration');
6673
+ if (counts.provider_history) parts.push(counts.provider_history + ' older provider');
6674
+ if (counts.system) parts.push(counts.system + ' system');
6675
+ return parts.length ? parts.join(' · ') : 'No alerts';
6676
+ }
6677
+
6678
+ function serviceAlertDisplayTime(value) {
6679
+ var t = Date.parse(value || '');
6680
+ if (!Number.isFinite(t)) return '';
6681
+ try {
6682
+ return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }).format(new Date(t));
6683
+ } catch {
6684
+ return new Date(t).toLocaleString();
6685
+ }
6686
+ }
6687
+
6688
+ function renderHealthIssue(issue) {
6689
+ if (!issue) return '';
6690
+ var safeId = esc(issue.id || '').replace(/'/g, '&#39;');
6691
+ var provider = [issue.provider, issue.model].filter(Boolean).join(' / ');
6692
+ var when = serviceAlertDisplayTime(issue.created_at);
6693
+ return '<div class="we-health-current">'
6694
+ + '<div class="we-health-current-copy">'
6695
+ + '<div class="we-health-current-title">' + esc(issue.title || 'Current provider issue') + '</div>'
6696
+ + '<div class="we-health-current-body">' + esc(issue.message || '') + '</div>'
6697
+ + '<div class="we-health-current-meta">' + esc([provider, when].filter(Boolean).join(' · ')) + '</div>'
6698
+ + '</div>'
6699
+ + '<div class="we-health-current-actions">'
6700
+ + '<button class="walle-btn primary" type="button" onclick="WE._testProviderFromAlert(\'' + safeId + '\')">Test provider</button>'
6701
+ + '<button class="walle-btn" type="button" onclick="navTo(\'setup\')">Open Setup</button>'
6702
+ + '<button class="we-service-alert-dismiss" type="button" onclick="WE._dismissAlert(\'' + safeId + '\')" title="Dismiss">&times;</button>'
6703
+ + '</div>'
6704
+ + '</div>';
6705
+ }
6706
+
6707
+ function renderHealthGroup(title, alerts, opts) {
6708
+ opts = opts || {};
6709
+ if (!alerts || !alerts.length) return '';
6710
+ var open = opts.open ? ' open' : '';
6711
+ var rows = alerts.map(function(alert) {
6712
+ var source = serviceAlertById(alert.id) || alert;
6713
+ return serviceAlertItemHtml(source);
6714
+ }).join('');
6715
+ return '<details class="we-health-group"' + open + '>'
6716
+ + '<summary><span>' + esc(title) + '</span><strong>' + alerts.length + '</strong></summary>'
6717
+ + '<div class="we-health-group-body">' + rows + '</div>'
6718
+ + '</details>';
6719
+ }
6720
+
6721
+ function renderServiceHealthPanel(alerts, health) {
6722
+ health = health || buildClientServiceHealth(alerts);
6723
+ if ((!alerts || !alerts.length) && (!health || health.level === 'ok')) return '';
6724
+ var level = health.level || 'info';
6725
+ var html = '<div id="walle-service-alerts" class="we-service-alerts we-health-card ' + esc(level) + '">';
6726
+ html += '<div class="we-health-header">';
6727
+ html += '<div class="we-health-status-dot" aria-hidden="true"></div>';
6728
+ html += '<div class="we-health-title-block"><div class="we-service-alerts-title">Service Health</div>'
6729
+ + '<div class="we-health-title">' + esc(health.title || 'Wall-E services') + '</div>'
6730
+ + '<div class="we-health-message">' + esc(health.message || '') + '</div></div>';
6731
+ html += '<div class="we-health-meta">' + esc(serviceAlertSummaryMeta(health)) + '</div>';
6732
+ html += '</div>';
6733
+ if (health.current_issue) html += renderHealthIssue(health.current_issue);
6734
+ if (health.integration_alerts && health.integration_alerts.length) {
6735
+ html += renderHealthGroup('Integrations', health.integration_alerts, { open: !health.current_issue });
6736
+ }
6737
+ if (health.disabled_skills && health.disabled_skills.length) {
6738
+ html += renderHealthGroup('Disabled background skills', health.disabled_skills, { open: !health.current_issue && !(health.integration_alerts || []).length });
6739
+ }
6740
+ if (health.provider_history && health.provider_history.length) {
6741
+ html += renderHealthGroup('Older provider history', health.provider_history, { open: false });
6742
+ }
6743
+ if (health.system_alerts && health.system_alerts.length) {
6744
+ html += renderHealthGroup('System alerts', health.system_alerts, { open: !health.current_issue && !(health.integration_alerts || []).length && !(health.disabled_skills || []).length });
6745
+ }
6746
+ html += '</div>';
6747
+ return html;
6748
+ }
6749
+
6533
6750
  function serviceAlertSearchText(alert) {
6534
6751
  return [
6535
6752
  alert && alert.id,
@@ -6580,18 +6797,14 @@ function renderStandupServiceAlerts(alerts) {
6580
6797
  + integrationAlerts.map(serviceAlertItemHtml).join('');
6581
6798
  }
6582
6799
 
6583
- function renderServiceAlerts(alerts) {
6800
+ function renderServiceAlerts(alerts, health) {
6584
6801
  var existing = document.getElementById('walle-service-alerts');
6585
6802
  renderStandupServiceAlerts(alerts);
6586
- if (!alerts || alerts.length === 0) {
6803
+ var html = renderServiceHealthPanel(alerts, health);
6804
+ if (!html) {
6587
6805
  if (existing) existing.remove();
6588
6806
  return;
6589
6807
  }
6590
- var items = alerts.map(serviceAlertItemHtml).join('');
6591
-
6592
- var html = '<div id="walle-service-alerts" class="we-service-alerts">'
6593
- + '<div class="we-service-alerts-title">Service Alerts</div>' + items + '</div>';
6594
-
6595
6808
  if (existing) {
6596
6809
  existing.outerHTML = html;
6597
6810
  } else {
@@ -6730,6 +6943,89 @@ WE._runAlertAction = function(alertId, actionName) {
6730
6943
  });
6731
6944
  };
6732
6945
 
6946
+ function providerForAlert(alert) {
6947
+ var issue = _normalizeProviderIssue(alert) || alert || {};
6948
+ return String(issue.provider || (_cachedActiveModel && _cachedActiveModel.provider) || '').trim();
6949
+ }
6950
+
6951
+ function providerLabelForTest(provider) {
6952
+ if (!provider) return 'provider';
6953
+ if (provider === 'deepseek') return 'DeepSeek';
6954
+ if (provider === 'openai') return 'OpenAI';
6955
+ if (provider === 'moonshot') return 'Moonshot / Kimi';
6956
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
6957
+ }
6958
+
6959
+ function testSetupProvider(provider, alertId) {
6960
+ provider = String(provider || '').trim();
6961
+ if (!provider) {
6962
+ if (typeof showToast === 'function') showToast('No active provider to test', 'var(--yellow, #e0af68)', 2600);
6963
+ return;
6964
+ }
6965
+ var params = new URLSearchParams({ provider: provider });
6966
+ if (_cachedSetupStatus && _cachedSetupStatus.auth_method) params.set('auth_method', _cachedSetupStatus.auth_method);
6967
+ if (typeof showToast === 'function') showToast('Testing ' + providerLabelForTest(provider) + '...', 'var(--accent)', 1600);
6968
+ fetch('/api/setup/test-key?' + params.toString(), { cache: 'no-store' })
6969
+ .then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
6970
+ .then(function(result) {
6971
+ var data = result.data || {};
6972
+ if (result.ok && data.ok) {
6973
+ if (typeof showToast === 'function') showToast(providerLabelForTest(provider) + ' is reachable', 'var(--green, #9ece6a)', 2600);
6974
+ if (alertId) WE._dismissAlert(alertId);
6975
+ else checkServiceAlerts();
6976
+ return;
6977
+ }
6978
+ var msg = data.error || 'Provider test failed';
6979
+ if (typeof showToast === 'function') showToast(msg, 'var(--red, #f7768e)', 4200);
6980
+ })
6981
+ .catch(function(err) {
6982
+ if (typeof showToast === 'function') showToast(err.message || 'Provider test failed', 'var(--red, #f7768e)', 4200);
6983
+ });
6984
+ }
6985
+
6986
+ WE._testProviderFromAlert = function(alertId) {
6987
+ var alert = serviceAlertById(alertId);
6988
+ if (!alert && _serviceHealth && _serviceHealth.current_issue && String(_serviceHealth.current_issue.id) === String(alertId)) {
6989
+ alert = _serviceHealth.current_issue;
6990
+ }
6991
+ testSetupProvider(providerForAlert(alert), alertId);
6992
+ };
6993
+
6994
+ WE._reviewSkillAlert = function(alertId) {
6995
+ var alert = serviceAlertById(alertId);
6996
+ var name = serviceAlertSkillName(alert);
6997
+ _skillsFilter.search = name || '';
6998
+ _skillsFilter.status = 'All';
6999
+ _skillsFilter.category = 'All';
7000
+ WE.showView('skills');
7001
+ };
7002
+
7003
+ WE._enableSkillFromAlert = function(alertId) {
7004
+ var alert = serviceAlertById(alertId);
7005
+ var name = serviceAlertSkillName(alert);
7006
+ if (!name) {
7007
+ if (typeof showToast === 'function') showToast('Could not identify the disabled skill', 'var(--red, #f7768e)', 3000);
7008
+ return;
7009
+ }
7010
+ api('/skills?enabled=0').then(function(resp) {
7011
+ var skills = resp.data || [];
7012
+ var match = null;
7013
+ for (var i = 0; i < skills.length; i++) {
7014
+ if (String(skills[i].name || '').toLowerCase() === name.toLowerCase()) {
7015
+ match = skills[i];
7016
+ break;
7017
+ }
7018
+ }
7019
+ if (!match) throw new Error('Skill is not disabled or was not found: ' + name);
7020
+ return apiPut('/skills/' + encodeURIComponent(match.id), { enabled: 1 });
7021
+ }).then(function() {
7022
+ if (typeof showToast === 'function') showToast('Re-enabled ' + name, 'var(--green, #9ece6a)', 2600);
7023
+ WE._dismissAlert(alertId);
7024
+ }).catch(function(err) {
7025
+ if (typeof showToast === 'function') showToast(err.message || 'Could not re-enable skill', 'var(--red, #f7768e)', 4200);
7026
+ });
7027
+ };
7028
+
6733
7029
  function findServiceAlert(alertId) {
6734
7030
  for (var i = 0; i < _serviceAlerts.length; i++) {
6735
7031
  if (String(_serviceAlerts[i].id) === String(alertId)) return _serviceAlerts[i];