@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. 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
- }