@pellux/goodvibes-tui 1.16.1 → 1.18.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 (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -0,0 +1,160 @@
1
+ /**
2
+ * NotificationsPanel — the visible home for notifications routed to the
3
+ * `panel_only` target (see notifications-feed.ts for why this exists).
4
+ *
5
+ * Renders every entry the feed has accumulated: standalone panel-routed
6
+ * notifications, and collapsed burst/batch groups shown honestly with their
7
+ * real running count and the plain-language reason they were held back from
8
+ * the conversation. Descriptive text (titles, bodies, reason descriptions)
9
+ * is always wrapped in full — never clipped to a column width.
10
+ */
11
+ import type { Line } from '../types/grid.ts';
12
+ import type { RoutingDecision } from '@/runtime/index.ts';
13
+ import { BasePanel } from './base-panel.ts';
14
+ import {
15
+ buildEmptyState,
16
+ buildKeyboardHints,
17
+ buildPanelWorkspace,
18
+ buildStyledPanelLine,
19
+ DEFAULT_PANEL_PALETTE,
20
+ resolveScrollablePanelSection,
21
+ } from './polish.ts';
22
+ import { wrapText } from '../utils/terminal-width.ts';
23
+ import { getSharedNotificationFeed, type PanelFeedEntry, type PanelNotificationFeed } from './notifications-feed.ts';
24
+
25
+ // Reuses the shared, mode-aware (dark/light) panel palette's existing
26
+ // bad/warn/info/dim tokens rather than minting new hex literals — this
27
+ // panel has no color needs the shared palette doesn't already name.
28
+ const C = DEFAULT_PANEL_PALETTE;
29
+
30
+ /** Plain-language description of why a notification never reached the conversation or status bar. Every reason code is covered so a new one fails visibly (a TypeScript error on this object) rather than silently. */
31
+ const REASON_DESCRIPTIONS: Record<RoutingDecision['reasonCode'], string> = {
32
+ allowed: 'routed here by domain verbosity',
33
+ quiet_while_typing: 'held while you were typing',
34
+ mode_context_minimal: 'held back by quiet mode',
35
+ mode_context_normal: 'held back by balanced mode',
36
+ burst_collapsed: 'rapid repeats collapsed',
37
+ batch_window_collapsed: 'repeated within a few seconds, collapsed',
38
+ domain_verbosity_low: "below this domain's verbosity setting",
39
+ };
40
+
41
+ function levelColor(level: PanelFeedEntry['level']): string {
42
+ switch (level) {
43
+ case 'critical': return C.bad;
44
+ case 'warning': return C.warn;
45
+ case 'debug': return C.dim;
46
+ default: return C.info;
47
+ }
48
+ }
49
+
50
+ function levelTag(level: PanelFeedEntry['level']): string {
51
+ return `[${level}]`;
52
+ }
53
+
54
+ /** Build every Line an entry needs — title/meta row, then the full body and reason text wrapped with no line cap (descriptive text is never clipped). */
55
+ function renderEntry(entry: PanelFeedEntry, width: number): Line[] {
56
+ const lines: Line[] = [];
57
+ const fg = levelColor(entry.level);
58
+ const countSuffix = entry.collapsedCount > 1 ? ` ×${entry.collapsedCount}` : '';
59
+ const headerText = `${levelTag(entry.level)} ${entry.domain} — ${entry.title}${countSuffix}`;
60
+ for (const wrapped of wrapText(headerText, Math.max(1, width - 1))) {
61
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${wrapped}`, fg, bold: true }]));
62
+ }
63
+ const reasonText = entry.collapsedCount > 1
64
+ ? ` ${entry.collapsedCount} notifications collapsed — ${REASON_DESCRIPTIONS[entry.reasonCode]}`
65
+ : ` ${REASON_DESCRIPTIONS[entry.reasonCode]}`;
66
+ for (const wrapped of wrapText(reasonText, Math.max(1, width - 1))) {
67
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${wrapped}`, fg: C.dim }]));
68
+ }
69
+ if (entry.body) {
70
+ for (const wrapped of wrapText(` ${entry.body}`, Math.max(1, width - 1))) {
71
+ lines.push(buildStyledPanelLine(width, [{ text: ` ${wrapped}`, fg: C.value }]));
72
+ }
73
+ }
74
+ return lines;
75
+ }
76
+
77
+ export class NotificationsPanel extends BasePanel {
78
+ private readonly unsubscribe: () => void;
79
+ private scrollOffset = 0;
80
+
81
+ constructor(private readonly feed: PanelNotificationFeed = getSharedNotificationFeed()) {
82
+ super('notifications', 'Notifications', 'N', 'runtime-ops');
83
+ this.unsubscribe = this.feed.subscribe(() => this.markDirty());
84
+ }
85
+
86
+ override onDestroy(): void {
87
+ this.unsubscribe();
88
+ super.onDestroy();
89
+ }
90
+
91
+ handleInput(key: string): boolean {
92
+ switch (key) {
93
+ case 'up': return this.scroll(-1);
94
+ case 'down': return this.scroll(1);
95
+ case 'pageup': return this.scroll(-10);
96
+ case 'pagedown': return this.scroll(10);
97
+ default: return false;
98
+ }
99
+ }
100
+
101
+ private scroll(delta: number): boolean {
102
+ const entries = this.feed.list();
103
+ if (entries.length <= 1) return false;
104
+ const maxOffset = Math.max(0, entries.length - 1);
105
+ const prev = this.scrollOffset;
106
+ this.scrollOffset = Math.max(0, Math.min(maxOffset, this.scrollOffset + delta));
107
+ if (this.scrollOffset !== prev) this.markDirty();
108
+ return true;
109
+ }
110
+
111
+ render(width: number, height: number): Line[] {
112
+ if (width <= 0 || height <= 0) return [];
113
+ const entries = this.feed.list();
114
+ const hintRow = buildKeyboardHints(width, [
115
+ { keys: 'Up/Down', label: 'scroll' },
116
+ { keys: 'PgUp/PgDn', label: 'page' },
117
+ ], DEFAULT_PANEL_PALETTE);
118
+
119
+ if (entries.length === 0) {
120
+ return buildPanelWorkspace(width, height, {
121
+ title: ' Notifications',
122
+ intro: 'Notifications held back from the conversation or status bar (panel_only) land here, including their real collapsed counts.',
123
+ sections: [{
124
+ lines: buildEmptyState(
125
+ width,
126
+ ' No panel-routed notifications yet',
127
+ 'Nothing has been routed here this session. Quiet/balanced-mode operational chatter and collapsed bursts will appear here honestly, with their real counts, once something routes to this target.',
128
+ [],
129
+ DEFAULT_PANEL_PALETTE,
130
+ ),
131
+ }],
132
+ footerLines: [hintRow],
133
+ palette: DEFAULT_PANEL_PALETTE,
134
+ });
135
+ }
136
+
137
+ const scrollableLines = entries.flatMap((entry) => renderEntry(entry, width - 2));
138
+ const section = resolveScrollablePanelSection(width, height, {
139
+ intro: 'Notifications held back from the conversation or status bar (panel_only) land here, including their real collapsed counts.',
140
+ footerLines: [hintRow],
141
+ palette: DEFAULT_PANEL_PALETTE,
142
+ section: {
143
+ title: `Notifications (${entries.length})`,
144
+ scrollableLines,
145
+ scrollOffset: this.scrollOffset,
146
+ minRows: 4,
147
+ appendWindowSummary: scrollableLines.length > 0 ? { dimColor: DEFAULT_PANEL_PALETTE.dim } : undefined,
148
+ },
149
+ });
150
+ this.scrollOffset = section.scrollOffset;
151
+
152
+ return buildPanelWorkspace(width, height, {
153
+ title: ' Notifications',
154
+ intro: 'Notifications held back from the conversation or status bar (panel_only) land here, including their real collapsed counts.',
155
+ sections: [section.section],
156
+ footerLines: [hintRow],
157
+ palette: DEFAULT_PANEL_PALETTE,
158
+ });
159
+ }
160
+ }
@@ -34,18 +34,18 @@ export interface WorkspaceTab {
34
34
  export class PanelManager {
35
35
  private registry: PanelRegistration[] = [];
36
36
  private retainedPanels = new Map<string, Panel>();
37
- /** Old/absorbed panel id -> merged target id (WO-1xx console merges). */
37
+ /** Old/absorbed panel id -> merged target id (from the console panel merges). */
38
38
  private aliases = new Map<string, string>();
39
39
  /**
40
- * Retired panel id -> modal name (W6.1 purge, MIGRATE-TO-MODAL surfaces).
40
+ * Retired panel id -> modal name (panels migrated to config-modal surfaces).
41
41
  * Unlike `aliases` (panel -> panel), a hit here means no panel is ever
42
42
  * constructed for this id — `open()` invokes `openModalCallback` instead
43
- * and returns a sentinel. Registrations are added by WO-A/B; this map and
44
- * the open()-time check are WO-C's mechanism only.
43
+ * and returns a sentinel. This map and the open()-time check are the
44
+ * sole mechanism for that redirect.
45
45
  */
46
46
  private modalRedirects = new Map<string, string>();
47
47
  /**
48
- * Modal name -> the surface the config-modal host renders (W6.1). Built once
48
+ * Modal name -> the surface the config-modal host renders. Built once
49
49
  * in builtin-modals.ts (closing over read-models) and looked up by
50
50
  * ui-openers' openModal callback. Distinct from `modalRedirects` (panel id ->
51
51
  * modal name): a redirect resolves a legacy panel id to a name; this map
@@ -100,7 +100,7 @@ export class PanelManager {
100
100
 
101
101
  registerType(registration: PanelRegistration): void {
102
102
  const existing = this.registry.findIndex(r => r.id === registration.id);
103
- // WO-152: registry-time icon-uniqueness assertion. Tab-bar icons are a
103
+ // registry-time icon-uniqueness assertion. Tab-bar icons are a
104
104
  // single glyph; two panels sharing one silently made the workspace tab
105
105
  // strip ambiguous (the historical W/R/U/K/M/Q/Y/J/P collisions). Compare
106
106
  // against every OTHER registration (excluding a re-registration of the
@@ -276,7 +276,7 @@ export class PanelManager {
276
276
  }
277
277
 
278
278
  /**
279
- * Hand an optional deep-link target (DEBT-5 item 4) to a panel that just
279
+ * Hand an optional deep-link target (item 4) to a panel that just
280
280
  * resolved from open(). Panels without a "node" concept simply don't
281
281
  * implement `receiveDeepLink` — the call is a no-op via optional chaining.
282
282
  */
@@ -496,7 +496,7 @@ export class PanelManager {
496
496
  this.bottomPane.panels.push(panel);
497
497
  this.bottomPane.activeIndex = 0;
498
498
  } else {
499
- // Open a predictable default panel in the bottom pane. W6.1 purge:
499
+ // Open a predictable default panel in the bottom pane. purge:
500
500
  // 'panel-list' was deleted (dead weight over a 5-panel registry —
501
501
  // see the DELETE disposition), so the default is explicitly
502
502
  // 'fleet' rather than falling back to registry[0] (whatever
@@ -606,7 +606,7 @@ export class PanelManager {
606
606
  toggle(): void {
607
607
  this._visible = !this._visible;
608
608
  // Auto-open a default panel if toggling visible with nothing open.
609
- // W6.1 purge: explicitly 'fleet' rather than registry[0] — see the
609
+ // purge: explicitly 'fleet' rather than registry[0] — see the
610
610
  // matching comment in toggleBottomPane() above.
611
611
  if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
612
612
  const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
@@ -21,11 +21,11 @@ import {
21
21
  } from './polish.ts';
22
22
 
23
23
  // Base chrome only — title band, state colors, and text tokens all come
24
- // straight from DEFAULT_PANEL_PALETTE (WO-002).
24
+ // straight from DEFAULT_PANEL_PALETTE.
25
25
  const C = DEFAULT_PANEL_PALETTE;
26
26
 
27
27
  /**
28
- * WO-134: PluginsPanel drives real plugin-manager mutations (enable/disable/
28
+ * PluginsPanel drives real plugin-manager mutations (enable/disable/
29
29
  * verify/lift-quarantine) from the keyboard, not just read-only inspection.
30
30
  * `PluginManagerObserver` alone (the read-only surface most panels need) has
31
31
  * no mutation methods, so the constructor param is widened to also require
@@ -87,7 +87,7 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
87
87
  || plugin.version.toLowerCase().includes(q);
88
88
  }
89
89
 
90
- // WO-134: selection is preserved across onActivate — BasePanel's default
90
+ // selection is preserved across onActivate — BasePanel's default
91
91
  // (mark dirty, no index reset) is exactly what we want, so no override.
92
92
 
93
93
  public override onDestroy(): void {
@@ -149,7 +149,7 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
149
149
  }
150
150
 
151
151
  /**
152
- * WO-134: optional one-key capture-to-memory for a quarantined plugin — 'm'
152
+ * optional one-key capture-to-memory for a quarantined plugin — 'm'
153
153
  * dispatches `/recall capture plugin <name>` (recall-capture.ts:146 pattern)
154
154
  * instead of requiring the operator to type the command by hand.
155
155
  */
@@ -37,7 +37,7 @@ function compactAnswerDetail(text: string): string {
37
37
  * recommendedAnswer echoing one of the scope answers verbatim, must not show
38
38
  * the same suggested answer twice), then the fixed ask-narrower/custom actions.
39
39
  *
40
- * DEBT-3: dismissing planning is no longer a pseudo answer-row — it is a
40
+ * dismissing planning is no longer a pseudo answer-row — it is a
41
41
  * first-class confirmed modal action (the `d` key), so no dismiss row is
42
42
  * produced here. A canned answer records against the current open question via
43
43
  * /plan answer; the custom row's free-form text is submitted to chat.
@@ -1,7 +1,7 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // provider-health-routes.ts
3
3
  //
4
- // Single-source auth-route posture for the provider console (WO-112 merge of
4
+ // Single-source auth-route posture for the provider console (merge of
5
5
  // the retired accounts panel into provider-health). All route data flows from
6
6
  // ProviderRuntimeInspectionQuery.inspectAll() snapshots; this module only
7
7
  // normalizes descriptors (synthesizing them for providers that do not declare
@@ -137,7 +137,7 @@ function pickRoute(
137
137
  /**
138
138
  * Derive the console's account/auth posture for one inspectAll() snapshot.
139
139
  * This is the only route-posture derivation in the TUI (the retired accounts
140
- * panel's parallel snapshot model was deleted with WO-112).
140
+ * panel's parallel snapshot model was deleted with).
141
141
  */
142
142
  export function buildAccountPosture(snapshot: ProviderRuntimeSnapshotLike): ProviderAccountPosture {
143
143
  const auth = snapshot.runtime.auth;
@@ -259,7 +259,7 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
259
259
  public constructor(options: SkillsPanelOptions) {
260
260
  super('skills', 'Skills', '▩', 'automation-control', options.componentHealthMonitor);
261
261
  this.showSelectionGutter = true; // I5: non-color selection affordance
262
- this.filterEnabled = true; // WO-153: converged modal '/' filter
262
+ this.filterEnabled = true; // converged modal '/' filter
263
263
  this.filterLabel = 'Filter';
264
264
  this.shellPaths = options.shellPaths;
265
265
  this.ecosystemPaths = options.ecosystemPaths;
@@ -367,12 +367,12 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
367
367
 
368
368
  public override onDestroy(): void {}
369
369
 
370
- // W6.1 (the purge): Enter used to open the skill's markdown source in the
370
+ // (the purge): Enter used to open the skill's markdown source in the
371
371
  // preview panel via a staged pendingOpenPreview flag + handlePanelIntegrationAction
372
372
  // (same pattern diff-panel.ts used). 'preview' is DELETE-disposition with no
373
373
  // successor surface (no file-picker-overlay preview to repoint to either —
374
374
  // verified), so that cross-panel jump was removed rather than repointed;
375
- // Enter is currently a no-op on this list (browse-only until WO-B migrates
375
+ // Enter is currently a no-op on this list (browse-only until migrates
376
376
  // Skills to a modal).
377
377
 
378
378
  public handleInput(key: string): boolean {
@@ -393,8 +393,7 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
393
393
  if (confirmResult === 'absorbed') return true;
394
394
 
395
395
  // I1: 'd' prompts delete confirmation — only outside filter mode, so 'd'
396
- // remains typeable into the filter query while it is active (WO-153:
397
- // converged modal '/' filter coexists with single-letter action keys).
396
+ // remains typeable into the filter query while it is active (// converged modal '/' filter coexists with single-letter action keys).
398
397
  if (!this.filterActive && key === 'd') {
399
398
  const skill = this.getSelectedItem();
400
399
  if (skill) {
@@ -7,7 +7,7 @@ import type { TurnEvent } from '@/runtime/index.ts';
7
7
  import type { UiEventFeed } from '../runtime/ui-events.ts';
8
8
  import type { UiReadModel, UiSessionSnapshot } from '../runtime/ui-read-models.ts';
9
9
  import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
10
- import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
10
+ import { calcSessionCost, describePricingSource, isModelPriced } from '../export/cost-utils.ts';
11
11
  import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
12
12
  import type { PanelIntegrationContext } from './types.ts';
13
13
  import {
@@ -460,7 +460,7 @@ export class TokenBudgetPanel extends BasePanel {
460
460
 
461
461
  if (status.guidanceMode !== 'off' && status.nextSteps.length > 0) {
462
462
  const nextStep = status.nextSteps[0]!;
463
- // WO-160: when the recommended next step is /compact and C is armed
463
+ // when the recommended next step is /compact and C is armed
464
464
  // (see handleInput's elevated-only C branch and the footer hint),
465
465
  // advertise the key instead of the command — pressing C already
466
466
  // dispatches it for real, so printing '/compact' here was a redundant
@@ -618,9 +618,17 @@ export class TokenBudgetPanel extends BasePanel {
618
618
  const modelId = this.getCurrentModelId();
619
619
  const priced = isModelPriced(modelId);
620
620
  const cost = calcSessionCost(u.input, u.output, u.cacheRead, u.cacheWrite, modelId);
621
+ // Dollars carry their source; an unknown price renders the explicit
622
+ // marker (never $0.00) and names the one-key fix on the cost panel.
623
+ const source = priced ? describePricingSource(modelId) : null;
621
624
  lines.push(buildStyledPanelLine(width, [
622
625
  { text: ' Cost: ', fg: C.label },
623
- { text: priced ? `$${cost.toFixed(4)}` : 'unpriced', fg: C.value, bold: true },
626
+ { text: priced ? `$${cost.toFixed(4)}` : 'price unknown', fg: C.value, bold: true },
627
+ ...(priced && source
628
+ ? [{ text: ` (${source})`, fg: C.dim }]
629
+ : priced
630
+ ? []
631
+ : [{ text: ' — set it with p on the cost panel', fg: C.dim }]),
624
632
  ]));
625
633
  }
626
634
 
@@ -17,7 +17,7 @@ export interface PanelIntegrationContext {
17
17
  }
18
18
 
19
19
  /**
20
- * A cross-panel deep-link target (DEBT-5 item 4): a specific node a panel
20
+ * A cross-panel deep-link target (item 4): a specific node a panel
21
21
  * should select + reveal when opened via `PanelManager.open(id, pane, target)`,
22
22
  * e.g. a Fleet tree node keyed by its process id and (optionally) its
23
23
  * ProcessKind for disambiguation. Panel-agnostic on purpose — only panels that
@@ -56,7 +56,7 @@ export type NamedKey =
56
56
  export type KeyName = NamedKey | (string & {});
57
57
 
58
58
  /**
59
- * WO-152: the former single 'monitoring' bucket held 33 panels pre-merge —
59
+ * the former single 'monitoring' bucket held 33 panels pre-merge —
60
60
  * too coarse for the picker to be useful and too large for any one operator
61
61
  * mental model. Split into named operator domains so each category holds a
62
62
  * bounded, coherent set (no category may exceed 10 registrations):
@@ -97,7 +97,7 @@ export interface Panel {
97
97
  isPinned: boolean;
98
98
  needsRender: boolean;
99
99
 
100
- // Dirty-flag contract (R2: activated panel render skipping)
100
+ // Dirty-flag contract (activated panel render skipping)
101
101
  /** Mark this panel as needing a re-render on the next frame. */
102
102
  invalidate(): void;
103
103
  /** Called by the compositor after a successful render to clear the dirty flag. */
@@ -154,7 +154,7 @@ export interface Panel {
154
154
 
155
155
  /**
156
156
  * Optional: consume a deep-link target handed in by `PanelManager.open(id,
157
- * pane, target)` (DEBT-5 item 4) — select + reveal the matching node on the
157
+ * pane, target)` (item 4) — select + reveal the matching node on the
158
158
  * panel's next snapshot. A panel without a "node" concept simply doesn't
159
159
  * implement this (the call is a no-op via optional chaining in
160
160
  * PanelManager). A panel that DOES implement it owns its own honest
@@ -169,7 +169,7 @@ export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' |
169
169
  factory: () => Panel;
170
170
  description: string;
171
171
  /**
172
- * WO-152 lifecycle flags. `preload` and `retainOnClose` are independent —
172
+ * lifecycle flags. `preload` and `retainOnClose` are independent —
173
173
  * a panel can eagerly instantiate without being kept alive on close, or be
174
174
  * kept alive on close without eager bootstrap instantiation. They happened
175
175
  * to be identical for all builtin panels before this split (both driven by
@@ -0,0 +1,123 @@
1
+ // ---------------------------------------------------------------------------
2
+ // workstream-graph-render.ts — render a workstream's task graph (fleet.graph.get)
3
+ // legibly, so the observability layer shows the dependency graph's shape,
4
+ // states, and pool posture WITHOUT opening any transcript.
5
+ //
6
+ // The SDK serves a WorkstreamGraphSnapshot: nodes (id/title/state, plus
7
+ // blockedReason, orphaned, stalled, agentId), edges (from → to dependency
8
+ // links), and the elastic-pool state (ready/running/atCap/maxSize). This module
9
+ // is a pure string renderer; every line is fit to the given width (no clipping,
10
+ // no horizontal overflow), so the same output drives a panel or a command.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
14
+ import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
15
+
16
+ export type WorkstreamGraphSnapshot = OperatorMethodOutput<'fleet.graph.get'>;
17
+ export type WorkstreamGraphNode = WorkstreamGraphSnapshot['nodes'][number];
18
+
19
+ /** The pool posture line: "N ready, M running[, at cap (fleet.maxSize=N)]". */
20
+ export function renderPoolSummary(pool: WorkstreamGraphSnapshot['pool']): string {
21
+ if (!pool) return 'no pool state';
22
+ const base = `${pool.ready} ready, ${pool.running} running`;
23
+ return pool.atCap ? `${base}, at cap (fleet.maxSize=${pool.maxSize})` : base;
24
+ }
25
+
26
+ /** The leading glyph for a node — orphaned/blocked/stalled tells win over the base state. */
27
+ function nodeGlyph(node: WorkstreamGraphNode): string {
28
+ if (node.orphaned) return '⊘';
29
+ if (node.blockedReason) return '⊘';
30
+ if (node.stalled) return '◒';
31
+ switch (node.state) {
32
+ case 'running': return '●';
33
+ case 'ready': return '○';
34
+ case 'done': return '✓';
35
+ case 'failed': return '✕';
36
+ default: return '·';
37
+ }
38
+ }
39
+
40
+ /** The state tell for a node: the "waiting on: X" for blocked, running/ready, plus orphaned/stalled markers. */
41
+ function nodeTell(node: WorkstreamGraphNode, needs: readonly string[]): string {
42
+ const parts: string[] = [];
43
+ if (node.blockedReason) {
44
+ parts.push(`blocked — waiting on: ${node.blockedReason}`);
45
+ } else if (node.state === 'running') {
46
+ parts.push(node.agentId ? `running (${node.agentId})` : 'running');
47
+ } else if (node.state === 'ready') {
48
+ parts.push('ready');
49
+ } else {
50
+ parts.push(node.state);
51
+ }
52
+ if (needs.length > 0 && !node.blockedReason) parts.push(`needs: ${needs.join(', ')}`);
53
+ if (node.orphaned) parts.push('orphaned');
54
+ if (node.stalled) parts.push('stalled');
55
+ return parts.join(' · ');
56
+ }
57
+
58
+ /** Fit a line to the width without overflowing (truncates with an ellipsis). */
59
+ function fit(line: string, width: number): string {
60
+ return getDisplayWidth(line) > width ? truncateDisplay(line, width) : line;
61
+ }
62
+
63
+ /**
64
+ * Wrap content to the width on word boundaries — NO clipping: the whole tell
65
+ * (including a long "waiting on: …" reason) survives onto indented continuation
66
+ * lines rather than being cut off. `firstIndent` leads the first line;
67
+ * `contIndent` leads each continuation. A single word longer than the width is
68
+ * hard-fit defensively so a line never overflows.
69
+ */
70
+ function wrapContent(content: string, width: number, firstIndent: string, contIndent: string): string[] {
71
+ const words = content.split(/\s+/).filter(Boolean);
72
+ const out: string[] = [];
73
+ let cur = firstIndent;
74
+ let hasWord = false;
75
+ for (const word of words) {
76
+ const candidate = hasWord ? `${cur} ${word}` : `${cur}${word}`;
77
+ if (getDisplayWidth(candidate) > width && hasWord) {
78
+ out.push(cur);
79
+ cur = `${contIndent}${word}`;
80
+ } else {
81
+ cur = candidate;
82
+ }
83
+ hasWord = true;
84
+ }
85
+ if (hasWord) out.push(cur);
86
+ return out.map((l) => (getDisplayWidth(l) > width ? fit(l, width) : l));
87
+ }
88
+
89
+ /**
90
+ * Render the whole snapshot as legible lines, each fit to `width`. The header
91
+ * carries the pool posture; every node is one line with its glyph, title, and
92
+ * state tell (ready / running / blocked-with-waiting-on / stalled / orphaned),
93
+ * with its dependency edges named inline ("needs: …"). An empty graph states so
94
+ * honestly rather than rendering a blank surface.
95
+ */
96
+ export function renderWorkstreamGraphLines(snapshot: WorkstreamGraphSnapshot, width: number): string[] {
97
+ const lines: string[] = [];
98
+ // Title and pool posture on their own lines so neither clips the other at a
99
+ // narrow width — the pool summary ("… at cap (fleet.maxSize=N)") is load-bearing.
100
+ lines.push(fit(snapshot.title, width));
101
+ lines.push(fit(` ${renderPoolSummary(snapshot.pool)}`, width));
102
+
103
+ if (snapshot.nodes.length === 0) {
104
+ lines.push(fit(' (no work items in this workstream)', width));
105
+ return lines;
106
+ }
107
+
108
+ const titleById = new Map<string, string>();
109
+ for (const node of snapshot.nodes) titleById.set(node.id, node.title);
110
+ const needsByNode = new Map<string, string[]>();
111
+ for (const edge of snapshot.edges) {
112
+ const list = needsByNode.get(edge.to) ?? [];
113
+ list.push(titleById.get(edge.from) ?? edge.from);
114
+ needsByNode.set(edge.to, list);
115
+ }
116
+
117
+ for (const node of snapshot.nodes) {
118
+ const glyph = nodeGlyph(node);
119
+ const tell = nodeTell(node, needsByNode.get(node.id) ?? []);
120
+ lines.push(...wrapContent(`${glyph} ${node.title} ${tell}`, width, ' ', ' '));
121
+ }
122
+ return lines;
123
+ }