@pellux/goodvibes-tui 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- package/src/core/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- package/src/core/stream-event-wiring.ts +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +33 -11
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
|
@@ -5,6 +5,7 @@ import { readOnboardingRuntimeState } from './state.ts';
|
|
|
5
5
|
import type {
|
|
6
6
|
OnboardingAcknowledgementSnapshot,
|
|
7
7
|
OnboardingConfigSnapshot,
|
|
8
|
+
OnboardingLegacyDaemonSnapshot,
|
|
8
9
|
OnboardingProviderRoutingSnapshot,
|
|
9
10
|
OnboardingRuntimeDefaultsSnapshot,
|
|
10
11
|
OnboardingServiceState,
|
|
@@ -198,6 +199,10 @@ function buildFallbackAuthSnapshot(): LocalAuthSnapshot {
|
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
function buildFallbackLegacyDaemonSnapshot(): OnboardingLegacyDaemonSnapshot {
|
|
203
|
+
return { present: false, active: false, path: '' };
|
|
204
|
+
}
|
|
205
|
+
|
|
201
206
|
function buildConfiguredSurfaceKinds(
|
|
202
207
|
surfaces: OnboardingConfigSnapshot['surfaces'],
|
|
203
208
|
): string[] {
|
|
@@ -320,6 +325,7 @@ export async function collectOnboardingSnapshot(
|
|
|
320
325
|
secretRecordsResult,
|
|
321
326
|
surfaceResult,
|
|
322
327
|
providerAccountsResult,
|
|
328
|
+
legacyDaemonResult,
|
|
323
329
|
] = await Promise.all([
|
|
324
330
|
loadOptionalSnapshot(
|
|
325
331
|
'subscriptions-active',
|
|
@@ -357,6 +363,11 @@ export async function collectOnboardingSnapshot(
|
|
|
357
363
|
deps.providerAccounts ? () => deps.providerAccounts!.loadSnapshot() : undefined,
|
|
358
364
|
null,
|
|
359
365
|
),
|
|
366
|
+
loadOptionalSnapshot(
|
|
367
|
+
'legacy-daemon',
|
|
368
|
+
deps.legacyDaemon ? () => Promise.resolve(deps.legacyDaemon!.detect()) : undefined,
|
|
369
|
+
buildFallbackLegacyDaemonSnapshot(),
|
|
370
|
+
),
|
|
360
371
|
]);
|
|
361
372
|
|
|
362
373
|
const collectionIssues: OnboardingSnapshotCollectionIssue[] = [];
|
|
@@ -369,6 +380,7 @@ export async function collectOnboardingSnapshot(
|
|
|
369
380
|
if (secretRecordsResult.issue) collectionIssues.push(secretRecordsResult.issue);
|
|
370
381
|
if (surfaceResult.issue) collectionIssues.push(surfaceResult.issue);
|
|
371
382
|
if (providerAccountsResult.issue) collectionIssues.push(providerAccountsResult.issue);
|
|
383
|
+
if (legacyDaemonResult.issue) collectionIssues.push(legacyDaemonResult.issue);
|
|
372
384
|
|
|
373
385
|
return {
|
|
374
386
|
capturedAt,
|
|
@@ -391,7 +403,20 @@ export async function collectOnboardingSnapshot(
|
|
|
391
403
|
snapshot: authSnapshotResult.value,
|
|
392
404
|
},
|
|
393
405
|
bindSettings: {
|
|
394
|
-
|
|
406
|
+
// danger.daemon (removed Wave 6 — see docs/decisions/2026-07-05-daemon-by-default.md)
|
|
407
|
+
// used to gate the pre-Wave-2 opt-in daemon posture; this site read it RAW
|
|
408
|
+
// (not through resolveDaemonEnabled) so the wizard's network-mode
|
|
409
|
+
// classification tracked "did the user explicitly request the legacy
|
|
410
|
+
// dangerous posture" rather than "does the daemon run" (which has defaulted
|
|
411
|
+
// true unconditionally since Wave 2, and would misclassify every default,
|
|
412
|
+
// local-only install as server-mode if read here). Every realistic config
|
|
413
|
+
// already evaluated this to `false` (unset, or an explicit `danger.daemon:
|
|
414
|
+
// false` both did); the alias's removal migration preserves the one case
|
|
415
|
+
// that changes real daemon behavior (explicit `false` -> `daemon.enabled:
|
|
416
|
+
// false`) but does not resurrect a signal for "explicitly true" — that path
|
|
417
|
+
// was always a no-op for actual daemon operation. With the alias gone there
|
|
418
|
+
// is no signal left to read, so this is pinned at its steady-state value.
|
|
419
|
+
daemonEnabled: false,
|
|
395
420
|
httpListenerEnabled: Boolean(config.danger.httpListener),
|
|
396
421
|
controlPlane: config.controlPlane,
|
|
397
422
|
httpListener: config.httpListener,
|
|
@@ -402,6 +427,7 @@ export async function collectOnboardingSnapshot(
|
|
|
402
427
|
records: sortSurfaceRecords(surfaceResult.value),
|
|
403
428
|
},
|
|
404
429
|
providerAccounts: providerAccountsResult.value,
|
|
430
|
+
legacyDaemon: legacyDaemonResult.value,
|
|
405
431
|
collectionIssues,
|
|
406
432
|
};
|
|
407
433
|
}
|
|
@@ -125,6 +125,29 @@ export interface OnboardingSurfacesSnapshot {
|
|
|
125
125
|
readonly records: readonly OnboardingSurfaceRecord[];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* W4-D1 wizard wiring: read-only detection of a legacy `goodvibes-daemon.service`
|
|
130
|
+
* systemd unit (see `../legacy-daemon-migration.ts`'s `LegacyUnitInfo`), carried
|
|
131
|
+
* on the snapshot so the Network step can show the guided migration action only
|
|
132
|
+
* when there is actually something to migrate. Never implies anything was
|
|
133
|
+
* touched — detection only.
|
|
134
|
+
*/
|
|
135
|
+
export interface OnboardingLegacyDaemonSnapshot {
|
|
136
|
+
readonly present: boolean;
|
|
137
|
+
readonly active: boolean;
|
|
138
|
+
readonly path: string;
|
|
139
|
+
/**
|
|
140
|
+
* F2 follow-up: the unit name this tool actually manages on this host,
|
|
141
|
+
* resolved from the `service.serviceName` config key at snapshot-collection
|
|
142
|
+
* time (`resolveConfiguredServiceName`, `../legacy-daemon-migration.ts`) —
|
|
143
|
+
* so the wizard's detection banner names the real unit instead of the
|
|
144
|
+
* hardcoded default. Optional so snapshots built without config access
|
|
145
|
+
* (fallbacks, older fixtures) stay valid; readers fall back to
|
|
146
|
+
* `MANAGED_SERVICE_NAME`.
|
|
147
|
+
*/
|
|
148
|
+
readonly trackedServiceName?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
export interface OnboardingProviderAccountRecord {
|
|
129
152
|
readonly providerId: string;
|
|
130
153
|
readonly configured: boolean;
|
|
@@ -170,7 +193,8 @@ export type OnboardingSnapshotCollectionIssueArea =
|
|
|
170
193
|
| 'secrets-records'
|
|
171
194
|
| 'surfaces'
|
|
172
195
|
| 'provider-accounts'
|
|
173
|
-
| 'acknowledgements'
|
|
196
|
+
| 'acknowledgements'
|
|
197
|
+
| 'legacy-daemon';
|
|
174
198
|
|
|
175
199
|
export interface OnboardingSnapshotCollectionIssue {
|
|
176
200
|
readonly area: OnboardingSnapshotCollectionIssueArea;
|
|
@@ -190,6 +214,7 @@ export interface OnboardingSnapshotState {
|
|
|
190
214
|
readonly bindSettings: OnboardingBindSettingsSnapshot;
|
|
191
215
|
readonly surfaces: OnboardingSurfacesSnapshot;
|
|
192
216
|
readonly providerAccounts: OnboardingProviderAccountsSnapshot | null;
|
|
217
|
+
readonly legacyDaemon: OnboardingLegacyDaemonSnapshot;
|
|
193
218
|
readonly collectionIssues: readonly OnboardingSnapshotCollectionIssue[];
|
|
194
219
|
}
|
|
195
220
|
|
|
@@ -377,6 +402,10 @@ export interface OnboardingProviderAccountReadHelper {
|
|
|
377
402
|
loadSnapshot(): Promise<OnboardingProviderAccountsSnapshot>;
|
|
378
403
|
}
|
|
379
404
|
|
|
405
|
+
export interface OnboardingLegacyDaemonReadHelper {
|
|
406
|
+
detect(): OnboardingLegacyDaemonSnapshot | Promise<OnboardingLegacyDaemonSnapshot>;
|
|
407
|
+
}
|
|
408
|
+
|
|
380
409
|
export type OnboardingShellPaths = Pick<
|
|
381
410
|
ShellPathService,
|
|
382
411
|
'workingDirectory' | 'resolveProjectPath' | 'resolveUserPath'
|
|
@@ -391,6 +420,7 @@ export interface OnboardingSnapshotDependencies {
|
|
|
391
420
|
readonly services: Pick<ServiceInspectionQuery, 'getAll' | 'inspect'>;
|
|
392
421
|
readonly surfaces?: OnboardingSurfaceReadHelper;
|
|
393
422
|
readonly providerAccounts?: OnboardingProviderAccountReadHelper;
|
|
423
|
+
readonly legacyDaemon?: OnboardingLegacyDaemonReadHelper;
|
|
394
424
|
readonly shellPaths?: OnboardingShellPaths;
|
|
395
425
|
readonly acknowledgementScope?: OnboardingStateScope;
|
|
396
426
|
}
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
* the new path will be inspected on the next daemon boot.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
18
|
+
import type { CompanionPairingResult } from '@pellux/goodvibes-sdk/platform/pairing';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Return the list of absolute operator-tokens.json paths the TUI may have written
|
|
@@ -26,3 +30,80 @@ export function workspaceOperatorTokenCandidates(workingDirectory: string): read
|
|
|
26
30
|
join(workingDirectory, '.goodvibes', 'tui', 'operator-tokens.json'),
|
|
27
31
|
];
|
|
28
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* F1 (adopt-an-already-running-external-daemon): resolve the operator/companion
|
|
36
|
+
* token this TUI process uses to authenticate with its daemon, honoring
|
|
37
|
+
* `GOODVIBES_DAEMON_TOKEN` as a non-interactive override.
|
|
38
|
+
*
|
|
39
|
+
* Without this override, the only way to make a TUI instance adopt an
|
|
40
|
+
* already-running external daemon that used an out-of-band or explicit token
|
|
41
|
+
* (rather than one derived from `getOrCreateCompanionToken` under a *shared*
|
|
42
|
+
* home directory) was to hand-write `<daemonHomeDir>/operator-tokens.json`
|
|
43
|
+
* before startup — there was no env var or CLI flag equivalent.
|
|
44
|
+
*
|
|
45
|
+
* `GOODVIBES_DAEMON_TOKEN` is already the documented env var `bin/goodvibes-daemon`
|
|
46
|
+
* honors for its own bearer token (see `src/daemon/cli.ts`'s `readDaemonCliTokens`)
|
|
47
|
+
* and the one `src/verification/live-verifier.ts`'s `readDaemonToken` already falls
|
|
48
|
+
* back to when probing a daemon's HTTP surface from the outside. Reusing the same
|
|
49
|
+
* name here means one env var configures both sides of an adopted-daemon setup —
|
|
50
|
+
* start the daemon with `GOODVIBES_DAEMON_TOKEN=<token>` and point the TUI at it
|
|
51
|
+
* with the same `GOODVIBES_DAEMON_TOKEN=<token>` plus
|
|
52
|
+
* `--config controlPlane.host=<host> --config controlPlane.port=<port>` — instead
|
|
53
|
+
* of introducing a second, TUI-only flag.
|
|
54
|
+
*
|
|
55
|
+
* When the override is set and does not already match the on-disk record, the
|
|
56
|
+
* file is rewritten so the override becomes durable for this home directory
|
|
57
|
+
* (an existing peerId is kept when present, so companion-pairing identity does
|
|
58
|
+
* not needlessly churn). Falls back to the existing `getOrCreateCompanionToken`
|
|
59
|
+
* behavior — read the existing file, or mint a fresh random token — when the
|
|
60
|
+
* override is unset.
|
|
61
|
+
*
|
|
62
|
+
* @param explicitToken - Takes precedence over `GOODVIBES_DAEMON_TOKEN` when
|
|
63
|
+
* provided. Used by the onboarding wizard's "connect to an existing daemon"
|
|
64
|
+
* action (a pasted token) so it shares this exact persistence logic with the
|
|
65
|
+
* env-var path instead of duplicating it.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDaemonCompanionToken(daemonHomeDir: string, explicitToken?: string): CompanionPairingResult {
|
|
68
|
+
const override = explicitToken?.trim() || process.env.GOODVIBES_DAEMON_TOKEN?.trim();
|
|
69
|
+
if (!override) return getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
70
|
+
|
|
71
|
+
const tokenPath = join(daemonHomeDir, 'operator-tokens.json');
|
|
72
|
+
let existingPeerId: string | undefined;
|
|
73
|
+
let existingCreatedAt: number | undefined;
|
|
74
|
+
if (existsSync(tokenPath)) {
|
|
75
|
+
try {
|
|
76
|
+
const record = JSON.parse(readFileSync(tokenPath, 'utf-8')) as {
|
|
77
|
+
token?: unknown;
|
|
78
|
+
peerId?: unknown;
|
|
79
|
+
createdAt?: unknown;
|
|
80
|
+
};
|
|
81
|
+
if (typeof record.token === 'string' && record.token === override) {
|
|
82
|
+
return {
|
|
83
|
+
token: override,
|
|
84
|
+
peerId: typeof record.peerId === 'string' ? record.peerId : randomBytes(12).toString('hex'),
|
|
85
|
+
createdAt: typeof record.createdAt === 'number' ? record.createdAt : Date.now(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (typeof record.peerId === 'string') existingPeerId = record.peerId;
|
|
89
|
+
if (typeof record.createdAt === 'number') existingCreatedAt = record.createdAt;
|
|
90
|
+
} catch {
|
|
91
|
+
// Malformed on-disk record — fall through and rewrite it with the override.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const record: CompanionPairingResult = {
|
|
96
|
+
token: override,
|
|
97
|
+
peerId: existingPeerId ?? randomBytes(12).toString('hex'),
|
|
98
|
+
createdAt: existingCreatedAt ?? Date.now(),
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
mkdirSync(dirname(tokenPath), { recursive: true });
|
|
102
|
+
writeFileSync(tokenPath, JSON.stringify(record, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
103
|
+
chmodSync(tokenPath, 0o600);
|
|
104
|
+
} catch {
|
|
105
|
+
// Best-effort persistence — the override still applies for this process
|
|
106
|
+
// even if the file could not be written (e.g. a read-only home directory).
|
|
107
|
+
}
|
|
108
|
+
return record;
|
|
109
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { OrchestratorCoreServices } from '@pellux/goodvibes-sdk/platform/co
|
|
|
2
2
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
3
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
4
4
|
import type { RuntimeServices } from './services.ts';
|
|
5
|
+
import { isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
5
6
|
|
|
6
7
|
/** The slice of the runtime services bag the shared orchestrator payload draws from. */
|
|
7
8
|
export type OrchestratorCoreServicesSource = Pick<
|
|
@@ -12,6 +13,8 @@ export type OrchestratorCoreServicesSource = Pick<
|
|
|
12
13
|
| 'sessionLineageTracker'
|
|
13
14
|
| 'idempotencyStore'
|
|
14
15
|
| 'memoryRegistry'
|
|
16
|
+
| 'codeIndexStore'
|
|
17
|
+
| 'codeIndexReindexScheduler'
|
|
15
18
|
>;
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -45,5 +48,12 @@ export function buildSharedOrchestratorCoreServices(input: {
|
|
|
45
48
|
sessionLineageTracker: services.sessionLineageTracker,
|
|
46
49
|
idempotencyStore: services.idempotencyStore,
|
|
47
50
|
memoryRegistry: services.memoryRegistry,
|
|
51
|
+
// Wave-5 Stage B — main-session code auto-injection + tool-site reindex. Injection is
|
|
52
|
+
// additionally gated by the default-off `agent-passive-code-injection` flag inside the
|
|
53
|
+
// SDK; here we supply the source, the live storage.codeIndexEnabled predicate, and the
|
|
54
|
+
// reindex scheduler.
|
|
55
|
+
codeIndex: services.codeIndexStore,
|
|
56
|
+
isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager),
|
|
57
|
+
codeIndexReindexScheduler: services.codeIndexReindexScheduler,
|
|
48
58
|
};
|
|
49
59
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resume-notice.ts — the boot-time "previous session found" transcript notice.
|
|
3
|
+
*
|
|
4
|
+
* UX-D item 1: a supervision-journey audit of 1.7.0 found that the TUI
|
|
5
|
+
* accumulates rich resumable state on disk (a saved conversation, workspace
|
|
6
|
+
* checkpoints, WRFC chain history) but never surfaces any of it at startup —
|
|
7
|
+
* an operator has no way to know it exists short of already knowing the
|
|
8
|
+
* right command. This module builds ONE compact, honest system-message block
|
|
9
|
+
* printed after the splash and before the first prompt, summarizing exactly
|
|
10
|
+
* what real state exists and how to reach it.
|
|
11
|
+
*
|
|
12
|
+
* Honesty constraints (verified against the actual runtime, not assumed):
|
|
13
|
+
* - There is no bare `/resume` command and no `/sessions` command (a
|
|
14
|
+
* pre-existing typo already lives in the splash's own hint at
|
|
15
|
+
* utils/splash-lines.ts — out of scope here, splash stays byte-identical).
|
|
16
|
+
* The real, working command is `/session resume <id|name>` — it always
|
|
17
|
+
* requires an explicit target. So this notice advertises
|
|
18
|
+
* `/session resume <id>` with the real last-session id substituted in,
|
|
19
|
+
* never a bare `/resume`.
|
|
20
|
+
* - `/checkpoints` works with zero arguments and behaves correctly at any
|
|
21
|
+
* checkpoint count (including zero) — advertised whenever the checkpoint
|
|
22
|
+
* manager is available in this session.
|
|
23
|
+
* - `/recall` (memory) is only advertised when the memory API is actually
|
|
24
|
+
* wired up in this session (context.clients?.knowledgeApi?.memory) —
|
|
25
|
+
* some runtimes don't have it, and claiming it works there would not be
|
|
26
|
+
* honest.
|
|
27
|
+
* - Every clause is independently gated on real data: a claim about
|
|
28
|
+
* checkpoints/chain history is only made when that data is known; "no
|
|
29
|
+
* chain history" means no chain clause is printed, not a fabricated one.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
33
|
+
import type { WrfcChain } from '@pellux/goodvibes-sdk/platform/agents';
|
|
34
|
+
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
35
|
+
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
36
|
+
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
37
|
+
|
|
38
|
+
// ─── Chain outcome ───────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Honest, human-facing outcome of a WRFC chain, derived from real chain
|
|
42
|
+
* state rather than guessed. `chain.state` alone cannot distinguish a
|
|
43
|
+
* user-cancelled chain from an ordinary review/gate failure. The SDK now
|
|
44
|
+
* records that distinction first-class as `chain.failureKind` ('cancelled'
|
|
45
|
+
* vs 'transport'/'other'), set by cancelChain()/failChain(), so that field
|
|
46
|
+
* is the primary source of truth. Snapshots persisted before the field
|
|
47
|
+
* existed lack it; for those we fall back to the owner-decision log, where
|
|
48
|
+
* cancelChain() also records a `chain_cancelled` decision. A chain still
|
|
49
|
+
* non-terminal after rehydrate's zombie-reap check (see wrfc-persistence.ts)
|
|
50
|
+
* is reported as 'interrupted' — re-imported and live again, not history.
|
|
51
|
+
*/
|
|
52
|
+
export type ChainOutcome = 'passed' | 'failed' | 'cancelled' | 'interrupted';
|
|
53
|
+
|
|
54
|
+
/** Terminal WRFC states (mirrors wrfc-persistence.ts's own TERMINAL_STATES). */
|
|
55
|
+
function isTerminalState(state: WrfcChain['state']): boolean {
|
|
56
|
+
return state === 'passed' || state === 'failed';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function describeChainOutcome(chain: WrfcChain): ChainOutcome {
|
|
60
|
+
if (!isTerminalState(chain.state)) return 'interrupted';
|
|
61
|
+
if (chain.state === 'passed') return 'passed';
|
|
62
|
+
// Primary: the SDK's first-class failureKind, authoritative for chains
|
|
63
|
+
// failed/cancelled under the current SDK.
|
|
64
|
+
if (chain.failureKind === 'cancelled') return 'cancelled';
|
|
65
|
+
// Fallback for pre-failureKind snapshots: consult the owner-decision log,
|
|
66
|
+
// where cancelChain() records a chain_cancelled decision.
|
|
67
|
+
if (chain.failureKind === undefined) {
|
|
68
|
+
const lastAction = chain.ownerDecisions.length > 0 ? chain.ownerDecisions[chain.ownerDecisions.length - 1]?.action : undefined;
|
|
69
|
+
if (lastAction === 'chain_cancelled') return 'cancelled';
|
|
70
|
+
}
|
|
71
|
+
return 'failed';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Pick the most recently completed (or, if still interrupted, most recently created) chain from a set. Null if the set is empty. */
|
|
75
|
+
export function mostRecentChain(chains: readonly WrfcChain[]): WrfcChain | null {
|
|
76
|
+
if (chains.length === 0) return null;
|
|
77
|
+
return [...chains].sort((a, b) => (b.completedAt ?? b.createdAt ?? 0) - (a.completedAt ?? a.createdAt ?? 0))[0]!;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Notice text ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export interface ResumeNoticeFacts {
|
|
83
|
+
/** Number of user turns in the last saved session. Null when there is no prior session (or it could not be read). */
|
|
84
|
+
readonly turnCount: number | null;
|
|
85
|
+
/** Session id of the last saved session — needed to build a truthful, directly-runnable resume hint. Null when there is no prior session. */
|
|
86
|
+
readonly lastSessionId: string | null;
|
|
87
|
+
/** Number of workspace checkpoints. Null when the checkpoint manager is unavailable in this session (not the same as zero). */
|
|
88
|
+
readonly checkpointCount: number | null;
|
|
89
|
+
/** Outcome of the most recently known WRFC chain. Null when there is no chain history at all. */
|
|
90
|
+
readonly lastChainOutcome: ChainOutcome | null;
|
|
91
|
+
/** Whether /recall (memory) is wired up in this session. */
|
|
92
|
+
readonly memoryAvailable: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function plural(n: number, word: string): string {
|
|
96
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the boot resume notice text, or null when there is nothing to
|
|
101
|
+
* report (no prior session, no checkpoints ever taken, no chain history —
|
|
102
|
+
* a clean/new working directory prints nothing, respecting quiet startup).
|
|
103
|
+
*/
|
|
104
|
+
export function buildResumeNotice(facts: ResumeNoticeFacts): string | null {
|
|
105
|
+
const hasSession = facts.lastSessionId !== null && facts.turnCount !== null;
|
|
106
|
+
const checkpointsKnown = facts.checkpointCount !== null;
|
|
107
|
+
const checkpointCount = facts.checkpointCount ?? 0;
|
|
108
|
+
const hasCheckpoints = checkpointsKnown && checkpointCount > 0;
|
|
109
|
+
const hasChainHistory = facts.lastChainOutcome !== null;
|
|
110
|
+
|
|
111
|
+
if (!hasSession && !hasCheckpoints && !hasChainHistory) return null;
|
|
112
|
+
|
|
113
|
+
const summary: string[] = [];
|
|
114
|
+
if (hasSession) summary.push(plural(facts.turnCount!, 'turn'));
|
|
115
|
+
// Show the checkpoint count whenever it's knowable and there's a reason to
|
|
116
|
+
// (anchored to an existing session, or checkpoints genuinely exist even
|
|
117
|
+
// without one) — never guessed when the manager is unavailable.
|
|
118
|
+
if (checkpointsKnown && (hasSession || hasCheckpoints)) summary.push(plural(checkpointCount, 'checkpoint'));
|
|
119
|
+
if (hasChainHistory) summary.push(`last chain: ${facts.lastChainOutcome}`);
|
|
120
|
+
|
|
121
|
+
const lead = hasSession ? 'Previous session found' : 'Workspace history found';
|
|
122
|
+
let notice = `${lead}: ${summary.join(', ')}`;
|
|
123
|
+
|
|
124
|
+
const hints: string[] = [];
|
|
125
|
+
// No bare `/resume` exists — /session resume always requires a target.
|
|
126
|
+
if (hasSession) hints.push(`/session resume ${facts.lastSessionId} to continue`);
|
|
127
|
+
if (hasCheckpoints) hints.push('/checkpoints to browse');
|
|
128
|
+
if (facts.memoryAvailable) hints.push('/recall for memory');
|
|
129
|
+
if (hints.length > 0) notice += ` — ${hints.join(' · ')}`;
|
|
130
|
+
|
|
131
|
+
return notice;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Fact gathering (I/O) ────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
export interface ResumeNoticeDeps {
|
|
137
|
+
readonly workingDirectory: string;
|
|
138
|
+
readonly homeDirectory: string;
|
|
139
|
+
/** Surface root used by session persistence — the TUI always uses 'tui'. */
|
|
140
|
+
readonly surfaceRoot: string;
|
|
141
|
+
/** Only `load()` is needed — kept narrow for testability. */
|
|
142
|
+
readonly sessionManager: Pick<SessionManager, 'load'>;
|
|
143
|
+
/** Undefined when checkpoints are not wired up in this session at all. Only `list()` is needed. */
|
|
144
|
+
readonly checkpointManager: Pick<WorkspaceCheckpointManager, 'list'> | undefined;
|
|
145
|
+
/** The full known-chain set from WrfcPersistence.knownChains, gathered post-rehydrate. */
|
|
146
|
+
readonly chainHistory: readonly WrfcChain[];
|
|
147
|
+
readonly memoryAvailable: boolean;
|
|
148
|
+
readonly router: Pick<SystemMessageRouter, 'high'>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Read the last session's real turn count from its saved JSONL file. A user
|
|
153
|
+
* turn is one stored message with role 'user' — the number of times the
|
|
154
|
+
* operator spoke, which is what "N turns" means to a human reading the
|
|
155
|
+
* notice (as opposed to a raw message count, which double-counts replies).
|
|
156
|
+
* Returns null when there is no last-session pointer, or the pointed-to
|
|
157
|
+
* session file is missing/corrupt — never a claim about a session that
|
|
158
|
+
* cannot actually be resumed.
|
|
159
|
+
*/
|
|
160
|
+
function readLastSessionTurns(deps: Pick<ResumeNoticeDeps, 'workingDirectory' | 'homeDirectory' | 'surfaceRoot' | 'sessionManager'>): { turnCount: number; lastSessionId: string } | null {
|
|
161
|
+
const lastSessionId = readLastSessionPointer({
|
|
162
|
+
workingDirectory: deps.workingDirectory,
|
|
163
|
+
homeDirectory: deps.homeDirectory,
|
|
164
|
+
surfaceRoot: deps.surfaceRoot,
|
|
165
|
+
});
|
|
166
|
+
if (!lastSessionId) return null;
|
|
167
|
+
try {
|
|
168
|
+
const { messages } = deps.sessionManager.load(lastSessionId);
|
|
169
|
+
const turnCount = messages.filter((m) => (m as { role?: unknown }).role === 'user').length;
|
|
170
|
+
return { turnCount, lastSessionId };
|
|
171
|
+
} catch {
|
|
172
|
+
// Pointer file present but the session it points to is gone/corrupt —
|
|
173
|
+
// there is nothing truthful to resume.
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function readCheckpointCount(mgr: ResumeNoticeDeps['checkpointManager']): Promise<number | null> {
|
|
179
|
+
if (!mgr) return null;
|
|
180
|
+
try {
|
|
181
|
+
return (await mgr.list()).length;
|
|
182
|
+
} catch {
|
|
183
|
+
// Checkpoint manager present but its cached init() rejection makes every
|
|
184
|
+
// call fail forever (see services.ts) — treat as "unknown", not zero.
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Gather real facts from disk/services and, if there is anything to report,
|
|
191
|
+
* print ONE compact system message via `deps.router.high`. No-op (and no
|
|
192
|
+
* message) when there is no prior session, no checkpoints, and no chain
|
|
193
|
+
* history — a fresh working directory stays quiet.
|
|
194
|
+
*/
|
|
195
|
+
export async function announceResumeState(deps: ResumeNoticeDeps): Promise<void> {
|
|
196
|
+
const session = readLastSessionTurns(deps);
|
|
197
|
+
const checkpointCount = await readCheckpointCount(deps.checkpointManager);
|
|
198
|
+
const lastChain = mostRecentChain(deps.chainHistory);
|
|
199
|
+
|
|
200
|
+
const notice = buildResumeNotice({
|
|
201
|
+
turnCount: session?.turnCount ?? null,
|
|
202
|
+
lastSessionId: session?.lastSessionId ?? null,
|
|
203
|
+
checkpointCount,
|
|
204
|
+
lastChainOutcome: lastChain ? describeChainOutcome(lastChain) : null,
|
|
205
|
+
memoryAvailable: deps.memoryAvailable,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (notice) deps.router.high(notice);
|
|
209
|
+
}
|
package/src/runtime/services.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { MediaProviderRegistry, ensureBuiltinMediaProviders } from '@pellux/good
|
|
|
23
23
|
import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
|
|
24
24
|
import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
25
25
|
import { AgentManager, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
|
|
26
|
-
import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, type CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
|
|
26
|
+
import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
|
|
27
27
|
import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
28
28
|
import type { RuntimeEventBus } from '@/runtime/index.ts';
|
|
29
29
|
import { createDomainDispatch } from './store/index.ts';
|
|
@@ -58,7 +58,7 @@ import { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
|
58
58
|
import { createProcessRegistry, type ProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
59
59
|
import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
|
|
60
60
|
import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
|
|
61
|
-
import { codeIndexDbPath, createCodeIndexServices } from './code-index-services.ts';
|
|
61
|
+
import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
62
62
|
import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
63
63
|
import {
|
|
64
64
|
registerDaemonHandlers,
|
|
@@ -227,6 +227,7 @@ export interface RuntimeServices {
|
|
|
227
227
|
readonly workstreamCommands: WorkstreamCommandService;
|
|
228
228
|
/** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
|
|
229
229
|
readonly codeIndexStore: CodeIndexStore;
|
|
230
|
+
readonly codeIndexReindexScheduler: CodeIndexReindexScheduler; // Wave-5 Stage B tool-site reindex
|
|
230
231
|
/** W2.1/W2.2: unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel. */
|
|
231
232
|
readonly processRegistry: ProcessRegistry;
|
|
232
233
|
readonly modeManager: ModeManager;
|
|
@@ -386,7 +387,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
386
387
|
});
|
|
387
388
|
const artifactStore = new ArtifactStore({ configManager });
|
|
388
389
|
const memoryEmbeddingRegistry = new MemoryEmbeddingProviderRegistry({ configManager });
|
|
389
|
-
|
|
390
|
+
// W6-C2 (E6): open the ONE home-scoped canonical store; legacy per-project TUI memory folds in at boot (foldTuiLegacyMemory).
|
|
391
|
+
const memoryDbPath = resolveCanonicalMemoryDbPath(homeDirectory);
|
|
390
392
|
const memoryStore = new MemoryStore(memoryDbPath, {
|
|
391
393
|
embeddingRegistry: memoryEmbeddingRegistry,
|
|
392
394
|
});
|
|
@@ -603,7 +605,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
603
605
|
// Wave 5 (wo804): repo source-tree code index, sharing memoryEmbeddingRegistry
|
|
604
606
|
// with MemoryStore above. Auto-build is config-gated (default off) — see
|
|
605
607
|
// code-index-services.ts's header doc.
|
|
606
|
-
const { codeIndexStore } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
|
|
608
|
+
const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
|
|
609
|
+
const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Wave-5 Stage B seam (agent here; main via orchestrator-core-services.ts)
|
|
607
610
|
// W2.1/W2.2: one shared process registry aggregating the managers above —
|
|
608
611
|
// the Fleet panel (panels/fleet-read-model.ts) is its first consumer.
|
|
609
612
|
// Constructed once here (not per-consumer) so the coalesced tick and the
|
|
@@ -678,6 +681,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
678
681
|
remoteRunnerRegistry,
|
|
679
682
|
knowledgeService,
|
|
680
683
|
memoryRegistry,
|
|
684
|
+
...codeInjectionOrchestratorDeps, // Wave-5 Stage B: agent-run code injection + tool-site reindex
|
|
681
685
|
archetypeLoader,
|
|
682
686
|
configManager,
|
|
683
687
|
providerRegistry,
|
|
@@ -779,16 +783,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
779
783
|
orchestrationEngine,
|
|
780
784
|
workstreamCommands,
|
|
781
785
|
codeIndexStore,
|
|
786
|
+
codeIndexReindexScheduler,
|
|
782
787
|
processRegistry,
|
|
783
788
|
modeManager,
|
|
784
789
|
fileUndoManager,
|
|
785
790
|
workspaceCheckpointManager,
|
|
786
791
|
integrationHelpers,
|
|
787
792
|
async rerootStores(newWorkingDir: string): Promise<void> {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
//
|
|
791
|
-
// tree, or it keeps pointing at the old working directory.
|
|
793
|
+
// W6-C2 (E6): the memory store is the home-scoped canonical cross-surface store and
|
|
794
|
+
// deliberately does NOT reroot per-project (that would re-silo memory, the E6
|
|
795
|
+
// regression). Only working-tree-bound stores (code index, project index) reroot.
|
|
792
796
|
await codeIndexStore.reroot(newWorkingDir, codeIndexDbPath(newWorkingDir));
|
|
793
797
|
await projectIndex.reroot(newWorkingDir);
|
|
794
798
|
},
|