@pellux/goodvibes-tui 1.9.1 → 1.9.2
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 +16 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/audio/player.ts +91 -5
- package/src/audio/spoken-turn-controller.ts +30 -2
- package/src/audio/spoken-turn-wiring.ts +3 -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/input/command-registry.ts +5 -5
- package/src/input/commands/config.ts +1 -1
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/planning-runtime.ts +1 -1
- package/src/input/commands/recall-review.ts +2 -2
- 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 +2 -2
- package/src/main.ts +5 -4
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/operations.ts +4 -4
- package/src/panels/builtin/shared.ts +1 -1
- 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/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-core.ts +3 -3
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +3 -3
- package/src/runtime/bootstrap.ts +5 -5
- 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/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 +2 -2
- package/src/runtime/process-lifecycle.ts +19 -5
- package/src/runtime/services.ts +14 -14
- 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/CHANGELOG.md
CHANGED
|
@@ -4,15 +4,26 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.9.2] — 2026-07-06
|
|
8
|
+
|
|
9
|
+
Patch release fixing text-to-speech playback so spoken responses play start-to-finish without losing audio at either end.
|
|
10
|
+
|
|
11
|
+
### Text-to-speech playback fixes
|
|
12
|
+
- The beginning of spoken output is no longer clipped: playback now waits for the audio player to actually be ready before sending it audio, and the player's read-ahead buffering was restored so the opening of a response survives normal startup delay.
|
|
13
|
+
- The end of spoken output is no longer cut off: audio now drains fully once a response finishes speaking naturally. When you quit while audio is still playing, the final bit of audio gets a bounded two-second window to finish before shutdown continues. Deliberate interrupts (stopping speech, starting a new response, Ctrl+C) still cut instantly as before. A spurious error message that could appear when stopping playback has also been removed.
|
|
14
|
+
|
|
15
|
+
### Other
|
|
16
|
+
- Documentation and code comments were cleaned up for clarity; no behavior changes.
|
|
17
|
+
|
|
7
18
|
## [1.9.1] — 2026-07-06
|
|
8
19
|
|
|
9
|
-
Follow-up patch that completes the
|
|
20
|
+
Follow-up patch that completes the client-side credential-status adoption for the TUI. The 1.9.0 release pinned SDK 1.0.0 and unified memory but left the client-side credential-status read for a follow-up; this release lands it. Ships against @pellux/goodvibes-sdk 1.0.0.
|
|
10
21
|
|
|
11
|
-
### Credential-status read as a daemon client
|
|
22
|
+
### Credential-status read as a daemon client
|
|
12
23
|
- When the TUI acts as a client of an adopted external daemon (`HostServiceStatus` mode `external`), provider/model/secret STATUS is read from that daemon's `credentials.get` wire method instead of the TUI's own surfaceRoot store. The read carries only status metadata (key / configured / usable / source / secure) — never a secret value, mirroring the SDK's status-only wire contract. The env-only API-key posture and all secret value-reads that provider auth needs stay local and are untouched; only STATUS reads use the daemon path.
|
|
13
24
|
- Honest degraded state, mirroring goodvibes-webui v1.0.1 `deriveCredentialAvailability` exactly: a 503 `CREDENTIAL_STORE_UNAVAILABLE` (matched by machine code), a `METHOD_NOT_FOUND` from an older daemon, or any transport failure resolves to a reason-carrying `available: false` — never a fabricated "configured", never a stale confident value. The TUI-hosted daemon continues to serve `credentials.get` automatically from its existing secrets manager (SDK auto-wiring), so no serve-side change was needed.
|
|
14
25
|
|
|
15
|
-
### Memory unification follow-up
|
|
26
|
+
### Memory unification follow-up
|
|
16
27
|
- The legacy per-project TUI memory store now actually folds into the home-scoped canonical store once at boot, after the canonical store initializes — making good on the behavior 1.9.0 described. The fold is id-keyed and idempotent (a re-run imports nothing new and never deletes the legacy file) and is strictly non-fatal: a fold error degrades to a warning and the canonical store serves on. The home-scoped store deliberately does not re-root with the working directory; only the code index and project index do.
|
|
17
28
|
|
|
18
29
|
## [1.7.0] — 2026-07-04
|
|
@@ -77,7 +88,7 @@ The evolution release: six batched development cycles (internal 1.2.0–1.6.0, w
|
|
|
77
88
|
|
|
78
89
|
## [1.1.0] — 2026-07-03
|
|
79
90
|
|
|
80
|
-
|
|
91
|
+
The first cycle of the post-1.0 evolution effort: trust repairs. Every change closes a defect found by live dogfooding v1.0.0 where the product reported something other than the truth. Ships against @pellux/goodvibes-sdk 0.36.0 (same effort, SDK side).
|
|
81
92
|
|
|
82
93
|
### Fixed
|
|
83
94
|
- Failing commands are visible again: the SDK masked every command-level failure as "Error: Unknown error", so a red test suite was literally invisible to the model — it would honestly conclude "no failure observed" while your tests were failing. Exit code, stdout, and stderr now reach the model byte-faithfully, and long-output truncation keeps the tail (where test runners print failures) instead of the head. Verified live: the model reads a sabotaged test's actual assertion diff and fixes the right side.
|
|
@@ -101,7 +112,7 @@ Wave 0 of the post-1.0 evolution effort: trust repairs. Every change closes a de
|
|
|
101
112
|
### Internal
|
|
102
113
|
- SDK dependency 0.35.0 → 0.36.0; foundation artifacts regenerated (new event contract entries).
|
|
103
114
|
- Local-SDK overlay dev tooling (`scripts/sdk-dev.ts link|status|restore`) with release-blocking gates in release.ts and publish-check so a local SDK can never ship.
|
|
104
|
-
-
|
|
115
|
+
- Verified three ways: full gate block, a 3-lens adversarial code review, and a live tmux scenario replay of the five previously-failing dogfood scenarios (all PASS).
|
|
105
116
|
|
|
106
117
|
## [1.0.0] — 2026-07-03
|
|
107
118
|
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
package/src/audio/player.ts
CHANGED
|
@@ -15,6 +15,14 @@ export interface StreamingAudioPlayer {
|
|
|
15
15
|
readonly available: boolean;
|
|
16
16
|
play(chunks: AsyncIterable<VoiceAudioChunk>, options: StreamingAudioPlaybackOptions): Promise<void>;
|
|
17
17
|
stop(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Resolves once the currently playing sink has finished naturally (its
|
|
20
|
+
* process closed) or after `timeoutMs`, whichever comes first; resolves
|
|
21
|
+
* immediately when nothing is playing. The exit path uses this to let the
|
|
22
|
+
* audio the user is already hearing finish inside a short bounded window
|
|
23
|
+
* instead of killing it mid-drain. stop() remains the instant cut.
|
|
24
|
+
*/
|
|
25
|
+
waitForDrain(timeoutMs: number): Promise<void>;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export interface StreamingAudioPlaybackOptions {
|
|
@@ -25,6 +33,8 @@ export interface StreamingAudioPlaybackOptions {
|
|
|
25
33
|
interface SpawnProcess {
|
|
26
34
|
readonly stdin: Writable;
|
|
27
35
|
once(event: 'close', listener: () => void): this;
|
|
36
|
+
once(event: 'spawn', listener: () => void): this;
|
|
37
|
+
once(event: 'error', listener: (error: unknown) => void): this;
|
|
28
38
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
29
39
|
}
|
|
30
40
|
type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnProcess;
|
|
@@ -32,6 +42,13 @@ type SpawnProcessFactory = (command: string, args: readonly string[]) => SpawnPr
|
|
|
32
42
|
export interface LocalStreamingAudioPlayerOptions {
|
|
33
43
|
readonly env?: NodeJS.ProcessEnv;
|
|
34
44
|
readonly spawnProcess?: SpawnProcessFactory;
|
|
45
|
+
/**
|
|
46
|
+
* Injected player command — the test seam companion to `spawnProcess`.
|
|
47
|
+
* When set, the constructor uses it verbatim instead of scanning PATH, so
|
|
48
|
+
* deterministic fake-sink tests never depend on mpv/ffplay being installed
|
|
49
|
+
* on the machine running them. Pass null to model "no player found".
|
|
50
|
+
*/
|
|
51
|
+
readonly command?: StreamingAudioPlayerCommand | null;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
@@ -40,7 +57,9 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
40
57
|
private readonly spawnProcess: SpawnProcessFactory;
|
|
41
58
|
|
|
42
59
|
constructor(options: LocalStreamingAudioPlayerOptions = {}) {
|
|
43
|
-
this.command =
|
|
60
|
+
this.command = options.command !== undefined
|
|
61
|
+
? options.command
|
|
62
|
+
: resolveStreamingAudioPlayerCommand(options.env ?? process.env);
|
|
44
63
|
this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
45
64
|
}
|
|
46
65
|
|
|
@@ -67,12 +86,27 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
67
86
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
68
87
|
|
|
69
88
|
try {
|
|
89
|
+
// Head survival: hold the first audio byte until the sink has actually
|
|
90
|
+
// started (its 'spawn' event) instead of writing into a process that has
|
|
91
|
+
// not exec'd yet. Writing before the player is up is the spawn race that
|
|
92
|
+
// clipped the beginning of playback. A spawn failure rejects here so the
|
|
93
|
+
// caller can report it honestly rather than swallowing a dead player.
|
|
94
|
+
await awaitReady(proc, options.signal);
|
|
95
|
+
if (options.signal?.aborted) return;
|
|
96
|
+
|
|
70
97
|
for await (const chunk of chunks) {
|
|
71
98
|
if (options.signal?.aborted) break;
|
|
72
99
|
if (chunk.data.byteLength === 0) continue;
|
|
73
100
|
await writeStdin(proc, chunk.data);
|
|
74
101
|
}
|
|
75
|
-
|
|
102
|
+
|
|
103
|
+
// An intentional interrupt (turn cancel / quit chord / /tts stop) has
|
|
104
|
+
// already torn the process down via `abort` and must cut immediately —
|
|
105
|
+
// do not wait on a graceful drain. A natural end-of-speech, by contrast,
|
|
106
|
+
// closes stdin and waits for the sink to play out every buffered sample
|
|
107
|
+
// so the tail of the response is never truncated.
|
|
108
|
+
if (options.signal?.aborted) return;
|
|
109
|
+
try { proc.stdin.end(); } catch { /* ignore */ }
|
|
76
110
|
await waitForExit(proc);
|
|
77
111
|
} finally {
|
|
78
112
|
options.signal?.removeEventListener('abort', abort);
|
|
@@ -87,14 +121,33 @@ export class LocalStreamingAudioPlayer implements StreamingAudioPlayer {
|
|
|
87
121
|
try { proc.stdin.destroy(); } catch { /* ignore */ }
|
|
88
122
|
try { proc.kill('SIGTERM'); } catch { /* ignore */ }
|
|
89
123
|
}
|
|
124
|
+
|
|
125
|
+
waitForDrain(timeoutMs: number): Promise<void> {
|
|
126
|
+
const proc = this.activeProcess;
|
|
127
|
+
if (!proc) return Promise.resolve();
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
let settled = false;
|
|
130
|
+
const settle = () => {
|
|
131
|
+
if (settled) return;
|
|
132
|
+
settled = true;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
resolve();
|
|
135
|
+
};
|
|
136
|
+
const timer = setTimeout(settle, timeoutMs);
|
|
137
|
+
proc.once('close', settle);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
90
140
|
}
|
|
91
141
|
|
|
92
142
|
export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = process.env): StreamingAudioPlayerCommand | null {
|
|
93
143
|
const mpv = findExecutable('mpv', env);
|
|
94
144
|
if (mpv) {
|
|
145
|
+
// No --cache=no: mpv's read-ahead cache buffers the incoming pipe so the
|
|
146
|
+
// opening audio survives device-open latency and network jitter instead of
|
|
147
|
+
// underrunning while the output device is still spinning up.
|
|
95
148
|
return {
|
|
96
149
|
command: mpv,
|
|
97
|
-
args: ['--no-terminal', '--really-quiet', '--force-window=no', '
|
|
150
|
+
args: ['--no-terminal', '--really-quiet', '--force-window=no', '-'],
|
|
98
151
|
label: 'mpv',
|
|
99
152
|
};
|
|
100
153
|
}
|
|
@@ -102,18 +155,51 @@ export function resolveStreamingAudioPlayerCommand(env: NodeJS.ProcessEnv = proc
|
|
|
102
155
|
if (ffplay) {
|
|
103
156
|
return {
|
|
104
157
|
command: ffplay,
|
|
105
|
-
args:
|
|
158
|
+
args: FFPLAY_BASE_ARGS,
|
|
106
159
|
label: 'ffplay',
|
|
107
160
|
};
|
|
108
161
|
}
|
|
109
162
|
return null;
|
|
110
163
|
}
|
|
111
164
|
|
|
165
|
+
// ffplay -autoexit quits as soon as its input ends, before the audio output
|
|
166
|
+
// buffer has drained — that clips the tail of the response. `apad` appends a
|
|
167
|
+
// short run of silence so the real audio is fully played out and only the
|
|
168
|
+
// trailing silence gets trimmed.
|
|
169
|
+
const FFPLAY_APAD = ['-af', 'apad=pad_dur=0.3'] as const;
|
|
170
|
+
const FFPLAY_BASE_ARGS = ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-i', 'pipe:0'] as const;
|
|
171
|
+
|
|
112
172
|
function buildPlayerArgs(command: StreamingAudioPlayerCommand, format?: string): readonly string[] {
|
|
113
173
|
if (command.label !== 'ffplay' || !format) return command.args;
|
|
114
174
|
const normalized = format.trim().toLowerCase();
|
|
115
175
|
if (!normalized || normalized.includes('/')) return command.args;
|
|
116
|
-
return ['-nodisp', '-autoexit', '-loglevel', 'error', '-f', normalized, '-i', 'pipe:0'];
|
|
176
|
+
return ['-nodisp', '-autoexit', '-loglevel', 'error', ...FFPLAY_APAD, '-f', normalized, '-i', 'pipe:0'];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* awaitReady — resolves once the spawned player has actually started (its
|
|
181
|
+
* 'spawn' event), rejects if it fails to start ('error'), and resolves early
|
|
182
|
+
* if the caller aborts during startup so an intentional interrupt is never
|
|
183
|
+
* blocked. This is the readiness gate that keeps the first audio byte from
|
|
184
|
+
* being written into a not-yet-running sink.
|
|
185
|
+
*/
|
|
186
|
+
function awaitReady(proc: SpawnProcess, signal?: AbortSignal): Promise<void> {
|
|
187
|
+
if (signal?.aborted) return Promise.resolve();
|
|
188
|
+
return new Promise<void>((resolve, reject) => {
|
|
189
|
+
let settled = false;
|
|
190
|
+
const settle = (action: () => void) => {
|
|
191
|
+
if (settled) return;
|
|
192
|
+
settled = true;
|
|
193
|
+
signal?.removeEventListener('abort', onAbort);
|
|
194
|
+
action();
|
|
195
|
+
};
|
|
196
|
+
const onSpawn = () => settle(resolve);
|
|
197
|
+
const onError = (error: unknown) => settle(() => reject(error instanceof Error ? error : new Error(String(error))));
|
|
198
|
+
const onAbort = () => settle(resolve);
|
|
199
|
+
proc.once('spawn', onSpawn);
|
|
200
|
+
proc.once('error', onError);
|
|
201
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
202
|
+
});
|
|
117
203
|
}
|
|
118
204
|
|
|
119
205
|
function findExecutable(name: string, env: NodeJS.ProcessEnv): string | null {
|
|
@@ -64,8 +64,8 @@ export class SpokenTurnController {
|
|
|
64
64
|
/**
|
|
65
65
|
* Returns whether speech was actually ACTIVE when stopped. The notice only
|
|
66
66
|
* prints in that case — stop() on an idle controller used to notify anyway,
|
|
67
|
-
* spamming "[TTS] Spoken output stopped." on every Ctrl+C (
|
|
68
|
-
*
|
|
67
|
+
* spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay
|
|
68
|
+
* fix); callers use the return to decide whether the press "did a
|
|
69
69
|
* job" (see handleCtrlC's consume-on-speech-stop).
|
|
70
70
|
*/
|
|
71
71
|
stop(message?: string): boolean {
|
|
@@ -85,6 +85,29 @@ export class SpokenTurnController {
|
|
|
85
85
|
return wasActive;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Exit-path teardown: drops everything not yet audible (pending arm,
|
|
90
|
+
* buffered text, queued chunks) but lets the audio the user is already
|
|
91
|
+
* hearing finish naturally, capped at `drainTimeoutMs`, before the hard
|
|
92
|
+
* stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their
|
|
93
|
+
* instant path through stop(); this is only for exiting the app while the
|
|
94
|
+
* final audio of a completed response is still draining.
|
|
95
|
+
*/
|
|
96
|
+
async stopForExit(drainTimeoutMs = 2000): Promise<void> {
|
|
97
|
+
this.pendingPrompt = null;
|
|
98
|
+
this.activeTurnId = null;
|
|
99
|
+
this.chunker?.reset();
|
|
100
|
+
this.chunker = null;
|
|
101
|
+
this.stopTimer();
|
|
102
|
+
// Cancel chunks that have not started playing; the chunk currently in the
|
|
103
|
+
// sink is not in this set (its controller is released before playback).
|
|
104
|
+
for (const controller of this.abortControllers) controller.abort();
|
|
105
|
+
this.abortControllers.clear();
|
|
106
|
+
await this.player.waitForDrain(drainTimeoutMs);
|
|
107
|
+
// Backstop: anything still alive after the window is torn down hard.
|
|
108
|
+
this.stop();
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
handleTurnEvent(event: TurnEvent): void {
|
|
89
112
|
if (event.type === 'TURN_SUBMITTED') {
|
|
90
113
|
this.maybeStartTurn(event.turnId, event.prompt);
|
|
@@ -170,6 +193,11 @@ export class SpokenTurnController {
|
|
|
170
193
|
if (abortController.signal.aborted) return;
|
|
171
194
|
const result = await resultPromise;
|
|
172
195
|
this.abortControllers.delete(abortController);
|
|
196
|
+
// Re-check after the await: an abort that landed while synthesis was in
|
|
197
|
+
// flight (deliberate stop or exit) makes the rejection expected — it
|
|
198
|
+
// must not route into reportError, which would print a spurious error
|
|
199
|
+
// and hard-stop a sink that may still be draining the previous chunk.
|
|
200
|
+
if (abortController.signal.aborted) return;
|
|
173
201
|
if (!result.ok) {
|
|
174
202
|
this.reportError(result.error);
|
|
175
203
|
return;
|
|
@@ -10,6 +10,8 @@ export interface SpokenTurnRuntime {
|
|
|
10
10
|
submitNextTurn(prompt: string): boolean;
|
|
11
11
|
/** Returns whether speech was actually active (see controller.stop). */
|
|
12
12
|
stop(message?: string): boolean;
|
|
13
|
+
/** Exit-path stop: lets the audio already playing drain, bounded (see controller.stopForExit). */
|
|
14
|
+
stopForExit(drainTimeoutMs?: number): Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface WireSpokenTurnRuntimeOptions {
|
|
@@ -63,5 +65,6 @@ export function wireSpokenTurnRuntime(options: WireSpokenTurnRuntimeOptions): Sp
|
|
|
63
65
|
unsubs,
|
|
64
66
|
submitNextTurn: (prompt) => controller.submitNextTurn(prompt),
|
|
65
67
|
stop: (message) => controller.stop(message),
|
|
68
|
+
stopForExit: (drainTimeoutMs) => controller.stopForExit(drainTimeoutMs),
|
|
66
69
|
};
|
|
67
70
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* credential-availability.ts —
|
|
2
|
+
* credential-availability.ts — client-side credential-status read.
|
|
3
3
|
*
|
|
4
4
|
* When the TUI acts as a CLIENT of an adopted external daemon (the host-service
|
|
5
5
|
* `mode === 'external'` topology — see runtime/bootstrap.ts), provider/model/secret
|
package/src/config/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
|
|
|
56
56
|
|
|
57
57
|
export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Client-side credential-status read (honest-degrade, status-only). When the
|
|
60
60
|
// TUI acts as a client of an adopted external daemon it reads provider/model/secret
|
|
61
61
|
// STATUS from that daemon's `credentials.get`, never from its own surfaceRoot store.
|
|
62
62
|
export {
|
|
@@ -8,7 +8,7 @@ import type { SpokenTurnRuntime } from '../audio/spoken-turn-wiring.ts';
|
|
|
8
8
|
* cap while still wiring `commandContext.isGenerating` (see W1.6 FIX 1).
|
|
9
9
|
*
|
|
10
10
|
* Returns whether SPEECH was actively stopped — a Ctrl+C that silenced live
|
|
11
|
-
* TTS is consumed by that job (
|
|
11
|
+
* TTS is consumed by that job (an earlier replay fix), same as a press that
|
|
12
12
|
* cleared a non-empty prompt; the quit chord starts from a quiet state.
|
|
13
13
|
*/
|
|
14
14
|
export function createCancelGeneration(
|
package/src/daemon/cli.ts
CHANGED
|
@@ -163,7 +163,7 @@ async function main(): Promise<void> {
|
|
|
163
163
|
logger.info('daemon: --hostname flag applied', { hostname: cliFlags.hostname });
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
//
|
|
166
|
+
// Service lifecycle: `install-service` / `uninstall-service` / `service-status` manage
|
|
167
167
|
// the systemd USER unit for the shared daemon. They run BEFORE the daemon boots —
|
|
168
168
|
// no runtime/services are constructed — and exit with the honest result code.
|
|
169
169
|
const serviceSubcommand = cli.positionals[0];
|
|
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
|
|
|
204
204
|
homeDirectory,
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
//
|
|
207
|
+
// Load persisted providers from disk so the provider registry is pre-populated
|
|
208
208
|
// on standalone daemon startup (same machinery the TUI uses after /scan).
|
|
209
209
|
const discoveryRoots = { homeDirectory, surfaceRoot: 'tui' };
|
|
210
210
|
const persistedProviders = loadPersistedProviders(discoveryRoots);
|
|
@@ -213,7 +213,7 @@ async function main(): Promise<void> {
|
|
|
213
213
|
logger.info('daemon: loaded persisted providers', { count: persistedProviders.length });
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Run a background LAN scan (non-blocking). Discovered servers are registered
|
|
217
217
|
// and persisted so subsequent daemon restarts benefit from the warm cache.
|
|
218
218
|
void scan().then((result) => {
|
|
219
219
|
if (result.servers.length > 0) {
|
|
@@ -245,7 +245,7 @@ async function main(): Promise<void> {
|
|
|
245
245
|
// If no explicit daemon token is set, use the companion token so mobile apps can connect.
|
|
246
246
|
const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
|
|
247
247
|
const companionTokenRecord = getOrCreateCompanionToken('tui', { daemonHomeDir });
|
|
248
|
-
//
|
|
248
|
+
// Fix (TUI 0.19.20): remove stale pre-0.21.28 workspace-scoped operator
|
|
249
249
|
// token files so only the canonical <daemonHomeDir>/operator-tokens.json survives.
|
|
250
250
|
const prune = pruneStaleOperatorTokens({
|
|
251
251
|
daemonHomeDir,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* TUI wiring — `goodvibes-daemon install-service | uninstall-service
|
|
3
3
|
* | service-status`.
|
|
4
4
|
*
|
|
5
5
|
* The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* on Windows) so N surfaces share ONE daemon that survives reboots, instead of
|
|
8
8
|
* every surface spawning a session-scoped daemon.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* bespoke systemd shim) was dead code and got deleted
|
|
10
|
+
* Drift note: the SDK's older `systemd-user-service.ts` (a Linux-only,
|
|
11
|
+
* bespoke systemd shim) was dead code and got deleted from the SDK. The SDK's
|
|
12
12
|
* REAL wired service machinery — reached in production by the daemon's own HTTP
|
|
13
13
|
* `/api/service/*` routes via facade-composition.ts — is
|
|
14
14
|
* `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* stray "enabled" symlink may remain until `systemctl --user daemon-reload`
|
|
27
27
|
* (offered back as a suggested follow-up) or the next login cleans it up.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* A fourth subcommand, `migrate-service`, closes the earlier "detect and
|
|
30
30
|
* disclose only" inheritance (W3 Finding 4, below) with a GUIDED, CONSENTED
|
|
31
31
|
* takeover of the legacy `goodvibes-daemon.service` unit. Design constraints,
|
|
32
32
|
* all load-bearing:
|
|
@@ -77,9 +77,9 @@ export {
|
|
|
77
77
|
export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migration.ts';
|
|
78
78
|
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
80
|
-
//
|
|
80
|
+
// Legacy-unit detection, plus guided migration.
|
|
81
81
|
//
|
|
82
|
-
// The prior
|
|
82
|
+
// The prior command installed the daemon's systemd unit under the
|
|
83
83
|
// literal name `goodvibes-daemon.service`. This module (rewired onto
|
|
84
84
|
// PlatformServiceManager, see the file banner above) manages a DIFFERENT
|
|
85
85
|
// unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
|
|
@@ -243,7 +243,7 @@ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServi
|
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
245
|
* `migrate-service`: the guided, consented takeover of the legacy
|
|
246
|
-
* `goodvibes-daemon.service` unit
|
|
246
|
+
* `goodvibes-daemon.service` unit. Thin wrapper over
|
|
247
247
|
* `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) — see
|
|
248
248
|
* that module for the design constraints (never auto-migrate,
|
|
249
249
|
* new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
|
|
@@ -314,7 +314,7 @@ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise
|
|
|
314
314
|
const extra: string[] = [];
|
|
315
315
|
if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
|
|
316
316
|
// W3 Finding 4: this command only ever touches the TRACKED unit
|
|
317
|
-
// (whatever name/path actually resolved
|
|
317
|
+
// (whatever name/path actually resolved — not necessarily the
|
|
318
318
|
// SERVICE_NAME constant) above — say so explicitly when a legacy unit
|
|
319
319
|
// also exists, so its continued presence is never a silent surprise.
|
|
320
320
|
if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
|
|
@@ -192,16 +192,16 @@ export interface CommandSessionServices {
|
|
|
192
192
|
* fresh Orchestrator's zeroed default (W0.9).
|
|
193
193
|
*/
|
|
194
194
|
readonly hydrateSessionUsage?: () => void;
|
|
195
|
-
/**
|
|
195
|
+
/** The orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
|
|
196
196
|
readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
|
|
197
|
-
/**
|
|
197
|
+
/** The repo source-tree code index — see runtime/code-index-services.ts. */
|
|
198
198
|
readonly codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
|
|
199
|
-
/**
|
|
199
|
+
/** Tool-site reindex scheduler — `/codebase status` reports its last activity. */
|
|
200
200
|
readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
|
|
201
|
-
/**
|
|
201
|
+
/** Whether the (default-off) `agent-passive-code-injection` flag is on — for `/codebase status`. */
|
|
202
202
|
readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
|
|
203
203
|
/**
|
|
204
|
-
*
|
|
204
|
+
* The MAIN interactive session's per-turn passive-injection
|
|
205
205
|
* honesty ring — `Orchestrator.getTurnInjections()`, the main-session
|
|
206
206
|
* counterpart to `AgentRecord.turnInjections` (wo801). `/recall injections`
|
|
207
207
|
* with no agent id reads this. Optional so command contexts built without an
|
|
@@ -10,7 +10,7 @@ export function registerConfigCommand(registry: CommandRegistry): void {
|
|
|
10
10
|
usage: '[category|key] | set <key> <value>',
|
|
11
11
|
argsHint: '[category|key] | set <key> <value>',
|
|
12
12
|
handler(args, ctx) {
|
|
13
|
-
//
|
|
13
|
+
// An earlier replay fix: `/config set <key> <value>` used to fall through to
|
|
14
14
|
// the workspace with the assignment silently ignored — the dishonest-
|
|
15
15
|
// fallthrough class. `set` is now a real verb.
|
|
16
16
|
if (args[0] === 'set') {
|
|
@@ -29,7 +29,7 @@ import { VALID_CLASSES, VALID_REVIEW_STATES, VALID_SCOPES } from './recall-share
|
|
|
29
29
|
* src/input/commands/session-content.ts) with no modal surface — so that
|
|
30
30
|
* work order deliberately did NOT touch /memory and used /recall instead.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
32
|
+
* Update from the core-verb naming pass (MEMORY fragmentation — worst-class
|
|
33
33
|
* collision #2): the agent's own `/memory` command was a plain alias for its
|
|
34
34
|
* `/recall`-equivalent the whole time, meaning "/memory" meant two unrelated
|
|
35
35
|
* things depending which surface you were on. The session-notes command was
|
|
@@ -262,7 +262,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
// Defense (
|
|
265
|
+
// Defense (review finding): a single verb-looking token is almost never a
|
|
266
266
|
// real planning goal — it is a mistyped or removed subcommand. The
|
|
267
267
|
// Planning modal used to dispatch `/plan dismiss`, which has no
|
|
268
268
|
// subcommand and silently fell through to this free-form branch, seeding
|
|
@@ -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
|
|
@@ -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;
|