@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3

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 (163) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3855 -3798
  3. package/dist/types/cli/update-cli.d.ts +1 -0
  4. package/dist/types/cli/update-cli.test.d.ts +1 -0
  5. package/dist/types/commands/update.d.ts +5 -0
  6. package/dist/types/config/config-file.d.ts +1 -1
  7. package/dist/types/config/settings-schema.d.ts +20 -0
  8. package/dist/types/dap/client.d.ts +9 -1
  9. package/dist/types/discovery/helpers.d.ts +2 -0
  10. package/dist/types/edit/file-snapshot-store.d.ts +9 -1
  11. package/dist/types/edit/hashline/execute.d.ts +1 -1
  12. package/dist/types/edit/hashline/params.d.ts +3 -6
  13. package/dist/types/edit/renderer.d.ts +1 -0
  14. package/dist/types/extensibility/plugins/parser.d.ts +2 -1
  15. package/dist/types/hindsight/client.d.ts +15 -11
  16. package/dist/types/hindsight/client.test.d.ts +1 -0
  17. package/dist/types/irc/bus.d.ts +5 -4
  18. package/dist/types/mcp/smithery-registry.d.ts +1 -0
  19. package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
  20. package/dist/types/modes/components/advisor-message.d.ts +4 -2
  21. package/dist/types/modes/components/tool-execution.d.ts +9 -6
  22. package/dist/types/modes/components/transcript-container.d.ts +1 -0
  23. package/dist/types/modes/theme/theme.d.ts +1 -1
  24. package/dist/types/session/agent-session.d.ts +13 -13
  25. package/dist/types/session/indexed-session-storage.d.ts +2 -2
  26. package/dist/types/session/session-storage.d.ts +31 -3
  27. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
  28. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
  29. package/dist/types/ssh/connection-manager.d.ts +19 -0
  30. package/dist/types/ssh/sshfs-mount.d.ts +10 -1
  31. package/dist/types/subprocess/worker-client.d.ts +20 -6
  32. package/dist/types/task/discovery.d.ts +5 -2
  33. package/dist/types/task/renderer.d.ts +1 -0
  34. package/dist/types/tools/ast-grep.d.ts +4 -2
  35. package/dist/types/tools/bash.d.ts +27 -0
  36. package/dist/types/tools/browser/render.d.ts +2 -0
  37. package/dist/types/tools/debug.d.ts +1 -0
  38. package/dist/types/tools/eval-render.d.ts +2 -0
  39. package/dist/types/tools/glob.d.ts +4 -2
  40. package/dist/types/tools/grep.d.ts +4 -3
  41. package/dist/types/tools/path-utils.d.ts +7 -0
  42. package/dist/types/tools/renderers.d.ts +24 -0
  43. package/dist/types/tools/ssh.d.ts +4 -1
  44. package/dist/types/tts/index.d.ts +2 -0
  45. package/dist/types/tts/speakable.d.ts +47 -0
  46. package/dist/types/tts/speech-enhancer.d.ts +46 -0
  47. package/dist/types/tts/streaming-player.d.ts +1 -2
  48. package/dist/types/tts/tts-client.d.ts +11 -10
  49. package/dist/types/tts/tts-protocol.d.ts +7 -0
  50. package/dist/types/tts/vocalizer.d.ts +15 -8
  51. package/dist/types/utils/fetch-timeout.d.ts +4 -0
  52. package/dist/types/utils/git.d.ts +2 -0
  53. package/dist/types/web/search/providers/base.d.ts +1 -0
  54. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  55. package/package.json +12 -12
  56. package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
  57. package/src/advisor/__tests__/advisor.test.ts +1 -1
  58. package/src/cli/gallery-fixtures/fs.ts +2 -2
  59. package/src/cli/gallery-fixtures/search.ts +2 -2
  60. package/src/cli/models-cli.ts +19 -0
  61. package/src/cli/update-cli.test.ts +28 -0
  62. package/src/cli/update-cli.ts +35 -8
  63. package/src/cli.ts +27 -5
  64. package/src/commands/update.ts +8 -2
  65. package/src/config/config-file.ts +6 -6
  66. package/src/config/model-resolver.ts +31 -10
  67. package/src/config/settings-schema.ts +21 -0
  68. package/src/cursor.ts +1 -1
  69. package/src/dap/client.ts +134 -36
  70. package/src/discovery/helpers.ts +8 -0
  71. package/src/edit/file-snapshot-store.ts +12 -1
  72. package/src/edit/hashline/diff.ts +4 -13
  73. package/src/edit/hashline/execute.ts +1 -1
  74. package/src/edit/hashline/params.ts +5 -12
  75. package/src/edit/renderer.ts +4 -2
  76. package/src/edit/streaming.ts +15 -5
  77. package/src/export/html/tool-views.generated.js +34 -34
  78. package/src/extensibility/custom-tools/loader.ts +3 -3
  79. package/src/extensibility/extensions/loader.ts +10 -3
  80. package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
  81. package/src/extensibility/plugins/installer.ts +12 -3
  82. package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
  83. package/src/extensibility/plugins/loader.ts +30 -1
  84. package/src/extensibility/plugins/manager.ts +32 -8
  85. package/src/extensibility/plugins/parser.ts +7 -5
  86. package/src/extensibility/tool-event-input.ts +1 -1
  87. package/src/hindsight/client.test.ts +33 -0
  88. package/src/hindsight/client.ts +42 -22
  89. package/src/internal-urls/docs-index.generated.txt +1 -1
  90. package/src/irc/bus.ts +5 -4
  91. package/src/main.ts +7 -1
  92. package/src/mcp/oauth-flow.ts +93 -4
  93. package/src/mcp/smithery-auth.ts +3 -0
  94. package/src/mcp/smithery-connect.ts +9 -0
  95. package/src/mcp/smithery-registry.test.ts +51 -0
  96. package/src/mcp/smithery-registry.ts +27 -4
  97. package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
  98. package/src/modes/components/advisor-message.ts +13 -10
  99. package/src/modes/components/assistant-message.ts +1 -1
  100. package/src/modes/components/move-overlay.ts +35 -23
  101. package/src/modes/components/status-line/component.ts +11 -4
  102. package/src/modes/components/tool-execution.ts +119 -20
  103. package/src/modes/components/transcript-container.ts +26 -0
  104. package/src/modes/components/tree-selector.ts +10 -3
  105. package/src/modes/controllers/event-controller.ts +23 -3
  106. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  107. package/src/modes/interactive-mode.ts +1 -0
  108. package/src/modes/rpc/rpc-client.ts +29 -16
  109. package/src/modes/theme/shimmer.ts +49 -15
  110. package/src/modes/theme/theme.ts +7 -0
  111. package/src/prompts/goals/goal-mode-context.md +4 -0
  112. package/src/prompts/goals/goal-todo-context.md +10 -2
  113. package/src/prompts/system/speech-rewrite.md +15 -0
  114. package/src/prompts/system/tiny-title-system.md +1 -1
  115. package/src/prompts/system/title-system-marker.md +2 -1
  116. package/src/prompts/system/title-system.md +2 -1
  117. package/src/prompts/tools/ast-grep.md +3 -3
  118. package/src/prompts/tools/glob.md +1 -1
  119. package/src/prompts/tools/grep.md +1 -1
  120. package/src/sdk.ts +8 -5
  121. package/src/session/agent-session.ts +329 -79
  122. package/src/session/indexed-session-storage.ts +40 -3
  123. package/src/session/session-history-format.ts +6 -2
  124. package/src/session/session-manager.ts +83 -14
  125. package/src/session/session-storage.ts +112 -26
  126. package/src/slash-commands/helpers/usage-report.ts +6 -1
  127. package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
  128. package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
  129. package/src/ssh/connection-manager.ts +44 -11
  130. package/src/ssh/sshfs-mount.ts +27 -4
  131. package/src/subprocess/worker-client.ts +161 -10
  132. package/src/task/discovery.ts +25 -2
  133. package/src/task/executor.ts +24 -4
  134. package/src/task/render.ts +26 -12
  135. package/src/task/renderer.ts +1 -0
  136. package/src/task/worktree.ts +144 -16
  137. package/src/tiny/text.ts +109 -17
  138. package/src/tools/ast-grep.ts +20 -17
  139. package/src/tools/bash.ts +46 -9
  140. package/src/tools/browser/render.ts +2 -0
  141. package/src/tools/debug.ts +1 -0
  142. package/src/tools/eval-render.ts +5 -2
  143. package/src/tools/gh-renderer.ts +3 -0
  144. package/src/tools/glob.ts +24 -20
  145. package/src/tools/grep.ts +39 -37
  146. package/src/tools/path-utils.ts +55 -10
  147. package/src/tools/read.ts +23 -6
  148. package/src/tools/renderers.ts +24 -0
  149. package/src/tools/ssh.ts +25 -7
  150. package/src/tts/index.ts +2 -0
  151. package/src/tts/speakable.ts +382 -0
  152. package/src/tts/speech-enhancer.ts +204 -0
  153. package/src/tts/streaming-player.ts +71 -16
  154. package/src/tts/tts-client.ts +11 -10
  155. package/src/tts/tts-protocol.ts +14 -5
  156. package/src/tts/tts-worker.ts +52 -49
  157. package/src/tts/vocalizer.ts +277 -46
  158. package/src/utils/clipboard.ts +49 -12
  159. package/src/utils/fetch-timeout.ts +10 -0
  160. package/src/utils/git.ts +8 -0
  161. package/src/web/search/index.ts +8 -0
  162. package/src/web/search/providers/base.ts +1 -0
  163. package/src/web/search/providers/gemini.ts +23 -6
@@ -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
  }
@@ -5,6 +5,33 @@ import { type Theme } from "../modes/theme/theme";
5
5
  import type { ToolSession } from ".";
6
6
  import { type OutputMeta } from "./output-meta";
7
7
  export declare const BASH_DEFAULT_PREVIEW_LINES = 10;
8
+ /**
9
+ * Shape a shell command line for an ACP-conformant `terminal/create` request.
10
+ *
11
+ * ACP's `command` field is documented as the executable and `args` as its
12
+ * argv tail (see https://agentclientprotocol.com/protocol/v1/terminals), so a
13
+ * spec-conformant client `spawn(command, args)`s them directly — no implicit
14
+ * shell. A raw `bash` tool line ("git status && echo x | head") therefore has
15
+ * to be wrapped in an explicit shell invocation, otherwise the client tries
16
+ * to spawn the whole line as argv[0] and fails with `ENOENT` for anything
17
+ * containing a space, pipe, `&&`, redirect, or `$(...)`.
18
+ *
19
+ * The wrap reuses the same shell binary + args the local `bash-executor` would
20
+ * pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows,
21
+ * `$SHELL` (bash/zsh) with the `sh` fallback on POSIX — so the ACP path
22
+ * preserves `bash` tool semantics (`$VAR`, `$(...)`, `source`, POSIX quoting,
23
+ * `-l`) instead of dropping to `cmd.exe` on Windows. The agent host's shell
24
+ * path is used as a proxy for the client's, matching the near-universal
25
+ * ACP deployment shape of an editor spawning omp as a co-hosted subprocess.
26
+ */
27
+ export declare function wrapShellLineForClientTerminal(line: string, shellConfig: {
28
+ shell: string;
29
+ args: string[];
30
+ prefix?: string | undefined;
31
+ }): {
32
+ command: string;
33
+ args: string[];
34
+ };
8
35
  /**
9
36
  * Bash patterns flagged as safety critical for approval policy.
10
37
  *
@@ -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,29 @@ 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);
55
+ /**
56
+ * Whether replacing a streamed pending placeholder with the first result
57
+ * requires a full viewport repaint. Use for merged renderers whose pending
58
+ * streamed args may have committed placeholder rows that the result render
59
+ * re-anchors instead of preserving.
60
+ */
61
+ forceFirstResultViewportRepaint?: boolean;
62
+ /**
63
+ * Whether settling a provisional partial result into the final render requires
64
+ * a full viewport repaint. Use when the result renderer changes chrome or
65
+ * frame topology at `options.isPartial: true -> false`.
66
+ */
67
+ forceResultViewportRepaintOnSettle?: boolean;
44
68
  };
45
69
  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;
@@ -69,5 +70,7 @@ export declare const sshToolRenderer: {
69
70
  mergeCallAndResult: boolean;
70
71
  provisionalPendingPreview: boolean;
71
72
  provisionalPartialResult: boolean;
73
+ forceFirstResultViewportRepaint: boolean;
74
+ forceResultViewportRepaintOnSettle: boolean;
72
75
  };
73
76
  export {};
@@ -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. */
@@ -0,0 +1,4 @@
1
+ /** Create an abort signal that fires after a timeout and preserves caller cancellation. */
2
+ export declare function withTimeoutSignal(timeoutMs: number, signal?: AbortSignal): AbortSignal;
3
+ /** Detect a timeout raised by an abortable fetch. */
4
+ export declare function isTimeoutError(error: unknown): boolean;
@@ -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
  };
@@ -51,6 +51,7 @@ export interface SearchParams {
51
51
  */
52
52
  sessionId?: string;
53
53
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
54
+ geminiModel?: string;
54
55
  }
55
56
  /** Base class for web search providers. */
56
57
  export declare abstract class SearchProvider {
@@ -32,6 +32,7 @@ export interface GeminiSearchParams extends GeminiToolParams {
32
32
  sessionId?: string;
33
33
  fetch?: FetchImpl;
34
34
  antigravityEndpointMode?: "auto" | "production" | "sandbox";
35
+ geminiModel?: string;
35
36
  }
36
37
  export declare function buildGeminiRequestTools(params: GeminiToolParams): Array<Record<string, Record<string, unknown>>>;
37
38
  /** First configured Gemini OAuth provider plus its pre-resolved access. */
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.3",
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.3",
60
+ "@oh-my-pi/omp-stats": "16.3.3",
61
+ "@oh-my-pi/pi-agent-core": "16.3.3",
62
+ "@oh-my-pi/pi-ai": "16.3.3",
63
+ "@oh-my-pi/pi-catalog": "16.3.3",
64
+ "@oh-my-pi/pi-mnemopi": "16.3.3",
65
+ "@oh-my-pi/pi-natives": "16.3.3",
66
+ "@oh-my-pi/pi-tui": "16.3.3",
67
+ "@oh-my-pi/pi-utils": "16.3.3",
68
+ "@oh-my-pi/pi-wire": "16.3.3",
69
+ "@oh-my-pi/snapcompact": "16.3.3",
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",
@@ -332,9 +332,15 @@ async function formatInPlace(targets: readonly string[]): Promise<void> {
332
332
  stdout: "pipe",
333
333
  stderr: "pipe",
334
334
  });
335
- const exit = await proc.exited;
335
+ // Drain both pipes concurrently with proc.exited to avoid a pipe-buffer
336
+ // deadlock — biome check can emit thousands of lines when it rewrites the
337
+ // generated registry, easily exceeding the ~64 KiB OS pipe buffer.
338
+ const [exit, , stderr] = await Promise.all([
339
+ proc.exited,
340
+ new Response(proc.stdout).text(),
341
+ new Response(proc.stderr).text(),
342
+ ]);
336
343
  if (exit !== 0) {
337
- const stderr = await new Response(proc.stderr).text();
338
344
  throw new Error(`biome check --write failed (exit ${exit}): ${stderr}`);
339
345
  }
340
346
  }
@@ -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: [
@@ -84,6 +84,14 @@ function writeLine(line = ""): void {
84
84
  process.stdout.write(`${line}\n`);
85
85
  }
86
86
 
87
+ function writeModelsConfigError(error: Error): void {
88
+ writeLine(chalk.yellow("Warning: models.yml validation failed — custom providers disabled"));
89
+ for (const line of error.message.split("\n")) {
90
+ writeLine(` ${line}`);
91
+ }
92
+ writeLine();
93
+ }
94
+
87
95
  function formatLimit(n: number | null): string {
88
96
  return n === null ? "-" : formatNumber(n);
89
97
  }
@@ -187,12 +195,23 @@ function renderProviderModels(
187
195
  }
188
196
  }
189
197
 
198
+ const configError = modelRegistry.getError();
199
+
190
200
  if (json) {
201
+ if (configError) {
202
+ process.stderr.write(
203
+ `Warning: models.yml validation failed — custom providers disabled\n${configError.message}\n`,
204
+ );
205
+ }
191
206
  const output: ModelsJson = { models: filtered.slice().sort(byProviderThenId).map(toModelJson) };
192
207
  writeLine(JSON.stringify(output));
193
208
  return;
194
209
  }
195
210
 
211
+ if (configError) {
212
+ writeModelsConfigError(configError);
213
+ }
214
+
196
215
  if (available.length === 0) {
197
216
  writeLine("No models available. Set API keys in environment variables.");
198
217
  return;
@@ -0,0 +1,28 @@
1
+ import { afterEach, describe, expect, it, vi } from "bun:test";
2
+ import { runUpdateCommand } from "./update-cli";
3
+
4
+ type FetchInput = string | URL | Request;
5
+ type FetchInit = RequestInit | BunFetchRequestInit;
6
+
7
+ describe("runUpdateCommand fetch cancellation", () => {
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ it("checks release metadata with a timeout signal", async () => {
13
+ let requestSignal: AbortSignal | undefined;
14
+ vi.spyOn(console, "log").mockImplementation(() => {});
15
+ const fetchStub = Object.assign(
16
+ async (_input: FetchInput, init?: FetchInit) => {
17
+ requestSignal = init?.signal ?? undefined;
18
+ return Response.json({ version: "999.0.0" });
19
+ },
20
+ { preconnect: globalThis.fetch.preconnect },
21
+ );
22
+ vi.spyOn(globalThis, "fetch").mockImplementation(fetchStub);
23
+
24
+ await runUpdateCommand({ force: false, check: true });
25
+
26
+ expect(requestSignal).toBeInstanceOf(AbortSignal);
27
+ });
28
+ });