@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.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 +53 -0
- package/dist/cli.js +3610 -3566
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/git.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli.ts +27 -5
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +11 -0
- package/src/cursor.ts +1 -1
- package/src/discovery/helpers.ts +8 -0
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/irc/bus.ts +5 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/tool-execution.ts +45 -12
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +16 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +163 -49
- package/src/session/session-history-format.ts +6 -2
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +16 -8
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +18 -36
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +9 -2
- package/src/tools/renderers.ts +11 -0
- package/src/tools/ssh.ts +17 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/git.ts +8 -0
package/src/tts/vocalizer.ts
CHANGED
|
@@ -2,32 +2,56 @@
|
|
|
2
2
|
* Streaming assistant speech-vocalization.
|
|
3
3
|
*
|
|
4
4
|
* The vocalizer turns the assistant's STREAMING output into spoken audio as a
|
|
5
|
-
* side effect of the normal turn.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* side effect of the normal turn. Two pipelines share the synthesis/playback
|
|
6
|
+
* tail:
|
|
7
|
+
*
|
|
8
|
+
* - Mechanical (default): deltas run through a {@link SpeakableStream} — which
|
|
9
|
+
* drops code/tables/markup, speaks link labels and URL hosts, and cuts
|
|
10
|
+
* speakable segments the moment a boundary appears — and each ready segment
|
|
11
|
+
* is pushed to the TTS worker.
|
|
12
|
+
* - Enhanced (`speech.enhanced`, requires a session-wired
|
|
13
|
+
* {@link SpeechEnhancer}): deltas accumulate into fence-aware markdown
|
|
14
|
+
* blocks; each block (small ones coalesced) is rewritten into natural spoken
|
|
15
|
+
* prose by the tiny/smol model — at most {@link MAX_REWRITES_IN_FLIGHT}
|
|
16
|
+
* completions in flight, results pushed strictly in order — then segmented
|
|
17
|
+
* by the same {@link SpeakableStream} as a safety net. A failed or timed-out
|
|
18
|
+
* rewrite falls back to mechanical cleanup of the raw block, so speech never
|
|
19
|
+
* blocks on the model.
|
|
20
|
+
*
|
|
21
|
+
* In both modes the synthesis session (worker, player) opens lazily on the
|
|
22
|
+
* first speakable segment, and an idle timer speaks buffered text when
|
|
23
|
+
* generation stalls (tool call, thinking block). One
|
|
24
|
+
* {@link StreamingAudioPlayer} per utterance plays chunks back gaplessly;
|
|
25
|
+
* utterance playback is chained so sequential utterances never overlap.
|
|
11
26
|
*
|
|
12
27
|
* Overspeech control:
|
|
13
|
-
* - {@link clear} stops playback instantly (kills
|
|
14
|
-
* in-flight synthesis — wired to a new turn, an
|
|
15
|
-
* sent message.
|
|
28
|
+
* - {@link clear} stops playback instantly (kills every live player) and
|
|
29
|
+
* aborts in-flight synthesis and rewrites — wired to a new turn, an
|
|
30
|
+
* Esc/Ctrl+C interrupt, and a sent message.
|
|
16
31
|
* - {@link duck}/{@link unduck} lower/restore the volume while the user is
|
|
17
32
|
* speaking (push-to-talk), so the assistant doesn't talk over them.
|
|
18
|
-
* - Sessions are chained, so sequential utterances queue and drain in order
|
|
19
|
-
* rather than overlapping.
|
|
20
33
|
*
|
|
21
34
|
* Errors are swallowed (debug-logged) so a synthesis or playback failure never
|
|
22
35
|
* throws into the turn. A process-level singleton ({@link vocalizer}) is shared
|
|
23
|
-
* by the event controller (streaming deltas) and the ask tool (spoken
|
|
36
|
+
* by the event controller (streaming deltas) and the ask tool (spoken
|
|
37
|
+
* questions); the event controller wires the per-session enhancer via
|
|
38
|
+
* {@link Vocalizer.setEnhancer}.
|
|
24
39
|
*/
|
|
25
40
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
26
41
|
import { settings } from "../config/settings";
|
|
27
42
|
import { DEFAULT_TTS_VOICE } from "./models";
|
|
43
|
+
import { SpeakableStream } from "./speakable";
|
|
44
|
+
import { BlockAccumulator, type SpeechEnhancer } from "./speech-enhancer";
|
|
28
45
|
import { createStreamingPlayer, DUCK_GAIN } from "./streaming-player";
|
|
29
46
|
import { type TtsStreamHandle, ttsClient } from "./tts-client";
|
|
30
47
|
|
|
48
|
+
/** Quiet time on the delta stream before the buffered partial is spoken. */
|
|
49
|
+
const IDLE_FLUSH_MS = 1000;
|
|
50
|
+
/** Coalesce completed blocks until this many chars before one rewrite call. */
|
|
51
|
+
const COALESCE_MIN_CHARS = 400;
|
|
52
|
+
/** Bounded rewrite concurrency across utterances. */
|
|
53
|
+
const MAX_REWRITES_IN_FLIGHT = 2;
|
|
54
|
+
|
|
31
55
|
export interface VocalizerPlayer {
|
|
32
56
|
start(sampleRate: number): void;
|
|
33
57
|
write(pcm: Float32Array): void;
|
|
@@ -36,40 +60,113 @@ export interface VocalizerPlayer {
|
|
|
36
60
|
stop(): void;
|
|
37
61
|
}
|
|
38
62
|
|
|
63
|
+
/**
|
|
64
|
+
* State of one enhanced-mode utterance. Detached from the vocalizer at
|
|
65
|
+
* {@link Vocalizer.flush} so in-flight rewrites finish and the session closes
|
|
66
|
+
* after the last ordered push, even while the next utterance streams.
|
|
67
|
+
*/
|
|
68
|
+
interface EnhancedUtterance {
|
|
69
|
+
blocks: BlockAccumulator;
|
|
70
|
+
/** Segments the (rewritten) prose; also the safety net for leaked markup. */
|
|
71
|
+
speakable: SpeakableStream;
|
|
72
|
+
/** Completed blocks held back for coalescing into one rewrite call. */
|
|
73
|
+
pending: string[];
|
|
74
|
+
pendingChars: number;
|
|
75
|
+
/** First rewrite skips coalescing for fast time-to-first-audio. */
|
|
76
|
+
dispatchedFirst: boolean;
|
|
77
|
+
/** FIFO chain that pushes rewrite results in block order. */
|
|
78
|
+
order: Promise<void>;
|
|
79
|
+
handle: TtsStreamHandle | null;
|
|
80
|
+
/** Aborted by {@link Vocalizer.clear} (current or detached); gates every deferred step. */
|
|
81
|
+
abort: AbortController;
|
|
82
|
+
}
|
|
83
|
+
|
|
39
84
|
export class Vocalizer {
|
|
40
|
-
/**
|
|
85
|
+
/** Mechanical mode: open stream session for the current utterance. */
|
|
41
86
|
#handle: TtsStreamHandle | null = null;
|
|
42
|
-
/**
|
|
43
|
-
#
|
|
44
|
-
/**
|
|
45
|
-
#
|
|
46
|
-
/**
|
|
87
|
+
/** Mechanical mode: markdown → segment transform for the current utterance. */
|
|
88
|
+
#speakable: SpeakableStream | null = null;
|
|
89
|
+
/** Enhanced mode: the utterance currently receiving deltas. */
|
|
90
|
+
#enhanced: EnhancedUtterance | null = null;
|
|
91
|
+
/** Per-session rewrite service; wired by the event controller, null elsewhere. */
|
|
92
|
+
#enhancer: SpeechEnhancer | null = null;
|
|
93
|
+
/** Fires when the delta stream goes quiet mid-sentence; speaks the partial. */
|
|
94
|
+
#idleTimer: NodeJS.Timeout | null = null;
|
|
95
|
+
/** Abort controllers of every not-yet-finished utterance; all aborted on {@link clear}. */
|
|
96
|
+
#liveAborts = new Set<AbortController>();
|
|
97
|
+
/** Players of every not-yet-finished utterance; ducked together, stopped on {@link clear}. */
|
|
98
|
+
#livePlayers = new Set<VocalizerPlayer>();
|
|
99
|
+
/** Serialized playback chain across utterances; awaited by {@link idle}. */
|
|
47
100
|
#chain: Promise<void> = Promise.resolve();
|
|
48
101
|
/** Whether the user is currently speaking; new sessions open ducked. */
|
|
49
102
|
#ducked = false;
|
|
103
|
+
/** Available rewrite slots; blocks queue when exhausted. */
|
|
104
|
+
#rewriteSlots = MAX_REWRITES_IN_FLIGHT;
|
|
105
|
+
#slotWaiters: Array<() => void> = [];
|
|
50
106
|
#createPlayer: () => VocalizerPlayer;
|
|
51
107
|
|
|
52
108
|
constructor(createPlayer: () => VocalizerPlayer = createStreamingPlayer) {
|
|
53
109
|
this.#createPlayer = createPlayer;
|
|
54
110
|
}
|
|
55
111
|
|
|
112
|
+
/** Wire (or drop) the per-session enhanced-rewrite service. */
|
|
113
|
+
setEnhancer(enhancer: SpeechEnhancer | null): void {
|
|
114
|
+
this.#enhancer = enhancer;
|
|
115
|
+
}
|
|
116
|
+
|
|
56
117
|
/**
|
|
57
|
-
* Stream a delta of assistant text into the
|
|
58
|
-
* is disabled. The
|
|
59
|
-
*
|
|
118
|
+
* Stream a delta of assistant text into the pipeline. No-op when
|
|
119
|
+
* vocalization is disabled. The synthesis session (worker, player) is only
|
|
120
|
+
* opened once the first speakable segment exists, so a reply that
|
|
121
|
+
* normalizes to silence (pure code, tables, URLs) costs nothing. The
|
|
122
|
+
* trailing partial is flushed by {@link flush} or the idle timer. The
|
|
123
|
+
* pipeline (enhanced vs mechanical) is latched per utterance.
|
|
60
124
|
*/
|
|
61
125
|
pushDelta(text: string): void {
|
|
62
126
|
if (!settings.get("speech.enabled")) return;
|
|
63
127
|
if (!text) return;
|
|
64
|
-
this.#
|
|
128
|
+
if (this.#enhanced || (!this.#speakable && this.#enhancer && settings.get("speech.enhanced"))) {
|
|
129
|
+
this.#pushEnhanced(text);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.#speakable ??= new SpeakableStream();
|
|
133
|
+
const speakable = this.#speakable;
|
|
134
|
+
this.#pushSegments(speakable.push(text));
|
|
135
|
+
this.#armIdle(() => {
|
|
136
|
+
if (this.#speakable !== speakable) return;
|
|
137
|
+
this.#pushSegments(speakable.flushIdle());
|
|
138
|
+
});
|
|
65
139
|
}
|
|
66
140
|
|
|
67
141
|
/**
|
|
68
|
-
* Close the current input stream (call at message/turn end).
|
|
69
|
-
*
|
|
70
|
-
*
|
|
142
|
+
* Close the current input stream (call at message/turn end). Drains the
|
|
143
|
+
* trailing partial as final segments; in enhanced mode the session ends
|
|
144
|
+
* only after the last in-flight rewrite has pushed, while the next
|
|
145
|
+
* utterance may already be streaming.
|
|
71
146
|
*/
|
|
72
147
|
flush(): void {
|
|
148
|
+
this.#clearIdleTimer();
|
|
149
|
+
const utterance = this.#enhanced;
|
|
150
|
+
if (utterance) {
|
|
151
|
+
this.#enhanced = null;
|
|
152
|
+
const last = utterance.blocks.flush();
|
|
153
|
+
if (last !== null) {
|
|
154
|
+
utterance.pending.push(last);
|
|
155
|
+
utterance.pendingChars += last.length;
|
|
156
|
+
}
|
|
157
|
+
this.#dispatchPending(utterance);
|
|
158
|
+
utterance.order = utterance.order.then(() => {
|
|
159
|
+
if (utterance.abort.signal.aborted) return;
|
|
160
|
+
utterance.handle?.end();
|
|
161
|
+
// A rewrite-only utterance (nothing speakable, no session) has no
|
|
162
|
+
// playback teardown to reap its abort controller — do it here.
|
|
163
|
+
if (!utterance.handle) this.#liveAborts.delete(utterance.abort);
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const speakable = this.#speakable;
|
|
168
|
+
this.#speakable = null;
|
|
169
|
+
if (speakable) this.#pushSegments(speakable.flush());
|
|
73
170
|
this.#handle?.end();
|
|
74
171
|
this.#handle = null;
|
|
75
172
|
}
|
|
@@ -79,34 +176,35 @@ export class Vocalizer {
|
|
|
79
176
|
* message): stream it in and immediately close the input. No-op when disabled.
|
|
80
177
|
*/
|
|
81
178
|
speak(text: string): void {
|
|
82
|
-
|
|
83
|
-
if (!text) return;
|
|
84
|
-
this.#ensureSession().push(text);
|
|
179
|
+
this.pushDelta(text);
|
|
85
180
|
this.flush();
|
|
86
181
|
}
|
|
87
182
|
|
|
88
183
|
/**
|
|
89
|
-
* Interrupt and drop
|
|
90
|
-
*
|
|
184
|
+
* Interrupt and drop every utterance, killing in-flight playback, synthesis,
|
|
185
|
+
* and rewrites (new turn / user message / Esc interrupt). Audio stops at once.
|
|
91
186
|
*/
|
|
92
187
|
clear(): void {
|
|
188
|
+
this.#clearIdleTimer();
|
|
189
|
+
this.#enhanced = null;
|
|
190
|
+
this.#speakable = null;
|
|
93
191
|
this.#handle = null;
|
|
94
|
-
this.#abort
|
|
95
|
-
this.#
|
|
96
|
-
this.#player
|
|
97
|
-
this.#
|
|
192
|
+
for (const abort of this.#liveAborts) abort.abort();
|
|
193
|
+
this.#liveAborts.clear();
|
|
194
|
+
for (const player of this.#livePlayers) player.stop();
|
|
195
|
+
this.#livePlayers.clear();
|
|
98
196
|
}
|
|
99
197
|
|
|
100
198
|
/** Lower the volume while the user is speaking (push-to-talk), so speech doesn't drown them out. */
|
|
101
199
|
duck(): void {
|
|
102
200
|
this.#ducked = true;
|
|
103
|
-
this.#player
|
|
201
|
+
for (const player of this.#livePlayers) player.setGain(DUCK_GAIN);
|
|
104
202
|
}
|
|
105
203
|
|
|
106
204
|
/** Restore full volume once the user stops speaking. */
|
|
107
205
|
unduck(): void {
|
|
108
206
|
this.#ducked = false;
|
|
109
|
-
this.#player
|
|
207
|
+
for (const player of this.#livePlayers) player.setGain(1);
|
|
110
208
|
}
|
|
111
209
|
|
|
112
210
|
/** Resolve once the playback chain has drained (tests / shutdown). */
|
|
@@ -114,26 +212,159 @@ export class Vocalizer {
|
|
|
114
212
|
return this.#chain;
|
|
115
213
|
}
|
|
116
214
|
|
|
215
|
+
// --- Enhanced pipeline ---------------------------------------------------
|
|
216
|
+
|
|
217
|
+
#pushEnhanced(text: string): void {
|
|
218
|
+
if (!this.#enhanced) this.#enhanced = this.#newEnhancedUtterance();
|
|
219
|
+
const utterance = this.#enhanced;
|
|
220
|
+
for (const block of utterance.blocks.push(text)) {
|
|
221
|
+
utterance.pending.push(block);
|
|
222
|
+
utterance.pendingChars += block.length;
|
|
223
|
+
// The first rewrite dispatches immediately for fast first audio;
|
|
224
|
+
// later blocks coalesce so a heading and its list cost one call.
|
|
225
|
+
if (!utterance.dispatchedFirst || utterance.pendingChars >= COALESCE_MIN_CHARS) {
|
|
226
|
+
this.#dispatchPending(utterance);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.#armIdle(() => {
|
|
230
|
+
if (this.#enhanced !== utterance) return;
|
|
231
|
+
const partial = utterance.blocks.flushPartial();
|
|
232
|
+
if (partial !== null) {
|
|
233
|
+
utterance.pending.push(partial);
|
|
234
|
+
utterance.pendingChars += partial.length;
|
|
235
|
+
}
|
|
236
|
+
this.#dispatchPending(utterance);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#newEnhancedUtterance(): EnhancedUtterance {
|
|
241
|
+
const abort = new AbortController();
|
|
242
|
+
this.#liveAborts.add(abort);
|
|
243
|
+
return {
|
|
244
|
+
blocks: new BlockAccumulator(),
|
|
245
|
+
speakable: new SpeakableStream(),
|
|
246
|
+
pending: [],
|
|
247
|
+
pendingChars: 0,
|
|
248
|
+
dispatchedFirst: false,
|
|
249
|
+
order: Promise.resolve(),
|
|
250
|
+
handle: null,
|
|
251
|
+
abort,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
117
255
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
256
|
+
* Send the coalesced pending blocks to the rewriter and chain the ordered
|
|
257
|
+
* push of the result. The completion runs concurrently (bounded by the
|
|
258
|
+
* slot pool); only the push into the synthesizer is serialized, so block
|
|
259
|
+
* N+1's rewrite overlaps block N's synthesis.
|
|
120
260
|
*/
|
|
121
|
-
#
|
|
122
|
-
if (
|
|
261
|
+
#dispatchPending(utterance: EnhancedUtterance): void {
|
|
262
|
+
if (utterance.pending.length === 0) return;
|
|
263
|
+
const block = utterance.pending.join("\n\n");
|
|
264
|
+
utterance.pending = [];
|
|
265
|
+
utterance.pendingChars = 0;
|
|
266
|
+
utterance.dispatchedFirst = true;
|
|
267
|
+
const result = this.#boundedRewrite(block, utterance.abort.signal);
|
|
268
|
+
utterance.order = utterance.order.then(async () => {
|
|
269
|
+
const rewritten = await result;
|
|
270
|
+
if (utterance.abort.signal.aborted) return;
|
|
271
|
+
// Empty string: the model judged the block unspeakable (pure code).
|
|
272
|
+
if (rewritten === "") return;
|
|
273
|
+
// Null: rewrite failed/timed out — mechanical cleanup of the raw
|
|
274
|
+
// block via the SpeakableStream keeps speech flowing.
|
|
275
|
+
const spoken = rewritten ?? block;
|
|
276
|
+
const segments = utterance.speakable.push(spoken.endsWith("\n") ? spoken : `${spoken}\n`);
|
|
277
|
+
if (segments.length === 0) return;
|
|
278
|
+
if (!utterance.handle) utterance.handle = this.#openSession(utterance.abort);
|
|
279
|
+
for (const segment of segments) utterance.handle.push(segment);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Run one rewrite through the bounded slot pool; never rejects. */
|
|
284
|
+
async #boundedRewrite(block: string, signal: AbortSignal): Promise<string | null> {
|
|
285
|
+
await this.#acquireSlot();
|
|
286
|
+
try {
|
|
287
|
+
if (signal.aborted || !this.#enhancer) return null;
|
|
288
|
+
return await this.#enhancer.rewrite(block, signal);
|
|
289
|
+
} finally {
|
|
290
|
+
this.#releaseSlot();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#acquireSlot(): Promise<void> {
|
|
295
|
+
if (this.#rewriteSlots > 0) {
|
|
296
|
+
this.#rewriteSlots--;
|
|
297
|
+
return Promise.resolve();
|
|
298
|
+
}
|
|
299
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
300
|
+
this.#slotWaiters.push(resolve);
|
|
301
|
+
return promise;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#releaseSlot(): void {
|
|
305
|
+
const next = this.#slotWaiters.shift();
|
|
306
|
+
if (next) next();
|
|
307
|
+
else this.#rewriteSlots++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Shared synthesis/playback tail --------------------------------------
|
|
311
|
+
|
|
312
|
+
/** Mechanical mode: feed ready segments, opening the session lazily. */
|
|
313
|
+
#pushSegments(segments: string[]): void {
|
|
314
|
+
if (segments.length === 0) return;
|
|
315
|
+
if (!this.#handle) {
|
|
316
|
+
const abort = new AbortController();
|
|
317
|
+
this.#liveAborts.add(abort);
|
|
318
|
+
this.#handle = this.#openSession(abort);
|
|
319
|
+
}
|
|
320
|
+
for (const segment of segments) this.#handle.push(segment);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Open a streaming-synthesis session and chain its playback after any
|
|
325
|
+
* prior utterance's, so sequential utterances never overlap.
|
|
326
|
+
*/
|
|
327
|
+
#openSession(abort: AbortController): TtsStreamHandle {
|
|
123
328
|
const modelKey = settings.get("tts.localModel");
|
|
124
329
|
const voice = settings.get("speech.voice") || DEFAULT_TTS_VOICE;
|
|
125
|
-
const abort = new AbortController();
|
|
126
|
-
this.#abort = abort;
|
|
127
330
|
const handle = ttsClient.synthesizeStream(modelKey, { voice, signal: abort.signal });
|
|
128
|
-
this.#handle = handle;
|
|
129
331
|
const player = this.#createPlayer();
|
|
130
332
|
player.setGain(this.#ducked ? DUCK_GAIN : 1);
|
|
131
|
-
this.#
|
|
132
|
-
this.#
|
|
333
|
+
this.#liveAborts.add(abort);
|
|
334
|
+
this.#livePlayers.add(player);
|
|
335
|
+
this.#chain = this.#chain.then(async () => {
|
|
336
|
+
try {
|
|
337
|
+
await this.#play(handle, player, abort.signal);
|
|
338
|
+
} finally {
|
|
339
|
+
this.#liveAborts.delete(abort);
|
|
340
|
+
this.#livePlayers.delete(player);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
133
343
|
return handle;
|
|
134
344
|
}
|
|
135
345
|
|
|
136
|
-
/**
|
|
346
|
+
/**
|
|
347
|
+
* (Re)arm the stall timer: if no delta arrives for {@link IDLE_FLUSH_MS},
|
|
348
|
+
* speak buffered text instead of holding it through a tool call or
|
|
349
|
+
* thinking block. The callback checks utterance identity itself.
|
|
350
|
+
*/
|
|
351
|
+
#armIdle(onIdle: () => void): void {
|
|
352
|
+
this.#clearIdleTimer();
|
|
353
|
+
const timer = setTimeout(() => {
|
|
354
|
+
this.#idleTimer = null;
|
|
355
|
+
onIdle();
|
|
356
|
+
}, IDLE_FLUSH_MS);
|
|
357
|
+
timer.unref?.();
|
|
358
|
+
this.#idleTimer = timer;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#clearIdleTimer(): void {
|
|
362
|
+
if (this.#idleTimer === null) return;
|
|
363
|
+
clearTimeout(this.#idleTimer);
|
|
364
|
+
this.#idleTimer = null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Feed each synthesized segment into the player in arrival order; abort stops it. */
|
|
137
368
|
async #play(handle: TtsStreamHandle, player: VocalizerPlayer, signal: AbortSignal): Promise<void> {
|
|
138
369
|
let started = false;
|
|
139
370
|
try {
|
package/src/utils/git.ts
CHANGED
|
@@ -1919,6 +1919,14 @@ export const ls = {
|
|
|
1919
1919
|
return ls.files(cwd, { others: true, excludeStandard: true, signal });
|
|
1920
1920
|
},
|
|
1921
1921
|
|
|
1922
|
+
/** List paths present in a ref, optionally filtered to specific paths. */
|
|
1923
|
+
async tree(cwd: string, ref: string, files: readonly string[] = [], signal?: AbortSignal): Promise<string[]> {
|
|
1924
|
+
const args = ["ls-tree", "--name-only", "-r", "-z", ref];
|
|
1925
|
+
if (files.length > 0) args.push("--", ...files);
|
|
1926
|
+
const raw = await runText(cwd, args, { readOnly: true, signal });
|
|
1927
|
+
return raw.split("\0").filter(entry => entry.length > 0);
|
|
1928
|
+
},
|
|
1929
|
+
|
|
1922
1930
|
/** List submodule paths (recursive). */
|
|
1923
1931
|
async submodules(cwd: string, signal?: AbortSignal): Promise<string[]> {
|
|
1924
1932
|
const output = await git(cwd, ["submodule", "--quiet", "foreach", "--recursive", "echo $sm_path"], {
|