@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,342 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { fitDisplay } from '../utils/terminal-width.ts';
4
- import { formatElapsed } from '../utils/format-elapsed.ts';
5
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
6
- import type { KeyName, PanelIntegrationContext } from './types.ts';
7
- import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config';
8
- import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
9
- import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
10
- import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
11
- import {
12
- buildEmptyState,
13
- buildKeyboardHints,
14
- buildKeyValueLine,
15
- buildPanelListRow,
16
- buildPanelLine,
17
- buildSummaryBlock,
18
- buildPanelWorkspace,
19
- DEFAULT_PANEL_PALETTE,
20
- } from './polish.ts';
21
-
22
- // Base chrome only — title band, state colors, and text tokens all come
23
- // straight from DEFAULT_PANEL_PALETTE (WO-002).
24
- const C = DEFAULT_PANEL_PALETTE;
25
-
26
- interface SubscriptionRow {
27
- readonly provider: string;
28
- readonly hasOAuthConfig: boolean;
29
- readonly subscription: ProviderSubscription | null;
30
- readonly pending: PendingSubscriptionLogin | null;
31
- }
32
-
33
- function statusOf(row: SubscriptionRow): 'active' | 'pending' | 'available' | 'unconfigured' {
34
- if (row.subscription) return 'active';
35
- if (row.pending) return 'pending';
36
- return row.hasOAuthConfig ? 'available' : 'unconfigured';
37
- }
38
-
39
- function statusColor(status: ReturnType<typeof statusOf>): string {
40
- switch (status) {
41
- case 'active':
42
- return C.good;
43
- case 'pending':
44
- return C.warn;
45
- case 'available':
46
- return C.info;
47
- case 'unconfigured':
48
- return C.dim;
49
- }
50
- }
51
-
52
- // Highlight window for "expiring soon" — mirrors the provider-console
53
- // convention of flagging tokens inside their last day of validity so the
54
- // operator can refresh/re-login before the subscription actually lapses.
55
- const EXPIRING_SOON_MS = 24 * 60 * 60 * 1000;
56
-
57
- function isExpiringSoon(subscription: ProviderSubscription | null): boolean {
58
- if (!subscription?.expiresAt) return false;
59
- const remaining = subscription.expiresAt - Date.now();
60
- return remaining > 0 && remaining <= EXPIRING_SOON_MS;
61
- }
62
-
63
- export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
64
- private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
65
- private readonly subscriptionManager: SubscriptionAccessQuery;
66
- private rows: SubscriptionRow[] = [];
67
- /** Pending logout confirmation — uses project-standard ConfirmState contract. */
68
- private confirm: ConfirmState<string> | null = null;
69
- // Set by handleInput (Enter on an 'available' row) and consumed on the very
70
- // next handlePanelIntegrationAction dispatch of that same key — handleInput
71
- // has no ctx.executeCommand, so the actual login-start dispatch happens via
72
- // the bridge (remote-panel.ts / token-budget-panel.ts pattern).
73
- private pendingCommand: { name: string; args: string[] } | null = null;
74
- // Background posture refresh while the panel is visible, so a login/logout
75
- // that completes elsewhere (e.g. the browser OAuth handshake finishing)
76
- // shows up without requiring an explicit 'r'.
77
- private refreshTimer: ReturnType<typeof setInterval> | null = null;
78
-
79
- public constructor(
80
- serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>,
81
- subscriptionManager: SubscriptionAccessQuery,
82
- ) {
83
- super('subscription', 'Subscriptions', 'B', 'providers');
84
- this.showSelectionGutter = true; // I5: non-color selection affordance
85
- this.serviceRegistry = serviceRegistry;
86
- this.subscriptionManager = subscriptionManager;
87
- }
88
-
89
- public override onActivate(): void {
90
- super.onActivate();
91
- this.refresh();
92
- if (this.refreshTimer === null) {
93
- this.refreshTimer = this.registerTimer(setInterval(() => {
94
- this.refresh();
95
- this.markDirty();
96
- }, 5_000));
97
- }
98
- }
99
-
100
- public override onDeactivate(): void {
101
- if (this.refreshTimer !== null) {
102
- this.clearTimer(this.refreshTimer);
103
- this.refreshTimer = null;
104
- }
105
- }
106
-
107
- /** Consumes the login-start command queued by handleInput's Enter-on-'available' branch. */
108
- public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
109
- if (!this.pendingCommand) return false;
110
- const { name, args } = this.pendingCommand;
111
- this.pendingCommand = null;
112
- void ctx.executeCommand?.(name, args);
113
- return true;
114
- }
115
-
116
- protected override getPalette() { return C; }
117
- protected override getEmptyStateMessage() { return ' No provider subscriptions are active yet.'; }
118
- protected override getEmptyStateActions() {
119
- return [
120
- { command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
121
- { command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
122
- { command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
123
- ];
124
- }
125
-
126
- protected getItems(): readonly SubscriptionRow[] {
127
- return this.rows;
128
- }
129
-
130
- protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
131
- const status = statusOf(row);
132
- // Expiring-soon highlight: the STATUS text stays 'ACTIVE' (the row is
133
- // still active), but it's colored as a warning and gets a trailing
134
- // marker so an about-to-lapse token stands out from the list.
135
- const expiringSoon = isExpiringSoon(row.subscription);
136
- return buildPanelListRow(width, [
137
- { text: fitDisplay(row.provider, 16), fg: C.value },
138
- { text: ` ${status.toUpperCase().padEnd(12)}`, fg: expiringSoon ? C.warn : statusColor(status) },
139
- { text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
140
- { text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
141
- ...(expiringSoon ? [{ text: ' expiring soon', fg: C.warn }] : []),
142
- ], C, { selected, selectedBg: C.selectBg });
143
- }
144
-
145
- public handleInput(key: KeyName): boolean {
146
- // Project-standard confirm contract: Enter/y confirm; n/Esc cancel; other absorbed.
147
- const confirmResult = handleConfirmInput(this.confirm, key);
148
- if (confirmResult === 'confirmed') {
149
- const provider = this.confirm!.subject;
150
- this.confirm = null;
151
- this.subscriptionManager.logout(provider);
152
- this.refresh();
153
- this.markDirty();
154
- return true;
155
- }
156
- if (confirmResult === 'cancelled') {
157
- this.confirm = null;
158
- this.markDirty();
159
- return true;
160
- }
161
- if (confirmResult === 'absorbed') return true;
162
-
163
- if (this.rows.length === 0) return false;
164
- const selected = this.getSelectedItem() ?? null;
165
- if (key === 'up' || key === 'k') {
166
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
167
- this.confirm = null;
168
- this.markDirty();
169
- return true;
170
- }
171
- if (key === 'down' || key === 'j') {
172
- this.selectedIndex = Math.min(this.rows.length - 1, this.selectedIndex + 1);
173
- this.confirm = null;
174
- this.markDirty();
175
- return true;
176
- }
177
- if (key === 'enter' || key === 'return') {
178
- if (selected?.subscription) {
179
- this.confirm = { subject: selected.provider, label: selected.provider };
180
- this.markDirty();
181
- return true;
182
- }
183
- // Enter on an 'available' row starts the real login flow via the
184
- // bridge instead of leaving it as a printed signpost.
185
- if (selected && statusOf(selected) === 'available') {
186
- this.pendingCommand = { name: 'subscription', args: ['login', selected.provider, 'start'] };
187
- return true;
188
- }
189
- return false;
190
- }
191
- if (key === 'r') {
192
- this.refresh();
193
- this.confirm = null;
194
- this.markDirty();
195
- return true;
196
- }
197
- return false;
198
- }
199
-
200
- private refresh(): void {
201
- const manager = this.subscriptionManager;
202
- const services = this.serviceRegistry.getAll();
203
- const builtinProviders = listBuiltinSubscriptionProviders();
204
- const providers = new Set<string>([
205
- ...builtinProviders.map((provider) => provider.provider),
206
- ...Object.values(services)
207
- .filter((service) => service.authType === 'oauth' && service.oauth)
208
- .map((service) => service.providerId ?? service.name),
209
- ...manager.list().map((entry) => entry.provider),
210
- ...manager.listPending().map((entry) => entry.provider),
211
- ]);
212
- this.rows = [...providers]
213
- .sort((a, b) => a.localeCompare(b))
214
- .map((provider) => {
215
- const service = Object.values(services).find((entry) => (
216
- (entry.providerId ?? entry.name) === provider && entry.authType === 'oauth' && entry.oauth
217
- ));
218
- const builtin = builtinProviders.find((entry) => entry.provider === provider);
219
- return {
220
- provider,
221
- hasOAuthConfig: Boolean(service?.oauth) || Boolean(builtin),
222
- subscription: manager.get(provider),
223
- pending: manager.getPending(provider),
224
- } satisfies SubscriptionRow;
225
- });
226
- this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
227
- }
228
-
229
- /** Footer hints that reflect the current view: confirm-pending vs browsing. */
230
- private buildFooterHint(width: number): Line {
231
- if (this.confirm) {
232
- return buildKeyboardHints(width, [
233
- { keys: 'y/Enter', label: 'confirm sign out' },
234
- { keys: 'n/Esc', label: 'cancel' },
235
- ], C);
236
- }
237
- const selected = this.getSelectedItem();
238
- const hints: Array<{ keys: string; label: string }> = [{ keys: 'Up/Down', label: 'select' }];
239
- if (selected?.subscription) hints.push({ keys: 'Enter', label: 'sign out' });
240
- else if (selected?.hasOAuthConfig) hints.push({ keys: 'Enter', label: 'sign in' });
241
- hints.push({ keys: 'r', label: 'refresh' });
242
- return buildKeyboardHints(width, hints, C);
243
- }
244
-
245
- // WO-139: refresh() moved out of render() (hooks-panel.ts:109 pattern) — it
246
- // now runs on onActivate(), on 'r', and on the 5s background timer above,
247
- // instead of rebuilding the row set (service registry scan + builtin
248
- // provider list + subscription-manager reads) on every single frame.
249
- public render(width: number, height: number): Line[] {
250
- this.clampSelection();
251
- const selected = this.getSelectedItem();
252
- const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
253
-
254
- const activeCount = this.rows.filter((row) => row.subscription).length;
255
- const pendingCount = this.rows.filter((row) => row.pending).length;
256
- const postureLines = [
257
- buildKeyValueLine(width, [
258
- { label: 'configured', value: String(this.rows.filter((row) => row.hasOAuthConfig).length), valueColor: C.info },
259
- { label: 'active', value: String(activeCount), valueColor: activeCount > 0 ? C.good : C.dim },
260
- { label: 'pending', value: String(pendingCount), valueColor: pendingCount > 0 ? C.warn : C.dim },
261
- { label: 'providers', value: String(this.rows.length), valueColor: C.value },
262
- ], C),
263
- buildKeyValueLine(width, [
264
- { label: 'selected', value: (selected?.provider ?? 'none'), valueColor: selected ? C.value : C.dim },
265
- { label: 'status', value: selected ? statusOf(selected) : 'n/a', valueColor: selected ? statusColor(statusOf(selected)) : C.dim },
266
- ], C),
267
- ];
268
-
269
- // Empty state: render posture + base empty state
270
- if (this.rows.length === 0) {
271
- const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
272
- const emptyLines = buildEmptyState(
273
- width,
274
- this.getEmptyStateMessage(),
275
- 'Built-in OAuth-capable providers and configured service providers will appear here once available for browser login or session import.',
276
- this.getEmptyStateActions(),
277
- C,
278
- );
279
- const workspace = buildPanelWorkspace(width, height, {
280
- title: 'Provider Subscriptions',
281
- intro,
282
- sections: [{ lines: [...summaryLines, ...emptyLines] }],
283
- footerLines: [
284
- this.buildFooterHint(width),
285
- ],
286
- palette: C,
287
- });
288
- while (workspace.length < height) workspace.push(createEmptyLine(width));
289
- return workspace.slice(0, height);
290
- }
291
-
292
- const selectedRow = selected;
293
- const detailRows: Line[] = [];
294
- if (selectedRow) {
295
- detailRows.push(buildKeyValueLine(width, [
296
- { label: 'provider', value: selectedRow.provider, valueColor: C.value },
297
- { label: 'status', value: statusOf(selectedRow), valueColor: statusColor(statusOf(selectedRow)) },
298
- { label: 'oauth config', value: selectedRow.hasOAuthConfig ? 'present' : 'missing', valueColor: selectedRow.hasOAuthConfig ? C.good : C.bad },
299
- ], C));
300
- if (selectedRow.subscription) {
301
- const expiringSoon = isExpiringSoon(selectedRow.subscription);
302
- const expires = selectedRow.subscription.expiresAt
303
- ? new Date(selectedRow.subscription.expiresAt).toISOString()
304
- : 'n/a';
305
- detailRows.push(buildKeyValueLine(width, [
306
- { label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
307
- { label: 'expires', value: expiringSoon ? `${expires} (expiring soon)` : expires, valueColor: expiringSoon ? C.warn : C.dim },
308
- ], C));
309
- detailRows.push(buildPanelLine(width, [[
310
- ` ${selectedRow.subscription.overrideAmbientApiKeys
311
- ? 'Provider subscription overrides ambient API-key resolution for this provider.'
312
- : 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
313
- C.dim,
314
- ]]));
315
- if (expiringSoon) {
316
- detailRows.push(buildPanelLine(width, [[' Token expires within 24h — sign out and re-login (Enter) to refresh it before it lapses.', C.warn]]));
317
- }
318
- if (this.confirm?.subject === selectedRow.provider) {
319
- detailRows.push(buildPanelLine(width, [[` Sign out ${selectedRow.provider}? Press y or Enter to confirm, n or Esc to cancel.`, C.warn]]));
320
- }
321
- } else if (selectedRow.pending) {
322
- const age = formatElapsed(Date.now() - selectedRow.pending.createdAt);
323
- detailRows.push(buildPanelLine(width, [[` Login is pending (started ${age} ago). Finish with /subscription login <provider> finish <code>.`, C.warn]]));
324
- } else if (selectedRow.hasOAuthConfig) {
325
- detailRows.push(buildPanelLine(width, [[' Ready for login. Press Enter to start it here.', C.dim]]));
326
- } else {
327
- detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
328
- }
329
- }
330
-
331
- const headerLines: Line[] = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
332
-
333
- return this.renderList(width, height, {
334
- title: 'Provider Subscriptions',
335
- header: headerLines,
336
- footer: [
337
- ...detailRows,
338
- this.buildFooterHint(width),
339
- ],
340
- });
341
- }
342
- }