@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-lifecycle wiring for the TUI shell.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from main() to keep the entrypoint under the architecture line-count
|
|
5
|
+
* gate. `installProcessLifecycle` builds the terminal-restoring crash/termination
|
|
6
|
+
* handlers and the graceful `exitApp` teardown, then returns them so main() can
|
|
7
|
+
* register them on the exact same process/stdin/stdout listeners it used before.
|
|
8
|
+
*
|
|
9
|
+
* Behavior is identical to the previous inline version: the synchronous terminal
|
|
10
|
+
* restore runs BEFORE the (possibly slow) async shutdown, the shutdown races a 3s
|
|
11
|
+
* hard timeout, the recovery file is deleted only when the durable save completed,
|
|
12
|
+
* signal exit codes are preserved (SIGHUP -> 129, otherwise 143; uncaught -> 1;
|
|
13
|
+
* exitApp -> 0), and the `exiting`/`terminalRestored` reentrancy guards are kept.
|
|
14
|
+
*
|
|
15
|
+
* Some captured values are late-bound or mutable in main(): the InputHandler, the
|
|
16
|
+
* render closure, and the terminal output guard are constructed AFTER this factory
|
|
17
|
+
* runs (injected as thunks), while `recoveryInterval` and `stopSpokenOutputForExit`
|
|
18
|
+
* are assigned later and read at exit time (injected as getters/setter). The
|
|
19
|
+
* `unsubs` registry is shared by reference and drained on exit.
|
|
20
|
+
*/
|
|
21
|
+
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
22
|
+
import { formatUserFacingErrorLine } from '../core/format-user-error.ts';
|
|
23
|
+
import { allowTerminalWrite } from './terminal-output-guard.ts';
|
|
24
|
+
import { buildPersistedSessionContext, deleteRecoveryFile } from '@/runtime/index.ts';
|
|
25
|
+
import type { BootstrapContext } from './bootstrap.ts';
|
|
26
|
+
import type { InputHandler } from '../input/handler.ts';
|
|
27
|
+
import type { ConversationMessageSnapshot } from '../core/conversation.ts';
|
|
28
|
+
|
|
29
|
+
/** ANSI escape sequences used by the synchronous terminal restore. */
|
|
30
|
+
export interface ProcessLifecycleAnsi {
|
|
31
|
+
readonly CLEAR_SCREEN: string;
|
|
32
|
+
readonly ALT_SCREEN_EXIT: string;
|
|
33
|
+
readonly PASTE_DISABLE: string;
|
|
34
|
+
readonly KEYBOARD_EXT_DISABLE: string;
|
|
35
|
+
readonly MOUSE_DISABLE: string;
|
|
36
|
+
readonly CURSOR_SHOW: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProcessLifecycleDeps {
|
|
40
|
+
readonly stdin: NodeJS.ReadStream;
|
|
41
|
+
readonly stdout: NodeJS.WriteStream;
|
|
42
|
+
readonly ctx: BootstrapContext;
|
|
43
|
+
/** Mirrors cli.flags.noAltScreen: whether the alt screen is exited on restore. */
|
|
44
|
+
readonly noAltScreen: boolean;
|
|
45
|
+
readonly ansi: ProcessLifecycleAnsi;
|
|
46
|
+
/** Late-bound: the InputHandler is constructed after this factory runs. */
|
|
47
|
+
readonly getInput: () => InputHandler;
|
|
48
|
+
/** Late-bound: the render closure is defined after this factory runs. */
|
|
49
|
+
readonly render: () => void;
|
|
50
|
+
/** Late-bound: the terminal output guard is installed after this factory runs. */
|
|
51
|
+
readonly getTerminalOutputGuard: () => { readonly dispose: () => void };
|
|
52
|
+
readonly getPromptContentWidth: () => number;
|
|
53
|
+
readonly buildSessionContinuityHints: () => Parameters<typeof buildPersistedSessionContext>[2];
|
|
54
|
+
/** Teardown registry shared with main(); drained on exit. */
|
|
55
|
+
readonly unsubs: ReadonlyArray<() => void>;
|
|
56
|
+
readonly getRecoveryInterval: () => ReturnType<typeof setInterval> | null;
|
|
57
|
+
readonly setRecoveryInterval: (value: ReturnType<typeof setInterval> | null) => void;
|
|
58
|
+
readonly getStopSpokenOutputForExit: () => (() => void) | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ProcessLifecycleHandlers {
|
|
62
|
+
readonly exitApp: () => Promise<void>;
|
|
63
|
+
readonly restoreTerminal: () => void;
|
|
64
|
+
readonly resizeHandler: () => void;
|
|
65
|
+
readonly sigintHandler: () => void;
|
|
66
|
+
readonly unhandledRejectionHandler: (reason: unknown) => void;
|
|
67
|
+
readonly uncaughtExceptionHandler: (err: Error) => void;
|
|
68
|
+
readonly terminationSignalHandler: (signal: NodeJS.Signals) => void;
|
|
69
|
+
readonly exitListener: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function installProcessLifecycle(deps: ProcessLifecycleDeps): ProcessLifecycleHandlers {
|
|
73
|
+
const {
|
|
74
|
+
stdin,
|
|
75
|
+
stdout,
|
|
76
|
+
ctx,
|
|
77
|
+
noAltScreen,
|
|
78
|
+
ansi,
|
|
79
|
+
getInput,
|
|
80
|
+
render,
|
|
81
|
+
getTerminalOutputGuard,
|
|
82
|
+
getPromptContentWidth,
|
|
83
|
+
buildSessionContinuityHints,
|
|
84
|
+
unsubs,
|
|
85
|
+
getRecoveryInterval,
|
|
86
|
+
setRecoveryInterval,
|
|
87
|
+
getStopSpokenOutputForExit,
|
|
88
|
+
} = deps;
|
|
89
|
+
|
|
90
|
+
const sigintHandler = (): void => getInput().feed('\x03');
|
|
91
|
+
let _unhandledRejectionCount = 0;
|
|
92
|
+
let _unhandledRejectionWindowStart = Date.now();
|
|
93
|
+
const unhandledRejectionHandler = (reason: unknown): void => {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
if (now - _unhandledRejectionWindowStart > 10000) {
|
|
96
|
+
_unhandledRejectionCount = 0;
|
|
97
|
+
_unhandledRejectionWindowStart = now;
|
|
98
|
+
}
|
|
99
|
+
_unhandledRejectionCount++;
|
|
100
|
+
const msg = summarizeError(reason);
|
|
101
|
+
if (_unhandledRejectionCount > 3) {
|
|
102
|
+
logger.error('CRITICAL: cascading unhandled rejections — consider restarting', {
|
|
103
|
+
count: _unhandledRejectionCount,
|
|
104
|
+
windowMs: now - _unhandledRejectionWindowStart,
|
|
105
|
+
error: String(reason),
|
|
106
|
+
});
|
|
107
|
+
ctx.systemMessageRouter.high(
|
|
108
|
+
`[Critical] Multiple errors detected (${_unhandledRejectionCount} in 10s). If the issue persists, please restart. Latest: ${msg}`
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
const formatted = formatUserFacingErrorLine(reason);
|
|
112
|
+
ctx.systemMessageRouter.high(`[Error] ${formatted}`);
|
|
113
|
+
logger.error('unhandledRejection', { error: String(reason) });
|
|
114
|
+
}
|
|
115
|
+
render();
|
|
116
|
+
};
|
|
117
|
+
const resizeHandler = (): void => {
|
|
118
|
+
getInput().setContentWidth(getPromptContentWidth());
|
|
119
|
+
ctx.compositor.resetDiff();
|
|
120
|
+
render();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let terminalRestored = false;
|
|
124
|
+
// Idempotent, synchronous-only terminal restore. Safe to call from process.on('exit'),
|
|
125
|
+
// signal handlers, uncaughtException, and exitApp. Disposes the output guard FIRST so a
|
|
126
|
+
// crash stack reaches the real stderr instead of being suppressed by the guard.
|
|
127
|
+
const restoreTerminal = (): void => {
|
|
128
|
+
if (terminalRestored) return;
|
|
129
|
+
terminalRestored = true;
|
|
130
|
+
const exitScreen = noAltScreen ? ansi.CLEAR_SCREEN : ansi.CLEAR_SCREEN + ansi.ALT_SCREEN_EXIT;
|
|
131
|
+
allowTerminalWrite(() => stdout.write(ansi.PASTE_DISABLE + ansi.KEYBOARD_EXT_DISABLE + ansi.MOUSE_DISABLE + ansi.CURSOR_SHOW + exitScreen));
|
|
132
|
+
getTerminalOutputGuard().dispose();
|
|
133
|
+
try { stdin.setRawMode(false); } catch { /* stdin may not be a TTY */ }
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const uncaughtExceptionHandler = (err: Error): void => {
|
|
137
|
+
restoreTerminal();
|
|
138
|
+
logger.error('uncaughtException — terminal restored, exiting', { error: summarizeError(err) });
|
|
139
|
+
process.exit(1);
|
|
140
|
+
};
|
|
141
|
+
const terminationSignalHandler = (signal: NodeJS.Signals): void => {
|
|
142
|
+
restoreTerminal();
|
|
143
|
+
logger.error(`Received ${signal} — terminal restored, exiting`, {});
|
|
144
|
+
process.exit(signal === 'SIGHUP' ? 129 : 143);
|
|
145
|
+
};
|
|
146
|
+
const exitListener = (): void => { restoreTerminal(); };
|
|
147
|
+
|
|
148
|
+
let exiting = false;
|
|
149
|
+
const exitApp = async (): Promise<void> => {
|
|
150
|
+
// Reentrancy guard: a second /exit or keypress during the awaited shutdown below
|
|
151
|
+
// must not re-run teardown or double-fire ctx.shutdown.
|
|
152
|
+
if (exiting) return;
|
|
153
|
+
exiting = true;
|
|
154
|
+
getStopSpokenOutputForExit()?.();
|
|
155
|
+
unsubs.forEach(fn => fn());
|
|
156
|
+
const interval = getRecoveryInterval();
|
|
157
|
+
if (interval !== null) { clearInterval(interval); setRecoveryInterval(null); }
|
|
158
|
+
stdin.removeAllListeners('data');
|
|
159
|
+
stdout.removeListener('resize', resizeHandler);
|
|
160
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
161
|
+
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
162
|
+
// Restore the terminal synchronously BEFORE the (possibly slow) async shutdown so the
|
|
163
|
+
// user is not left staring at a frozen alt screen while we drain and persist.
|
|
164
|
+
restoreTerminal();
|
|
165
|
+
const snapshot = ctx.conversation.toJSON() as { messages: Array<ConversationMessageSnapshot>; timestamp?: number };
|
|
166
|
+
let shutdownOk = false;
|
|
167
|
+
try {
|
|
168
|
+
// Race the graceful shutdown against a hard timeout — externalServices.stop() can hang
|
|
169
|
+
// and we must still exit; deferredStartup.drain only budgets 100ms internally.
|
|
170
|
+
await Promise.race([
|
|
171
|
+
ctx.shutdown({ ...snapshot, ...buildPersistedSessionContext(snapshot.messages, ctx.conversation.getTitleSource(), buildSessionContinuityHints()) }).then(() => { shutdownOk = true; }),
|
|
172
|
+
new Promise<void>((resolve) => setTimeout(resolve, 3000)),
|
|
173
|
+
]);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logger.debug('ctx.shutdown error during exitApp (non-fatal)', { error: summarizeError(err) });
|
|
176
|
+
}
|
|
177
|
+
// Only remove the recovery fallback once the durable shutdown save actually completed,
|
|
178
|
+
// so an interrupted or timed-out shutdown leaves the snapshot for the next launch.
|
|
179
|
+
if (shutdownOk) {
|
|
180
|
+
deleteRecoveryFile({ homeDirectory: ctx.services.homeDirectory });
|
|
181
|
+
}
|
|
182
|
+
process.exit(0);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
exitApp,
|
|
187
|
+
restoreTerminal,
|
|
188
|
+
resizeHandler,
|
|
189
|
+
sigintHandler,
|
|
190
|
+
unhandledRejectionHandler,
|
|
191
|
+
uncaughtExceptionHandler,
|
|
192
|
+
terminationSignalHandler,
|
|
193
|
+
exitListener,
|
|
194
|
+
};
|
|
195
|
+
}
|
package/src/runtime/services.ts
CHANGED
|
@@ -63,6 +63,7 @@ import { CacheHitTracker } from '@pellux/goodvibes-sdk/platform/providers';
|
|
|
63
63
|
import { FavoritesStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
64
64
|
import { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
65
65
|
import { ModelLimitsService } from '@pellux/goodvibes-sdk/platform/providers';
|
|
66
|
+
import { inferFallbackContextWindow } from '@pellux/goodvibes-sdk/platform/providers';
|
|
66
67
|
import { KeybindingsManager } from '../input/keybindings.ts';
|
|
67
68
|
import { SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
|
|
68
69
|
import { SessionLineageTracker } from '@pellux/goodvibes-sdk/platform/core';
|
|
@@ -123,7 +124,9 @@ function buildFallbackModelDefinition(provider: string, modelId: string): ModelD
|
|
|
123
124
|
reasoning: isReasoningProvider,
|
|
124
125
|
multimodal: isReasoningProvider,
|
|
125
126
|
},
|
|
126
|
-
|
|
127
|
+
// Pre-catalog fallback uses the SDK's family-aware inference (SDK 0.35.0+),
|
|
128
|
+
// matching the post-catalog window so the meter/compaction denominator agrees.
|
|
129
|
+
contextWindow: inferFallbackContextWindow(provider, modelId),
|
|
127
130
|
contextWindowProvenance: 'fallback',
|
|
128
131
|
selectable: true,
|
|
129
132
|
tier: 'standard',
|
|
@@ -154,6 +154,9 @@ export function enrichModelEntries(
|
|
|
154
154
|
// Determine source: custom/local providers carry provenance on ModelDefinition;
|
|
155
155
|
// for catalog models, if getContextWindowForModel returned more than the
|
|
156
156
|
// static contextWindow it came from OpenRouter, else it's the registry value.
|
|
157
|
+
// NOTE: This is a best-effort heuristic. When the OpenRouter live value equals
|
|
158
|
+
// the static registry value the badge shows 'registry' even though OpenRouter
|
|
159
|
+
// was consulted. The contextWindow value itself is always accurate.
|
|
157
160
|
let contextWindowSource: ModelPickerEntry['contextWindowSource'];
|
|
158
161
|
if (model.contextWindowProvenance) {
|
|
159
162
|
contextWindowSource = model.contextWindowProvenance;
|
|
@@ -97,6 +97,10 @@ export interface ModelPickerEntry {
|
|
|
97
97
|
* - `fallback` — default constant (no config or API source)
|
|
98
98
|
* - `openrouter` — sourced from OpenRouter model data (built-in catalog models)
|
|
99
99
|
* - `registry` — static value in the built-in model registry
|
|
100
|
+
*
|
|
101
|
+
* For built-in catalog models this badge is best-effort: when the OpenRouter
|
|
102
|
+
* resolved value equals the static registry value the source shows 'registry'
|
|
103
|
+
* even if OpenRouter was consulted at runtime.
|
|
100
104
|
*/
|
|
101
105
|
readonly contextWindowSource:
|
|
102
106
|
| 'provider_api'
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 2. `WrfcPersistence.rehydrate(router)` is called once on boot (after the
|
|
12
12
|
* SystemMessageRouter is available). It reads the snapshot, identifies
|
|
13
|
-
* chains that were in a non-terminal state at last write,
|
|
14
|
-
* high-priority 'wrfc' system message per interrupted chain
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* chains that were in a non-terminal state at last write, emits a
|
|
14
|
+
* high-priority 'wrfc' system message per interrupted chain, and re-imports
|
|
15
|
+
* each interrupted chain into the WrfcController so it reappears as a panel
|
|
16
|
+
* row and can be resumed by the operator. The `interruptedChains` accessor
|
|
17
|
+
* additionally exposes the recovered set for inspection.
|
|
17
18
|
*
|
|
18
19
|
* 3. Snapshot lifecycle:
|
|
19
20
|
* - Terminal chains ('passed' | 'failed') are pruned from the snapshot
|
|
@@ -57,6 +58,14 @@ interface WrfcSnapshot {
|
|
|
57
58
|
/** Subset of WrfcController needed by this module. */
|
|
58
59
|
export interface WrfcControllerReader {
|
|
59
60
|
listChains(): WrfcChain[];
|
|
61
|
+
/**
|
|
62
|
+
* Re-import a chain recovered from a previous process so it reappears in the
|
|
63
|
+
* controller's in-memory map and becomes selectable/resumable from the panel.
|
|
64
|
+
* Optional so read-only test doubles can omit it; the real WrfcController
|
|
65
|
+
* provides it. `force` is left at its default — on a fresh start the map is
|
|
66
|
+
* empty so importing never clobbers a live chain.
|
|
67
|
+
*/
|
|
68
|
+
importChain?(chain: WrfcChain, force?: boolean): boolean;
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
export interface WrfcPersistenceOptions {
|
|
@@ -64,7 +73,7 @@ export interface WrfcPersistenceOptions {
|
|
|
64
73
|
readonly snapshotPath: string;
|
|
65
74
|
/** Factory for the current SystemMessageRouter — may return null before it is wired. */
|
|
66
75
|
readonly getSystemMessageRouter: () => SystemMessageRouter | null;
|
|
67
|
-
/** WrfcController
|
|
76
|
+
/** WrfcController access — listChains() (read) plus optional importChain() (re-import on rehydrate). */
|
|
68
77
|
readonly controller: WrfcControllerReader;
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -140,6 +149,12 @@ class WrfcPersistenceImpl implements WrfcPersistence {
|
|
|
140
149
|
|
|
141
150
|
const router = this.getSystemMessageRouter();
|
|
142
151
|
for (const chain of interrupted) {
|
|
152
|
+
// Re-import so the chain reappears in the controller's in-memory map and
|
|
153
|
+
// becomes selectable/resumable from the panel. On a fresh process start
|
|
154
|
+
// the map is empty, so importChain (force=false) never clobbers a live
|
|
155
|
+
// chain. The accessor is optional for read-only test doubles.
|
|
156
|
+
this.controller.importChain?.(chain);
|
|
157
|
+
|
|
143
158
|
const msg =
|
|
144
159
|
`[WRFC] Chain ${chain.id.slice(0, 12)} (${chain.task.slice(0, 60).trim()}) ` +
|
|
145
160
|
`was interrupted by a restart — state was '${chain.state}' ` +
|
package/src/shell/ui-openers.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { SecretsManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
13
13
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
14
14
|
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
15
15
|
import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
|
|
16
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
16
17
|
|
|
17
18
|
type WireShellUiOpenersOptions = {
|
|
18
19
|
commandContext: CommandContext;
|
|
@@ -203,7 +204,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
203
204
|
input.modelPicker.openAllModels(models, getCurrentModelForPickerTarget());
|
|
204
205
|
render();
|
|
205
206
|
})().catch((error: unknown) => {
|
|
206
|
-
commandContext.print?.(`Model picker failed to open: ${
|
|
207
|
+
commandContext.print?.(`Model picker failed to open: ${summarizeError(error)}`);
|
|
207
208
|
render();
|
|
208
209
|
});
|
|
209
210
|
};
|
|
@@ -223,7 +224,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
|
|
|
223
224
|
input.modelPicker.openProviders(providers, getCurrentProviderForPickerTarget());
|
|
224
225
|
render();
|
|
225
226
|
})().catch((error: unknown) => {
|
|
226
|
-
commandContext.print?.(`Provider picker failed to open: ${
|
|
227
|
+
commandContext.print?.(`Provider picker failed to open: ${summarizeError(error)}`);
|
|
227
228
|
render();
|
|
228
229
|
});
|
|
229
230
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ms-duration formatters for the panel layer.
|
|
3
|
+
*
|
|
4
|
+
* Three families are exported, each matching a distinct behavior cluster.
|
|
5
|
+
* DO NOT replace these with formatElapsed from utils/format-elapsed.ts —
|
|
6
|
+
* formatElapsed floors to integer seconds and lacks the null/'?ms' guards
|
|
7
|
+
* that the latency panels require.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format a latency value in milliseconds with sub-second precision.
|
|
12
|
+
*
|
|
13
|
+
* Used by: debug-panel, provider-health-panel, provider-stats-panel
|
|
14
|
+
*
|
|
15
|
+
* ms <= 0 → 'n/a'
|
|
16
|
+
* ms >= 10000 → '12.3s' (one decimal)
|
|
17
|
+
* ms >= 1000 → '1.23s' (two decimals)
|
|
18
|
+
* else → '500ms' (integer ms)
|
|
19
|
+
*/
|
|
20
|
+
export function formatLatencyMs(ms: number): string {
|
|
21
|
+
if (ms <= 0) return 'n/a';
|
|
22
|
+
if (ms >= 10_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
23
|
+
if (ms >= 1_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
24
|
+
return `${Math.round(ms)}ms`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format a task/agent duration with minute-rolling notation.
|
|
29
|
+
*
|
|
30
|
+
* Used by: tool-inspector-panel, agent-inspector-shared
|
|
31
|
+
*
|
|
32
|
+
* ms < 1000 → '500ms'
|
|
33
|
+
* ms < 60000 → '3.5s'
|
|
34
|
+
* else → '1m30s'
|
|
35
|
+
*/
|
|
36
|
+
export function formatDuration(ms: number): string {
|
|
37
|
+
if (ms < 1000) return `${ms}ms`;
|
|
38
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
39
|
+
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format a short eval/forensics duration; treats undefined as '?ms'.
|
|
44
|
+
*
|
|
45
|
+
* Used by: forensics-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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parametrised number abbreviation helper.
|
|
3
|
+
*
|
|
4
|
+
* Each call site can reproduce its exact current output by passing the
|
|
5
|
+
* appropriate options rather than a single "agreed" format being imposed.
|
|
6
|
+
*
|
|
7
|
+
* Ranges (default opts: guard=1_000, decimals=1, rounding='fixed'):
|
|
8
|
+
* n < guard → String(n) e.g. 999 → '999'
|
|
9
|
+
* n < 1_000_000 → X.Xk e.g. 1500 → '1.5k'
|
|
10
|
+
* n < 1_000_000_000 → X.XM e.g. 1.5M → '1.5M'
|
|
11
|
+
* (with bSuffix) → X.XB / X.XT
|
|
12
|
+
* (with noM) → always k, never M
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface AbbreviateCountOpts {
|
|
16
|
+
/**
|
|
17
|
+
* Values strictly below this are returned as-is via String(n).
|
|
18
|
+
* Default: 1_000
|
|
19
|
+
*/
|
|
20
|
+
guard?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Decimal places used for the k suffix (and for M when mDecimals is not set).
|
|
23
|
+
* Default: 1
|
|
24
|
+
*/
|
|
25
|
+
decimals?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Override decimal places for the M suffix only.
|
|
28
|
+
* Default: same as decimals.
|
|
29
|
+
*/
|
|
30
|
+
mDecimals?: number;
|
|
31
|
+
/**
|
|
32
|
+
* 'fixed' → value.toFixed(decimals) (default)
|
|
33
|
+
* 'round' → String(Math.round(value)) — always integer, decimals ignored
|
|
34
|
+
*/
|
|
35
|
+
rounding?: 'fixed' | 'round';
|
|
36
|
+
/**
|
|
37
|
+
* When true, extend formatting to B (billions) and T (trillions).
|
|
38
|
+
* Default: false
|
|
39
|
+
*/
|
|
40
|
+
bSuffix?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* When true, never produce M/B/T — format as k even for millions.
|
|
43
|
+
* Default: false
|
|
44
|
+
*/
|
|
45
|
+
noM?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format a non-negative integer with k/M/B/T magnitude suffix.
|
|
50
|
+
*
|
|
51
|
+
* Preserves each call site's exact current format by accepting
|
|
52
|
+
* per-site option overrides (guard, decimals, rounding, etc.).
|
|
53
|
+
*/
|
|
54
|
+
export function abbreviateCount(n: number, opts?: AbbreviateCountOpts): string {
|
|
55
|
+
const guard = opts?.guard ?? 1_000;
|
|
56
|
+
const decimals = opts?.decimals ?? 1;
|
|
57
|
+
const mDecimals = opts?.mDecimals ?? decimals;
|
|
58
|
+
const rounding = opts?.rounding ?? 'fixed';
|
|
59
|
+
const noM = opts?.noM ?? false;
|
|
60
|
+
const bSuffix = opts?.bSuffix ?? false;
|
|
61
|
+
|
|
62
|
+
/** Render a scaled value according to the configured rounding mode. */
|
|
63
|
+
const fmt = (val: number, d: number): string =>
|
|
64
|
+
rounding === 'round' ? String(Math.round(val)) : val.toFixed(d);
|
|
65
|
+
|
|
66
|
+
if (n < guard) return String(n);
|
|
67
|
+
if (noM || n < 1_000_000) return fmt(n / 1_000, decimals) + 'k';
|
|
68
|
+
if (!bSuffix || n < 1_000_000_000) return fmt(n / 1_000_000, mDecimals) + 'M';
|
|
69
|
+
if (n < 1_000_000_000_000) return fmt(n / 1_000_000_000, decimals) + 'B';
|
|
70
|
+
return fmt(n / 1_000_000_000_000, decimals) + 'T';
|
|
71
|
+
}
|
|
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { auditGoodVibesHome } from '../config/goodvibes-home-audit.ts';
|
|
5
5
|
import { buildVerificationLedger } from './verification-ledger.ts';
|
|
6
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
7
|
|
|
7
8
|
export type LiveVerificationStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
8
9
|
|
|
@@ -111,7 +112,7 @@ function runCommand(command: string, args: string[], cwd: string, timeoutMs = 15
|
|
|
111
112
|
resolveCommand({
|
|
112
113
|
exitCode: -1,
|
|
113
114
|
stdout: '',
|
|
114
|
-
stderr:
|
|
115
|
+
stderr: summarizeError(error),
|
|
115
116
|
timedOut,
|
|
116
117
|
});
|
|
117
118
|
});
|
|
@@ -161,7 +162,7 @@ function commandCheck(
|
|
|
161
162
|
title,
|
|
162
163
|
status: 'fail',
|
|
163
164
|
summary: 'Command succeeded but did not return valid JSON.',
|
|
164
|
-
detail:
|
|
165
|
+
detail: summarizeError(error),
|
|
165
166
|
};
|
|
166
167
|
}
|
|
167
168
|
}
|
|
@@ -208,7 +209,7 @@ async function fetchCheck(
|
|
|
208
209
|
title,
|
|
209
210
|
status: 'fail',
|
|
210
211
|
summary: 'Request failed.',
|
|
211
|
-
detail:
|
|
212
|
+
detail: summarizeError(error),
|
|
212
213
|
};
|
|
213
214
|
}
|
|
214
215
|
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.
|
|
9
|
+
let _version = '0.28.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-compaction helper — TASK-058.
|
|
3
|
-
*
|
|
4
|
-
* Evaluates whether auto-compact should run after a turn completes and, if so,
|
|
5
|
-
* triggers compaction and posts an honest transcript notice.
|
|
6
|
-
*
|
|
7
|
-
* behavior.autoCompactThreshold: SDK schema range [10, 100], default 80.
|
|
8
|
-
* Auto-compact is active whenever the threshold is in its valid range (>0).
|
|
9
|
-
* The display/suggestion path (hint + meter) is always active regardless.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
|
-
import type { ConversationManager } from './conversation';
|
|
14
|
-
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
15
|
-
import type { SystemMessageRouter } from './system-message-router';
|
|
16
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
|
-
import { getLastCompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
19
|
-
import type { CompactionContext } from '@pellux/goodvibes-sdk/platform/core';
|
|
20
|
-
import { buildCompactionPreview, buildCompactionAfterNotice } from '../renderer/compaction-preview.ts';
|
|
21
|
-
|
|
22
|
-
export interface AutoCompactDeps {
|
|
23
|
-
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
24
|
-
readonly conversation: ConversationManager;
|
|
25
|
-
readonly providerRegistry: ProviderRegistry;
|
|
26
|
-
readonly systemMessageRouter: SystemMessageRouter;
|
|
27
|
-
readonly model: string;
|
|
28
|
-
readonly provider: string;
|
|
29
|
-
readonly lastInputTokens: number;
|
|
30
|
-
readonly contextWindow: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Run after each TURN_COMPLETED event.
|
|
35
|
-
*
|
|
36
|
-
* Reads behavior.autoCompactThreshold from config (SDK default: 80, range [10, 100]).
|
|
37
|
-
* When usage is at or above the threshold, compacts the conversation and posts
|
|
38
|
-
* an honest transcript notice so the user understands any summary discontinuity.
|
|
39
|
-
*
|
|
40
|
-
* This function is intentionally non-throwing; failures are logged and
|
|
41
|
-
* surfaced via the system message router.
|
|
42
|
-
*/
|
|
43
|
-
export async function maybeAutoCompact(deps: AutoCompactDeps): Promise<void> {
|
|
44
|
-
// SDK schema default is 80; valid range is [10, 100]. The ?? 0 fallback is a
|
|
45
|
-
// defensive guard for missing/null values only — not a normal operating state.
|
|
46
|
-
const rawThreshold = Number(deps.configManager.get('behavior.autoCompactThreshold') ?? 0);
|
|
47
|
-
const thresholdPct = Number.isFinite(rawThreshold) ? rawThreshold : 0;
|
|
48
|
-
|
|
49
|
-
// Defensive guard: skip only when threshold is missing/non-positive (real config defaults to 80).
|
|
50
|
-
if (thresholdPct <= 0 || deps.contextWindow <= 0) return;
|
|
51
|
-
|
|
52
|
-
const usagePct = Math.min(100, Math.round((Math.max(0, deps.lastInputTokens) / deps.contextWindow) * 100));
|
|
53
|
-
if (usagePct < thresholdPct) return;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
logger.debug('auto-compact triggered', { usagePct, thresholdPct });
|
|
57
|
-
// Pre-compact preview — uses buildCompactionPreview for an honest estimate.
|
|
58
|
-
const messages = deps.conversation.getMessagesForLLM();
|
|
59
|
-
const sessionMemoryStore = deps.conversation.getSessionMemoryStore();
|
|
60
|
-
const sessionMemories = sessionMemoryStore?.list() ?? [];
|
|
61
|
-
const pinnedMemoryCount = sessionMemories.length;
|
|
62
|
-
const preview = buildCompactionPreview({
|
|
63
|
-
messages,
|
|
64
|
-
contextWindow: deps.contextWindow,
|
|
65
|
-
pinnedMemoryCount,
|
|
66
|
-
trigger: 'auto',
|
|
67
|
-
});
|
|
68
|
-
deps.systemMessageRouter.routeSystemMessage(preview, 'high');
|
|
69
|
-
const eventBefore = getLastCompactionEvent();
|
|
70
|
-
const compactionCtx: CompactionContext = {
|
|
71
|
-
messages,
|
|
72
|
-
sessionMemories,
|
|
73
|
-
agents: [],
|
|
74
|
-
wrfcChains: [],
|
|
75
|
-
activePlan: null,
|
|
76
|
-
lineageEntries: [],
|
|
77
|
-
compactionCount: 0,
|
|
78
|
-
contextWindow: deps.contextWindow,
|
|
79
|
-
trigger: 'auto',
|
|
80
|
-
extractionModelId: deps.model,
|
|
81
|
-
extractionProvider: deps.provider,
|
|
82
|
-
};
|
|
83
|
-
await deps.conversation.compact(
|
|
84
|
-
deps.providerRegistry,
|
|
85
|
-
deps.model,
|
|
86
|
-
'auto',
|
|
87
|
-
deps.provider,
|
|
88
|
-
compactionCtx,
|
|
89
|
-
);
|
|
90
|
-
// Post-compact notice using real CompactionEvent figures.
|
|
91
|
-
const eventAfter = getLastCompactionEvent();
|
|
92
|
-
if (eventAfter !== null && eventAfter !== eventBefore) {
|
|
93
|
-
deps.systemMessageRouter.routeSystemMessage(
|
|
94
|
-
buildCompactionAfterNotice({ event: eventAfter, pinnedMemoryCount }),
|
|
95
|
-
'low',
|
|
96
|
-
);
|
|
97
|
-
} else {
|
|
98
|
-
deps.systemMessageRouter.routeSystemMessage(
|
|
99
|
-
'[Context] Auto-compact complete — older turns summarised. Use /compact to compact again manually.',
|
|
100
|
-
'low',
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
logger.error('auto-compact failed', { error: summarizeError(err) });
|
|
105
|
-
deps.systemMessageRouter.routeSystemMessage(
|
|
106
|
-
`[Context] Auto-compact failed: ${summarizeError(err)}. Use /compact to try manually.`,
|
|
107
|
-
'high',
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|