@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
@@ -125,6 +125,29 @@ export interface OnboardingSurfacesSnapshot {
125
125
  readonly records: readonly OnboardingSurfaceRecord[];
126
126
  }
127
127
 
128
+ /**
129
+ * W4-D1 wizard wiring: read-only detection of a legacy `goodvibes-daemon.service`
130
+ * systemd unit (see `../legacy-daemon-migration.ts`'s `LegacyUnitInfo`), carried
131
+ * on the snapshot so the Network step can show the guided migration action only
132
+ * when there is actually something to migrate. Never implies anything was
133
+ * touched — detection only.
134
+ */
135
+ export interface OnboardingLegacyDaemonSnapshot {
136
+ readonly present: boolean;
137
+ readonly active: boolean;
138
+ readonly path: string;
139
+ /**
140
+ * F2 follow-up: the unit name this tool actually manages on this host,
141
+ * resolved from the `service.serviceName` config key at snapshot-collection
142
+ * time (`resolveConfiguredServiceName`, `../legacy-daemon-migration.ts`) —
143
+ * so the wizard's detection banner names the real unit instead of the
144
+ * hardcoded default. Optional so snapshots built without config access
145
+ * (fallbacks, older fixtures) stay valid; readers fall back to
146
+ * `MANAGED_SERVICE_NAME`.
147
+ */
148
+ readonly trackedServiceName?: string;
149
+ }
150
+
128
151
  export interface OnboardingProviderAccountRecord {
129
152
  readonly providerId: string;
130
153
  readonly configured: boolean;
@@ -170,7 +193,8 @@ export type OnboardingSnapshotCollectionIssueArea =
170
193
  | 'secrets-records'
171
194
  | 'surfaces'
172
195
  | 'provider-accounts'
173
- | 'acknowledgements';
196
+ | 'acknowledgements'
197
+ | 'legacy-daemon';
174
198
 
175
199
  export interface OnboardingSnapshotCollectionIssue {
176
200
  readonly area: OnboardingSnapshotCollectionIssueArea;
@@ -190,6 +214,7 @@ export interface OnboardingSnapshotState {
190
214
  readonly bindSettings: OnboardingBindSettingsSnapshot;
191
215
  readonly surfaces: OnboardingSurfacesSnapshot;
192
216
  readonly providerAccounts: OnboardingProviderAccountsSnapshot | null;
217
+ readonly legacyDaemon: OnboardingLegacyDaemonSnapshot;
193
218
  readonly collectionIssues: readonly OnboardingSnapshotCollectionIssue[];
194
219
  }
195
220
 
@@ -377,6 +402,10 @@ export interface OnboardingProviderAccountReadHelper {
377
402
  loadSnapshot(): Promise<OnboardingProviderAccountsSnapshot>;
378
403
  }
379
404
 
405
+ export interface OnboardingLegacyDaemonReadHelper {
406
+ detect(): OnboardingLegacyDaemonSnapshot | Promise<OnboardingLegacyDaemonSnapshot>;
407
+ }
408
+
380
409
  export type OnboardingShellPaths = Pick<
381
410
  ShellPathService,
382
411
  'workingDirectory' | 'resolveProjectPath' | 'resolveUserPath'
@@ -391,6 +420,7 @@ export interface OnboardingSnapshotDependencies {
391
420
  readonly services: Pick<ServiceInspectionQuery, 'getAll' | 'inspect'>;
392
421
  readonly surfaces?: OnboardingSurfaceReadHelper;
393
422
  readonly providerAccounts?: OnboardingProviderAccountReadHelper;
423
+ readonly legacyDaemon?: OnboardingLegacyDaemonReadHelper;
394
424
  readonly shellPaths?: OnboardingShellPaths;
395
425
  readonly acknowledgementScope?: OnboardingStateScope;
396
426
  }
@@ -11,7 +11,11 @@
11
11
  * the new path will be inspected on the next daemon boot.
12
12
  */
13
13
 
14
- import { join } from 'node:path';
14
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { randomBytes } from 'node:crypto';
16
+ import { dirname, join } from 'node:path';
17
+ import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
18
+ import type { CompanionPairingResult } from '@pellux/goodvibes-sdk/platform/pairing';
15
19
 
16
20
  /**
17
21
  * Return the list of absolute operator-tokens.json paths the TUI may have written
@@ -26,3 +30,80 @@ export function workspaceOperatorTokenCandidates(workingDirectory: string): read
26
30
  join(workingDirectory, '.goodvibes', 'tui', 'operator-tokens.json'),
27
31
  ];
28
32
  }
33
+
34
+ /**
35
+ * F1 (adopt-an-already-running-external-daemon): resolve the operator/companion
36
+ * token this TUI process uses to authenticate with its daemon, honoring
37
+ * `GOODVIBES_DAEMON_TOKEN` as a non-interactive override.
38
+ *
39
+ * Without this override, the only way to make a TUI instance adopt an
40
+ * already-running external daemon that used an out-of-band or explicit token
41
+ * (rather than one derived from `getOrCreateCompanionToken` under a *shared*
42
+ * home directory) was to hand-write `<daemonHomeDir>/operator-tokens.json`
43
+ * before startup — there was no env var or CLI flag equivalent.
44
+ *
45
+ * `GOODVIBES_DAEMON_TOKEN` is already the documented env var `bin/goodvibes-daemon`
46
+ * honors for its own bearer token (see `src/daemon/cli.ts`'s `readDaemonCliTokens`)
47
+ * and the one `src/verification/live-verifier.ts`'s `readDaemonToken` already falls
48
+ * back to when probing a daemon's HTTP surface from the outside. Reusing the same
49
+ * name here means one env var configures both sides of an adopted-daemon setup —
50
+ * start the daemon with `GOODVIBES_DAEMON_TOKEN=<token>` and point the TUI at it
51
+ * with the same `GOODVIBES_DAEMON_TOKEN=<token>` plus
52
+ * `--config controlPlane.host=<host> --config controlPlane.port=<port>` — instead
53
+ * of introducing a second, TUI-only flag.
54
+ *
55
+ * When the override is set and does not already match the on-disk record, the
56
+ * file is rewritten so the override becomes durable for this home directory
57
+ * (an existing peerId is kept when present, so companion-pairing identity does
58
+ * not needlessly churn). Falls back to the existing `getOrCreateCompanionToken`
59
+ * behavior — read the existing file, or mint a fresh random token — when the
60
+ * override is unset.
61
+ *
62
+ * @param explicitToken - Takes precedence over `GOODVIBES_DAEMON_TOKEN` when
63
+ * provided. Used by the onboarding wizard's "connect to an existing daemon"
64
+ * action (a pasted token) so it shares this exact persistence logic with the
65
+ * env-var path instead of duplicating it.
66
+ */
67
+ export function resolveDaemonCompanionToken(daemonHomeDir: string, explicitToken?: string): CompanionPairingResult {
68
+ const override = explicitToken?.trim() || process.env.GOODVIBES_DAEMON_TOKEN?.trim();
69
+ if (!override) return getOrCreateCompanionToken('tui', { daemonHomeDir });
70
+
71
+ const tokenPath = join(daemonHomeDir, 'operator-tokens.json');
72
+ let existingPeerId: string | undefined;
73
+ let existingCreatedAt: number | undefined;
74
+ if (existsSync(tokenPath)) {
75
+ try {
76
+ const record = JSON.parse(readFileSync(tokenPath, 'utf-8')) as {
77
+ token?: unknown;
78
+ peerId?: unknown;
79
+ createdAt?: unknown;
80
+ };
81
+ if (typeof record.token === 'string' && record.token === override) {
82
+ return {
83
+ token: override,
84
+ peerId: typeof record.peerId === 'string' ? record.peerId : randomBytes(12).toString('hex'),
85
+ createdAt: typeof record.createdAt === 'number' ? record.createdAt : Date.now(),
86
+ };
87
+ }
88
+ if (typeof record.peerId === 'string') existingPeerId = record.peerId;
89
+ if (typeof record.createdAt === 'number') existingCreatedAt = record.createdAt;
90
+ } catch {
91
+ // Malformed on-disk record — fall through and rewrite it with the override.
92
+ }
93
+ }
94
+
95
+ const record: CompanionPairingResult = {
96
+ token: override,
97
+ peerId: existingPeerId ?? randomBytes(12).toString('hex'),
98
+ createdAt: existingCreatedAt ?? Date.now(),
99
+ };
100
+ try {
101
+ mkdirSync(dirname(tokenPath), { recursive: true });
102
+ writeFileSync(tokenPath, JSON.stringify(record, null, 2), { encoding: 'utf-8', mode: 0o600 });
103
+ chmodSync(tokenPath, 0o600);
104
+ } catch {
105
+ // Best-effort persistence — the override still applies for this process
106
+ // even if the file could not be written (e.g. a read-only home directory).
107
+ }
108
+ return record;
109
+ }
@@ -0,0 +1,59 @@
1
+ import type { OrchestratorCoreServices } from '@pellux/goodvibes-sdk/platform/core';
2
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
3
+ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
4
+ import type { RuntimeServices } from './services.ts';
5
+ import { isCodeInjectionSettingEnabled } from './code-index-services.ts';
6
+
7
+ /** The slice of the runtime services bag the shared orchestrator payload draws from. */
8
+ export type OrchestratorCoreServicesSource = Pick<
9
+ RuntimeServices,
10
+ | 'planManager'
11
+ | 'adaptivePlanner'
12
+ | 'sessionMemoryStore'
13
+ | 'sessionLineageTracker'
14
+ | 'idempotencyStore'
15
+ | 'memoryRegistry'
16
+ | 'codeIndexStore'
17
+ | 'codeIndexReindexScheduler'
18
+ >;
19
+
20
+ /**
21
+ * The single source of truth for the `Orchestrator.setCoreServices()` payload
22
+ * fields shared by BOTH call sites (runtime/bootstrap.ts and main.ts). Each
23
+ * site spreads this and adds its own site-specific extras (bootstrap:
24
+ * cacheHitTracker; main: favoritesStore) — setCoreServices() merges, so the
25
+ * later main.ts call only overlays, never erases.
26
+ *
27
+ * Wave-5 (wo805) BLOCKER regression guard: `memoryRegistry` here is what turns
28
+ * on per-turn passive knowledge injection for the MAIN interactive session —
29
+ * the SDK turn loop hard-gates on `coreServices.memoryRegistry` (undefined is
30
+ * a silent no-op: Orchestrator.getTurnInjections() stays empty forever and
31
+ * `/recall injections` renders a misleading empty state). Both call sites once
32
+ * omitted it independently; routing them through this one function (with
33
+ * src/test/runtime/orchestrator-core-services.test.ts pinning the field) is
34
+ * what keeps that from regressing.
35
+ */
36
+ export function buildSharedOrchestratorCoreServices(input: {
37
+ readonly services: OrchestratorCoreServicesSource;
38
+ readonly configManager: ConfigManager;
39
+ readonly providerRegistry: ProviderRegistry;
40
+ }): OrchestratorCoreServices {
41
+ const { services, configManager, providerRegistry } = input;
42
+ return {
43
+ configManager,
44
+ providerRegistry,
45
+ planManager: services.planManager,
46
+ adaptivePlanner: services.adaptivePlanner,
47
+ sessionMemoryStore: services.sessionMemoryStore,
48
+ sessionLineageTracker: services.sessionLineageTracker,
49
+ idempotencyStore: services.idempotencyStore,
50
+ memoryRegistry: services.memoryRegistry,
51
+ // Wave-5 Stage B — main-session code auto-injection + tool-site reindex. Injection is
52
+ // additionally gated by the default-off `agent-passive-code-injection` flag inside the
53
+ // SDK; here we supply the source, the live storage.codeIndexEnabled predicate, and the
54
+ // reindex scheduler.
55
+ codeIndex: services.codeIndexStore,
56
+ isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager),
57
+ codeIndexReindexScheduler: services.codeIndexReindexScheduler,
58
+ };
59
+ }
@@ -34,6 +34,8 @@ export interface ProcessLifecycleAnsi {
34
34
  readonly KEYBOARD_EXT_DISABLE: string;
35
35
  readonly MOUSE_DISABLE: string;
36
36
  readonly CURSOR_SHOW: string;
37
+ /** Disables terminal focus-event reporting (DECSET ?1004l) — see main.ts FOCUS_ENABLE. */
38
+ readonly FOCUS_DISABLE: string;
37
39
  }
38
40
 
39
41
  export interface ProcessLifecycleDeps {
@@ -128,7 +130,7 @@ export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLife
128
130
  if (terminalRestored) return;
129
131
  terminalRestored = true;
130
132
  const exitScreen = noAltScreen ? ansi.CLEAR_SCREEN : ansi.CLEAR_SCREEN + ansi.ALT_SCREEN_EXIT;
131
- allowTerminalWrite(() => stdout.write(ansi.PASTE_DISABLE + ansi.KEYBOARD_EXT_DISABLE + ansi.MOUSE_DISABLE + ansi.CURSOR_SHOW + exitScreen));
133
+ allowTerminalWrite(() => stdout.write(ansi.PASTE_DISABLE + ansi.KEYBOARD_EXT_DISABLE + ansi.MOUSE_DISABLE + ansi.FOCUS_DISABLE + ansi.CURSOR_SHOW + exitScreen));
132
134
  getTerminalOutputGuard().dispose();
133
135
  try { stdin.setRawMode(false); } catch { /* stdin may not be a TTY */ }
134
136
  };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * resume-notice.ts — the boot-time "previous session found" transcript notice.
3
+ *
4
+ * UX-D item 1: a supervision-journey audit of 1.7.0 found that the TUI
5
+ * accumulates rich resumable state on disk (a saved conversation, workspace
6
+ * checkpoints, WRFC chain history) but never surfaces any of it at startup —
7
+ * an operator has no way to know it exists short of already knowing the
8
+ * right command. This module builds ONE compact, honest system-message block
9
+ * printed after the splash and before the first prompt, summarizing exactly
10
+ * what real state exists and how to reach it.
11
+ *
12
+ * Honesty constraints (verified against the actual runtime, not assumed):
13
+ * - There is no bare `/resume` command and no `/sessions` command (a
14
+ * pre-existing typo already lives in the splash's own hint at
15
+ * utils/splash-lines.ts — out of scope here, splash stays byte-identical).
16
+ * The real, working command is `/session resume <id|name>` — it always
17
+ * requires an explicit target. So this notice advertises
18
+ * `/session resume <id>` with the real last-session id substituted in,
19
+ * never a bare `/resume`.
20
+ * - `/checkpoints` works with zero arguments and behaves correctly at any
21
+ * checkpoint count (including zero) — advertised whenever the checkpoint
22
+ * manager is available in this session.
23
+ * - `/recall` (memory) is only advertised when the memory API is actually
24
+ * wired up in this session (context.clients?.knowledgeApi?.memory) —
25
+ * some runtimes don't have it, and claiming it works there would not be
26
+ * honest.
27
+ * - Every clause is independently gated on real data: a claim about
28
+ * checkpoints/chain history is only made when that data is known; "no
29
+ * chain history" means no chain clause is printed, not a fabricated one.
30
+ */
31
+
32
+ import { readLastSessionPointer } from '@/runtime/index.ts';
33
+ import type { WrfcChain } from '@pellux/goodvibes-sdk/platform/agents';
34
+ import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
35
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
36
+ import type { SystemMessageRouter } from '../core/system-message-router.ts';
37
+
38
+ // ─── Chain outcome ───────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Honest, human-facing outcome of a WRFC chain, derived from real chain
42
+ * state rather than guessed. `chain.state` alone cannot distinguish a
43
+ * user-cancelled chain from an ordinary review/gate failure. The SDK now
44
+ * records that distinction first-class as `chain.failureKind` ('cancelled'
45
+ * vs 'transport'/'other'), set by cancelChain()/failChain(), so that field
46
+ * is the primary source of truth. Snapshots persisted before the field
47
+ * existed lack it; for those we fall back to the owner-decision log, where
48
+ * cancelChain() also records a `chain_cancelled` decision. A chain still
49
+ * non-terminal after rehydrate's zombie-reap check (see wrfc-persistence.ts)
50
+ * is reported as 'interrupted' — re-imported and live again, not history.
51
+ */
52
+ export type ChainOutcome = 'passed' | 'failed' | 'cancelled' | 'interrupted';
53
+
54
+ /** Terminal WRFC states (mirrors wrfc-persistence.ts's own TERMINAL_STATES). */
55
+ function isTerminalState(state: WrfcChain['state']): boolean {
56
+ return state === 'passed' || state === 'failed';
57
+ }
58
+
59
+ export function describeChainOutcome(chain: WrfcChain): ChainOutcome {
60
+ if (!isTerminalState(chain.state)) return 'interrupted';
61
+ if (chain.state === 'passed') return 'passed';
62
+ // Primary: the SDK's first-class failureKind, authoritative for chains
63
+ // failed/cancelled under the current SDK.
64
+ if (chain.failureKind === 'cancelled') return 'cancelled';
65
+ // Fallback for pre-failureKind snapshots: consult the owner-decision log,
66
+ // where cancelChain() records a chain_cancelled decision.
67
+ if (chain.failureKind === undefined) {
68
+ const lastAction = chain.ownerDecisions.length > 0 ? chain.ownerDecisions[chain.ownerDecisions.length - 1]?.action : undefined;
69
+ if (lastAction === 'chain_cancelled') return 'cancelled';
70
+ }
71
+ return 'failed';
72
+ }
73
+
74
+ /** Pick the most recently completed (or, if still interrupted, most recently created) chain from a set. Null if the set is empty. */
75
+ export function mostRecentChain(chains: readonly WrfcChain[]): WrfcChain | null {
76
+ if (chains.length === 0) return null;
77
+ return [...chains].sort((a, b) => (b.completedAt ?? b.createdAt ?? 0) - (a.completedAt ?? a.createdAt ?? 0))[0]!;
78
+ }
79
+
80
+ // ─── Notice text ─────────────────────────────────────────────────────────────
81
+
82
+ export interface ResumeNoticeFacts {
83
+ /** Number of user turns in the last saved session. Null when there is no prior session (or it could not be read). */
84
+ readonly turnCount: number | null;
85
+ /** Session id of the last saved session — needed to build a truthful, directly-runnable resume hint. Null when there is no prior session. */
86
+ readonly lastSessionId: string | null;
87
+ /** Number of workspace checkpoints. Null when the checkpoint manager is unavailable in this session (not the same as zero). */
88
+ readonly checkpointCount: number | null;
89
+ /** Outcome of the most recently known WRFC chain. Null when there is no chain history at all. */
90
+ readonly lastChainOutcome: ChainOutcome | null;
91
+ /** Whether /recall (memory) is wired up in this session. */
92
+ readonly memoryAvailable: boolean;
93
+ }
94
+
95
+ function plural(n: number, word: string): string {
96
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
97
+ }
98
+
99
+ /**
100
+ * Build the boot resume notice text, or null when there is nothing to
101
+ * report (no prior session, no checkpoints ever taken, no chain history —
102
+ * a clean/new working directory prints nothing, respecting quiet startup).
103
+ */
104
+ export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
105
+ const hasSession = facts.lastSessionId !== null && facts.turnCount !== null;
106
+ const checkpointsKnown = facts.checkpointCount !== null;
107
+ const checkpointCount = facts.checkpointCount ?? 0;
108
+ const hasCheckpoints = checkpointsKnown && checkpointCount > 0;
109
+ const hasChainHistory = facts.lastChainOutcome !== null;
110
+
111
+ if (!hasSession && !hasCheckpoints && !hasChainHistory) return null;
112
+
113
+ const summary: string[] = [];
114
+ if (hasSession) summary.push(plural(facts.turnCount!, 'turn'));
115
+ // Show the checkpoint count whenever it's knowable and there's a reason to
116
+ // (anchored to an existing session, or checkpoints genuinely exist even
117
+ // without one) — never guessed when the manager is unavailable.
118
+ if (checkpointsKnown && (hasSession || hasCheckpoints)) summary.push(plural(checkpointCount, 'checkpoint'));
119
+ if (hasChainHistory) summary.push(`last chain: ${facts.lastChainOutcome}`);
120
+
121
+ const lead = hasSession ? 'Previous session found' : 'Workspace history found';
122
+ let notice = `${lead}: ${summary.join(', ')}`;
123
+
124
+ const hints: string[] = [];
125
+ // No bare `/resume` exists — /session resume always requires a target.
126
+ if (hasSession) hints.push(`/session resume ${facts.lastSessionId} to continue`);
127
+ if (hasCheckpoints) hints.push('/checkpoints to browse');
128
+ if (facts.memoryAvailable) hints.push('/recall for memory');
129
+ if (hints.length > 0) notice += ` — ${hints.join(' · ')}`;
130
+
131
+ return notice;
132
+ }
133
+
134
+ // ─── Fact gathering (I/O) ────────────────────────────────────────────────────
135
+
136
+ export interface ResumeNoticeDeps {
137
+ readonly workingDirectory: string;
138
+ readonly homeDirectory: string;
139
+ /** Surface root used by session persistence — the TUI always uses 'tui'. */
140
+ readonly surfaceRoot: string;
141
+ /** Only `load()` is needed — kept narrow for testability. */
142
+ readonly sessionManager: Pick<SessionManager, 'load'>;
143
+ /** Undefined when checkpoints are not wired up in this session at all. Only `list()` is needed. */
144
+ readonly checkpointManager: Pick<WorkspaceCheckpointManager, 'list'> | undefined;
145
+ /** The full known-chain set from WrfcPersistence.knownChains, gathered post-rehydrate. */
146
+ readonly chainHistory: readonly WrfcChain[];
147
+ readonly memoryAvailable: boolean;
148
+ readonly router: Pick<SystemMessageRouter, 'high'>;
149
+ }
150
+
151
+ /**
152
+ * Read the last session's real turn count from its saved JSONL file. A user
153
+ * turn is one stored message with role 'user' — the number of times the
154
+ * operator spoke, which is what "N turns" means to a human reading the
155
+ * notice (as opposed to a raw message count, which double-counts replies).
156
+ * Returns null when there is no last-session pointer, or the pointed-to
157
+ * session file is missing/corrupt — never a claim about a session that
158
+ * cannot actually be resumed.
159
+ */
160
+ function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, 'workingDirectory' | 'homeDirectory' | 'surfaceRoot' | 'sessionManager'>): { turnCount: number; lastSessionId: string } | null {
161
+ const lastSessionId = readLastSessionPointer({
162
+ workingDirectory: deps.workingDirectory,
163
+ homeDirectory: deps.homeDirectory,
164
+ surfaceRoot: deps.surfaceRoot,
165
+ });
166
+ if (!lastSessionId) return null;
167
+ try {
168
+ const { messages } = deps.sessionManager.load(lastSessionId);
169
+ const turnCount = messages.filter((m) => (m as { role?: unknown }).role === 'user').length;
170
+ return { turnCount, lastSessionId };
171
+ } catch {
172
+ // Pointer file present but the session it points to is gone/corrupt —
173
+ // there is nothing truthful to resume.
174
+ return null;
175
+ }
176
+ }
177
+
178
+ async function readCheckpointCount(mgr: ResumeNoticeDeps['checkpointManager']): Promise<number | null> {
179
+ if (!mgr) return null;
180
+ try {
181
+ return (await mgr.list()).length;
182
+ } catch {
183
+ // Checkpoint manager present but its cached init() rejection makes every
184
+ // call fail forever (see services.ts) — treat as "unknown", not zero.
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Gather real facts from disk/services and, if there is anything to report,
191
+ * print ONE compact system message via `deps.router.high`. No-op (and no
192
+ * message) when there is no prior session, no checkpoints, and no chain
193
+ * history — a fresh working directory stays quiet.
194
+ */
195
+ export async function announceResumeState(deps: ResumeNoticeDeps): Promise<void> {
196
+ const session = readLastSessionTurns(deps);
197
+ const checkpointCount = await readCheckpointCount(deps.checkpointManager);
198
+ const lastChain = mostRecentChain(deps.chainHistory);
199
+
200
+ const notice = buildResumeNotice({
201
+ turnCount: session?.turnCount ?? null,
202
+ lastSessionId: session?.lastSessionId ?? null,
203
+ checkpointCount,
204
+ lastChainOutcome: lastChain ? describeChainOutcome(lastChain) : null,
205
+ memoryAvailable: deps.memoryAvailable,
206
+ });
207
+
208
+ if (notice) deps.router.high(notice);
209
+ }