@pellux/goodvibes-tui 1.9.1 → 1.10.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 +36 -5
- package/README.md +1 -1
- package/docs/foundation-artifacts/knowledge-graphql.graphql +5 -0
- package/docs/foundation-artifacts/knowledge-store.sql +33 -0
- package/docs/foundation-artifacts/operator-contract.json +4026 -1376
- package/package.json +2 -2
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-wiring.ts +10 -2
- package/src/cli/management-utils.ts +4 -0
- package/src/config/credential-availability.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/core/turn-cancellation.ts +1 -1
- package/src/daemon/cli.ts +4 -4
- package/src/daemon/service-commands.ts +8 -8
- package/src/export/cost-utils.ts +9 -1
- package/src/input/command-registry.ts +16 -5
- package/src/input/commands/config.ts +1 -1
- package/src/input/commands/control-room-runtime.ts +12 -5
- package/src/input/commands/incident-runtime.ts +2 -2
- package/src/input/commands/memory.ts +12 -12
- package/src/input/commands/planning-runtime.ts +1 -1
- package/src/input/commands/recall-bundle.ts +8 -8
- package/src/input/commands/recall-capture.ts +3 -3
- package/src/input/commands/recall-query.ts +41 -15
- package/src/input/commands/recall-review.ts +12 -12
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/feed-context-factory.ts +1 -1
- package/src/input/handler-content-actions.ts +2 -2
- package/src/input/handler-feed-routes.ts +2 -2
- package/src/input/handler-feed.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +3 -3
- package/src/input/handler-picker-routes.ts +2 -2
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/model-picker.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-apply.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +3 -3
- package/src/input/onboarding/onboarding-wizard-steps.ts +1 -1
- package/src/input/session-picker-modal.ts +1 -1
- package/src/input/settings-modal-data.ts +56 -2
- package/src/main.ts +8 -6
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/operations.ts +4 -4
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/cost-tracker-panel.ts +2 -2
- package/src/panels/fleet-panel-format.ts +1 -1
- package/src/panels/fleet-panel.ts +15 -15
- package/src/panels/fleet-read-model.ts +11 -11
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +2 -2
- package/src/panels/fleet-tabs.ts +4 -4
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/modals/memory-modal.ts +77 -16
- package/src/panels/types.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/fleet-tab-strip.ts +2 -2
- package/src/renderer/footer-tips.ts +1 -1
- package/src/renderer/model-workspace.ts +1 -1
- package/src/renderer/session-picker-modal.ts +4 -4
- package/src/renderer/shell-surface.ts +1 -1
- package/src/renderer/status-glyphs.ts +3 -3
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +2 -2
- package/src/renderer/turn-injection.ts +3 -3
- package/src/renderer/ui-factory.ts +7 -7
- package/src/renderer/ui-primitives.ts +3 -3
- package/src/runtime/bootstrap-command-context.ts +4 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -1
- package/src/runtime/bootstrap-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +18 -7
- package/src/runtime/bootstrap.ts +22 -22
- package/src/runtime/code-index-services.ts +2 -2
- package/src/runtime/index.ts +11 -3
- package/src/runtime/legacy-daemon-migration.ts +9 -9
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/memory-spine-transport.ts +289 -0
- package/src/runtime/onboarding/snapshot.ts +3 -3
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/operator-token-cleanup.ts +2 -2
- package/src/runtime/orchestrator-core-services.ts +29 -4
- package/src/runtime/process-lifecycle.ts +19 -5
- package/src/runtime/services.ts +27 -28
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-spine-transport.ts +2 -2
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/workstream-services.ts +1 -1
- package/src/runtime/wrfc-persistence.ts +2 -2
- package/src/shell/ui-openers.ts +2 -2
- package/src/version.ts +1 -1
- package/src/audio/spoken-turn-controller.ts +0 -243
- package/src/audio/text-chunker.ts +0 -110
|
@@ -4,11 +4,11 @@ import type { CommandContext } from '../command-registry.ts';
|
|
|
4
4
|
import type { MemoryBundle, MemorySearchFilter } from '@pellux/goodvibes-sdk/platform/state';
|
|
5
5
|
import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope, resolveBundlePath } from './recall-shared.ts';
|
|
6
6
|
import { requireShellPaths } from './runtime-services.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { getMemorySpine } from './recall-query.ts';
|
|
8
8
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
9
|
|
|
10
|
-
export function handleRecallExport(args: string[], context: CommandContext): void {
|
|
11
|
-
const memory =
|
|
10
|
+
export async function handleRecallExport(args: string[], context: CommandContext): Promise<void> {
|
|
11
|
+
const memory = getMemorySpine(context);
|
|
12
12
|
if (!memory) {
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
@@ -40,7 +40,7 @@ export function handleRecallExport(args: string[], context: CommandContext): voi
|
|
|
40
40
|
filter.cls = cls;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const bundle = memory.exportBundle(filter);
|
|
43
|
+
const bundle = await memory.exportBundle(filter);
|
|
44
44
|
const targetPath = resolveBundlePath(pathArg, requireShellPaths(context));
|
|
45
45
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
46
46
|
writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
|
|
@@ -48,7 +48,7 @@ export function handleRecallExport(args: string[], context: CommandContext): voi
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function handleRecallImport(args: string[], context: CommandContext): Promise<void> {
|
|
51
|
-
const memory =
|
|
51
|
+
const memory = getMemorySpine(context);
|
|
52
52
|
if (!memory) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
@@ -84,8 +84,8 @@ function inspectBundle(bundle: MemoryBundle): string {
|
|
|
84
84
|
].join('\n');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function handleRecallHandoffExport(args: string[], context: CommandContext): void {
|
|
88
|
-
const memory =
|
|
87
|
+
export async function handleRecallHandoffExport(args: string[], context: CommandContext): Promise<void> {
|
|
88
|
+
const memory = getMemorySpine(context);
|
|
89
89
|
if (!memory) {
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
@@ -100,7 +100,7 @@ export function handleRecallHandoffExport(args: string[], context: CommandContex
|
|
|
100
100
|
context.print(`[recall] Unknown scope "${scopeRaw ?? ''}". Valid: ${VALID_SCOPES.join(', ')}`);
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
const bundle = memory.exportBundle({ scope: scopeRaw });
|
|
103
|
+
const bundle = await memory.exportBundle({ scope: scopeRaw });
|
|
104
104
|
const targetPath = resolveBundlePath(pathArg, requireShellPaths(context));
|
|
105
105
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
106
106
|
writeFileSync(targetPath, JSON.stringify(bundle, null, 2) + '\n', 'utf-8');
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
buildPolicyPreflightMemoryAddOptions,
|
|
8
8
|
} from '@pellux/goodvibes-sdk/platform/state';
|
|
9
9
|
import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope } from './recall-shared.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { getMemorySpine } from './recall-query.ts';
|
|
11
11
|
|
|
12
12
|
export async function handleRecallAdd(args: string[], context: CommandContext): Promise<void> {
|
|
13
|
-
const memory =
|
|
13
|
+
const memory = getMemorySpine(context);
|
|
14
14
|
if (!memory) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
@@ -70,7 +70,7 @@ export async function handleRecallAdd(args: string[], context: CommandContext):
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export async function handleRecallCapture(args: string[], context: CommandContext): Promise<void> {
|
|
73
|
-
const memory =
|
|
73
|
+
const memory = getMemorySpine(context);
|
|
74
74
|
const pluginManager = context.extensions.pluginManager;
|
|
75
75
|
if (!memory) {
|
|
76
76
|
return;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { CommandContext } from '../command-registry.ts';
|
|
2
2
|
import type { MemoryApi } from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
3
|
+
import type { MemorySpineClient } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
3
4
|
import type { MemorySearchFilter } from '@pellux/goodvibes-sdk/platform/state';
|
|
4
5
|
import { VALID_CLASSES, VALID_SCOPES, isValidClass, isValidScope } from './recall-shared.ts';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The local, host-only `MemoryApi` — kept for `/recall explain` and `/recall
|
|
9
|
+
* vector` only. Both are ruled explicitly out of the wire catalog in
|
|
10
|
+
* docs/decisions/2026-07-06-memory-wire-full-detach.md (SDK repo): `explain`
|
|
11
|
+
* is a client-side projection over whatever read surface is active, not a
|
|
12
|
+
* store operation, and vector-index rebuild is host/admin maintenance a wire
|
|
13
|
+
* client does not own. Every other `/recall` subcommand reads/writes through
|
|
14
|
+
* {@link getMemorySpine} instead, so it detaches fully from the local store
|
|
15
|
+
* file when a daemon is adopted.
|
|
16
|
+
*/
|
|
6
17
|
export function getMemoryApi(context: CommandContext): MemoryApi | null {
|
|
7
18
|
const memoryApi = context.clients?.knowledgeApi?.memory;
|
|
8
19
|
if (!memoryApi) {
|
|
@@ -12,8 +23,23 @@ export function getMemoryApi(context: CommandContext): MemoryApi | null {
|
|
|
12
23
|
return memoryApi;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The cross-surface memory spine client — routes local when offline/embedded,
|
|
28
|
+
* over the wire when a daemon has been adopted, never both. `/recall`'s
|
|
29
|
+
* browse/link/queue/export/import subcommands use this so they never read a
|
|
30
|
+
* divergent local copy while a daemon owns the canonical store.
|
|
31
|
+
*/
|
|
32
|
+
export function getMemorySpine(context: CommandContext): MemorySpineClient | null {
|
|
33
|
+
const memorySpine = context.clients?.memorySpine;
|
|
34
|
+
if (!memorySpine) {
|
|
35
|
+
context.print('[recall] Memory spine is not available in this runtime.');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return memorySpine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleRecallSearch(args: string[], context: CommandContext): Promise<void> {
|
|
42
|
+
const memory = getMemorySpine(context);
|
|
17
43
|
if (!memory) {
|
|
18
44
|
return;
|
|
19
45
|
}
|
|
@@ -54,8 +80,8 @@ export function handleRecallSearch(args: string[], context: CommandContext): voi
|
|
|
54
80
|
if (queryTokens.length) filter.query = queryTokens.join(' ');
|
|
55
81
|
if (semantic) filter.semantic = true;
|
|
56
82
|
|
|
57
|
-
const semanticResults = semantic ? memory.searchSemantic(filter) : [];
|
|
58
|
-
const results = semantic ? semanticResults.map((entry) => entry.record) : memory.
|
|
83
|
+
const semanticResults = semantic ? await memory.searchSemantic(filter) : [];
|
|
84
|
+
const results = semantic ? semanticResults.map((entry) => entry.record) : await memory.list(filter);
|
|
59
85
|
if (!results.length) {
|
|
60
86
|
context.print('[recall] No records found.');
|
|
61
87
|
return;
|
|
@@ -110,8 +136,8 @@ function formatVectorStats(
|
|
|
110
136
|
].join('\n');
|
|
111
137
|
}
|
|
112
138
|
|
|
113
|
-
export function handleRecallGet(args: string[], context: CommandContext): void {
|
|
114
|
-
const memory =
|
|
139
|
+
export async function handleRecallGet(args: string[], context: CommandContext): Promise<void> {
|
|
140
|
+
const memory = getMemorySpine(context);
|
|
115
141
|
if (!memory) {
|
|
116
142
|
return;
|
|
117
143
|
}
|
|
@@ -120,7 +146,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
|
|
|
120
146
|
context.print('[recall] Usage: /recall get <id>');
|
|
121
147
|
return;
|
|
122
148
|
}
|
|
123
|
-
const record = memory.get(id);
|
|
149
|
+
const record = await memory.get(id);
|
|
124
150
|
if (!record) {
|
|
125
151
|
context.print(`[recall] Record not found: ${id}`);
|
|
126
152
|
return;
|
|
@@ -142,7 +168,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
|
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
|
|
145
|
-
const links = memory.linksFor(id);
|
|
171
|
+
const links = await memory.linksFor(id);
|
|
146
172
|
if (links.length) {
|
|
147
173
|
context.print(' Links:');
|
|
148
174
|
for (const link of links) {
|
|
@@ -154,7 +180,7 @@ export function handleRecallGet(args: string[], context: CommandContext): void {
|
|
|
154
180
|
}
|
|
155
181
|
|
|
156
182
|
export async function handleRecallLink(args: string[], context: CommandContext): Promise<void> {
|
|
157
|
-
const memory =
|
|
183
|
+
const memory = getMemorySpine(context);
|
|
158
184
|
if (!memory) {
|
|
159
185
|
return;
|
|
160
186
|
}
|
|
@@ -171,8 +197,8 @@ export async function handleRecallLink(args: string[], context: CommandContext):
|
|
|
171
197
|
context.print(`[recall] Linked: ${fromId} -> ${toId} [${relation}]`);
|
|
172
198
|
}
|
|
173
199
|
|
|
174
|
-
export function handleRecallRemove(args: string[], context: CommandContext): void {
|
|
175
|
-
const memory =
|
|
200
|
+
export async function handleRecallRemove(args: string[], context: CommandContext): Promise<void> {
|
|
201
|
+
const memory = getMemorySpine(context);
|
|
176
202
|
if (!memory) {
|
|
177
203
|
return;
|
|
178
204
|
}
|
|
@@ -181,7 +207,7 @@ export function handleRecallRemove(args: string[], context: CommandContext): voi
|
|
|
181
207
|
context.print('[recall] Usage: /recall remove <id>');
|
|
182
208
|
return;
|
|
183
209
|
}
|
|
184
|
-
const removed = memory.delete(id);
|
|
210
|
+
const removed = await memory.delete(id);
|
|
185
211
|
if (!removed) {
|
|
186
212
|
context.print(`[recall] Record not found: ${id}`);
|
|
187
213
|
return;
|
|
@@ -189,8 +215,8 @@ export function handleRecallRemove(args: string[], context: CommandContext): voi
|
|
|
189
215
|
context.print(`[recall] Deleted: ${id}`);
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
export function handleRecallList(args: string[], context: CommandContext): void {
|
|
193
|
-
const memory =
|
|
218
|
+
export async function handleRecallList(args: string[], context: CommandContext): Promise<void> {
|
|
219
|
+
const memory = getMemorySpine(context);
|
|
194
220
|
if (!memory) {
|
|
195
221
|
return;
|
|
196
222
|
}
|
|
@@ -207,7 +233,7 @@ export function handleRecallList(args: string[], context: CommandContext): void
|
|
|
207
233
|
filter.scope = scope;
|
|
208
234
|
}
|
|
209
235
|
|
|
210
|
-
const records = memory.
|
|
236
|
+
const records = await memory.list(filter);
|
|
211
237
|
if (!records.length) {
|
|
212
238
|
context.print('[recall] No records.');
|
|
213
239
|
return;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { CommandContext } from '../command-registry.ts';
|
|
2
2
|
import { VALID_REVIEW_STATES, VALID_SCOPES, isValidReviewState, isValidScope } from './recall-shared.ts';
|
|
3
|
-
import { getMemoryApi } from './recall-query.ts';
|
|
3
|
+
import { getMemoryApi, getMemorySpine } from './recall-query.ts';
|
|
4
4
|
import { buildMainSessionTurnInjectionsText, buildTurnInjectionsText } from '../../renderer/turn-injection.ts';
|
|
5
5
|
|
|
6
|
-
export function handleRecallQueue(args: string[], context: CommandContext): void {
|
|
7
|
-
const memory =
|
|
6
|
+
export async function handleRecallQueue(args: string[], context: CommandContext): Promise<void> {
|
|
7
|
+
const memory = getMemorySpine(context);
|
|
8
8
|
if (!memory) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
@@ -29,7 +29,7 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
|
|
|
29
29
|
// When a scope filter is requested, fetch a larger pool then slice to the
|
|
30
30
|
// requested limit so scope-sparse queues still return meaningful results.
|
|
31
31
|
const fetchLimit = scopeFilter ? 1000 : limit;
|
|
32
|
-
const rawQueue = memory.reviewQueue(fetchLimit);
|
|
32
|
+
const rawQueue = await memory.reviewQueue(fetchLimit);
|
|
33
33
|
const queue = scopeFilter
|
|
34
34
|
? rawQueue.filter((record) => record.scope === scopeFilter).slice(0, limit)
|
|
35
35
|
: rawQueue;
|
|
@@ -45,8 +45,8 @@ export function handleRecallQueue(args: string[], context: CommandContext): void
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function handleRecallReview(args: string[], context: CommandContext): void {
|
|
49
|
-
const memory =
|
|
48
|
+
export async function handleRecallReview(args: string[], context: CommandContext): Promise<void> {
|
|
49
|
+
const memory = getMemorySpine(context);
|
|
50
50
|
if (!memory) {
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
@@ -64,7 +64,7 @@ export function handleRecallReview(args: string[], context: CommandContext): voi
|
|
|
64
64
|
const reviewedBy = byIdx !== -1 ? rest[byIdx + 1] : 'operator';
|
|
65
65
|
const staleReason = reasonIdx !== -1 ? rest.slice(reasonIdx + 1).join(' ') : undefined;
|
|
66
66
|
|
|
67
|
-
const record = memory.
|
|
67
|
+
const record = await memory.updateReview(id, {
|
|
68
68
|
state: stateRaw,
|
|
69
69
|
confidence: Number.isFinite(confidence) ? confidence : undefined,
|
|
70
70
|
reviewedBy,
|
|
@@ -104,9 +104,9 @@ export function handleRecallExplain(args: string[], context: CommandContext): vo
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
107
|
+
* Per-turn passive knowledge injection inspectability.
|
|
108
108
|
*
|
|
109
|
-
* With an explicit agent id, reads `AgentRecord.turnInjections`
|
|
109
|
+
* With an explicit agent id, reads `AgentRecord.turnInjections`
|
|
110
110
|
* via `context.ops.agentManager.exportState()` — the CommandContext-exposed
|
|
111
111
|
* `ShellAgentManagerService` doesn't have a `getStatus`/`list` pair, but
|
|
112
112
|
* `exportState()` returns the same full `AgentRecord[]` (it's what
|
|
@@ -137,8 +137,8 @@ export function handleRecallInjections(args: string[], context: CommandContext):
|
|
|
137
137
|
context.print(buildTurnInjectionsText(agentId, record.turnInjections ?? []));
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
export function handleRecallPromote(args: string[], context: CommandContext): void {
|
|
141
|
-
const memory =
|
|
140
|
+
export async function handleRecallPromote(args: string[], context: CommandContext): Promise<void> {
|
|
141
|
+
const memory = getMemorySpine(context);
|
|
142
142
|
if (!memory) {
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
@@ -148,7 +148,7 @@ export function handleRecallPromote(args: string[], context: CommandContext): vo
|
|
|
148
148
|
context.print(`[recall] Usage: /recall promote <id> <${VALID_SCOPES.join('|')}>`);
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
const record = memory.update(id, { scope });
|
|
151
|
+
const record = await memory.update(id, { scope });
|
|
152
152
|
if (!record) {
|
|
153
153
|
context.print(`[recall] Record not found: ${id}`);
|
|
154
154
|
return;
|
|
@@ -384,7 +384,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
|
|
|
384
384
|
},
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
//
|
|
387
|
+
// From the core-verb naming pass (MEMORY fragmentation — worst-class
|
|
388
388
|
// collision #2): this command used to be named /memory, colliding with
|
|
389
389
|
// the agent's /memory (an alias that forwards to /recall, the durable
|
|
390
390
|
// cross-session MemoryStore) — same command name, two unrelated features.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from handler.ts (
|
|
4
|
+
* Extracted from handler.ts (review follow-up, 0.18.23) to keep handler.ts under the
|
|
5
5
|
* 800-line architecture cap.
|
|
6
6
|
*
|
|
7
7
|
* Two exported functions:
|
|
@@ -447,8 +447,8 @@ export function handleCtrlC(
|
|
|
447
447
|
} catch {
|
|
448
448
|
// Non-fatal to the exit chord; the quit-window bookkeeping still runs.
|
|
449
449
|
}
|
|
450
|
-
// A press that silenced LIVE speech is consumed by that job (
|
|
451
|
-
//
|
|
450
|
+
// A press that silenced LIVE speech is consumed by that job (an earlier replay
|
|
451
|
+
// fix) — symmetric with the prompt-clearing press above. The quit chord
|
|
452
452
|
// ("Ctrl+C x2") starts from a quiet state; turn-aborts still count toward
|
|
453
453
|
// the double-press.
|
|
454
454
|
if (stoppedSpeech) return;
|
|
@@ -189,7 +189,7 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
|
|
|
189
189
|
export type IndicatorFocusRouteState = {
|
|
190
190
|
indicatorFocused: boolean;
|
|
191
191
|
modalOpened: (name: string) => void;
|
|
192
|
-
/**
|
|
192
|
+
/** The footer process indicator's [Enter] opens the Fleet panel (F2 also opens it; the process modal was retired). */
|
|
193
193
|
openFleetPanel: () => void;
|
|
194
194
|
requestRender: () => void;
|
|
195
195
|
};
|
|
@@ -337,7 +337,7 @@ export type KeyRouteState = {
|
|
|
337
337
|
commandRegistry?: CommandRegistry | null;
|
|
338
338
|
autocomplete: AutocompleteEngine | null;
|
|
339
339
|
blockActionsMenu: { open: (block: BlockMeta) => void };
|
|
340
|
-
/**
|
|
340
|
+
/** F2 opens+focuses the Fleet panel (which subsumes the retired process modal). */
|
|
341
341
|
openFleetPanel: () => void;
|
|
342
342
|
modalOpened: (name: string) => void;
|
|
343
343
|
saveUndoState: () => void;
|
|
@@ -186,7 +186,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
|
|
|
186
186
|
// focus to it. panelManager.open() only makes the panel active — focus is a
|
|
187
187
|
// separate axis (see PanelManager.focusPanels()/getFocusTarget()); without the
|
|
188
188
|
// focusPanels() call, j/k/i/K land silently in the composer until Tab. Used by
|
|
189
|
-
// the footer indicator's [Enter] and by F2 (
|
|
189
|
+
// the footer indicator's [Enter] and by F2 (F2 and the footer
|
|
190
190
|
// indicator both subsume the retired process modal). Mirrors the Ctrl+P
|
|
191
191
|
// panel-picker launcher (ui-openers.ts openPanelPicker).
|
|
192
192
|
const openFleetPanel = (): void => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Friction fix: the onboarding wizard's "Connect to
|
|
3
3
|
* this daemon now" action, wired from the Network step's `network.daemon-source`
|
|
4
4
|
* = 'adopt' fields. Extracted from handler-onboarding.ts to keep that file
|
|
5
5
|
* under the architecture line cap.
|
|
@@ -88,8 +88,8 @@ export async function handleConnectExistingDaemonForHandler(handler: InputHandle
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
* `goodvibes-daemon.service` unit (
|
|
91
|
+
* The guided UX entry point for migrating a legacy
|
|
92
|
+
* `goodvibes-daemon.service` unit (an earlier release shipped detect+disclose only; this
|
|
93
93
|
* closes that inheritance). This is a thin wrapper over
|
|
94
94
|
* `detectLegacyUnit` + `runLegacyDaemonMigration`
|
|
95
95
|
* (`../runtime/legacy-daemon-migration.ts`) — the actual migration mechanics
|
|
@@ -22,7 +22,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
22
22
|
|
|
23
23
|
if (token.type === 'key') {
|
|
24
24
|
if (token.logicalName === 'escape') {
|
|
25
|
-
//
|
|
25
|
+
// Search now starts focused by default (see
|
|
26
26
|
// ModelPickerModal.openAllModels() doc comment). Escape with an empty
|
|
27
27
|
// query used to only blurSearch() here, leaving the picker open and
|
|
28
28
|
// requiring a SECOND Escape to actually close it — confusing on a
|
|
@@ -133,7 +133,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
|
|
|
133
133
|
state.modelPicker.focusSearch();
|
|
134
134
|
} else if (state.modelPicker.searchFocused) {
|
|
135
135
|
// Symmetric with 'down' (below): search now starts focused by default
|
|
136
|
-
//
|
|
136
|
+
// so 'up' must have a way out of it too, or it would be a
|
|
137
137
|
// silent no-op the very first time a user reaches for the list.
|
|
138
138
|
state.modelPicker.blurSearch();
|
|
139
139
|
} else {
|
|
@@ -121,7 +121,7 @@ export function handleGlobalShortcutToken(
|
|
|
121
121
|
case 'panel-close': {
|
|
122
122
|
const pm = state.panelManager;
|
|
123
123
|
const active = pm.getActivePanel();
|
|
124
|
-
//
|
|
124
|
+
// Give the active panel a chance to consume Ctrl+X for an
|
|
125
125
|
// in-panel action (FleetPanel session-tab detach) before it closes the
|
|
126
126
|
// panel outright — see Panel.interceptPanelClose's doc comment.
|
|
127
127
|
if (active?.interceptPanelClose?.()) {
|
|
@@ -264,7 +264,7 @@ export class ModelPickerModal {
|
|
|
264
264
|
/**
|
|
265
265
|
* Open showing all models — entry point for /model.
|
|
266
266
|
*
|
|
267
|
-
*
|
|
267
|
+
* Search starts FOCUSED. A live tmux repro showed the actual
|
|
268
268
|
* friction wasn't the filter itself (it already works — fuzzy multi-word
|
|
269
269
|
* substring over id/displayName/provider) but that with search unfocused by
|
|
270
270
|
* default, typing a search term went character-by-character into single-key
|
|
@@ -295,7 +295,7 @@ export class ModelPickerModal {
|
|
|
295
295
|
this.scrollOffset = 0;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
/** Open showing providers first — entry point for /provider.
|
|
298
|
+
/** Open showing providers first — entry point for /provider. Search starts focused — see openAllModels(). */
|
|
299
299
|
openProviders(providers: string[], currentProvider: string): void {
|
|
300
300
|
this.previousMode = null;
|
|
301
301
|
this.providers = providers;
|
|
@@ -86,13 +86,13 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
86
86
|
setConfig('service.autostart', hasServers);
|
|
87
87
|
setConfig('service.restartOnFailure', true);
|
|
88
88
|
// One-Platform daemon-by-default (SDK docs/decisions/2026-07-05-daemon-by-default.md):
|
|
89
|
-
// the loopback-only cross-surface session daemon is safe-by-design
|
|
90
|
-
// loopback bind, auth-gated, rate-limited) and runs regardless of whether
|
|
89
|
+
// the loopback-only cross-surface session daemon is safe-by-design
|
|
90
|
+
// (loopback bind, auth-gated, rate-limited) and runs regardless of whether
|
|
91
91
|
// browser/LAN/webhook/external-app capabilities are selected here — it is no
|
|
92
92
|
// longer bundled with those network-exposing surfaces, so onboarding leaves
|
|
93
93
|
// daemon.enabled untouched and lets the SDK's own default-true (or an existing
|
|
94
94
|
// explicit user override) govern. (The deprecated danger.daemon alias this
|
|
95
|
-
// comment used to also name was removed
|
|
95
|
+
// comment used to also name was later removed.)
|
|
96
96
|
setConfig('controlPlane.enabled', hasServers);
|
|
97
97
|
setConfig('danger.httpListener', httpListener);
|
|
98
98
|
setConfig('web.enabled', browserAccess);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Friction fix: the Network step's daemon-source choice
|
|
3
3
|
* — start a new daemon owned by this TUI (default) vs connect to one that is
|
|
4
4
|
* already running elsewhere with a known token. Extracted from
|
|
5
5
|
* onboarding-wizard-steps.ts to keep that file under the architecture line cap.
|
|
@@ -78,7 +78,7 @@ export function pushDaemonAdoptionFields(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* The guided, visible entry point for migrating a detected legacy
|
|
82
82
|
* `goodvibes-daemon.service` unit — closes the fast-follow-up flagged when
|
|
83
83
|
* `handleMigrateLegacyDaemonServiceForHandler` (handler-onboarding-daemon-adopt.ts)
|
|
84
84
|
* shipped fully built but not wired to any onboarding control. Independent of
|
|
@@ -107,7 +107,7 @@ export function pushLegacyDaemonMigrationFields(
|
|
|
107
107
|
kind: 'status',
|
|
108
108
|
id: 'network.migrate-legacy-daemon-detected',
|
|
109
109
|
label: 'Legacy daemon service detected',
|
|
110
|
-
//
|
|
110
|
+
// Follow-up: name the unit this host actually resolves (carried on the
|
|
111
111
|
// snapshot from `resolveConfiguredServiceName` at collection time) — the
|
|
112
112
|
// MANAGED_SERVICE_NAME fallback only covers snapshots built without it.
|
|
113
113
|
hint: legacyUnitNote(legacy, legacy.trackedServiceName ?? MANAGED_SERVICE_NAME),
|
|
@@ -533,7 +533,7 @@ export function buildNetworkStep(controller: OnboardingWizardControllerLike): On
|
|
|
533
533
|
},
|
|
534
534
|
];
|
|
535
535
|
|
|
536
|
-
//
|
|
536
|
+
// Recognize an adoptable already-running daemon (see onboarding-wizard-network-adopt.ts).
|
|
537
537
|
const daemonSource = getDaemonSource(controller);
|
|
538
538
|
pushDaemonAdoptionFields(fields, controller, daemonSource, bindSettings);
|
|
539
539
|
pushLegacyDaemonMigrationFields(fields, controller);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* control-plane session, see below), tracks selected index, and handles
|
|
7
7
|
* load/delete actions.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Union-sessions surface: additionally surfaces the cross-surface
|
|
10
10
|
* session union from `sessionBroker` (a SessionReadFacade — normally
|
|
11
11
|
* `uiServices.sessions.sessionBroker`, the SessionUnionCache) so a user can
|
|
12
12
|
* SEE what sessions are live/closed across every surface sharing this
|
|
@@ -15,6 +15,7 @@ import { buildSubscriptionEntries } from './settings-modal-subscriptions.ts';
|
|
|
15
15
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
16
16
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
17
17
|
import { CODE_INDEX_ENABLED_CONFIG_KEY } from '../runtime/code-index-services.ts';
|
|
18
|
+
import { BUDGET_ALERT_USD_CONFIG_KEY, BUDGET_ALERT_USD_DEFAULT, readBudgetAlertUsd } from '../export/cost-utils.ts';
|
|
18
19
|
import {
|
|
19
20
|
THEME_MODE_CONFIG_KEY,
|
|
20
21
|
THEME_MODE_VALUES,
|
|
@@ -146,6 +147,18 @@ export function buildSettingGroups(
|
|
|
146
147
|
behaviorEntries.push(buildNotifyAfterSecondsSyntheticEntry(configManager));
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// Inject the synthetic behavior.budgetAlertUsd entry into the behavior
|
|
151
|
+
// category. This key is TUI-local (not in the SDK ConfigKey union), which
|
|
152
|
+
// previously left it with no schema-driven inspection surface at all: it
|
|
153
|
+
// never appeared in /config, and /settings-sync show rejects any key not
|
|
154
|
+
// in CONFIG_KEYS. The Cost panel's 'b' key and /cost budget <usd> remain
|
|
155
|
+
// the primary way to change it; this entry makes the current effective
|
|
156
|
+
// value (and whether it's still the "no budget configured" default)
|
|
157
|
+
// visible from /config behavior too, same rationale as notifyAfterSeconds.
|
|
158
|
+
if (behaviorEntries && !behaviorEntries.some((e) => e.setting.key === (BUDGET_ALERT_USD_CONFIG_KEY as ConfigKey))) {
|
|
159
|
+
behaviorEntries.push(buildBudgetAlertUsdSyntheticEntry(configManager));
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
// Inject the W2.3 alert-class toggles + master focus gate. TUI-local
|
|
150
163
|
// synthetic settings, same rationale as notifyAfterSeconds above.
|
|
151
164
|
if (behaviorEntries) {
|
|
@@ -156,7 +169,7 @@ export function buildSettingGroups(
|
|
|
156
169
|
}
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
//
|
|
172
|
+
// Inject the storage.codeIndexEnabled toggle into the
|
|
160
173
|
// storage category. TUI-local synthetic setting (not in the SDK ConfigKey
|
|
161
174
|
// union — see code-index-services.ts), same rationale as
|
|
162
175
|
// notifyAfterSeconds above: opt-in, default off, states its own bounds.
|
|
@@ -287,6 +300,47 @@ export function buildNotifyAfterSecondsSyntheticEntry(configManager: Pick<Config
|
|
|
287
300
|
};
|
|
288
301
|
}
|
|
289
302
|
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// behavior.budgetAlertUsd synthetic setting
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The synthetic ConfigSetting descriptor for behavior.budgetAlertUsd.
|
|
309
|
+
*
|
|
310
|
+
* This key is TUI-local and is not yet in the SDK ConfigKey union — it never
|
|
311
|
+
* appeared in CONFIG_SCHEMA, so it was invisible to every schema-driven
|
|
312
|
+
* inspection surface (/config, /settings-sync show <key>) even though the
|
|
313
|
+
* value round-trips correctly through configManager.get/set. The descriptor
|
|
314
|
+
* is injected into the behavior settings group so /config behavior shows the
|
|
315
|
+
* real current threshold, not just the behavior.notifyOnBudgetBreach gate.
|
|
316
|
+
*
|
|
317
|
+
* 0 = no budget configured (disabled). Any positive number = the USD
|
|
318
|
+
* threshold. Default matches BUDGET_ALERT_USD_DEFAULT in cost-utils.ts, the
|
|
319
|
+
* single source of truth CostTrackerPanel and budget-breach-notifier.ts share.
|
|
320
|
+
*/
|
|
321
|
+
export const BUDGET_ALERT_USD_SYNTHETIC_SETTING: ConfigSetting = {
|
|
322
|
+
key: BUDGET_ALERT_USD_CONFIG_KEY as ConfigKey,
|
|
323
|
+
type: 'number',
|
|
324
|
+
default: BUDGET_ALERT_USD_DEFAULT,
|
|
325
|
+
description: 'Session cost-budget alert threshold in USD (0 = no budget configured). Set via the Cost panel\'s "b" key or /cost budget <usd>; this entry only displays the current effective value.',
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Build the synthetic SettingEntry for behavior.budgetAlertUsd.
|
|
330
|
+
*
|
|
331
|
+
* Delegates parsing/fallback to readBudgetAlertUsd (cost-utils.ts) so this
|
|
332
|
+
* display entry can never disagree with what CostTrackerPanel and the
|
|
333
|
+
* background budget-breach notifier actually read.
|
|
334
|
+
*/
|
|
335
|
+
export function buildBudgetAlertUsdSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): SettingEntry {
|
|
336
|
+
const currentValue = readBudgetAlertUsd((key) => configManager.get(key as ConfigKey));
|
|
337
|
+
return {
|
|
338
|
+
setting: BUDGET_ALERT_USD_SYNTHETIC_SETTING,
|
|
339
|
+
currentValue,
|
|
340
|
+
isDefault: currentValue === BUDGET_ALERT_USD_DEFAULT,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
290
344
|
// ---------------------------------------------------------------------------
|
|
291
345
|
// W2.3 alert-class synthetic settings — behavior.notifyOn* + notifyOnlyWhenUnfocused
|
|
292
346
|
// ---------------------------------------------------------------------------
|
|
@@ -342,7 +396,7 @@ export function buildNotifyAlertSyntheticEntries(configManager: Pick<ConfigManag
|
|
|
342
396
|
}
|
|
343
397
|
|
|
344
398
|
// ---------------------------------------------------------------------------
|
|
345
|
-
//
|
|
399
|
+
// storage.codeIndexEnabled synthetic setting
|
|
346
400
|
// ---------------------------------------------------------------------------
|
|
347
401
|
|
|
348
402
|
/**
|
package/src/main.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
32
32
|
import { registerBuiltinPanels } from './panels/builtin-panels.ts';
|
|
33
33
|
import { bootstrapRuntime } from './runtime/bootstrap.ts';
|
|
34
34
|
import type { BootstrapContext } from './runtime/bootstrap.ts';
|
|
35
|
-
import { buildSharedOrchestratorCoreServices } from './runtime/orchestrator-core-services.ts';
|
|
35
|
+
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './runtime/orchestrator-core-services.ts';
|
|
36
36
|
import type { HITLMode } from '@pellux/goodvibes-sdk/platform/state';
|
|
37
37
|
import { readLastSessionPointer, writeRecoveryFile } from '@/runtime/index.ts';
|
|
38
38
|
import { handleBlockingShellInput, type PendingPermissionState } from './shell/blocking-input.ts';
|
|
@@ -202,7 +202,7 @@ async function main() {
|
|
|
202
202
|
|
|
203
203
|
const unsubs: Array<() => void> = [];
|
|
204
204
|
let recoveryInterval: ReturnType<typeof setInterval> | null = null;
|
|
205
|
-
let stopSpokenOutputForExit: (() => void) | null = null;
|
|
205
|
+
let stopSpokenOutputForExit: (() => Promise<void>) | null = null;
|
|
206
206
|
let recoveryPending = false;
|
|
207
207
|
// Which file the current recovery prompt should load/delete from (see recovery-input-helpers.ts).
|
|
208
208
|
let recoverySource: 'live' | 'preserved' = 'live';
|
|
@@ -240,7 +240,8 @@ async function main() {
|
|
|
240
240
|
events: uiServices.events,
|
|
241
241
|
notify: (message) => { systemMessageRouter.high(message); render(); },
|
|
242
242
|
});
|
|
243
|
-
|
|
243
|
+
// Exit-path stop: bounded drain of the audio already playing (see stopForExit).
|
|
244
|
+
stopSpokenOutputForExit = () => spokenTurns.stopForExit();
|
|
244
245
|
unsubs.push(...spokenTurns.unsubs);
|
|
245
246
|
unsubs.push(attachSpokenTurnModelRouting({
|
|
246
247
|
orchestrator,
|
|
@@ -287,6 +288,7 @@ async function main() {
|
|
|
287
288
|
// Snapshot pre-submission state for failover retryTurn; also clears visited set.
|
|
288
289
|
retryCtx = { count: conversation.getMessageCount(), text: processedText, content, opts: inputOptions };
|
|
289
290
|
streamResult.clearFailoverVisited();
|
|
291
|
+
await refreshMemoryRecallSnapshot(ctx.services); // pre-turn recall-snapshot refresh (SDK 1.2.0 full detach)
|
|
290
292
|
orchestrator.handleUserInput(processedText, content, inputOptions).catch((err: unknown) => {
|
|
291
293
|
logger.debug('handleUserInput safety catch (already handled by runTurn)', { error: summarizeError(err) });
|
|
292
294
|
});
|
|
@@ -497,7 +499,7 @@ async function main() {
|
|
|
497
499
|
lastInputTokens: orchestrator.lastInputTokens,
|
|
498
500
|
commandArgsHint,
|
|
499
501
|
hitlMode: modeManager.getHITLMode(),
|
|
500
|
-
//
|
|
502
|
+
// Cross-surface spine posture segment (adopted-daemon mode only).
|
|
501
503
|
sessionSpineStatus: (() => { const s = uiServices.platform.externalServices?.inspect(); return s?.sessionSpineActive && s.sessionSpineStatus && s.sessionSpineStatus !== 'unknown' ? s.sessionSpineStatus : undefined; })(), runningAgentCount, runningProcessCount,
|
|
502
504
|
// Composer must not read as focused while the panel/process indicator owns keyboard focus.
|
|
503
505
|
promptFocused: !input.panelFocused && !input.indicatorFocused,
|
|
@@ -646,7 +648,7 @@ async function main() {
|
|
|
646
648
|
const terminalOutputGuard = installTuiTerminalOutputGuard({ stdout, stderr: process.stderr, onCapture: (total) => { commandContext.session.runtime.terminalWritesIntercepted = total; render(); } });
|
|
647
649
|
|
|
648
650
|
setRenderRequest(renderNow); // bootstrap's 16ms coalescer calls the composite directly
|
|
649
|
-
setPanelFrameRequester(render); // live panels repaint when idle (
|
|
651
|
+
setPanelFrameRequester(render); // live panels repaint when idle (a replay finding: fleet sat stale until keypress)
|
|
650
652
|
orchestratorRefs.requestRender = render;
|
|
651
653
|
commandContext.renderRequest = render;
|
|
652
654
|
wireShellUiOpeners({
|
|
@@ -699,7 +701,7 @@ async function main() {
|
|
|
699
701
|
const { count, text, content: rContent, opts: rOpts } = retryCtx;
|
|
700
702
|
// Roll back to pre-submission count, then re-submit. SDK gap — no retry-in-place (see handoff).
|
|
701
703
|
conversation.removeMessagesAfter(count);
|
|
702
|
-
orchestrator.handleUserInput(text, rContent, rOpts).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
|
|
704
|
+
void refreshMemoryRecallSnapshot(ctx.services).then(() => orchestrator.handleUserInput(text, rContent, rOpts)).catch((e: unknown) => logger.debug('retryTurn', { error: summarizeError(e) }));
|
|
703
705
|
};
|
|
704
706
|
const streamResult: WireStreamEventMetricsResult = wireStreamEventMetrics({
|
|
705
707
|
events: uiServices.events, orchestrator, providerRegistry,
|
package/src/panels/base-panel.ts
CHANGED
|
@@ -14,7 +14,7 @@ const ERROR_FG = '#ef4444';
|
|
|
14
14
|
* markDirty() only sets a flag the compositor reads when a frame is ALREADY
|
|
15
15
|
* being composed for another reason (input, streaming) — so a live panel
|
|
16
16
|
* (fleet ticks, registry subscriptions) sat visibly stale while the app was
|
|
17
|
-
* idle until the next keypress (
|
|
17
|
+
* idle until the next keypress (a replay finding). The scheduler
|
|
18
18
|
* coalesces same-tick requests, so this stays cheap.
|
|
19
19
|
*/
|
|
20
20
|
let panelFrameRequester: (() => void) | null = null;
|