@pellux/goodvibes-tui 1.16.0 → 1.17.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 (220) hide show
  1. package/CHANGELOG.md +95 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  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/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/onboarding/onboarding-wizard.ts +22 -8
  167. package/src/renderer/panel-composite.ts +2 -2
  168. package/src/renderer/panel-workspace-bar.ts +1 -1
  169. package/src/renderer/process-indicator.ts +1 -1
  170. package/src/renderer/selection-modal-overlay.ts +102 -24
  171. package/src/renderer/session-picker-modal.ts +2 -2
  172. package/src/renderer/settings-modal-helpers.ts +14 -5
  173. package/src/renderer/settings-modal.ts +127 -39
  174. package/src/renderer/shell-surface.ts +3 -0
  175. package/src/renderer/terminal-bg-probe.ts +1 -1
  176. package/src/renderer/theme.ts +3 -3
  177. package/src/renderer/tool-call.ts +1 -1
  178. package/src/renderer/tool-result-summary.ts +1 -1
  179. package/src/renderer/turn-injection.ts +7 -6
  180. package/src/renderer/ui-factory.ts +6 -1
  181. package/src/runtime/bootstrap-command-context.ts +4 -1
  182. package/src/runtime/bootstrap-command-parts.ts +6 -4
  183. package/src/runtime/bootstrap-core.ts +35 -35
  184. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  185. package/src/runtime/bootstrap-shell.ts +6 -5
  186. package/src/runtime/bootstrap.ts +34 -32
  187. package/src/runtime/channel-composition.ts +46 -0
  188. package/src/runtime/code-index-services.ts +3 -4
  189. package/src/runtime/daemon-attach-notices.ts +120 -0
  190. package/src/runtime/durability-services.ts +55 -0
  191. package/src/runtime/embedded-service-factories.ts +40 -0
  192. package/src/runtime/feature-settings.ts +147 -0
  193. package/src/runtime/index.ts +4 -21
  194. package/src/runtime/legacy-daemon-migration.ts +29 -19
  195. package/src/runtime/memory-fold.ts +1 -1
  196. package/src/runtime/notification-dispatch.ts +110 -0
  197. package/src/runtime/onboarding/apply.ts +3 -29
  198. package/src/runtime/onboarding/fast-path.ts +39 -6
  199. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  200. package/src/runtime/onboarding/snapshot.ts +0 -1
  201. package/src/runtime/onboarding/types.ts +9 -3
  202. package/src/runtime/provider-fallback.ts +55 -0
  203. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  204. package/src/runtime/release-artifacts.ts +19 -103
  205. package/src/runtime/resume-notice.ts +1 -1
  206. package/src/runtime/services.ts +81 -82
  207. package/src/runtime/session-continuity-hints.ts +38 -0
  208. package/src/runtime/surface-feature-flags.ts +40 -9
  209. package/src/runtime/terminal-output-guard.ts +2 -2
  210. package/src/runtime/trust/workspace-registration.ts +10 -2
  211. package/src/runtime/trust/workspace-trust.ts +58 -31
  212. package/src/runtime/ui-services.ts +3 -1
  213. package/src/runtime/update-check.ts +15 -69
  214. package/src/runtime/workstream-services.ts +2 -2
  215. package/src/shell/blocking-input.ts +102 -6
  216. package/src/shell/ui-openers.ts +38 -6
  217. package/src/utils/format-duration.ts +1 -1
  218. package/src/verification/verification-ledger.ts +21 -4
  219. package/src/version.ts +1 -1
  220. package/src/input/commands/flags-runtime.ts +0 -328
@@ -9,7 +9,7 @@ import type {
9
9
  import { encodeConnectionPayload, generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
10
10
 
11
11
  // ---------------------------------------------------------------------------
12
- // QR Code → 'pairing' config-modal surface (W6.1 group-B port). Connection
12
+ // QR Code → 'pairing' config-modal surface (group-B port). Connection
13
13
  // URL/token/username(/password) plus a scannable QR block — read/navigate only.
14
14
  // Token regeneration is a destructive mutation (it invalidates any live
15
15
  // companion session) that the panel gated behind an in-panel confirm; per
@@ -18,7 +18,7 @@ import { encodeConnectionPayload, generateQrMatrix, renderQrToString } from '@pe
18
18
  // buildView, cached) so a missing daemon home degrades honestly rather than
19
19
  // throwing at modal-registration time.
20
20
  //
21
- // DEBT-3: the regenerate action now dispatches the real `/qrcode regenerate`
21
+ // the regenerate action now dispatches the real `/qrcode regenerate`
22
22
  // verb (which rotates the shared operator-token store) and, once it resolves,
23
23
  // RE-PULLS the lazy connection-info thunk so the modal shows the rotated token +
24
24
  // QR in place without being re-opened. `confirm: true` keeps the two-press
@@ -18,12 +18,12 @@ import type {
18
18
  import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction } from '../project-planning-answer-actions.ts';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
- // Project Planning → 'planning' config-modal surface (W6.1 group-B port). Shows
21
+ // Project Planning → 'planning' config-modal surface (group-B port). Shows
22
22
  // readiness/questions/decisions/task-graph/handoff — read-only except choosing
23
23
  // an answer to the current open question, approving execution, dismissing the
24
24
  // plan, or refreshing.
25
25
  //
26
- // DEBT-3 — the seams are now real:
26
+ // — the seams are now real:
27
27
  // - A CANNED answer to a real open question dispatches `/plan answer <id> <text>`
28
28
  // (records it; the open-question gap clears on the next refine).
29
29
  // - The CUSTOM free-form typed answer (and any answer to a synthetic readiness
@@ -13,7 +13,7 @@ import type {
13
13
  } from '@pellux/goodvibes-sdk/platform/plugins';
14
14
 
15
15
  // ---------------------------------------------------------------------------
16
- // Plugins → config-modal surface (W6.1 group-B port). Mirrors the retired
16
+ // Plugins → config-modal surface (group-B port). Mirrors the retired
17
17
  // PluginsPanel's read surface (list/capabilities/getTrustRecord/
18
18
  // getQuarantineRecord) plus `verify` (a pure read-only signature check).
19
19
  // enable/disable/liftQuarantine are DROPPED from the dep shape on purpose:
@@ -18,7 +18,7 @@ import type {
18
18
  } from '../../input/config-modal-types.ts';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
- // Policy → config-modal surface (W6.1 group-B port). Governance CONTENT view,
21
+ // Policy → config-modal surface (group-B port). Governance CONTENT view,
22
22
  // not a selectable list — current/candidate bundle state, diff, divergence
23
23
  // gate, history, permission audit, lint findings, preflight review, and
24
24
  // simulation samples render as non-selectable rows (the host windows/scrolls
@@ -12,7 +12,7 @@ import type {
12
12
  } from '../../input/config-modal-types.ts';
13
13
 
14
14
  // ---------------------------------------------------------------------------
15
- // Security → config-modal surface (W6.1 group-B port). Two tabs: 'Tokens' (the
15
+ // Security → config-modal surface (group-B port). Two tabs: 'Tokens' (the
16
16
  // token scope/rotation audit list under the governance summary) and
17
17
  // 'Governance' (policy preflight, MCP/plugin quarantine, latest incident, and
18
18
  // the MCP attack-path review). Read-model-backed, so refresh() is a no-op —
@@ -8,7 +8,7 @@ import type { SkillOrigin, SkillRecord } from '../skills-panel.ts';
8
8
  import { listInstalledEcosystemEntries, type EcosystemCatalogPathOptions, type ShellPathService } from '@/runtime/index.ts';
9
9
 
10
10
  // ---------------------------------------------------------------------------
11
- // Skills → config-modal surface (W6.1 group-B port). Project-local and global
11
+ // Skills → config-modal surface (group-B port). Project-local and global
12
12
  // skill-pack discovery. Disk scans happen only in refresh() (never in
13
13
  // buildView), mirroring the panel's onActivate()-triggered load. Browse-only:
14
14
  // the panel never had a command-routable mutation verb (no /skills delete/remove
@@ -7,7 +7,7 @@ import type {
7
7
  import type { WorkPlanItemStatus } from '../../work-plans/work-plan-store.ts';
8
8
 
9
9
  // ---------------------------------------------------------------------------
10
- // Work Plan → config-modal surface (W6.1 group-B port). Lists the active
10
+ // Work Plan → config-modal surface (group-B port). Lists the active
11
11
  // persistent plan's checklist items with a progress summary + status counts.
12
12
  // All mutations (add/edit item, cycle/set status, remove, clear-completed)
13
13
  // route to the existing `/work-plan` command path (charter: no
@@ -65,8 +65,8 @@ class WorkPlanModalSurface implements ConfigModalSurface {
65
65
  private readonly hasRow = (row: ConfigModalRow | null): boolean => row !== null;
66
66
 
67
67
  /** 'i'/'w' jump to the selected item's linked agent/WRFC chain in Fleet
68
- * (DEBT-5 item 4 — restores the retired WorkPlanPanel's per-item deep-link,
69
- * lost when W6.1 migrated this surface to a modal without it). Gated on
68
+ * (item 4 — restores the retired WorkPlanPanel's per-item deep-link,
69
+ * lost when migrated this surface to a modal without it). Gated on
70
70
  * the link actually being present, same as the retired panel gated the key
71
71
  * on `item?.linked?.agentId`/`wrfcId` before ever reaching the jump. */
72
72
  private readonly hasAgentLink = (row: ConfigModalRow | null): boolean =>
@@ -0,0 +1,127 @@
1
+ /**
2
+ * PanelNotificationFeed — the read-model behind the `panel_only` notification
3
+ * target.
4
+ *
5
+ * The SDK's NotificationRouter (`@/runtime/index.ts` -> platform runtime `ui`
6
+ * barrel) can route a notification's `RoutingDecision.target` to
7
+ * `'panel_only'`, meaning: don't surface this in the conversation or the
8
+ * status bar, but it must still reach a real, visible place. Before this
9
+ * file, nothing in this repo consumed that target outside of tests — a
10
+ * panel-routed notification (including every burst- or batch-collapsed one)
11
+ * had nowhere to go. This feed is that place: a caller holding a routed
12
+ * `Notification` + `RoutingDecision` pair calls `record()`, and
13
+ * `notifications-panel.ts` renders whatever has accumulated.
14
+ *
15
+ * Collapsed groups (reasonCode `burst_collapsed` / `batch_window_collapsed`)
16
+ * are tracked by their `batchKey` and accumulate an honest running count
17
+ * instead of rendering one row per suppressed duplicate.
18
+ *
19
+ * Production wiring — something in the running app actually calling
20
+ * `record()` with live notifications — rides the SDK round that turns on
21
+ * adaptive notification suppression by default; this feed and its panel are
22
+ * the render target that work lands on, built ahead of it so the target is
23
+ * never missing when that switch flips.
24
+ */
25
+ import type { Notification, RoutingDecision } from '@/runtime/index.ts';
26
+
27
+ export interface PanelFeedEntry {
28
+ readonly key: string;
29
+ readonly domain: string;
30
+ readonly level: Notification['level'];
31
+ readonly title: string;
32
+ readonly body: string | undefined;
33
+ /** Unix ms of the most recent notification folded into this entry. */
34
+ readonly timestamp: number;
35
+ readonly reasonCode: RoutingDecision['reasonCode'];
36
+ /** How many notifications this entry represents. 1 for a standalone item; >1 for a collapsed group. Always the true count — never estimated. */
37
+ readonly collapsedCount: number;
38
+ }
39
+
40
+ const MAX_ENTRIES = 200;
41
+
42
+ /** Reason codes whose notifications fold into one running-count entry per batch key, rather than one entry per occurrence. */
43
+ const COLLAPSING_REASON_CODES: ReadonlySet<RoutingDecision['reasonCode']> = new Set([
44
+ 'burst_collapsed',
45
+ 'batch_window_collapsed',
46
+ ]);
47
+
48
+ export class PanelNotificationFeed {
49
+ private readonly entries = new Map<string, PanelFeedEntry>();
50
+ /** Insertion order of `entries` keys (oldest first), for bounded eviction. */
51
+ private order: string[] = [];
52
+ private readonly listeners = new Set<() => void>();
53
+
54
+ /**
55
+ * Record a routed notification. Only notifications actually targeted at
56
+ * `panel_only` are kept — a caller passing anything else is a mistake (the
57
+ * router sent it elsewhere), so it's dropped rather than shown in the
58
+ * wrong place.
59
+ */
60
+ record(notification: Notification, decision: RoutingDecision): void {
61
+ if (decision.target !== 'panel_only') return;
62
+
63
+ const collapsing = COLLAPSING_REASON_CODES.has(decision.reasonCode) && Boolean(decision.batchKey);
64
+ const key = collapsing ? `group:${decision.batchKey}` : `single:${notification.id}`;
65
+ const previousCount = collapsing ? (this.entries.get(key)?.collapsedCount ?? 0) : 0;
66
+
67
+ const entry: PanelFeedEntry = {
68
+ key,
69
+ domain: notification.domain,
70
+ level: notification.level,
71
+ title: notification.title,
72
+ body: notification.body,
73
+ timestamp: notification.timestamp,
74
+ reasonCode: decision.reasonCode,
75
+ collapsedCount: previousCount + 1,
76
+ };
77
+
78
+ if (!this.entries.has(key)) {
79
+ this.order.push(key);
80
+ if (this.order.length > MAX_ENTRIES) {
81
+ const evicted = this.order.shift();
82
+ if (evicted !== undefined) this.entries.delete(evicted);
83
+ }
84
+ }
85
+ this.entries.set(key, entry);
86
+ this.emitChange();
87
+ }
88
+
89
+ /** Every entry, most recently updated first. */
90
+ list(): readonly PanelFeedEntry[] {
91
+ const items: PanelFeedEntry[] = [];
92
+ for (const key of this.order) {
93
+ const entry = this.entries.get(key);
94
+ if (entry) items.push(entry);
95
+ }
96
+ return items.reverse();
97
+ }
98
+
99
+ clear(): void {
100
+ this.entries.clear();
101
+ this.order = [];
102
+ this.emitChange();
103
+ }
104
+
105
+ /** Subscribe to feed changes (e.g. to mark a panel dirty). Returns an unsubscribe function. */
106
+ subscribe(listener: () => void): () => void {
107
+ this.listeners.add(listener);
108
+ return () => this.listeners.delete(listener);
109
+ }
110
+
111
+ private emitChange(): void {
112
+ for (const listener of this.listeners) listener();
113
+ }
114
+ }
115
+
116
+ let sharedFeed: PanelNotificationFeed | null = null;
117
+
118
+ /**
119
+ * The process-wide `panel_only` feed. Lazily created so the running app has
120
+ * exactly one feed that every future caller and the NotificationsPanel agree
121
+ * on, while tests construct their own isolated `PanelNotificationFeed`
122
+ * instance instead of reaching for this one.
123
+ */
124
+ export function getSharedNotificationFeed(): PanelNotificationFeed {
125
+ if (!sharedFeed) sharedFeed = new PanelNotificationFeed();
126
+ return sharedFeed;
127
+ }
@@ -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