@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.
- package/CHANGELOG.md +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -4,9 +4,14 @@ import type { SessionSnapshot } from '@/runtime/index.ts';
|
|
|
4
4
|
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
5
5
|
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
6
6
|
import { replayJournalForSession } from '../core/session-recovery.ts';
|
|
7
|
+
import { applyHunkKey, buildModifiedEditArgs, type HunkSelectionState } from '../permissions/hunk-selection.ts';
|
|
7
8
|
|
|
8
9
|
export type PendingPermissionState = PermissionRequest & {
|
|
9
|
-
resolve: (approved: boolean, remember?: boolean) => void;
|
|
10
|
+
resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
|
|
11
|
+
/** Present only when isHunkSelectable(request) was true when the prompt was opened. */
|
|
12
|
+
hunkState?: HunkSelectionState;
|
|
13
|
+
/** True once the user pressed `d` to expand a condensed low-risk card. (UX-B 2b.) */
|
|
14
|
+
detailsExpanded?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export type BlockingInputHandlerOptions = {
|
|
@@ -19,6 +24,15 @@ export type BlockingInputHandlerOptions = {
|
|
|
19
24
|
render: () => void;
|
|
20
25
|
loadRecoveryConversation: () => SessionSnapshot | null;
|
|
21
26
|
deleteRecoveryFile: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Copy the live recovery.jsonl aside to a preserved sibling so the dismiss
|
|
29
|
+
* message's promise ("still on disk") survives this session's later 60s
|
|
30
|
+
* autosaves, which would otherwise overwrite the shared recovery file with
|
|
31
|
+
* the CURRENT (post-dismiss) session's state (W3 Finding 3). Optional so
|
|
32
|
+
* every pre-existing test/caller that doesn't care about preservation is
|
|
33
|
+
* unaffected; when omitted, dismiss behaves exactly as before.
|
|
34
|
+
*/
|
|
35
|
+
preserveRecoveryFile?: () => { readonly preserved: boolean; readonly replacedPrevious: boolean };
|
|
22
36
|
/**
|
|
23
37
|
* Absolute home directory used to locate the transcript journal for this
|
|
24
38
|
* recovery session. Required for journal replay on Ctrl+R restore.
|
|
@@ -65,6 +79,7 @@ export function handleBlockingShellInput(
|
|
|
65
79
|
render,
|
|
66
80
|
loadRecoveryConversation,
|
|
67
81
|
deleteRecoveryFile,
|
|
82
|
+
preserveRecoveryFile,
|
|
68
83
|
homeDirectory,
|
|
69
84
|
sessionId,
|
|
70
85
|
persistSnapshot,
|
|
@@ -73,6 +88,24 @@ export function handleBlockingShellInput(
|
|
|
73
88
|
|
|
74
89
|
if (pendingPermission) {
|
|
75
90
|
const req = pendingPermission;
|
|
91
|
+
|
|
92
|
+
if (req.hunkState) {
|
|
93
|
+
const { state, commit } = applyHunkKey(req.hunkState, data);
|
|
94
|
+
if (commit === 'apply') {
|
|
95
|
+
req.resolve(true, false, buildModifiedEditArgs(req, state));
|
|
96
|
+
render();
|
|
97
|
+
return { handled: true, pendingPermission: null, recoveryPending };
|
|
98
|
+
}
|
|
99
|
+
if (commit === 'cancel') {
|
|
100
|
+
req.resolve(false, false);
|
|
101
|
+
abortTurn();
|
|
102
|
+
render();
|
|
103
|
+
return { handled: true, pendingPermission: null, recoveryPending };
|
|
104
|
+
}
|
|
105
|
+
render();
|
|
106
|
+
return { handled: true, pendingPermission: { ...req, hunkState: state }, recoveryPending };
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
const key = data.toLowerCase().trim();
|
|
77
110
|
|
|
78
111
|
if (key === 'y') {
|
|
@@ -94,6 +127,16 @@ export function handleBlockingShellInput(
|
|
|
94
127
|
return { handled: true, pendingPermission: null, recoveryPending };
|
|
95
128
|
}
|
|
96
129
|
|
|
130
|
+
if (key === 'd') {
|
|
131
|
+
// Toggle the condensed↔full detail view without resolving the request.
|
|
132
|
+
render();
|
|
133
|
+
return {
|
|
134
|
+
handled: true,
|
|
135
|
+
pendingPermission: { ...req, detailsExpanded: !req.detailsExpanded },
|
|
136
|
+
recoveryPending,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
97
140
|
render();
|
|
98
141
|
return { handled: true, pendingPermission, recoveryPending };
|
|
99
142
|
}
|
|
@@ -135,10 +178,31 @@ export function handleBlockingShellInput(
|
|
|
135
178
|
return { handled: true, pendingPermission: null, recoveryPending: false };
|
|
136
179
|
}
|
|
137
180
|
|
|
138
|
-
//
|
|
139
|
-
|
|
181
|
+
// Any other key demonstrates the user's intent to ignore the banner and
|
|
182
|
+
// keep working — the prompt's own text invites exactly this ("start
|
|
183
|
+
// typing to ignore it"). Previously this branch re-posted the same
|
|
184
|
+
// '[Recovery] Ctrl+R to restore...' line on EVERY such key and never
|
|
185
|
+
// cleared recoveryPending, so a user who took that invitation got a
|
|
186
|
+
// fresh [Recovery] line injected into the transcript around every
|
|
187
|
+
// character they typed, forever. Dismiss ONCE instead: clear
|
|
188
|
+
// recoveryPending so it stops re-asserting, but do NOT delete the
|
|
189
|
+
// recovery file — dismiss is not discard, and the file remains
|
|
190
|
+
// restorable by the automatic recovery check on the next launch.
|
|
191
|
+
//
|
|
192
|
+
// W3 Finding 3: the promise below ("still on disk; you will be asked
|
|
193
|
+
// again") used to go false silently — main.ts's 60s autosave overwrites
|
|
194
|
+
// the single shared recovery.jsonl with the CURRENT session's state
|
|
195
|
+
// within a minute. preserveRecoveryFile (when wired) copies it aside
|
|
196
|
+
// NOW, while it still holds the dismissed session's data, so the
|
|
197
|
+
// promise stays true. If an earlier dismiss's preserved snapshot gets
|
|
198
|
+
// replaced by this one, say so honestly instead of silently discarding it.
|
|
199
|
+
const preserveResult = preserveRecoveryFile?.();
|
|
200
|
+
if (preserveResult?.replacedPrevious) {
|
|
201
|
+
systemMessageRouter.low('[Recovery] Replacing the previously preserved (unrestored) snapshot with this one.');
|
|
202
|
+
}
|
|
203
|
+
systemMessageRouter.high('[Recovery] Dismissed — the unsaved session is still on disk; you will be asked again next time GoodVibes starts here.');
|
|
140
204
|
render();
|
|
141
|
-
return { handled: false, pendingPermission, recoveryPending:
|
|
205
|
+
return { handled: false, pendingPermission, recoveryPending: false };
|
|
142
206
|
}
|
|
143
207
|
|
|
144
208
|
return { handled: false, pendingPermission, recoveryPending };
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* architecture line ceiling; main() wires these with its live services.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
8
9
|
import type { ConversationMessageSnapshot } from '../core/conversation.ts';
|
|
9
|
-
import type { SessionSnapshot } from '@/runtime/index.ts';
|
|
10
|
+
import type { SessionReturnContextSummary, SessionSnapshot } from '@/runtime/index.ts';
|
|
11
|
+
import {
|
|
12
|
+
checkRecoveryFile,
|
|
13
|
+
deleteRecoveryFile as deleteLiveRecoveryFile,
|
|
14
|
+
getRecoveryFilePath,
|
|
15
|
+
loadRecoveryConversation as loadLiveRecoveryConversation,
|
|
16
|
+
} from '@/runtime/index.ts';
|
|
10
17
|
|
|
11
18
|
export interface PersistRecoveryDeps {
|
|
12
19
|
readonly sessionManager: {
|
|
@@ -41,6 +48,168 @@ export function createReopenRecoveryPanels(deps: ReopenPanelsDeps): (snapshot: S
|
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// W3 Finding 3: preserve the dismissed recovery snapshot.
|
|
53
|
+
//
|
|
54
|
+
// main.ts's 60s autosave (writeRecoveryFile) overwrites the single shared
|
|
55
|
+
// recovery.jsonl with the CURRENT session's live state within a minute — so
|
|
56
|
+
// the dismiss banner's promise ("still on disk; you will be asked again next
|
|
57
|
+
// time") went false silently well before the next launch. Fix: the moment
|
|
58
|
+
// the user dismisses (blocking-input.ts), copy recovery.jsonl aside to a
|
|
59
|
+
// preserved sibling file (recovery.jsonl.preserved) so it survives this
|
|
60
|
+
// session's later autosaves. Startup then checks BOTH the live file and the
|
|
61
|
+
// preserved sibling and offers whichever is newer (pickNewestRecoveryInfo),
|
|
62
|
+
// bounded to exactly one preserved file — a later dismiss replaces it, and
|
|
63
|
+
// that replacement is reported honestly (PreserveRecoveryResult.replacedPrevious)
|
|
64
|
+
// rather than silently dropping the older snapshot.
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export interface RecoveryFileDeps {
|
|
68
|
+
readonly homeDirectory: string;
|
|
69
|
+
readonly surfaceRoot?: string | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PreservedRecoveryInfo {
|
|
73
|
+
readonly title: string;
|
|
74
|
+
readonly timestamp: number;
|
|
75
|
+
readonly sessionId: string;
|
|
76
|
+
readonly returnContext?: SessionReturnContextSummary | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PreserveRecoveryResult {
|
|
80
|
+
/** True if a live recovery.jsonl existed and was copied to the preserved sibling. */
|
|
81
|
+
readonly preserved: boolean;
|
|
82
|
+
/** True if an existing preserved sibling (from an earlier dismiss) was overwritten. */
|
|
83
|
+
readonly replacedPrevious: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function preservedRecoveryPath(deps: RecoveryFileDeps): string {
|
|
87
|
+
return `${getRecoveryFilePath(deps.homeDirectory, deps.surfaceRoot)}.preserved`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readRecoveryMetaLine(path: string): { title?: string; timestamp?: number; sessionId?: string; returnContext?: SessionReturnContextSummary } | null {
|
|
91
|
+
try {
|
|
92
|
+
if (!existsSync(path)) return null;
|
|
93
|
+
const firstLine = readFileSync(path, 'utf-8').split('\n')[0];
|
|
94
|
+
if (!firstLine) return null;
|
|
95
|
+
return JSON.parse(firstLine) as { title?: string; timestamp?: number; sessionId?: string; returnContext?: SessionReturnContextSummary };
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Copy the live recovery.jsonl aside to the preserved sibling. Called once, at dismiss. */
|
|
102
|
+
export function createPreserveRecoveryFile(deps: RecoveryFileDeps): () => PreserveRecoveryResult {
|
|
103
|
+
return () => {
|
|
104
|
+
const live = getRecoveryFilePath(deps.homeDirectory, deps.surfaceRoot);
|
|
105
|
+
const preserved = preservedRecoveryPath(deps);
|
|
106
|
+
if (!existsSync(live)) return { preserved: false, replacedPrevious: false };
|
|
107
|
+
const replacedPrevious = existsSync(preserved);
|
|
108
|
+
try {
|
|
109
|
+
copyFileSync(live, preserved);
|
|
110
|
+
return { preserved: true, replacedPrevious };
|
|
111
|
+
} catch {
|
|
112
|
+
return { preserved: false, replacedPrevious: false };
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Startup check for the preserved sibling — mirrors the SDK's
|
|
119
|
+
* checkRecoveryFile meta read, but for the fixed preserved path. No
|
|
120
|
+
* mtime-vs-last-clean-save gate: the preserved file is inert (never
|
|
121
|
+
* autosaved-over) until explicitly replaced or cleared, so its mere
|
|
122
|
+
* existence is actionable.
|
|
123
|
+
*/
|
|
124
|
+
export function checkPreservedRecoveryFile(deps: RecoveryFileDeps): PreservedRecoveryInfo | null {
|
|
125
|
+
const meta = readRecoveryMetaLine(preservedRecoveryPath(deps));
|
|
126
|
+
if (!meta) return null;
|
|
127
|
+
return { title: meta.title ?? '', timestamp: meta.timestamp ?? 0, sessionId: meta.sessionId ?? '', returnContext: meta.returnContext };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Load the preserved sibling's full snapshot — same JSONL shape as recovery.jsonl since it's a byte-identical copy. */
|
|
131
|
+
export function loadPreservedRecoveryConversation(deps: RecoveryFileDeps): SessionSnapshot | null {
|
|
132
|
+
const path = preservedRecoveryPath(deps);
|
|
133
|
+
try {
|
|
134
|
+
if (!existsSync(path)) return null;
|
|
135
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
136
|
+
if (lines.length < 2) return { messages: [] };
|
|
137
|
+
const metaLine = JSON.parse(lines[0]!) as { title?: string; titleSource?: SessionSnapshot['titleSource']; returnContext?: SessionSnapshot['returnContext'] };
|
|
138
|
+
return {
|
|
139
|
+
title: metaLine.title,
|
|
140
|
+
titleSource: metaLine.titleSource,
|
|
141
|
+
returnContext: metaLine.returnContext,
|
|
142
|
+
messages: lines.slice(1).map((line) => {
|
|
143
|
+
const { type: _type, ...rest } = JSON.parse(line) as { type: string } & Record<string, unknown>;
|
|
144
|
+
return rest;
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Remove the preserved sibling — called after it has been restored or discarded. */
|
|
153
|
+
export function deletePreservedRecoveryFile(deps: RecoveryFileDeps): void {
|
|
154
|
+
try {
|
|
155
|
+
unlinkSync(preservedRecoveryPath(deps));
|
|
156
|
+
} catch {
|
|
157
|
+
// missing file is fine
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pick the newer of the live vs. preserved recovery info for the startup
|
|
163
|
+
* prompt, tagging which source it came from so the caller knows which
|
|
164
|
+
* load/delete pair to use. Null when neither exists.
|
|
165
|
+
*/
|
|
166
|
+
export function pickNewestRecoveryInfo<T extends { readonly timestamp: number }>(
|
|
167
|
+
live: T | null,
|
|
168
|
+
preserved: T | null,
|
|
169
|
+
): (T & { readonly source: 'live' | 'preserved' }) | null {
|
|
170
|
+
if (!live && !preserved) return null;
|
|
171
|
+
if (!live) return { ...(preserved as T), source: 'preserved' };
|
|
172
|
+
if (!preserved) return { ...live, source: 'live' };
|
|
173
|
+
return preserved.timestamp > live.timestamp
|
|
174
|
+
? { ...(preserved as T), source: 'preserved' }
|
|
175
|
+
: { ...live, source: 'live' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Startup entry point: resolve which recovery prompt (if any) main.ts should
|
|
180
|
+
* show — the live recovery.jsonl, or an earlier dismiss's preserved sibling,
|
|
181
|
+
* whichever is newer. Bundles checkRecoveryFile + checkPreservedRecoveryFile
|
|
182
|
+
* + pickNewestRecoveryInfo so main.ts's entrypoint stays a one-liner.
|
|
183
|
+
*/
|
|
184
|
+
export function resolveStartupRecoveryInfo(
|
|
185
|
+
deps: RecoveryFileDeps & { readonly workingDirectory: string },
|
|
186
|
+
): (PreservedRecoveryInfo & { readonly source: 'live' | 'preserved' }) | null {
|
|
187
|
+
const live = checkRecoveryFile({ workingDirectory: deps.workingDirectory, homeDirectory: deps.homeDirectory });
|
|
188
|
+
const preserved = checkPreservedRecoveryFile(deps);
|
|
189
|
+
return pickNewestRecoveryInfo(live, preserved);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface RecoveryFileOps {
|
|
193
|
+
readonly loadRecoveryConversation: () => SessionSnapshot | null;
|
|
194
|
+
readonly deleteRecoveryFile: () => void;
|
|
195
|
+
readonly preserveRecoveryFile?: () => PreserveRecoveryResult;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build the three source-aware recovery callbacks main.ts's stdin handler
|
|
200
|
+
* wires into handleBlockingShellInput. `getSource` is read fresh by
|
|
201
|
+
* load/delete on every call (main.ts's recoverySource can change between
|
|
202
|
+
* events); preserveRecoveryFile is only ever wired when the CURRENT prompt
|
|
203
|
+
* came from the live file (see createPreserveRecoveryFile's doc comment).
|
|
204
|
+
*/
|
|
205
|
+
export function createRecoveryFileOps(getSource: () => 'live' | 'preserved', deps: RecoveryFileDeps): RecoveryFileOps {
|
|
206
|
+
return {
|
|
207
|
+
loadRecoveryConversation: () => (getSource() === 'preserved' ? loadPreservedRecoveryConversation(deps) : loadLiveRecoveryConversation(deps)),
|
|
208
|
+
deleteRecoveryFile: () => (getSource() === 'preserved' ? deletePreservedRecoveryFile(deps) : deleteLiveRecoveryFile(deps)),
|
|
209
|
+
preserveRecoveryFile: getSource() === 'live' ? createPreserveRecoveryFile(deps) : undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
44
213
|
export interface ErrorAffordanceDeps {
|
|
45
214
|
/** True when the failover retry context is armed (a retry is actually possible). */
|
|
46
215
|
readonly retryArmed: boolean;
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -10,9 +10,13 @@ import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
|
10
10
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
11
11
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
|
+
import type { MemoryEmbeddingProviderRegistry } from '@pellux/goodvibes-sdk/platform/state';
|
|
13
14
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
14
|
-
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
15
|
+
import type { EmbeddingProviderPickerEntry, ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
16
|
+
import type { SelectionItem } from '../input/selection-modal.ts';
|
|
15
17
|
import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
|
|
18
|
+
import { setActiveThemeMode } from '../renderer/theme.ts';
|
|
19
|
+
import { THEME_MODE_CONFIG_KEY, coerceThemeModeSetting } from '../renderer/theme-mode-config.ts';
|
|
16
20
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
21
|
|
|
18
22
|
type WireShellUiOpenersOptions = {
|
|
@@ -28,6 +32,8 @@ type WireShellUiOpenersOptions = {
|
|
|
28
32
|
subscriptionManager: SubscriptionManager;
|
|
29
33
|
secretsManager?: Pick<SecretsManager, 'delete' | 'get' | 'set'>;
|
|
30
34
|
serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
|
|
35
|
+
/** B29: backs the model picker's 'embeddings' target and its own item-list mode. */
|
|
36
|
+
memoryEmbeddingRegistry: Pick<MemoryEmbeddingProviderRegistry, 'getDefaultProviderId' | 'setDefaultProvider' | 'status'>;
|
|
31
37
|
workingDirectory: string;
|
|
32
38
|
homeDirectory: string;
|
|
33
39
|
getConfiguredProviderIds: () => string[];
|
|
@@ -93,6 +99,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
93
99
|
subscriptionManager,
|
|
94
100
|
secretsManager,
|
|
95
101
|
serviceRegistry,
|
|
102
|
+
memoryEmbeddingRegistry,
|
|
96
103
|
workingDirectory,
|
|
97
104
|
homeDirectory,
|
|
98
105
|
getConfiguredProviderIds,
|
|
@@ -125,6 +132,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
125
132
|
if (target === 'helper') return String(configManager.get('helper.globalModel') || runtime.model);
|
|
126
133
|
if (target === 'tool') return String(configManager.get('tools.llmModel') || runtime.model);
|
|
127
134
|
if (target === 'tts') return String(configManager.get('tts.llmModel') || runtime.model);
|
|
135
|
+
if (target === 'embeddings') return memoryEmbeddingRegistry.getDefaultProviderId();
|
|
128
136
|
return runtime.model;
|
|
129
137
|
};
|
|
130
138
|
|
|
@@ -134,10 +142,29 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
134
142
|
if (target === 'helper') return String(configManager.get('helper.globalProvider') || runtime.provider);
|
|
135
143
|
if (target === 'tool') return String(configManager.get('tools.llmProvider') || runtime.provider);
|
|
136
144
|
if (target === 'tts') return String(configManager.get('tts.llmProvider') || runtime.provider);
|
|
145
|
+
if (target === 'embeddings') return memoryEmbeddingRegistry.getDefaultProviderId();
|
|
137
146
|
return runtime.provider;
|
|
138
147
|
};
|
|
139
148
|
|
|
140
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Fetch the embedding-provider status list once per picker-open and shape
|
|
151
|
+
* it into the picker's own tiny item type. Uses the registry's async
|
|
152
|
+
* `.status()` (not the sync `.list()`) specifically because `.list()` does
|
|
153
|
+
* not carry `configured` — and unconfigured providers must be shown
|
|
154
|
+
* honestly, not hidden.
|
|
155
|
+
*/
|
|
156
|
+
async function resolveEmbeddingProviderEntries(): Promise<EmbeddingProviderPickerEntry[]> {
|
|
157
|
+
const statuses = await memoryEmbeddingRegistry.status();
|
|
158
|
+
return statuses.map((status) => ({
|
|
159
|
+
id: status.id,
|
|
160
|
+
label: status.label,
|
|
161
|
+
dimensions: status.dimensions,
|
|
162
|
+
configured: status.configured,
|
|
163
|
+
...(status.detail ? { detail: status.detail } : {}),
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const buildModelPickerTargets = (embeddingEntries: EmbeddingProviderPickerEntry[]): ModelPickerTargetInfo[] => {
|
|
141
168
|
const mainProvider = getProviderIdFromModel(configManager.get('provider.model') || runtime.provider).trim();
|
|
142
169
|
const mainModel = String(configManager.get('provider.model') || runtime.model || '').trim();
|
|
143
170
|
const helperProvider = String(configManager.get('helper.globalProvider') ?? '').trim();
|
|
@@ -146,6 +173,11 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
146
173
|
const toolModel = String(configManager.get('tools.llmModel') ?? '').trim();
|
|
147
174
|
const ttsProvider = String(configManager.get('tts.llmProvider') ?? '').trim();
|
|
148
175
|
const ttsModel = String(configManager.get('tts.llmModel') ?? '').trim();
|
|
176
|
+
const embeddingProviderId = memoryEmbeddingRegistry.getDefaultProviderId();
|
|
177
|
+
const embeddingEntry = embeddingEntries.find((entry) => entry.id === embeddingProviderId);
|
|
178
|
+
const embeddingNote = embeddingEntry
|
|
179
|
+
? `${embeddingEntry.id} · ${embeddingEntry.dimensions}d${embeddingEntry.configured ? '' : ' · unconfigured'}`
|
|
180
|
+
: `${embeddingProviderId} · unregistered`;
|
|
149
181
|
|
|
150
182
|
return [
|
|
151
183
|
{
|
|
@@ -184,12 +216,28 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
184
216
|
enabled: true,
|
|
185
217
|
inherited: ttsProvider.length === 0 && ttsModel.length === 0,
|
|
186
218
|
},
|
|
219
|
+
{
|
|
220
|
+
target: 'embeddings',
|
|
221
|
+
label: 'Embeddings',
|
|
222
|
+
description: 'Embedding provider used for memory search and the code index. Not an LLM route — no model concept.',
|
|
223
|
+
provider: embeddingProviderId,
|
|
224
|
+
model: '',
|
|
225
|
+
enabled: true,
|
|
226
|
+
inherited: false,
|
|
227
|
+
configuredNote: embeddingNote,
|
|
228
|
+
},
|
|
187
229
|
];
|
|
188
230
|
};
|
|
189
231
|
|
|
190
232
|
commandContext.openModelPicker = () => {
|
|
191
233
|
void (async () => {
|
|
192
|
-
|
|
234
|
+
// D5: getSelectableModels() is catalog-driven and can list models whose
|
|
235
|
+
// `provider` id (e.g. 'google', sourced from the pricing catalog) was never
|
|
236
|
+
// handed to providerRegistry.register()/registerRuntimeProvider(). Selecting
|
|
237
|
+
// such a model fails hard at turn time with ProviderNotFoundError
|
|
238
|
+
// ("Provider 'X' is not registered."). Filter to runtime-registered
|
|
239
|
+
// providers only, so the picker never offers a model that cannot work.
|
|
240
|
+
const models = providerRegistry.getSelectableModels().filter((m) => providerRegistry.has(m.provider));
|
|
193
241
|
const configuredIds = new Set(getConfiguredProviderIds());
|
|
194
242
|
input.modelPicker.configuredProviders = configuredIds;
|
|
195
243
|
const providerIds = [...new Set(models.map((m) => m.provider))];
|
|
@@ -199,8 +247,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
199
247
|
input.modelPicker.pinnedIds = new Set(pinned);
|
|
200
248
|
});
|
|
201
249
|
void input.modelPicker.loadRecentModels().catch(() => {}); // best-effort: prefetch for UI, failure is non-visible
|
|
250
|
+
const embeddingEntries = await resolveEmbeddingProviderEntries();
|
|
251
|
+
input.modelPicker.embeddingProviders = embeddingEntries;
|
|
202
252
|
input.modalOpened('modelPicker');
|
|
203
|
-
input.modelPicker.setTargetInfos(buildModelPickerTargets());
|
|
253
|
+
input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
|
|
204
254
|
input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
|
|
205
255
|
render();
|
|
206
256
|
})().catch((error: unknown) => {
|
|
@@ -214,13 +264,21 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
214
264
|
|
|
215
265
|
commandContext.openProviderPicker = () => {
|
|
216
266
|
void (async () => {
|
|
217
|
-
|
|
267
|
+
// D5: listModels() surfaces every catalog provider id, not just the ones
|
|
268
|
+
// actually registered on this runtime (see openModelPicker above for the
|
|
269
|
+
// same class of bug). Intersect against providerRegistry.has() so the
|
|
270
|
+
// provider picker never offers a provider that will fail with
|
|
271
|
+
// "Provider not registered" once a model under it is selected.
|
|
272
|
+
const providers = [...new Set(providerRegistry.listModels().map((model) => model.provider))]
|
|
273
|
+
.filter((providerId) => providerRegistry.has(providerId));
|
|
218
274
|
const configuredIds = new Set(getConfiguredProviderIds());
|
|
219
275
|
input.modelPicker.configuredProviders = configuredIds;
|
|
220
276
|
const secretProviderIds = await resolveSecretProviderIds();
|
|
221
277
|
input.modelPicker.configuredViaMap = buildConfiguredViaMap(providers, configuredIds, subscriptionManager, secretProviderIds);
|
|
278
|
+
const embeddingEntries = await resolveEmbeddingProviderEntries();
|
|
279
|
+
input.modelPicker.embeddingProviders = embeddingEntries;
|
|
222
280
|
input.modalOpened('modelPicker');
|
|
223
|
-
input.modelPicker.setTargetInfos(buildModelPickerTargets());
|
|
281
|
+
input.modelPicker.setTargetInfos(buildModelPickerTargets(embeddingEntries));
|
|
224
282
|
input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
|
|
225
283
|
render();
|
|
226
284
|
})().catch((error: unknown) => {
|
|
@@ -229,6 +287,15 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
229
287
|
});
|
|
230
288
|
};
|
|
231
289
|
|
|
290
|
+
commandContext.completeEmbeddingProviderSelection = (providerId: string) => {
|
|
291
|
+
try {
|
|
292
|
+
memoryEmbeddingRegistry.setDefaultProvider(providerId);
|
|
293
|
+
} catch (error: unknown) {
|
|
294
|
+
commandContext.print?.(`Failed to set embedding provider: ${summarizeError(error)}`);
|
|
295
|
+
}
|
|
296
|
+
render();
|
|
297
|
+
};
|
|
298
|
+
|
|
232
299
|
commandContext.openSelection = (title, items, opts, callback) => {
|
|
233
300
|
input.openSelection(title, items, opts, callback);
|
|
234
301
|
};
|
|
@@ -272,15 +339,69 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
272
339
|
commandContext.openSettingsModal = (target?: string) => {
|
|
273
340
|
input.modalOpened('settings');
|
|
274
341
|
input.settingsModal.open(configManager, featureFlags, subscriptionManager, serviceRegistry, mcpRegistry, secretsManager, {
|
|
275
|
-
onSettingApplied: (change) =>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
342
|
+
onSettingApplied: (change) => {
|
|
343
|
+
// DEBT-2: forced dark/light applies immediately (rebuild palettes + full
|
|
344
|
+
// repaint); auto only re-probes at startup, so it takes effect next launch.
|
|
345
|
+
if (String(change.key) === THEME_MODE_CONFIG_KEY) {
|
|
346
|
+
const next = coerceThemeModeSetting(change.value);
|
|
347
|
+
if (next === 'dark' || next === 'light') {
|
|
348
|
+
setActiveThemeMode(next);
|
|
349
|
+
commandContext.requestFullRepaint?.();
|
|
350
|
+
return { message: `Theme mode: ${next} (applied now)` };
|
|
351
|
+
}
|
|
352
|
+
return { message: 'Theme mode: auto (probes terminal on next startup)' };
|
|
353
|
+
}
|
|
354
|
+
return syncServiceSettingToPlatform(
|
|
355
|
+
{ configManager, workingDirectory, homeDirectory },
|
|
356
|
+
change,
|
|
357
|
+
);
|
|
358
|
+
},
|
|
279
359
|
});
|
|
280
360
|
input.settingsModal.selectTarget(target);
|
|
281
361
|
render();
|
|
282
362
|
};
|
|
283
363
|
|
|
364
|
+
// W6.1 (the purge): open a MIGRATE-TO-MODAL surface by name. Fed from both
|
|
365
|
+
// ctx.openModal(name) (migrated command front-doors) and PanelManager's
|
|
366
|
+
// open()-time modal-redirect callback (a legacy panel id resolving to a modal
|
|
367
|
+
// name). Surfaces are registered in builtin-modals.ts; a name with no
|
|
368
|
+
// registered surface degrades honestly to a print rather than a blank modal.
|
|
369
|
+
// The stack name is the stable 'config' slot (one config modal at a time —
|
|
370
|
+
// opening another swaps the surface), so Esc close/return and the modal-stack
|
|
371
|
+
// machinery need only the single 'config' case (handler-ui-state.ts).
|
|
372
|
+
// Some panel-id redirects (and migrated front-doors) resolve to a NATIVE
|
|
373
|
+
// modal that is not a ConfigModalSurface — e.g. `sessions` -> `sessionPicker`,
|
|
374
|
+
// where 'sessionPicker' is the real session-picker modal opened by
|
|
375
|
+
// commandContext.openSessionPicker below, NOT a registered config surface.
|
|
376
|
+
// getModalSurface can never find these, so consult this small name->opener
|
|
377
|
+
// dispatch FIRST. Each entry reuses the same opener the front-door command
|
|
378
|
+
// uses (no duplicated open logic); resolved lazily so wiring order does not
|
|
379
|
+
// matter. Any name that is neither a native modal nor a registered surface
|
|
380
|
+
// still falls through to the honest "not available" print below.
|
|
381
|
+
const nativeModalOpeners: Record<string, () => void> = {
|
|
382
|
+
// openSessionPicker is wired below in this same function; call it lazily
|
|
383
|
+
// (optional-chained for the type, always present at invoke time).
|
|
384
|
+
sessionPicker: () => commandContext.openSessionPicker?.(),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
commandContext.openModal = (name: string) => {
|
|
388
|
+
const openNative = nativeModalOpeners[name];
|
|
389
|
+
if (openNative) {
|
|
390
|
+
openNative();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const surface = panelManager.getModalSurface(name);
|
|
394
|
+
if (!surface) {
|
|
395
|
+
commandContext.print(`'${name}' is not available yet in this build.`);
|
|
396
|
+
render();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
input.modalOpened('config');
|
|
400
|
+
input.configModal.open(surface, render);
|
|
401
|
+
render();
|
|
402
|
+
};
|
|
403
|
+
panelManager.setOpenModalCallback(commandContext.openModal);
|
|
404
|
+
|
|
284
405
|
commandContext.openMcpWorkspace = () => {
|
|
285
406
|
input.openMcpWorkspace(commandContext);
|
|
286
407
|
render();
|
|
@@ -296,25 +417,43 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
296
417
|
// Focus ownership lives in PanelManager (focusTarget); read it there rather
|
|
297
418
|
// than tracking a parallel input.panelFocused flag. Toggle semantics: if the
|
|
298
419
|
// workspace is visible AND already focused, hide it; otherwise reveal and
|
|
299
|
-
// focus it (opening
|
|
420
|
+
// focus it (opening a picker when nothing is open yet).
|
|
300
421
|
if (panelManager.isVisible() && panelManager.getFocusTarget() === 'panel') {
|
|
301
422
|
panelManager.hide();
|
|
302
423
|
panelManager.focusPrompt();
|
|
303
424
|
conversation.setSplashSuppressed(false);
|
|
304
425
|
conversation.rebuildHistory();
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
panelManager.
|
|
315
|
-
|
|
316
|
-
|
|
426
|
+
render();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (panelManager.getAllOpen().length === 0) {
|
|
430
|
+
// W6.1 (the purge): 'panel-list' (a browse-all-panels picker PANEL)
|
|
431
|
+
// was DELETE-disposition — a picker over a handful of panels is dead
|
|
432
|
+
// weight now. Its replacement is this selection MODAL, built from the
|
|
433
|
+
// live registry (PanelManager.getRegisteredTypes()) rather than a
|
|
434
|
+
// hardcoded list, so it can never list a retired/deleted id.
|
|
435
|
+
const items: SelectionItem[] = panelManager.getRegisteredTypes().map((entry) => ({
|
|
436
|
+
id: entry.id,
|
|
437
|
+
label: `${entry.icon} ${entry.name}`,
|
|
438
|
+
detail: entry.description,
|
|
439
|
+
category: entry.category,
|
|
440
|
+
primaryAction: 'select',
|
|
441
|
+
}));
|
|
442
|
+
commandContext.openSelection?.('Open Panel', items, { allowSearch: true }, (result) => {
|
|
443
|
+
if (!result) return;
|
|
444
|
+
panelManager.open(result.item.id);
|
|
445
|
+
panelManager.show();
|
|
446
|
+
panelManager.focusPanels();
|
|
447
|
+
conversation.setSplashSuppressed(true);
|
|
448
|
+
conversation.rebuildHistory();
|
|
449
|
+
render();
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
317
452
|
}
|
|
453
|
+
panelManager.show();
|
|
454
|
+
panelManager.focusPanels();
|
|
455
|
+
conversation.setSplashSuppressed(true);
|
|
456
|
+
conversation.rebuildHistory();
|
|
318
457
|
render();
|
|
319
458
|
};
|
|
320
459
|
|
|
@@ -329,10 +468,16 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
329
468
|
render();
|
|
330
469
|
};
|
|
331
470
|
|
|
332
|
-
commandContext.showPanel = (panelId, pane) => {
|
|
333
|
-
|
|
471
|
+
commandContext.showPanel = (panelId, pane, target, opts) => {
|
|
472
|
+
// DEBT-5: forward the deep-link target so a jumped-to panel lands on the
|
|
473
|
+
// right row (fleet --target); undefined for ordinary opens.
|
|
474
|
+
panelManager.open(panelId, pane, target);
|
|
334
475
|
panelManager.show();
|
|
335
|
-
|
|
476
|
+
// UX-C focus rule 1a: every registered caller of showPanel is a slash
|
|
477
|
+
// command (/panel open, /routes, /approval, /tasks, /ops-control, ...) —
|
|
478
|
+
// the command path leaves focus in the composer ("the user is
|
|
479
|
+
// mid-command-flow") unless the caller explicitly asks to grab it.
|
|
480
|
+
if (opts?.focus) panelManager.focusPanels();
|
|
336
481
|
conversation.setSplashSuppressed(true);
|
|
337
482
|
conversation.rebuildHistory();
|
|
338
483
|
render();
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared ms-duration formatters for the panel layer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two families are exported, each matching a distinct behavior cluster.
|
|
5
5
|
* DO NOT replace these with formatElapsed from utils/format-elapsed.ts —
|
|
6
6
|
* formatElapsed floors to integer seconds and lacks the null/'?ms' guards
|
|
7
7
|
* that the latency panels require.
|
|
8
|
+
*
|
|
9
|
+
* W6.1 (the purge): formatShortDuration (used only by incident-review-panel
|
|
10
|
+
* and eval-panel, both removed — RETIRE-INTO-FLEET and DELETE respectively)
|
|
11
|
+
* was removed as a genuinely orphaned export — no remaining caller anywhere
|
|
12
|
+
* in src/.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Format a latency value in milliseconds with sub-second precision.
|
|
12
17
|
*
|
|
13
|
-
* Used by:
|
|
18
|
+
* Used by: provider-health-panel
|
|
14
19
|
*
|
|
15
20
|
* ms <= 0 → 'n/a'
|
|
16
21
|
* ms >= 10000 → '12.3s' (one decimal)
|
|
@@ -27,7 +32,7 @@ export function formatLatencyMs(ms: number): string {
|
|
|
27
32
|
/**
|
|
28
33
|
* Format a task/agent duration with minute-rolling notation.
|
|
29
34
|
*
|
|
30
|
-
* Used by:
|
|
35
|
+
* Used by: agent-inspector-shared
|
|
31
36
|
*
|
|
32
37
|
* ms < 1000 → '500ms'
|
|
33
38
|
* ms < 60000 → '3.5s'
|
|
@@ -38,18 +43,3 @@ export function formatDuration(ms: number): string {
|
|
|
38
43
|
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
39
44
|
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Format a short eval/forensics duration; treats undefined as '?ms'.
|
|
44
|
-
*
|
|
45
|
-
* Used by: incident-review-panel, eval-panel
|
|
46
|
-
*
|
|
47
|
-
* undefined → '?ms'
|
|
48
|
-
* ms < 1000 → '500ms'
|
|
49
|
-
* else → '1.5s'
|
|
50
|
-
*/
|
|
51
|
-
export function formatShortDuration(ms: number | undefined): string {
|
|
52
|
-
if (ms === undefined) return '?ms';
|
|
53
|
-
if (ms < 1000) return `${ms}ms`;
|
|
54
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
55
|
-
}
|