@pellux/goodvibes-tui 1.19.5 → 1.20.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 +12 -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/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/tui-startup.ts +131 -15
- package/src/config/surface.ts +21 -0
- package/src/config/tui-extension-settings.ts +9 -1
- package/src/core/bookmark-navigation.ts +44 -0
- package/src/core/conversation-line-cache.ts +87 -19
- package/src/core/conversation-rendering.ts +186 -28
- package/src/core/conversation-tool-groups.ts +152 -0
- package/src/core/conversation-types.ts +30 -3
- package/src/core/conversation.ts +17 -0
- package/src/core/rewind-turn-anchors.ts +12 -13
- package/src/core/session-recovery.ts +62 -7
- package/src/core/session-resume-core.ts +206 -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 +19 -2
- 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 +55 -56
- package/src/panels/builtin/operations.ts +2 -1
- package/src/panels/provider-health-domains.ts +2 -2
- 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/help-overlay.ts +69 -53
- package/src/renderer/selection-modal-overlay.ts +66 -10
- package/src/renderer/shell-surface.ts +15 -0
- package/src/renderer/tool-result-expanded-lines.ts +48 -0
- package/src/renderer/ui-factory.ts +1 -1
- 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 -1
- package/src/runtime/process-lifecycle.ts +63 -2
- package/src/runtime/recovery-autosave.ts +48 -0
- package/src/runtime/recovery-offer-wiring.ts +89 -0
- package/src/runtime/recovery-prompt.ts +312 -0
- package/src/runtime/resume-notice.ts +26 -21
- 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 +17 -98
- 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
package/src/main.ts
CHANGED
|
@@ -37,12 +37,16 @@ import { selfUpdateAtLaunch } from './cli/launch-auto-update.ts';
|
|
|
37
37
|
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './runtime/orchestrator-core-services.ts';
|
|
38
38
|
import { createSessionContinuityHintsBuilder } from './runtime/session-continuity-hints.ts';
|
|
39
39
|
import { resolveWebSurfaceUrl } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
|
|
40
|
-
import { readLastSessionPointer
|
|
40
|
+
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
41
|
+
import { startRecoveryAutosave } from './runtime/recovery-autosave.ts';
|
|
42
|
+
import { scheduleRecoveryOffer } from './runtime/recovery-prompt.ts';
|
|
43
|
+
import { buildRecoveryOfferWiring } from './runtime/recovery-offer-wiring.ts';
|
|
41
44
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
42
|
-
import {
|
|
45
|
+
import { handleErrorAffordanceKey } from './shell/recovery-input-helpers.ts';
|
|
46
|
+
import { createRetryAffordanceState, disarmRetryAffordance, retryAffordanceHint, wireRetryAffordanceOnError } from './shell/retry-affordance.ts';
|
|
43
47
|
import { wireShellUiOpeners } from './shell/ui-openers.ts';
|
|
44
48
|
import { deriveComposerState } from './core/composer-state.ts';
|
|
45
|
-
import {
|
|
49
|
+
import { resolveFoldedBookmarkLine } from './core/bookmark-navigation.ts';
|
|
46
50
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
47
51
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
48
52
|
import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-startup.ts';
|
|
@@ -59,7 +63,7 @@ import { footerFleetCost } from './panels/fleet-read-model.ts';
|
|
|
59
63
|
import { formatUserFacingErrorLine } from './core/format-user-error.ts';
|
|
60
64
|
import { wireStreamEventMetrics, createStreamMetrics, type StreamMetrics, type WireStreamEventMetricsResult } from './core/stream-event-wiring.ts';
|
|
61
65
|
import { wireTurnEventHandlers } from './core/turn-event-wiring.ts';
|
|
62
|
-
import {
|
|
66
|
+
import { resolveContextStatusHint } from './renderer/context-status-hint.ts';
|
|
63
67
|
import { isEffectiveDangerMode } from './config/index.ts';
|
|
64
68
|
import { createScriptableStatusline } from './core/scriptable-statusline.ts';
|
|
65
69
|
import { applyComposerCapture } from './input/composer-capture.ts';
|
|
@@ -76,6 +80,7 @@ import { setPanelFrameRequester } from './panels/base-panel.ts';
|
|
|
76
80
|
|
|
77
81
|
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';
|
|
78
82
|
import { installBackgroundThemeProbe } from './renderer/terminal-bg-probe.ts';
|
|
83
|
+
import { VERSION } from './version.ts';
|
|
79
84
|
|
|
80
85
|
async function main() {
|
|
81
86
|
const stdout = process.stdout;
|
|
@@ -85,6 +90,12 @@ async function main() {
|
|
|
85
90
|
homeDirectory: homedir(),
|
|
86
91
|
}, 'goodvibes');
|
|
87
92
|
|
|
93
|
+
// Between binary start and the first frame, boot used to be silent (config
|
|
94
|
+
// load, trust manager, memory init, hooks). One honest pre-alt-screen line,
|
|
95
|
+
// placed after the help/version/completion early-exits above so those stay
|
|
96
|
+
// byte-clean.
|
|
97
|
+
stdout.write(`goodvibes v${VERSION} starting…\n`);
|
|
98
|
+
|
|
88
99
|
// Launch-time self-update, before any bootstrap or terminal mode change; on
|
|
89
100
|
// an installed update this restarts onto the swapped binary and never returns.
|
|
90
101
|
const launchUpdateLines = await selfUpdateAtLaunch({ configManager, stdout });
|
|
@@ -134,8 +145,11 @@ async function main() {
|
|
|
134
145
|
applyConfiguredHitlMode(configManager, modeManager);
|
|
135
146
|
applyTuiRuntimeConfigDefaults(configManager);
|
|
136
147
|
|
|
137
|
-
// Re-surface pre-TUI launch-update lines in-session (the alt screen wipes
|
|
138
|
-
|
|
148
|
+
// Re-surface pre-TUI launch-update lines in-session (the alt screen wipes
|
|
149
|
+
// stdout). Launch-time update mechanics are routine, not urgent — low
|
|
150
|
+
// priority, not high (a skipped/deferred check is not the kind of thing
|
|
151
|
+
// that should compete with real session alerts for attention).
|
|
152
|
+
for (const line of launchUpdateLines) systemMessageRouter.low(`[Update] ${line}`);
|
|
139
153
|
|
|
140
154
|
const panelManager = ctx.services.panelManager;
|
|
141
155
|
const buildSessionContinuityHints = createSessionContinuityHintsBuilder({ readModels: uiServices.readModels, panelManager });
|
|
@@ -301,13 +315,14 @@ async function main() {
|
|
|
301
315
|
const jumpToBookmark = (key: string) => {
|
|
302
316
|
conversation.getDisplayBlocks();
|
|
303
317
|
const block = conversation.getBlockRegistry().find((entry) => entry.collapseKey === key);
|
|
304
|
-
|
|
318
|
+
const line = block?.startLine ?? resolveFoldedBookmarkLine(conversation, key);
|
|
319
|
+
if (line === null) {
|
|
305
320
|
systemMessageRouter.high(`[Bookmark] Not found: ${key}`);
|
|
306
321
|
render();
|
|
307
322
|
return;
|
|
308
323
|
}
|
|
309
324
|
scrollLocked = false;
|
|
310
|
-
scrollTop = Math.max(0,
|
|
325
|
+
scrollTop = Math.max(0, line);
|
|
311
326
|
render();
|
|
312
327
|
};
|
|
313
328
|
|
|
@@ -322,6 +337,9 @@ async function main() {
|
|
|
322
337
|
commandContext.submitInput = submitInput;
|
|
323
338
|
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
324
339
|
commandContext.stopSpokenOutput = () => spokenTurns.stop(); commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
340
|
+
// Read-only view of pending [TEXT: pN, M lines] fold markers, so /pastes
|
|
341
|
+
// can preview a folded paste's actual content before the user submits it.
|
|
342
|
+
commandContext.getPendingPastes = () => input.pasteRegistry;
|
|
325
343
|
commandContext.executeCommand = (name, args) => commandRegistry.execute(name, args, commandContext);
|
|
326
344
|
// Late-patched: bootstrap.ts populates uiServices.platform.externalServices AFTER commandContext is built.
|
|
327
345
|
commandContext.platform.externalServices = uiServices.platform.externalServices;
|
|
@@ -421,7 +439,7 @@ async function main() {
|
|
|
421
439
|
model: runtime.model,
|
|
422
440
|
provider: runtime.provider,
|
|
423
441
|
toolCount,
|
|
424
|
-
lastSessionId: readLastSessionPointer({
|
|
442
|
+
lastSessionId: readLastSessionPointer({ surface: ctx.services.surface }) ?? undefined,
|
|
425
443
|
};
|
|
426
444
|
|
|
427
445
|
const renderNow = () => {
|
|
@@ -455,16 +473,10 @@ async function main() {
|
|
|
455
473
|
hasAttachments: input.getImageAttachments().size > 0,
|
|
456
474
|
turnState: sessionSnapshot.turnState,
|
|
457
475
|
});
|
|
458
|
-
const
|
|
459
|
-
configManager,
|
|
476
|
+
const contextStatusHint = resolveContextStatusHint({
|
|
477
|
+
evaluate: (args) => evaluateSessionMaintenance({ configManager, ...args, sessionMemoryCount: ctx.services.sessionMemoryStore.list().length }),
|
|
460
478
|
currentTokens: orchestrator.lastInputTokens,
|
|
461
479
|
contextWindow,
|
|
462
|
-
sessionMemoryCount: ctx.services.sessionMemoryStore.list().length,
|
|
463
|
-
});
|
|
464
|
-
const contextStatusHint = buildContextStatusHint({
|
|
465
|
-
level: maintenanceStatus.level,
|
|
466
|
-
autoCompactEnabled: maintenanceStatus.autoCompactEnabled,
|
|
467
|
-
usagePct: maintenanceStatus.usagePct,
|
|
468
480
|
});
|
|
469
481
|
const footerLines = buildShellFooter({
|
|
470
482
|
width,
|
|
@@ -484,6 +496,7 @@ async function main() {
|
|
|
484
496
|
provider: runtime.provider,
|
|
485
497
|
contextWindow,
|
|
486
498
|
contextStatusHint,
|
|
499
|
+
retryHint: retryAffordanceHint(retryAffordance),
|
|
487
500
|
scriptableStatusLine: scriptableStatusline.current(),
|
|
488
501
|
// Compact footer posture on short terminals so the shell stays usable.
|
|
489
502
|
compact: height < 30,
|
|
@@ -678,9 +691,7 @@ async function main() {
|
|
|
678
691
|
providerRegistry,
|
|
679
692
|
systemMessageRouter,
|
|
680
693
|
hookDispatcher,
|
|
681
|
-
|
|
682
|
-
homeDirectory,
|
|
683
|
-
sessionManager: ctx.services.sessionManager,
|
|
694
|
+
surface: ctx.services.surface,
|
|
684
695
|
gitStatusProvider,
|
|
685
696
|
lastGitInfoRef,
|
|
686
697
|
buildSessionContinuityHints,
|
|
@@ -694,7 +705,10 @@ async function main() {
|
|
|
694
705
|
let retryCtx: { count: number; text: string; content?: ContentPart[]; opts?: Parameters<typeof orchestrator.handleUserInput>[2] } | null = null;
|
|
695
706
|
// One-key retry affordance, active right after a user-visible TURN_ERROR: 'r' re-submits on the
|
|
696
707
|
// current provider, 'm' opens the model picker, any other character clears it and routes normally.
|
|
697
|
-
|
|
708
|
+
// Surfaced as a transient FOOTER hint (see retryAffordanceHint below), not a transcript message.
|
|
709
|
+
// Time-bounded: onExpire repaints once the 60s disarm timer fires, so a stray keypress hours
|
|
710
|
+
// later can never trigger a real retry — see retry-affordance.ts.
|
|
711
|
+
const retryAffordance = createRetryAffordanceState({ onExpire: render });
|
|
698
712
|
const retryTurn = (): void => {
|
|
699
713
|
if (!retryCtx) return;
|
|
700
714
|
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
@@ -710,13 +724,7 @@ async function main() {
|
|
|
710
724
|
});
|
|
711
725
|
unsubs.push(...streamResult.unsubs);
|
|
712
726
|
// Activate one-key retry affordance when a user-visible error surfaces.
|
|
713
|
-
streamResult.onErrorSurfaced(
|
|
714
|
-
if (retryCtx) {
|
|
715
|
-
errorAffordanceActive = true;
|
|
716
|
-
systemMessageRouter.low(exhausted ? '[Retry] r retry same provider · m switch model' : '[Retry] r retry · m switch model');
|
|
717
|
-
render();
|
|
718
|
-
}
|
|
719
|
-
});
|
|
727
|
+
wireRetryAffordanceOnError(streamResult.onErrorSurfaced, retryAffordance, () => retryCtx !== null, render);
|
|
720
728
|
|
|
721
729
|
// Register terminal-restoring crash/termination handlers BEFORE entering raw mode so a throw
|
|
722
730
|
// during setup or the initial render still restores the terminal; 'exit' is the final safety net.
|
|
@@ -734,7 +742,8 @@ async function main() {
|
|
|
734
742
|
// OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
|
|
735
743
|
const themeProbe = installBackgroundThemeProbe({ configManager, isTTY: Boolean(stdout.isTTY), env: process.env, writeQuery: (b) => allowTerminalWrite(() => stdout.write(b)), requestRepaint: () => { compositor.resetDiff(); render(); } });
|
|
736
744
|
|
|
737
|
-
|
|
745
|
+
// continueRecovery lets --continue/bare --resume check the target session for a live crash snapshot newer than its store before resuming (see tui-startup.ts).
|
|
746
|
+
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.high(line) } });
|
|
738
747
|
|
|
739
748
|
stdin.on('data', (raw: string) => {
|
|
740
749
|
const data = themeProbe.filterInput(raw); if (data.length === 0) return;
|
|
@@ -747,9 +756,10 @@ async function main() {
|
|
|
747
756
|
return;
|
|
748
757
|
}
|
|
749
758
|
// One-key retry affordance: armed after a user-visible TURN_ERROR; any key other than r/m dismisses it and routes normally.
|
|
750
|
-
if (
|
|
751
|
-
|
|
759
|
+
if (retryAffordance.armed) {
|
|
760
|
+
disarmRetryAffordance(retryAffordance);
|
|
752
761
|
if (handleErrorAffordanceKey(data, { retryArmed: retryCtx !== null, retry: retryTurn, openModelPicker: () => commandContext.openModelPicker?.(), render })) return;
|
|
762
|
+
render(); // disarm must repaint immediately so the footer hint clears even when the key fell through
|
|
753
763
|
}
|
|
754
764
|
// One-key jump to a spawned CI fix-session: 'j' attaches, any other key dismisses and routes normally.
|
|
755
765
|
if (fixSessionAttachArmed !== null) {
|
|
@@ -764,37 +774,26 @@ async function main() {
|
|
|
764
774
|
process.on('unhandledRejection', unhandledRejectionHandler);
|
|
765
775
|
stdout.on('resize', resizeHandler);
|
|
766
776
|
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
// before the initial render so the restored content shows immediately.
|
|
773
|
-
autoRestoreRecoverySession({
|
|
774
|
-
workingDirectory: workingDir,
|
|
775
|
-
homeDirectory,
|
|
776
|
-
conversation,
|
|
777
|
-
persistSnapshot: createPersistRecoverySnapshot({ sessionManager: ctx.services.sessionManager, runtime, conversation }),
|
|
778
|
-
reopenPanels: createReopenRecoveryPanels({ panelManager, render }),
|
|
779
|
-
systemMessageRouter,
|
|
780
|
-
});
|
|
777
|
+
// State restores happen ONLY when the user explicitly asks — a CLI flag
|
|
778
|
+
// (--continue/--resume/--fork), a slash command (/session resume,
|
|
779
|
+
// /checkpoints, /rewind), or the startup recovery offer's modal. There is
|
|
780
|
+
// deliberately no unconditional auto-restore here: a bare launch never
|
|
781
|
+
// loads a saved conversation on its own (owner ruling).
|
|
781
782
|
|
|
782
783
|
// Initial render
|
|
783
784
|
conversation.rebuildHistory();
|
|
784
785
|
render();
|
|
785
786
|
|
|
786
|
-
//
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
runtime.sessionId,
|
|
793
|
-
conversation.title ?? '',
|
|
794
|
-
{ workingDirectory: workingDir, homeDirectory },
|
|
795
|
-
);
|
|
796
|
-
}, 60_000);
|
|
787
|
+
// Crash-recovery snapshot: asked, never assumed. Scheduled after the first
|
|
788
|
+
// frame so the question is drawn rather than posed at a blank terminal.
|
|
789
|
+
scheduleRecoveryOffer(buildRecoveryOfferWiring({
|
|
790
|
+
surface: ctx.services.surface, sessionManager: ctx.services.sessionManager, runtime, conversation, commandContext,
|
|
791
|
+
writeLastSessionPointer, receipt: (line) => systemMessageRouter.high(line), render,
|
|
792
|
+
}));
|
|
797
793
|
|
|
794
|
+
// Auto-save to recovery file every 60s + multi-instance liveness-marker
|
|
795
|
+
// refresh — see runtime/recovery-autosave.ts.
|
|
796
|
+
recoveryInterval = startRecoveryAutosave({ conversation, runtime, surface: ctx.services.surface, buildSessionContinuityHints });
|
|
798
797
|
}
|
|
799
798
|
|
|
800
799
|
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
|
|
|
@@ -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;
|
|
@@ -12,34 +12,77 @@
|
|
|
12
12
|
* - compacting → shows in-progress text so the user knows something is running
|
|
13
13
|
* - watch → no hint (not yet actionable)
|
|
14
14
|
* - stable/unknown → no hint
|
|
15
|
+
*
|
|
16
|
+
* Evidence policy (why the guards below exist)
|
|
17
|
+
* ────────────────────────────────────────────
|
|
18
|
+
* A fresh boot printed "Context high (0% used) — auto-compact will run before
|
|
19
|
+
* the next turn." before a single token had been counted. The evaluator was
|
|
20
|
+
* not at fault: `evaluateSessionMaintenance` reaches 'suggest-compact' when
|
|
21
|
+
* `remainingTokens <= 15_000`, and at boot the resolved window is still the
|
|
22
|
+
* SDK's DEFAULT_CONTEXT_WINDOW fallback (8,192 — the "8.2k" the footer meter
|
|
23
|
+
* shows before the real window arrives), so 8,192 free tokens tripped that
|
|
24
|
+
* rule instantly. Arithmetically consistent; factually false. No context had
|
|
25
|
+
* been consumed and no auto-compact was imminent.
|
|
26
|
+
*
|
|
27
|
+
* So a pressure claim here has to be backed by evidence, not merely by a
|
|
28
|
+
* level:
|
|
29
|
+
* 1. Usage has actually been measured. At zero tokens consumed there is no
|
|
30
|
+
* pressure to report, whatever the window says.
|
|
31
|
+
* 2. The window is one the provider vouched for, not the fallback constant.
|
|
32
|
+
* A model whose real window happens to equal the fallback is
|
|
33
|
+
* indistinguishable from "not resolved yet", and in that ambiguity
|
|
34
|
+
* staying quiet is the honest choice — the Tokens panel and /context
|
|
35
|
+
* still show the real numbers either way.
|
|
36
|
+
* Neither guard touches 'compacting', which reports work that is provably
|
|
37
|
+
* running rather than a prediction about headroom.
|
|
15
38
|
*/
|
|
16
39
|
|
|
40
|
+
import { DEFAULT_CONTEXT_WINDOW } from '@pellux/goodvibes-sdk/platform/providers';
|
|
17
41
|
import type { PanelSessionMaintenanceLevel } from '../panels/session-maintenance.ts';
|
|
18
42
|
|
|
19
|
-
|
|
43
|
+
interface ContextStatusHintOptions {
|
|
20
44
|
/** Maintenance level from evaluateSessionMaintenance. */
|
|
21
45
|
readonly level: PanelSessionMaintenanceLevel;
|
|
22
46
|
/** Whether auto-compaction is active (threshold > 0 in config). */
|
|
23
47
|
readonly autoCompactEnabled: boolean;
|
|
24
48
|
/** Current usage percent 0–100. */
|
|
25
49
|
readonly usagePct: number;
|
|
50
|
+
/** Tokens actually counted so far this session (orchestrator.lastInputTokens). */
|
|
51
|
+
readonly currentTokens: number;
|
|
52
|
+
/** The resolved context window the usage percent was computed against. */
|
|
53
|
+
readonly contextWindow: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether the numbers backing a pressure claim are real yet: usage has been
|
|
58
|
+
* counted, and the window is not the provider fallback the app boots with.
|
|
59
|
+
*/
|
|
60
|
+
function hasRealContextNumbers(
|
|
61
|
+
options: Pick<ContextStatusHintOptions, 'currentTokens' | 'contextWindow' | 'usagePct'>,
|
|
62
|
+
): boolean {
|
|
63
|
+
if (options.currentTokens <= 0 || options.usagePct <= 0) return false;
|
|
64
|
+
if (options.contextWindow <= 0 || options.contextWindow === DEFAULT_CONTEXT_WINDOW) return false;
|
|
65
|
+
return true;
|
|
26
66
|
}
|
|
27
67
|
|
|
28
68
|
/**
|
|
29
69
|
* Build the passive status-line hint text for context pressure.
|
|
30
70
|
*
|
|
31
|
-
* Returns null when no hint is warranted (stable / watch / unknown)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
71
|
+
* Returns null when no hint is warranted (stable / watch / unknown), and also
|
|
72
|
+
* when a pressure level is not yet backed by real numbers — see the evidence
|
|
73
|
+
* policy above. The caller renders this as a dim informational line — no
|
|
74
|
+
* prompts, no blocking, no confirmation required.
|
|
34
75
|
*/
|
|
35
|
-
|
|
76
|
+
function buildContextStatusHint(options: ContextStatusHintOptions): string | null {
|
|
36
77
|
const { level, autoCompactEnabled, usagePct } = options;
|
|
37
78
|
|
|
38
79
|
switch (level) {
|
|
39
80
|
case 'needs-repair':
|
|
81
|
+
if (!hasRealContextNumbers(options)) return null;
|
|
40
82
|
return ` Context pressure critical (${usagePct}% used) — compaction needs attention. Run /compact or /health review.`;
|
|
41
83
|
|
|
42
84
|
case 'suggest-compact':
|
|
85
|
+
if (!hasRealContextNumbers(options)) return null;
|
|
43
86
|
if (autoCompactEnabled) {
|
|
44
87
|
return ` Context high (${usagePct}% used) — auto-compact will run before the next turn.`;
|
|
45
88
|
}
|
|
@@ -52,3 +95,28 @@ export function buildContextStatusHint(options: ContextStatusHintOptions): strin
|
|
|
52
95
|
return null;
|
|
53
96
|
}
|
|
54
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Evaluate session maintenance and turn it into the footer hint in one call.
|
|
101
|
+
* The ONE public seam of this module, deliberately: the guards above need the
|
|
102
|
+
* exact `currentTokens` / `contextWindow` the evaluator was handed, and a
|
|
103
|
+
* caller free to pass different values to each half is how they drift apart.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveContextStatusHint(input: {
|
|
106
|
+
readonly evaluate: (args: { readonly currentTokens: number; readonly contextWindow: number }) => {
|
|
107
|
+
readonly level: PanelSessionMaintenanceLevel;
|
|
108
|
+
readonly autoCompactEnabled: boolean;
|
|
109
|
+
readonly usagePct: number;
|
|
110
|
+
};
|
|
111
|
+
readonly currentTokens: number;
|
|
112
|
+
readonly contextWindow: number;
|
|
113
|
+
}): string | null {
|
|
114
|
+
const status = input.evaluate({ currentTokens: input.currentTokens, contextWindow: input.contextWindow });
|
|
115
|
+
return buildContextStatusHint({
|
|
116
|
+
level: status.level,
|
|
117
|
+
autoCompactEnabled: status.autoCompactEnabled,
|
|
118
|
+
usagePct: status.usagePct,
|
|
119
|
+
currentTokens: input.currentTokens,
|
|
120
|
+
contextWindow: input.contextWindow,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -38,6 +38,22 @@ export function buildConversationViewport(
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Absolute history-line index of the bottom-most visible row of the
|
|
43
|
+
* conversation viewport. The buffer bottom-aligns content shorter than the
|
|
44
|
+
* viewport (InfiniteBuffer.getSnapshot pads with blank lines at the TOP, not
|
|
45
|
+
* the bottom — see history.ts), so the naive `scrollTop + viewportHeight - 1`
|
|
46
|
+
* overshoots past the real last line whenever the whole transcript already
|
|
47
|
+
* fits on screen. Clamping to `lineCount - 1` handles both cases with one
|
|
48
|
+
* formula: when scrollLocked at the true bottom (the common case), this is
|
|
49
|
+
* exactly the last rendered content line — the block the user is actually
|
|
50
|
+
* looking at, not whatever sits at the TOP of the viewport (scrollTop alone).
|
|
51
|
+
*/
|
|
52
|
+
export function getViewportBottomLine(scrollTop: number, viewportHeight: number, lineCount: number): number {
|
|
53
|
+
if (lineCount <= 0) return 0;
|
|
54
|
+
return Math.max(0, Math.min(scrollTop + viewportHeight - 1, lineCount - 1));
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
export function overlayViewportBottom(
|
|
42
58
|
viewport: readonly Line[],
|
|
43
59
|
overlay: readonly Line[],
|
|
@@ -16,6 +16,7 @@ import { renderSessionPickerModal } from './session-picker-modal.ts';
|
|
|
16
16
|
import { renderProfilePickerModal } from './profile-picker-modal.ts';
|
|
17
17
|
import { renderBookmarkModal } from './bookmark-modal.ts';
|
|
18
18
|
import { renderHelpOverlay, renderShortcutsOverlay } from './help-overlay.ts';
|
|
19
|
+
import { renderBlockActionsMenu } from './block-actions-overlay.ts';
|
|
19
20
|
import { renderAutocompleteOverlay } from './autocomplete-overlay.ts';
|
|
20
21
|
import { renderOnboardingWizard } from './onboarding/onboarding-wizard.ts';
|
|
21
22
|
import { overlayViewportBottom, replaceViewportWithOverlay } from './conversation-layout.ts';
|
|
@@ -118,6 +119,11 @@ export function applyConversationOverlays(
|
|
|
118
119
|
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
if (!fullscreenClaimed && input.blockActionsMenu.active) {
|
|
123
|
+
const lines = renderBlockActionsMenu(input.blockActionsMenu, conversationWidth, viewportHeight);
|
|
124
|
+
next = overlayViewportBottom(next, lines, conversationWidth, viewportHeight, bottomDockInset);
|
|
125
|
+
}
|
|
126
|
+
|
|
121
127
|
if (input.helpOverlayActive) {
|
|
122
128
|
const lines = renderHelpOverlay(conversationWidth, keybindingsManager, commandRegistry.getAll(), input.helpScrollOffset, viewportHeight);
|
|
123
129
|
next = replaceViewportWithOverlay(lines, conversationWidth, viewportHeight);
|