@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
@@ -2,18 +2,24 @@
2
2
  * workspace-trust.ts — per-workspace trust gate.
3
3
  *
4
4
  * The first time GoodVibes opens a workspace (a cwd / project root it has no
5
- * prior state for), that workspace is "restricted": only read-category tools
6
- * run; write, execute, and delegate tools are denied until the user marks the
7
- * workspace trusted. This is NOT a parallel permission checker — the decision
8
- * is consulted by the real permission machinery at its final ask layer (the
9
- * requestPermission callback wired in bootstrap-core.ts), so it composes with,
10
- * and cannot drift from, the existing PermissionManager layer chain.
5
+ * prior decision for), that workspace is "undecided": only read-category
6
+ * tools run; the first write, execute, or delegate tool request raises the
7
+ * trust question AT THAT MOMENT — the exact point it has a real consequence —
8
+ * instead of silently failing or being decided by a side effect of the
9
+ * product's own droppings. This is NOT a parallel permission checker — the
10
+ * decision is consulted by the real permission machinery at its final ask
11
+ * layer (the requestPermission callback wired in bootstrap-core.ts), so it
12
+ * composes with, and cannot drift from, the existing PermissionManager layer
13
+ * chain.
11
14
  *
12
- * Trust is persisted per-workspace in <cwd>/.goodvibes/tui/trust.json. A
13
- * workspace that already carries GoodVibes runtime state (prior sessions,
14
- * checkpoints, state, memory, or a project onboarding marker) is grandfathered
15
- * as trusted on first load, so the gate only ever prompts for genuinely new
16
- * places.
15
+ * Trust is persisted per-workspace in <cwd>/.goodvibes/tui/trust.json. There
16
+ * is no more grandfathering: a workspace that already carries prior GoodVibes
17
+ * runtime state gets no special treatment (that side-effect-based shortcut
18
+ * used to paper over the fact that the first session's write attempts were
19
+ * silently denied without ever recording a real decision — now that the first
20
+ * attempt properly raises the question and persists the answer, the shortcut
21
+ * is unnecessary and would only hide a decision the user should actually see
22
+ * once, even on an upgrade).
17
23
  */
18
24
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
19
25
  import { join } from 'node:path';
@@ -25,13 +31,41 @@ type AskCallback = (request: PermissionPromptRequest) => Promise<PermissionPromp
25
31
 
26
32
  /**
27
33
  * Wrap the permission machinery's final ask callback with the workspace trust
28
- * gate: in an untrusted workspace, deny any non-read tool outright instead of
29
- * asking. This runs inside PermissionManager's own layer chain (it IS the ask
34
+ * gate. This runs inside PermissionManager's own layer chain (it IS the ask
30
35
  * layer), so it composes with — and cannot drift from — the real machinery.
36
+ *
37
+ * - Trusted workspace: every category passes through to `ask` unchanged.
38
+ * - Restricted workspace with an EXPLICIT prior decision: non-read categories
39
+ * are denied outright, same as before — that IS what "restricted" means
40
+ * once the user has actually chosen it.
41
+ * - Undecided workspace (no decision yet, ever): the first non-read request
42
+ * raises `requestTrustDecision()` — one modal, persisted via `setLevel` —
43
+ * and, if the answer is 'trusted', the ORIGINAL request is forwarded to
44
+ * `ask` so the very thing the user just approved actually happens instead
45
+ * of failing anyway. Concurrent requests that arrive before the first
46
+ * decision resolves await the same in-flight prompt rather than opening a
47
+ * second one.
31
48
  */
32
- export function trustGatedAsk(manager: Pick<WorkspaceTrustManager, 'isCategoryAllowed'>, ask: AskCallback): AskCallback {
33
- return (request) =>
34
- manager.isCategoryAllowed(request.category) ? ask(request) : Promise.resolve({ approved: false });
49
+ export function trustGatedAsk(
50
+ manager: Pick<WorkspaceTrustManager, 'isCategoryAllowed' | 'isDecided' | 'setLevel'>,
51
+ ask: AskCallback,
52
+ requestTrustDecision: () => Promise<WorkspaceTrustLevel>,
53
+ ): AskCallback {
54
+ let pendingDecision: Promise<WorkspaceTrustLevel> | null = null;
55
+ return async (request) => {
56
+ if (manager.isCategoryAllowed(request.category)) return ask(request);
57
+ if (!manager.isDecided()) {
58
+ if (!pendingDecision) {
59
+ pendingDecision = requestTrustDecision().finally(() => {
60
+ pendingDecision = null;
61
+ });
62
+ }
63
+ const level = await pendingDecision;
64
+ await manager.setLevel(level);
65
+ if (manager.isCategoryAllowed(request.category)) return ask(request);
66
+ }
67
+ return { approved: false };
68
+ };
35
69
  }
36
70
 
37
71
  export type WorkspaceTrustLevel = 'trusted' | 'restricted';
@@ -115,8 +149,6 @@ export function readPersistedWorkspaceTrust(paths: WorkspaceTrustPaths): Persist
115
149
 
116
150
  export interface WorkspaceTrustManagerOptions {
117
151
  readonly shellPaths: WorkspaceTrustPaths;
118
- /** Whether the workspace already had prior GoodVibes runtime state at startup. */
119
- readonly hadPriorState: boolean;
120
152
  }
121
153
 
122
154
  export class WorkspaceTrustManager {
@@ -124,20 +156,22 @@ export class WorkspaceTrustManager {
124
156
  private grandfathered = false;
125
157
  private loaded = false;
126
158
  private readonly store: JsonFileStore<PersistedWorkspaceTrust>;
127
- private readonly hadPriorState: boolean;
128
159
 
129
160
  constructor(options: WorkspaceTrustManagerOptions) {
130
161
  this.store = new JsonFileStore<PersistedWorkspaceTrust>(
131
162
  options.shellPaths.resolveProjectPath('tui', TRUST_FILE),
132
163
  );
133
- this.hadPriorState = options.hadPriorState;
134
164
  }
135
165
 
136
166
  /**
137
- * Load the persisted decision. If none exists, grandfather a workspace that
138
- * already had prior GoodVibes state (persisting the decision so it is never
139
- * re-evaluated); otherwise the workspace stays undecided and the gate treats
140
- * it as restricted until the user chooses.
167
+ * Load the persisted decision, if one exists. No grandfathering: a
168
+ * workspace with no persisted decision stays undecided (the gate treats it
169
+ * as restricted, and the first non-read tool request raises the trust
170
+ * question via `trustGatedAsk`'s `requestTrustDecision` callback) — even
171
+ * one that already carries prior GoodVibes runtime state. `grandfathered`
172
+ * on an already-persisted decision is read-only history from before this
173
+ * fix (status/doctor still report it honestly); nothing new is ever
174
+ * grandfathered.
141
175
  */
142
176
  async load(): Promise<void> {
143
177
  if (this.loaded) return;
@@ -145,13 +179,6 @@ export class WorkspaceTrustManager {
145
179
  if (persisted && (persisted.level === 'trusted' || persisted.level === 'restricted')) {
146
180
  this.level = persisted.level;
147
181
  this.grandfathered = persisted.grandfathered ?? false;
148
- this.loaded = true;
149
- return;
150
- }
151
- if (this.hadPriorState) {
152
- this.level = 'trusted';
153
- this.grandfathered = true;
154
- await this.persist();
155
182
  }
156
183
  this.loaded = true;
157
184
  }
@@ -59,6 +59,8 @@ export interface UiPlatformServices {
59
59
  readonly surfaceRegistry: RuntimeServices['surfaceRegistry'];
60
60
  readonly subscriptionManager: RuntimeServices['subscriptionManager'];
61
61
  readonly secretsManager: SecretsManager;
62
+ /** Per-device pairing tokens — backs the pairing modal QR and the settings device surface. */
63
+ readonly pairingTokens: RuntimeServices['pairingTokens'];
62
64
  readonly tokenAuditor: RuntimeServices['tokenAuditor'];
63
65
  readonly replayEngine: RuntimeServices['replayEngine'];
64
66
  readonly webhookNotifier: RuntimeServices['webhookNotifier'];
@@ -77,6 +79,8 @@ export interface UiPlatformServices {
77
79
  readonly sessionSpineActive?: boolean;
78
80
  readonly sessionSpineStatus?: 'unknown' | 'online' | 'offline';
79
81
  };
82
+ /** Undelivered daemon receipts (update/crash/migration) for the attach-time consuming read; empty when no daemon runs. */
83
+ collectDaemonReceipts(): readonly { readonly id: string; readonly text: string; readonly at: number }[];
80
84
  restart(): Promise<{
81
85
  readonly daemonRunning: boolean;
82
86
  readonly daemonPortInUse?: boolean;
@@ -113,7 +117,7 @@ export interface UiRuntimeSharedServices {
113
117
  readonly distributedRuntime: RuntimeServices['distributedRuntime'];
114
118
  readonly remoteRunnerRegistry: RuntimeServices['remoteRunnerRegistry'] & RemoteRunnerRegistry;
115
119
  readonly remoteSupervisor: RuntimeServices['remoteSupervisor'] & RemoteSupervisor;
116
- /** W2.2: the shared live process registry backing the Fleet panel. */
120
+ /** the shared live process registry backing the Fleet panel. */
117
121
  readonly processRegistry: RuntimeServices['processRegistry'];
118
122
  /** The shared runtime event bus — the Fleet panel subscribes to its 'communication' domain for the honest steer-consumed signal. */
119
123
  readonly runtimeBus: RuntimeServices['runtimeBus'];
@@ -186,6 +190,7 @@ export function createUiRuntimeServices(
186
190
  surfaceRegistry: runtimeServices.surfaceRegistry,
187
191
  subscriptionManager: runtimeServices.subscriptionManager,
188
192
  secretsManager: runtimeServices.secretsManager,
193
+ pairingTokens: runtimeServices.pairingTokens,
189
194
  tokenAuditor: runtimeServices.tokenAuditor,
190
195
  replayEngine: runtimeServices.replayEngine,
191
196
  webhookNotifier: runtimeServices.webhookNotifier,
@@ -1,79 +1,25 @@
1
1
  /**
2
2
  * Pure logic for `/update`: version comparison, the latest-release-tag
3
- * redirect lookup, and honest install-kind detection. No filesystem or
4
- * process side effects here — everything takes its inputs as arguments so
5
- * tests exercise it without touching the network or the host, matching the
6
- * repo rule that tests never compare against the live build VERSION and
7
- * never make live network calls.
3
+ * redirect lookup, and honest install-kind detection.
4
+ *
5
+ * Version comparison and the release-tag lookup are re-exported from the
6
+ * SDK's canonical update policy module (platform/runtime/self-update), which
7
+ * was hoisted from this file's semantics — one mechanism everywhere.
8
+ * Install-kind detection stays local: it encodes how THIS package is
9
+ * installed (compiled binary vs bun/npm package vs source run) and what
10
+ * command replaces a swap for each kind.
8
11
  *
9
12
  * The self-update download/verify/swap orchestration that USES these lives
10
13
  * in src/input/commands/update-runtime.ts; this module only decides "is
11
14
  * there a newer version" and "can this install be swapped in place".
12
15
  */
13
-
14
- export function normalizeVersion(version: string): string {
15
- return version.replace(/^v/i, '').trim();
16
- }
17
-
18
- /**
19
- * Compares two version strings component-wise as dotted non-negative
20
- * integers (a leading "v" is ignored, matching the "vX.Y.Z" release tag
21
- * format). Returns -1/0/1 for a<b / a==b / a>b. Non-numeric or missing
22
- * components are treated as 0, so "1.2" and "1.2.0" compare equal.
23
- */
24
- export function compareVersions(a: string, b: string): -1 | 0 | 1 {
25
- const partsA = normalizeVersion(a).split('.');
26
- const partsB = normalizeVersion(b).split('.');
27
- const length = Math.max(partsA.length, partsB.length);
28
- for (let i = 0; i < length; i++) {
29
- const x = Number.parseInt(partsA[i] ?? '0', 10) || 0;
30
- const y = Number.parseInt(partsB[i] ?? '0', 10) || 0;
31
- if (x < y) return -1;
32
- if (x > y) return 1;
33
- }
34
- return 0;
35
- }
36
-
37
- /**
38
- * Extracts the release tag from a GitHub "/releases/latest" redirect
39
- * Location header, e.g. "https://github.com/mgd34msu/goodvibes-tui/releases/tag/v1.13.2"
40
- * -> "v1.13.2". Mirrors scripts/install.sh's own redirect-URL tag parsing
41
- * (it takes everything after the final slash of the redirect target).
42
- */
43
- export function parseReleaseTagFromLocation(location: string | null | undefined): string | null {
44
- if (!location) return null;
45
- const segments = location.split('/').filter((segment) => segment.length > 0);
46
- const tag = segments[segments.length - 1];
47
- return tag && tag.length > 0 ? tag : null;
48
- }
49
-
50
- /** Minimal shape of the subset of the fetch API this module needs, so tests inject a stub instead of the real network. */
51
- export interface UpdateFetchLike {
52
- (url: string, init?: { method?: string; redirect?: 'manual' | 'follow' | 'error' }): Promise<{
53
- readonly ok: boolean;
54
- readonly status: number;
55
- readonly url: string;
56
- readonly headers: { get(name: string): string | null };
57
- text(): Promise<string>;
58
- arrayBuffer(): Promise<ArrayBuffer>;
59
- }>;
60
- }
61
-
62
- /**
63
- * Resolves the latest release tag the same way scripts/install.sh does:
64
- * a HEAD request with redirects NOT followed, reading the tag out of the
65
- * response's redirect Location header. Throws if no tag can be resolved —
66
- * callers must not silently fall back to "already current".
67
- */
68
- export async function resolveLatestReleaseTag(fetchImpl: UpdateFetchLike, releasesLatestUrl: string): Promise<string> {
69
- const response = await fetchImpl(releasesLatestUrl, { method: 'HEAD', redirect: 'manual' });
70
- const location = response.headers.get('location');
71
- const tag = parseReleaseTagFromLocation(location) ?? (response.url !== releasesLatestUrl ? parseReleaseTagFromLocation(response.url) : null);
72
- if (!tag) {
73
- throw new Error(`could not resolve the latest release tag from ${releasesLatestUrl} (no redirect Location header)`);
74
- }
75
- return tag;
76
- }
16
+ export {
17
+ compareVersions,
18
+ normalizeVersion,
19
+ parseReleaseTagFromLocation,
20
+ resolveLatestReleaseTag,
21
+ type UpdateFetchLike,
22
+ } from '@pellux/goodvibes-sdk/platform/runtime/self-update';
77
23
 
78
24
  /**
79
25
  * How this running process was installed, detected honestly from
@@ -2,7 +2,7 @@
2
2
  // workstream-services.ts — phase/work-item orchestration engine
3
3
  //
4
4
  // Constructs the TUI's OrchestrationEngine instance (@pellux/goodvibes-sdk/
5
- // platform/orchestration, landed on SDK main as wo701/W4.1) and a thin
5
+ // platform/orchestration, landed on SDK main as) and a thin
6
6
  // command-facing facade around it. Extracted into its own module rather than
7
7
  // built inline in services.ts: services.ts sits at the architecture check's
8
8
  // 800-line cap (scripts/check-architecture.ts), so any new service gets its
@@ -17,7 +17,7 @@
17
17
  // (input/commands/workstream-runtime.ts) needs a create -> propose -> approve
18
18
  // -> launch flow (Pillar-3 doctrine: render the plan in the transcript before
19
19
  // spending anything, mirroring /plan's approve step). REALITY-WINS DIVERGENCE
20
- // from the wo703 design brief: the brief recommended an ENGINE-owned draft; the
20
+ // from the design brief: the brief recommended an ENGINE-owned draft; the
21
21
  // real engine (verified against the linked SDK build) exposes no pre-creation
22
22
  // draft concept at all, so WorkstreamDraft below is TUI-owned state, held on
23
23
  // this module's facade instance (constructed once, threaded onto
@@ -1,17 +1,21 @@
1
- import type { ConversationManager } from '../core/conversation';
2
- import type { PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions';
3
- import type { SessionSnapshot } from '@/runtime/index.ts';
4
- import type { SystemMessageRouter } from '../core/system-message-router.ts';
5
- import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
6
- import { replayJournalForSession } from '../core/session-recovery.ts';
1
+ import type { PermissionRequest, RememberTier } from '@pellux/goodvibes-sdk/platform/permissions';
7
2
  import { applyHunkKey, buildModifiedEditArgs, type HunkSelectionState } from '../permissions/hunk-selection.ts';
8
3
 
9
4
  export type PendingPermissionState = PermissionRequest & {
10
- resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
5
+ resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>, extras?: { rememberTier?: RememberTier; reason?: string }) => void;
11
6
  /** Present only when isHunkSelectable(request) was true when the prompt was opened. */
12
7
  hunkState?: HunkSelectionState;
13
- /** True once the user pressed `d` to expand a condensed low-risk card. (UX-B 2b.) */
8
+ /** True once the user pressed `d` to expand a condensed low-risk card. (2b.) */
14
9
  detailsExpanded?: boolean;
10
+ /**
11
+ * Active typed-reply mode: 'deny-reason' (started by typing while the card
12
+ * is up — Enter denies with the typed reason as feedback) or 'exec-answer'
13
+ * (an exec-prompt ask; Enter approves with the typed answer feeding the
14
+ * running command's stdin). Undefined = plain card keys.
15
+ */
16
+ replyMode?: 'deny-reason' | 'exec-answer';
17
+ /** The reply draft while replyMode is active. */
18
+ replyBuffer?: string;
15
19
  /**
16
20
  * Epoch ms when the prompt first appeared. Keystrokes that arrive within
17
21
  * APPROVAL_INPUT_DEBOUNCE_MS of this are swallowed (not interpreted as an
@@ -34,47 +38,8 @@ export const APPROVAL_INPUT_DEBOUNCE_MS = 350;
34
38
  export type BlockingInputHandlerOptions = {
35
39
  data: string;
36
40
  pendingPermission: PendingPermissionState | null;
37
- recoveryPending: boolean;
38
41
  abortTurn: () => void;
39
- conversation: ConversationManager;
40
- systemMessageRouter: SystemMessageRouter;
41
42
  render: () => void;
42
- loadRecoveryConversation: () => SessionSnapshot | null;
43
- deleteRecoveryFile: () => void;
44
- /**
45
- * Copy the live recovery.jsonl aside to a preserved sibling so the dismiss
46
- * message's promise ("still on disk") survives this session's later 60s
47
- * autosaves, which would otherwise overwrite the shared recovery file with
48
- * the CURRENT (post-dismiss) session's state (W3 Finding 3). Optional so
49
- * every pre-existing test/caller that doesn't care about preservation is
50
- * unaffected; when omitted, dismiss behaves exactly as before.
51
- */
52
- preserveRecoveryFile?: () => { readonly preserved: boolean; readonly replacedPrevious: boolean };
53
- /**
54
- * Absolute home directory used to locate the transcript journal for this
55
- * recovery session. Required for journal replay on Ctrl+R restore.
56
- */
57
- homeDirectory: string;
58
- /**
59
- * The session ID that the recovery file belongs to. Required for journal
60
- * replay so the correct journal path can be resolved.
61
- */
62
- sessionId: string;
63
- /**
64
- * Persist the post-replay snapshot so the WAL gap is durably closed.
65
- * Called with the replayed message list. Best-effort — failures are swallowed
66
- * inside replayJournalForSession.
67
- */
68
- persistSnapshot: (messages: ConversationMessageSnapshot[]) => void;
69
- /**
70
- * Optional callback invoked after Ctrl+R restore to reopen panels captured in
71
- * the recovery snapshot's returnContext. When provided (as wired in main.ts),
72
- * the callback iterates snapshot.returnContext.openPanels and calls
73
- * panelManager.open() for each entry, then panelManager.show() + render() to
74
- * restore the panel posture from the recovered session. When omitted, panel
75
- * posture is not restored.
76
- */
77
- reopenPanels?: (snapshot: SessionSnapshot) => void;
78
43
  /** Injectable clock for the approval-input debounce; defaults to Date.now(). Tests only. */
79
44
  now?: number;
80
45
  };
@@ -82,7 +47,6 @@ export type BlockingInputHandlerOptions = {
82
47
  export type BlockingInputHandlerResult = {
83
48
  handled: boolean;
84
49
  pendingPermission: PendingPermissionState | null;
85
- recoveryPending: boolean;
86
50
  };
87
51
 
88
52
  export function handleBlockingShellInput(
@@ -91,18 +55,8 @@ export function handleBlockingShellInput(
91
55
  const {
92
56
  data,
93
57
  pendingPermission,
94
- recoveryPending,
95
58
  abortTurn,
96
- conversation,
97
- systemMessageRouter,
98
59
  render,
99
- loadRecoveryConversation,
100
- deleteRecoveryFile,
101
- preserveRecoveryFile,
102
- homeDirectory,
103
- sessionId,
104
- persistSnapshot,
105
- reopenPanels,
106
60
  } = options;
107
61
 
108
62
  if (pendingPermission) {
@@ -115,7 +69,7 @@ export function handleBlockingShellInput(
115
69
  const now = options.now ?? Date.now();
116
70
  if (req.openedAt !== undefined && now - req.openedAt < APPROVAL_INPUT_DEBOUNCE_MS) {
117
71
  render();
118
- return { handled: true, pendingPermission, recoveryPending };
72
+ return { handled: true, pendingPermission };
119
73
  }
120
74
 
121
75
  if (req.hunkState) {
@@ -123,16 +77,79 @@ export function handleBlockingShellInput(
123
77
  if (commit === 'apply') {
124
78
  req.resolve(true, false, buildModifiedEditArgs(req, state));
125
79
  render();
126
- return { handled: true, pendingPermission: null, recoveryPending };
80
+ return { handled: true, pendingPermission: null };
127
81
  }
128
82
  if (commit === 'cancel') {
129
83
  req.resolve(false, false);
130
84
  abortTurn();
131
85
  render();
132
- return { handled: true, pendingPermission: null, recoveryPending };
86
+ return { handled: true, pendingPermission: null };
87
+ }
88
+ render();
89
+ return { handled: true, pendingPermission: { ...req, hunkState: state } };
90
+ }
91
+
92
+ // Ctrl+C is ALWAYS the hard abort: deny and kill the turn, in every mode.
93
+ if (data === '\x03') {
94
+ req.resolve(false, false);
95
+ abortTurn();
96
+ render();
97
+ return { handled: true, pendingPermission: null };
98
+ }
99
+
100
+ // Typed-reply mode: deny-with-reason (started by typing on any card) or
101
+ // exec-answer (an exec-prompt ask opens in this mode). Every printable
102
+ // key is text here — nothing is a card command.
103
+ if (req.replyMode) {
104
+ const buffer = req.replyBuffer ?? '';
105
+ if (data === '\r' || data === '\n') {
106
+ if (req.replyMode === 'exec-answer') {
107
+ // The typed answer feeds the running command's stdin via the
108
+ // decision's modifiedArgs (SDK exec-prompt wiring contract).
109
+ req.resolve(true, false, { answer: buffer });
110
+ } else {
111
+ // Deny is feedback: the reason rides the structured "user declined"
112
+ // tool result so the model can adapt — the turn is NOT aborted.
113
+ req.resolve(false, false, undefined, { reason: buffer });
114
+ }
115
+ render();
116
+ return { handled: true, pendingPermission: null };
117
+ }
118
+ if (data === '\x1b') {
119
+ if (req.replyMode === 'exec-answer') {
120
+ // Esc on an exec-prompt: clear a draft first; with nothing typed it
121
+ // declines the prompt (the run gets the honest unanswered result).
122
+ if (buffer.length > 0) {
123
+ render();
124
+ return { handled: true, pendingPermission: { ...req, replyBuffer: '' } };
125
+ }
126
+ req.resolve(false, false);
127
+ render();
128
+ return { handled: true, pendingPermission: null };
129
+ }
130
+ // Esc leaves deny-reason mode back to the plain card.
131
+ render();
132
+ return { handled: true, pendingPermission: { ...req, replyMode: undefined, replyBuffer: undefined } };
133
+ }
134
+ if (data === '\x7f' || data === '\b') {
135
+ render();
136
+ return { handled: true, pendingPermission: { ...req, replyBuffer: buffer.slice(0, -1) } };
137
+ }
138
+ if (data.length >= 1 && !data.startsWith('\x1b') && data >= ' ') {
139
+ render();
140
+ return { handled: true, pendingPermission: { ...req, replyBuffer: buffer + data } };
133
141
  }
134
142
  render();
135
- return { handled: true, pendingPermission: { ...req, hunkState: state }, recoveryPending };
143
+ return { handled: true, pendingPermission };
144
+ }
145
+
146
+ // Scroll, mouse, PageUp/Down, arrow, and panel-navigation keys — plus a
147
+ // bare Esc — are not card answers. Pass them through to the normal input
148
+ // handler so the transcript stays scrollable and Esc only drops focus. The
149
+ // request stays pending (answer it with y/n or a remember tier when ready);
150
+ // Ctrl+C above is still the hard abort, and 'n' still denies.
151
+ if (data.startsWith('\x1b')) {
152
+ return { handled: false, pendingPermission };
136
153
  }
137
154
 
138
155
  const key = data.toLowerCase().trim();
@@ -140,20 +157,37 @@ export function handleBlockingShellInput(
140
157
  if (key === 'y') {
141
158
  req.resolve(true, false);
142
159
  render();
143
- return { handled: true, pendingPermission: null, recoveryPending };
160
+ return { handled: true, pendingPermission: null };
144
161
  }
145
162
 
146
163
  if (key === 'a') {
147
- req.resolve(true, true);
164
+ // Legacy always-this-session choice — the 'session' remember tier.
165
+ req.resolve(true, true, undefined, { rememberTier: 'session' });
166
+ render();
167
+ return { handled: true, pendingPermission: null };
168
+ }
169
+
170
+ // Numbered remember tiers ([1]..[N], most specific first) from the SDK's
171
+ // rememberOptions: approve AND remember at that tier. A generalizing tier
172
+ // writes a durable user-origin rule; 'session' only caches in memory.
173
+ if (/^[1-9]$/.test(key) && req.rememberOptions && req.rememberOptions.length > 0) {
174
+ const option = req.rememberOptions[Number(key) - 1];
175
+ if (option) {
176
+ req.resolve(true, option.tier === 'session', undefined, { rememberTier: option.tier });
177
+ render();
178
+ return { handled: true, pendingPermission: null };
179
+ }
148
180
  render();
149
- return { handled: true, pendingPermission: null, recoveryPending };
181
+ return { handled: true, pendingPermission };
150
182
  }
151
183
 
152
- if (key === 'n' || data === '\x1b' || data === '\x03') {
184
+ if (key === 'n') {
185
+ // Plain deny — feedback to the model (the SDK renders an honest "user
186
+ // declined" result), never a turn abort. Ctrl+C above is the hard stop;
187
+ // Esc drops focus (handled above) rather than denying.
153
188
  req.resolve(false, false);
154
- abortTurn();
155
189
  render();
156
- return { handled: true, pendingPermission: null, recoveryPending };
190
+ return { handled: true, pendingPermission: null };
157
191
  }
158
192
 
159
193
  if (key === 'd') {
@@ -162,77 +196,19 @@ export function handleBlockingShellInput(
162
196
  return {
163
197
  handled: true,
164
198
  pendingPermission: { ...req, detailsExpanded: !req.detailsExpanded },
165
- recoveryPending,
166
199
  };
167
200
  }
168
201
 
169
- render();
170
- return { handled: true, pendingPermission, recoveryPending };
171
- }
172
-
173
- if (recoveryPending) {
174
- if (data === '\x12') {
175
- const recovery = loadRecoveryConversation();
176
- if (recovery) {
177
- conversation.fromJSON({
178
- messages: recovery.messages as Parameters<typeof conversation.fromJSON>[0]['messages'],
179
- title: recovery.title,
180
- titleSource: recovery.titleSource,
181
- });
182
- // Replay journal records that post-date the recovery snapshot so turns
183
- // written after the last recovery-file write (but before SIGKILL) are
184
- // not silently dropped. snapshotTimestamp=0 when timestamp is absent so
185
- // all journal records are replayed — safer than dropping.
186
- replayJournalForSession({
187
- homeDirectory,
188
- sessionId,
189
- snapshotTimestamp: recovery.timestamp ?? 0,
190
- conversation,
191
- persistSnapshot,
192
- });
193
- reopenPanels?.(recovery);
194
- systemMessageRouter.high('[Recovery] Session restored.');
195
- deleteRecoveryFile();
196
- } else {
197
- systemMessageRouter.high('[Recovery] Failed to restore saved data.');
198
- }
202
+ // Any other printable key starts deny-with-reason: what the user types
203
+ // becomes the denial feedback, seeded with this first character.
204
+ if (data.length === 1 && data >= ' ') {
199
205
  render();
200
- return { handled: true, pendingPermission: null, recoveryPending: false };
206
+ return { handled: true, pendingPermission: { ...req, replyMode: 'deny-reason', replyBuffer: data } };
201
207
  }
202
208
 
203
- if (data === '\x1b' || data === '\x03') {
204
- systemMessageRouter.high('[Recovery] Discarded recovery data.');
205
- deleteRecoveryFile();
206
- render();
207
- return { handled: true, pendingPermission: null, recoveryPending: false };
208
- }
209
-
210
- // Any other key demonstrates the user's intent to ignore the banner and
211
- // keep working — the prompt's own text invites exactly this ("start
212
- // typing to ignore it"). Previously this branch re-posted the same
213
- // '[Recovery] Ctrl+R to restore...' line on EVERY such key and never
214
- // cleared recoveryPending, so a user who took that invitation got a
215
- // fresh [Recovery] line injected into the transcript around every
216
- // character they typed, forever. Dismiss ONCE instead: clear
217
- // recoveryPending so it stops re-asserting, but do NOT delete the
218
- // recovery file — dismiss is not discard, and the file remains
219
- // restorable by the automatic recovery check on the next launch.
220
- //
221
- // W3 Finding 3: the promise below ("still on disk; you will be asked
222
- // again") used to go false silently — main.ts's 60s autosave overwrites
223
- // the single shared recovery.jsonl with the CURRENT session's state
224
- // within a minute. preserveRecoveryFile (when wired) copies it aside
225
- // NOW, while it still holds the dismissed session's data, so the
226
- // promise stays true. If an earlier dismiss's preserved snapshot gets
227
- // replaced by this one, say so honestly instead of silently discarding it.
228
- const preserveResult = preserveRecoveryFile?.();
229
- if (preserveResult?.replacedPrevious) {
230
- systemMessageRouter.low('[Recovery] Replacing the previously preserved (unrestored) snapshot with this one.');
231
- }
232
- systemMessageRouter.high('[Recovery] Dismissed — the unsaved session is still on disk; you will be asked again next time GoodVibes starts here.');
233
209
  render();
234
- return { handled: false, pendingPermission, recoveryPending: false };
210
+ return { handled: true, pendingPermission };
235
211
  }
236
212
 
237
- return { handled: false, pendingPermission, recoveryPending };
213
+ return { handled: false, pendingPermission };
238
214
  }