@pellux/goodvibes-tui 0.25.0 → 0.27.0

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 (146) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +2422 -1040
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/conversation-rendering.ts +2 -2
  8. package/src/core/conversation.ts +49 -2
  9. package/src/core/session-recovery.ts +24 -18
  10. package/src/core/system-message-router.ts +42 -26
  11. package/src/core/turn-event-wiring.ts +12 -16
  12. package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +29 -24
  13. package/src/daemon/handlers/calendar/index.ts +316 -0
  14. package/src/daemon/handlers/context.ts +29 -0
  15. package/src/daemon/handlers/contracts.ts +77 -0
  16. package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
  17. package/src/daemon/handlers/drafts/index.ts +17 -0
  18. package/src/daemon/handlers/drafts/register.ts +331 -0
  19. package/src/daemon/handlers/email/config.ts +164 -0
  20. package/src/daemon/handlers/email/index.ts +43 -0
  21. package/src/daemon/handlers/email/read-handlers.ts +80 -0
  22. package/src/daemon/handlers/email/runtime.ts +140 -0
  23. package/src/daemon/handlers/email/validation.ts +147 -0
  24. package/src/daemon/handlers/email/write-handlers.ts +133 -0
  25. package/src/daemon/handlers/errors.ts +18 -0
  26. package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
  27. package/src/daemon/handlers/inbox/index.ts +211 -0
  28. package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
  29. package/src/daemon/{channels → handlers}/inbox/poller.ts +6 -5
  30. package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
  31. package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +10 -11
  32. package/src/daemon/{channels → handlers}/inbox/providers/email.ts +2 -1
  33. package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
  34. package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +4 -3
  35. package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +11 -12
  36. package/src/daemon/handlers/index.ts +107 -0
  37. package/src/daemon/handlers/register.ts +161 -0
  38. package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
  39. package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
  40. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  41. package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
  42. package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
  43. package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
  44. package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
  45. package/src/daemon/handlers/remote/index.ts +119 -0
  46. package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
  47. package/src/daemon/handlers/remote/service.ts +191 -0
  48. package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
  49. package/src/daemon/handlers/routing/index.ts +261 -0
  50. package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
  51. package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
  52. package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
  53. package/src/daemon/handlers/triage/index.ts +57 -0
  54. package/src/daemon/handlers/triage/integration.ts +213 -0
  55. package/src/daemon/{triage → handlers/triage}/pipeline.ts +60 -71
  56. package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
  57. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  58. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  59. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  60. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  61. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  62. package/src/daemon/handlers/triage/types.ts +50 -0
  63. package/src/export/gist-uploader.ts +3 -1
  64. package/src/input/command-registry.ts +1 -0
  65. package/src/input/commands/cloudflare-runtime.ts +2 -1
  66. package/src/input/commands/guidance-runtime.ts +2 -4
  67. package/src/input/commands/health-runtime.ts +13 -7
  68. package/src/input/commands/knowledge.ts +2 -1
  69. package/src/input/commands/runtime-services.ts +40 -10
  70. package/src/input/handler-command-route.ts +1 -1
  71. package/src/input/handler-interactions.ts +2 -1
  72. package/src/input/handler-modal-routes.ts +2 -1
  73. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  74. package/src/input/handler-onboarding.ts +8 -8
  75. package/src/input/handler-ui-state.ts +1 -1
  76. package/src/input/tts-settings-actions.ts +2 -1
  77. package/src/main.ts +52 -59
  78. package/src/panels/agent-inspector-panel.ts +72 -2
  79. package/src/panels/agent-logs-panel.ts +127 -19
  80. package/src/panels/base-panel.ts +2 -1
  81. package/src/panels/builtin/agent.ts +3 -1
  82. package/src/panels/builtin/development.ts +1 -0
  83. package/src/panels/builtin/session.ts +1 -1
  84. package/src/panels/context-visualizer-panel.ts +24 -14
  85. package/src/panels/cost-tracker-panel.ts +7 -13
  86. package/src/panels/debug-panel.ts +11 -22
  87. package/src/panels/docs-panel.ts +14 -24
  88. package/src/panels/file-explorer-panel.ts +2 -1
  89. package/src/panels/file-preview-panel.ts +2 -1
  90. package/src/panels/git-panel.ts +10 -17
  91. package/src/panels/marketplace-panel.ts +2 -1
  92. package/src/panels/memory-panel.ts +2 -1
  93. package/src/panels/project-planning-panel.ts +5 -4
  94. package/src/panels/provider-health-panel.ts +56 -67
  95. package/src/panels/schedule-panel.ts +4 -7
  96. package/src/panels/session-browser-panel.ts +13 -21
  97. package/src/panels/skills-panel.ts +2 -1
  98. package/src/panels/tasks-panel.ts +2 -7
  99. package/src/panels/thinking-panel.ts +6 -11
  100. package/src/panels/token-budget-panel.ts +31 -15
  101. package/src/panels/tool-inspector-panel.ts +10 -18
  102. package/src/panels/work-plan-panel.ts +2 -1
  103. package/src/panels/wrfc-panel.ts +37 -35
  104. package/src/renderer/agent-detail-modal.ts +2 -2
  105. package/src/renderer/modal-utils.ts +0 -10
  106. package/src/renderer/process-modal.ts +2 -2
  107. package/src/runtime/bootstrap-command-context.ts +3 -0
  108. package/src/runtime/bootstrap-command-parts.ts +3 -1
  109. package/src/runtime/bootstrap-core.ts +31 -31
  110. package/src/runtime/bootstrap-shell.ts +1 -0
  111. package/src/runtime/onboarding/apply.ts +4 -3
  112. package/src/runtime/onboarding/snapshot.ts +4 -3
  113. package/src/runtime/process-lifecycle.ts +195 -0
  114. package/src/runtime/services.ts +52 -36
  115. package/src/runtime/wrfc-persistence.ts +20 -5
  116. package/src/shell/ui-openers.ts +3 -2
  117. package/src/verification/live-verifier.ts +4 -3
  118. package/src/version.ts +1 -1
  119. package/src/core/context-auto-compact.ts +0 -110
  120. package/src/daemon/calendar/index.ts +0 -52
  121. package/src/daemon/calendar/register.ts +0 -527
  122. package/src/daemon/channels/drafts/index.ts +0 -22
  123. package/src/daemon/channels/drafts/register.ts +0 -449
  124. package/src/daemon/channels/inbox/index.ts +0 -58
  125. package/src/daemon/channels/inbox/register.ts +0 -247
  126. package/src/daemon/channels/routing/index.ts +0 -39
  127. package/src/daemon/channels/routing/register.ts +0 -296
  128. package/src/daemon/email/index.ts +0 -68
  129. package/src/daemon/email/register.ts +0 -715
  130. package/src/daemon/operator/index.ts +0 -43
  131. package/src/daemon/operator/register-helper.ts +0 -150
  132. package/src/daemon/operator/surfaces.ts +0 -137
  133. package/src/daemon/operator/types.ts +0 -207
  134. package/src/daemon/remote/backends/index.ts +0 -34
  135. package/src/daemon/remote/index.ts +0 -74
  136. package/src/daemon/remote/register.ts +0 -411
  137. package/src/daemon/triage/index.ts +0 -59
  138. package/src/daemon/triage/integration.ts +0 -179
  139. package/src/daemon/triage/register.ts +0 -231
  140. package/src/daemon/triage/tagger.ts +0 -777
  141. /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
  142. /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
  143. /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
  144. /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
  145. /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
  146. /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
@@ -36,6 +36,7 @@ import {
36
36
  buildPanelWorkspace,
37
37
  buildSummaryBlock,
38
38
  DEFAULT_PANEL_PALETTE,
39
+ extendPalette,
39
40
  resolvePrimaryScrollableSection,
40
41
  type PanelWorkspaceSection,
41
42
  } from './polish.ts';
@@ -62,22 +63,10 @@ export interface ProviderHealthPanelDeps {
62
63
  // Colors
63
64
  // ---------------------------------------------------------------------------
64
65
 
65
- const C = {
66
- title: '#00ffff',
67
- online: '#5fd700',
68
- rateLimit: '#ffaf00',
69
- error: '#ff5f5f',
70
- unknown: '244',
71
- label: '244',
72
- value: '252',
73
- dim: '240',
74
- provName: '#e2e8f0',
75
- errMsg: '#ff5f5f',
76
- latGood: '#5fd700',
77
- latWarn: '#ffaf00',
78
- latBad: '#ff5f5f',
79
- separator: '#374151',
80
- } as const;
66
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
67
+ title: '#00ffff',
68
+ unknown: '244',
69
+ });
81
70
 
82
71
  const LATENCY_WARN_MS = 2_000;
83
72
  const LATENCY_BAD_MS = 5_000;
@@ -88,11 +77,11 @@ const LATENCY_BAD_MS = 5_000;
88
77
 
89
78
  function statusDot(status: ProviderStatus): { char: string; color: string } {
90
79
  switch (status) {
91
- case 'healthy': return { char: '●', color: C.online };
92
- case 'degraded': return { char: '◑', color: C.rateLimit };
93
- case 'rate_limited': return { char: '◐', color: C.rateLimit };
94
- case 'auth_error': return { char: '✕', color: C.error };
95
- case 'unavailable': return { char: '✕', color: C.error };
80
+ case 'healthy': return { char: '●', color: C.good };
81
+ case 'degraded': return { char: '◑', color: C.warn };
82
+ case 'rate_limited': return { char: '◐', color: C.warn };
83
+ case 'auth_error': return { char: '✕', color: C.bad };
84
+ case 'unavailable': return { char: '✕', color: C.bad };
96
85
  default: return { char: '○', color: C.unknown };
97
86
  }
98
87
  }
@@ -109,9 +98,9 @@ function statusLabel(status: ProviderStatus): string {
109
98
  }
110
99
 
111
100
  function latencyColor(ms: number): string {
112
- if (ms >= LATENCY_BAD_MS) return C.latBad;
113
- if (ms >= LATENCY_WARN_MS) return C.latWarn;
114
- return C.latGood;
101
+ if (ms >= LATENCY_BAD_MS) return C.bad;
102
+ if (ms >= LATENCY_WARN_MS) return C.warn;
103
+ return C.good;
115
104
  }
116
105
 
117
106
  function fmtMs(ms: number): string {
@@ -154,11 +143,11 @@ interface ProviderRuntimeRecord {
154
143
  function domainColor(level: HealthDomainSummary['level']): string {
155
144
  switch (level) {
156
145
  case 'good':
157
- return C.online;
146
+ return C.good;
158
147
  case 'warn':
159
- return C.rateLimit;
148
+ return C.warn;
160
149
  case 'bad':
161
- return C.error;
150
+ return C.bad;
162
151
  default:
163
152
  return C.value;
164
153
  }
@@ -184,9 +173,9 @@ function routeColor(route: ProviderPanelAuthRoute): string {
184
173
  case 'subscription-oauth':
185
174
  return C.title;
186
175
  case 'service-oauth':
187
- return C.online;
176
+ return C.good;
188
177
  case 'api-key':
189
- return C.rateLimit;
178
+ return C.warn;
190
179
  case 'secret-ref':
191
180
  return C.value;
192
181
  case 'anonymous':
@@ -200,12 +189,12 @@ function routeColor(route: ProviderPanelAuthRoute): string {
200
189
  function freshnessColor(freshness: ProviderPanelAuthFreshness): string {
201
190
  switch (freshness) {
202
191
  case 'expired':
203
- return C.error;
192
+ return C.bad;
204
193
  case 'expiring':
205
194
  case 'pending':
206
- return C.rateLimit;
195
+ return C.warn;
207
196
  case 'healthy':
208
- return C.online;
197
+ return C.good;
209
198
  default:
210
199
  return C.dim;
211
200
  }
@@ -552,10 +541,10 @@ export class ProviderHealthPanel extends BasePanel {
552
541
  { command: '/provider', summary: 'review current provider and model selection' },
553
542
  { command: '/subscription', summary: 'review provider login and subscription state' },
554
543
  ],
555
- { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
544
+ { ...DEFAULT_PANEL_PALETTE, header: C.title },
556
545
  ),
557
546
  }],
558
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
547
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
559
548
  });
560
549
  }
561
550
 
@@ -581,14 +570,14 @@ export class ProviderHealthPanel extends BasePanel {
581
570
  const postureLines = [
582
571
  buildKeyValueLine(width, [
583
572
  { label: 'providers', value: String(providers.length), valueColor: C.value },
584
- { label: 'online', value: String(online), valueColor: C.online },
585
- { label: 'rate-limited', value: String(rateLimited), valueColor: C.rateLimit },
586
- { label: 'error', value: String(errored), valueColor: C.error },
587
- { label: 'auth alerts', value: String(expiringAuth), valueColor: expiringAuth > 0 ? C.rateLimit : C.dim },
588
- { label: 'account issues', value: String(accountIssues), valueColor: accountIssues > 0 ? C.error : C.dim },
589
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
590
- buildGuidanceLine(width, '/provider', 'review provider selection and routing if health posture degrades', { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
591
- buildGuidanceLine(width, '/accounts', 'inspect auth routes, fallback posture, and billing-path safety', { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }),
573
+ { label: 'online', value: String(online), valueColor: C.good },
574
+ { label: 'rate-limited', value: String(rateLimited), valueColor: C.warn },
575
+ { label: 'error', value: String(errored), valueColor: C.bad },
576
+ { label: 'auth alerts', value: String(expiringAuth), valueColor: expiringAuth > 0 ? C.warn : C.dim },
577
+ { label: 'account issues', value: String(accountIssues), valueColor: accountIssues > 0 ? C.bad : C.dim },
578
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }),
579
+ buildGuidanceLine(width, '/provider', 'review provider selection and routing if health posture degrades', { ...DEFAULT_PANEL_PALETTE, header: C.title }),
580
+ buildGuidanceLine(width, '/accounts', 'inspect auth routes, fallback posture, and billing-path safety', { ...DEFAULT_PANEL_PALETTE, header: C.title }),
592
581
  ];
593
582
 
594
583
  const domainLines: Line[] = [];
@@ -605,16 +594,16 @@ export class ProviderHealthPanel extends BasePanel {
605
594
  })) {
606
595
  domainLines.push(buildPanelLine(width, [
607
596
  [' ', C.label],
608
- [domain.name.padEnd(14), C.provName],
597
+ [domain.name.padEnd(14), C.value],
609
598
  [domain.summary.slice(0, Math.max(0, width - 36)).padEnd(Math.max(0, width - 36)), domainColor(domain.level)],
610
599
  [' ', C.label],
611
600
  [domain.next.slice(0, 20), C.dim],
612
601
  ]));
613
602
  for (const detail of domain.details.slice(0, 2)) {
614
- domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.dim));
603
+ domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
615
604
  }
616
605
  if (domain.nextSteps.length > 1) {
617
- domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
606
+ domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
618
607
  }
619
608
  }
620
609
 
@@ -632,61 +621,61 @@ export class ProviderHealthPanel extends BasePanel {
632
621
  session: session.session,
633
622
  });
634
623
  maintenanceLines.push(buildKeyValueLine(width, [
635
- { label: 'level', value: maintenance.level, valueColor: maintenance.level === 'needs-repair' ? C.error : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.rateLimit : C.online },
624
+ { label: 'level', value: maintenance.level, valueColor: maintenance.level === 'needs-repair' ? C.bad : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.warn : C.good },
636
625
  { label: 'guidance', value: maintenance.guidanceMode, valueColor: C.value },
637
- { label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.error : maintenance.usagePct >= 70 ? C.rateLimit : C.value },
626
+ { label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.bad : maintenance.usagePct >= 70 ? C.warn : C.value },
638
627
  { label: 'remaining', value: maintenance.remainingTokens.toLocaleString(), valueColor: C.value },
639
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
628
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
640
629
  for (const reason of maintenance.reasons.slice(0, 3)) {
641
- maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.dim));
630
+ maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
642
631
  }
643
632
  if (maintenance.nextSteps.length > 0) {
644
- maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
633
+ maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
645
634
  }
646
635
  if (selectedName) {
647
636
  const status = selectedHealth?.status ?? 'unknown';
648
637
  selectedLines.push(buildKeyValueLine(width, [
649
- { label: 'provider', value: selectedName, valueColor: C.provName },
638
+ { label: 'provider', value: selectedName, valueColor: C.value },
650
639
  { label: 'status', value: statusLabel(status), valueColor: statusDot(status).color },
651
640
  { label: 'last ok', value: fmtAgo(selectedHealth?.lastSuccessAt), valueColor: C.value },
652
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
641
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
653
642
  if (selectedHealth?.rateLimitExpiresAt && selectedHealth.rateLimitExpiresAt > Date.now()) {
654
- selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.rateLimit));
643
+ selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
655
644
  }
656
645
  if (selectedHealth?.lastErrorMessage) {
657
- selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
646
+ selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
658
647
  }
659
648
  if (selectedAccount) {
660
649
  selectedLines.push(buildKeyValueLine(width, [
661
650
  { label: 'route', value: selectedAccount.activeRoute, valueColor: routeColor(selectedAccount.activeRoute) },
662
651
  { label: 'preferred', value: selectedAccount.preferredRoute, valueColor: C.dim },
663
652
  { label: 'freshness', value: selectedAccount.authFreshness, valueColor: freshnessColor(selectedAccount.authFreshness) },
664
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
653
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
665
654
  selectedLines.push(buildKeyValueLine(width, [
666
655
  { label: 'models', value: String(selectedAccount.modelCount), valueColor: C.value },
667
- { label: 'active', value: selectedAccount.active ? 'yes' : 'no', valueColor: selectedAccount.active ? C.online : C.dim },
668
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
669
- selectedLines.push(...buildBodyText(width, `Auth route: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.dim));
656
+ { label: 'active', value: selectedAccount.active ? 'yes' : 'no', valueColor: selectedAccount.active ? C.good : C.dim },
657
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
658
+ selectedLines.push(...buildBodyText(width, `Auth route: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
670
659
  if (selectedAccount.fallbackRisk) {
671
- selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.rateLimit));
660
+ selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
672
661
  }
673
662
  if (selectedAccount.issues.length > 0) {
674
- selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
663
+ selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
675
664
  }
676
665
  if (selectedAccount.recommendedActions.length > 0) {
677
- selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
666
+ selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
678
667
  }
679
668
  }
680
669
  }
681
670
 
682
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Health posture', postureLines, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }) };
671
+ const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Health posture', postureLines, { ...DEFAULT_PANEL_PALETTE, header: C.title }) };
683
672
  const domainsSection: PanelWorkspaceSection = { title: 'Repair Domains', lines: domainLines };
684
673
  const maintenanceSections = maintenanceLines.length > 0 ? [{ title: 'Session Maintenance', lines: maintenanceLines } satisfies PanelWorkspaceSection] : [];
685
- const selectedSections = selectedLines.length > 0 ? [{ lines: buildDetailBlock(width, 'Selected provider', selectedLines, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }) } satisfies PanelWorkspaceSection] : [];
674
+ const selectedSections = selectedLines.length > 0 ? [{ lines: buildDetailBlock(width, 'Selected provider', selectedLines, { ...DEFAULT_PANEL_PALETTE, header: C.title }) } satisfies PanelWorkspaceSection] : [];
686
675
  const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
687
676
  intro,
688
677
  footerLines: [buildPanelLine(width, [[' j/k or Up/Down move live cooldowns refresh while active', C.dim]])],
689
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
678
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
690
679
  beforeSections: [postureSection, domainsSection, ...maintenanceSections],
691
680
  section: {
692
681
  title: 'Providers',
@@ -696,13 +685,13 @@ export class ProviderHealthPanel extends BasePanel {
696
685
  const latency = health?.lastLatencyMs !== undefined ? fmtMs(health.lastLatencyMs) : 'n/a';
697
686
  const latencyFg = health?.lastLatencyMs !== undefined ? latencyColor(health.lastLatencyMs) : C.dim;
698
687
  return buildPanelListRow(width, [
699
- { text: name.padEnd(16), fg: C.provName },
688
+ { text: name.padEnd(16), fg: C.value },
700
689
  { text: statusLabel(status).padEnd(14), fg: statusDot(status).color },
701
690
  { text: ' lat ', fg: C.label },
702
691
  { text: latency.padEnd(8), fg: latencyFg },
703
692
  { text: ' ok ', fg: C.label },
704
693
  { text: fmtAgo(health?.lastSuccessAt).padEnd(10), fg: C.value },
705
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, { selected: absolute === this._selectedIndex, selectedBg: '#111827' });
694
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }, { selected: absolute === this._selectedIndex, selectedBg: '#111827' });
706
695
  }),
707
696
  selectedIndex: this._selectedIndex,
708
697
  scrollOffset: this._scrollOffset,
@@ -725,7 +714,7 @@ export class ProviderHealthPanel extends BasePanel {
725
714
  intro,
726
715
  sections,
727
716
  footerLines: [buildPanelLine(width, [[' j/k or Up/Down move live cooldowns refresh while active', C.dim]])],
728
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
717
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
729
718
  });
730
719
  }
731
720
  }
@@ -8,6 +8,7 @@ import {
8
8
  buildEmptyState,
9
9
  buildPanelLine,
10
10
  buildPanelWorkspace,
11
+ extendPalette,
11
12
  resolveScrollablePanelSection,
12
13
  DEFAULT_PANEL_PALETTE,
13
14
  type PanelWorkspaceSection,
@@ -17,12 +18,9 @@ import {
17
18
  // Colors
18
19
  // ---------------------------------------------------------------------------
19
20
 
20
- const C = {
21
- header: '#00d7ff',
22
- sectionHeader: '244',
21
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
23
22
  enabled: '#5fd700',
24
23
  disabled: '#6c6c6c',
25
- selected: '#1c1c1c',
26
24
  selectedFg: '#ffffff',
27
25
  id: '238',
28
26
  cron: '#af87ff',
@@ -32,8 +30,7 @@ const C = {
32
30
  runCount: '#ffaf00',
33
31
  statusRunning: '#5fd700',
34
32
  statusFailed: '#ff5f5f',
35
- hint: '240',
36
- } as const;
33
+ });
37
34
 
38
35
  // ---------------------------------------------------------------------------
39
36
  // View items
@@ -289,7 +286,7 @@ export class SchedulePanel extends BasePanel {
289
286
  * Row 3: prompt preview [history]
290
287
  */
291
288
  private renderTask(task: AutomationJob, history: AutomationRun[], selected: boolean, width: number): Line[] {
292
- const bg = selected ? C.selected : undefined;
289
+ const bg = selected ? C.selectBg : undefined;
293
290
  const fgBase = selected ? C.selectedFg : undefined;
294
291
 
295
292
  const bullet = task.enabled ? '* ' : 'o ';
@@ -15,6 +15,7 @@ import {
15
15
  buildPanelWorkspace,
16
16
  resolveScrollablePanelSection,
17
17
  DEFAULT_PANEL_PALETTE,
18
+ extendPalette,
18
19
  type PanelWorkspaceSection,
19
20
  } from './polish.ts';
20
21
  import { truncateDisplay } from '../utils/terminal-width.ts';
@@ -28,24 +29,15 @@ import {
28
29
  } from './search-focus.ts';
29
30
  import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
30
31
 
31
- const C = {
32
- headerBg: '#1a1a2e',
33
- headerFg: '#ffffff',
34
- statusBar: '#222233',
35
- statusFg: '#aaaaaa',
36
- selected: '#00ffff',
37
- selectedBg: '#1a2a3a',
38
- normal: '#ccccdd',
39
- dim: '#555566',
40
- label: '#8888bb',
41
- value: '#ccccdd',
42
- dateFg: '#6699aa',
43
- modelFg: '#99aacc',
44
- countFg: '#88bbcc',
45
- warnFg: '#ffcc44',
46
- errorFg: '#ff6666',
47
- separator: '#333355',
48
- } as const;
32
+ // Panel-specific extras only; shared tones come from DEFAULT_PANEL_PALETTE so
33
+ // theme changes propagate. `selected` is a deliberate bright-cyan cursor accent
34
+ // and the date/model/count column tints have no clean shared equivalent.
35
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
36
+ selected: '#00ffff',
37
+ dateFg: '#6699aa',
38
+ modelFg: '#99aacc',
39
+ countFg: '#88bbcc',
40
+ });
49
41
 
50
42
  function shortDate(ts: number): string {
51
43
  const d = new Date(ts);
@@ -308,7 +300,7 @@ export class SessionBrowserPanel extends BasePanel {
308
300
  }
309
301
 
310
302
  private _renderSession(width: number, sess: SessionInfo, isCursor: boolean): Line {
311
- const bg = isCursor ? C.selectedBg : '';
303
+ const bg = isCursor ? C.selectBg : '';
312
304
  const date = shortDate(sess.timestamp);
313
305
  const cnt = String(sess.messageCount).padStart(3) + 'm ';
314
306
  const model = (sess.model || 'unknown').slice(0, 18).padEnd(18) + ' ';
@@ -317,10 +309,10 @@ export class SessionBrowserPanel extends BasePanel {
317
309
  return buildStyledPanelLine(width, [
318
310
  { text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
319
311
  { text: date, fg: C.dateFg, bg },
320
- { text: ' ', fg: C.normal, bg },
312
+ { text: ' ', fg: C.value, bg },
321
313
  { text: cnt, fg: C.countFg, bg },
322
314
  { text: model, fg: C.modelFg, bg },
323
- { text: title, fg: isCursor ? C.selected : C.normal, bg, bold: isCursor },
315
+ { text: title, fg: isCursor ? C.selected : C.value, bg, bold: isCursor },
324
316
  ]);
325
317
  }
326
318
 
@@ -16,6 +16,7 @@ import {
16
16
  getPanelSearchFocusTransition,
17
17
  isPanelSearchCancel,
18
18
  } from './search-focus.ts';
19
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
19
20
 
20
21
  const C = {
21
22
  ...DEFAULT_PANEL_PALETTE,
@@ -259,7 +260,7 @@ export class SkillsPanel extends SearchableListPanel<SkillRecord> {
259
260
  this.invalidateFilter();
260
261
  });
261
262
  } catch (err) {
262
- this.setError(err instanceof Error ? err.message : String(err));
263
+ this.setError(summarizeError(err));
263
264
  }
264
265
  this.markDirty();
265
266
  })();
@@ -14,6 +14,7 @@ import {
14
14
  buildPanelWorkspace,
15
15
  DEFAULT_PANEL_PALETTE,
16
16
  } from './polish.ts';
17
+ import { formatElapsed } from '../utils/format-elapsed.ts';
17
18
 
18
19
  const C = {
19
20
  ...DEFAULT_PANEL_PALETTE,
@@ -35,13 +36,7 @@ function formatWhen(value?: number): string {
35
36
 
36
37
  function formatDuration(startedAt?: number, endedAt?: number): string {
37
38
  if (!startedAt) return 'n/a';
38
- const end = endedAt ?? Date.now();
39
- const ms = Math.max(0, end - startedAt);
40
- if (ms < 1_000) return `${ms}ms`;
41
- if (ms < 60_000) return `${(ms / 1_000).toFixed(ms < 10_000 ? 1 : 0)}s`;
42
- const mins = Math.floor(ms / 60_000);
43
- const secs = Math.floor((ms % 60_000) / 1_000);
44
- return `${mins}m ${secs}s`;
39
+ return formatElapsed(Math.max(0, (endedAt ?? Date.now()) - startedAt));
45
40
  }
46
41
 
47
42
  function kindLabel(kind: RuntimeTask['kind']): string {
@@ -12,25 +12,20 @@ import {
12
12
  buildStyledPanelLine,
13
13
  buildPanelWorkspace,
14
14
  resolveScrollablePanelSection,
15
+ extendPalette,
15
16
  DEFAULT_PANEL_PALETTE,
16
17
  type PanelWorkspaceSection,
17
18
  } from './polish.ts';
18
19
  import { truncateDisplay } from '../utils/terminal-width.ts';
19
20
 
20
- const C = {
21
- headerBg: '#1a1a2e',
22
- headerFg: '#ffffff',
23
- statusBar: '#222233',
24
- statusFg: '#aaaaaa',
21
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
22
+ // Domain-specific reasoning tones (purples) and the active-stream cyan have no
23
+ // clean shared equivalent; selection background maps to the shared selectBg.
25
24
  reasoningFg: '#aa88ff',
26
25
  activeFg: '#cc99ff',
27
- dimFg: '#555566',
28
26
  turnLabel: '#7766bb',
29
27
  activeLabel: '#00ffff',
30
- collapsedFg: '#888899',
31
- selected: '#00ffff',
32
- selectedBg: '#1a2a3a',
33
- } as const;
28
+ });
34
29
 
35
30
  interface ReasoningBlock {
36
31
  turnId: number;
@@ -195,7 +190,7 @@ export class ThinkingPanel extends BasePanel {
195
190
  }
196
191
 
197
192
  private _renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
198
- const bg = isCursor ? C.selectedBg : '';
193
+ const bg = isCursor ? C.selectBg : '';
199
194
  if (row.kind === 'header') {
200
195
  const indicator = this.blocks[row.blockIndex]?.collapsed ? '▸' : '▾';
201
196
  const active = this.blocks[row.blockIndex]?.active;
@@ -12,6 +12,7 @@ import {
12
12
  buildPanelWorkspace,
13
13
  resolveScrollablePanelSection,
14
14
  DEFAULT_PANEL_PALETTE,
15
+ extendPalette,
15
16
  type PanelWorkspaceSection,
16
17
  } from './polish.ts';
17
18
 
@@ -32,22 +33,16 @@ interface TurnUsage {
32
33
  // Colors
33
34
  // ---------------------------------------------------------------------------
34
35
 
35
- const C = {
36
- title: '#00ffff',
36
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
37
+ // Token data-series colors (input/output/cache) — categorical, no shared equivalent
37
38
  input: '#00ffff',
38
39
  output: '#d000ff',
39
40
  cacheRead: '#00d700',
40
41
  cacheWrite: '#ffaf00',
42
+ // Progress-bar track shade and per-turn table sub-header gray
41
43
  barBg: '236',
42
- label: '244',
43
- value: '252',
44
- sectionHeader: '238',
45
- warnYellow: '#ffaf00',
46
- warnRed: '#ff5f5f',
47
- dim: '240',
48
- good: '#5fd700',
49
44
  turnHeader: '242',
50
- } as const;
45
+ });
51
46
 
52
47
  // Warning thresholds for context window usage
53
48
  const WARN_YELLOW = 0.70;
@@ -99,11 +94,17 @@ export class TokenBudgetPanel extends BasePanel {
99
94
  private sessionReadModel: UiReadModel<UiSessionSnapshot> | null = null;
100
95
  private readonly sessionMemoryStore: SessionMemoryQuery;
101
96
  private readonly configManager: Pick<ConfigManager, 'get'>;
97
+ private readonly requestRender: () => void;
102
98
 
103
- constructor(sessionMemoryStore: SessionMemoryQuery, configManager: Pick<ConfigManager, 'get'>) {
99
+ constructor(
100
+ sessionMemoryStore: SessionMemoryQuery,
101
+ configManager: Pick<ConfigManager, 'get'>,
102
+ requestRender: () => void = () => {},
103
+ ) {
104
104
  super('tokens', 'Tokens', 'T', 'monitoring');
105
105
  this.sessionMemoryStore = sessionMemoryStore;
106
106
  this.configManager = configManager;
107
+ this.requestRender = requestRender;
107
108
  }
108
109
 
109
110
  // ---------------------------------------------------------------------------
@@ -149,7 +150,7 @@ export class TokenBudgetPanel extends BasePanel {
149
150
  this.turnHistory.shift();
150
151
  }
151
152
 
152
- this.refresh();
153
+ this._refreshAndRender();
153
154
  }
154
155
 
155
156
  // ---------------------------------------------------------------------------
@@ -161,11 +162,17 @@ export class TokenBudgetPanel extends BasePanel {
161
162
  this.refresh();
162
163
  // Poll every 2 s while active so the context bar stays current during streaming
163
164
  if (this.refreshTimer !== null) clearInterval(this.refreshTimer);
164
- this.refreshTimer = setInterval(() => this.refresh(), 2_000);
165
+ this.refreshTimer = setInterval(() => this._refreshAndRender(), 2_000);
165
166
  }
166
167
 
167
168
  override onDeactivate(): void {
168
169
  super.onDeactivate();
170
+ // Stop the off-screen poll so a backgrounded Tokens tab does not keep
171
+ // refreshing (and, once requestRender is wired, force a repaint) every 2 s.
172
+ if (this.refreshTimer !== null) {
173
+ clearInterval(this.refreshTimer);
174
+ this.refreshTimer = null;
175
+ }
169
176
  }
170
177
 
171
178
  override onDestroy(): void {
@@ -194,6 +201,15 @@ export class TokenBudgetPanel extends BasePanel {
194
201
  this.markDirty();
195
202
  }
196
203
 
204
+ /**
205
+ * Refresh data and request a repaint. Used by the background poll and on
206
+ * turn completion, where no key event will otherwise drive a render.
207
+ */
208
+ private _refreshAndRender(): void {
209
+ this.refresh();
210
+ this.requestRender();
211
+ }
212
+
197
213
  // ---------------------------------------------------------------------------
198
214
  // Rendering
199
215
  // ---------------------------------------------------------------------------
@@ -369,10 +385,10 @@ export class TokenBudgetPanel extends BasePanel {
369
385
  let barColor: string;
370
386
  let warnSuffix = '';
371
387
  if (pct >= WARN_RED) {
372
- barColor = C.warnRed;
388
+ barColor = C.bad;
373
389
  warnSuffix = ' ! CRITICAL';
374
390
  } else if (pct >= WARN_YELLOW) {
375
- barColor = C.warnYellow;
391
+ barColor = C.warn;
376
392
  warnSuffix = ' ! HIGH';
377
393
  } else {
378
394
  barColor = C.good;
@@ -13,26 +13,18 @@ import {
13
13
  buildPanelWorkspace,
14
14
  resolveScrollablePanelSection,
15
15
  DEFAULT_PANEL_PALETTE,
16
+ extendPalette,
16
17
  type PanelWorkspaceSection,
17
18
  } from './polish.ts';
18
19
  import { truncateDisplay } from '../utils/terminal-width.ts';
19
20
 
20
- const C = {
21
- headerBg: '#1a1a2e',
22
- headerFg: '#ffffff',
23
- statusBar: '#222233',
24
- statusFg: '#aaaaaa',
25
- toolFg: '#00ccff',
26
- resultFg: '#66ddff',
27
- errorFg: '#ff6666',
28
- durationFg: '#aaaa66',
29
- argsFg: '#aaaaaa',
30
- dimFg: '#555566',
31
- selected: '#00ffff',
32
- selectedBg: '#1a2a3a',
33
- labelFg: '#8888bb',
34
- filterFg: '#ffcc44',
35
- } as const;
21
+ // Panel-specific accents only; shared tones come from DEFAULT_PANEL_PALETTE so
22
+ // theme changes propagate. selectedBg->selectBg, errorFg->bad are shared keys.
23
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
24
+ selected: '#00ffff',
25
+ toolFg: '#00ccff',
26
+ argsFg: '#aaaaaa',
27
+ });
36
28
 
37
29
  const MAX_ENTRIES = 500;
38
30
 
@@ -271,7 +263,7 @@ export class ToolInspectorPanel extends BasePanel {
271
263
  }
272
264
 
273
265
  private _renderRow(width: number, row: FlatRow, isCursor: boolean): Line {
274
- const bg = isCursor ? C.selectedBg : '';
266
+ const bg = isCursor ? C.selectBg : '';
275
267
  if (row.kind === 'call') {
276
268
  return buildStyledPanelLine(width, [
277
269
  { text: isCursor ? '▸' : ' ', fg: C.selected, bg, bold: isCursor },
@@ -280,7 +272,7 @@ export class ToolInspectorPanel extends BasePanel {
280
272
  }
281
273
  return buildStyledPanelLine(width, [
282
274
  { text: ' ', fg: C.argsFg, bg },
283
- { text: truncateDisplay(row.text, Math.max(0, width - 2)), fg: row.isError ? C.errorFg : C.argsFg, bg },
275
+ { text: truncateDisplay(row.text, Math.max(0, width - 2)), fg: row.isError ? C.bad : C.argsFg, bg },
284
276
  ]);
285
277
  }
286
278
 
@@ -2,6 +2,7 @@ import type { Line } from '../types/grid.ts';
2
2
  import { UIFactory } from '../renderer/ui-factory.ts';
3
3
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
4
  import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
5
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
6
 
6
7
  const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
7
8
  pending: '[ ]',
@@ -98,7 +99,7 @@ export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
98
99
  return super.handleInput(key);
99
100
  }
100
101
  } catch (error) {
101
- this.setError(error instanceof Error ? error.message : String(error));
102
+ this.setError(summarizeError(error));
102
103
  return true;
103
104
  }
104
105
  }