@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.
- package/CHANGELOG.md +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- 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
|
|
6
|
-
* run; write, execute,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
13
|
-
* workspace that already carries
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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(
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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
|
-
/**
|
|
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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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. (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
160
|
+
return { handled: true, pendingPermission: null };
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
if (key === 'a') {
|
|
147
|
-
|
|
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
|
|
181
|
+
return { handled: true, pendingPermission };
|
|
150
182
|
}
|
|
151
183
|
|
|
152
|
-
if (key === 'n'
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
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:
|
|
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:
|
|
210
|
+
return { handled: true, pendingPermission };
|
|
235
211
|
}
|
|
236
212
|
|
|
237
|
-
return { handled: false, pendingPermission
|
|
213
|
+
return { handled: false, pendingPermission };
|
|
238
214
|
}
|