@pellux/goodvibes-tui 0.26.0 → 0.28.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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  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/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,4 +1,5 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
+ import { formatLatencyMs } from '../utils/format-duration.ts';
2
3
  import { BasePanel } from './base-panel.ts';
3
4
  import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
4
5
  import type { ProviderAuthRouteDescriptor } from '@pellux/goodvibes-sdk/platform/providers';
@@ -26,16 +27,17 @@ import type {
26
27
  } from '../runtime/ui-read-models.ts';
27
28
  import { evaluateSessionMaintenance } from '@/runtime/index.ts';
28
29
  import {
30
+ buildAlignedRow,
29
31
  buildBodyText,
30
32
  buildDetailBlock,
31
33
  buildEmptyState,
32
34
  buildGuidanceLine,
35
+ buildKeyboardHints,
33
36
  buildKeyValueLine,
34
- buildPanelListRow,
35
- buildPanelLine,
36
37
  buildPanelWorkspace,
37
38
  buildSummaryBlock,
38
39
  DEFAULT_PANEL_PALETTE,
40
+ extendPalette,
39
41
  resolvePrimaryScrollableSection,
40
42
  type PanelWorkspaceSection,
41
43
  } from './polish.ts';
@@ -62,22 +64,11 @@ export interface ProviderHealthPanelDeps {
62
64
  // Colors
63
65
  // ---------------------------------------------------------------------------
64
66
 
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;
67
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
68
+ title: '#00ffff',
69
+ unknown: '244',
70
+ rowSelectBg: '#111827',
71
+ });
81
72
 
82
73
  const LATENCY_WARN_MS = 2_000;
83
74
  const LATENCY_BAD_MS = 5_000;
@@ -88,11 +79,11 @@ const LATENCY_BAD_MS = 5_000;
88
79
 
89
80
  function statusDot(status: ProviderStatus): { char: string; color: string } {
90
81
  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 };
82
+ case 'healthy': return { char: '●', color: C.good };
83
+ case 'degraded': return { char: '◑', color: C.warn };
84
+ case 'rate_limited': return { char: '◐', color: C.warn };
85
+ case 'auth_error': return { char: '✕', color: C.bad };
86
+ case 'unavailable': return { char: '✕', color: C.bad };
96
87
  default: return { char: '○', color: C.unknown };
97
88
  }
98
89
  }
@@ -109,16 +100,13 @@ function statusLabel(status: ProviderStatus): string {
109
100
  }
110
101
 
111
102
  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;
103
+ if (ms >= LATENCY_BAD_MS) return C.bad;
104
+ if (ms >= LATENCY_WARN_MS) return C.warn;
105
+ return C.good;
115
106
  }
116
107
 
117
108
  function fmtMs(ms: number): string {
118
- if (ms <= 0) return 'n/a';
119
- if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
120
- if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
121
- return `${Math.round(ms)}ms`;
109
+ return formatLatencyMs(ms);
122
110
  }
123
111
 
124
112
  function fmtAgo(ts: number | undefined): string {
@@ -154,11 +142,11 @@ interface ProviderRuntimeRecord {
154
142
  function domainColor(level: HealthDomainSummary['level']): string {
155
143
  switch (level) {
156
144
  case 'good':
157
- return C.online;
145
+ return C.good;
158
146
  case 'warn':
159
- return C.rateLimit;
147
+ return C.warn;
160
148
  case 'bad':
161
- return C.error;
149
+ return C.bad;
162
150
  default:
163
151
  return C.value;
164
152
  }
@@ -184,9 +172,9 @@ function routeColor(route: ProviderPanelAuthRoute): string {
184
172
  case 'subscription-oauth':
185
173
  return C.title;
186
174
  case 'service-oauth':
187
- return C.online;
175
+ return C.good;
188
176
  case 'api-key':
189
- return C.rateLimit;
177
+ return C.warn;
190
178
  case 'secret-ref':
191
179
  return C.value;
192
180
  case 'anonymous':
@@ -200,12 +188,12 @@ function routeColor(route: ProviderPanelAuthRoute): string {
200
188
  function freshnessColor(freshness: ProviderPanelAuthFreshness): string {
201
189
  switch (freshness) {
202
190
  case 'expired':
203
- return C.error;
191
+ return C.bad;
204
192
  case 'expiring':
205
193
  case 'pending':
206
- return C.rateLimit;
194
+ return C.warn;
207
195
  case 'healthy':
208
- return C.online;
196
+ return C.good;
209
197
  default:
210
198
  return C.dim;
211
199
  }
@@ -552,10 +540,10 @@ export class ProviderHealthPanel extends BasePanel {
552
540
  { command: '/provider', summary: 'review current provider and model selection' },
553
541
  { command: '/subscription', summary: 'review provider login and subscription state' },
554
542
  ],
555
- { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
543
+ { ...DEFAULT_PANEL_PALETTE, header: C.title },
556
544
  ),
557
545
  }],
558
- palette: { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' },
546
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
559
547
  });
560
548
  }
561
549
 
@@ -581,14 +569,14 @@ export class ProviderHealthPanel extends BasePanel {
581
569
  const postureLines = [
582
570
  buildKeyValueLine(width, [
583
571
  { 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' }),
572
+ { label: 'online', value: String(online), valueColor: C.good },
573
+ { label: 'rate-limited', value: String(rateLimited), valueColor: C.warn },
574
+ { label: 'error', value: String(errored), valueColor: C.bad },
575
+ { label: 'auth alerts', value: String(expiringAuth), valueColor: expiringAuth > 0 ? C.warn : C.dim },
576
+ { label: 'account issues', value: String(accountIssues), valueColor: accountIssues > 0 ? C.bad : C.dim },
577
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }),
578
+ buildGuidanceLine(width, '/provider', 'review provider selection and routing if health posture degrades', { ...DEFAULT_PANEL_PALETTE, header: C.title }),
579
+ buildGuidanceLine(width, '/accounts', 'inspect auth routes, fallback posture, and billing-path safety', { ...DEFAULT_PANEL_PALETTE, header: C.title }),
592
580
  ];
593
581
 
594
582
  const domainLines: Line[] = [];
@@ -603,18 +591,24 @@ export class ProviderHealthPanel extends BasePanel {
603
591
  worktrees: this.deps.worktrees.getSnapshot(),
604
592
  session: this.deps.session.getSnapshot(),
605
593
  })) {
606
- domainLines.push(buildPanelLine(width, [
607
- [' ', C.label],
608
- [domain.name.padEnd(14), C.provName],
609
- [domain.summary.slice(0, Math.max(0, width - 36)).padEnd(Math.max(0, width - 36)), domainColor(domain.level)],
610
- [' ', C.label],
611
- [domain.next.slice(0, 20), C.dim],
612
- ]));
594
+ domainLines.push(buildAlignedRow(
595
+ width,
596
+ [
597
+ { text: domain.name, fg: C.value, bold: true },
598
+ { text: domain.summary, fg: domainColor(domain.level) },
599
+ { text: domain.next, fg: C.dim },
600
+ ],
601
+ [
602
+ { width: 14 },
603
+ { width: Math.max(10, width - 38) },
604
+ { width: 20 },
605
+ ],
606
+ ));
613
607
  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));
608
+ domainLines.push(...buildBodyText(width, ` ${detail}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
615
609
  }
616
610
  if (domain.nextSteps.length > 1) {
617
- domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
611
+ domainLines.push(...buildBodyText(width, ` next: ${domain.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
618
612
  }
619
613
  }
620
614
 
@@ -632,77 +626,114 @@ export class ProviderHealthPanel extends BasePanel {
632
626
  session: session.session,
633
627
  });
634
628
  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 },
629
+ { label: 'level', value: maintenance.level, valueColor: maintenance.level === 'needs-repair' ? C.bad : maintenance.level === 'suggest-compact' || maintenance.level === 'watch' ? C.warn : C.good },
636
630
  { 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 },
631
+ { label: 'usage', value: `${maintenance.usagePct}%`, valueColor: maintenance.usagePct >= 80 ? C.bad : maintenance.usagePct >= 70 ? C.warn : C.value },
638
632
  { label: 'remaining', value: maintenance.remainingTokens.toLocaleString(), valueColor: C.value },
639
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
633
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
640
634
  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));
635
+ maintenanceLines.push(...buildBodyText(width, reason, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
642
636
  }
643
637
  if (maintenance.nextSteps.length > 0) {
644
- maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
638
+ maintenanceLines.push(...buildBodyText(width, `Next: ${maintenance.nextSteps.join(' | ')}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
645
639
  }
646
640
  if (selectedName) {
647
641
  const status = selectedHealth?.status ?? 'unknown';
648
642
  selectedLines.push(buildKeyValueLine(width, [
649
- { label: 'provider', value: selectedName, valueColor: C.provName },
643
+ { label: 'provider', value: selectedName, valueColor: C.value },
650
644
  { label: 'status', value: statusLabel(status), valueColor: statusDot(status).color },
651
645
  { label: 'last ok', value: fmtAgo(selectedHealth?.lastSuccessAt), valueColor: C.value },
652
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
646
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
653
647
  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));
648
+ selectedLines.push(...buildBodyText(width, `Cooldown: ${fmtCooldown(selectedHealth.rateLimitExpiresAt)}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
655
649
  }
656
650
  if (selectedHealth?.lastErrorMessage) {
657
- selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
651
+ selectedLines.push(...buildBodyText(width, `Last error: ${selectedHealth.lastErrorMessage}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
658
652
  }
659
653
  if (selectedAccount) {
660
654
  selectedLines.push(buildKeyValueLine(width, [
661
655
  { label: 'route', value: selectedAccount.activeRoute, valueColor: routeColor(selectedAccount.activeRoute) },
662
656
  { label: 'preferred', value: selectedAccount.preferredRoute, valueColor: C.dim },
663
657
  { label: 'freshness', value: selectedAccount.authFreshness, valueColor: freshnessColor(selectedAccount.authFreshness) },
664
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }));
658
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
665
659
  selectedLines.push(buildKeyValueLine(width, [
666
660
  { 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));
661
+ { label: 'active', value: selectedAccount.active ? 'yes' : 'no', valueColor: selectedAccount.active ? C.good : C.dim },
662
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title }));
663
+ selectedLines.push(...buildBodyText(width, `Auth route: ${selectedAccount.activeRouteReason}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.dim));
670
664
  if (selectedAccount.fallbackRisk) {
671
- selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.rateLimit));
665
+ selectedLines.push(...buildBodyText(width, `Fallback: ${selectedAccount.fallbackRisk}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.warn));
672
666
  }
673
667
  if (selectedAccount.issues.length > 0) {
674
- selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.errMsg));
668
+ selectedLines.push(...buildBodyText(width, `Issue: ${selectedAccount.issues[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.bad));
675
669
  }
676
670
  if (selectedAccount.recommendedActions.length > 0) {
677
- selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, C.title));
671
+ selectedLines.push(...buildBodyText(width, `Next: ${selectedAccount.recommendedActions[0]!}`, { ...DEFAULT_PANEL_PALETTE, header: C.title }, C.title));
678
672
  }
679
673
  }
680
674
  }
681
675
 
682
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Health posture', postureLines, { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }) };
676
+ const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Health posture', postureLines, { ...DEFAULT_PANEL_PALETTE, header: C.title }) };
683
677
  const domainsSection: PanelWorkspaceSection = { title: 'Repair Domains', lines: domainLines };
684
678
  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] : [];
679
+ const selectedSections = selectedLines.length > 0 ? [{ lines: buildDetailBlock(width, 'Selected provider', selectedLines, { ...DEFAULT_PANEL_PALETTE, header: C.title }) } satisfies PanelWorkspaceSection] : [];
680
+ const footerHint = buildKeyboardHints(width, [
681
+ { keys: 'j/k or Up/Down', label: 'select provider' },
682
+ { keys: '/provider', label: 'switch model' },
683
+ { keys: '/accounts', label: 'auth routes' },
684
+ ], { ...DEFAULT_PANEL_PALETTE, header: C.title });
685
+ const providerColumnHeader = buildAlignedRow(
686
+ width,
687
+ [
688
+ { text: 'provider', fg: C.label, bold: true },
689
+ { text: 'status', fg: C.label, bold: true },
690
+ { text: 'lat', fg: C.label, bold: true },
691
+ { text: 'last ok', fg: C.label, bold: true },
692
+ { text: 'auth', fg: C.label, bold: true },
693
+ ],
694
+ [
695
+ { width: 18 },
696
+ { width: 13 },
697
+ { width: 8, align: 'right' },
698
+ { width: 10, align: 'right' },
699
+ { width: 12 },
700
+ ],
701
+ { marker: '▸' },
702
+ );
686
703
  const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
687
704
  intro,
688
- 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' },
705
+ footerLines: [footerHint],
706
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
690
707
  beforeSections: [postureSection, domainsSection, ...maintenanceSections],
691
708
  section: {
692
709
  title: 'Providers',
710
+ fixedLines: [providerColumnHeader],
693
711
  scrollableLines: providers.map((name, absolute) => {
694
712
  const health = this.providerHealthTracker.get(name);
695
713
  const status = health?.status ?? 'unknown';
714
+ const dot = statusDot(status);
696
715
  const latency = health?.lastLatencyMs !== undefined ? fmtMs(health.lastLatencyMs) : 'n/a';
697
716
  const latencyFg = health?.lastLatencyMs !== undefined ? latencyColor(health.lastLatencyMs) : C.dim;
698
- return buildPanelListRow(width, [
699
- { text: name.padEnd(16), fg: C.provName },
700
- { text: statusLabel(status).padEnd(14), fg: statusDot(status).color },
701
- { text: ' lat ', fg: C.label },
702
- { text: latency.padEnd(8), fg: latencyFg },
703
- { text: ' ok ', fg: C.label },
704
- { text: fmtAgo(health?.lastSuccessAt).padEnd(10), fg: C.value },
705
- ], { ...DEFAULT_PANEL_PALETTE, header: C.title, headerBg: '#0f172a' }, { selected: absolute === this._selectedIndex, selectedBg: '#111827' });
717
+ const account = this._accountRecords.get(name);
718
+ const authFg = account ? freshnessColor(account.authFreshness) : C.dim;
719
+ return buildAlignedRow(
720
+ width,
721
+ [
722
+ { text: `${dot.char} ${name}`, fg: C.value, bold: absolute === this._selectedIndex },
723
+ { text: statusLabel(status), fg: dot.color },
724
+ { text: latency, fg: latencyFg },
725
+ { text: fmtAgo(health?.lastSuccessAt), fg: C.value },
726
+ { text: account ? account.authFreshness : '-', fg: authFg },
727
+ ],
728
+ [
729
+ { width: 18 },
730
+ { width: 13 },
731
+ { width: 8, align: 'right' },
732
+ { width: 10, align: 'right' },
733
+ { width: 12 },
734
+ ],
735
+ { selected: absolute === this._selectedIndex, selectedBg: C.rowSelectBg, marker: '▸' },
736
+ );
706
737
  }),
707
738
  selectedIndex: this._selectedIndex,
708
739
  scrollOffset: this._scrollOffset,
@@ -724,8 +755,8 @@ export class ProviderHealthPanel extends BasePanel {
724
755
  title: 'Health',
725
756
  intro,
726
757
  sections,
727
- 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' },
758
+ footerLines: [footerHint],
759
+ palette: { ...DEFAULT_PANEL_PALETTE, header: C.title },
729
760
  });
730
761
  }
731
762
  }
@@ -5,18 +5,31 @@ import type { UiEventFeed } from '../runtime/ui-events.ts';
5
5
  import type { UiProvidersSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
6
6
  import {
7
7
  buildEmptyState,
8
+ buildKeyboardHints,
8
9
  buildKeyValueLine,
10
+ buildPanelLine,
9
11
  buildStyledPanelLine,
10
12
  buildPanelWorkspace,
11
13
  DEFAULT_PANEL_PALETTE,
14
+ extendPalette,
12
15
  type PanelWorkspaceSection,
13
16
  } from './polish.ts';
14
17
  import { truncateDisplay } from '../utils/terminal-width.ts';
18
+ import { formatLatencyMs } from '../utils/format-duration.ts';
15
19
 
16
20
  // ---------------------------------------------------------------------------
17
21
  // Constants
18
22
  // ---------------------------------------------------------------------------
19
23
 
24
+ // Domain-specific tones not covered by the base palette (latency/token greys).
25
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
26
+ muted2: '#6b7280', // secondary dim grey for unit labels
27
+ sep: '#374151', // inline separator grey
28
+ tokenDim: '#64748b', // token-count grey
29
+ p95: '#a78bfa', // p95 latency accent
30
+ latYellow: '#eab308', // mid-latency warning
31
+ });
32
+
20
33
  const SPARKLINE_CHARS = '._-:=+*#';
21
34
  const LATENCY_RING_SIZE = 20;
22
35
 
@@ -202,7 +215,10 @@ export class ProviderStatsPanel extends BasePanel {
202
215
  width,
203
216
  ' No providers registered',
204
217
  'Load or configure a provider to begin collecting per-provider latency and error metrics.',
205
- [],
218
+ [
219
+ { command: '/provider', summary: 'review current provider and model selection' },
220
+ { command: '/subscription', summary: 'sign in to a subscription-backed provider' },
221
+ ],
206
222
  DEFAULT_PANEL_PALETTE,
207
223
  ),
208
224
  },
@@ -245,6 +261,18 @@ export class ProviderStatsPanel extends BasePanel {
245
261
  title: ' Provider Stats',
246
262
  intro: 'Per-provider request performance, latency distribution, error pressure, and session totals.',
247
263
  sections: providerSections,
264
+ footerLines: [
265
+ buildPanelLine(width, [
266
+ [' sparkline ', C.muted2],
267
+ ['low', C.good],
268
+ [' → ', C.muted2],
269
+ ['high latency', C.bad],
270
+ ]),
271
+ buildKeyboardHints(width, [
272
+ { keys: '/provider', label: 'switch provider/model' },
273
+ { keys: '/health', label: 'live provider health' },
274
+ ], DEFAULT_PANEL_PALETTE),
275
+ ],
248
276
  palette: DEFAULT_PANEL_PALETTE,
249
277
  });
250
278
  }
@@ -262,7 +290,7 @@ export class ProviderStatsPanel extends BasePanel {
262
290
 
263
291
  // Determine health
264
292
  const hasErrors = rec !== undefined && rec.errors > 0;
265
- const dotColor = hasErrors ? '#ef4444' : '#22c55e';
293
+ const dotColor = hasErrors ? C.bad : C.good;
266
294
 
267
295
  // Model ID (truncated)
268
296
  const modelId = rec?.lastModelId ?? 'n/a';
@@ -271,18 +299,18 @@ export class ProviderStatsPanel extends BasePanel {
271
299
  // Header row: * provider model
272
300
  // Build as segments to avoid multi-byte char indexing issues
273
301
  const headerLine = buildStyledPanelLine(width, [
274
- { text: ' ', fg: '#94a3b8' },
302
+ { text: ' ', fg: C.label },
275
303
  { text: '●', fg: dotColor },
276
- { text: ' ', fg: '#94a3b8' },
277
- { text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg: '#e2e8f0', bold: true },
278
- { text: modelDisplay, fg: '#cbd5e1' },
304
+ { text: ' ', fg: C.label },
305
+ { text: `${truncateDisplay(provName, 14).padEnd(14)} `, fg: C.value, bold: true },
306
+ { text: modelDisplay, fg: C.accent },
279
307
  ]);
280
308
 
281
309
  rows.push(headerLine);
282
310
 
283
311
  if (rec === undefined || rec.requests === 0) {
284
312
  rows.push(buildStyledPanelLine(width, [
285
- { text: ' No requests yet.', fg: '#6b7280' },
313
+ { text: ' No requests yet.', fg: C.muted2 },
286
314
  ]));
287
315
  } else {
288
316
  const avgLatency = this._avg(rec.latencies);
@@ -291,24 +319,24 @@ export class ProviderStatsPanel extends BasePanel {
291
319
  const sparkline = this._sparkline(rec.latencies);
292
320
 
293
321
  const latFg = avgLatency < LATENCY_GREEN
294
- ? '#22c55e'
322
+ ? C.good
295
323
  : avgLatency < LATENCY_YELLOW
296
- ? '#eab308'
297
- : '#ef4444';
324
+ ? C.latYellow
325
+ : C.bad;
298
326
 
299
327
  const segments = [
300
- { text: ' avg ', fg: '#6b7280' },
328
+ { text: ' avg ', fg: C.muted2 },
301
329
  { text: this._fmtMs(avgLatency).padStart(6), fg: latFg, bold: true },
302
- { text: ' p95 ', fg: '#6b7280' },
303
- { text: this._fmtMs(p95Latency).padStart(6), fg: '#a78bfa' },
304
- { text: ' ', fg: '#374151' },
330
+ { text: ' p95 ', fg: C.muted2 },
331
+ { text: this._fmtMs(p95Latency).padStart(6), fg: C.p95 },
332
+ { text: ' ', fg: C.sep },
305
333
  { text: sparkline, fg: latFg },
306
- { text: ' err ', fg: '#6b7280' },
307
- { text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ? '#ef4444' : '#22c55e' },
308
- { text: ` ${rec.requests.toString().padStart(4)}r`, fg: '#94a3b8' },
334
+ { text: ' err ', fg: C.muted2 },
335
+ { text: `${errRate.toFixed(0).padStart(3)}%`, fg: errRate > 0 ? C.bad : C.good },
336
+ { text: ` ${rec.requests.toString().padStart(4)}r`, fg: C.label },
309
337
  ] as const;
310
338
  const tokenSegment = rec.totalTokens > 0
311
- ? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg: '#64748b' }]
339
+ ? [{ text: ` ${rec.totalTokens.toString().padStart(6)}tok`, fg: C.tokenDim }]
312
340
  : [];
313
341
  rows.push(buildStyledPanelLine(width, [...segments, ...tokenSegment]));
314
342
  }
@@ -351,10 +379,7 @@ export class ProviderStatsPanel extends BasePanel {
351
379
  }
352
380
 
353
381
  private _fmtMs(ms: number): string {
354
- if (ms <= 0) return 'n/a';
355
- if (ms >= 10000) return `${(ms / 1000).toFixed(1)}s`;
356
- if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
357
- return `${Math.round(ms)}ms`;
382
+ return formatLatencyMs(ms);
358
383
  }
359
384
 
360
385
  private _latencyColor(ms: number): string {
@@ -2,9 +2,12 @@ import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
3
  import { BasePanel } from './base-panel.ts';
4
4
  import {
5
+ buildKeyboardHints,
5
6
  buildPanelLine,
7
+ buildPanelTitle,
6
8
  DEFAULT_PANEL_PALETTE,
7
9
  } from './polish.ts';
10
+ import { truncateDisplay } from '../utils/terminal-width.ts';
8
11
  import { renderQrMatrix, generateQrMatrix } from '../renderer/qr-renderer.ts';
9
12
  import { encodeConnectionPayload } from '@pellux/goodvibes-sdk/platform/pairing';
10
13
 
@@ -101,32 +104,41 @@ export class QrPanel extends BasePanel {
101
104
  const lines: Line[] = [];
102
105
 
103
106
  const { url, token, username, password } = this.connectionInfo;
107
+ const valueWidth = Math.max(0, width - 12);
108
+
109
+ // ── Title + purpose: tell the operator exactly what this code is for ────
110
+ lines.push(buildPanelTitle(width, 'Companion Pairing', C));
111
+ lines.push(
112
+ buildPanelLine(width, [
113
+ [' Scan with the GoodVibes companion app to pair this session.', C.hint],
114
+ ]),
115
+ );
104
116
 
105
117
  // ── Connection info header ─────────────────────────────────────────────
106
118
  lines.push(createEmptyLine(width));
107
119
  lines.push(
108
120
  buildPanelLine(width, [
109
121
  [' URL ', C.label],
110
- [url.slice(0, Math.max(0, width - 12)), C.url],
122
+ [truncateDisplay(url, valueWidth), C.url],
111
123
  ]),
112
124
  );
113
125
  lines.push(
114
126
  buildPanelLine(width, [
115
127
  [' Token ', C.label],
116
- [token, C.token],
128
+ [truncateDisplay(token, valueWidth), C.token],
117
129
  ]),
118
130
  );
119
131
  lines.push(
120
132
  buildPanelLine(width, [
121
133
  [' Username ', C.label],
122
- [username.slice(0, Math.max(0, width - 12)), C.value],
134
+ [truncateDisplay(username, valueWidth), C.value],
123
135
  ]),
124
136
  );
125
137
  if (password !== undefined) {
126
138
  lines.push(
127
139
  buildPanelLine(width, [
128
140
  [' Password ', C.label],
129
- [password, C.value],
141
+ [truncateDisplay(password, valueWidth), C.value],
130
142
  ]),
131
143
  );
132
144
  }
@@ -158,13 +170,13 @@ export class QrPanel extends BasePanel {
158
170
  );
159
171
  }
160
172
 
161
- // ── Hints ──────────────────────────────────────────────────────────────
162
- const hintsLine = buildPanelLine(width, [
163
- [' r ', C.hint],
164
- ['regenerate ', C.dim],
165
- [' c ', C.hint],
166
- ['copy token', C.dim],
167
- ]);
173
+ // ── Hints ── only advertise actions that are actually wired ─────────────
174
+ const hints: Array<{ keys: string; label: string }> = [];
175
+ if (this.regenerateToken) hints.push({ keys: 'r', label: 'regenerate token' });
176
+ if (this.copyToClipboard) hints.push({ keys: 'c', label: 'copy token' });
177
+ const hintsLine = hints.length > 0
178
+ ? buildKeyboardHints(width, hints, C)
179
+ : buildPanelLine(width, [[' Pairing is read-only in this surface.', C.dim]]);
168
180
 
169
181
  // Push hints at the bottom if we have room, otherwise append after QR
170
182
  const remaining = height - lines.length;
@@ -200,7 +200,7 @@ export class RemotePanel extends BasePanel {
200
200
  if (daemon.lastError) {
201
201
  postureLines.push(buildPanelLine(width, [
202
202
  [' daemon error ', C.label],
203
- [daemon.lastError.slice(0, Math.max(0, width - 14)), C.error],
203
+ [truncateDisplay(daemon.lastError, Math.max(0, width - 14)), C.error],
204
204
  ]));
205
205
  }
206
206
  postureLines.push(
@@ -208,9 +208,16 @@ export class RemotePanel extends BasePanel {
208
208
  buildGuidanceLine(width, '/remote capabilities', 'inspect transport support before routing remote work or reattaching a session', C),
209
209
  );
210
210
 
211
+ const canBrowse = activeConnections.length > 0 || contracts.length > 0;
212
+ const canSwitch = contracts.length > 0 && activeConnections.length > 0;
213
+ const navHint = !canBrowse
214
+ ? ` focus=${this.browseMode} idle - no connections or contracts to browse`
215
+ : canSwitch
216
+ ? ` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`
217
+ : ` focus=${this.browseMode} Up/Down move`;
211
218
  const footerLines = [
212
219
  buildGuidanceLine(width, '/remote setup', 'review bridge, tunnel, env, and bootstrap flows for self-hosted remote work', C),
213
- buildPanelLine(width, [[` focus=${this.browseMode} Up/Down move Tab switch connections/contracts`, C.dim]]),
220
+ buildPanelLine(width, [[navHint, C.dim]]),
214
221
  ] as const;
215
222
 
216
223
  if (activeConnections.length === 0 && contracts.length === 0) {
@@ -318,7 +325,7 @@ export class RemotePanel extends BasePanel {
318
325
  [' Review: ', C.label],
319
326
  [supervisorEntry.negotiation.reviewMode, supervisorEntry.negotiation.reviewMode === 'wrfc' ? C.ok : C.dim],
320
327
  ]));
321
- detailRows.push(buildPanelLine(width, [[` ${supervisorEntry.heartbeat.detail}`.slice(0, width), C.dim]]));
328
+ detailRows.push(buildPanelLine(width, [[truncateDisplay(` ${supervisorEntry.heartbeat.detail}`, width), C.dim]]));
322
329
  }
323
330
 
324
331
  const recentArtifact = snapshot.artifacts.find((artifact) => artifact.runnerId === selected.agentId);
@@ -400,7 +407,7 @@ export class RemotePanel extends BasePanel {
400
407
  { text: ` ${connection.transportState.padEnd(18)}`, fg: stateColor(connection.transportState) },
401
408
  { text: ` msgs=${String(connection.messageCount).padEnd(6)}`, fg: C.info },
402
409
  { text: ` errs=${String(connection.errorCount).padEnd(4)}`, fg: connection.errorCount > 0 ? C.warn : C.dim },
403
- { text: ` ${connection.label}`.slice(0, Math.max(0, width - 56)), fg: C.dim },
410
+ { text: truncateDisplay(` ${connection.label}`, Math.max(0, width - 56)), fg: C.dim },
404
411
  ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
405
412
  })
406
413
  : [
@@ -408,7 +415,7 @@ export class RemotePanel extends BasePanel {
408
415
  return buildPanelListRow(width, [
409
416
  { text: contract.runnerId.padEnd(18), fg: C.value },
410
417
  { text: ` ${contract.transport.state.padEnd(18)}`, fg: stateColor(contract.transport.state) },
411
- { text: ` ${contract.template}`.slice(0, Math.max(0, width - 42)), fg: C.dim },
418
+ { text: truncateDisplay(` ${contract.template}`, Math.max(0, width - 42)), fg: C.dim },
412
419
  ], C, { selected: absolute === this.selectedIndex, selectedBg: C.headerBg });
413
420
  }),
414
421
  buildPanelLine(width, [[' No active connection is currently attached to these contracts.', C.dim]]),