@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/cli.js +3610 -3566
  3. package/dist/types/config/settings-schema.d.ts +10 -0
  4. package/dist/types/discovery/helpers.d.ts +2 -0
  5. package/dist/types/irc/bus.d.ts +5 -4
  6. package/dist/types/session/agent-session.d.ts +13 -13
  7. package/dist/types/task/discovery.d.ts +5 -2
  8. package/dist/types/task/renderer.d.ts +1 -0
  9. package/dist/types/tools/ast-grep.d.ts +4 -2
  10. package/dist/types/tools/browser/render.d.ts +2 -0
  11. package/dist/types/tools/debug.d.ts +1 -0
  12. package/dist/types/tools/eval-render.d.ts +2 -0
  13. package/dist/types/tools/glob.d.ts +4 -2
  14. package/dist/types/tools/grep.d.ts +4 -3
  15. package/dist/types/tools/path-utils.d.ts +7 -0
  16. package/dist/types/tools/renderers.d.ts +11 -0
  17. package/dist/types/tools/ssh.d.ts +2 -1
  18. package/dist/types/tts/index.d.ts +2 -0
  19. package/dist/types/tts/speakable.d.ts +47 -0
  20. package/dist/types/tts/speech-enhancer.d.ts +46 -0
  21. package/dist/types/tts/streaming-player.d.ts +1 -2
  22. package/dist/types/tts/tts-client.d.ts +11 -10
  23. package/dist/types/tts/tts-protocol.d.ts +7 -0
  24. package/dist/types/tts/vocalizer.d.ts +15 -8
  25. package/dist/types/utils/git.d.ts +2 -0
  26. package/package.json +12 -12
  27. package/src/advisor/__tests__/advisor.test.ts +1 -1
  28. package/src/cli/gallery-fixtures/fs.ts +2 -2
  29. package/src/cli/gallery-fixtures/search.ts +2 -2
  30. package/src/cli.ts +27 -5
  31. package/src/config/model-resolver.ts +31 -10
  32. package/src/config/settings-schema.ts +11 -0
  33. package/src/cursor.ts +1 -1
  34. package/src/discovery/helpers.ts +8 -0
  35. package/src/export/html/tool-views.generated.js +34 -34
  36. package/src/extensibility/custom-tools/loader.ts +3 -3
  37. package/src/extensibility/extensions/loader.ts +10 -3
  38. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  39. package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
  40. package/src/extensibility/plugins/loader.ts +30 -1
  41. package/src/irc/bus.ts +5 -4
  42. package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
  43. package/src/modes/components/assistant-message.ts +1 -1
  44. package/src/modes/components/move-overlay.ts +35 -23
  45. package/src/modes/components/tool-execution.ts +45 -12
  46. package/src/modes/components/tree-selector.ts +10 -3
  47. package/src/modes/controllers/event-controller.ts +16 -0
  48. package/src/prompts/goals/goal-mode-context.md +4 -0
  49. package/src/prompts/goals/goal-todo-context.md +10 -2
  50. package/src/prompts/system/speech-rewrite.md +15 -0
  51. package/src/prompts/system/tiny-title-system.md +1 -1
  52. package/src/prompts/system/title-system-marker.md +2 -1
  53. package/src/prompts/system/title-system.md +2 -1
  54. package/src/prompts/tools/ast-grep.md +3 -3
  55. package/src/prompts/tools/glob.md +1 -1
  56. package/src/prompts/tools/grep.md +1 -1
  57. package/src/sdk.ts +8 -5
  58. package/src/session/agent-session.ts +163 -49
  59. package/src/session/session-history-format.ts +6 -2
  60. package/src/task/discovery.ts +25 -2
  61. package/src/task/executor.ts +24 -4
  62. package/src/task/render.ts +26 -12
  63. package/src/task/renderer.ts +1 -0
  64. package/src/task/worktree.ts +144 -16
  65. package/src/tiny/text.ts +109 -17
  66. package/src/tools/ast-grep.ts +20 -17
  67. package/src/tools/bash.ts +16 -8
  68. package/src/tools/browser/render.ts +2 -0
  69. package/src/tools/debug.ts +1 -0
  70. package/src/tools/eval-render.ts +5 -2
  71. package/src/tools/gh-renderer.ts +3 -0
  72. package/src/tools/glob.ts +24 -20
  73. package/src/tools/grep.ts +18 -36
  74. package/src/tools/path-utils.ts +55 -10
  75. package/src/tools/read.ts +9 -2
  76. package/src/tools/renderers.ts +11 -0
  77. package/src/tools/ssh.ts +17 -7
  78. package/src/tts/index.ts +2 -0
  79. package/src/tts/speakable.ts +382 -0
  80. package/src/tts/speech-enhancer.ts +204 -0
  81. package/src/tts/streaming-player.ts +71 -16
  82. package/src/tts/tts-client.ts +11 -10
  83. package/src/tts/tts-protocol.ts +14 -5
  84. package/src/tts/tts-worker.ts +52 -49
  85. package/src/tts/vocalizer.ts +277 -46
  86. package/src/utils/git.ts +8 -0
@@ -4752,6 +4752,16 @@ export declare const SETTINGS_SCHEMA: {
4752
4752
  }];
4753
4753
  };
4754
4754
  };
4755
+ readonly "speech.enhanced": {
4756
+ readonly type: "boolean";
4757
+ readonly default: false;
4758
+ readonly ui: {
4759
+ readonly tab: "providers";
4760
+ readonly group: "Services";
4761
+ readonly label: "Enhanced Speech Rewriting";
4762
+ readonly description: "Rewrite assistant output into natural spoken prose with the tiny/smol model before synthesis (describes code, drops links and markdown). Falls back to mechanical cleanup on failure";
4763
+ };
4764
+ };
4755
4765
  readonly "speech.voice": {
4756
4766
  readonly type: "enum";
4757
4767
  readonly values: readonly string[];
@@ -236,6 +236,8 @@ export declare function resolveActiveProjectRegistryPath(cwd: string): Promise<s
236
236
  * never alias as the project registry.
237
237
  */
238
238
  export declare function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<string | undefined>;
239
+ /** Register a process-global plugin cache invalidator called whenever plugin roots are cleared. */
240
+ export declare function registerPluginCacheInvalidator(invalidator: () => void): void;
239
241
  /**
240
242
  * List all installed Claude Code plugin roots from the plugin cache.
241
243
  * Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
@@ -8,10 +8,11 @@
8
8
  * agents receive the message as a non-interrupting aside at the next step
9
9
  * boundary (see AgentSession.deliverIrcMessage). Replies are real turns by
10
10
  * the recipient, observed via `wait` — with one exception: when the sender
11
- * awaits a reply and the recipient is mid-turn with async execution
12
- * disabled, the recipient session generates an ephemeral side-channel
13
- * auto-reply (it may be blocked in a synchronous task spawn whose batch
14
- * includes the sender, so a real turn could never happen in time).
11
+ * awaits a reply and the recipient cannot run a real reply turn in time
12
+ * (mid-turn with async execution disabled possibly blocked in a
13
+ * synchronous task spawn whose batch includes the sender or idle in plan
14
+ * mode, where autonomous wake turns are suppressed), the recipient session
15
+ * generates an ephemeral side-channel auto-reply.
15
16
  */
16
17
  import { AgentLifecycleManager } from "../registry/agent-lifecycle";
17
18
  import { AgentRegistry } from "../registry/agent-registry";
@@ -882,12 +882,10 @@ export declare class AgentSession {
882
882
  /**
883
883
  * Set model directly.
884
884
  * Validates that a credential source is configured (synchronously, without
885
- * refreshing OAuth or running command-backed key programs). The active
886
- * session switches by default; when `currentContextTokens` is provided and
887
- * exceeds the refreshed candidate's context window, the live switch is
888
- * skipped while role persistence still runs. Returns whether the active
889
- * model actually switched, computed against the refreshed metadata so
890
- * dynamic providers (e.g. llama.cpp) honor their post-load contextWindow.
885
+ * refreshing OAuth or running command-backed key programs). Active switches
886
+ * always take effect; if the current transcript is too large for the target
887
+ * model, the next prompt's compaction/error path owns that recovery instead
888
+ * of leaving the session pinned to the old model.
891
889
  * @throws Error if no API key available for the model
892
890
  */
893
891
  setModel(model: Model, role?: string, options?: {
@@ -1163,18 +1161,20 @@ export declare class AgentSession {
1163
1161
  *
1164
1162
  * - mid-turn → queued on the aside channel and folded in at the next step
1165
1163
  * boundary (non-interrupting, like async-result deliveries) → "injected";
1164
+ * - idle in plan mode → appended into context without waking an autonomous
1165
+ * turn (convergence stays user-driven) → "injected";
1166
1166
  * - idle → starts a real turn with the message so the recipient wakes
1167
1167
  * → "woken".
1168
1168
  *
1169
1169
  * Never blocks on the recipient's turn: the wake turn is fire-and-forget.
1170
1170
  *
1171
- * When the sender expects a reply (`send await:true`) and this session is
1172
- * mid-turn with async execution disabled, the next step boundary may be
1173
- * gated on the sender's own batch finishing (blocking task spawns), so a
1174
- * real reply turn can never happen in time. In that case an ephemeral
1175
- * side-channel auto-reply is generated from the current context (the old
1176
- * `respondAsBackground` path) and sent back over the bus on this agent's
1177
- * behalf.
1171
+ * When the sender expects a reply (`send await:true`) and this session
1172
+ * cannot produce a real reply turn in time mid-turn with async execution
1173
+ * disabled (the next step boundary may be gated on the sender's own batch
1174
+ * finishing), or idle in plan mode (wake turns are suppressed) an
1175
+ * ephemeral side-channel auto-reply is generated from the current context
1176
+ * (the old `respondAsBackground` path) and sent back over the bus on this
1177
+ * agent's behalf.
1178
1178
  */
1179
1179
  deliverIrcMessage(msg: IrcMessage, opts?: {
1180
1180
  expectsReply?: boolean;
@@ -6,8 +6,11 @@ export interface DiscoveryResult {
6
6
  }
7
7
  /**
8
8
  * Discover agents from filesystem and merge with bundled agents.
9
- *
10
- * Precedence (highest wins): project .omp, user .omp, Claude plugin agents, then bundled
9
+ * Precedence (highest wins): project `.omp/agents`, user `.omp/agents`,
10
+ * OMP extension-package agents in `listOmpExtensionRoots` source order
11
+ * (CLI roots > project `extensions:` settings > user `extensions:` settings >
12
+ * installed npm/link plugins), Claude marketplace plugin agents (project
13
+ * scope before user), then bundled.
11
14
  * @param cwd - Current working directory for project agent discovery
12
15
  */
13
16
  export declare function discoverAgents(cwd: string, home?: string): Promise<DiscoveryResult>;
@@ -10,4 +10,5 @@ export declare const taskToolRenderer: {
10
10
  readonly renderCall: typeof renderCall;
11
11
  readonly renderResult: typeof renderResult;
12
12
  readonly mergeCallAndResult: true;
13
+ readonly animatedPartialResult: true;
13
14
  };
@@ -7,7 +7,7 @@ import type { ToolSession } from ".";
7
7
  import type { OutputMeta } from "./output-meta";
8
8
  declare const astGrepSchema: import("arktype/internal/variants/object.ts").ObjectType<{
9
9
  pat: string;
10
- paths: string[];
10
+ path?: string | undefined;
11
11
  skip?: number | undefined;
12
12
  }, {}>;
13
13
  export interface AstGrepToolDetails {
@@ -44,7 +44,7 @@ export declare class AstGrepTool implements AgentTool<typeof astGrepSchema, AstG
44
44
  readonly description: string;
45
45
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
46
46
  pat: string;
47
- paths: string[];
47
+ path?: string | undefined;
48
48
  skip?: number | undefined;
49
49
  }, {}>;
50
50
  readonly strict = true;
@@ -55,6 +55,8 @@ export declare class AstGrepTool implements AgentTool<typeof astGrepSchema, AstG
55
55
  }
56
56
  interface AstGrepRenderArgs {
57
57
  pat?: string;
58
+ path?: string | string[];
59
+ /** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
58
60
  paths?: string[];
59
61
  skip?: number;
60
62
  }
@@ -35,6 +35,8 @@ interface BrowserRenderContext {
35
35
  previewLines?: number;
36
36
  }
37
37
  export declare const browserToolRenderer: {
38
+ animatedPendingPreview: (args: unknown) => boolean;
39
+ animatedPartialResult: (args: unknown) => boolean;
38
40
  renderCall(args: BrowserRenderArgs, options: RenderResultOptions, theme: Theme): Component;
39
41
  renderResult(result: {
40
42
  content: Array<{
@@ -85,6 +85,7 @@ interface DebugToolDetails {
85
85
  interface DebugRenderArgs extends Partial<DebugParams> {
86
86
  }
87
87
  export declare const debugToolRenderer: {
88
+ animatedPartialResult: boolean;
88
89
  renderCall(args: DebugRenderArgs, _options: RenderResultOptions, theme: Theme): Component;
89
90
  renderResult(result: {
90
91
  content: Array<{
@@ -40,6 +40,8 @@ interface EvalRenderContext {
40
40
  */
41
41
  export declare function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void;
42
42
  export declare const evalToolRenderer: {
43
+ animatedPendingPreview: boolean;
44
+ animatedPartialResult: boolean;
43
45
  renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component;
44
46
  renderResult(result: {
45
47
  content: Array<{
@@ -7,7 +7,7 @@ import { type TruncationResult } from "../session/streaming-output";
7
7
  import type { ToolSession } from ".";
8
8
  import { type OutputMeta } from "./output-meta";
9
9
  declare const findSchema: import("arktype/internal/variants/object.ts").ObjectType<{
10
- paths: string[];
10
+ path?: string | undefined;
11
11
  hidden?: boolean | undefined;
12
12
  gitignore?: boolean | undefined;
13
13
  limit?: number | undefined;
@@ -64,7 +64,7 @@ export declare class GlobTool implements AgentTool<typeof findSchema, GlobToolDe
64
64
  readonly label = "Glob";
65
65
  readonly description: string;
66
66
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
67
- paths: string[];
67
+ path?: string | undefined;
68
68
  hidden?: boolean | undefined;
69
69
  gitignore?: boolean | undefined;
70
70
  limit?: number | undefined;
@@ -75,6 +75,8 @@ export declare class GlobTool implements AgentTool<typeof findSchema, GlobToolDe
75
75
  execute(_toolCallId: string, params: typeof findSchema.infer, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<GlobToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<GlobToolDetails>>;
76
76
  }
77
77
  interface GlobRenderArgs {
78
+ path?: string | string[];
79
+ /** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
78
80
  paths?: string | string[];
79
81
  limit?: number;
80
82
  }
@@ -7,13 +7,12 @@ import type { ToolSession } from ".";
7
7
  import type { OutputMeta } from "./output-meta";
8
8
  declare const searchSchema: import("arktype/internal/variants/object.ts").ObjectType<{
9
9
  pattern: string;
10
- paths?: string | string[] | undefined;
10
+ path?: string | undefined;
11
11
  case?: boolean | undefined;
12
12
  gitignore?: boolean | undefined;
13
13
  skip?: number | null | undefined;
14
14
  }, {}>;
15
15
  export type GrepToolInput = typeof searchSchema.infer;
16
- export declare function toPathList(input: string | string[] | undefined): string[];
17
16
  /** Maximum number of distinct files surfaced in a single response. The
18
17
  * agent paginates further pages via `skip`. */
19
18
  export declare const DEFAULT_FILE_LIMIT = 20;
@@ -66,7 +65,7 @@ export declare class GrepTool implements AgentTool<typeof searchSchema, GrepTool
66
65
  readonly description: string;
67
66
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
68
67
  pattern: string;
69
- paths?: string | string[] | undefined;
68
+ path?: string | undefined;
70
69
  case?: boolean | undefined;
71
70
  gitignore?: boolean | undefined;
72
71
  skip?: number | null | undefined;
@@ -77,6 +76,8 @@ export declare class GrepTool implements AgentTool<typeof searchSchema, GrepTool
77
76
  }
78
77
  interface GrepRenderArgs {
79
78
  pattern: string;
79
+ path?: string | string[];
80
+ /** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
80
81
  paths?: string | string[];
81
82
  case?: boolean;
82
83
  gitignore?: boolean;
@@ -117,6 +117,13 @@ export declare function formatPathRelativeToCwd(filePath: string, cwd: string, o
117
117
  */
118
118
  export declare function stripOuterDoubleQuotes(input: string): string;
119
119
  export declare function normalizePathLikeInput(input: string): string;
120
+ /**
121
+ * Normalize a path argument that may arrive as a single string, a JSON-encoded
122
+ * string array (`'["a.ts"]'`), or an actual array into a flat `string[]`.
123
+ * Delimited single strings (`"a.ts b.ts"`) are left for
124
+ * {@link expandDelimitedPathEntries} to split.
125
+ */
126
+ export declare function toPathList(input: string | string[] | undefined): string[];
120
127
  export declare function hasGlobPathChars(filePath: string): boolean;
121
128
  type PathEntrySplitter = (item: string) => {
122
129
  basePath: string;
@@ -41,5 +41,16 @@ export type ToolRenderer = {
41
41
  * with the final render and may commit like any settled stream.
42
42
  */
43
43
  provisionalPartialResult?: boolean;
44
+ /**
45
+ * Whether the renderer's pending-call path visibly consumes
46
+ * `options.spinnerFrame`. Used to avoid scheduling repaint ticks for live
47
+ * partial calls whose bytes cannot change between spinner frames.
48
+ */
49
+ animatedPendingPreview?: boolean | ((args: unknown) => boolean);
50
+ /**
51
+ * Whether the renderer's partial-result path visibly consumes
52
+ * `options.spinnerFrame`.
53
+ */
54
+ animatedPartialResult?: boolean | ((args: unknown) => boolean);
44
55
  };
45
56
  export declare const toolRenderers: Record<string, ToolRenderer>;
@@ -55,7 +55,8 @@ interface SshRenderContext {
55
55
  totalVisualLines?: number;
56
56
  }
57
57
  export declare const sshToolRenderer: {
58
- renderCall(args: SshRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
58
+ animatedPendingPreview: boolean;
59
+ renderCall(args: SshRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component;
59
60
  renderResult(result: {
60
61
  content: Array<{
61
62
  type: string;
@@ -1,6 +1,8 @@
1
1
  export * from "./downloader";
2
2
  export * from "./models";
3
3
  export * from "./runtime";
4
+ export * from "./speakable";
5
+ export * from "./speech-enhancer";
4
6
  export * from "./tts-client";
5
7
  export * from "./tts-protocol";
6
8
  export * from "./tts-worker";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Streaming markdown → speakable-segment transform for assistant speech.
3
+ *
4
+ * Sits between the assistant's raw streaming text deltas and the TTS engine,
5
+ * deciding both *what* is worth speaking and *when* a piece of text is ready
6
+ * to synthesize. Three passes:
7
+ *
8
+ * 1. Block pass (per character, stateful): drops fenced code blocks and table
9
+ * rows, strips heading/bullet/blockquote markers (numbered-list markers are
10
+ * spoken as "1, …"), and turns newlines into hard segment breaks.
11
+ * 2. Segmentation (stateful): emits a segment the moment a sentence boundary
12
+ * appears — no next-sentence confirmation, which is what made the previous
13
+ * engine-side splitter stall a full sentence behind generation. The first
14
+ * segment cuts early at a clause boundary for fast time-to-first-audio, and
15
+ * over-long unpunctuated runs are force-split so no segment exceeds the
16
+ * synthesizer's input budget.
17
+ * 3. Inline normalization (per segment): markdown links speak their label,
18
+ * bare URLs speak their host, inline-code ticks and emphasis markers are
19
+ * stripped, multi-directory file paths collapse to their basename, HTML
20
+ * tags are dropped, and whitespace is collapsed. Segments with no letters
21
+ * or digits left are not spoken at all.
22
+ *
23
+ * Pure and synchronous — the vocalizer owns timers (idle flush) and the
24
+ * session lifecycle, so this class stays trivially unit-testable.
25
+ */
26
+ /**
27
+ * One per utterance. Feed raw assistant deltas through {@link push}; each call
28
+ * returns the segments that became ready to speak. {@link flush} drains the
29
+ * remainder at message end; {@link flushIdle} drains it when generation stalls
30
+ * mid-sentence so speech doesn't sit on buffered text through a tool call.
31
+ */
32
+ export declare class SpeakableStream {
33
+ #private;
34
+ /** Consume a raw delta; returns segments now ready to speak, in order. */
35
+ push(delta: string): string[];
36
+ /** Message end: drain everything left, including a trailing partial sentence. */
37
+ flush(): string[];
38
+ /**
39
+ * Generation stalled (tool call, thinking block): speak what we have rather
40
+ * than sit silent on buffered text. Keeps block state so the stream resumes
41
+ * afterwards, and refuses stubby mid-sentence fragments — the buffer must be
42
+ * a complete thought (trailing sentence punctuation) or at least
43
+ * {@link MIN_SEGMENT} long, so a stall right after "The" stays silent
44
+ * instead of turning into choppy one-word speech.
45
+ */
46
+ flushIdle(): string[];
47
+ }
@@ -0,0 +1,46 @@
1
+ import type { ModelRegistry } from "../config/model-registry";
2
+ import type { Settings } from "../config/settings";
3
+ /** Session-scoped dependencies; mirrors the auto-thinking classifier's deps. */
4
+ export interface SpeechEnhancerDeps {
5
+ settings: Settings;
6
+ registry: ModelRegistry;
7
+ sessionId: string;
8
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined;
9
+ }
10
+ /**
11
+ * Rewrites one markdown block into spoken prose via the tiny/smol role.
12
+ * Constructed per session by the event controller and handed to the vocalizer.
13
+ */
14
+ export declare class SpeechEnhancer {
15
+ #private;
16
+ constructor(deps: SpeechEnhancerDeps);
17
+ /**
18
+ * Rewrite `block` for speech. Returns the spoken text (empty string when
19
+ * the model judged the block unspeakable — pure code/markup), or null when
20
+ * the rewrite failed, timed out, or no model/key resolved; the caller then
21
+ * falls back to mechanical normalization.
22
+ */
23
+ rewrite(block: string, signal?: AbortSignal): Promise<string | null>;
24
+ }
25
+ /**
26
+ * Fence-aware paragraph accumulator over raw streaming deltas. One instance
27
+ * per utterance.
28
+ */
29
+ export declare class BlockAccumulator {
30
+ #private;
31
+ /** Feed a delta; returns the blocks it completed, in order. */
32
+ push(delta: string): string[];
33
+ /**
34
+ * Message end: drain everything. An unterminated code fence is dropped from
35
+ * its opening line onward (a truncated block is never worth speaking); the
36
+ * prose before it still comes out.
37
+ */
38
+ flush(): string | null;
39
+ /**
40
+ * Generation stalled: drain the pending partial block — unless we are
41
+ * inside a code fence, where the only thing buffered is code and speaking
42
+ * or rewriting half a fence would re-introduce vocalized code. Fence state
43
+ * is preserved so the eventual closing fence still matches.
44
+ */
45
+ flushPartial(): string | null;
46
+ }
@@ -12,8 +12,7 @@ export interface StreamingPlayerLookup {
12
12
  * and plays it to the default output device. An empty list means no streaming
13
13
  * backend is available and the caller should fall back to per-file playback.
14
14
  *
15
- * - darwin: none; `afplay` is file-only, so macOS uses the interruptible
16
- * per-file fallback.
15
+ * - darwin: `ffmpeg` (AudioToolbox output device) sox's `play` (coreaudio).
17
16
  * - linux/other POSIX: `ffmpeg` (`-f pulse` then `-f alsa`) → `paplay`/`aplay`
18
17
  * raw fallbacks.
19
18
  * - win32: none (PowerShell `SoundPlayer` is file-only).
@@ -17,7 +17,7 @@ export interface TtsStreamOptions {
17
17
  voice?: string;
18
18
  signal?: AbortSignal;
19
19
  }
20
- /** One synthesized sentence of a streaming session, in emission order. */
20
+ /** One synthesized segment of a streaming session, in emission order. */
21
21
  export interface TtsAudioChunk {
22
22
  index: number;
23
23
  text: string;
@@ -25,10 +25,10 @@ export interface TtsAudioChunk {
25
25
  sampleRate: number;
26
26
  }
27
27
  /**
28
- * A live streaming-synthesis session. Feed text incrementally with {@link push}
29
- * and close the input with {@link end}; `chunks` yields each synthesized
30
- * sentence's audio as soon as it is ready, then completes once the worker
31
- * finishes draining the closed input.
28
+ * A live streaming-synthesis session. Feed complete speakable segments with
29
+ * {@link push} (the worker synthesizes each push as-is) and close the input
30
+ * with {@link end}; `chunks` yields each segment's audio as soon as it is
31
+ * ready, then completes once the worker finishes draining the closed input.
32
32
  */
33
33
  export interface TtsStreamHandle {
34
34
  push(text: string): void;
@@ -51,11 +51,12 @@ export declare class TtsClient {
51
51
  onProgress(listener: (event: TtsProgressEvent) => void): () => void;
52
52
  synthesize(modelKey: string, text: string, options?: TtsSynthesizeOptions): Promise<TtsAudio | null>;
53
53
  /**
54
- * Open a streaming-synthesis session. Text is fed incrementally through the
55
- * returned handle's `push`/`end`; audio is emitted one synthesized sentence at
56
- * a time via `chunks`, so playback can begin before the full text is known.
57
- * Returns an inert handle (immediately-ended `chunks`) for unknown models or
58
- * an already-aborted signal, and fails the iterator if the worker cannot spawn.
54
+ * Open a streaming-synthesis session. Complete speakable segments are fed
55
+ * through the returned handle's `push`/`end`; audio is emitted one segment
56
+ * at a time via `chunks`, so playback can begin before the full text is
57
+ * known. Returns an inert handle (immediately-ended `chunks`) for unknown
58
+ * models or an already-aborted signal, and fails the iterator if the worker
59
+ * cannot spawn.
59
60
  */
60
61
  synthesizeStream(modelKey: string, options?: TtsStreamOptions): TtsStreamHandle;
61
62
  downloadModel(modelKey: string, options?: TtsDownloadOptions): Promise<boolean>;
@@ -91,5 +91,12 @@ export type TtsWorkerOutbound = {
91
91
  */
92
92
  export interface TtsTransport {
93
93
  send(message: TtsWorkerOutbound): void;
94
+ /**
95
+ * Send and resolve once the message has drained into the IPC channel.
96
+ * Streaming synthesis awaits this per audio chunk: ONNX inference blocks
97
+ * the worker's event loop for seconds at a time, so fire-and-forget sends
98
+ * queue unflushed until the session ends and arrive as one burst.
99
+ */
100
+ sendAndFlush(message: TtsWorkerOutbound): Promise<void>;
94
101
  onMessage(handler: (message: TtsWorkerInbound) => void): () => void;
95
102
  }
@@ -1,3 +1,4 @@
1
+ import { type SpeechEnhancer } from "./speech-enhancer";
1
2
  export interface VocalizerPlayer {
2
3
  start(sampleRate: number): void;
3
4
  write(pcm: Float32Array): void;
@@ -8,16 +9,22 @@ export interface VocalizerPlayer {
8
9
  export declare class Vocalizer {
9
10
  #private;
10
11
  constructor(createPlayer?: () => VocalizerPlayer);
12
+ /** Wire (or drop) the per-session enhanced-rewrite service. */
13
+ setEnhancer(enhancer: SpeechEnhancer | null): void;
11
14
  /**
12
- * Stream a delta of assistant text into the engine. No-op when vocalization
13
- * is disabled. The engine buffers the running text and emits audio for each
14
- * complete sentence; the trailing partial is flushed by {@link flush}.
15
+ * Stream a delta of assistant text into the pipeline. No-op when
16
+ * vocalization is disabled. The synthesis session (worker, player) is only
17
+ * opened once the first speakable segment exists, so a reply that
18
+ * normalizes to silence (pure code, tables, URLs) costs nothing. The
19
+ * trailing partial is flushed by {@link flush} or the idle timer. The
20
+ * pipeline (enhanced vs mechanical) is latched per utterance.
15
21
  */
16
22
  pushDelta(text: string): void;
17
23
  /**
18
- * Close the current input stream (call at message/turn end). The engine
19
- * flushes its trailing partial as a final chunk; the player keeps draining
20
- * queued audio until it completes.
24
+ * Close the current input stream (call at message/turn end). Drains the
25
+ * trailing partial as final segments; in enhanced mode the session ends
26
+ * only after the last in-flight rewrite has pushed, while the next
27
+ * utterance may already be streaming.
21
28
  */
22
29
  flush(): void;
23
30
  /**
@@ -26,8 +33,8 @@ export declare class Vocalizer {
26
33
  */
27
34
  speak(text: string): void;
28
35
  /**
29
- * Interrupt and drop the current session, killing in-flight playback and
30
- * synthesis (new turn / user message / Esc interrupt). Audio stops at once.
36
+ * Interrupt and drop every utterance, killing in-flight playback, synthesis,
37
+ * and rewrites (new turn / user message / Esc interrupt). Audio stops at once.
31
38
  */
32
39
  clear(): void;
33
40
  /** Lower the volume while the user is speaking (push-to-talk), so speech doesn't drown them out. */
@@ -385,6 +385,8 @@ export declare const ls: {
385
385
  }): Promise<string[]>;
386
386
  /** List untracked files (excludes ignored). */
387
387
  untracked(cwd: string, signal?: AbortSignal): Promise<string[]>;
388
+ /** List paths present in a ref, optionally filtered to specific paths. */
389
+ tree(cwd: string, ref: string, files?: readonly string[], signal?: AbortSignal): Promise<string[]>;
388
390
  /** List submodule paths (recursive). */
389
391
  submodules(cwd: string, signal?: AbortSignal): Promise<string[]>;
390
392
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.3.0",
4
+ "version": "16.3.2",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@oh-my-pi/hashline": "16.3.0",
60
- "@oh-my-pi/omp-stats": "16.3.0",
61
- "@oh-my-pi/pi-agent-core": "16.3.0",
62
- "@oh-my-pi/pi-ai": "16.3.0",
63
- "@oh-my-pi/pi-catalog": "16.3.0",
64
- "@oh-my-pi/pi-mnemopi": "16.3.0",
65
- "@oh-my-pi/pi-natives": "16.3.0",
66
- "@oh-my-pi/pi-tui": "16.3.0",
67
- "@oh-my-pi/pi-utils": "16.3.0",
68
- "@oh-my-pi/pi-wire": "16.3.0",
69
- "@oh-my-pi/snapcompact": "16.3.0",
59
+ "@oh-my-pi/hashline": "16.3.2",
60
+ "@oh-my-pi/omp-stats": "16.3.2",
61
+ "@oh-my-pi/pi-agent-core": "16.3.2",
62
+ "@oh-my-pi/pi-ai": "16.3.2",
63
+ "@oh-my-pi/pi-catalog": "16.3.2",
64
+ "@oh-my-pi/pi-mnemopi": "16.3.2",
65
+ "@oh-my-pi/pi-natives": "16.3.2",
66
+ "@oh-my-pi/pi-tui": "16.3.2",
67
+ "@oh-my-pi/pi-utils": "16.3.2",
68
+ "@oh-my-pi/pi-wire": "16.3.2",
69
+ "@oh-my-pi/snapcompact": "16.3.2",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -41,7 +41,7 @@ describe("advisor", () => {
41
41
  type: "toolCall",
42
42
  id: "search-timeout",
43
43
  name: "grep",
44
- arguments: { pattern: "needle", paths: ["packages/coding-agent/src"] },
44
+ arguments: { pattern: "needle", path: "packages/coding-agent/src" },
45
45
  },
46
46
  ],
47
47
  timestamp: 1,
@@ -182,8 +182,8 @@ export const fsFixtures: Record<string, GalleryFixture> = {
182
182
  glob: {
183
183
  label: "Glob",
184
184
  // Streaming: glob half-typed, no limit yet.
185
- streamingArgs: { paths: ["packages/coding-agent/src/tools/*-render"] },
186
- args: { paths: ["packages/coding-agent/src/**/*.test.ts"], limit: 50 },
185
+ streamingArgs: { path: "packages/coding-agent/src/tools/*-render" },
186
+ args: { path: "packages/coding-agent/src/**/*.test.ts", limit: 50 },
187
187
  result: {
188
188
  content: [
189
189
  {
@@ -9,7 +9,7 @@ export const searchFixtures: Record<string, GalleryFixture> = {
9
9
  },
10
10
  args: {
11
11
  pattern: "useState",
12
- paths: ["packages/tui/src"],
12
+ path: "packages/tui/src",
13
13
  },
14
14
  result: {
15
15
  content: [
@@ -160,7 +160,7 @@ export const searchFixtures: Record<string, GalleryFixture> = {
160
160
  },
161
161
  args: {
162
162
  pat: "useState($A)",
163
- paths: ["packages/tui/src/components"],
163
+ path: "packages/tui/src/components",
164
164
  },
165
165
  result: {
166
166
  content: [
package/src/cli.ts CHANGED
@@ -173,14 +173,20 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
173
173
  * worker is idle, and hard-kills the process on parent `disconnect`.
174
174
  */
175
175
  async function runIpcSubprocessWorker<In, Out>(
176
- start: (transport: { send(message: Out): void; onMessage(handler: (message: In) => void): () => void }) => void,
176
+ start: (transport: {
177
+ send(message: Out): void;
178
+ sendAndFlush(message: Out): Promise<void>;
179
+ onMessage(handler: (message: In) => void): () => void;
180
+ }) => void,
177
181
  ): Promise<void> {
178
182
  const { promise: shuttingDown, resolve: shutdown } = Promise.withResolvers<void>();
183
+ type IpcSend = (this: NodeJS.Process, message: unknown, callback?: (error: Error | null) => void) => boolean;
184
+ // `process.send` only exists when spawned with an IPC channel; the parent
185
+ // always spawns us that way. If it's missing, the parent vanished and
186
+ // there's no one to talk to.
187
+ const ipcSend = (): IpcSend | undefined => (process as NodeJS.Process & { send?: IpcSend }).send;
179
188
  const send = (message: Out): void => {
180
- // `process.send` only exists when spawned with an IPC channel; the
181
- // parent always spawns us that way. If it's missing, the parent
182
- // vanished and there's no one to talk to.
183
- const sender = (process as NodeJS.Process & { send?: (m: unknown) => boolean }).send;
189
+ const sender = ipcSend();
184
190
  if (!sender) {
185
191
  shutdown();
186
192
  return;
@@ -191,8 +197,24 @@ async function runIpcSubprocessWorker<In, Out>(
191
197
  shutdown();
192
198
  }
193
199
  };
200
+ const sendAndFlush = (message: Out): Promise<void> => {
201
+ const sender = ipcSend();
202
+ if (!sender) {
203
+ shutdown();
204
+ return Promise.resolve();
205
+ }
206
+ const { promise, resolve } = Promise.withResolvers<void>();
207
+ try {
208
+ sender.call(process, message, () => resolve());
209
+ } catch {
210
+ shutdown();
211
+ resolve();
212
+ }
213
+ return promise;
214
+ };
194
215
  start({
195
216
  send,
217
+ sendAndFlush,
196
218
  onMessage(handler) {
197
219
  const wrap = (data: unknown): void => handler(data as In);
198
220
  process.on("message", wrap);