@oh-my-pi/pi-coding-agent 17.0.9 → 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 +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- 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/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -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/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -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/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- 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 +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- 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/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- 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/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,321 +1,120 @@
|
|
|
1
|
-
|
|
2
|
-
* Gapless streaming audio output for assistant speech.
|
|
3
|
-
*
|
|
4
|
-
* Replaces the spawn-`afplay`-per-sentence approach (a fresh process per chunk
|
|
5
|
-
* meant audible gaps, per-spawn latency, and no way to interrupt a clip mid-play)
|
|
6
|
-
* with a single persistent player process fed raw 32-bit-float mono PCM over
|
|
7
|
-
* stdin. Chunks are queued and drained by one writer so segments play back to
|
|
8
|
-
* back; writes are paced to stay only {@link LEAD_SECONDS} ahead of realtime so
|
|
9
|
-
* ducking and stop take effect promptly instead of after seconds of buffered
|
|
10
|
-
* audio. {@link StreamingAudioPlayer.stop} kills the process for instant silence.
|
|
11
|
-
*
|
|
12
|
-
* Where no streaming backend exists (Windows, or a host without ffmpeg/sox), it
|
|
13
|
-
* degrades to the per-file {@link playAudioFile} path so speech still works —
|
|
14
|
-
* just without gapless playback or mid-clip interruption. A backend that spawns
|
|
15
|
-
* but dies early (e.g. an ffmpeg built without its platform audio device) is
|
|
16
|
-
* detected via its exit and the session downgrades to per-file playback without
|
|
17
|
-
* dropping the chunk being played.
|
|
18
|
-
*/
|
|
19
|
-
import * as fs from "node:fs/promises";
|
|
20
|
-
import * as os from "node:os";
|
|
21
|
-
import * as path from "node:path";
|
|
22
|
-
import { $which, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
23
|
-
import type { FileSink, Subprocess } from "bun";
|
|
24
|
-
import { getToolPath } from "../utils/tools-manager";
|
|
25
|
-
import { type PlayerCommand, playAudioFile } from "./player";
|
|
26
|
-
import { encodeWav } from "./wav";
|
|
1
|
+
import { AudioPlayback } from "@oh-my-pi/pi-natives";
|
|
27
2
|
|
|
28
|
-
/** Kokoro emits 24 kHz mono
|
|
3
|
+
/** Kokoro emits 24 kHz mono PCM when a chunk does not declare a rate. */
|
|
29
4
|
const DEFAULT_SAMPLE_RATE = 24_000;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/** Output gain applied while ducked (the user is speaking over the assistant). */
|
|
5
|
+
|
|
6
|
+
/** Output gain applied while the user speaks over assistant audio. */
|
|
33
7
|
export const DUCK_GAIN = 0.25;
|
|
34
8
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
which?: (bin: string) => string | null;
|
|
38
|
-
ffmpeg?: () => string | null;
|
|
9
|
+
function errorFrom(cause: unknown): Error {
|
|
10
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
39
11
|
}
|
|
40
12
|
|
|
41
13
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* and plays it to the default output device. An empty list means no streaming
|
|
45
|
-
* backend is available and the caller should fall back to per-file playback.
|
|
46
|
-
*
|
|
47
|
-
* - darwin: `ffmpeg` (AudioToolbox output device) → sox's `play` (coreaudio).
|
|
48
|
-
* - linux/other POSIX: `ffmpeg` (`-f pulse` then `-f alsa`) → `paplay`/`aplay`
|
|
49
|
-
* raw fallbacks.
|
|
50
|
-
* - win32: none (PowerShell `SoundPlayer` is file-only).
|
|
51
|
-
*/
|
|
52
|
-
export function streamingPlayerCommandsFor(
|
|
53
|
-
platform: NodeJS.Platform,
|
|
54
|
-
sampleRate: number,
|
|
55
|
-
lookup: StreamingPlayerLookup = {},
|
|
56
|
-
): PlayerCommand[] {
|
|
57
|
-
const which = lookup.which ?? $which;
|
|
58
|
-
const ffmpeg = lookup.ffmpeg ?? ((): string | null => getToolPath("ffmpeg"));
|
|
59
|
-
const rate = String(sampleRate > 0 ? sampleRate : DEFAULT_SAMPLE_RATE);
|
|
60
|
-
const input = ["-loglevel", "error", "-nostdin", "-f", "f32le", "-ar", rate, "-ac", "1", "-i", "pipe:0"];
|
|
61
|
-
|
|
62
|
-
if (platform === "darwin") {
|
|
63
|
-
const commands: PlayerCommand[] = [];
|
|
64
|
-
const ffmpegBin = ffmpeg();
|
|
65
|
-
if (ffmpegBin) commands.push({ cmd: ffmpegBin, args: [...input, "-f", "audiotoolbox", "default"] });
|
|
66
|
-
const play = which("play");
|
|
67
|
-
if (play) {
|
|
68
|
-
commands.push({
|
|
69
|
-
cmd: play,
|
|
70
|
-
args: ["-q", "-t", "raw", "-e", "floating-point", "-b", "32", "-r", rate, "-c", "1", "-"],
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
return commands;
|
|
74
|
-
}
|
|
75
|
-
if (platform === "win32") {
|
|
76
|
-
return [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const commands: PlayerCommand[] = [];
|
|
80
|
-
const ffmpegBin = ffmpeg();
|
|
81
|
-
if (ffmpegBin) {
|
|
82
|
-
commands.push({ cmd: ffmpegBin, args: [...input, "-f", "pulse", "default"] });
|
|
83
|
-
commands.push({ cmd: ffmpegBin, args: [...input, "-f", "alsa", "default"] });
|
|
84
|
-
}
|
|
85
|
-
const paplay = which("paplay");
|
|
86
|
-
if (paplay) commands.push({ cmd: paplay, args: ["--raw", `--rate=${rate}`, "--format=float32le", "--channels=1"] });
|
|
87
|
-
const aplay = which("aplay");
|
|
88
|
-
if (aplay) commands.push({ cmd: aplay, args: ["-q", "-f", "FLOAT_LE", "-r", rate, "-c", "1", "-"] });
|
|
89
|
-
return commands;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Single-session gapless player. Lifecycle: {@link start} once, {@link write}
|
|
94
|
-
* chunks in order, then {@link end} to drain or {@link stop} to abort. Not
|
|
95
|
-
* reusable after stop/end — create a new instance per utterance.
|
|
14
|
+
* One native gapless playback session. Call {@link start}, enqueue mono `f32`
|
|
15
|
+
* chunks with {@link write}, then {@link end} to drain or {@link stop} to abort.
|
|
96
16
|
*/
|
|
97
17
|
export class StreamingAudioPlayer {
|
|
98
|
-
#
|
|
18
|
+
#native: AudioPlayback | null = null;
|
|
99
19
|
#sampleRate = DEFAULT_SAMPLE_RATE;
|
|
100
20
|
#gain = 1;
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#sink: FileSink | null = null;
|
|
104
|
-
/** Streaming backends not yet tried; consumed head-first by {@link #spawnStream}. */
|
|
105
|
-
#candidates: PlayerCommand[] | null = null;
|
|
106
|
-
#writtenSec = 0;
|
|
107
|
-
#startedAt = 0;
|
|
108
|
-
#started = false;
|
|
21
|
+
#error: Error | null = null;
|
|
22
|
+
#ending: Promise<void> | null = null;
|
|
109
23
|
#inputClosed = false;
|
|
110
24
|
#stopped = false;
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
25
|
+
#failNative(native: AudioPlayback, cause: unknown): void {
|
|
26
|
+
this.#error = errorFrom(cause);
|
|
27
|
+
if (this.#native === native) this.#native = null;
|
|
28
|
+
try {
|
|
29
|
+
native.stop();
|
|
30
|
+
} catch {
|
|
31
|
+
// Preserve the original playback failure.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
114
34
|
|
|
115
|
-
/**
|
|
116
|
-
start(sampleRate
|
|
117
|
-
if (this.#
|
|
118
|
-
this.#started = true;
|
|
35
|
+
/** Opens the default speaker at the stream's logical sample rate. */
|
|
36
|
+
start(sampleRate = DEFAULT_SAMPLE_RATE): void {
|
|
37
|
+
if (this.#native || this.#error || this.#inputClosed || this.#stopped) return;
|
|
119
38
|
this.#sampleRate = sampleRate > 0 ? sampleRate : DEFAULT_SAMPLE_RATE;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
39
|
+
let native: AudioPlayback | undefined;
|
|
40
|
+
try {
|
|
41
|
+
native = new AudioPlayback(this.#sampleRate);
|
|
42
|
+
native.setGain(this.#gain);
|
|
43
|
+
this.#native = native;
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
if (native) this.#failNative(native, cause);
|
|
46
|
+
else this.#error = errorFrom(cause);
|
|
47
|
+
}
|
|
123
48
|
}
|
|
124
49
|
|
|
125
|
-
/**
|
|
50
|
+
/** Queues one mono `f32` PCM chunk without copying it in TypeScript. */
|
|
126
51
|
write(pcm: Float32Array): void {
|
|
127
|
-
if (this.#stopped) return;
|
|
128
|
-
this.#
|
|
129
|
-
this.#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
/** Scale subsequent output (1 = normal, <1 = ducked). Applies within {@link LEAD_SECONDS}. */
|
|
133
|
-
setGain(gain: number): void {
|
|
134
|
-
this.#gain = gain < 0 ? 0 : gain;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Close the input; resolves once all queued audio has finished playing. */
|
|
138
|
-
async end(): Promise<void> {
|
|
139
|
-
this.#inputClosed = true;
|
|
140
|
-
this.#signal();
|
|
141
|
-
await this.#drain;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Stop immediately: kill the player, drop everything still queued. */
|
|
145
|
-
stop(): void {
|
|
146
|
-
if (this.#stopped) return;
|
|
147
|
-
this.#stopped = true;
|
|
148
|
-
this.#queue.length = 0;
|
|
149
|
-
this.#abortController.abort();
|
|
150
|
-
this.#signal();
|
|
52
|
+
if (this.#inputClosed || this.#stopped || pcm.length === 0) return;
|
|
53
|
+
if (!this.#native && !this.#error) this.start(this.#sampleRate);
|
|
54
|
+
const native = this.#native;
|
|
55
|
+
if (!native) return;
|
|
151
56
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
void Promise.resolve(this.#sink?.end()).catch(() => {});
|
|
156
|
-
} catch {}
|
|
157
|
-
try {
|
|
158
|
-
this.#proc?.kill("SIGKILL");
|
|
159
|
-
} catch {}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Spawn the next untried streaming backend; false once the list is
|
|
164
|
-
* exhausted. A backend that spawns but dies early (e.g. an ffmpeg built
|
|
165
|
-
* without this platform's audio output device) would otherwise swallow PCM
|
|
166
|
-
* into a dead pipe, so its exit advances to the next candidate — or to
|
|
167
|
-
* per-file playback — and #writeStream's failure path replays the
|
|
168
|
-
* in-flight chunk.
|
|
169
|
-
*/
|
|
170
|
-
#spawnStream(): boolean {
|
|
171
|
-
this.#candidates ??= streamingPlayerCommandsFor(process.platform, this.#sampleRate);
|
|
172
|
-
for (let command = this.#candidates.shift(); command; command = this.#candidates.shift()) {
|
|
173
|
-
const { cmd, args } = command;
|
|
174
|
-
try {
|
|
175
|
-
const proc = Bun.spawn([cmd, ...args], {
|
|
176
|
-
stdin: "pipe",
|
|
177
|
-
stdout: "ignore",
|
|
178
|
-
stderr: "ignore",
|
|
179
|
-
});
|
|
180
|
-
this.#proc = proc;
|
|
181
|
-
this.#sink = proc.stdin;
|
|
182
|
-
void proc.exited.then(code => {
|
|
183
|
-
if (this.#proc !== proc || this.#stopped || this.#inputClosed) return;
|
|
184
|
-
logger.debug("tts: streaming backend exited early; trying next backend", { cmd, code });
|
|
185
|
-
this.#proc = null;
|
|
186
|
-
this.#sink = null;
|
|
187
|
-
this.#mode = this.#spawnStream() ? "stream" : "file";
|
|
188
|
-
});
|
|
189
|
-
return true;
|
|
190
|
-
} catch (error) {
|
|
191
|
-
logger.debug("tts: streaming player spawn failed", {
|
|
192
|
-
cmd,
|
|
193
|
-
error: error instanceof Error ? error.message : String(error),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
57
|
+
native.write(pcm);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
this.#failNative(native, cause);
|
|
196
60
|
}
|
|
197
|
-
return false;
|
|
198
61
|
}
|
|
199
62
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
async #drainLoop(): Promise<void> {
|
|
63
|
+
/** Applies gain at render time, including to samples already queued natively. */
|
|
64
|
+
setGain(gain: number): void {
|
|
65
|
+
if (!Number.isFinite(gain) || gain < 0) throw new RangeError("Audio gain must be a finite non-negative number");
|
|
66
|
+
this.#gain = gain;
|
|
67
|
+
const native = this.#native;
|
|
68
|
+
if (!native) return;
|
|
207
69
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (this.#inputClosed) break;
|
|
212
|
-
await this.#waitForWork();
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
if (this.#mode === "stream") {
|
|
216
|
-
// Pace writes so the player buffers ~LEAD_SECONDS, no more, keeping
|
|
217
|
-
// ducking and stop responsive instead of locked behind buffered audio.
|
|
218
|
-
const ahead = this.#writtenSec - (performance.now() - this.#startedAt) / 1000;
|
|
219
|
-
if (ahead > LEAD_SECONDS) {
|
|
220
|
-
await Bun.sleep((ahead - LEAD_SECONDS) * 1000);
|
|
221
|
-
if (this.#stopped) return;
|
|
222
|
-
}
|
|
223
|
-
if (await this.#writeStream(chunk)) {
|
|
224
|
-
this.#writtenSec += chunk.length / this.#sampleRate;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
// Backend died mid-write: move to the next streaming candidate
|
|
228
|
-
// (or the file path) and replay this exact chunk so nothing is
|
|
229
|
-
// dropped.
|
|
230
|
-
this.#mode = this.#spawnStream() ? "stream" : "file";
|
|
231
|
-
if (this.#mode === "stream" && (await this.#writeStream(chunk))) {
|
|
232
|
-
this.#writtenSec += chunk.length / this.#sampleRate;
|
|
233
|
-
} else {
|
|
234
|
-
this.#mode = "file";
|
|
235
|
-
await this.#playFile(chunk);
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
await this.#playFile(chunk);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (!this.#stopped && this.#mode === "stream") {
|
|
242
|
-
try {
|
|
243
|
-
await this.#sink?.end();
|
|
244
|
-
} catch {}
|
|
245
|
-
if (this.#proc) {
|
|
246
|
-
try {
|
|
247
|
-
await this.#proc.exited;
|
|
248
|
-
} catch {}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
logger.debug("tts: streaming player drain failed", {
|
|
253
|
-
error: error instanceof Error ? error.message : String(error),
|
|
254
|
-
});
|
|
70
|
+
native.setGain(gain);
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
this.#failNative(native, cause);
|
|
255
73
|
}
|
|
256
74
|
}
|
|
257
75
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.#
|
|
262
|
-
|
|
263
|
-
if (this.#
|
|
264
|
-
this.#
|
|
265
|
-
|
|
76
|
+
/** Closes input and resolves after every queued sample reaches the speaker. */
|
|
77
|
+
end(): Promise<void> {
|
|
78
|
+
if (this.#ending) return this.#ending;
|
|
79
|
+
if (this.#stopped) return Promise.resolve();
|
|
80
|
+
this.#inputClosed = true;
|
|
81
|
+
if (this.#error) {
|
|
82
|
+
this.#stopped = true;
|
|
83
|
+
return Promise.reject(this.#error);
|
|
266
84
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
* Write one chunk into the backend's stdin and await the flush — a broken
|
|
272
|
-
* pipe rejects here (not as an unhandled rejection later), so the caller
|
|
273
|
-
* can replay the exact chunk on the next backend.
|
|
274
|
-
*/
|
|
275
|
-
async #writeStream(pcm: Float32Array): Promise<boolean> {
|
|
276
|
-
const sink = this.#sink;
|
|
277
|
-
if (!sink) return false;
|
|
278
|
-
try {
|
|
279
|
-
sink.write(this.#bytes(pcm));
|
|
280
|
-
await sink.flush();
|
|
281
|
-
return true;
|
|
282
|
-
} catch (error) {
|
|
283
|
-
logger.debug("tts: streaming write failed", {
|
|
284
|
-
error: error instanceof Error ? error.message : String(error),
|
|
285
|
-
});
|
|
286
|
-
return false;
|
|
85
|
+
const native = this.#native;
|
|
86
|
+
if (!native) {
|
|
87
|
+
this.#stopped = true;
|
|
88
|
+
return Promise.resolve();
|
|
287
89
|
}
|
|
90
|
+
this.#ending = native
|
|
91
|
+
.end()
|
|
92
|
+
.catch(cause => {
|
|
93
|
+
throw errorFrom(cause);
|
|
94
|
+
})
|
|
95
|
+
.finally(() => {
|
|
96
|
+
if (this.#native === native) this.#native = null;
|
|
97
|
+
this.#stopped = true;
|
|
98
|
+
});
|
|
99
|
+
return this.#ending;
|
|
288
100
|
}
|
|
289
101
|
|
|
290
|
-
|
|
291
|
-
|
|
102
|
+
/** Stops immediately and discards queued audio. Safe to call repeatedly. */
|
|
103
|
+
stop(): void {
|
|
104
|
+
this.#inputClosed = true;
|
|
105
|
+
this.#stopped = true;
|
|
106
|
+
const native = this.#native;
|
|
107
|
+
this.#native = null;
|
|
108
|
+
if (!native) return;
|
|
292
109
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
logger.debug("tts: file playback failed", {
|
|
297
|
-
error: error instanceof Error ? error.message : String(error),
|
|
298
|
-
});
|
|
299
|
-
} finally {
|
|
300
|
-
await fs.unlink(wavPath).catch(() => {});
|
|
110
|
+
native.stop();
|
|
111
|
+
} catch {
|
|
112
|
+
// Best-effort abort during session teardown.
|
|
301
113
|
}
|
|
302
114
|
}
|
|
303
|
-
|
|
304
|
-
/** Raw f32le bytes for the stream sink, applying gain only when ducked (avoids a copy at unity). */
|
|
305
|
-
#bytes(pcm: Float32Array): Uint8Array {
|
|
306
|
-
if (this.#gain === 1) return new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
307
|
-
return new Uint8Array(this.#scaled(pcm).buffer);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
#scaled(pcm: Float32Array): Float32Array {
|
|
311
|
-
if (this.#gain === 1) return pcm;
|
|
312
|
-
const out = new Float32Array(pcm.length);
|
|
313
|
-
for (let i = 0; i < pcm.length; i++) out[i] = (pcm[i] ?? 0) * this.#gain;
|
|
314
|
-
return out;
|
|
315
|
-
}
|
|
316
115
|
}
|
|
317
116
|
|
|
318
|
-
/**
|
|
117
|
+
/** Creates the single-use player used by the speech vocalizer. */
|
|
319
118
|
export function createStreamingPlayer(): StreamingAudioPlayer {
|
|
320
119
|
return new StreamingAudioPlayer();
|
|
321
120
|
}
|
package/src/tts/vocalizer.ts
CHANGED
|
@@ -100,6 +100,8 @@ export class Vocalizer {
|
|
|
100
100
|
#chain: Promise<void> = Promise.resolve();
|
|
101
101
|
/** Whether the user is currently speaking; new sessions open ducked. */
|
|
102
102
|
#ducked = false;
|
|
103
|
+
/** Active scopes that suppress local TTS while another speech surface owns audio. */
|
|
104
|
+
#suspensions = 0;
|
|
103
105
|
/** Available rewrite slots; blocks queue when exhausted. */
|
|
104
106
|
#rewriteSlots = MAX_REWRITES_IN_FLIGHT;
|
|
105
107
|
#slotWaiters: Array<() => void> = [];
|
|
@@ -114,6 +116,21 @@ export class Vocalizer {
|
|
|
114
116
|
this.#enhancer = enhancer;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Suppress new vocalization until the returned idempotent release function runs.
|
|
121
|
+
* Existing synthesis and playback stop immediately; nested scopes release independently.
|
|
122
|
+
*/
|
|
123
|
+
suspend(): () => void {
|
|
124
|
+
this.#suspensions += 1;
|
|
125
|
+
this.clear();
|
|
126
|
+
let released = false;
|
|
127
|
+
return () => {
|
|
128
|
+
if (released) return;
|
|
129
|
+
released = true;
|
|
130
|
+
this.#suspensions = Math.max(0, this.#suspensions - 1);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
/**
|
|
118
135
|
* Stream a delta of assistant text into the pipeline. No-op when
|
|
119
136
|
* vocalization is disabled. The synthesis session (worker, player) is only
|
|
@@ -123,7 +140,7 @@ export class Vocalizer {
|
|
|
123
140
|
* pipeline (enhanced vs mechanical) is latched per utterance.
|
|
124
141
|
*/
|
|
125
142
|
pushDelta(text: string): void {
|
|
126
|
-
if (!settings.get("speech.enabled")) return;
|
|
143
|
+
if (this.#suspensions > 0 || !settings.get("speech.enabled")) return;
|
|
127
144
|
if (!text) return;
|
|
128
145
|
if (this.#enhanced || (!this.#speakable && this.#enhancer && settings.get("speech.enhanced"))) {
|
|
129
146
|
this.#pushEnhanced(text);
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClipboardImage } from "@oh-my-pi/pi-natives";
|
|
2
2
|
import * as native from "@oh-my-pi/pi-natives";
|
|
3
3
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import MAC_FILE_URL_SCRIPT from "./mac-file-urls.applescript" with { type: "text" };
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Run a subprocess and capture its stdout without blocking the event loop.
|
|
@@ -52,36 +53,6 @@ function isWsl(): boolean {
|
|
|
52
53
|
return process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// AppleScript that returns the POSIX paths of every file URL currently on the
|
|
56
|
-
// macOS pasteboard, one path per line. `pbpaste(1)` only surfaces plain text,
|
|
57
|
-
// EPS, or RTF, so a Finder `Cmd+C` (which puts only a `public.file-url`
|
|
58
|
-
// representation on the pasteboard) makes `pbpaste` empty. AppleScript's
|
|
59
|
-
// `«class furl»` coercion reaches the file-URL representation directly and
|
|
60
|
-
// works for both single-file and multi-file selections. The `try` blocks
|
|
61
|
-
// suppress the `-1700` "can't make … into type" error AppleScript raises when
|
|
62
|
-
// the clipboard holds no file URLs, so the script's exit status only reflects
|
|
63
|
-
// `osascript` itself.
|
|
64
|
-
const MAC_FILE_URL_SCRIPT = [
|
|
65
|
-
"on run",
|
|
66
|
-
'\tset output to ""',
|
|
67
|
-
"\ttry",
|
|
68
|
-
"\t\tset theClip to the clipboard as «class furl»",
|
|
69
|
-
"\t\tif class of theClip is list then",
|
|
70
|
-
"\t\t\trepeat with anItem in theClip",
|
|
71
|
-
"\t\t\t\ttry",
|
|
72
|
-
"\t\t\t\t\tset output to output & POSIX path of anItem & linefeed",
|
|
73
|
-
"\t\t\t\tend try",
|
|
74
|
-
"\t\t\tend repeat",
|
|
75
|
-
"\t\telse",
|
|
76
|
-
"\t\t\ttry",
|
|
77
|
-
"\t\t\t\tset output to POSIX path of theClip & linefeed",
|
|
78
|
-
"\t\t\tend try",
|
|
79
|
-
"\t\tend if",
|
|
80
|
-
"\tend try",
|
|
81
|
-
"\treturn output",
|
|
82
|
-
"end run",
|
|
83
|
-
].join("\n");
|
|
84
|
-
|
|
85
56
|
/**
|
|
86
57
|
* Read file paths from the macOS pasteboard's `public.file-url` representation.
|
|
87
58
|
*
|
package/src/utils/jj.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from "node:fs
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { $which } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { LRUCache } from "lru-cache/raw";
|
|
@@ -63,6 +63,8 @@ export class JjCommandError extends Error {
|
|
|
63
63
|
// ════════════════════════════════════════════════════════════════════════════
|
|
64
64
|
// Internal: Core execution
|
|
65
65
|
// ════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
const WORKING_COPY_LABEL_REVSET = "@ | heads(::@ & bookmarks())";
|
|
67
|
+
const WORKING_COPY_LABEL_TEMPLATE = 'change_id.shortest(8) ++ "|" ++ local_bookmarks ++ "\\n"';
|
|
66
68
|
|
|
67
69
|
function ensureAvailable(): void {
|
|
68
70
|
if (!$which("jj")) {
|
|
@@ -121,6 +123,19 @@ async function runText(cwd: string, args: readonly string[], options: CommandOpt
|
|
|
121
123
|
return (await runChecked(cwd, args, options)).stdout;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
async function runOptionalText(
|
|
127
|
+
cwd: string,
|
|
128
|
+
args: readonly string[],
|
|
129
|
+
options: CommandOptions = {},
|
|
130
|
+
): Promise<string | null> {
|
|
131
|
+
try {
|
|
132
|
+
const result = await jj(cwd, args, options);
|
|
133
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
function splitLines(text: string): string[] {
|
|
125
140
|
return text
|
|
126
141
|
.split("\n")
|
|
@@ -135,6 +150,30 @@ function buildDiffArgs(options: DiffOptions): string[] {
|
|
|
135
150
|
return args;
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
function parseWorkingCopyLabel(raw: string): string | null {
|
|
154
|
+
let changeId: string | null = null;
|
|
155
|
+
for (const line of raw.split("\n")) {
|
|
156
|
+
const sep = line.indexOf("|");
|
|
157
|
+
const change = (sep === -1 ? line : line.slice(0, sep)).trim();
|
|
158
|
+
const bookmarks = sep === -1 ? "" : line.slice(sep + 1).trim();
|
|
159
|
+
if (changeId === null && change) changeId = change;
|
|
160
|
+
if (bookmarks) return bookmarks.replace(/\s+/g, " ");
|
|
161
|
+
}
|
|
162
|
+
return changeId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseStatusSummary(raw: string): git.GitStatusSummary {
|
|
166
|
+
let unstaged = 0;
|
|
167
|
+
let untracked = 0;
|
|
168
|
+
for (const line of raw.split("\n")) {
|
|
169
|
+
const type = line.trim()[0];
|
|
170
|
+
if (!type) continue;
|
|
171
|
+
if (type === "A") untracked++;
|
|
172
|
+
else unstaged++;
|
|
173
|
+
}
|
|
174
|
+
return { staged: 0, unstaged, untracked };
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
// ════════════════════════════════════════════════════════════════════════════
|
|
139
178
|
// Internal: Repository resolution
|
|
140
179
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -153,7 +192,16 @@ async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
|
|
|
153
192
|
// of the default workspace. Either form is a real workspace, so match on
|
|
154
193
|
// `.jj/repo` presence rather than the inner `store/` directory.
|
|
155
194
|
try {
|
|
156
|
-
await fs.stat(path.join(dir, ".jj", "repo"));
|
|
195
|
+
await fs.promises.stat(path.join(dir, ".jj", "repo"));
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function hasJjWorkspaceMetadataSync(dir: string): boolean {
|
|
203
|
+
try {
|
|
204
|
+
fs.statSync(path.join(dir, ".jj", "repo"));
|
|
157
205
|
return true;
|
|
158
206
|
} catch {
|
|
159
207
|
return false;
|
|
@@ -180,6 +228,21 @@ async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
|
180
228
|
return undefined;
|
|
181
229
|
}
|
|
182
230
|
|
|
231
|
+
function findWorkspaceRootSync(cwd: string): string | undefined {
|
|
232
|
+
const key = path.resolve(cwd);
|
|
233
|
+
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key)?.root;
|
|
234
|
+
|
|
235
|
+
for (let dir: string | undefined = key; dir; dir = parentOf(dir)) {
|
|
236
|
+
if (hasJjWorkspaceMetadataSync(dir)) {
|
|
237
|
+
workspaceRootCache.set(key, { root: dir });
|
|
238
|
+
return dir;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
workspaceRootCache.set(key, {});
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
183
246
|
/**
|
|
184
247
|
* Resolve the `.jj/repo` directory backing a workspace root, following the file
|
|
185
248
|
* indirection used by non-default workspaces. `jj workspace add` writes a FILE at
|
|
@@ -190,8 +253,8 @@ async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
|
190
253
|
async function resolveRepoDir(root: string): Promise<string> {
|
|
191
254
|
const jjDir = path.join(root, ".jj");
|
|
192
255
|
const repoPath = path.join(jjDir, "repo");
|
|
193
|
-
if ((await fs.stat(repoPath)).isFile()) {
|
|
194
|
-
const target = (await fs.readFile(repoPath, "utf8")).trim();
|
|
256
|
+
if ((await fs.promises.stat(repoPath)).isFile()) {
|
|
257
|
+
const target = (await fs.promises.readFile(repoPath, "utf8")).trim();
|
|
195
258
|
return path.resolve(jjDir, target);
|
|
196
259
|
}
|
|
197
260
|
return repoPath;
|
|
@@ -221,6 +284,56 @@ export const diff = Object.assign(
|
|
|
221
284
|
},
|
|
222
285
|
);
|
|
223
286
|
|
|
287
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
288
|
+
// API: working copy
|
|
289
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
290
|
+
|
|
291
|
+
/** Jujutsu working-copy metadata used by status displays. */
|
|
292
|
+
export const workingCopy = {
|
|
293
|
+
/**
|
|
294
|
+
* Label `@` with its nearest bookmark, falling back to its short change ID.
|
|
295
|
+
* Returns `null` when `jj` is unavailable or the query fails.
|
|
296
|
+
*/
|
|
297
|
+
async label(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
298
|
+
const raw = await runOptionalText(
|
|
299
|
+
cwd,
|
|
300
|
+
[
|
|
301
|
+
"log",
|
|
302
|
+
"--no-graph",
|
|
303
|
+
"--ignore-working-copy",
|
|
304
|
+
"-r",
|
|
305
|
+
WORKING_COPY_LABEL_REVSET,
|
|
306
|
+
"-T",
|
|
307
|
+
WORKING_COPY_LABEL_TEMPLATE,
|
|
308
|
+
],
|
|
309
|
+
{ signal },
|
|
310
|
+
);
|
|
311
|
+
return raw === null ? null : parseWorkingCopyLabel(raw);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/** Parse working-copy label query output. */
|
|
315
|
+
parseLabel: parseWorkingCopyLabel,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
// API: status
|
|
320
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
321
|
+
|
|
322
|
+
/** Jujutsu working-copy status derived from the changes in `@`. */
|
|
323
|
+
export const status = {
|
|
324
|
+
/**
|
|
325
|
+
* Count changes in `@` relative to its parent using the Git status shape.
|
|
326
|
+
* Jujutsu has no index, so `staged` is always zero.
|
|
327
|
+
*/
|
|
328
|
+
async summary(cwd: string, signal?: AbortSignal): Promise<git.GitStatusSummary | null> {
|
|
329
|
+
const raw = await runOptionalText(cwd, ["diff", "-r", "@", "--summary", "--ignore-working-copy"], { signal });
|
|
330
|
+
return raw === null ? null : parseStatusSummary(raw);
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/** Parse `jj diff --summary` output into status counts. */
|
|
334
|
+
parse: parseStatusSummary,
|
|
335
|
+
};
|
|
336
|
+
|
|
224
337
|
// ════════════════════════════════════════════════════════════════════════════
|
|
225
338
|
// API: repo
|
|
226
339
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -231,6 +344,14 @@ export const repo = {
|
|
|
231
344
|
workspaceRootCache.clear();
|
|
232
345
|
},
|
|
233
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Resolve the current workspace root synchronously from on-disk metadata.
|
|
349
|
+
* Intended for render paths that cannot await filesystem I/O.
|
|
350
|
+
*/
|
|
351
|
+
rootSync(cwd: string): string | null {
|
|
352
|
+
return findWorkspaceRootSync(cwd) ?? null;
|
|
353
|
+
},
|
|
354
|
+
|
|
234
355
|
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
|
235
356
|
async root(cwd: string): Promise<string | null> {
|
|
236
357
|
return (await findWorkspaceRoot(cwd)) ?? null;
|