@pellux/goodvibes-tui 0.22.0 → 0.24.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 +47 -0
- package/README.md +17 -8
- package/package.json +1 -1
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +116 -344
- package/src/cli/surface-command.ts +1 -1
- package/src/core/context-auto-compact.ts +43 -10
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/long-task-notifier.ts +145 -0
- package/src/core/session-recovery.ts +147 -0
- package/src/core/stream-event-wiring.ts +199 -7
- package/src/core/transcript-journal.ts +339 -0
- package/src/core/turn-event-wiring.ts +67 -4
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +0 -2
- package/src/input/commands/diff-runtime.ts +1 -1
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +23 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-runtime.ts +1 -2
- package/src/input/commands/memory-product-runtime.ts +2 -2
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/onboarding-runtime.ts +0 -1
- package/src/input/commands/policy.ts +1 -1
- package/src/input/commands/profile-sync-runtime.ts +4 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +0 -1
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/session-workflow.ts +32 -2
- package/src/input/commands/session.ts +1 -1
- package/src/input/commands/settings-sync-runtime.ts +9 -9
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +56 -6
- package/src/input/commands/work-plan-runtime.ts +8 -8
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +6 -0
- package/src/input/handler-feed-routes.ts +19 -1
- package/src/input/handler-feed.ts +11 -0
- package/src/input/handler-prompt-buffer.ts +28 -0
- package/src/input/handler-shortcuts.ts +88 -2
- package/src/input/handler-ui-state.ts +2 -2
- package/src/input/handler.ts +39 -3
- package/src/input/keybindings.ts +33 -3
- package/src/input/kill-ring.ts +134 -0
- package/src/input/model-picker.ts +18 -1
- package/src/input/search.ts +18 -6
- package/src/input/settings-modal-activation.ts +134 -0
- package/src/input/settings-modal-adjustment.ts +124 -0
- package/src/input/settings-modal-data.ts +53 -0
- package/src/input/settings-modal.ts +48 -145
- package/src/main.ts +50 -50
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/provider-health-domains.ts +3 -3
- package/src/panels/provider-health-panel.ts +13 -9
- package/src/panels/provider-health-tracker.ts +7 -4
- package/src/panels/settings-sync-panel.ts +3 -3
- package/src/panels/work-plan-panel.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/diff-view.ts +2 -2
- package/src/renderer/help-overlay.ts +1 -0
- package/src/renderer/model-picker-overlay.ts +23 -11
- package/src/renderer/progress.ts +3 -3
- package/src/renderer/search-overlay.ts +8 -5
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +1 -1
- package/src/renderer/ui-factory.ts +11 -0
- package/src/runtime/bootstrap-core.ts +92 -0
- package/src/runtime/bootstrap-hook-bridge.ts +18 -0
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/shell/blocking-input.ts +32 -0
- package/src/shell/recovery-input-helpers.ts +71 -0
- package/src/utils/browser.ts +29 -0
- package/src/utils/terminal-width.ts +10 -3
- package/src/version.ts +1 -1
package/src/cli/management.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname
|
|
3
|
-
import net from 'node:net';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import { networkInterfaces } from 'node:os';
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
6
3
|
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../config/index.ts';
|
|
7
4
|
import { CONFIG_SCHEMA } from '../config/index.ts';
|
|
8
5
|
import { formatProviderModel, getModelIdFromProviderModel } from '../config/provider-model.ts';
|
|
9
|
-
import { bootstrapRuntime } from '../runtime/bootstrap.ts';
|
|
10
|
-
import { createRuntimeServices } from '../runtime/services.ts';
|
|
11
|
-
import { createRuntimeStore } from '../runtime/store/index.ts';
|
|
12
|
-
import type { RuntimeServices } from '../runtime/services.ts';
|
|
13
6
|
import { SecretsManager } from '../config/secrets.ts';
|
|
14
|
-
import { RuntimeEventBus, type TurnEvent } from '@/runtime/index.ts';
|
|
15
|
-
import { createShellPathService } from '@/runtime/index.ts';
|
|
16
7
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
8
|
import { listProviderRuntimeSnapshots } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
|
+
import type { CanonicalModel } from '@pellux/goodvibes-sdk/platform/providers';
|
|
18
10
|
import { BUILTIN_SECRET_PROVIDER_SOURCES, describeSecretRef, isSecretRefInput, resolveSecretRef } from '@pellux/goodvibes-sdk/platform/config';
|
|
19
11
|
import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
20
12
|
import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config';
|
|
@@ -31,105 +23,46 @@ import { handleServiceCommand } from './service-command.ts';
|
|
|
31
23
|
import { handleBundleCommand } from './bundle-command.ts';
|
|
32
24
|
import { buildListenerTestResult, formatListenerTestResult, handleSurfacesCommand } from './surface-command.ts';
|
|
33
25
|
import { buildControlPlaneStatusResult, formatControlPlaneStatus, handleSecrets, handleSessions, handleTasks, renderPairing, renderRemote, renderSubscriptions, renderWeb } from './management-commands.ts';
|
|
26
|
+
import {
|
|
27
|
+
yesNo,
|
|
28
|
+
formatJsonOrText,
|
|
29
|
+
hasCommandFlag,
|
|
30
|
+
withRuntimeServices,
|
|
31
|
+
runNonInteractiveAgent,
|
|
32
|
+
isPresentConfigValue,
|
|
33
|
+
urlHostForBindHost,
|
|
34
|
+
probeTcp,
|
|
35
|
+
readAuthPaths,
|
|
36
|
+
exitCodeForText,
|
|
37
|
+
splitCommandOption,
|
|
38
|
+
readOptionValue,
|
|
39
|
+
readOptionValues,
|
|
40
|
+
commandValues,
|
|
41
|
+
readPassword,
|
|
42
|
+
} from './management-utils.ts';
|
|
34
43
|
|
|
35
44
|
interface CliCommandResult {
|
|
36
45
|
readonly handled: boolean;
|
|
37
46
|
readonly exitCode: number;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (output === 'Bundle has no config object to import.') return 1;
|
|
58
|
-
return 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function splitCommandOption(token: string): { readonly name: string; readonly value: string | undefined } {
|
|
62
|
-
const index = token.indexOf('=');
|
|
63
|
-
if (index < 0) return { name: token, value: undefined };
|
|
64
|
-
return { name: token.slice(0, index), value: token.slice(index + 1) };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readOptionValue(args: readonly string[], name: string): string | undefined {
|
|
68
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
-
const token = args[index]!;
|
|
70
|
-
const split = splitCommandOption(token);
|
|
71
|
-
if (split.name !== name) continue;
|
|
72
|
-
if (split.value !== undefined) return split.value;
|
|
73
|
-
const next = args[index + 1];
|
|
74
|
-
if (next === undefined || next.startsWith('--')) return undefined;
|
|
75
|
-
return next;
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function readOptionValues(args: readonly string[], name: string): string[] {
|
|
81
|
-
const values: string[] = [];
|
|
82
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
83
|
-
const token = args[index]!;
|
|
84
|
-
const split = splitCommandOption(token);
|
|
85
|
-
if (split.name !== name) continue;
|
|
86
|
-
if (split.value !== undefined) {
|
|
87
|
-
values.push(split.value);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const next = args[index + 1];
|
|
91
|
-
if (next !== undefined && !next.startsWith('--')) values.push(next);
|
|
92
|
-
}
|
|
93
|
-
return values;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function hasCommandFlag(args: readonly string[], name: string): boolean {
|
|
97
|
-
return args.some((arg) => splitCommandOption(arg).name === name);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function commandValues(args: readonly string[]): string[] {
|
|
101
|
-
const values: string[] = [];
|
|
102
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
103
|
-
const token = args[index]!;
|
|
104
|
-
if (!token.startsWith('--')) {
|
|
105
|
-
values.push(token);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!token.includes('=') && args[index + 1] && !args[index + 1]!.startsWith('--')) index += 1;
|
|
109
|
-
}
|
|
110
|
-
return values;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function readPassword(args: readonly string[]): string | null {
|
|
114
|
-
const explicit = readOptionValue(args, '--password');
|
|
115
|
-
if (explicit !== undefined) return explicit;
|
|
116
|
-
if (hasCommandFlag(args, '--password-stdin')) return readFileSync(0, 'utf-8').trimEnd();
|
|
117
|
-
return process.env.GOODVIBES_AUTH_PASSWORD ?? null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function extractAuthorizationCode(input: string): string {
|
|
121
|
-
try {
|
|
122
|
-
const url = new URL(input);
|
|
123
|
-
return url.searchParams.get('code') ?? input;
|
|
124
|
-
} catch {
|
|
125
|
-
return input;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function isPresentConfigValue(value: unknown): boolean {
|
|
130
|
-
if (typeof value === 'string') return value.trim().length > 0;
|
|
131
|
-
return value !== undefined && value !== null && value !== false;
|
|
132
|
-
}
|
|
49
|
+
export {
|
|
50
|
+
yesNo,
|
|
51
|
+
formatJsonOrText,
|
|
52
|
+
hasCommandFlag,
|
|
53
|
+
extractAuthorizationCode,
|
|
54
|
+
isPresentConfigValue,
|
|
55
|
+
getNestedValue,
|
|
56
|
+
urlHostForBindHost,
|
|
57
|
+
enableServicePosture,
|
|
58
|
+
enableEndpointLanDefault,
|
|
59
|
+
applyTargetEndpointFlagsOrDefault,
|
|
60
|
+
openBrowser,
|
|
61
|
+
probeTcp,
|
|
62
|
+
withRuntimeServices,
|
|
63
|
+
readAuthPaths,
|
|
64
|
+
runNonInteractiveAgent,
|
|
65
|
+
} from './management-utils.ts';
|
|
133
66
|
|
|
134
67
|
function inferProviderFromRegistryKey(modelKey: string): string {
|
|
135
68
|
if (modelKey.includes(':')) return modelKey.split(':')[0] || 'openai';
|
|
@@ -137,236 +70,6 @@ function inferProviderFromRegistryKey(modelKey: string): string {
|
|
|
137
70
|
return 'openai';
|
|
138
71
|
}
|
|
139
72
|
|
|
140
|
-
export function getNestedValue(source: unknown, key: string): unknown {
|
|
141
|
-
let cursor = source;
|
|
142
|
-
for (const part of key.split('.')) {
|
|
143
|
-
if (cursor == null || typeof cursor !== 'object') return undefined;
|
|
144
|
-
cursor = (cursor as Record<string, unknown>)[part];
|
|
145
|
-
}
|
|
146
|
-
return cursor;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getLocalNetworkIp(): string {
|
|
150
|
-
try {
|
|
151
|
-
const nets = networkInterfaces();
|
|
152
|
-
for (const name of Object.keys(nets)) {
|
|
153
|
-
for (const netInfo of nets[name] ?? []) {
|
|
154
|
-
if (netInfo.family === 'IPv4' && !netInfo.internal) return netInfo.address;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
} catch {
|
|
158
|
-
return '127.0.0.1';
|
|
159
|
-
}
|
|
160
|
-
return '127.0.0.1';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function connectHostForBindHost(host: string): string {
|
|
164
|
-
if (host === '0.0.0.0' || host === '::') return '127.0.0.1';
|
|
165
|
-
return host || '127.0.0.1';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function urlHostForBindHost(host: string): string {
|
|
169
|
-
if (host === '0.0.0.0' || host === '::') return getLocalNetworkIp();
|
|
170
|
-
return host || '127.0.0.1';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function enableServicePosture(config: ConfigManager): void {
|
|
174
|
-
config.setDynamic('service.enabled', true);
|
|
175
|
-
config.setDynamic('service.autostart', true);
|
|
176
|
-
config.setDynamic('service.restartOnFailure', true);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function enableEndpointLanDefault(config: ConfigManager, endpoint: RuntimeEndpointId): void {
|
|
180
|
-
const binding = resolveRuntimeEndpointBinding(config, endpoint);
|
|
181
|
-
if (binding.hostMode === 'custom') return;
|
|
182
|
-
if (endpoint === 'controlPlane') {
|
|
183
|
-
config.setDynamic('controlPlane.hostMode', 'network');
|
|
184
|
-
config.setDynamic('controlPlane.host', '0.0.0.0');
|
|
185
|
-
config.setDynamic('controlPlane.allowRemote', true);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (endpoint === 'httpListener') {
|
|
189
|
-
config.setDynamic('httpListener.hostMode', 'network');
|
|
190
|
-
config.setDynamic('httpListener.host', '0.0.0.0');
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
config.setDynamic('web.hostMode', 'network');
|
|
194
|
-
config.setDynamic('web.host', '0.0.0.0');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function applyTargetEndpointFlagsOrDefault(
|
|
198
|
-
runtime: CliCommandRuntime,
|
|
199
|
-
endpoint: RuntimeEndpointId,
|
|
200
|
-
): string | null {
|
|
201
|
-
const errors = applyRuntimeEndpointFlagOverrides(runtime.configManager, endpoint, runtime.cli.flags);
|
|
202
|
-
if (errors.length > 0) return errors.join('\n');
|
|
203
|
-
if (runtime.cli.flags.hostname === undefined) {
|
|
204
|
-
enableEndpointLanDefault(runtime.configManager, endpoint);
|
|
205
|
-
}
|
|
206
|
-
if (endpoint === 'controlPlane') {
|
|
207
|
-
const binding = resolveRuntimeEndpointBinding(runtime.configManager, endpoint);
|
|
208
|
-
runtime.configManager.setDynamic('controlPlane.allowRemote', binding.hostMode !== 'local');
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function openBrowser(url: string): string {
|
|
214
|
-
const platform = process.platform;
|
|
215
|
-
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
216
|
-
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
217
|
-
try {
|
|
218
|
-
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
219
|
-
child.once('error', () => {});
|
|
220
|
-
child.unref();
|
|
221
|
-
return 'browser open requested';
|
|
222
|
-
} catch (error) {
|
|
223
|
-
return `browser open failed: ${summarizeError(error)}`;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function probeTcp(host: string, port: number, timeoutMs = 750): Promise<boolean> {
|
|
228
|
-
return await new Promise<boolean>((resolve) => {
|
|
229
|
-
const socket = net.createConnection({ host: connectHostForBindHost(host), port });
|
|
230
|
-
const finish = (value: boolean) => {
|
|
231
|
-
socket.removeAllListeners();
|
|
232
|
-
socket.destroy();
|
|
233
|
-
resolve(value);
|
|
234
|
-
};
|
|
235
|
-
socket.setTimeout(timeoutMs);
|
|
236
|
-
socket.once('connect', () => finish(true));
|
|
237
|
-
socket.once('timeout', () => finish(false));
|
|
238
|
-
socket.once('error', () => finish(false));
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export async function withRuntimeServices<T>(
|
|
243
|
-
runtime: CliCommandRuntime,
|
|
244
|
-
fn: (services: RuntimeServices) => Promise<T> | T,
|
|
245
|
-
): Promise<T> {
|
|
246
|
-
const runtimeBus = new RuntimeEventBus();
|
|
247
|
-
const runtimeStore = createRuntimeStore();
|
|
248
|
-
const services = createRuntimeServices({
|
|
249
|
-
configManager: runtime.configManager,
|
|
250
|
-
runtimeBus,
|
|
251
|
-
runtimeStore,
|
|
252
|
-
workingDir: runtime.workingDirectory,
|
|
253
|
-
homeDirectory: runtime.homeDirectory,
|
|
254
|
-
});
|
|
255
|
-
services.providerRegistry.initModelLimits();
|
|
256
|
-
services.benchmarkStore.initBenchmarks();
|
|
257
|
-
services.providerRegistry.initCatalog();
|
|
258
|
-
try {
|
|
259
|
-
await services.providerRegistry.ready();
|
|
260
|
-
return await fn(services);
|
|
261
|
-
} finally {
|
|
262
|
-
services.providerRegistry.stopWatching();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function readAuthPaths(runtime: CliCommandRuntime) {
|
|
267
|
-
const shellPaths = createShellPathService({
|
|
268
|
-
workingDirectory: runtime.workingDirectory,
|
|
269
|
-
homeDirectory: runtime.homeDirectory,
|
|
270
|
-
});
|
|
271
|
-
const userStorePath = shellPaths.resolveUserPath('tui', 'auth-users.json');
|
|
272
|
-
const bootstrapCredentialPath = shellPaths.resolveUserPath('tui', 'auth-bootstrap.txt');
|
|
273
|
-
const operatorTokenPath = join(runtime.homeDirectory, '.goodvibes', 'daemon', 'operator-tokens.json');
|
|
274
|
-
return {
|
|
275
|
-
userStorePath,
|
|
276
|
-
userStorePresent: existsSync(userStorePath),
|
|
277
|
-
bootstrapCredentialPath,
|
|
278
|
-
bootstrapCredentialPresent: existsSync(bootstrapCredentialPath),
|
|
279
|
-
operatorTokenPath,
|
|
280
|
-
operatorTokenPresent: existsSync(operatorTokenPath),
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export async function runNonInteractiveAgent(runtime: CliCommandRuntime): Promise<number> {
|
|
285
|
-
const prompt = runtime.cli.flags.prompt ?? runtime.cli.positionals.join(' ').trim();
|
|
286
|
-
if (!prompt) {
|
|
287
|
-
console.error('Usage: goodvibes run|exec [prompt]');
|
|
288
|
-
return 2;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const outputFormat = runtime.cli.flags.outputFormat;
|
|
292
|
-
const ctx = await bootstrapRuntime(process.stdout, {
|
|
293
|
-
configManager: runtime.configManager,
|
|
294
|
-
workingDir: runtime.workingDirectory,
|
|
295
|
-
homeDirectory: runtime.homeDirectory,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const events: TurnEvent[] = [];
|
|
299
|
-
let finalResponse = '';
|
|
300
|
-
let finalError = '';
|
|
301
|
-
let finalStopReason = '';
|
|
302
|
-
let exitCode = 0;
|
|
303
|
-
|
|
304
|
-
const done = new Promise<void>((resolve) => {
|
|
305
|
-
const unsubs = [
|
|
306
|
-
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'STREAM_DELTA' }>>('STREAM_DELTA', ({ payload }) => {
|
|
307
|
-
events.push(payload);
|
|
308
|
-
if (outputFormat === 'stream-json') {
|
|
309
|
-
process.stdout.write(JSON.stringify({ type: payload.type, content: payload.content, accumulated: payload.accumulated }) + '\n');
|
|
310
|
-
}
|
|
311
|
-
}),
|
|
312
|
-
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_COMPLETED' }>>('TURN_COMPLETED', ({ payload }) => {
|
|
313
|
-
events.push(payload);
|
|
314
|
-
finalResponse = payload.response;
|
|
315
|
-
finalStopReason = payload.stopReason;
|
|
316
|
-
for (const unsub of unsubs) unsub();
|
|
317
|
-
resolve();
|
|
318
|
-
}),
|
|
319
|
-
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_ERROR' }>>('TURN_ERROR', ({ payload }) => {
|
|
320
|
-
events.push(payload);
|
|
321
|
-
finalError = payload.error;
|
|
322
|
-
finalStopReason = payload.stopReason;
|
|
323
|
-
exitCode = 1;
|
|
324
|
-
for (const unsub of unsubs) unsub();
|
|
325
|
-
resolve();
|
|
326
|
-
}),
|
|
327
|
-
ctx.runtimeBus.on<Extract<TurnEvent, { type: 'TURN_CANCEL' }>>('TURN_CANCEL', ({ payload }) => {
|
|
328
|
-
events.push(payload);
|
|
329
|
-
finalError = payload.reason ?? 'cancelled';
|
|
330
|
-
finalStopReason = payload.stopReason;
|
|
331
|
-
exitCode = 130;
|
|
332
|
-
for (const unsub of unsubs) unsub();
|
|
333
|
-
resolve();
|
|
334
|
-
}),
|
|
335
|
-
];
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
await ctx.orchestrator.handleUserInput(prompt);
|
|
340
|
-
await done;
|
|
341
|
-
if (outputFormat === 'json') {
|
|
342
|
-
process.stdout.write(JSON.stringify({
|
|
343
|
-
ok: exitCode === 0,
|
|
344
|
-
response: finalResponse,
|
|
345
|
-
error: finalError || undefined,
|
|
346
|
-
stopReason: finalStopReason,
|
|
347
|
-
sessionId: ctx.runtime.sessionId,
|
|
348
|
-
model: ctx.runtime.model,
|
|
349
|
-
provider: ctx.runtime.provider,
|
|
350
|
-
events: events.length,
|
|
351
|
-
}, null, 2) + '\n');
|
|
352
|
-
} else if (outputFormat !== 'stream-json') {
|
|
353
|
-
process.stdout.write((exitCode === 0 ? finalResponse : finalError) + '\n');
|
|
354
|
-
} else {
|
|
355
|
-
process.stdout.write(JSON.stringify({
|
|
356
|
-
type: exitCode === 0 ? 'TURN_COMPLETED' : 'TURN_ERROR',
|
|
357
|
-
ok: exitCode === 0,
|
|
358
|
-
response: finalResponse,
|
|
359
|
-
error: finalError || undefined,
|
|
360
|
-
stopReason: finalStopReason,
|
|
361
|
-
}) + '\n');
|
|
362
|
-
}
|
|
363
|
-
} finally {
|
|
364
|
-
const snapshot = ctx.conversation.toJSON() as Parameters<typeof ctx.shutdown>[0];
|
|
365
|
-
await ctx.shutdown(snapshot);
|
|
366
|
-
}
|
|
367
|
-
return exitCode;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
73
|
async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
371
74
|
return await withRuntimeServices(runtime, async (services) => {
|
|
372
75
|
const [sub = 'list', ...rest] = runtime.cli.commandArgs;
|
|
@@ -471,6 +174,40 @@ async function renderProviders(runtime: CliCommandRuntime): Promise<string> {
|
|
|
471
174
|
});
|
|
472
175
|
}
|
|
473
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Build the synthetic chain entry list for `goodvibes models chain` output.
|
|
179
|
+
* Pure transformation: accepts canonical model data and an optional lowercase filter key,
|
|
180
|
+
* returns a serializable array suitable for both JSON and text formatting.
|
|
181
|
+
*
|
|
182
|
+
* @internal exported for unit testing
|
|
183
|
+
*/
|
|
184
|
+
export function buildSyntheticChainEntries(
|
|
185
|
+
canonicalModels: readonly CanonicalModel[],
|
|
186
|
+
filterKey?: string,
|
|
187
|
+
): Array<{
|
|
188
|
+
id: string;
|
|
189
|
+
tier: string;
|
|
190
|
+
backendCount: number;
|
|
191
|
+
keyedBackendCount: number;
|
|
192
|
+
backends: Array<{ position: number; provider: string; model: string; registryKey: string }>;
|
|
193
|
+
}> {
|
|
194
|
+
const filtered = filterKey
|
|
195
|
+
? canonicalModels.filter((m) => m.id.toLowerCase().includes(filterKey))
|
|
196
|
+
: canonicalModels;
|
|
197
|
+
return filtered.map((m) => ({
|
|
198
|
+
id: m.id,
|
|
199
|
+
tier: m.tier,
|
|
200
|
+
backendCount: m.backendCount,
|
|
201
|
+
keyedBackendCount: m.keyedBackendCount,
|
|
202
|
+
backends: m.backends.map((b, idx) => ({
|
|
203
|
+
position: idx,
|
|
204
|
+
provider: b.providerName,
|
|
205
|
+
model: b.modelId,
|
|
206
|
+
registryKey: b.registryKey ?? `${b.providerName}:${b.modelId}`,
|
|
207
|
+
})),
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
474
211
|
async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
475
212
|
return await withRuntimeServices(runtime, async (services) => {
|
|
476
213
|
const [subOrFilter, ...rest] = runtime.cli.commandArgs;
|
|
@@ -544,23 +281,58 @@ async function renderModels(runtime: CliCommandRuntime): Promise<string> {
|
|
|
544
281
|
...recent.map((model) => ` ${model}`),
|
|
545
282
|
].join('\n'));
|
|
546
283
|
}
|
|
284
|
+
if (subOrFilter === 'chain' || subOrFilter === 'chains') {
|
|
285
|
+
// List synthetic model fallback ladders — backend composition for each synthetic model.
|
|
286
|
+
const canonicalModels = services.providerRegistry.getSyntheticCanonicalModels();
|
|
287
|
+
if (canonicalModels.length === 0) {
|
|
288
|
+
return formatJsonOrText(runtime.cli)([], 'No synthetic models found in the current catalog.');
|
|
289
|
+
}
|
|
290
|
+
const filterKey = rest[0]?.toLowerCase();
|
|
291
|
+
const filtered = filterKey
|
|
292
|
+
? canonicalModels.filter((m) => m.id.toLowerCase().includes(filterKey))
|
|
293
|
+
: canonicalModels;
|
|
294
|
+
const value = buildSyntheticChainEntries(filtered);
|
|
295
|
+
return formatJsonOrText(runtime.cli)(value, [
|
|
296
|
+
`GoodVibes synthetic model chains${filterKey ? ` (${filterKey})` : ''}`,
|
|
297
|
+
...value.flatMap((m) => [
|
|
298
|
+
` ${m.id} [${m.tier}] ${m.keyedBackendCount}/${m.backendCount} backends configured`,
|
|
299
|
+
...m.backends.map((b) => ` ${b.position}. ${b.provider}/${b.model}`),
|
|
300
|
+
]),
|
|
301
|
+
].join('\n'));
|
|
302
|
+
}
|
|
547
303
|
const filter = subOrFilter === 'list' ? rest[0]?.toLowerCase() : subOrFilter?.toLowerCase();
|
|
548
304
|
const models = services.providerRegistry
|
|
549
305
|
.getSelectableModels()
|
|
550
306
|
.filter((model) => !filter || model.provider.toLowerCase() === filter || model.registryKey.toLowerCase().includes(filter))
|
|
551
307
|
.slice(0, 200);
|
|
552
|
-
const value = models.map((model) =>
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
308
|
+
const value = models.map((model) => {
|
|
309
|
+
const synthInfo = model.provider === 'synthetic'
|
|
310
|
+
? services.providerRegistry.getSyntheticModelInfoFromCatalog(model.id)
|
|
311
|
+
: null;
|
|
312
|
+
return {
|
|
313
|
+
registryKey: model.registryKey,
|
|
314
|
+
provider: model.provider,
|
|
315
|
+
...classifyModelProvider(model.provider),
|
|
316
|
+
id: model.id,
|
|
317
|
+
displayName: model.displayName,
|
|
318
|
+
contextWindow: services.providerRegistry.getContextWindowForModel(model),
|
|
319
|
+
current: model.registryKey === current,
|
|
320
|
+
...(synthInfo !== null ? {
|
|
321
|
+
isSynthetic: true,
|
|
322
|
+
syntheticTier: synthInfo.tier,
|
|
323
|
+
syntheticBackends: synthInfo.backendCount,
|
|
324
|
+
syntheticConfiguredBackends: synthInfo.keyedBackendCount,
|
|
325
|
+
} : {}),
|
|
326
|
+
};
|
|
327
|
+
});
|
|
561
328
|
return formatJsonOrText(runtime.cli)(value, [
|
|
562
329
|
`GoodVibes models${filter ? ` (${filter})` : ''}`,
|
|
563
|
-
...value.map((model) =>
|
|
330
|
+
...value.map((model) => {
|
|
331
|
+
const synthLabel = model.isSynthetic
|
|
332
|
+
? ` [synthetic ${model.syntheticConfiguredBackends}/${model.syntheticBackends}p]`
|
|
333
|
+
: '';
|
|
334
|
+
return ` ${model.current ? '*' : ' '} ${model.registryKey.padEnd(42)} setup=${model.setupClass} ctx=${model.contextWindow.toLocaleString()}${synthLabel} ${model.displayName}`;
|
|
335
|
+
}),
|
|
564
336
|
].join('\n'));
|
|
565
337
|
});
|
|
566
338
|
}
|
|
@@ -15,6 +15,9 @@ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers'
|
|
|
15
15
|
import type { SystemMessageRouter } from './system-message-router';
|
|
16
16
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
17
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';
|
|
18
21
|
|
|
19
22
|
export interface AutoCompactDeps {
|
|
20
23
|
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
@@ -51,22 +54,52 @@ export async function maybeAutoCompact(deps: AutoCompactDeps): Promise<void> {
|
|
|
51
54
|
|
|
52
55
|
try {
|
|
53
56
|
logger.debug('auto-compact triggered', { usagePct, thresholdPct });
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
deps.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
};
|
|
60
83
|
await deps.conversation.compact(
|
|
61
84
|
deps.providerRegistry,
|
|
62
85
|
deps.model,
|
|
63
86
|
'auto',
|
|
64
87
|
deps.provider,
|
|
88
|
+
compactionCtx,
|
|
65
89
|
);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
103
|
} catch (err) {
|
|
71
104
|
logger.error('auto-compact failed', { error: summarizeError(err) });
|
|
72
105
|
deps.systemMessageRouter.routeSystemMessage(
|
|
@@ -11,10 +11,13 @@ import { LAYOUT } from '../renderer/layout.ts';
|
|
|
11
11
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
import { renderConversationCollapsedFragment, renderConversationEventLine } from '../renderer/conversation-surface.ts';
|
|
13
13
|
import { GLYPHS } from '../renderer/ui-primitives.ts';
|
|
14
|
-
import type { BlockMeta
|
|
14
|
+
import type { BlockMeta } from './conversation-types.ts';
|
|
15
|
+
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
16
|
import { parseDiffForApply } from '@pellux/goodvibes-sdk/platform/core';
|
|
16
17
|
import { extractUserDisplayText } from '@pellux/goodvibes-sdk/platform/core';
|
|
17
|
-
|
|
18
|
+
// SystemMessageKind imported from runtime directly to avoid cycle:
|
|
19
|
+
// conversation-rendering.ts → system-message-router.ts → conversation.ts → conversation-rendering.ts
|
|
20
|
+
import type { SystemMessageKind } from '@/runtime/index.ts';
|
|
18
21
|
|
|
19
22
|
const T = DARK_THEME;
|
|
20
23
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-types.ts — shared TUI extension types for ConversationManager.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from conversation.ts so that conversation-rendering.ts can import
|
|
5
|
+
* BlockMeta without creating a circular dependency:
|
|
6
|
+
* conversation.ts ↔ conversation-rendering.ts
|
|
7
|
+
*
|
|
8
|
+
* Both files import from this module; conversation.ts re-exports BlockMeta for
|
|
9
|
+
* backward compatibility of all existing importers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { BlockMeta as SdkBlockMeta } from '@pellux/goodvibes-sdk/platform/core';
|
|
13
|
+
|
|
14
|
+
/** TUI extends the SDK BlockMeta with rendering position fields. */
|
|
15
|
+
export interface BlockMeta extends SdkBlockMeta {
|
|
16
|
+
/** Index of this block (increments per renderable block). */
|
|
17
|
+
blockIndex: number;
|
|
18
|
+
/** First rendered line index in the history buffer. */
|
|
19
|
+
startLine: number;
|
|
20
|
+
/** Number of rendered lines (when not collapsed). */
|
|
21
|
+
lineCount: number;
|
|
22
|
+
/** Stable key for collapse state persistence across rebuilds (e.g. msg_N). */
|
|
23
|
+
collapseKey: string;
|
|
24
|
+
}
|
package/src/core/conversation.ts
CHANGED
|
@@ -5,11 +5,14 @@ import type { ToolCall, ToolResult } from '@pellux/goodvibes-sdk/platform/types'
|
|
|
5
5
|
import type { ProviderMessage, ContentPart } from '@pellux/goodvibes-sdk/platform/providers';
|
|
6
6
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
7
7
|
import type { TranscriptEventKind } from '@pellux/goodvibes-sdk/platform/core';
|
|
8
|
-
|
|
8
|
+
// SystemMessageKind imported from runtime directly to avoid cycle:
|
|
9
|
+
// conversation.ts → system-message-router.ts → conversation.ts
|
|
10
|
+
import type { SystemMessageKind } from '@/runtime/index.ts';
|
|
9
11
|
import {
|
|
10
12
|
ConversationManager as SdkConversationManager,
|
|
11
13
|
type BlockMeta as SdkBlockMeta,
|
|
12
14
|
} from '@pellux/goodvibes-sdk/platform/core';
|
|
15
|
+
import type { BlockMeta } from './conversation-types.ts';
|
|
13
16
|
import {
|
|
14
17
|
addConversationSplashScreen,
|
|
15
18
|
appendConversationMessages,
|
|
@@ -38,17 +41,9 @@ export type {
|
|
|
38
41
|
|
|
39
42
|
export type { SdkBlockMeta };
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
blockIndex: number;
|
|
45
|
-
/** First rendered line index in the history buffer. */
|
|
46
|
-
startLine: number;
|
|
47
|
-
/** Number of rendered lines (when not collapsed). */
|
|
48
|
-
lineCount: number;
|
|
49
|
-
/** Stable key for collapse state persistence across rebuilds (e.g. msg_N). */
|
|
50
|
-
collapseKey: string;
|
|
51
|
-
}
|
|
44
|
+
// BlockMeta is defined in ./conversation-types.ts to avoid a circular dep
|
|
45
|
+
// with conversation-rendering.ts; re-exported here for backward compatibility.
|
|
46
|
+
export type { BlockMeta };
|
|
52
47
|
|
|
53
48
|
// Import internal types needed for rendering helpers
|
|
54
49
|
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|