@pellux/goodvibes-tui 1.19.6 → 1.20.1
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 +13 -0
- package/LICENSE +21 -0
- package/README.md +143 -1613
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -7
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/ensure-goodvibes-gitignore.ts +16 -4
- package/src/cli/entrypoint.ts +5 -1
- package/src/cli/launch-auto-update.ts +63 -4
- package/src/cli/management.ts +1 -1
- package/src/cli/tui-startup.ts +131 -15
- package/src/config/surface.ts +21 -0
- package/src/config/tui-extension-settings.ts +9 -1
- package/src/core/active-model-identity.ts +153 -0
- package/src/core/conversation-line-cache.ts +1 -1
- package/src/core/conversation-rendering.ts +70 -24
- package/src/core/conversation-tool-groups.ts +23 -10
- package/src/core/conversation-types.ts +9 -0
- package/src/core/conversation-user-receipts.ts +42 -0
- package/src/core/conversation.ts +20 -17
- package/src/core/rewind-turn-anchors.ts +12 -13
- package/src/core/session-recovery.ts +59 -2
- package/src/core/session-resume-core.ts +206 -0
- package/src/core/stream-event-wiring.ts +174 -14
- package/src/core/system-message-router.ts +22 -0
- package/src/core/transcript-journal.ts +32 -6
- package/src/core/turn-event-wiring.ts +55 -12
- package/src/input/command-args-hint.ts +11 -2
- package/src/input/command-registry.ts +19 -0
- package/src/input/commands/checkpoint-runtime.ts +3 -3
- package/src/input/commands/health-runtime.ts +1 -3
- package/src/input/commands/local-runtime.ts +1 -1
- package/src/input/commands/rewind-runtime.ts +132 -1
- package/src/input/commands/runtime-services.ts +11 -1
- package/src/input/commands/secret-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +9 -1
- package/src/input/commands/session-picker-filter.ts +28 -0
- package/src/input/commands/session-workflow.ts +62 -73
- package/src/input/commands/session.ts +36 -1
- package/src/input/commands/shell-core.ts +57 -5
- package/src/input/commands/update-runtime.ts +120 -4
- package/src/input/commands/websearch-runtime.ts +1 -1
- package/src/input/handler-content-actions.ts +63 -39
- package/src/input/handler-feed-routes.ts +17 -3
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +19 -23
- package/src/input/handler-modal-token-routes.ts +5 -0
- package/src/input/handler-types.ts +4 -1
- package/src/input/handler-ui-state.ts +6 -2
- package/src/input/handler.ts +19 -9
- package/src/input/search.ts +94 -2
- package/src/input/selection-modal-queue.ts +176 -0
- package/src/main.ts +66 -61
- package/src/panels/builtin/operations.ts +2 -1
- package/src/panels/provider-health-domains.ts +2 -2
- package/src/{cli → providers}/provider-classification.ts +12 -0
- package/src/renderer/block-actions-overlay.ts +59 -0
- package/src/renderer/block-actions.ts +1 -3
- package/src/renderer/context-status-hint.ts +73 -5
- package/src/renderer/conversation-layout.ts +16 -0
- package/src/renderer/conversation-overlays.ts +6 -0
- package/src/renderer/header-line.ts +146 -0
- package/src/renderer/help-overlay.ts +69 -53
- package/src/renderer/selection-modal-overlay.ts +66 -10
- package/src/renderer/shell-surface.ts +51 -0
- package/src/renderer/tool-result-expanded-lines.ts +48 -0
- package/src/renderer/ui-factory.ts +26 -79
- package/src/runtime/bootstrap-command-context.ts +5 -1
- package/src/runtime/bootstrap-command-parts.ts +5 -2
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +62 -44
- package/src/runtime/bootstrap-shell.ts +31 -16
- package/src/runtime/bootstrap.ts +13 -18
- package/src/runtime/code-index-services.ts +19 -0
- package/src/runtime/durability-services.ts +11 -7
- package/src/runtime/index.ts +12 -0
- package/src/runtime/process-lifecycle.ts +63 -2
- package/src/runtime/recovery-autosave.ts +48 -0
- package/src/runtime/recovery-decisions.ts +126 -0
- package/src/runtime/recovery-offer-wiring.ts +89 -0
- package/src/runtime/recovery-prompt.ts +362 -0
- package/src/runtime/resume-notice.ts +28 -93
- package/src/runtime/services.ts +14 -16
- package/src/runtime/session-liveness-marker.ts +120 -0
- package/src/runtime/session-pointer-surface.ts +22 -0
- package/src/runtime/session-resume-liveness-confirm.ts +84 -0
- package/src/runtime/session-storage-services.ts +41 -0
- package/src/runtime/workspace-checkpointing.ts +10 -2
- package/src/shell/recovery-input-helpers.ts +7 -3
- package/src/shell/retry-affordance.ts +123 -0
- package/src/shell/ui-openers.ts +9 -0
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selection-modal-queue.ts — Serializes overlapping InputHandler.openSelection() calls.
|
|
3
|
+
*
|
|
4
|
+
* openSelection (handler.ts) writes into a singleton SelectionModal plus a
|
|
5
|
+
* single callback slot. A second openSelection call while one is already
|
|
6
|
+
* showing used to silently overwrite both — the pre-empted caller's callback
|
|
7
|
+
* was never invoked, not even with null, so any caller that awaits it (see
|
|
8
|
+
* the `ask()` helper in runtime/recovery-prompt.ts, which wraps the callback
|
|
9
|
+
* in a Promise) hung forever. This is reachable in production: the
|
|
10
|
+
* `--continue` liveness-confirm modal (session-resume-liveness-confirm.ts)
|
|
11
|
+
* races the startup recovery offer, which is scheduled on a 0ms macrotask
|
|
12
|
+
* (scheduleRecoveryOffer in runtime/recovery-prompt.ts).
|
|
13
|
+
*
|
|
14
|
+
* Fix: queue overlapping requests FIFO and open each automatically once the
|
|
15
|
+
* previous one resolves, through the SAME callback the caller already
|
|
16
|
+
* supplied — openSelection's public signature never changes.
|
|
17
|
+
*
|
|
18
|
+
* Why the drain is deferred to a microtask instead of running inline inside
|
|
19
|
+
* the resolving callback: both resolution paths — the select/enter path in
|
|
20
|
+
* handler-modal-routes.ts's dispatchSelectionAction, and the Escape path in
|
|
21
|
+
* handler-modal-stack.ts's handleEscape/closeSelection — take a snapshot of
|
|
22
|
+
* the current selectionCallback before dispatch and write some form of that
|
|
23
|
+
* snapshot back into handler/context state AFTER invoking the callback (the
|
|
24
|
+
* select path re-reads via getSelectionCallback; the Escape path threads a
|
|
25
|
+
* local variable back out through handleEscapeForHandler in
|
|
26
|
+
* handler-interactions.ts). If this module opened the next queued request
|
|
27
|
+
* synchronously inside the resolving callback, those post-callback
|
|
28
|
+
* write-backs would immediately clobber the freshly-set callback for the
|
|
29
|
+
* newly-opened modal. Deferring past the end of the synchronous resolution
|
|
30
|
+
* (queueMicrotask) lets all of that settle first, then opens the next
|
|
31
|
+
* request cleanly — the same shape as the production race this fixes, which
|
|
32
|
+
* itself resolves via a deferred (macrotask) call.
|
|
33
|
+
*/
|
|
34
|
+
import type { SelectionItem, SelectionAction, SelectionResult, SelectionModal } from './selection-modal.ts';
|
|
35
|
+
|
|
36
|
+
export type SelectionModalCallback = (result: SelectionResult | null) => void;
|
|
37
|
+
|
|
38
|
+
export interface SelectionOpenOpts {
|
|
39
|
+
preSelectId?: string;
|
|
40
|
+
allowSearch?: boolean;
|
|
41
|
+
customActions?: Map<string, SelectionAction>;
|
|
42
|
+
primaryVerbLabel?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PendingSelectionRequest {
|
|
46
|
+
readonly title: string;
|
|
47
|
+
readonly items: SelectionItem[];
|
|
48
|
+
readonly opts: SelectionOpenOpts | undefined;
|
|
49
|
+
readonly callback: SelectionModalCallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum number of openSelection() calls held while one is already showing.
|
|
54
|
+
* In practice at most two calls ever race in production (the liveness-confirm
|
|
55
|
+
* modal vs. the recovery offer's 0ms macrotask), so 8 is generous headroom
|
|
56
|
+
* for any future chained-picker sequence without letting a misbehaving
|
|
57
|
+
* caller queue without bound. Once full, the overflowing call is resolved
|
|
58
|
+
* with `null` immediately — a bounded queue must still give every caller a
|
|
59
|
+
* definite answer, never silently drop one.
|
|
60
|
+
*/
|
|
61
|
+
export const MAX_QUEUED_SELECTIONS = 8;
|
|
62
|
+
|
|
63
|
+
/** Dependencies the queue needs from InputHandler, kept minimal and explicit. */
|
|
64
|
+
export interface SelectionQueueHost {
|
|
65
|
+
readonly selectionModal: Pick<SelectionModal, 'active' | 'open'>;
|
|
66
|
+
/** Assigns InputHandler.selectionCallback (the live resolution slot). */
|
|
67
|
+
setSelectionCallback(callback: SelectionModalCallback | null): void;
|
|
68
|
+
/** Mirrors the callback into the in-flight feed context, when a feed() call is in progress (no-op otherwise). */
|
|
69
|
+
syncFeedSelectionCallback(callback: SelectionModalCallback | null): void;
|
|
70
|
+
modalOpened(name: string): void;
|
|
71
|
+
requestRender(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Owns the FIFO of pending openSelection() requests for one InputHandler instance. */
|
|
75
|
+
export class SelectionModalQueue {
|
|
76
|
+
private readonly pending: PendingSelectionRequest[] = [];
|
|
77
|
+
|
|
78
|
+
constructor(private readonly host: SelectionQueueHost) {}
|
|
79
|
+
|
|
80
|
+
/** Number of requests currently queued (excludes whichever modal is actively showing, if any). */
|
|
81
|
+
public get size(): number {
|
|
82
|
+
return this.pending.length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Open `request` immediately if no selection modal is currently showing;
|
|
87
|
+
* otherwise queue it FIFO. If the queue is already at MAX_QUEUED_SELECTIONS,
|
|
88
|
+
* the request is resolved with `null` right away instead of being queued.
|
|
89
|
+
*/
|
|
90
|
+
public request(request: PendingSelectionRequest): void {
|
|
91
|
+
if (this.host.selectionModal.active) {
|
|
92
|
+
if (this.pending.length >= MAX_QUEUED_SELECTIONS) {
|
|
93
|
+
request.callback(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.pending.push(request);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.openNow(request);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve every still-queued request with `null`. Called when the handler
|
|
104
|
+
* is torn down (see the exitApp wrap in handler.ts's constructor) so no
|
|
105
|
+
* queued caller is ever left permanently pending.
|
|
106
|
+
*/
|
|
107
|
+
public clear(): void {
|
|
108
|
+
while (this.pending.length > 0) {
|
|
109
|
+
const next = this.pending.shift();
|
|
110
|
+
next?.callback(null);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private openNow(request: PendingSelectionRequest): void {
|
|
115
|
+
const wrapped: SelectionModalCallback = (result) => {
|
|
116
|
+
request.callback(result);
|
|
117
|
+
// Only a terminal resolution (select/cancel/escape) actually closes
|
|
118
|
+
// the modal — dispatchSelectionAction's toggle/increment/decrement
|
|
119
|
+
// branch invokes the callback but leaves the modal open for further
|
|
120
|
+
// interaction, so only drain the queue once the modal has gone
|
|
121
|
+
// inactive, never on every callback invocation.
|
|
122
|
+
if (!this.host.selectionModal.active) {
|
|
123
|
+
queueMicrotask(() => this.drain());
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
this.host.modalOpened('selection');
|
|
127
|
+
this.host.selectionModal.open(request.title, request.items, request.opts);
|
|
128
|
+
this.host.setSelectionCallback(wrapped);
|
|
129
|
+
this.host.syncFeedSelectionCallback(wrapped);
|
|
130
|
+
this.host.requestRender();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private drain(): void {
|
|
134
|
+
// Something else already opened a modal in the meantime (e.g. a
|
|
135
|
+
// synchronously chained openSelection call from inside the resolving
|
|
136
|
+
// callback) — leave the queue alone; that modal's own resolution will
|
|
137
|
+
// drain it in turn.
|
|
138
|
+
if (this.host.selectionModal.active) return;
|
|
139
|
+
const next = this.pending.shift();
|
|
140
|
+
if (!next) return;
|
|
141
|
+
this.openNow(next);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** InputHandler's actual field/method shape — satisfies this structurally, no import needed. */
|
|
146
|
+
export interface SelectionQueueOwner {
|
|
147
|
+
readonly selectionModal: Pick<SelectionModal, 'active' | 'open'>;
|
|
148
|
+
selectionCallback: SelectionModalCallback | null;
|
|
149
|
+
syncFeedSelectionCallback: ((callback: SelectionModalCallback | null) => void) | null;
|
|
150
|
+
modalOpened(name: string): void;
|
|
151
|
+
requestRender(): void;
|
|
152
|
+
exitApp: () => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds the queue for one InputHandler and wires teardown: wraps
|
|
157
|
+
* `owner.exitApp` (the one path guaranteed to run when the handler is torn
|
|
158
|
+
* down) so every still-queued selection request resolves with `null`
|
|
159
|
+
* instead of being left pending. Keeps this bookkeeping out of handler.ts,
|
|
160
|
+
* which only needs `this.selectionQueue = attachSelectionModalQueue(this);`.
|
|
161
|
+
*/
|
|
162
|
+
export function attachSelectionModalQueue(owner: SelectionQueueOwner): SelectionModalQueue {
|
|
163
|
+
const queue = new SelectionModalQueue({
|
|
164
|
+
selectionModal: owner.selectionModal,
|
|
165
|
+
setSelectionCallback: (callback) => { owner.selectionCallback = callback; },
|
|
166
|
+
syncFeedSelectionCallback: (callback) => { owner.syncFeedSelectionCallback?.(callback); },
|
|
167
|
+
modalOpened: (name) => owner.modalOpened(name),
|
|
168
|
+
requestRender: () => owner.requestRender(),
|
|
169
|
+
});
|
|
170
|
+
const suppliedExitApp = owner.exitApp;
|
|
171
|
+
owner.exitApp = () => {
|
|
172
|
+
queue.clear();
|
|
173
|
+
suppliedExitApp();
|
|
174
|
+
};
|
|
175
|
+
return queue;
|
|
176
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -24,7 +24,8 @@ import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvi
|
|
|
24
24
|
import { GitStatusProvider } from './renderer/git-status.ts';
|
|
25
25
|
import type { GitHeaderInfo } from './renderer/git-status.ts';
|
|
26
26
|
import { createShellLayout } from './renderer/layout-engine.ts';
|
|
27
|
-
import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
|
|
27
|
+
import { buildShellFooter, estimateShellFooterHeight, promptCursorOffset } from './renderer/shell-surface.ts';
|
|
28
|
+
import { createFailoverTurnState, resolveActiveModelDisplay } from './core/active-model-identity.ts';
|
|
28
29
|
import { computePromptContentWidth } from './renderer/prompt-content-width.ts';
|
|
29
30
|
import { buildConversationViewport } from './renderer/conversation-layout.ts';
|
|
30
31
|
import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
|
|
@@ -37,13 +38,16 @@ import { selfUpdateAtLaunch } from './cli/launch-auto-update.ts';
|
|
|
37
38
|
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './runtime/orchestrator-core-services.ts';
|
|
38
39
|
import { createSessionContinuityHintsBuilder } from './runtime/session-continuity-hints.ts';
|
|
39
40
|
import { resolveWebSurfaceUrl } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
|
|
40
|
-
import { readLastSessionPointer
|
|
41
|
+
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
42
|
+
import { startRecoveryAutosave } from './runtime/recovery-autosave.ts';
|
|
43
|
+
import { scheduleRecoveryOffer } from './runtime/recovery-prompt.ts';
|
|
44
|
+
import { buildRecoveryOfferWiring } from './runtime/recovery-offer-wiring.ts';
|
|
41
45
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
42
46
|
import { handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
|
|
47
|
+
import { createRetryAffordanceState, disarmRetryAffordance, retryAffordanceHint, wireRetryAffordanceOnError } from './shell/retry-affordance.ts';
|
|
43
48
|
import { wireShellUiOpeners } from './shell/ui-openers.ts';
|
|
44
49
|
import { deriveComposerState } from './core/composer-state.ts';
|
|
45
50
|
import { resolveFoldedBookmarkLine } from './core/bookmark-navigation.ts';
|
|
46
|
-
import { buildPersistedSessionContext, getReturnContextMode, maybeAssistReturnContextSummary } from '@/runtime/index.ts';
|
|
47
51
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
48
52
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
49
53
|
import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-startup.ts';
|
|
@@ -60,7 +64,7 @@ import { footerFleetCost } from './panels/fleet-read-model.ts';
|
|
|
60
64
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
61
65
|
import { wireStreamEventMetrics, createStreamMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
62
66
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
63
|
-
import {
|
|
67
|
+
import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
|
|
64
68
|
import { isEffectiveDangerMode } from './config/index.ts';
|
|
65
69
|
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
66
70
|
import { applyComposerCapture } from './input/composer-capture.ts';
|
|
@@ -77,6 +81,7 @@ import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
|
77
81
|
|
|
78
82
|
import { ALT_SCREEN_ENTER, ALT_SCREEN_EXIT, MOUSE_ENABLE, MOUSE_DISABLE, CURSOR_HIDE, CURSOR_SHOW, CLEAR_SCREEN, KEYBOARD_EXT_ENABLE, KEYBOARD_EXT_DISABLE, PASTE_ENABLE, PASTE_DISABLE, FOCUS_ENABLE, FOCUS_DISABLE } from './renderer/terminal-escapes.ts';
|
|
79
83
|
import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
|
|
84
|
+
import { VERSION } from './version.ts';
|
|
80
85
|
|
|
81
86
|
async function main() {
|
|
82
87
|
const stdout = process.stdout;
|
|
@@ -86,6 +91,12 @@ async function main() {
|
|
|
86
91
|
homeDirectory: homedir(),
|
|
87
92
|
}, 'goodvibes');
|
|
88
93
|
|
|
94
|
+
// Between binary start and the first frame, boot used to be silent (config
|
|
95
|
+
// load, trust manager, memory init, hooks). One honest pre-alt-screen line,
|
|
96
|
+
// placed after the help/version/completion early-exits above so those stay
|
|
97
|
+
// byte-clean.
|
|
98
|
+
stdout.write(`goodvibes v${VERSION} starting…\n`);
|
|
99
|
+
|
|
89
100
|
// Launch-time self-update, before any bootstrap or terminal mode change; on
|
|
90
101
|
// an installed update this restarts onto the swapped binary and never returns.
|
|
91
102
|
const launchUpdateLines = await selfUpdateAtLaunch({ configManager, stdout });
|
|
@@ -135,8 +146,11 @@ async function main() {
|
|
|
135
146
|
applyConfiguredHitlMode(configManager, modeManager);
|
|
136
147
|
applyTuiRuntimeConfigDefaults(configManager);
|
|
137
148
|
|
|
138
|
-
// Re-surface pre-TUI launch-update lines in-session (the alt screen wipes
|
|
139
|
-
|
|
149
|
+
// Re-surface pre-TUI launch-update lines in-session (the alt screen wipes
|
|
150
|
+
// stdout). Launch-time update mechanics are routine, not urgent — low
|
|
151
|
+
// priority, not high (a skipped/deferred check is not the kind of thing
|
|
152
|
+
// that should compete with real session alerts for attention).
|
|
153
|
+
for (const line of launchUpdateLines) systemMessageRouter.low(`[Update] ${line}`);
|
|
140
154
|
|
|
141
155
|
const panelManager = ctx.services.panelManager;
|
|
142
156
|
const buildSessionContinuityHints = createSessionContinuityHintsBuilder({ readModels: uiServices.readModels, panelManager });
|
|
@@ -165,6 +179,8 @@ async function main() {
|
|
|
165
179
|
let lastMaxScroll: number | null = null;
|
|
166
180
|
// Stream and tool-timer state; mutated by wireStreamEventMetrics handlers, read during render.
|
|
167
181
|
const streamMetrics: StreamMetrics = createStreamMetrics();
|
|
182
|
+
// Live failover record — written by the failover path, read every frame so the header and footer agree.
|
|
183
|
+
const failoverState = createFailoverTurnState();
|
|
168
184
|
|
|
169
185
|
const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
|
|
170
186
|
|
|
@@ -324,6 +340,9 @@ async function main() {
|
|
|
324
340
|
commandContext.submitInput = submitInput;
|
|
325
341
|
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
326
342
|
commandContext.stopSpokenOutput = () => spokenTurns.stop(); commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
343
|
+
// Read-only view of pending [TEXT: pN, M lines] fold markers, so /pastes
|
|
344
|
+
// can preview a folded paste's actual content before the user submits it.
|
|
345
|
+
commandContext.getPendingPastes = () => input.pasteRegistry;
|
|
327
346
|
commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
|
|
328
347
|
// Late-patched: bootstrap.ts populates uiServices.platform.externalServices AFTER commandContext is built.
|
|
329
348
|
commandContext.platform.externalServices = uiServices.platform.externalServices;
|
|
@@ -423,7 +442,7 @@ async function main() {
|
|
|
423
442
|
model: runtime.model,
|
|
424
443
|
provider: runtime.provider,
|
|
425
444
|
toolCount,
|
|
426
|
-
lastSessionId: readLastSessionPointer({
|
|
445
|
+
lastSessionId: readLastSessionPointer({ surface: ctx.services.surface }) ?? undefined,
|
|
427
446
|
};
|
|
428
447
|
|
|
429
448
|
const renderNow = () => {
|
|
@@ -438,10 +457,9 @@ async function main() {
|
|
|
438
457
|
const sessionSnapshot = uiServices.readModels.session.getSnapshot();
|
|
439
458
|
const agentSnapshot = uiServices.readModels.agents.getSnapshot();
|
|
440
459
|
|
|
441
|
-
const
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
);
|
|
460
|
+
const activeModel = resolveActiveModelDisplay({ serving: currentModel, configuredRegistryKey: configManager.get('provider.model') as string, configuredLabel: runtime.model, configuredProvider: runtime.provider, failover: failoverState.current() });
|
|
461
|
+
const headerLines = UIFactory.createHeader(width, activeModel.headerModel, activeModel.headerProvider, conversation.title || undefined, lastGitInfoRef.value, undefined, activeModel.divergenceNote);
|
|
462
|
+
const managerAgents = agentManager.list().filter((a) => a.status === 'running' || a.status === 'pending');
|
|
445
463
|
const runtimeAgents = agentSnapshot.active;
|
|
446
464
|
const runningAgentSummary = summarizeRunningAgents(managerAgents, runtimeAgents, ctx.services.wrfcController.listChains());
|
|
447
465
|
const runningAgentCount = runningAgentSummary.count;
|
|
@@ -454,38 +472,27 @@ async function main() {
|
|
|
454
472
|
commandMode: input.commandMode,
|
|
455
473
|
panelFocused: input.panelFocused,
|
|
456
474
|
pendingApproval: pendingPermission !== null,
|
|
457
|
-
hasAttachments: input.getImageAttachments().size > 0,
|
|
458
|
-
turnState: sessionSnapshot.turnState,
|
|
459
|
-
});
|
|
460
|
-
const maintenanceStatus = evaluateSessionMaintenance({
|
|
461
|
-
configManager,
|
|
462
|
-
currentTokens: orchestrator.lastInputTokens,
|
|
463
|
-
contextWindow,
|
|
464
|
-
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
475
|
+
hasAttachments: input.getImageAttachments().size > 0, turnState: sessionSnapshot.turnState,
|
|
465
476
|
});
|
|
466
|
-
const contextStatusHint =
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
usagePct: maintenanceStatus.usagePct,
|
|
477
|
+
const contextStatusHint = resolveContextStatusHint({
|
|
478
|
+
evaluate: (args) => evaluateSessionMaintenance({ configManager, ...args, sessionMemoryCount: ctx.services.sessionMemoryStore.list().length }),
|
|
479
|
+
currentTokens: orchestrator.lastInputTokens, contextWindow,
|
|
470
480
|
});
|
|
471
481
|
const footerLines = buildShellFooter({
|
|
472
482
|
width,
|
|
473
483
|
promptText: promptInfo.visibleLines.join('\n'),
|
|
474
484
|
promptLineCount: promptInfo.visibleLines.length,
|
|
475
|
-
promptCursorPos: promptInfo
|
|
476
|
-
? promptInfo.visibleLines
|
|
477
|
-
.slice(0, promptInfo.visibleCursorLine)
|
|
478
|
-
.reduce((sum: number, line: string) => sum + line.length + 1, 0) + promptInfo.visibleCursorCol
|
|
479
|
-
: undefined,
|
|
485
|
+
promptCursorPos: promptCursorOffset(promptInfo),
|
|
480
486
|
usage: { up: orchestrator.usage.input, down: orchestrator.usage.output, fleetCostUsd: footerFleetCost(() => ctx.services.processRegistry.query().nodes, runningAgentCount > 0) },
|
|
481
487
|
showExitNotice: input.showExitNotice,
|
|
482
488
|
lastCopyTime: input.lastCopyTime,
|
|
483
|
-
model:
|
|
489
|
+
model: activeModel.footerModel, modelNote: activeModel.divergenceNote,
|
|
484
490
|
toolCount: toolRegistry.list().length,
|
|
485
491
|
workingDir,
|
|
486
|
-
provider:
|
|
492
|
+
provider: activeModel.footerProvider,
|
|
487
493
|
contextWindow,
|
|
488
494
|
contextStatusHint,
|
|
495
|
+
retryHint: retryAffordanceHint(retryAffordance),
|
|
489
496
|
scriptableStatusLine: scriptableStatusline.current(),
|
|
490
497
|
// Compact footer posture on short terminals so the shell stays usable.
|
|
491
498
|
compact: height < 30,
|
|
@@ -680,9 +687,7 @@ async function main() {
|
|
|
680
687
|
providerRegistry,
|
|
681
688
|
systemMessageRouter,
|
|
682
689
|
hookDispatcher,
|
|
683
|
-
|
|
684
|
-
homeDirectory,
|
|
685
|
-
sessionManager: ctx.services.sessionManager,
|
|
690
|
+
surface: ctx.services.surface,
|
|
686
691
|
gitStatusProvider,
|
|
687
692
|
lastGitInfoRef,
|
|
688
693
|
buildSessionContinuityHints,
|
|
@@ -696,29 +701,31 @@ async function main() {
|
|
|
696
701
|
let retryCtx: { count: number; text: string; content?: ContentPart[]; opts?: Parameters<typeof orchestrator.handleUserInput>[2] } | null = null;
|
|
697
702
|
// One-key retry affordance, active right after a user-visible TURN_ERROR: 'r' re-submits on the
|
|
698
703
|
// current provider, 'm' opens the model picker, any other character clears it and routes normally.
|
|
699
|
-
|
|
700
|
-
|
|
704
|
+
// Surfaced as a transient FOOTER hint (see retryAffordanceHint below), not a transcript message.
|
|
705
|
+
// Time-bounded: onExpire repaints once the 60s disarm timer fires, so a stray keypress hours
|
|
706
|
+
// later can never trigger a real retry — see retry-affordance.ts.
|
|
707
|
+
const retryAffordance = createRetryAffordanceState({ onExpire: render });
|
|
708
|
+
const retryTurn = (notice?: string): void => {
|
|
701
709
|
if (!retryCtx) return;
|
|
702
710
|
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
703
711
|
// Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
|
|
712
|
+
// The rollback erases the failed turn's transcript — the failover notice included, which is how
|
|
713
|
+
// that notice used to vanish before anyone could read it. The caller hands it over instead, and
|
|
714
|
+
// it is posted here: after the rollback, above the prompt it explains.
|
|
704
715
|
conversation.removeMessagesAfter(count);
|
|
716
|
+
if (notice) systemMessageRouter.userReceipt(notice);
|
|
705
717
|
void refreshMemoryRecallSnapshot(ctx.services).then(() => orchestrator.handleUserInput(text, rContent, rOpts)).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
|
|
706
718
|
};
|
|
707
719
|
const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
|
|
708
720
|
events: uiServices.events, orchestrator, providerRegistry,
|
|
709
721
|
systemMessageRouter, render, metrics: streamMetrics,
|
|
710
722
|
providerOptimizer: ctx.services.providerOptimizer, costLookup: providerRegistry, retryTurn,
|
|
723
|
+
failoverState, getConfiguredRegistryKey: () => configManager.get('provider.model') as string | undefined,
|
|
711
724
|
isApprovalPending: () => pendingPermission !== null,
|
|
712
725
|
});
|
|
713
726
|
unsubs.push(...streamResult.unsubs);
|
|
714
727
|
// Activate one-key retry affordance when a user-visible error surfaces.
|
|
715
|
-
streamResult.onErrorSurfaced(
|
|
716
|
-
if (retryCtx) {
|
|
717
|
-
errorAffordanceActive = true;
|
|
718
|
-
systemMessageRouter.low(exhausted ? '[Retry] r retry same provider · m switch model' : '[Retry] r retry · m switch model');
|
|
719
|
-
render();
|
|
720
|
-
}
|
|
721
|
-
});
|
|
728
|
+
wireRetryAffordanceOnError(streamResult.onErrorSurfaced, retryAffordance, () => retryCtx !== null, render);
|
|
722
729
|
|
|
723
730
|
// Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a throw
|
|
724
731
|
// during setup or the initial render still restores the terminal; 'exit' is the final safety net.
|
|
@@ -736,7 +743,8 @@ async function main() {
|
|
|
736
743
|
// OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
|
|
737
744
|
const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
|
|
738
745
|
|
|
739
|
-
|
|
746
|
+
// continueRecovery lets --continue/bare --resume check the target session for a live crash snapshot newer than its store before resuming (see tui-startup.ts).
|
|
747
|
+
applyInitialTuiCliState({ cli, input, commandRegistry, commandContext, shellPaths: ctx.services.shellPaths, surface: ctx.services.surface, render, continueRecovery: { sessionManager: ctx.services.sessionManager, runtime, conversation, writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line) } });
|
|
740
748
|
|
|
741
749
|
stdin.on('data', (raw: string) => {
|
|
742
750
|
const data = themeProbe.filterInput(raw); if (data.length === 0) return;
|
|
@@ -749,9 +757,10 @@ async function main() {
|
|
|
749
757
|
return;
|
|
750
758
|
}
|
|
751
759
|
// One-key retry affordance: armed after a user-visible TURN_ERROR; any key other than r/m dismisses it and routes normally.
|
|
752
|
-
if (
|
|
753
|
-
|
|
760
|
+
if (retryAffordance.armed) {
|
|
761
|
+
disarmRetryAffordance(retryAffordance);
|
|
754
762
|
if (handleErrorAffordanceKey(data, { retryArmed: retryCtx !== null, retry: retryTurn, openModelPicker: () => commandContext.openModelPicker?.(), render })) return;
|
|
763
|
+
render(); // disarm must repaint immediately so the footer hint clears even when the key fell through
|
|
755
764
|
}
|
|
756
765
|
// One-key jump to a spawned CI fix-session: 'j' attaches, any other key dismisses and routes normally.
|
|
757
766
|
if (fixSessionAttachArmed !== null) {
|
|
@@ -768,28 +777,24 @@ async function main() {
|
|
|
768
777
|
|
|
769
778
|
// State restores happen ONLY when the user explicitly asks — a CLI flag
|
|
770
779
|
// (--continue/--resume/--fork), a slash command (/session resume,
|
|
771
|
-
// /checkpoints, /rewind), or the
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
// recovery snapshot is surfaced (never applied) by the boot resume
|
|
775
|
-
// notice — see announceResumeState in runtime/resume-notice.ts.
|
|
780
|
+
// /checkpoints, /rewind), or the startup recovery offer's modal. There is
|
|
781
|
+
// deliberately no unconditional auto-restore here: a bare launch never
|
|
782
|
+
// loads a saved conversation on its own (owner ruling).
|
|
776
783
|
|
|
777
784
|
// Initial render
|
|
778
785
|
conversation.rebuildHistory();
|
|
779
786
|
render();
|
|
780
787
|
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
runtime.sessionId,
|
|
788
|
-
conversation.title ?? '',
|
|
789
|
-
{ workingDirectory: workingDir, homeDirectory },
|
|
790
|
-
);
|
|
791
|
-
}, 60_000);
|
|
788
|
+
// Crash-recovery snapshot: asked, never assumed. Scheduled after the first
|
|
789
|
+
// frame so the question is drawn rather than posed at a blank terminal.
|
|
790
|
+
scheduleRecoveryOffer(buildRecoveryOfferWiring({
|
|
791
|
+
surface: ctx.services.surface, sessionManager: ctx.services.sessionManager, runtime, conversation, commandContext,
|
|
792
|
+
writeLastSessionPointer, receipt: (line) => systemMessageRouter.userReceipt(line), render,
|
|
793
|
+
}));
|
|
792
794
|
|
|
795
|
+
// Auto-save to recovery file every 60s + multi-instance liveness-marker
|
|
796
|
+
// refresh — see runtime/recovery-autosave.ts.
|
|
797
|
+
recoveryInterval = startRecoveryAutosave({ conversation, runtime, surface: ctx.services.surface, buildSessionContinuityHints });
|
|
793
798
|
}
|
|
794
799
|
|
|
795
800
|
main().catch(reportFatalStartupError);
|
|
@@ -9,6 +9,7 @@ import { LocalAuthPanel } from '../local-auth-panel.ts';
|
|
|
9
9
|
import { NotificationsPanel } from '../notifications-panel.ts';
|
|
10
10
|
import type { ResolvedBuiltinPanelDeps } from './shared.ts';
|
|
11
11
|
import { requireUiServices } from './shared.ts';
|
|
12
|
+
import { GOODVIBES_TUI_SURFACE_ROOT } from '../../config/surface.ts';
|
|
12
13
|
|
|
13
14
|
// the former single 'monitoring' category (33 panels pre-merge) is
|
|
14
15
|
// split into five operator domains, applied per-registration below. Kept in
|
|
@@ -113,7 +114,7 @@ export function registerOperationsPanels(manager: PanelManager, deps: ResolvedBu
|
|
|
113
114
|
// (AgentManager's bounded retention ring), empty once evicted (the
|
|
114
115
|
// panel degrades to the on-disk ledger fallback in that case).
|
|
115
116
|
getConversationSnapshot: (agentId: string) => ui.agents.agentManager.getConversationSnapshot(agentId),
|
|
116
|
-
resolveSessionLogPath: (agentId: string) => ui.environment.shellPaths.resolveProjectPath(
|
|
117
|
+
resolveSessionLogPath: (agentId: string) => ui.environment.shellPaths.resolveProjectPath(GOODVIBES_TUI_SURFACE_ROOT, 'sessions', `${agentId}.jsonl`),
|
|
117
118
|
// Queue a message for a live in-process agent/wrfc-subtask member.
|
|
118
119
|
steer: (id: string, text: string) => fleetReadModel.steer(id, text),
|
|
119
120
|
}, deps.configManager, fleetActs, fleetSpawn),
|
|
@@ -181,14 +181,14 @@ export function buildProviderHealthDomainSummaries(
|
|
|
181
181
|
summary: continuity.recoveryFilePresent
|
|
182
182
|
? `recovery file present for ${continuity.sessionId || '(unknown session)'}`
|
|
183
183
|
: continuity.lastSessionPointer ? `last session pointer ${continuity.lastSessionPointer}` : 'no last-session pointer',
|
|
184
|
-
next: continuity.recoveryFilePresent ? '/
|
|
184
|
+
next: continuity.recoveryFilePresent ? '/resume <id>' : '/session list',
|
|
185
185
|
details: [
|
|
186
186
|
continuity.returnContext ? `last activity: ${continuity.returnContext.activityLabel}` : '',
|
|
187
187
|
continuity.returnContext ? `resume posture: ${continuity.returnContext.statusLabel}` : '',
|
|
188
188
|
!continuity.lastSessionPointer ? 'no persisted last-session pointer recorded' : '',
|
|
189
189
|
].filter(Boolean),
|
|
190
190
|
nextSteps: continuity.recoveryFilePresent
|
|
191
|
-
? ['/session list', '/
|
|
191
|
+
? ['/session list', '/resume <id>', '/health continuity']
|
|
192
192
|
: ['/session list'],
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-classification.ts — how a provider is paid for: a raw API key, a
|
|
3
|
+
* cloud account, a stored subscription/OAuth session, a local runtime, or
|
|
4
|
+
* honestly unknown.
|
|
5
|
+
*
|
|
6
|
+
* Lives in the foundation `providers` layer (it imports nothing) because both
|
|
7
|
+
* the CLI management surfaces and the in-session failover notice need it: when
|
|
8
|
+
* automatic failover moves a turn onto a different backend, the notice names
|
|
9
|
+
* the billing class it moved from and to, so a switch from a metered API key
|
|
10
|
+
* onto the user's own subscription is something the user can see and object to
|
|
11
|
+
* rather than discover on an invoice.
|
|
12
|
+
*/
|
|
1
13
|
export type ProviderSetupClass =
|
|
2
14
|
| 'api-key'
|
|
3
15
|
| 'cloud-account'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderBlockActionsMenu — renders the BlockActionsMenu (src/renderer/block-actions.ts)
|
|
3
|
+
* as a docked overlay, so opening it via Enter-on-an-empty-composer actually
|
|
4
|
+
* shows something. Follows the established docked-overlay pattern (see
|
|
5
|
+
* selection-modal-overlay.ts / bookmark-modal.ts): ModalFactory sizes the box
|
|
6
|
+
* to its content, and every text section wraps rather than clips, so the
|
|
7
|
+
* block summary and action list are always shown in full.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Line } from '../types/grid.ts';
|
|
11
|
+
import { ModalFactory, type ModalSection } from './modal-factory.ts';
|
|
12
|
+
import type { BlockActionsMenu } from './block-actions.ts';
|
|
13
|
+
import { describeBlockForReceipt } from '../input/handler-content-actions.ts';
|
|
14
|
+
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
15
|
+
import { formatHints } from './hint-grammar.ts';
|
|
16
|
+
|
|
17
|
+
export function renderBlockActionsMenu(
|
|
18
|
+
menu: BlockActionsMenu,
|
|
19
|
+
width: number,
|
|
20
|
+
viewportHeight = 24,
|
|
21
|
+
): Line[] {
|
|
22
|
+
if (!menu.active || !menu.block) return [];
|
|
23
|
+
|
|
24
|
+
const summary = describeBlockForReceipt(menu.block);
|
|
25
|
+
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
26
|
+
margin: 4,
|
|
27
|
+
maxWidth: 64,
|
|
28
|
+
chromeRows: 5,
|
|
29
|
+
minContentRows: 3,
|
|
30
|
+
maxContentRows: Math.max(6, viewportHeight - 8),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sections: ModalSection[] = [
|
|
34
|
+
{ type: 'text', content: `Target: ${summary}`, style: { dim: true } },
|
|
35
|
+
{ type: 'separator' },
|
|
36
|
+
{
|
|
37
|
+
type: 'list',
|
|
38
|
+
items: menu.actions.map((action, i) => ({
|
|
39
|
+
label: `[${action.key}] ${action.label}`,
|
|
40
|
+
selected: i === menu.selectedIndex,
|
|
41
|
+
})),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
return ModalFactory.createModal(
|
|
46
|
+
{
|
|
47
|
+
title: 'Block Actions',
|
|
48
|
+
width: metrics.boxWidth,
|
|
49
|
+
margin: metrics.margin,
|
|
50
|
+
sections,
|
|
51
|
+
hints: [formatHints([
|
|
52
|
+
{ key: 'Up/Down', verb: 'Navigate' },
|
|
53
|
+
{ key: 'Enter', verb: 'Select' },
|
|
54
|
+
{ key: 'Esc', verb: 'Close' },
|
|
55
|
+
])],
|
|
56
|
+
},
|
|
57
|
+
width,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BlockMeta } from '../core/conversation';
|
|
2
2
|
|
|
3
|
-
export type BlockActionId = 'copy' | 'bookmark' | 'toggle' | 'apply'
|
|
3
|
+
export type BlockActionId = 'copy' | 'bookmark' | 'toggle' | 'apply';
|
|
4
4
|
|
|
5
5
|
export interface BlockAction {
|
|
6
6
|
id: BlockActionId;
|
|
@@ -13,7 +13,6 @@ const ALL_ACTIONS: BlockAction[] = [
|
|
|
13
13
|
{ id: 'bookmark', label: 'Bookmark', key: 'b' },
|
|
14
14
|
{ id: 'toggle', label: 'Collapse/Expand',key: 'Tab' },
|
|
15
15
|
{ id: 'apply', label: 'Apply diff', key: 'a' },
|
|
16
|
-
{ id: 'rerun', label: 'Re-run tool', key: 'r' },
|
|
17
16
|
];
|
|
18
17
|
|
|
19
18
|
/**
|
|
@@ -37,7 +36,6 @@ export class BlockActionsMenu {
|
|
|
37
36
|
this.block = block;
|
|
38
37
|
this.actions = ALL_ACTIONS.filter(a => {
|
|
39
38
|
if (a.id === 'apply') return block.type === 'diff';
|
|
40
|
-
if (a.id === 'rerun') return block.type === 'tool';
|
|
41
39
|
return true;
|
|
42
40
|
});
|
|
43
41
|
this.selectedIndex = 0;
|