@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.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 (196) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/cli.js +6325 -4420
  3. package/dist/types/cli/bench-cli.d.ts +1 -0
  4. package/dist/types/config/model-resolver.d.ts +6 -3
  5. package/dist/types/config/settings-schema.d.ts +62 -0
  6. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  7. package/dist/types/launch/broker-list-order.test.d.ts +1 -0
  8. package/dist/types/live/attestation.d.ts +2 -0
  9. package/dist/types/live/controller.d.ts +10 -2
  10. package/dist/types/live/protocol.d.ts +1 -1
  11. package/dist/types/live/transport.d.ts +6 -19
  12. package/dist/types/live/visualizer.d.ts +8 -11
  13. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  14. package/dist/types/modes/components/custom-message.d.ts +1 -1
  15. package/dist/types/modes/components/message-frame.d.ts +8 -4
  16. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  17. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  18. package/dist/types/modes/interactive-mode.d.ts +3 -6
  19. package/dist/types/modes/types.d.ts +9 -5
  20. package/dist/types/modes/utils/ui-helpers.d.ts +1 -6
  21. package/dist/types/session/agent-session-types.d.ts +8 -1
  22. package/dist/types/session/agent-session.d.ts +20 -1
  23. package/dist/types/session/auth-storage.d.ts +1 -1
  24. package/dist/types/session/eval-runner.d.ts +2 -0
  25. package/dist/types/session/messages.d.ts +2 -0
  26. package/dist/types/session/session-tools.d.ts +15 -0
  27. package/dist/types/session/streaming-output.d.ts +8 -0
  28. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  29. package/dist/types/stt/index.d.ts +0 -2
  30. package/dist/types/stt/stt-controller.d.ts +7 -0
  31. package/dist/types/task/executor.d.ts +3 -1
  32. package/dist/types/task/structured-subagent.d.ts +3 -0
  33. package/dist/types/task/types.d.ts +11 -11
  34. package/dist/types/thinking.d.ts +13 -0
  35. package/dist/types/tiny/title-client.d.ts +10 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -1
  37. package/dist/types/tools/computer/protocol.d.ts +43 -0
  38. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  39. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  40. package/dist/types/tools/computer/worker.d.ts +15 -0
  41. package/dist/types/tools/computer-renderer.d.ts +22 -0
  42. package/dist/types/tools/computer.d.ts +71 -0
  43. package/dist/types/tools/context.d.ts +2 -0
  44. package/dist/types/tools/default-renderer.d.ts +21 -0
  45. package/dist/types/tools/eval-format/index.d.ts +7 -0
  46. package/dist/types/tools/eval-format/javascript.d.ts +2 -0
  47. package/dist/types/tools/eval-format/julia.d.ts +2 -0
  48. package/dist/types/tools/eval-format/python.d.ts +2 -0
  49. package/dist/types/tools/eval-format/ruby.d.ts +2 -0
  50. package/dist/types/tools/index.d.ts +2 -0
  51. package/dist/types/tools/resolve.d.ts +7 -0
  52. package/dist/types/tools/todo.d.ts +4 -1
  53. package/dist/types/tts/streaming-player.d.ts +10 -43
  54. package/dist/types/utils/tools-manager.d.ts +1 -2
  55. package/dist/types/web/search/providers/base.d.ts +16 -0
  56. package/dist/types/web/search/providers/brave.d.ts +2 -0
  57. package/dist/types/web/search/providers/firecrawl.d.ts +2 -0
  58. package/dist/types/web/search/providers/gemini.d.ts +3 -0
  59. package/dist/types/web/search/providers/jina.d.ts +2 -0
  60. package/dist/types/web/search/providers/kagi.d.ts +2 -0
  61. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  62. package/dist/types/web/search/providers/parallel.d.ts +2 -0
  63. package/dist/types/web/search/providers/perplexity.d.ts +3 -0
  64. package/dist/types/web/search/providers/searxng.d.ts +10 -0
  65. package/dist/types/web/search/providers/tavily.d.ts +8 -0
  66. package/dist/types/web/search/query.d.ts +190 -0
  67. package/package.json +12 -12
  68. package/src/cli/args.ts +1 -0
  69. package/src/cli/bench-cli.ts +12 -1
  70. package/src/cli/setup-cli.ts +2 -14
  71. package/src/cli/web-search-cli.ts +7 -0
  72. package/src/cli.ts +8 -0
  73. package/src/config/model-registry.ts +6 -0
  74. package/src/config/model-resolver.ts +17 -6
  75. package/src/config/settings-schema.ts +66 -0
  76. package/src/debug/raw-sse-buffer.ts +157 -30
  77. package/src/eval/executor-base.ts +1 -0
  78. package/src/eval/js/executor.ts +2 -0
  79. package/src/exec/bash-executor.ts +1 -0
  80. package/src/export/share.ts +4 -3
  81. package/src/extensibility/extensions/wrapper.ts +68 -12
  82. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  83. package/src/launch/broker-list-order.test.ts +89 -0
  84. package/src/launch/broker.ts +49 -8
  85. package/src/live/attestation.ts +91 -0
  86. package/src/live/controller.ts +76 -23
  87. package/src/live/protocol.test.ts +3 -3
  88. package/src/live/protocol.ts +1 -1
  89. package/src/live/transport.ts +72 -140
  90. package/src/live/visualizer.ts +114 -134
  91. package/src/modes/components/assistant-message.ts +7 -2
  92. package/src/modes/components/custom-message.ts +4 -1
  93. package/src/modes/components/message-frame.ts +14 -8
  94. package/src/modes/components/session-account-selector.ts +62 -0
  95. package/src/modes/components/tool-execution.ts +17 -110
  96. package/src/modes/controllers/input-controller.ts +55 -47
  97. package/src/modes/controllers/live-command-controller.ts +82 -5
  98. package/src/modes/controllers/selector-controller.ts +62 -0
  99. package/src/modes/interactive-mode.ts +79 -5
  100. package/src/modes/types.ts +10 -4
  101. package/src/modes/utils/ui-helpers.ts +7 -8
  102. package/src/prompts/system/computer-safety.md +14 -0
  103. package/src/prompts/system/resolve-device-reminder.md +1 -1
  104. package/src/prompts/system/workflow-notice.md +1 -1
  105. package/src/prompts/tools/ast-edit.md +1 -0
  106. package/src/prompts/tools/bash.md +1 -0
  107. package/src/prompts/tools/computer.md +26 -0
  108. package/src/prompts/tools/eval.md +2 -2
  109. package/src/prompts/tools/task.md +5 -2
  110. package/src/prompts/tools/web-search.md +2 -0
  111. package/src/sdk.ts +13 -4
  112. package/src/session/agent-session-types.ts +9 -0
  113. package/src/session/agent-session.ts +70 -0
  114. package/src/session/auth-storage.ts +1 -0
  115. package/src/session/eval-runner.ts +5 -0
  116. package/src/session/messages.ts +3 -0
  117. package/src/session/queued-messages.ts +7 -1
  118. package/src/session/session-tools.ts +37 -0
  119. package/src/session/streaming-output.ts +52 -5
  120. package/src/slash-commands/builtin-registry.ts +165 -9
  121. package/src/slash-commands/helpers/session-pin.ts +44 -0
  122. package/src/stt/downloader.ts +0 -2
  123. package/src/stt/index.ts +0 -2
  124. package/src/stt/stt-controller.ts +57 -146
  125. package/src/system-prompt.ts +4 -0
  126. package/src/task/executor.ts +17 -9
  127. package/src/task/index.ts +14 -34
  128. package/src/task/structured-subagent.ts +4 -0
  129. package/src/task/types.ts +15 -13
  130. package/src/thinking.ts +27 -0
  131. package/src/tiny/title-client.ts +22 -0
  132. package/src/tools/ast-edit.ts +4 -1
  133. package/src/tools/bash-interactive.ts +90 -86
  134. package/src/tools/bash.ts +13 -0
  135. package/src/tools/browser/render.ts +2 -1
  136. package/src/tools/builtin-names.ts +1 -0
  137. package/src/tools/computer/protocol.ts +28 -0
  138. package/src/tools/computer/supervisor.ts +258 -0
  139. package/src/tools/computer/worker-entry.ts +25 -0
  140. package/src/tools/computer/worker.ts +135 -0
  141. package/src/tools/computer-renderer.ts +108 -0
  142. package/src/tools/computer.ts +433 -0
  143. package/src/tools/context.ts +2 -0
  144. package/src/tools/default-renderer.ts +139 -0
  145. package/src/tools/essential-tools.ts +1 -0
  146. package/src/tools/eval-format/index.ts +24 -0
  147. package/src/tools/eval-format/javascript.ts +952 -0
  148. package/src/tools/eval-format/julia.ts +446 -0
  149. package/src/tools/eval-format/python.ts +544 -0
  150. package/src/tools/eval-format/ruby.ts +380 -0
  151. package/src/tools/eval-render.ts +12 -6
  152. package/src/tools/index.ts +5 -0
  153. package/src/tools/renderers.ts +2 -0
  154. package/src/tools/resolve.ts +10 -1
  155. package/src/tools/todo.ts +58 -6
  156. package/src/tools/xdev.ts +54 -26
  157. package/src/tts/streaming-player.ts +81 -340
  158. package/src/utils/clipboard.ts +1 -30
  159. package/src/utils/mac-file-urls.applescript +37 -0
  160. package/src/utils/tool-choice.ts +14 -0
  161. package/src/utils/tools-manager.ts +1 -19
  162. package/src/web/search/index.ts +28 -5
  163. package/src/web/search/providers/anthropic.ts +62 -4
  164. package/src/web/search/providers/base.ts +16 -0
  165. package/src/web/search/providers/brave.ts +30 -3
  166. package/src/web/search/providers/codex.ts +14 -1
  167. package/src/web/search/providers/duckduckgo.ts +23 -1
  168. package/src/web/search/providers/ecosia.ts +5 -1
  169. package/src/web/search/providers/exa.ts +28 -1
  170. package/src/web/search/providers/firecrawl.ts +37 -3
  171. package/src/web/search/providers/gemini.ts +12 -2
  172. package/src/web/search/providers/google.ts +2 -1
  173. package/src/web/search/providers/jina.ts +31 -8
  174. package/src/web/search/providers/kagi.ts +10 -1
  175. package/src/web/search/providers/kimi.ts +16 -1
  176. package/src/web/search/providers/mojeek.ts +14 -1
  177. package/src/web/search/providers/parallel.ts +48 -2
  178. package/src/web/search/providers/perplexity.ts +94 -6
  179. package/src/web/search/providers/searxng.ts +145 -9
  180. package/src/web/search/providers/startpage.ts +8 -2
  181. package/src/web/search/providers/synthetic.ts +7 -1
  182. package/src/web/search/providers/tavily.ts +52 -3
  183. package/src/web/search/providers/tinyfish.ts +6 -1
  184. package/src/web/search/providers/xai.ts +49 -2
  185. package/src/web/search/providers/zai.ts +19 -1
  186. package/src/web/search/query.ts +850 -0
  187. package/dist/types/stt/recorder.d.ts +0 -30
  188. package/dist/types/stt/transcriber.d.ts +0 -14
  189. package/dist/types/stt/wav.d.ts +0 -29
  190. package/dist/types/tts/player.d.ts +0 -32
  191. package/src/live/audio-worklet.txt +0 -59
  192. package/src/live/browser-runtime.txt +0 -221
  193. package/src/stt/recorder.ts +0 -551
  194. package/src/stt/transcriber.ts +0 -60
  195. package/src/stt/wav.ts +0 -173
  196. package/src/tts/player.ts +0 -137
@@ -33,6 +33,7 @@ import { theme } from "../modes/theme/theme";
33
33
  import type { InteractiveModeContext } from "../modes/types";
34
34
  import { extractLastCodeBlock, extractLastCommand } from "../modes/utils/copy-targets";
35
35
  import type { AgentSession, FreshSessionResult } from "../session/agent-session";
36
+ import type { SessionOAuthAccountList } from "../session/agent-session-types";
36
37
  import { COMPACT_MODES, parseCompactArgs } from "../session/compact-modes";
37
38
  import { resolveResumableSession } from "../session/session-listing";
38
39
  import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
@@ -52,6 +53,7 @@ import { createMarketplaceManager } from "./helpers/marketplace-manager";
52
53
  import { handleMcpAcp } from "./helpers/mcp";
53
54
  import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
54
55
  import { describeRedeemOutcome, type ResetUsageAccount, toResetUsageAccounts } from "./helpers/reset-usage";
56
+ import { matchSessionPinAccounts, toSessionPinAccounts } from "./helpers/session-pin";
55
57
  import { handleSshAcp } from "./helpers/ssh";
56
58
  import { launchStatsDashboard, parseStatsDashboardArgs } from "./helpers/stats-dashboard";
57
59
  import { handleTodoAcp } from "./helpers/todo";
@@ -88,6 +90,26 @@ function formatFastModeStatus(session: AgentSession): string {
88
90
  return session.isFastModeEnabled() ? "on" : "off";
89
91
  }
90
92
 
93
+ /** `/computer status` label for the session-effective `computer.enabled` value. */
94
+ function formatComputerUseStatus(session: AgentSession): string {
95
+ return session.settings.get("computer.enabled") ? "on" : "off";
96
+ }
97
+
98
+ /**
99
+ * Apply a session-scoped computer-use toggle: flip the active tool slate first
100
+ * (so a failed enable never leaves a stale settings override), then record the
101
+ * runtime override — never `settings.set`, which would persist to settings.json.
102
+ * Returns the operator feedback line.
103
+ */
104
+ async function applyComputerUseToggle(session: AgentSession, enable: boolean): Promise<string> {
105
+ const applied = await session.setComputerToolEnabled(enable);
106
+ if (enable && !applied) {
107
+ return "Computer use is unavailable in this session.";
108
+ }
109
+ session.settings.override("computer.enabled", enable);
110
+ return `Computer use ${enable ? "enabled" : "disabled"} for this session.`;
111
+ }
112
+
91
113
  const AUTOCOMPLETE_DETAIL_LIMIT = 48;
92
114
 
93
115
  function shortDetail(value: string, limit = AUTOCOMPLETE_DETAIL_LIMIT): string {
@@ -196,6 +218,74 @@ async function handleUsageResetCommand(
196
218
  await output(describeRedeemOutcome(outcome, target.label));
197
219
  }
198
220
 
221
+ async function handleSessionPinCommand(
222
+ arg: string,
223
+ session: AgentSession,
224
+ output: SlashCommandRuntime["output"],
225
+ ): Promise<void> {
226
+ if (session.isStreaming) {
227
+ await output("Cannot pin an account while the session is streaming.");
228
+ return;
229
+ }
230
+ let accountList: SessionOAuthAccountList | undefined;
231
+ try {
232
+ accountList = await session.listCurrentProviderOAuthAccounts();
233
+ } catch (error) {
234
+ await output(`Could not load provider accounts: ${errorMessage(error)}`);
235
+ return;
236
+ }
237
+ if (!accountList) {
238
+ await output("Select a model before pinning a provider account.");
239
+ return;
240
+ }
241
+ const provider = getOAuthProviders().find(candidate => candidate.id === accountList.provider);
242
+ const providerName = provider?.name ?? accountList.provider;
243
+ const accounts = toSessionPinAccounts(accountList.accounts);
244
+ if (accounts.length === 0) {
245
+ const source = session.modelRegistry.authStorage.describeCredentialSource(
246
+ accountList.provider,
247
+ session.sessionId,
248
+ );
249
+ await output(
250
+ source
251
+ ? `No stored OAuth accounts for ${providerName}. Current auth comes from ${source}.`
252
+ : `No stored OAuth accounts for ${providerName}. Use /login to add one.`,
253
+ );
254
+ return;
255
+ }
256
+
257
+ const selector = arg.trim();
258
+ if (!selector) {
259
+ const lines = [`OAuth accounts for ${providerName}:`];
260
+ for (const account of accounts) {
261
+ lines.push(`${account.position + 1}. ${account.label}${account.active ? " (active)" : ""}`);
262
+ }
263
+ lines.push("", "Pin one with `/session pin <number|email|account id>`.");
264
+ await output(lines.join("\n"));
265
+ return;
266
+ }
267
+
268
+ const matches = matchSessionPinAccounts(accounts, selector);
269
+ if (matches.length === 0) {
270
+ await output(`No ${providerName} account matches "${selector}".`);
271
+ return;
272
+ }
273
+ if (matches.length > 1) {
274
+ await output(
275
+ `"${selector}" matches multiple ${providerName} accounts: ${matches
276
+ .map(account => `${account.position + 1}. ${account.label}`)
277
+ .join(", ")}. Use the account number.`,
278
+ );
279
+ return;
280
+ }
281
+ const account = matches[0];
282
+ if (!account || !session.pinCurrentProviderOAuthAccount(account.credentialId)) {
283
+ await output(`${account?.label ?? selector} is no longer available to pin.`);
284
+ return;
285
+ }
286
+ await output(`Pinned ${account.label} to this session for ${providerName}.`);
287
+ }
288
+
199
289
  /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
200
290
  function parseShakeMode(args: string): ShakeMode | { error: string } {
201
291
  const verb = args.trim().toLowerCase();
@@ -472,6 +562,49 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
472
562
  runtime.ctx.editor.setText("");
473
563
  },
474
564
  },
565
+ {
566
+ name: "computer",
567
+ description: "Toggle the native computer-use tool for this session",
568
+ acpDescription: "Toggle computer use",
569
+ acpInputHint: "[on|off|status]",
570
+ subcommands: [
571
+ { name: "on", description: "Enable computer use for this session" },
572
+ { name: "off", description: "Disable computer use for this session" },
573
+ { name: "status", description: "Show computer use status" },
574
+ ],
575
+ allowArgs: true,
576
+ getTuiAutocompleteDescription: runtime => `Computer: ${formatComputerUseStatus(runtime.ctx.session)}`,
577
+ handle: async (command, runtime) => {
578
+ const arg = command.args.trim().toLowerCase();
579
+ if (arg === "status") {
580
+ await runtime.output(`Computer use is ${formatComputerUseStatus(runtime.session)}.`);
581
+ return commandConsumed();
582
+ }
583
+ if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
584
+ const enable = arg === "off" ? false : arg === "on" || !runtime.session.settings.get("computer.enabled");
585
+ await runtime.output(await applyComputerUseToggle(runtime.session, enable));
586
+ return commandConsumed();
587
+ }
588
+ return usage("Usage: /computer [on|off|status]", runtime);
589
+ },
590
+ handleTui: async (command, runtime) => {
591
+ const arg = command.args.trim().toLowerCase();
592
+ if (arg === "status") {
593
+ runtime.ctx.showStatus(`Computer use is ${formatComputerUseStatus(runtime.ctx.session)}.`);
594
+ runtime.ctx.editor.setText("");
595
+ return;
596
+ }
597
+ if (!arg || arg === "toggle" || arg === "on" || arg === "off") {
598
+ const enable =
599
+ arg === "off" ? false : arg === "on" || !runtime.ctx.session.settings.get("computer.enabled");
600
+ runtime.ctx.showStatus(await applyComputerUseToggle(runtime.ctx.session, enable));
601
+ runtime.ctx.editor.setText("");
602
+ return;
603
+ }
604
+ runtime.ctx.showStatus("Usage: /computer [on|off|status]");
605
+ runtime.ctx.editor.setText("");
606
+ },
607
+ },
475
608
  {
476
609
  name: "prewalk",
477
610
  description: "Switch to a fast/cheap model at the next action (works even without --prewalk)",
@@ -981,15 +1114,21 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
981
1114
  {
982
1115
  name: "session",
983
1116
  description: "Session management commands",
984
- acpDescription: "Show session information",
985
- acpInputHint: "info|delete",
1117
+ acpDescription: "Show or configure the current session",
1118
+ acpInputHint: "[info|delete|pin [account]]",
986
1119
  subcommands: [
987
1120
  { name: "info", description: "Show session info and stats" },
988
1121
  { name: "delete", description: "Delete current session and return to selector" },
1122
+ {
1123
+ name: "pin",
1124
+ description: "Pin the current provider to a stored OAuth account",
1125
+ usage: "[account]",
1126
+ },
989
1127
  ],
990
1128
  allowArgs: true,
991
1129
  handle: async (command, runtime) => {
992
- if (!command.args || command.args === "info") {
1130
+ const { verb, rest } = parseSubcommand(command.args);
1131
+ if (!verb || (verb === "info" && !rest)) {
993
1132
  await runtime.output(
994
1133
  [
995
1134
  `Session: ${runtime.session.sessionId}`,
@@ -999,7 +1138,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
999
1138
  );
1000
1139
  return commandConsumed();
1001
1140
  }
1002
- if (command.args === "delete") {
1141
+ if (verb === "delete" && !rest) {
1003
1142
  if (runtime.session.isStreaming) return usage("Cannot delete the session while streaming.", runtime);
1004
1143
  const sessionFile = runtime.sessionManager.getSessionFile();
1005
1144
  if (!sessionFile) return usage("No session file to delete (in-memory session).", runtime);
@@ -1018,17 +1157,34 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1018
1157
  );
1019
1158
  return commandConsumed();
1020
1159
  }
1021
- return usage("Usage: /session [info|delete]", runtime);
1160
+ if (verb === "pin") {
1161
+ await handleSessionPinCommand(rest, runtime.session, runtime.output);
1162
+ return commandConsumed();
1163
+ }
1164
+ return usage("Usage: /session [info|delete|pin [account]]", runtime);
1022
1165
  },
1023
1166
  handleTui: async (command, runtime) => {
1024
- const sub = command.args.trim().toLowerCase() || "info";
1025
- if (sub === "delete") {
1167
+ const { verb, rest } = parseSubcommand(command.args);
1168
+ if (verb === "delete" && !rest) {
1026
1169
  runtime.ctx.editor.setText("");
1027
1170
  await runtime.ctx.handleSessionDeleteCommand();
1028
1171
  return;
1029
1172
  }
1030
- // Default: show session info
1031
- await runtime.ctx.handleSessionCommand();
1173
+ if (verb === "pin") {
1174
+ if (rest) {
1175
+ await handleSessionPinCommand(rest, runtime.ctx.session, text => runtime.ctx.showStatus(text));
1176
+ refreshStatusLine(runtime.ctx);
1177
+ } else {
1178
+ await runtime.ctx.showSessionPinSelector();
1179
+ }
1180
+ runtime.ctx.editor.setText("");
1181
+ return;
1182
+ }
1183
+ if (!verb || (verb === "info" && !rest)) {
1184
+ await runtime.ctx.handleSessionCommand();
1185
+ } else {
1186
+ runtime.ctx.showStatus("Usage: /session [info|delete|pin [account]]");
1187
+ }
1032
1188
  runtime.ctx.editor.setText("");
1033
1189
  },
1034
1190
  },
@@ -0,0 +1,44 @@
1
+ import type { OAuthAccountSummary } from "../../session/auth-storage";
2
+ import { formatActiveAccountLabel } from "./active-oauth-account";
3
+
4
+ /** Stored OAuth account rendered and matched by `/session pin`. */
5
+ export interface SessionPinAccount extends OAuthAccountSummary {
6
+ label: string;
7
+ }
8
+
9
+ /** Add stable user-facing labels to provider account summaries. */
10
+ export function toSessionPinAccounts(accounts: readonly OAuthAccountSummary[]): SessionPinAccount[] {
11
+ return accounts.map(account => {
12
+ const enterpriseUrl = account.enterpriseUrl?.trim();
13
+ return {
14
+ ...account,
15
+ label: (formatActiveAccountLabel(account) ?? enterpriseUrl) || `OAuth credential #${account.credentialId}`,
16
+ };
17
+ });
18
+ }
19
+
20
+ /** Match a `/session pin` selector by 1-based position or exact account identity. */
21
+ export function matchSessionPinAccounts(accounts: readonly SessionPinAccount[], selector: string): SessionPinAccount[] {
22
+ const wanted = selector.trim().toLowerCase();
23
+ if (!wanted) return [];
24
+ if (wanted === "active") return accounts.filter(account => account.active);
25
+
26
+ if (/^\d+$/.test(wanted)) {
27
+ const position = Number(wanted) - 1;
28
+ const positioned = accounts.find(account => account.position === position);
29
+ if (positioned) return [positioned];
30
+ }
31
+
32
+ return accounts.filter(account =>
33
+ [
34
+ account.label,
35
+ account.email,
36
+ account.accountId,
37
+ account.projectId,
38
+ account.enterpriseUrl,
39
+ account.orgId,
40
+ account.orgName,
41
+ `OAuth credential #${account.credentialId}`,
42
+ ].some(value => value?.trim().toLowerCase() === wanted),
43
+ );
44
+ }
@@ -4,7 +4,6 @@ import { getTinyModelsCacheDir } from "@oh-my-pi/pi-utils";
4
4
  import { sttClient } from "./asr-client";
5
5
  import type { SttProgressStatus } from "./asr-protocol";
6
6
  import { resolveSttModelSpec } from "./models";
7
- import { ensureRecorder } from "./recorder";
8
7
 
9
8
  export interface DownloadProgress {
10
9
  stage: string;
@@ -129,7 +128,6 @@ export async function downloadSttModel(
129
128
  // ── Public API ─────────────────────────────────────────────────────
130
129
 
131
130
  export async function ensureSTTDependencies(options?: EnsureOptions): Promise<void> {
132
- await ensureRecorder(progress => options?.onProgress?.(progress), options?.signal);
133
131
  await downloadSttModel(
134
132
  resolveSttModelSpec(options?.modelName).key,
135
133
  progress => {
package/src/stt/index.ts CHANGED
@@ -4,5 +4,3 @@ export * from "./downloader";
4
4
  export * from "./models";
5
5
  export * from "./stt-controller";
6
6
  export * from "./submit-trigger";
7
- export * from "./transcriber";
8
- export * from "./wav";
@@ -1,22 +1,10 @@
1
- import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
- import { logger, Snowflake } from "@oh-my-pi/pi-utils";
1
+ import { AudioCapture } from "@oh-my-pi/pi-natives";
2
+ import { logger } from "@oh-my-pi/pi-utils";
5
3
  import { settings } from "../config/settings";
6
4
  import { type SttStreamHandle, sttClient } from "./asr-client";
7
5
  import { downloadSttModel, isSttModelCached } from "./downloader";
8
6
  import { resolveSttModelSpec } from "./models";
9
- import {
10
- detectRecorder,
11
- ensureRecorder,
12
- type RecordingHandle,
13
- type StreamingRecordingHandle,
14
- startRecording,
15
- startStreamingRecording,
16
- verifyRecordingFile,
17
- } from "./recorder";
18
7
  import { evaluateSubmitTrigger } from "./submit-trigger";
19
- import { transcribe } from "./transcriber";
20
8
 
21
9
  export type SttState = "idle" | "recording" | "transcribing";
22
10
 
@@ -38,26 +26,34 @@ interface Editor {
38
26
  deleteBeforeCursor(count: number): void;
39
27
  }
40
28
 
29
+ interface CaptureHandle {
30
+ stop(): void;
31
+ }
32
+
33
+ type CaptureFactory = (onAudio: (error: Error | null, samples: Float32Array) => void) => CaptureHandle;
34
+
35
+ /** Coordinates native microphone capture with incremental local transcription. */
41
36
  export class STTController {
42
37
  #state: SttState = "idle";
43
38
  #resolvedModelKey: string | null = null;
44
39
  #toggling = false;
45
40
  #stopAfterStart = false;
46
41
  #disposed = false;
47
-
48
- // Batch (single-shot) capture.
49
- #recordingHandle: RecordingHandle | null = null;
50
- #tempFile: string | null = null;
51
- #transcriptionAbort: AbortController | null = null;
42
+ readonly #createCapture: CaptureFactory;
52
43
 
53
44
  // Live streaming capture.
54
45
  #stream: SttStreamHandle | null = null;
55
- #streamRecorder: StreamingRecordingHandle | null = null;
46
+ #streamRecorder: CaptureHandle | null = null;
56
47
  #streamEditor: Editor | null = null;
57
48
  #streamCommitted = false;
58
49
  #streamAbort: AbortController | null = null;
59
50
  #streamUtterance = "";
60
51
 
52
+ /** Creates a controller; tests may replace the hardware capture boundary. */
53
+ constructor(createCapture: CaptureFactory = onAudio => new AudioCapture(16_000, onAudio)) {
54
+ this.#createCapture = createCapture;
55
+ }
56
+
61
57
  get state(): SttState {
62
58
  return this.#state;
63
59
  }
@@ -79,7 +75,7 @@ export class STTController {
79
75
  await this.#start(editor, options);
80
76
  break;
81
77
  case "recording":
82
- await this.#stop(editor, options);
78
+ await this.#stop(options);
83
79
  break;
84
80
  case "transcribing":
85
81
  options.showStatus("Transcription in progress...");
@@ -87,7 +83,7 @@ export class STTController {
87
83
  }
88
84
  if (this.#stopAfterStart && this.#state === "recording") {
89
85
  this.#stopAfterStart = false;
90
- await this.#stop(editor, options);
86
+ await this.#stop(options);
91
87
  } else if (this.#state !== "recording") {
92
88
  this.#stopAfterStart = false;
93
89
  }
@@ -103,19 +99,13 @@ export class STTController {
103
99
  // (with progress) instead of blocking silently at stop.
104
100
  if (this.#resolvedModelKey === modelKey) return true;
105
101
  try {
106
- // Only clear the status line if we actually wrote to it: the cached
107
- // fast path (recorder on PATH, model present) emits nothing, so an
108
- // unconditional clear would be a stray write.
102
+ // Only clear the status line when preflight emitted progress; the
103
+ // cached-model fast path emits nothing.
109
104
  let wroteStatus = false;
110
105
  const status = (msg: string): void => {
111
106
  wroteStatus = true;
112
107
  options.showStatus(msg);
113
108
  };
114
- // A recorder is required to capture audio; startRecording /
115
- // startStreamingRecording only *detect* a recorder and throw when none
116
- // exists, so provision one here. Instant when sox/ffmpeg/arecord is on
117
- // PATH — only a first-run static-ffmpeg download actually blocks.
118
- await ensureRecorder(p => status(p.stage + (p.percent != null ? ` (${p.percent}%)` : "")));
119
109
  // Loading the multi-hundred-MB speech model into the worker is what made
120
110
  // the old "Checking STT dependencies…" step slow. Don't pay it before
121
111
  // recording: when the weights are already cached, start now and warm the
@@ -157,30 +147,15 @@ export class STTController {
157
147
 
158
148
  async #start(editor: Editor, options: ToggleOptions): Promise<void> {
159
149
  if (!(await this.#ensureDeps(options))) return;
160
- // Live transcription needs a recorder that can pipe PCM; the Windows
161
- // PowerShell mci fallback records to a file, so it stays single-shot.
162
- if (this.#recorderCanStream()) {
163
- await this.#startStreaming(editor, options);
164
- return;
165
- }
166
- await this.#startBatchRecording(options);
150
+ await this.#startStreaming(editor, options);
167
151
  }
168
152
 
169
- async #stop(editor: Editor, options: ToggleOptions): Promise<void> {
170
- if (this.#stream) {
171
- await this.#stopStreaming(options);
172
- return;
173
- }
174
- await this.#stopBatch(editor, options);
153
+ async #stop(options: ToggleOptions): Promise<void> {
154
+ await this.#stopStreaming(options);
175
155
  }
176
156
 
177
157
  // ── Live streaming ──────────────────────────────────────────────
178
158
 
179
- #recorderCanStream(): boolean {
180
- const recorder = detectRecorder();
181
- return recorder !== null && recorder.tool !== "powershell";
182
- }
183
-
184
159
  /** Segment text gets a leading space once a prior segment is committed, so
185
160
  * phrases join naturally; the first phrase is inserted at the cursor as-is. */
186
161
  #prefixed(text: string): string {
@@ -218,18 +193,38 @@ export class STTController {
218
193
  },
219
194
  });
220
195
  this.#stream = stream;
221
- let recorder: StreamingRecordingHandle | null = null;
196
+ let recorder: CaptureHandle;
222
197
  try {
223
- recorder = await startStreamingRecording(samples => stream.pushAudio(samples));
224
- } catch (err) {
225
- logger.warn("STT streaming recorder failed to start; falling back to batch recording", {
226
- error: err instanceof Error ? err.message : String(err),
198
+ recorder = this.#createCapture((error, samples) => {
199
+ if (this.#disposed || this.#stream !== stream || this.#state !== "recording") return;
200
+ if (error) {
201
+ logger.error("Native microphone capture failed", { error: error.message });
202
+ const activeRecorder = this.#streamRecorder;
203
+ this.#streamRecorder = null;
204
+ try {
205
+ activeRecorder?.stop();
206
+ } catch (cause) {
207
+ logger.debug("stt: microphone cleanup failed", {
208
+ error: cause instanceof Error ? cause.message : String(cause),
209
+ });
210
+ }
211
+ this.#streamAbort?.abort(error);
212
+ stream.cancel();
213
+ this.#streamEditor?.clearVolatileText();
214
+ options.requestRender?.();
215
+ this.#cleanupStream();
216
+ this.#setState("idle", options);
217
+ options.showWarning(error.message);
218
+ return;
219
+ }
220
+ stream.pushAudio(samples);
227
221
  });
228
- }
229
- if (!recorder) {
222
+ } catch (err) {
230
223
  stream.cancel();
231
224
  this.#cleanupStream();
232
- await this.#startBatchRecording(options);
225
+ const msg = err instanceof Error ? err.message : "Failed to start microphone capture";
226
+ options.showWarning(msg);
227
+ logger.error("STT recording failed to start", { error: msg });
233
228
  return;
234
229
  }
235
230
  this.#streamRecorder = recorder;
@@ -247,7 +242,7 @@ export class STTController {
247
242
  this.#setState("transcribing", options);
248
243
  // Stop the mic first so no further audio is fed, then flush the worker.
249
244
  try {
250
- await recorder?.stop();
245
+ recorder?.stop();
251
246
  } catch (err) {
252
247
  logger.debug("stt: streaming recorder stop failed", {
253
248
  error: err instanceof Error ? err.message : String(err),
@@ -306,103 +301,19 @@ export class STTController {
306
301
  this.#streamUtterance = "";
307
302
  }
308
303
 
309
- // ── Batch (single-shot) ─────────────────────────────────────────
310
-
311
- async #startBatchRecording(options: ToggleOptions): Promise<void> {
312
- const id = Snowflake.next();
313
- this.#tempFile = path.join(os.tmpdir(), `omp-stt-${id}.wav`);
314
- try {
315
- this.#recordingHandle = await startRecording(this.#tempFile);
316
- this.#setState("recording", options);
317
- logger.debug("STT recording started", { tempFile: this.#tempFile });
318
- } catch (err) {
319
- this.#tempFile = null;
320
- const msg = err instanceof Error ? err.message : "Failed to start recording";
321
- options.showWarning(msg);
322
- logger.error("STT recording failed to start", { error: msg });
323
- }
324
- }
325
-
326
- async #stopBatch(editor: Editor, options: ToggleOptions): Promise<void> {
327
- const handle = this.#recordingHandle;
328
- const tempFile = this.#tempFile;
329
- this.#recordingHandle = null;
330
-
331
- if (!handle || !tempFile) {
332
- this.#setState("idle", options);
333
- return;
334
- }
335
-
336
- try {
337
- await handle.stop();
338
- // Validate the recording produced a usable file
339
- await verifyRecordingFile(tempFile);
340
- this.#setState("transcribing", options);
341
-
342
- const sttSettings = {
343
- modelName: settings.get("stt.modelName") as string | undefined,
344
- language: settings.get("stt.language") as string | undefined,
345
- };
346
- this.#transcriptionAbort = new AbortController();
347
- const text = await transcribe(tempFile, { ...sttSettings, signal: this.#transcriptionAbort.signal });
348
- this.#transcriptionAbort = null;
349
- if (this.#disposed) return;
350
- if (text.length > 0) {
351
- const trigger = settings.get("stt.submitTrigger");
352
- const { submit, trimTrailing } = evaluateSubmitTrigger(text, trigger);
353
- const textToInsert = trimTrailing > 0 ? text.slice(0, -trimTrailing) : text;
354
- if (textToInsert.length > 0) {
355
- editor.insertText(textToInsert);
356
- }
357
- options.showStatus("");
358
- if (submit) {
359
- editor.submit();
360
- }
361
- } else {
362
- options.showStatus("No speech detected.");
363
- }
364
- if (!this.#disposed) this.#setState("idle", options);
365
- } catch (err) {
366
- if (this.#disposed) return;
367
- if (err instanceof DOMException && err.name === "AbortError") {
368
- this.#setState("idle", options);
369
- return;
370
- }
371
- const msg = err instanceof Error ? err.message : "Transcription failed";
372
- options.showWarning(msg);
373
- logger.error("STT transcription failed", { error: msg });
374
- this.#setState("idle", options);
375
- } finally {
376
- try {
377
- await fs.rm(tempFile, { force: true });
378
- } catch {
379
- // best effort cleanup
380
- }
381
- this.#tempFile = null;
382
- }
383
- }
384
-
385
304
  dispose(): void {
386
305
  this.#disposed = true;
387
- if (this.#transcriptionAbort) {
388
- this.#transcriptionAbort.abort();
389
- this.#transcriptionAbort = null;
390
- }
391
306
  if (this.#streamAbort) {
392
307
  this.#streamAbort.abort();
393
308
  this.#streamAbort = null;
394
309
  }
395
310
  this.#stream?.cancel();
396
- this.#streamRecorder?.stop().catch(() => {});
397
- this.#cleanupStream();
398
- if (this.#recordingHandle) {
399
- this.#recordingHandle.stop().catch(() => {});
400
- this.#recordingHandle = null;
401
- }
402
- if (this.#tempFile) {
403
- fs.rm(this.#tempFile, { force: true }).catch(() => {});
404
- this.#tempFile = null;
311
+ try {
312
+ this.#streamRecorder?.stop();
313
+ } catch {
314
+ // best effort cleanup
405
315
  }
316
+ this.#cleanupStream();
406
317
  this.#state = "idle";
407
318
  this.#resolvedModelKey = null;
408
319
  }
@@ -17,6 +17,7 @@ import { expandAtImports } from "./discovery/at-imports";
17
17
  import { loadSkills, type Skill } from "./extensibility/skills";
18
18
  import { hasObsidian } from "./internal-urls/vault-protocol";
19
19
  import activeRepoContextTemplate from "./prompts/system/active-repo-context.md" with { type: "text" };
20
+ import computerSafetyPrompt from "./prompts/system/computer-safety.md" with { type: "text" };
20
21
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
21
22
  import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
22
23
  import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
@@ -839,6 +840,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
839
840
  };
840
841
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
841
842
  const systemPrompt = [rendered];
843
+ if (toolNames.includes("computer")) {
844
+ systemPrompt.push(computerSafetyPrompt.trim());
845
+ }
842
846
  // Custom prompt templates already render context files and append text; the
843
847
  // project footer still carries environment, cwd, workspace, and dir-context.
844
848
  const projectPrompt = prompt
@@ -46,7 +46,7 @@ import type { AuthStorage } from "../session/auth-storage";
46
46
  import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
47
47
  import { SessionManager } from "../session/session-manager";
48
48
  import { truncateTail } from "../session/streaming-output";
49
- import type { ConfiguredThinkingLevel } from "../thinking";
49
+ import { type ConfiguredThinkingLevel, resolveTaskEffortLevel, type TaskEffort } from "../thinking";
50
50
  import type { ContextFileEntry, ToolSession } from "../tools";
51
51
  import { resolveEvalBackends } from "../tools/eval-backends";
52
52
  import { isIrcEnabled } from "../tools/hub";
@@ -323,6 +323,8 @@ export interface ExecutorOptions {
323
323
  */
324
324
  parentActiveModelPattern?: string;
325
325
  thinkingLevel?: ConfiguredThinkingLevel;
326
+ /** Caller-requested coarse effort (`lo`/`med`/`hi`); maps onto the resolved model's supported thinking range and wins over {@link thinkingLevel}. */
327
+ effort?: TaskEffort;
326
328
  /** Schema used to validate the final structured completion. */
327
329
  outputSchema?: unknown;
328
330
  /** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
@@ -2644,16 +2646,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
2644
2646
  if (model?.contextWindow && model.contextWindow > 0) {
2645
2647
  progress.contextWindow = model.contextWindow;
2646
2648
  }
2649
+ // Caller-requested coarse effort maps onto the resolved model's
2650
+ // supported range; undefined (no effort, or no controllable effort
2651
+ // surface) falls through to the normal selectors below.
2652
+ const effortLevel = options.effort !== undefined ? resolveTaskEffortLevel(model, options.effort) : undefined;
2647
2653
  if (model) {
2648
- progress.resolvedModel = explicitThinkingLevel
2649
- ? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
2650
- : formatModelStringWithRouting(model);
2654
+ const displayLevel = effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : undefined);
2655
+ progress.resolvedModel =
2656
+ displayLevel !== undefined
2657
+ ? formatModelSelectorValue(formatModelStringWithRouting(model), displayLevel)
2658
+ : formatModelStringWithRouting(model);
2651
2659
  }
2652
- // Precedence: explicit `:level` suffix on the resolved model pattern >
2653
- // agent-definition default (e.g. task's `auto`) > pattern-derived level.
2654
- const effectiveThinkingLevel = explicitThinkingLevel
2655
- ? resolvedThinkingLevel
2656
- : (thinkingLevel ?? resolvedThinkingLevel);
2660
+ // Precedence: caller `effort` > explicit `:level` suffix on the resolved
2661
+ // model pattern > agent-definition default (e.g. task's `auto`) >
2662
+ // pattern-derived level.
2663
+ const effectiveThinkingLevel =
2664
+ effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : (thinkingLevel ?? resolvedThinkingLevel));
2657
2665
  resolvedAt = performance.now();
2658
2666
  // Per-agent prewalk: the agent definition's `prewalk` frontmatter or the
2659
2667
  // `task.agentPrewalk` settings override hands the subagent off to a