@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.1
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 +24 -0
- package/dist/cli.js +3686 -4013
- package/dist/types/config/settings-schema.d.ts +58 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +10 -2
- package/dist/types/live/protocol.d.ts +1 -1
- package/dist/types/live/transport.d.ts +6 -19
- package/dist/types/live/visualizer.d.ts +8 -11
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/session/agent-session-types.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +20 -1
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/eval-runner.d.ts +2 -0
- package/dist/types/session/messages.d.ts +2 -0
- package/dist/types/session/session-tools.d.ts +15 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tts/streaming-player.d.ts +10 -43
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli.ts +8 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/settings-schema.ts +61 -0
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/exec/bash-executor.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +76 -23
- package/src/live/protocol.test.ts +3 -3
- package/src/live/protocol.ts +1 -1
- package/src/live/transport.ts +72 -140
- package/src/live/visualizer.ts +114 -134
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/tool-execution.ts +17 -110
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +82 -5
- package/src/modes/controllers/selector-controller.ts +62 -0
- package/src/modes/interactive-mode.ts +19 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session-types.ts +9 -0
- package/src/session/agent-session.ts +56 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/eval-runner.ts +5 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-tools.ts +37 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/slash-commands/builtin-registry.ts +165 -9
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +4 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/bash-interactive.ts +90 -86
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/index.ts +5 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/xdev.ts +54 -26
- package/src/tts/streaming-player.ts +81 -340
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/live/audio-worklet.txt +0 -59
- package/src/live/browser-runtime.txt +0 -221
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { AudioCapture } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
3
|
import { settings } from "../config/settings";
|
|
6
4
|
import { type SttStreamHandle, sttClient } from "./asr-client";
|
|
7
5
|
import { downloadSttModel, isSttModelCached } from "./downloader";
|
|
8
6
|
import { resolveSttModelSpec } from "./models";
|
|
9
|
-
import {
|
|
10
|
-
detectRecorder,
|
|
11
|
-
ensureRecorder,
|
|
12
|
-
type RecordingHandle,
|
|
13
|
-
type StreamingRecordingHandle,
|
|
14
|
-
startRecording,
|
|
15
|
-
startStreamingRecording,
|
|
16
|
-
verifyRecordingFile,
|
|
17
|
-
} from "./recorder";
|
|
18
7
|
import { evaluateSubmitTrigger } from "./submit-trigger";
|
|
19
|
-
import { transcribe } from "./transcriber";
|
|
20
8
|
|
|
21
9
|
export type SttState = "idle" | "recording" | "transcribing";
|
|
22
10
|
|
|
@@ -38,26 +26,34 @@ interface Editor {
|
|
|
38
26
|
deleteBeforeCursor(count: number): void;
|
|
39
27
|
}
|
|
40
28
|
|
|
29
|
+
interface CaptureHandle {
|
|
30
|
+
stop(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type CaptureFactory = (onAudio: (error: Error | null, samples: Float32Array) => void) => CaptureHandle;
|
|
34
|
+
|
|
35
|
+
/** Coordinates native microphone capture with incremental local transcription. */
|
|
41
36
|
export class STTController {
|
|
42
37
|
#state: SttState = "idle";
|
|
43
38
|
#resolvedModelKey: string | null = null;
|
|
44
39
|
#toggling = false;
|
|
45
40
|
#stopAfterStart = false;
|
|
46
41
|
#disposed = false;
|
|
47
|
-
|
|
48
|
-
// Batch (single-shot) capture.
|
|
49
|
-
#recordingHandle: RecordingHandle | null = null;
|
|
50
|
-
#tempFile: string | null = null;
|
|
51
|
-
#transcriptionAbort: AbortController | null = null;
|
|
42
|
+
readonly #createCapture: CaptureFactory;
|
|
52
43
|
|
|
53
44
|
// Live streaming capture.
|
|
54
45
|
#stream: SttStreamHandle | null = null;
|
|
55
|
-
#streamRecorder:
|
|
46
|
+
#streamRecorder: CaptureHandle | null = null;
|
|
56
47
|
#streamEditor: Editor | null = null;
|
|
57
48
|
#streamCommitted = false;
|
|
58
49
|
#streamAbort: AbortController | null = null;
|
|
59
50
|
#streamUtterance = "";
|
|
60
51
|
|
|
52
|
+
/** Creates a controller; tests may replace the hardware capture boundary. */
|
|
53
|
+
constructor(createCapture: CaptureFactory = onAudio => new AudioCapture(16_000, onAudio)) {
|
|
54
|
+
this.#createCapture = createCapture;
|
|
55
|
+
}
|
|
56
|
+
|
|
61
57
|
get state(): SttState {
|
|
62
58
|
return this.#state;
|
|
63
59
|
}
|
|
@@ -79,7 +75,7 @@ export class STTController {
|
|
|
79
75
|
await this.#start(editor, options);
|
|
80
76
|
break;
|
|
81
77
|
case "recording":
|
|
82
|
-
await this.#stop(
|
|
78
|
+
await this.#stop(options);
|
|
83
79
|
break;
|
|
84
80
|
case "transcribing":
|
|
85
81
|
options.showStatus("Transcription in progress...");
|
|
@@ -87,7 +83,7 @@ export class STTController {
|
|
|
87
83
|
}
|
|
88
84
|
if (this.#stopAfterStart && this.#state === "recording") {
|
|
89
85
|
this.#stopAfterStart = false;
|
|
90
|
-
await this.#stop(
|
|
86
|
+
await this.#stop(options);
|
|
91
87
|
} else if (this.#state !== "recording") {
|
|
92
88
|
this.#stopAfterStart = false;
|
|
93
89
|
}
|
|
@@ -103,19 +99,13 @@ export class STTController {
|
|
|
103
99
|
// (with progress) instead of blocking silently at stop.
|
|
104
100
|
if (this.#resolvedModelKey === modelKey) return true;
|
|
105
101
|
try {
|
|
106
|
-
// Only clear the status line
|
|
107
|
-
// fast path
|
|
108
|
-
// unconditional clear would be a stray write.
|
|
102
|
+
// Only clear the status line when preflight emitted progress; the
|
|
103
|
+
// cached-model fast path emits nothing.
|
|
109
104
|
let wroteStatus = false;
|
|
110
105
|
const status = (msg: string): void => {
|
|
111
106
|
wroteStatus = true;
|
|
112
107
|
options.showStatus(msg);
|
|
113
108
|
};
|
|
114
|
-
// A recorder is required to capture audio; startRecording /
|
|
115
|
-
// startStreamingRecording only *detect* a recorder and throw when none
|
|
116
|
-
// exists, so provision one here. Instant when sox/ffmpeg/arecord is on
|
|
117
|
-
// PATH — only a first-run static-ffmpeg download actually blocks.
|
|
118
|
-
await ensureRecorder(p => status(p.stage + (p.percent != null ? ` (${p.percent}%)` : "")));
|
|
119
109
|
// Loading the multi-hundred-MB speech model into the worker is what made
|
|
120
110
|
// the old "Checking STT dependencies…" step slow. Don't pay it before
|
|
121
111
|
// recording: when the weights are already cached, start now and warm the
|
|
@@ -157,30 +147,15 @@ export class STTController {
|
|
|
157
147
|
|
|
158
148
|
async #start(editor: Editor, options: ToggleOptions): Promise<void> {
|
|
159
149
|
if (!(await this.#ensureDeps(options))) return;
|
|
160
|
-
|
|
161
|
-
// PowerShell mci fallback records to a file, so it stays single-shot.
|
|
162
|
-
if (this.#recorderCanStream()) {
|
|
163
|
-
await this.#startStreaming(editor, options);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
await this.#startBatchRecording(options);
|
|
150
|
+
await this.#startStreaming(editor, options);
|
|
167
151
|
}
|
|
168
152
|
|
|
169
|
-
async #stop(
|
|
170
|
-
|
|
171
|
-
await this.#stopStreaming(options);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
await this.#stopBatch(editor, options);
|
|
153
|
+
async #stop(options: ToggleOptions): Promise<void> {
|
|
154
|
+
await this.#stopStreaming(options);
|
|
175
155
|
}
|
|
176
156
|
|
|
177
157
|
// ── Live streaming ──────────────────────────────────────────────
|
|
178
158
|
|
|
179
|
-
#recorderCanStream(): boolean {
|
|
180
|
-
const recorder = detectRecorder();
|
|
181
|
-
return recorder !== null && recorder.tool !== "powershell";
|
|
182
|
-
}
|
|
183
|
-
|
|
184
159
|
/** Segment text gets a leading space once a prior segment is committed, so
|
|
185
160
|
* phrases join naturally; the first phrase is inserted at the cursor as-is. */
|
|
186
161
|
#prefixed(text: string): string {
|
|
@@ -218,18 +193,38 @@ export class STTController {
|
|
|
218
193
|
},
|
|
219
194
|
});
|
|
220
195
|
this.#stream = stream;
|
|
221
|
-
let recorder:
|
|
196
|
+
let recorder: CaptureHandle;
|
|
222
197
|
try {
|
|
223
|
-
recorder =
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
198
|
+
recorder = this.#createCapture((error, samples) => {
|
|
199
|
+
if (this.#disposed || this.#stream !== stream || this.#state !== "recording") return;
|
|
200
|
+
if (error) {
|
|
201
|
+
logger.error("Native microphone capture failed", { error: error.message });
|
|
202
|
+
const activeRecorder = this.#streamRecorder;
|
|
203
|
+
this.#streamRecorder = null;
|
|
204
|
+
try {
|
|
205
|
+
activeRecorder?.stop();
|
|
206
|
+
} catch (cause) {
|
|
207
|
+
logger.debug("stt: microphone cleanup failed", {
|
|
208
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
this.#streamAbort?.abort(error);
|
|
212
|
+
stream.cancel();
|
|
213
|
+
this.#streamEditor?.clearVolatileText();
|
|
214
|
+
options.requestRender?.();
|
|
215
|
+
this.#cleanupStream();
|
|
216
|
+
this.#setState("idle", options);
|
|
217
|
+
options.showWarning(error.message);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
stream.pushAudio(samples);
|
|
227
221
|
});
|
|
228
|
-
}
|
|
229
|
-
if (!recorder) {
|
|
222
|
+
} catch (err) {
|
|
230
223
|
stream.cancel();
|
|
231
224
|
this.#cleanupStream();
|
|
232
|
-
|
|
225
|
+
const msg = err instanceof Error ? err.message : "Failed to start microphone capture";
|
|
226
|
+
options.showWarning(msg);
|
|
227
|
+
logger.error("STT recording failed to start", { error: msg });
|
|
233
228
|
return;
|
|
234
229
|
}
|
|
235
230
|
this.#streamRecorder = recorder;
|
|
@@ -247,7 +242,7 @@ export class STTController {
|
|
|
247
242
|
this.#setState("transcribing", options);
|
|
248
243
|
// Stop the mic first so no further audio is fed, then flush the worker.
|
|
249
244
|
try {
|
|
250
|
-
|
|
245
|
+
recorder?.stop();
|
|
251
246
|
} catch (err) {
|
|
252
247
|
logger.debug("stt: streaming recorder stop failed", {
|
|
253
248
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -306,103 +301,19 @@ export class STTController {
|
|
|
306
301
|
this.#streamUtterance = "";
|
|
307
302
|
}
|
|
308
303
|
|
|
309
|
-
// ── Batch (single-shot) ─────────────────────────────────────────
|
|
310
|
-
|
|
311
|
-
async #startBatchRecording(options: ToggleOptions): Promise<void> {
|
|
312
|
-
const id = Snowflake.next();
|
|
313
|
-
this.#tempFile = path.join(os.tmpdir(), `omp-stt-${id}.wav`);
|
|
314
|
-
try {
|
|
315
|
-
this.#recordingHandle = await startRecording(this.#tempFile);
|
|
316
|
-
this.#setState("recording", options);
|
|
317
|
-
logger.debug("STT recording started", { tempFile: this.#tempFile });
|
|
318
|
-
} catch (err) {
|
|
319
|
-
this.#tempFile = null;
|
|
320
|
-
const msg = err instanceof Error ? err.message : "Failed to start recording";
|
|
321
|
-
options.showWarning(msg);
|
|
322
|
-
logger.error("STT recording failed to start", { error: msg });
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
async #stopBatch(editor: Editor, options: ToggleOptions): Promise<void> {
|
|
327
|
-
const handle = this.#recordingHandle;
|
|
328
|
-
const tempFile = this.#tempFile;
|
|
329
|
-
this.#recordingHandle = null;
|
|
330
|
-
|
|
331
|
-
if (!handle || !tempFile) {
|
|
332
|
-
this.#setState("idle", options);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
await handle.stop();
|
|
338
|
-
// Validate the recording produced a usable file
|
|
339
|
-
await verifyRecordingFile(tempFile);
|
|
340
|
-
this.#setState("transcribing", options);
|
|
341
|
-
|
|
342
|
-
const sttSettings = {
|
|
343
|
-
modelName: settings.get("stt.modelName") as string | undefined,
|
|
344
|
-
language: settings.get("stt.language") as string | undefined,
|
|
345
|
-
};
|
|
346
|
-
this.#transcriptionAbort = new AbortController();
|
|
347
|
-
const text = await transcribe(tempFile, { ...sttSettings, signal: this.#transcriptionAbort.signal });
|
|
348
|
-
this.#transcriptionAbort = null;
|
|
349
|
-
if (this.#disposed) return;
|
|
350
|
-
if (text.length > 0) {
|
|
351
|
-
const trigger = settings.get("stt.submitTrigger");
|
|
352
|
-
const { submit, trimTrailing } = evaluateSubmitTrigger(text, trigger);
|
|
353
|
-
const textToInsert = trimTrailing > 0 ? text.slice(0, -trimTrailing) : text;
|
|
354
|
-
if (textToInsert.length > 0) {
|
|
355
|
-
editor.insertText(textToInsert);
|
|
356
|
-
}
|
|
357
|
-
options.showStatus("");
|
|
358
|
-
if (submit) {
|
|
359
|
-
editor.submit();
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
options.showStatus("No speech detected.");
|
|
363
|
-
}
|
|
364
|
-
if (!this.#disposed) this.#setState("idle", options);
|
|
365
|
-
} catch (err) {
|
|
366
|
-
if (this.#disposed) return;
|
|
367
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
368
|
-
this.#setState("idle", options);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const msg = err instanceof Error ? err.message : "Transcription failed";
|
|
372
|
-
options.showWarning(msg);
|
|
373
|
-
logger.error("STT transcription failed", { error: msg });
|
|
374
|
-
this.#setState("idle", options);
|
|
375
|
-
} finally {
|
|
376
|
-
try {
|
|
377
|
-
await fs.rm(tempFile, { force: true });
|
|
378
|
-
} catch {
|
|
379
|
-
// best effort cleanup
|
|
380
|
-
}
|
|
381
|
-
this.#tempFile = null;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
304
|
dispose(): void {
|
|
386
305
|
this.#disposed = true;
|
|
387
|
-
if (this.#transcriptionAbort) {
|
|
388
|
-
this.#transcriptionAbort.abort();
|
|
389
|
-
this.#transcriptionAbort = null;
|
|
390
|
-
}
|
|
391
306
|
if (this.#streamAbort) {
|
|
392
307
|
this.#streamAbort.abort();
|
|
393
308
|
this.#streamAbort = null;
|
|
394
309
|
}
|
|
395
310
|
this.#stream?.cancel();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
this.#recordingHandle = null;
|
|
401
|
-
}
|
|
402
|
-
if (this.#tempFile) {
|
|
403
|
-
fs.rm(this.#tempFile, { force: true }).catch(() => {});
|
|
404
|
-
this.#tempFile = null;
|
|
311
|
+
try {
|
|
312
|
+
this.#streamRecorder?.stop();
|
|
313
|
+
} catch {
|
|
314
|
+
// best effort cleanup
|
|
405
315
|
}
|
|
316
|
+
this.#cleanupStream();
|
|
406
317
|
this.#state = "idle";
|
|
407
318
|
this.#resolvedModelKey = null;
|
|
408
319
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { expandAtImports } from "./discovery/at-imports";
|
|
|
17
17
|
import { loadSkills, type Skill } from "./extensibility/skills";
|
|
18
18
|
import { hasObsidian } from "./internal-urls/vault-protocol";
|
|
19
19
|
import activeRepoContextTemplate from "./prompts/system/active-repo-context.md" with { type: "text" };
|
|
20
|
+
import computerSafetyPrompt from "./prompts/system/computer-safety.md" with { type: "text" };
|
|
20
21
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
21
22
|
import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
|
|
22
23
|
import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
|
|
@@ -839,6 +840,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
839
840
|
};
|
|
840
841
|
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
841
842
|
const systemPrompt = [rendered];
|
|
843
|
+
if (toolNames.includes("computer")) {
|
|
844
|
+
systemPrompt.push(computerSafetyPrompt.trim());
|
|
845
|
+
}
|
|
842
846
|
// Custom prompt templates already render context files and append text; the
|
|
843
847
|
// project footer still carries environment, cwd, workspace, and dir-context.
|
|
844
848
|
const projectPrompt = prompt
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -196,6 +196,28 @@ export class TinyTitleClient {
|
|
|
196
196
|
return () => this.#progressListeners.delete(listener);
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Spawn the tiny-model worker ahead of first use without loading any model.
|
|
201
|
+
* Called from idle TUI startup so the first {@link generate} reuses a live,
|
|
202
|
+
* unref'd subprocess instead of paying subprocess-spawn latency on the submit
|
|
203
|
+
* hot path (issue #6462). No-ops for online / non-local keys and for models
|
|
204
|
+
* already marked failed. A no-op `ping` round-trips the transport to fault in
|
|
205
|
+
* the worker's module graph; no pending request is registered, so
|
|
206
|
+
* {@link #syncWorkerRef} leaves the worker unref'd and idle sessions still exit.
|
|
207
|
+
*/
|
|
208
|
+
prewarm(modelKey: string): void {
|
|
209
|
+
if (!isTinyTitleLocalModelKey(modelKey) || this.#failedModels.has(modelKey)) return;
|
|
210
|
+
try {
|
|
211
|
+
const worker = this.#ensureWorker();
|
|
212
|
+
worker.send({ type: "ping", id: String(++this.#nextRequestId) });
|
|
213
|
+
} catch (error) {
|
|
214
|
+
logger.debug("tiny-title: prewarm failed", {
|
|
215
|
+
modelKey,
|
|
216
|
+
error: error instanceof Error ? error.message : String(error),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
199
221
|
async generate(modelKey: string, message: string, signal?: AbortSignal): Promise<string | null>;
|
|
200
222
|
async generate(modelKey: string, message: string, options?: TinyTitleGenerateOptions): Promise<string | null>;
|
|
201
223
|
async generate(
|
|
@@ -340,92 +340,96 @@ export async function runInteractiveBashPty(
|
|
|
340
340
|
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
341
341
|
maxColumns: resolveOutputMaxColumns(settings),
|
|
342
342
|
});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
() => {
|
|
388
|
-
try {
|
|
389
|
-
session.kill();
|
|
390
|
-
} catch {
|
|
391
|
-
// ignore
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
);
|
|
395
|
-
void session
|
|
396
|
-
.start(
|
|
397
|
-
{
|
|
398
|
-
command: options.command,
|
|
399
|
-
cwd: options.cwd,
|
|
400
|
-
timeoutMs: options.timeoutMs,
|
|
401
|
-
// Interactive PTY: inherit the user's environment (the Rust side
|
|
402
|
-
// applies these as overrides), with a real TERM so editors,
|
|
403
|
-
// pagers, and TUIs behave like a normal terminal.
|
|
404
|
-
env: {
|
|
405
|
-
TERM: "xterm-256color",
|
|
406
|
-
...options.env,
|
|
407
|
-
},
|
|
408
|
-
signal: options.signal,
|
|
409
|
-
cols,
|
|
410
|
-
rows,
|
|
411
|
-
shell: resolvedShell,
|
|
343
|
+
try {
|
|
344
|
+
const result = await ui.custom<BashInteractiveResult>(
|
|
345
|
+
(tui, uiTheme, _keybindings, done) => {
|
|
346
|
+
const session = new PtySession();
|
|
347
|
+
const component = new BashInteractiveOverlayComponent(
|
|
348
|
+
options.command,
|
|
349
|
+
uiTheme,
|
|
350
|
+
() => tui.terminal.rows,
|
|
351
|
+
XtermTerminal,
|
|
352
|
+
);
|
|
353
|
+
component.setSession(session);
|
|
354
|
+
let finished = false;
|
|
355
|
+
const finalize = (run: PtyRunResult) => {
|
|
356
|
+
if (finished) return;
|
|
357
|
+
finished = true;
|
|
358
|
+
component.setComplete({ exitCode: run.exitCode, cancelled: run.cancelled, timedOut: run.timedOut });
|
|
359
|
+
tui.requestRender();
|
|
360
|
+
void (async () => {
|
|
361
|
+
await component.flushOutput();
|
|
362
|
+
const summary = await sink.dump();
|
|
363
|
+
done({
|
|
364
|
+
exitCode: run.exitCode,
|
|
365
|
+
cancelled: run.cancelled,
|
|
366
|
+
timedOut: run.timedOut,
|
|
367
|
+
...summary,
|
|
368
|
+
});
|
|
369
|
+
})();
|
|
370
|
+
};
|
|
371
|
+
const cols = Math.max(20, tui.terminal.columns - 2);
|
|
372
|
+
const rows = Math.max(5, tui.terminal.rows - 4);
|
|
373
|
+
component.setHandlers(
|
|
374
|
+
data => {
|
|
375
|
+
try {
|
|
376
|
+
session.write(data);
|
|
377
|
+
} catch {
|
|
378
|
+
// ignore writes after command exits
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
() => {
|
|
382
|
+
try {
|
|
383
|
+
session.kill();
|
|
384
|
+
} catch {
|
|
385
|
+
// ignore
|
|
386
|
+
}
|
|
412
387
|
},
|
|
413
|
-
(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
388
|
+
() => {
|
|
389
|
+
try {
|
|
390
|
+
session.kill();
|
|
391
|
+
} catch {
|
|
392
|
+
// ignore
|
|
393
|
+
}
|
|
419
394
|
},
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
395
|
+
);
|
|
396
|
+
void session
|
|
397
|
+
.start(
|
|
398
|
+
{
|
|
399
|
+
command: options.command,
|
|
400
|
+
cwd: options.cwd,
|
|
401
|
+
timeoutMs: options.timeoutMs,
|
|
402
|
+
// Interactive PTY: inherit the user's environment (the Rust side
|
|
403
|
+
// applies these as overrides), with a real TERM so editors,
|
|
404
|
+
// pagers, and TUIs behave like a normal terminal.
|
|
405
|
+
env: {
|
|
406
|
+
TERM: "xterm-256color",
|
|
407
|
+
...options.env,
|
|
408
|
+
},
|
|
409
|
+
signal: options.signal,
|
|
410
|
+
cols,
|
|
411
|
+
rows,
|
|
412
|
+
shell: resolvedShell,
|
|
413
|
+
},
|
|
414
|
+
(err, chunk) => {
|
|
415
|
+
if (finished || err || !chunk) return;
|
|
416
|
+
component.appendOutput(chunk);
|
|
417
|
+
const normalizedChunk = normalizeCaptureChunk(chunk);
|
|
418
|
+
sink.push(normalizedChunk);
|
|
419
|
+
tui.requestRender();
|
|
420
|
+
},
|
|
421
|
+
)
|
|
422
|
+
.then(finalize)
|
|
423
|
+
.catch(error => {
|
|
424
|
+
sink.push(`PTY error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
425
|
+
finalize({ exitCode: undefined, cancelled: false, timedOut: false });
|
|
426
|
+
});
|
|
427
|
+
return component;
|
|
428
|
+
},
|
|
429
|
+
{ overlay: true },
|
|
430
|
+
);
|
|
431
|
+
return result;
|
|
432
|
+
} finally {
|
|
433
|
+
await sink.dispose();
|
|
434
|
+
}
|
|
431
435
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
|
|
2
|
+
|
|
3
|
+
export const COMPUTER_WORKER_ARG = "__omp_worker_computer";
|
|
4
|
+
|
|
5
|
+
export type ComputerWorkerInbound =
|
|
6
|
+
| { type: "ping"; id: string }
|
|
7
|
+
| { type: "init"; options: DesktopSessionOptions }
|
|
8
|
+
| { type: "execute"; id: string; actions: DesktopAction[] }
|
|
9
|
+
| { type: "close" };
|
|
10
|
+
|
|
11
|
+
export type ComputerWorkerOutbound =
|
|
12
|
+
| { type: "pong"; id: string }
|
|
13
|
+
| { type: "ready"; capabilities: DesktopCapabilities }
|
|
14
|
+
| { type: "result"; id: string; capture: DesktopCapture; capabilities: DesktopCapabilities }
|
|
15
|
+
| { type: "error"; id?: string; error: ComputerWorkerError }
|
|
16
|
+
| { type: "closed" };
|
|
17
|
+
|
|
18
|
+
export interface ComputerWorkerError {
|
|
19
|
+
name: string;
|
|
20
|
+
message: string;
|
|
21
|
+
stack?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ComputerWorkerTransport {
|
|
25
|
+
send(message: ComputerWorkerOutbound, transfer?: Bun.Transferable[]): void;
|
|
26
|
+
onMessage(handler: (message: ComputerWorkerInbound) => void): () => void;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|