@oh-my-pi/pi-coding-agent 17.1.0 → 17.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli.js +3686 -4013
  3. package/dist/types/config/settings-schema.d.ts +58 -0
  4. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  5. package/dist/types/live/attestation.d.ts +2 -0
  6. package/dist/types/live/controller.d.ts +10 -2
  7. package/dist/types/live/protocol.d.ts +1 -1
  8. package/dist/types/live/transport.d.ts +6 -19
  9. package/dist/types/live/visualizer.d.ts +8 -11
  10. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  11. package/dist/types/modes/components/custom-message.d.ts +1 -1
  12. package/dist/types/modes/components/message-frame.d.ts +8 -4
  13. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  14. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  15. package/dist/types/modes/interactive-mode.d.ts +1 -0
  16. package/dist/types/modes/types.d.ts +1 -0
  17. package/dist/types/session/agent-session-types.d.ts +8 -1
  18. package/dist/types/session/agent-session.d.ts +20 -1
  19. package/dist/types/session/auth-storage.d.ts +1 -1
  20. package/dist/types/session/eval-runner.d.ts +2 -0
  21. package/dist/types/session/messages.d.ts +2 -0
  22. package/dist/types/session/session-tools.d.ts +15 -0
  23. package/dist/types/session/streaming-output.d.ts +8 -0
  24. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  25. package/dist/types/stt/index.d.ts +0 -2
  26. package/dist/types/stt/stt-controller.d.ts +7 -0
  27. package/dist/types/tiny/title-client.d.ts +10 -0
  28. package/dist/types/tools/builtin-names.d.ts +1 -1
  29. package/dist/types/tools/computer/protocol.d.ts +43 -0
  30. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  31. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  32. package/dist/types/tools/computer/worker.d.ts +15 -0
  33. package/dist/types/tools/computer-renderer.d.ts +22 -0
  34. package/dist/types/tools/computer.d.ts +71 -0
  35. package/dist/types/tools/context.d.ts +2 -0
  36. package/dist/types/tools/default-renderer.d.ts +21 -0
  37. package/dist/types/tools/index.d.ts +2 -0
  38. package/dist/types/tts/streaming-player.d.ts +10 -43
  39. package/dist/types/utils/tools-manager.d.ts +1 -2
  40. package/package.json +12 -12
  41. package/src/cli/args.ts +1 -0
  42. package/src/cli/setup-cli.ts +2 -14
  43. package/src/cli.ts +8 -0
  44. package/src/config/model-registry.ts +6 -0
  45. package/src/config/settings-schema.ts +61 -0
  46. package/src/eval/executor-base.ts +1 -0
  47. package/src/eval/js/executor.ts +2 -0
  48. package/src/exec/bash-executor.ts +1 -0
  49. package/src/extensibility/extensions/wrapper.ts +68 -12
  50. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  51. package/src/live/attestation.ts +91 -0
  52. package/src/live/controller.ts +76 -23
  53. package/src/live/protocol.test.ts +3 -3
  54. package/src/live/protocol.ts +1 -1
  55. package/src/live/transport.ts +72 -140
  56. package/src/live/visualizer.ts +114 -134
  57. package/src/modes/components/assistant-message.ts +7 -2
  58. package/src/modes/components/custom-message.ts +4 -1
  59. package/src/modes/components/message-frame.ts +14 -8
  60. package/src/modes/components/session-account-selector.ts +62 -0
  61. package/src/modes/components/tool-execution.ts +17 -110
  62. package/src/modes/controllers/input-controller.ts +47 -39
  63. package/src/modes/controllers/live-command-controller.ts +82 -5
  64. package/src/modes/controllers/selector-controller.ts +62 -0
  65. package/src/modes/interactive-mode.ts +19 -0
  66. package/src/modes/types.ts +1 -0
  67. package/src/prompts/system/computer-safety.md +14 -0
  68. package/src/prompts/tools/computer.md +26 -0
  69. package/src/sdk.ts +5 -0
  70. package/src/session/agent-session-types.ts +9 -0
  71. package/src/session/agent-session.ts +56 -0
  72. package/src/session/auth-storage.ts +1 -0
  73. package/src/session/eval-runner.ts +5 -0
  74. package/src/session/messages.ts +3 -0
  75. package/src/session/session-tools.ts +37 -0
  76. package/src/session/streaming-output.ts +52 -5
  77. package/src/slash-commands/builtin-registry.ts +165 -9
  78. package/src/slash-commands/helpers/session-pin.ts +44 -0
  79. package/src/stt/downloader.ts +0 -2
  80. package/src/stt/index.ts +0 -2
  81. package/src/stt/stt-controller.ts +57 -146
  82. package/src/system-prompt.ts +4 -0
  83. package/src/tiny/title-client.ts +22 -0
  84. package/src/tools/bash-interactive.ts +90 -86
  85. package/src/tools/builtin-names.ts +1 -0
  86. package/src/tools/computer/protocol.ts +28 -0
  87. package/src/tools/computer/supervisor.ts +258 -0
  88. package/src/tools/computer/worker-entry.ts +25 -0
  89. package/src/tools/computer/worker.ts +135 -0
  90. package/src/tools/computer-renderer.ts +108 -0
  91. package/src/tools/computer.ts +433 -0
  92. package/src/tools/context.ts +2 -0
  93. package/src/tools/default-renderer.ts +139 -0
  94. package/src/tools/essential-tools.ts +1 -0
  95. package/src/tools/index.ts +5 -0
  96. package/src/tools/renderers.ts +2 -0
  97. package/src/tools/xdev.ts +54 -26
  98. package/src/tts/streaming-player.ts +81 -340
  99. package/src/utils/clipboard.ts +1 -30
  100. package/src/utils/mac-file-urls.applescript +37 -0
  101. package/src/utils/tool-choice.ts +14 -0
  102. package/src/utils/tools-manager.ts +1 -19
  103. package/dist/types/stt/recorder.d.ts +0 -30
  104. package/dist/types/stt/transcriber.d.ts +0 -14
  105. package/dist/types/stt/wav.d.ts +0 -29
  106. package/dist/types/tts/player.d.ts +0 -32
  107. package/src/live/audio-worklet.txt +0 -59
  108. package/src/live/browser-runtime.txt +0 -221
  109. package/src/stt/recorder.ts +0 -551
  110. package/src/stt/transcriber.ts +0 -60
  111. package/src/stt/wav.ts +0 -173
  112. package/src/tts/player.ts +0 -137
@@ -60,6 +60,7 @@ interface SessionToolsOptions {
60
60
  autoApprove?: boolean;
61
61
  toolRegistry?: Map<string, AgentTool>;
62
62
  createVibeTools?: () => AgentTool[];
63
+ createComputerTool?: () => Promise<AgentTool | null>;
63
64
  builtInToolNames?: Iterable<string>;
64
65
  presentationPinnedToolNames?: ReadonlySet<string>;
65
66
  ensureWriteRegistered?: () => Promise<boolean>;
@@ -84,6 +85,7 @@ export class SessionTools {
84
85
  #autoApprove: boolean;
85
86
  #toolRegistry: Map<string, AgentTool>;
86
87
  #createVibeTools: (() => AgentTool[]) | undefined;
88
+ #createComputerTool: SessionToolsOptions["createComputerTool"];
87
89
  #installedVibeToolNames = new Set<string>();
88
90
  #builtInToolNames: Set<string>;
89
91
  #rpcHostToolNames = new Set<string>();
@@ -111,6 +113,7 @@ export class SessionTools {
111
113
  this.#autoApprove = options.autoApprove === true;
112
114
  this.#toolRegistry = options.toolRegistry ?? new Map();
113
115
  this.#createVibeTools = options.createVibeTools;
116
+ this.#createComputerTool = options.createComputerTool;
114
117
  this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
115
118
  this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
116
119
  this.#ensureWriteRegistered = options.ensureWriteRegistered;
@@ -696,6 +699,40 @@ export class SessionTools {
696
699
  await this.applyActiveToolsByName([...new Set(nextActive)]);
697
700
  }
698
701
 
702
+ /**
703
+ * Session-scoped enable/disable for the settings-gated `computer` tool.
704
+ *
705
+ * `createTools` derives the built-in slate once at session start, so a runtime
706
+ * `computer.enabled` override alone never changes the active tools. Enabling
707
+ * builds the tool through the config factory on first use (later toggles reuse
708
+ * the registry entry, so only one desktop controller is ever registered) and
709
+ * activates it; disabling drops it from the active set while keeping the
710
+ * registry entry. Takes effect before the next model call.
711
+ *
712
+ * @returns false when enabling was requested but this session cannot build the
713
+ * tool (e.g. restricted child sessions have no factory).
714
+ */
715
+ async setComputerToolEnabled(enabled: boolean): Promise<boolean> {
716
+ const active = this.getEnabledToolNames();
717
+ if (!enabled) {
718
+ if (active.includes("computer")) {
719
+ await this.applyActiveToolsByName(active.filter(name => name !== "computer"));
720
+ }
721
+ return true;
722
+ }
723
+ if (!this.#toolRegistry.has("computer")) {
724
+ const tool = await this.#createComputerTool?.();
725
+ if (tool?.name !== "computer") return false;
726
+ const wrapped = this.#wrapRuntimeTool(tool);
727
+ this.#toolRegistry.set(wrapped.name, wrapped);
728
+ this.#builtInToolNames.add(wrapped.name);
729
+ }
730
+ if (!active.includes("computer")) {
731
+ await this.applyActiveToolsByName([...active, "computer"]);
732
+ }
733
+ return true;
734
+ }
735
+
699
736
  /** Rebuilds the stable base prompt for the current tools and model. */
700
737
  async refreshBaseSystemPrompt(): Promise<void> {
701
738
  if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
@@ -756,6 +756,10 @@ export class OutputSink {
756
756
  // Queue of chunks waiting for the file sink to be created.
757
757
  #pendingFileWrites?: string[];
758
758
  #fileReady = false;
759
+ /** In-flight sink creation, awaited by finalize/dispose so a fd opened by a late chunk is still released. */
760
+ #fileCreation?: Promise<void>;
761
+ /** Set once the spill file has been closed; guards double-close and post-finalize resurrection. */
762
+ #finalized = false;
759
763
 
760
764
  readonly #artifactPath?: string;
761
765
  readonly #artifactId?: string;
@@ -842,6 +846,7 @@ export class OutputSink {
842
846
  * synchronously. File sink writes are deferred and serialized internally.
843
847
  */
844
848
  push(chunk: string): void {
849
+ if (this.#finalized) return;
845
850
  chunk = sanitizeWithOptionalSixelPassthrough(chunk, text => sanitizeText(this.#normalizeCarriageReturns(text)));
846
851
 
847
852
  // Throttled onChunk: coalesce chunks arriving inside the throttle window.
@@ -1018,7 +1023,7 @@ export class OutputSink {
1018
1023
  // resolves (typically <2). The cap is enforced on drain.
1019
1024
  if (!this.#pendingFileWrites) {
1020
1025
  this.#pendingFileWrites = [chunk];
1021
- void this.#createFileSink();
1026
+ this.#fileCreation = this.#createFileSink();
1022
1027
  } else {
1023
1028
  this.#pendingFileWrites.push(chunk);
1024
1029
  }
@@ -1255,10 +1260,7 @@ export class OutputSink {
1255
1260
  this.#flushPendingChunk();
1256
1261
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1257
1262
 
1258
- if (this.#file) {
1259
- this.#flushArtifactTailIfCapped();
1260
- await this.#file.sink.end();
1261
- }
1263
+ await this.#finalizeFile();
1262
1264
 
1263
1265
  // Compose the visible output. With head retention, splice head + marker
1264
1266
  // + tail when content was elided. Otherwise return the rolling buffer.
@@ -1321,6 +1323,51 @@ export class OutputSink {
1321
1323
  artifactId: this.#file?.artifactId,
1322
1324
  };
1323
1325
  }
1326
+
1327
+ /**
1328
+ * Flush any capped artifact tail and close the spill file descriptor,
1329
+ * awaiting an in-flight sink creation so a descriptor opened by a late
1330
+ * chunk is still released. Idempotent via {@link #finalized}: the artifact
1331
+ * is finalized exactly once whether the caller reached {@link dump} or
1332
+ * bailed through {@link dispose}. `#file` is left set so {@link dump} can
1333
+ * still read `artifactId` for its summary.
1334
+ */
1335
+ async #finalizeFile(): Promise<void> {
1336
+ if (this.#finalized) return;
1337
+ this.#finalized = true;
1338
+ if (this.#fileCreation) {
1339
+ await this.#fileCreation.catch(() => undefined);
1340
+ }
1341
+ const file = this.#file;
1342
+ if (!file) return;
1343
+ // The tail/notice replay writes to the sink and can throw (e.g. a disk
1344
+ // write error). Closing the descriptor MUST still happen — otherwise the
1345
+ // fd leaks and the replay error masks the original tool error that put us
1346
+ // on this path. Both failures are swallowed so dispose() never throws.
1347
+ try {
1348
+ this.#flushArtifactTailIfCapped();
1349
+ } catch {
1350
+ /* ignore */
1351
+ } finally {
1352
+ try {
1353
+ await file.sink.end();
1354
+ } catch {
1355
+ /* ignore */
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ /**
1361
+ * Release the artifact spill descriptor on an exit path that skips
1362
+ * {@link dump} — a thrown error or abort. Idempotent and safe in a
1363
+ * `finally`: if {@link dump} already ran this is a no-op, otherwise it
1364
+ * flushes the capped tail and closes the sink so the descriptor is not
1365
+ * leaked until a later unrelated read hits `EMFILE` (issue #6463).
1366
+ */
1367
+ async dispose(): Promise<void> {
1368
+ this.#clearPendingChunkTimer();
1369
+ await this.#finalizeFile();
1370
+ }
1324
1371
  }
1325
1372
 
1326
1373
  // =============================================================================
@@ -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";