@oh-my-pi/pi-coding-agent 13.14.0 → 13.15.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 (90) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/package.json +10 -8
  3. package/src/autoresearch/command-initialize.md +34 -0
  4. package/src/autoresearch/command-resume.md +17 -0
  5. package/src/autoresearch/contract.ts +332 -0
  6. package/src/autoresearch/dashboard.ts +447 -0
  7. package/src/autoresearch/git.ts +243 -0
  8. package/src/autoresearch/helpers.ts +458 -0
  9. package/src/autoresearch/index.ts +693 -0
  10. package/src/autoresearch/prompt.md +227 -0
  11. package/src/autoresearch/resume-message.md +16 -0
  12. package/src/autoresearch/state.ts +386 -0
  13. package/src/autoresearch/tools/init-experiment.ts +310 -0
  14. package/src/autoresearch/tools/log-experiment.ts +833 -0
  15. package/src/autoresearch/tools/run-experiment.ts +640 -0
  16. package/src/autoresearch/types.ts +218 -0
  17. package/src/cli/args.ts +8 -2
  18. package/src/cli/initial-message.ts +58 -0
  19. package/src/config/keybindings.ts +417 -212
  20. package/src/config/model-registry.ts +1 -0
  21. package/src/config/model-resolver.ts +57 -9
  22. package/src/config/settings-schema.ts +38 -10
  23. package/src/config/settings.ts +1 -4
  24. package/src/exec/bash-executor.ts +7 -5
  25. package/src/export/html/template.css +43 -13
  26. package/src/export/html/template.generated.ts +1 -1
  27. package/src/export/html/template.html +1 -0
  28. package/src/export/html/template.js +107 -0
  29. package/src/extensibility/extensions/types.ts +31 -8
  30. package/src/internal-urls/docs-index.generated.ts +1 -1
  31. package/src/lsp/index.ts +1 -1
  32. package/src/main.ts +44 -44
  33. package/src/mcp/oauth-discovery.ts +1 -1
  34. package/src/modes/acp/acp-agent.ts +957 -0
  35. package/src/modes/acp/acp-event-mapper.ts +531 -0
  36. package/src/modes/acp/acp-mode.ts +13 -0
  37. package/src/modes/acp/index.ts +2 -0
  38. package/src/modes/components/agent-dashboard.ts +5 -4
  39. package/src/modes/components/bash-execution.ts +40 -11
  40. package/src/modes/components/custom-editor.ts +47 -47
  41. package/src/modes/components/extensions/extension-dashboard.ts +2 -1
  42. package/src/modes/components/history-search.ts +2 -1
  43. package/src/modes/components/hook-editor.ts +2 -1
  44. package/src/modes/components/hook-input.ts +8 -7
  45. package/src/modes/components/hook-selector.ts +15 -10
  46. package/src/modes/components/keybinding-hints.ts +9 -9
  47. package/src/modes/components/login-dialog.ts +3 -3
  48. package/src/modes/components/mcp-add-wizard.ts +2 -1
  49. package/src/modes/components/model-selector.ts +14 -3
  50. package/src/modes/components/oauth-selector.ts +2 -1
  51. package/src/modes/components/python-execution.ts +2 -3
  52. package/src/modes/components/session-selector.ts +2 -1
  53. package/src/modes/components/settings-selector.ts +2 -1
  54. package/src/modes/components/status-line-segment-editor.ts +2 -1
  55. package/src/modes/components/tool-execution.ts +4 -5
  56. package/src/modes/components/tree-selector.ts +3 -2
  57. package/src/modes/components/user-message-selector.ts +3 -8
  58. package/src/modes/components/user-message.ts +16 -0
  59. package/src/modes/controllers/command-controller.ts +0 -2
  60. package/src/modes/controllers/extension-ui-controller.ts +89 -4
  61. package/src/modes/controllers/input-controller.ts +29 -23
  62. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  63. package/src/modes/index.ts +1 -0
  64. package/src/modes/interactive-mode.ts +17 -5
  65. package/src/modes/print-mode.ts +1 -1
  66. package/src/modes/prompt-action-autocomplete.ts +7 -7
  67. package/src/modes/rpc/rpc-mode.ts +7 -2
  68. package/src/modes/rpc/rpc-types.ts +1 -0
  69. package/src/modes/theme/theme.ts +53 -44
  70. package/src/modes/types.ts +9 -2
  71. package/src/modes/utils/hotkeys-markdown.ts +19 -19
  72. package/src/modes/utils/keybinding-matchers.ts +21 -0
  73. package/src/modes/utils/ui-helpers.ts +1 -1
  74. package/src/patch/hashline.ts +139 -127
  75. package/src/patch/index.ts +77 -59
  76. package/src/patch/shared.ts +19 -11
  77. package/src/prompts/tools/hashline.md +43 -116
  78. package/src/sdk.ts +34 -17
  79. package/src/session/agent-session.ts +123 -30
  80. package/src/session/session-manager.ts +32 -31
  81. package/src/session/streaming-output.ts +87 -37
  82. package/src/tools/ask.ts +56 -30
  83. package/src/tools/bash-interactive.ts +2 -6
  84. package/src/tools/bash-interceptor.ts +1 -39
  85. package/src/tools/bash-skill-urls.ts +1 -1
  86. package/src/tools/browser.ts +1 -1
  87. package/src/tools/gemini-image.ts +1 -1
  88. package/src/tools/python.ts +2 -2
  89. package/src/tools/resolve.ts +1 -1
  90. package/src/utils/child-process.ts +88 -0
@@ -141,6 +141,55 @@ function isAlias(id: string): boolean {
141
141
  return !datePattern.test(id);
142
142
  }
143
143
 
144
+ /**
145
+ * Find an exact model reference match.
146
+ * Supports either a bare model id or a canonical provider/modelId reference.
147
+ * When matching by bare id, ambiguous matches across providers are rejected.
148
+ */
149
+ export function findExactModelReferenceMatch(
150
+ modelReference: string,
151
+ availableModels: Model<Api>[],
152
+ ): Model<Api> | undefined {
153
+ const trimmedReference = modelReference.trim();
154
+ if (!trimmedReference) {
155
+ return undefined;
156
+ }
157
+
158
+ const normalizedReference = trimmedReference.toLowerCase();
159
+
160
+ const canonicalMatches = availableModels.filter(
161
+ model => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference,
162
+ );
163
+ if (canonicalMatches.length === 1) {
164
+ return canonicalMatches[0];
165
+ }
166
+ if (canonicalMatches.length > 1) {
167
+ return undefined;
168
+ }
169
+
170
+ const slashIndex = trimmedReference.indexOf("/");
171
+ if (slashIndex !== -1) {
172
+ const provider = trimmedReference.substring(0, slashIndex).trim();
173
+ const modelId = trimmedReference.substring(slashIndex + 1).trim();
174
+ if (provider && modelId) {
175
+ const providerMatches = availableModels.filter(
176
+ model =>
177
+ model.provider.toLowerCase() === provider.toLowerCase() &&
178
+ model.id.toLowerCase() === modelId.toLowerCase(),
179
+ );
180
+ if (providerMatches.length === 1) {
181
+ return providerMatches[0];
182
+ }
183
+ if (providerMatches.length > 1) {
184
+ return undefined;
185
+ }
186
+ }
187
+ }
188
+
189
+ const idMatches = availableModels.filter(model => model.id.toLowerCase() === normalizedReference);
190
+ return idMatches.length === 1 ? idMatches[0] : undefined;
191
+ }
192
+
144
193
  /**
145
194
  * Try to match a pattern to a model from the available models list.
146
195
  * Returns the matched model or undefined if no match found.
@@ -150,17 +199,17 @@ function tryMatchModel(
150
199
  availableModels: Model<Api>[],
151
200
  context: ModelPreferenceContext,
152
201
  ): Model<Api> | undefined {
153
- // Check for provider/modelId format (provider is everything before the first /)
202
+ // Try exact reference match first (handles provider/modelId and bare id with ambiguity rejection)
203
+ const exactRefMatch = findExactModelReferenceMatch(modelPattern, availableModels);
204
+ if (exactRefMatch) {
205
+ return exactRefMatch;
206
+ }
207
+
208
+ // Check for provider/modelId format — fuzzy match within provider
154
209
  const slashIndex = modelPattern.indexOf("/");
155
210
  if (slashIndex !== -1) {
156
211
  const provider = modelPattern.substring(0, slashIndex);
157
212
  const modelId = modelPattern.substring(slashIndex + 1);
158
- const providerMatch = availableModels.find(
159
- m => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
160
- );
161
- if (providerMatch) {
162
- return providerMatch;
163
- }
164
213
 
165
214
  const providerModels = availableModels.filter(m => m.provider.toLowerCase() === provider.toLowerCase());
166
215
  if (providerModels.length > 0) {
@@ -187,10 +236,9 @@ function tryMatchModel(
187
236
  return scored[0]?.model;
188
237
  }
189
238
  }
190
- // No exact provider/model match - fall through to other matching
191
239
  }
192
240
 
193
- // Check for exact ID match (case-insensitive)
241
+ // Exact ID match (case-insensitive) — with ambiguity across providers handled by preference
194
242
  const exactMatches = availableModels.filter(m => m.id.toLowerCase() === modelPattern.toLowerCase());
195
243
  if (exactMatches.length > 0) {
196
244
  return pickPreferredModel(exactMatches, context);
@@ -139,6 +139,43 @@ type SettingDef =
139
139
  // under `as const` while still letting SettingValue infer the correct element type.
140
140
  const EMPTY_STRING_ARRAY: string[] = [];
141
141
  const EMPTY_STRING_RECORD: Record<string, string> = {};
142
+ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
143
+ {
144
+ pattern: "^\\s*(cat|head|tail|less|more)\\s+",
145
+ tool: "read",
146
+ message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
147
+ },
148
+ {
149
+ pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
150
+ tool: "grep",
151
+ message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
152
+ },
153
+ {
154
+ pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
155
+ tool: "find",
156
+ message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
157
+ },
158
+ {
159
+ pattern: "^\\s*sed\\s+(-i|--in-place)",
160
+ tool: "edit",
161
+ message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
162
+ },
163
+ {
164
+ pattern: "^\\s*perl\\s+.*-[pn]?i",
165
+ tool: "edit",
166
+ message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
167
+ },
168
+ {
169
+ pattern: "^\\s*awk\\s+.*-i\\s+inplace",
170
+ tool: "edit",
171
+ message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
172
+ },
173
+ {
174
+ pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
175
+ tool: "write",
176
+ message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
177
+ },
178
+ ];
142
179
 
143
180
  export const SETTINGS_SCHEMA = {
144
181
  // ────────────────────────────────────────────────────────────────────────
@@ -943,16 +980,7 @@ export const SETTINGS_SCHEMA = {
943
980
  default: false,
944
981
  ui: { tab: "editing", label: "Bash Interceptor", description: "Block shell commands that have dedicated tools" },
945
982
  },
946
-
947
- "bashInterceptor.simpleLs": {
948
- type: "boolean",
949
- default: true,
950
- ui: {
951
- tab: "editing",
952
- label: "Intercept `ls`",
953
- description: "Intercept bare ls commands (when interceptor is enabled)",
954
- },
955
- },
983
+ "bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
956
984
 
957
985
  // Python
958
986
  "python.toolMode": {
@@ -341,10 +341,7 @@ export class Settings {
341
341
  * Get bash interceptor rules (typed accessor for complex array config).
342
342
  */
343
343
  getBashInterceptorRules(): BashInterceptorRule[] {
344
- const patterns = (this.#merged.bashInterceptor as { patterns?: unknown[] })?.patterns;
345
- if (!Array.isArray(patterns)) return [];
346
-
347
- return patterns.filter((p): p is BashInterceptorRule => typeof p === "object" && p !== null && "pattern" in p);
344
+ return this.get("bashInterceptor.patterns");
348
345
  }
349
346
 
350
347
  /**
@@ -69,11 +69,16 @@ export async function executeBash(command: string, options?: BashExecutorOptions
69
69
  onChunk: options?.onChunk,
70
70
  artifactPath: options?.artifactPath,
71
71
  artifactId: options?.artifactId,
72
+ // Throttle the streaming preview callback to avoid saturating the
73
+ // event loop when commands produce massive output (e.g. seq 1 50M).
74
+ chunkThrottleMs: options?.onChunk ? 50 : 0,
72
75
  });
73
76
 
74
- let pendingChunks = Promise.resolve();
77
+ // sink.push() is synchronous — buffer management, counters, and onChunk
78
+ // all run inline. File writes (artifact path) are handled asynchronously
79
+ // inside the sink. No promise chain needed.
75
80
  const enqueueChunk = (chunk: string) => {
76
- pendingChunks = pendingChunks.then(() => sink.push(chunk)).catch(() => {});
81
+ sink.push(chunk);
77
82
  };
78
83
 
79
84
  if (options?.signal?.aborted) {
@@ -160,8 +165,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
160
165
  hardTimeoutDeferred.promise.then(() => ({ kind: "hard-timeout" as const })),
161
166
  ]);
162
167
 
163
- await pendingChunks;
164
-
165
168
  if (winner.kind === "hard-timeout") {
166
169
  if (shellSession) {
167
170
  resetSession = true;
@@ -215,7 +218,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
215
218
  if (userSignal) {
216
219
  userSignal.removeEventListener("abort", abortHandler);
217
220
  }
218
- await pendingChunks;
219
221
  if (resetSession) {
220
222
  shellSessions.delete(sessionKey);
221
223
  }
@@ -2,6 +2,10 @@
2
2
 
3
3
  :root {
4
4
  --line-height: 18px; /* 12px font * 1.5 */
5
+ --sidebar-width: 400px;
6
+ --sidebar-min-width: 240px;
7
+ --sidebar-max-width: 840px;
8
+ --sidebar-resizer-width: 6px;
5
9
  }
6
10
 
7
11
  body {
@@ -12,6 +16,11 @@
12
16
  background: var(--body-bg);
13
17
  }
14
18
 
19
+ body.sidebar-resizing {
20
+ cursor: col-resize;
21
+ user-select: none;
22
+ }
23
+
15
24
  #app {
16
25
  display: flex;
17
26
  min-height: 100vh;
@@ -19,7 +28,9 @@
19
28
 
20
29
  /* Sidebar */
21
30
  #sidebar {
22
- width: 400px;
31
+ width: var(--sidebar-width);
32
+ min-width: var(--sidebar-width);
33
+ max-width: var(--sidebar-width);
23
34
  background: var(--container-bg);
24
35
  flex-shrink: 0;
25
36
  display: flex;
@@ -203,8 +214,28 @@
203
214
  flex-shrink: 0;
204
215
  }
205
216
 
217
+ #sidebar-resizer {
218
+ width: var(--sidebar-resizer-width);
219
+ flex-shrink: 0;
220
+ position: sticky;
221
+ top: 0;
222
+ height: 100vh;
223
+ cursor: col-resize;
224
+ touch-action: none;
225
+ background: transparent;
226
+ border-right: 1px solid transparent;
227
+ }
228
+
229
+ #sidebar-resizer:hover,
230
+ body.sidebar-resizing #sidebar-resizer {
231
+ background: var(--selectedBg);
232
+ border-right-color: var(--dim);
233
+ }
234
+
206
235
  /* Main content */
207
236
  #content {
237
+ flex: 1;
238
+ min-width: 0;
208
239
  flex: 1;
209
240
  overflow-y: auto;
210
241
  padding: var(--line-height) calc(var(--line-height) * 2);
@@ -841,17 +872,19 @@
841
872
  @media (max-width: 900px) {
842
873
  #sidebar {
843
874
  position: fixed;
844
- left: -400px;
845
- width: 400px;
875
+ transform: translateX(-100%);
876
+ width: min(var(--sidebar-width), 100vw);
877
+ min-width: 0;
878
+ max-width: 100vw;
846
879
  top: 0;
847
880
  bottom: 0;
848
881
  height: 100vh;
849
882
  z-index: 99;
850
- transition: left 0.3s;
883
+ transition: transform 0.3s;
851
884
  }
852
885
 
853
886
  #sidebar.open {
854
- left: 0;
887
+ transform: translateX(0);
855
888
  }
856
889
 
857
890
  #sidebar-overlay.open {
@@ -866,6 +899,10 @@
866
899
  display: block;
867
900
  }
868
901
 
902
+ #sidebar-resizer {
903
+ display: none;
904
+ }
905
+
869
906
  #content {
870
907
  padding: var(--line-height) 16px;
871
908
  }
@@ -875,15 +912,8 @@
875
912
  }
876
913
  }
877
914
 
878
- @media (max-width: 500px) {
879
- #sidebar {
880
- width: 100vw;
881
- left: -100vw;
882
- }
883
- }
884
-
885
915
  @media print {
886
- #sidebar, #sidebar-toggle { display: none !important; }
916
+ #sidebar, #sidebar-toggle, #sidebar-resizer { display: none !important; }
887
917
  body { background: white; color: black; }
888
918
  #content { max-width: none; }
889
919
  }