@oh-my-pi/pi-coding-agent 16.2.11 → 16.2.13

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 (89) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/cli.js +2982 -2825
  3. package/dist/types/cli/bench-cli.d.ts +0 -5
  4. package/dist/types/cli/dry-balance-cli.d.ts +0 -5
  5. package/dist/types/cli/models-cli.d.ts +1 -1
  6. package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
  7. package/dist/types/config/model-registry.d.ts +1 -23
  8. package/dist/types/config/model-resolver.d.ts +10 -15
  9. package/dist/types/config/models-config-schema.d.ts +12 -6
  10. package/dist/types/config/models-config.d.ts +9 -6
  11. package/dist/types/config/settings-schema.d.ts +31 -1
  12. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +15 -0
  14. package/dist/types/lsp/client.d.ts +9 -3
  15. package/dist/types/modes/controllers/tool-args-reveal.d.ts +0 -5
  16. package/dist/types/task/spawn-policy.d.ts +17 -0
  17. package/dist/types/task/spawn-policy.test.d.ts +1 -0
  18. package/dist/types/task/types.d.ts +8 -2
  19. package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
  20. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
  21. package/dist/types/tools/browser/registry.d.ts +2 -0
  22. package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
  23. package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
  24. package/dist/types/tools/eval.d.ts +3 -6
  25. package/dist/types/tools/write.d.ts +1 -1
  26. package/dist/types/utils/image-vision-fallback.d.ts +1 -1
  27. package/package.json +12 -12
  28. package/src/advisor/__tests__/advisor.test.ts +1 -1
  29. package/src/auto-thinking/classifier.ts +1 -1
  30. package/src/cli/bench-cli.ts +3 -12
  31. package/src/cli/dry-balance-cli.ts +1 -6
  32. package/src/cli/models-cli.ts +3 -81
  33. package/src/commands/models.ts +1 -2
  34. package/src/commit/model-selection.ts +4 -4
  35. package/src/config/inline-tool-descriptors-mode.ts +1 -2
  36. package/src/config/model-discovery.ts +32 -6
  37. package/src/config/model-registry.ts +11 -225
  38. package/src/config/model-resolver.ts +23 -135
  39. package/src/config/models-config-schema.ts +13 -22
  40. package/src/config/prompt-templates.ts +20 -0
  41. package/src/config/settings-schema.ts +35 -1
  42. package/src/config/settings.ts +60 -24
  43. package/src/eval/__tests__/agent-bridge.test.ts +17 -3
  44. package/src/eval/agent-bridge.ts +7 -9
  45. package/src/eval/completion-bridge.ts +1 -1
  46. package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
  47. package/src/eval/py/runner.py +169 -18
  48. package/src/extensibility/extensions/model-api.ts +1 -3
  49. package/src/extensibility/extensions/runner.ts +62 -5
  50. package/src/lsp/client.ts +162 -45
  51. package/src/lsp/index.ts +31 -21
  52. package/src/main.ts +0 -1
  53. package/src/mcp/transports/http.ts +19 -17
  54. package/src/mcp/transports/stdio.test.ts +51 -1
  55. package/src/mcp/transports/stdio.ts +19 -9
  56. package/src/memories/index.ts +0 -1
  57. package/src/mnemopi/backend.ts +1 -1
  58. package/src/modes/components/model-selector.ts +32 -186
  59. package/src/modes/controllers/event-controller.ts +8 -39
  60. package/src/modes/controllers/selector-controller.ts +0 -1
  61. package/src/modes/controllers/tool-args-reveal.ts +0 -12
  62. package/src/prompts/system/subagent-system-prompt.md +2 -2
  63. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  64. package/src/prompts/tools/eval.md +2 -2
  65. package/src/prompts/tools/ssh.md +1 -0
  66. package/src/prompts/tools/task.md +1 -1
  67. package/src/sdk.ts +2 -9
  68. package/src/session/agent-session.ts +89 -8
  69. package/src/session/session-context.ts +2 -1
  70. package/src/session/session-manager.ts +2 -1
  71. package/src/session/unexpected-stop-classifier.ts +1 -1
  72. package/src/task/index.ts +23 -27
  73. package/src/task/spawn-policy.test.ts +63 -0
  74. package/src/task/spawn-policy.ts +58 -0
  75. package/src/task/types.ts +77 -6
  76. package/src/tools/__tests__/eval-description.test.ts +19 -0
  77. package/src/tools/browser/cmux/cmux-tab.ts +66 -24
  78. package/src/tools/browser/registry.ts +25 -0
  79. package/src/tools/browser/run-cancellation.ts +47 -0
  80. package/src/tools/browser/tab-supervisor.ts +120 -7
  81. package/src/tools/browser/tab-worker.ts +22 -10
  82. package/src/tools/browser.ts +1 -0
  83. package/src/tools/eval.ts +15 -10
  84. package/src/tools/inspect-image.ts +1 -1
  85. package/src/tools/ssh.ts +8 -1
  86. package/src/tools/write.ts +49 -9
  87. package/src/utils/commit-message-generator.ts +0 -1
  88. package/src/utils/image-vision-fallback.ts +2 -3
  89. package/src/utils/title-generator.ts +1 -1
@@ -152,7 +152,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
152
152
  const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
153
153
  if (!pattern) return undefined;
154
154
  const expanded = expandRoleAlias(pattern, this.session.settings);
155
- return resolveModelFromString(expanded, availableModels, matchPreferences, modelRegistry);
155
+ return resolveModelFromString(expanded, availableModels, matchPreferences);
156
156
  };
157
157
 
158
158
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
package/src/tools/ssh.ts CHANGED
@@ -26,7 +26,7 @@ import { clampTimeout } from "./tool-timeouts";
26
26
  const sshSchema = type({
27
27
  host: type("string").describe("ssh host"),
28
28
  command: type("string").describe("remote command"),
29
- "cwd?": type("string").describe("remote working directory"),
29
+ "cwd?": type("string").describe("remote working directory; omit unless required, never ~ or ~/..."),
30
30
  "timeout?": type("number").describe("timeout in seconds"),
31
31
  });
32
32
 
@@ -89,6 +89,12 @@ function quoteCmdPath(value: string): string {
89
89
  const escaped = value.replace(/"/g, '""');
90
90
  return `"${escaped}"`;
91
91
  }
92
+ function assertValidSshCwd(cwd: string | undefined): void {
93
+ if (!cwd) return;
94
+ if (cwd === "~" || cwd.startsWith("~/")) {
95
+ throw new ToolError("SSH cwd must be an absolute remote path; omit cwd instead of using ~.");
96
+ }
97
+ }
92
98
 
93
99
  function buildRemoteCommand(command: string, cwd: string | undefined, info: SSHHostInfo): string {
94
100
  if (!cwd) return command;
@@ -177,6 +183,7 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
177
183
  if (!hostConfig) {
178
184
  throw new ToolError(`SSH host not loaded: ${host}`);
179
185
  }
186
+ assertValidSshCwd(cwd);
180
187
 
181
188
  const hostInfo = await ensureHostInfo(hostConfig);
182
189
  const remoteCommand = buildRemoteCommand(command, cwd, hostInfo);
@@ -52,6 +52,7 @@ import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from
52
52
  import {
53
53
  cachedRenderedString,
54
54
  createRenderedStringCache,
55
+ Ellipsis,
55
56
  formatDiagnostics,
56
57
  formatErrorDetail,
57
58
  formatExpandHint,
@@ -61,6 +62,8 @@ import {
61
62
  type RenderedStringCache,
62
63
  replaceTabs,
63
64
  shortenPath,
65
+ TRUNCATE_LENGTHS,
66
+ truncateToWidth,
64
67
  } from "./render-utils";
65
68
  import {
66
69
  deleteRowByKey,
@@ -165,6 +168,18 @@ function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: str
165
168
  }
166
169
  }
167
170
 
171
+ function emitWriteProgress(
172
+ onUpdate: AgentToolUpdateCallback<WriteToolDetails> | undefined,
173
+ content: string,
174
+ displayPath: string,
175
+ resolvedPath?: string,
176
+ ): void {
177
+ onUpdate?.({
178
+ content: [{ type: "text", text: `Writing ${content.length} bytes to ${shortenPath(displayPath)}...` }],
179
+ details: resolvedPath ? { resolvedPath } : {},
180
+ });
181
+ }
182
+
168
183
  /**
169
184
  * If `content` begins with a `#!` shebang, ensure the file is executable.
170
185
  *
@@ -781,7 +796,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
781
796
  _toolCallId: string,
782
797
  { path: rawPath, content }: WriteParams,
783
798
  signal?: AbortSignal,
784
- _onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
799
+ onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
785
800
  context?: AgentToolContext,
786
801
  ): Promise<AgentToolResult<WriteToolDetails>> {
787
802
  // Strip a hashline `[path#TAG]` wrapper up front so every downstream
@@ -807,6 +822,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
807
822
  // Handler-owned writes (vault:// notes, host URIs) mutate user
808
823
  // data outside the local sandbox — plan mode must reject them.
809
824
  enforcePlanModeWrite(this.session, path, { op: "update" });
825
+ emitWriteProgress(onUpdate, cleanContent, path);
810
826
  await handler.write(parsed, cleanContent, { cwd: this.session.cwd, signal });
811
827
  let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
812
828
  if (stripped) {
@@ -826,6 +842,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
826
842
  `Conflict URI scope '/${conflictUri.scope}' is read-only — read \`conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`,
827
843
  );
828
844
  }
845
+ emitWriteProgress(onUpdate, cleanContent, path);
829
846
  const result =
830
847
  conflictUri.id === "*"
831
848
  ? await this.#resolveAllConflicts(cleanContent, stripped, signal)
@@ -844,6 +861,14 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
844
861
  op: resolvedArchivePath.exists ? "update" : "create",
845
862
  });
846
863
 
864
+ emitWriteProgress(
865
+ onUpdate,
866
+ cleanContent,
867
+ `${formatPathRelativeToCwd(resolvedArchivePath.absolutePath, this.session.cwd)}:${
868
+ resolvedArchivePath.archiveSubPath
869
+ }`,
870
+ resolvedArchivePath.absolutePath,
871
+ );
847
872
  const archiveResult = await this.#writeArchiveEntry(cleanContent, resolvedArchivePath);
848
873
  if (stripped) {
849
874
  const firstText = archiveResult.content.find(
@@ -861,6 +886,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
861
886
  if (resolvedSqlitePath) {
862
887
  enforcePlanModeWrite(this.session, resolvedSqlitePath.sqlitePath, { op: "update" });
863
888
 
889
+ emitWriteProgress(onUpdate, cleanContent, path, resolvedSqlitePath.absolutePath);
864
890
  const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath);
865
891
  if (stripped) {
866
892
  const firstText = sqliteResult.content.find(
@@ -883,11 +909,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
883
909
  await assertEditableFile(absolutePath, path);
884
910
  }
885
911
 
912
+ const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
913
+ emitWriteProgress(onUpdate, cleanContent, displayPath, absolutePath);
914
+
886
915
  // Try ACP bridge first for editor-visible filesystem paths. Internal
887
916
  // artifacts such as local:// plans are owned by OMP, not the editor.
888
917
  if (await routeWriteThroughBridge(this.session, path, absolutePath, cleanContent)) {
889
918
  const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
890
- const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
891
919
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
892
920
  const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
893
921
  let resultText = header ? `${header}\n${writeLine}` : writeLine;
@@ -908,7 +936,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
908
936
  this.session.bumpFileMutationVersion?.(absolutePath);
909
937
  const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
910
938
 
911
- const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
912
939
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
913
940
  const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
914
941
  let resultText = header ? `${header}\n${writeLine}` : writeLine;
@@ -1128,14 +1155,19 @@ export const writeToolRenderer = {
1128
1155
  }));
1129
1156
  }
1130
1157
 
1158
+ const isPartial = options.isPartial === true;
1159
+ const progressText = result.content?.find(c => c.type === "text")?.text ?? "";
1131
1160
  const lineCount = countLines(fileContent);
1132
1161
  const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
1133
- const execSuffix = result.details?.madeExecutable
1134
- ? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
1135
- : "";
1162
+ const execSuffix =
1163
+ !isPartial && result.details?.madeExecutable
1164
+ ? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
1165
+ : "";
1136
1166
  const header = renderStatusLine(
1137
1167
  {
1138
- iconOverride: uiTheme.styledSymbol("tool.write", "accent"),
1168
+ icon: isPartial ? "running" : undefined,
1169
+ iconOverride: isPartial ? undefined : uiTheme.styledSymbol("tool.write", "accent"),
1170
+ spinnerFrame: options.spinnerFrame,
1139
1171
  title: "Write",
1140
1172
  description: `${langIcon} ${pathDisplay}${lineSuffix}${execSuffix}`,
1141
1173
  },
@@ -1147,7 +1179,15 @@ export const writeToolRenderer = {
1147
1179
  return framedBlock(uiTheme, width => {
1148
1180
  const { expanded } = options;
1149
1181
  let body = renderContentPreview(fileContent, expanded, lang, uiTheme, previewCache);
1150
- if (diagnostics) {
1182
+ if (isPartial && progressText) {
1183
+ const safeProgressText = truncateToWidth(
1184
+ replaceTabs(progressText),
1185
+ TRUNCATE_LENGTHS.LINE,
1186
+ Ellipsis.Unicode,
1187
+ );
1188
+ body = `${uiTheme.fg("muted", safeProgressText)}${body ? `\n${body}` : ""}`;
1189
+ }
1190
+ if (!isPartial && diagnostics) {
1151
1191
  const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
1152
1192
  uiTheme.getLangIcon(getLanguageFromPath(fp)),
1153
1193
  );
@@ -1162,7 +1202,7 @@ export const writeToolRenderer = {
1162
1202
  return {
1163
1203
  header,
1164
1204
  sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
1165
- state: "success",
1205
+ state: isPartial ? "pending" : "success",
1166
1206
  borderColor: "borderMuted",
1167
1207
  width,
1168
1208
  };
@@ -55,7 +55,6 @@ function getSmolModelCandidates(
55
55
  const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
56
56
  settings,
57
57
  matchPreferences,
58
- modelRegistry: registry,
59
58
  });
60
59
  addCandidate(configuredSmol.model, configuredSmol.thinkingLevel);
61
60
 
@@ -39,8 +39,7 @@ const DESCRIPTION_UNAVAILABLE_NOTE =
39
39
  "[Image description unavailable: the vision model returned no usable text. The image was saved for further analysis.]";
40
40
 
41
41
  /** Registry surface needed to resolve a vision model and authorize requests. */
42
- export type VisionFallbackRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey" | "resolver"> &
43
- Partial<Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">>;
42
+ export type VisionFallbackRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey" | "resolver">;
44
43
 
45
44
  export interface DescribeAttachedImagesDeps {
46
45
  /** Active (text-only) model the prompt is destined for. */
@@ -108,7 +107,7 @@ function resolveVisionModel(deps: DescribeAttachedImagesDeps): Model<Api> | unde
108
107
  const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
109
108
  if (!pattern) return undefined;
110
109
  const expanded = expandRoleAlias(pattern, deps.settings);
111
- const model = resolveModelFromString(expanded, available, preferences, deps.modelRegistry);
110
+ const model = resolveModelFromString(expanded, available, preferences);
112
111
  return model?.input.includes("image") ? model : undefined;
113
112
  };
114
113
  return (
@@ -72,7 +72,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
72
72
  const availableModels = registry.getAvailable();
73
73
  if (availableModels.length === 0) return undefined;
74
74
 
75
- const titleModel = resolveRoleSelection(["tiny", "commit", "smol"], settings, availableModels, registry)?.model;
75
+ const titleModel = resolveRoleSelection(["tiny", "commit", "smol"], settings, availableModels)?.model;
76
76
  if (titleModel) return titleModel;
77
77
 
78
78
  if (currentModel) return currentModel;