@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }