@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/dist/cli.js +3855 -3798
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +24 -0
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/utils/git.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli.ts +27 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +21 -0
- package/src/cursor.ts +1 -1
- package/src/dap/client.ts +134 -36
- package/src/discovery/helpers.ts +8 -0
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/irc/bus.ts +5 -4
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +119 -20
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +23 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +329 -79
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-history-format.ts +6 -2
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +46 -9
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +39 -37
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +23 -6
- package/src/tools/renderers.ts +24 -0
- package/src/tools/ssh.ts +25 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/git.ts +8 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
package/src/tiny/text.ts
CHANGED
|
@@ -152,6 +152,34 @@ const FILLER_TITLE_TOKENS = new Set<string>([
|
|
|
152
152
|
]);
|
|
153
153
|
|
|
154
154
|
const TITLE_WORD = /[\p{L}\p{N}]+/gu;
|
|
155
|
+
const COMMON_TITLE_ACRONYMS = new Set<string>([
|
|
156
|
+
"API",
|
|
157
|
+
"CLI",
|
|
158
|
+
"CPU",
|
|
159
|
+
"CRUD",
|
|
160
|
+
"CSS",
|
|
161
|
+
"DNS",
|
|
162
|
+
"ETL",
|
|
163
|
+
"GPU",
|
|
164
|
+
"HTML",
|
|
165
|
+
"HTTP",
|
|
166
|
+
"HTTPS",
|
|
167
|
+
"ID",
|
|
168
|
+
"JSON",
|
|
169
|
+
"LLM",
|
|
170
|
+
"REST",
|
|
171
|
+
"SDK",
|
|
172
|
+
"SSH",
|
|
173
|
+
"TCP",
|
|
174
|
+
"TLS",
|
|
175
|
+
"TUI",
|
|
176
|
+
"UI",
|
|
177
|
+
"URI",
|
|
178
|
+
"URL",
|
|
179
|
+
"UX",
|
|
180
|
+
"XML",
|
|
181
|
+
"YAML",
|
|
182
|
+
]);
|
|
155
183
|
|
|
156
184
|
/**
|
|
157
185
|
* True when a first user message is too low-signal to title (greeting, ack,
|
|
@@ -191,51 +219,115 @@ export function normalizeGeneratedTitle(value: string | null | undefined, source
|
|
|
191
219
|
* Reconcile a generated title's casing against the user's own message.
|
|
192
220
|
*
|
|
193
221
|
* The title prompt asks for sentence case, but small title models still mangle
|
|
194
|
-
* casing
|
|
195
|
-
* (`daemon` → `dAemon`)
|
|
196
|
-
* (`TinyVMM` → `tinyvmm`)
|
|
197
|
-
*
|
|
222
|
+
* casing three ways: they sprout stray interior capitals on ordinary words
|
|
223
|
+
* (`daemon` → `dAemon`), they flatten proper nouns the user cased distinctively
|
|
224
|
+
* (`TinyVMM` → `tinyvmm`), and they title-case ALL-CAPS acronyms as if they
|
|
225
|
+
* were sentence words (`CNPG` → `Cnpg`). The user's message is the source of
|
|
226
|
+
* truth, so per title token:
|
|
198
227
|
* 1. typed verbatim in the message → keep it (the user established the casing);
|
|
199
228
|
* 2. else the message has the same word with *distinctive* mixed casing
|
|
200
229
|
* (`TinyVMM`, `iOS`, `IDs`) → adopt the user's casing (restoration);
|
|
201
|
-
* 3. else
|
|
230
|
+
* 3. else the model produced a plain title-cased artifact (`Cnpg`) whose
|
|
231
|
+
* lowercased form is a likely ALL-CAPS acronym in a non-shouty source
|
|
232
|
+
* (`CNPG`, `API`, `ETL`) → restore the source acronym;
|
|
233
|
+
* 4. else it's a camelCase artifact (lowercase word + stray interior capital,
|
|
202
234
|
* `dAemon`) the user never wrote → lowercase it;
|
|
203
|
-
*
|
|
235
|
+
* 5. else leave it — preserves model-cased proper nouns like `GitHub`, `OAuth`.
|
|
204
236
|
*
|
|
205
|
-
* Restoration is limited to
|
|
206
|
-
*
|
|
207
|
-
* emphatic all-caps (`ALL ERROR
|
|
237
|
+
* Restoration is limited to avoid three failure modes: a sentence that merely
|
|
238
|
+
* *starts* with `For` can't force a mid-title `for` to `For` (distinctive
|
|
239
|
+
* requires interior mixed casing); emphatic all-caps input (`ALL ERROR
|
|
240
|
+
* HANDLING`, `FIX the BUG NOW`) is never re-shouted — see {@link isShoutySource};
|
|
241
|
+
* and ordinary all-caps English words (`FIX`, `WORK`, `BUG`) are not treated as
|
|
242
|
+
* restorable acronyms unless they carry a stronger acronym signal.
|
|
208
243
|
*/
|
|
209
244
|
function reconcileTitleCasing(title: string, sourceText: string): string {
|
|
210
245
|
const verbatim = new Set<string>();
|
|
211
246
|
const distinctive = new Map<string, string>();
|
|
247
|
+
const acronyms = new Map<string, string>();
|
|
248
|
+
const shouty = isShoutySource(sourceText);
|
|
212
249
|
for (const [token] of sourceText.matchAll(TITLE_WORD)) {
|
|
213
250
|
verbatim.add(token);
|
|
214
251
|
if (isDistinctiveCasing(token)) {
|
|
215
252
|
const lower = token.toLowerCase();
|
|
216
253
|
if (!distinctive.has(lower)) distinctive.set(lower, token);
|
|
254
|
+
} else if (!shouty && isAllCapsAcronym(token)) {
|
|
255
|
+
const lower = token.toLowerCase();
|
|
256
|
+
if (!acronyms.has(lower)) acronyms.set(lower, token);
|
|
217
257
|
}
|
|
218
258
|
}
|
|
219
259
|
return title.replace(TITLE_WORD, token => {
|
|
220
260
|
if (verbatim.has(token)) return token;
|
|
221
|
-
const
|
|
261
|
+
const lower = token.toLowerCase();
|
|
262
|
+
const restored = distinctive.get(lower);
|
|
222
263
|
if (restored) return restored;
|
|
223
|
-
|
|
264
|
+
if (isTitleCasedArtifact(token)) {
|
|
265
|
+
const acronym = acronyms.get(lower);
|
|
266
|
+
if (acronym) return acronym;
|
|
267
|
+
}
|
|
268
|
+
return isCamelArtifact(token) ? lower : token;
|
|
224
269
|
});
|
|
225
270
|
}
|
|
226
271
|
|
|
227
272
|
/** Mixed-case identifier the user cased deliberately (`TinyVMM`, `iOS`, `IDs`):
|
|
228
273
|
* an interior/repeated capital plus at least one lowercase letter. Only these
|
|
229
|
-
* are restored when the model flattens them.
|
|
230
|
-
*
|
|
231
|
-
* Pure all-caps is intentionally excluded. The model preserves its own acronyms
|
|
232
|
-
* verbatim regardless, so restoring all-caps from the source would only ever
|
|
233
|
-
* re-shout emphatic input (`ALL ERROR HANDLING`, `FIX THE BUG`) over the
|
|
234
|
-
* sentence case the prompt asks for. */
|
|
274
|
+
* are restored when the model flattens them. */
|
|
235
275
|
function isDistinctiveCasing(token: string): boolean {
|
|
236
276
|
return /\p{Ll}/u.test(token) && /\p{L}\p{Lu}/u.test(token);
|
|
237
277
|
}
|
|
238
278
|
|
|
279
|
+
/** Multi-letter ALL-CAPS source token with a stronger acronym signal than a
|
|
280
|
+
* plain emphasized word. Consonant-only tokens (`CNPG`, `SQL`, `JWT`) are
|
|
281
|
+
* restored, digit-bearing identifiers are restored, and common technical
|
|
282
|
+
* acronyms (`API`, `JSON`, `URL`) are allowlisted. Ordinary emphasized words
|
|
283
|
+
* (`FIX`, `WORK`, `BUG`) contain vowels and are not restored from source. */
|
|
284
|
+
function isAllCapsAcronym(token: string): boolean {
|
|
285
|
+
if (!isAllCapsWord(token)) return false;
|
|
286
|
+
const upper = token.toUpperCase();
|
|
287
|
+
if (COMMON_TITLE_ACRONYMS.has(upper)) return true;
|
|
288
|
+
if (/\p{N}/u.test(token)) return true;
|
|
289
|
+
return !/[AEIOU]/.test(upper);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Multi-letter ALL-CAPS word in the source. Used for shout detection, not for
|
|
293
|
+
* acronym restoration — shouted English words (`FIX`, `WORK`) still count as
|
|
294
|
+
* shouty even though they are not restorable acronyms. Requires an actual
|
|
295
|
+
* uppercase letter so caseless scripts (CJK) never register as shouting and
|
|
296
|
+
* disable acronym restoration for those messages. */
|
|
297
|
+
function isAllCapsWord(token: string): boolean {
|
|
298
|
+
const letters = token.match(/\p{L}/gu);
|
|
299
|
+
if (!letters || letters.length < 2) return false;
|
|
300
|
+
return /\p{Lu}/u.test(token) && !/\p{Ll}/u.test(token);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Plain title-cased word (`Cnpg`, `Etl`): starts uppercase, has one-or-more
|
|
304
|
+
* lowercase letters, no interior uppercase. This is the artifact a title model
|
|
305
|
+
* produces when it sentence-cases an unfamiliar ALL-CAPS acronym; PascalCase
|
|
306
|
+
* proper nouns like `GitHub`/`OAuth` have an interior capital and are
|
|
307
|
+
* excluded so we don't misidentify them. */
|
|
308
|
+
function isTitleCasedArtifact(token: string): boolean {
|
|
309
|
+
if (!/^\p{Lu}/u.test(token)) return false;
|
|
310
|
+
if (!/\p{Ll}/u.test(token)) return false;
|
|
311
|
+
return !/\p{Lu}/u.test(token.slice(1));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** True when the source text is shouting — ≥2 consecutive multi-letter
|
|
315
|
+
* ALL-CAPS tokens (`FIX the BUG NOW` has `BUG NOW`; `ALL ERROR HANDLING`
|
|
316
|
+
* has all three adjacent). Acronym restoration is disabled for shouty input
|
|
317
|
+
* so we don't re-shout emphatic prose the model correctly de-shouted. */
|
|
318
|
+
function isShoutySource(sourceText: string): boolean {
|
|
319
|
+
let run = 0;
|
|
320
|
+
for (const [token] of sourceText.matchAll(TITLE_WORD)) {
|
|
321
|
+
if (isAllCapsWord(token)) {
|
|
322
|
+
run += 1;
|
|
323
|
+
if (run >= 2) return true;
|
|
324
|
+
} else {
|
|
325
|
+
run = 0;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
239
331
|
/** A lowercase word carrying a stray interior capital (`dAemon`, `cReate`): the
|
|
240
332
|
* model-mangled shape we flatten when the user never wrote it. PascalCase proper
|
|
241
333
|
* nouns (`GitHub`, `OAuth`) start uppercase and are left untouched. */
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
|
19
19
|
import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
20
20
|
import { formatMatchLine } from "./match-line-format";
|
|
21
21
|
import type { OutputMeta } from "./output-meta";
|
|
22
|
-
import { resolveToolSearchScope } from "./path-utils";
|
|
22
|
+
import { resolveToolSearchScope, toPathList } from "./path-utils";
|
|
23
23
|
import {
|
|
24
24
|
appendParseErrorsBulletList,
|
|
25
25
|
capParseErrors,
|
|
@@ -37,11 +37,9 @@ import { toolResult } from "./tool-result";
|
|
|
37
37
|
|
|
38
38
|
const astGrepSchema = type({
|
|
39
39
|
pat: type("string").describe("ast pattern"),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.atLeastLength(1)
|
|
44
|
-
.describe("files, directories, globs, or internal URLs to search"),
|
|
40
|
+
"path?": type("string").describe(
|
|
41
|
+
'file, directory, glob, or internal URL to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
|
|
42
|
+
),
|
|
45
43
|
"skip?": type("number").describe("matches to skip"),
|
|
46
44
|
});
|
|
47
45
|
|
|
@@ -159,23 +157,23 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
159
157
|
readonly examples: readonly ToolExample<typeof astGrepSchema.inferIn>[] = [
|
|
160
158
|
{
|
|
161
159
|
caption: "Search TypeScript files under src",
|
|
162
|
-
call: { pat: "console.log($$$)",
|
|
160
|
+
call: { pat: "console.log($$$)", path: "src/**/*.ts" },
|
|
163
161
|
},
|
|
164
162
|
{
|
|
165
163
|
caption: "Named imports from a specific package",
|
|
166
|
-
call: { pat: 'import { $$$IMPORTS } from "react"',
|
|
164
|
+
call: { pat: 'import { $$$IMPORTS } from "react"', path: "src/**/*.ts" },
|
|
167
165
|
},
|
|
168
166
|
{
|
|
169
167
|
caption: "Arrow functions assigned to a const",
|
|
170
|
-
call: { pat: "const $NAME = ($$$ARGS) => $BODY",
|
|
168
|
+
call: { pat: "const $NAME = ($$$ARGS) => $BODY", path: "src/utils/**/*.ts" },
|
|
171
169
|
},
|
|
172
170
|
{
|
|
173
171
|
caption: "Method call on any object, ignoring method name with `$_`",
|
|
174
|
-
call: { pat: "logger.$_($$$ARGS)",
|
|
172
|
+
call: { pat: "logger.$_($$$ARGS)", path: "src/**/*.ts" },
|
|
175
173
|
},
|
|
176
174
|
{
|
|
177
175
|
caption: "Loosest existence check for a symbol in one file",
|
|
178
|
-
call: { pat: "processItems",
|
|
176
|
+
call: { pat: "processItems", path: "src/worker.ts" },
|
|
179
177
|
},
|
|
180
178
|
];
|
|
181
179
|
readonly loadMode = "discoverable";
|
|
@@ -201,8 +199,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
201
199
|
if (!Number.isFinite(skip) || skip < 0) {
|
|
202
200
|
throw new ToolError("skip must be a non-negative number");
|
|
203
201
|
}
|
|
202
|
+
const scopedPaths = toPathList(params.path);
|
|
203
|
+
const rawPaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
204
204
|
const scope = await resolveToolSearchScope({
|
|
205
|
-
rawPaths
|
|
205
|
+
rawPaths,
|
|
206
206
|
cwd: this.session.cwd,
|
|
207
207
|
internalUrlAction: "search",
|
|
208
208
|
settings: this.session.settings,
|
|
@@ -275,7 +275,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
275
275
|
|
|
276
276
|
if (result.matches.length === 0) {
|
|
277
277
|
const noMatchMessage = cappedParseErrors.length
|
|
278
|
-
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `
|
|
278
|
+
? "No matches found. Parse issues mean the query may be mis-scoped; narrow `path` before concluding absence."
|
|
279
279
|
: "No matches found";
|
|
280
280
|
const parseMessage = cappedParseErrors.length
|
|
281
281
|
? `\n${formatParseErrors(cappedParseErrors, parseErrorsTotal).join("\n")}`
|
|
@@ -375,7 +375,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
375
375
|
displayContent: displayLines.join("\n"),
|
|
376
376
|
};
|
|
377
377
|
if (result.limitReached) {
|
|
378
|
-
outputLines.push("", "Result limit reached; narrow
|
|
378
|
+
outputLines.push("", "Result limit reached; narrow path or increase limit.");
|
|
379
379
|
}
|
|
380
380
|
if (cappedParseErrors.length) {
|
|
381
381
|
outputLines.push("", ...formatParseErrors(cappedParseErrors, parseErrorsTotal));
|
|
@@ -392,6 +392,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
392
392
|
|
|
393
393
|
interface AstGrepRenderArgs {
|
|
394
394
|
pat?: string;
|
|
395
|
+
path?: string | string[];
|
|
396
|
+
/** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
|
|
395
397
|
paths?: string[];
|
|
396
398
|
skip?: number;
|
|
397
399
|
}
|
|
@@ -402,7 +404,8 @@ export const astGrepToolRenderer = {
|
|
|
402
404
|
inline: true,
|
|
403
405
|
renderCall(args: AstGrepRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
404
406
|
const meta: string[] = [];
|
|
405
|
-
|
|
407
|
+
const scopePaths = toPathList(args.path ?? args.paths);
|
|
408
|
+
if (scopePaths.length) meta.push(`in ${scopePaths.join(", ")}`);
|
|
406
409
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
407
410
|
|
|
408
411
|
const description = args.pat ?? "?";
|
|
@@ -436,7 +439,7 @@ export const astGrepToolRenderer = {
|
|
|
436
439
|
const header = renderStatusLine({ icon: "warning", title: "AST Grep", description, meta }, uiTheme);
|
|
437
440
|
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
438
441
|
if (details?.parseErrors?.length) {
|
|
439
|
-
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `
|
|
442
|
+
lines.push(uiTheme.fg("warning", "Query may be mis-scoped; narrow `path` before concluding absence"));
|
|
440
443
|
appendParseErrorsBulletList(lines, details.parseErrors, uiTheme, details.parseErrorsTotal);
|
|
441
444
|
}
|
|
442
445
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -487,7 +490,7 @@ export const astGrepToolRenderer = {
|
|
|
487
490
|
|
|
488
491
|
const extraLines: string[] = [];
|
|
489
492
|
if (limitReached) {
|
|
490
|
-
extraLines.push(uiTheme.fg("warning", "limit reached; narrow
|
|
493
|
+
extraLines.push(uiTheme.fg("warning", "limit reached; narrow path or increase limit"));
|
|
491
494
|
}
|
|
492
495
|
if (details?.parseErrors?.length) {
|
|
493
496
|
extraLines.push(
|
package/src/tools/bash.ts
CHANGED
|
@@ -46,6 +46,33 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
46
46
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
47
47
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Shape a shell command line for an ACP-conformant `terminal/create` request.
|
|
51
|
+
*
|
|
52
|
+
* ACP's `command` field is documented as the executable and `args` as its
|
|
53
|
+
* argv tail (see https://agentclientprotocol.com/protocol/v1/terminals), so a
|
|
54
|
+
* spec-conformant client `spawn(command, args)`s them directly — no implicit
|
|
55
|
+
* shell. A raw `bash` tool line ("git status && echo x | head") therefore has
|
|
56
|
+
* to be wrapped in an explicit shell invocation, otherwise the client tries
|
|
57
|
+
* to spawn the whole line as argv[0] and fails with `ENOENT` for anything
|
|
58
|
+
* containing a space, pipe, `&&`, redirect, or `$(...)`.
|
|
59
|
+
*
|
|
60
|
+
* The wrap reuses the same shell binary + args the local `bash-executor` would
|
|
61
|
+
* pick via `settings.getShellConfig()` — Git Bash / `bash.exe` on Windows,
|
|
62
|
+
* `$SHELL` (bash/zsh) with the `sh` fallback on POSIX — so the ACP path
|
|
63
|
+
* preserves `bash` tool semantics (`$VAR`, `$(...)`, `source`, POSIX quoting,
|
|
64
|
+
* `-l`) instead of dropping to `cmd.exe` on Windows. The agent host's shell
|
|
65
|
+
* path is used as a proxy for the client's, matching the near-universal
|
|
66
|
+
* ACP deployment shape of an editor spawning omp as a co-hosted subprocess.
|
|
67
|
+
*/
|
|
68
|
+
export function wrapShellLineForClientTerminal(
|
|
69
|
+
line: string,
|
|
70
|
+
shellConfig: { shell: string; args: string[]; prefix?: string | undefined },
|
|
71
|
+
): { command: string; args: string[] } {
|
|
72
|
+
const finalLine = shellConfig.prefix ? `${shellConfig.prefix} ${line}` : line;
|
|
73
|
+
return { command: shellConfig.shell, args: [...shellConfig.args, finalLine] };
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/**
|
|
50
77
|
* Bash patterns flagged as safety critical for approval policy.
|
|
51
78
|
*
|
|
@@ -842,8 +869,10 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
842
869
|
// Skip when pty=true (PTY needs the local terminal UI).
|
|
843
870
|
if (clientBridge?.capabilities.terminal && clientBridge.createTerminal && !pty) {
|
|
844
871
|
const bridgeWallTimeStart = performance.now();
|
|
872
|
+
const shellSpawn = wrapShellLineForClientTerminal(command, this.session.settings.getShellConfig());
|
|
845
873
|
const handle = await clientBridge.createTerminal({
|
|
846
|
-
command,
|
|
874
|
+
command: shellSpawn.command,
|
|
875
|
+
args: shellSpawn.args,
|
|
847
876
|
cwd: commandCwd,
|
|
848
877
|
env: resolvedEnv
|
|
849
878
|
? Object.entries(resolvedEnv).map(([name, value]) => ({ name, value: value as string }))
|
|
@@ -1151,22 +1180,30 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1151
1180
|
renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
1152
1181
|
const renderArgs = toBashRenderArgs(args, config);
|
|
1153
1182
|
const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
|
|
1154
|
-
const header =
|
|
1155
|
-
config.showHeader === false
|
|
1156
|
-
? undefined
|
|
1157
|
-
: renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
|
|
1158
1183
|
const outputBlock = new CachedOutputBlock();
|
|
1159
1184
|
return markFramedBlockComponent({
|
|
1160
|
-
render: (width: number): readonly string[] =>
|
|
1161
|
-
|
|
1185
|
+
render: (width: number): readonly string[] => {
|
|
1186
|
+
const header =
|
|
1187
|
+
config.showHeader === false
|
|
1188
|
+
? undefined
|
|
1189
|
+
: renderStatusLine(
|
|
1190
|
+
{
|
|
1191
|
+
icon: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
1192
|
+
spinnerFrame: options.spinnerFrame,
|
|
1193
|
+
title: config.resolveTitle(args, options),
|
|
1194
|
+
},
|
|
1195
|
+
uiTheme,
|
|
1196
|
+
);
|
|
1197
|
+
return outputBlock.render(
|
|
1162
1198
|
{
|
|
1163
1199
|
header,
|
|
1164
|
-
state: "pending",
|
|
1200
|
+
state: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
1165
1201
|
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
1166
1202
|
width,
|
|
1167
1203
|
},
|
|
1168
1204
|
uiTheme,
|
|
1169
|
-
)
|
|
1205
|
+
);
|
|
1206
|
+
},
|
|
1170
1207
|
invalidate: () => {
|
|
1171
1208
|
outputBlock.invalidate();
|
|
1172
1209
|
},
|
|
@@ -187,6 +187,8 @@ function extractTextOutput(content: Array<{ type: string; text?: string }> | und
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export const browserToolRenderer = {
|
|
190
|
+
animatedPendingPreview: (args: unknown) => (args as BrowserRenderArgs).action === "run",
|
|
191
|
+
animatedPartialResult: (args: unknown) => (args as BrowserRenderArgs).action === "run",
|
|
190
192
|
renderCall(args: BrowserRenderArgs, options: RenderResultOptions, theme: Theme): Component {
|
|
191
193
|
const action = args.action;
|
|
192
194
|
if (action === "run") {
|
package/src/tools/debug.ts
CHANGED
|
@@ -583,6 +583,7 @@ function summarizeDebugCall(args: DebugRenderArgs): string {
|
|
|
583
583
|
}
|
|
584
584
|
|
|
585
585
|
export const debugToolRenderer = {
|
|
586
|
+
animatedPartialResult: true,
|
|
586
587
|
renderCall(args: DebugRenderArgs, _options: RenderResultOptions, theme: Theme): Component {
|
|
587
588
|
const text = renderStatusLine({ icon: "pending", title: "Debug", description: summarizeDebugCall(args) }, theme);
|
|
588
589
|
return new Text(text, 0, 0);
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -489,6 +489,8 @@ function formatCellOutputLines(
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
export const evalToolRenderer = {
|
|
492
|
+
animatedPendingPreview: true,
|
|
493
|
+
animatedPartialResult: true,
|
|
492
494
|
renderCall(args: EvalRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
493
495
|
const cells = getRenderCells(args);
|
|
494
496
|
|
|
@@ -502,7 +504,7 @@ export const evalToolRenderer = {
|
|
|
502
504
|
|
|
503
505
|
return markFramedBlockComponent({
|
|
504
506
|
render: (width: number): readonly string[] => {
|
|
505
|
-
const key = `${options.expanded ? 1 : 0}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
507
|
+
const key = `${options.expanded ? 1 : 0}|${options.spinnerFrame ?? "-"}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
|
|
506
508
|
if (cached && cached.key === key && cached.width === width) {
|
|
507
509
|
return cached.result;
|
|
508
510
|
}
|
|
@@ -518,7 +520,8 @@ export const evalToolRenderer = {
|
|
|
518
520
|
index: i,
|
|
519
521
|
total: cells.length,
|
|
520
522
|
title: cell.title,
|
|
521
|
-
status: "pending",
|
|
523
|
+
status: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
524
|
+
spinnerFrame: options.spinnerFrame,
|
|
522
525
|
width,
|
|
523
526
|
// Viewport-sized tail window following the newest streamed code
|
|
524
527
|
// line; renderResult keeps the same cap so the cell never snaps
|
package/src/tools/gh-renderer.ts
CHANGED
|
@@ -416,6 +416,9 @@ function renderWatchCall(args: GithubToolRenderArgs, options: RenderResultOption
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
export const githubToolRenderer = {
|
|
419
|
+
// No animatedPendingPreview: renderCall materializes plain Text components
|
|
420
|
+
// once per display rebuild (no render closure), so a live spinner interval
|
|
421
|
+
// would request 30fps repaints while the visible glyph stays frozen.
|
|
419
422
|
renderCall(args: GithubToolRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
420
423
|
const op = typeof args.op === "string" && args.op.trim().length > 0 ? args.op.trim() : undefined;
|
|
421
424
|
if (op === "run_watch") {
|
package/src/tools/glob.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
partitionExistingPaths,
|
|
27
27
|
resolveExplicitFindPatterns,
|
|
28
28
|
resolveToCwd,
|
|
29
|
+
toPathList,
|
|
29
30
|
} from "./path-utils";
|
|
30
31
|
import {
|
|
31
32
|
createCachedComponent,
|
|
@@ -38,11 +39,9 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
|
38
39
|
import { toolResult } from "./tool-result";
|
|
39
40
|
|
|
40
41
|
const findSchema = type({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.atLeastLength(1)
|
|
45
|
-
.describe("globs including search paths"),
|
|
42
|
+
"path?": type("string").describe(
|
|
43
|
+
'glob, file, or directory to search — a single path or a semicolon-delimited list ("src/**/*.ts; test/**/*.ts"). Omitted -> searches the workspace root (".")',
|
|
44
|
+
),
|
|
46
45
|
"hidden?": type("boolean").describe("include hidden files"),
|
|
47
46
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
48
47
|
"limit?": type("number").describe("max results"),
|
|
@@ -110,19 +109,19 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
110
109
|
readonly examples: readonly ToolExample<typeof findSchema.infer>[] = [
|
|
111
110
|
{
|
|
112
111
|
caption: "Glob files",
|
|
113
|
-
call: {
|
|
112
|
+
call: { path: "src/**/*.ts" },
|
|
114
113
|
},
|
|
115
114
|
{
|
|
116
|
-
caption: "Multiple targets —
|
|
117
|
-
call: {
|
|
115
|
+
caption: "Multiple targets — semicolon-delimited list",
|
|
116
|
+
call: { path: "src/**/*.ts; test/**/*.ts" },
|
|
118
117
|
},
|
|
119
118
|
{
|
|
120
119
|
caption: "Glob gitignored files like .env",
|
|
121
|
-
call: {
|
|
120
|
+
call: { path: ".env*", gitignore: false },
|
|
122
121
|
},
|
|
123
122
|
{
|
|
124
123
|
caption: "Glob directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
|
|
125
|
-
call: {
|
|
124
|
+
call: { path: "**/tests" },
|
|
126
125
|
},
|
|
127
126
|
];
|
|
128
127
|
readonly strict = true;
|
|
@@ -144,13 +143,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
144
143
|
onUpdate?: AgentToolUpdateCallback<GlobToolDetails>,
|
|
145
144
|
_context?: AgentToolContext,
|
|
146
145
|
): Promise<AgentToolResult<GlobToolDetails>> {
|
|
147
|
-
const {
|
|
146
|
+
const { path: pathInput, limit, hidden, gitignore } = params;
|
|
148
147
|
|
|
149
148
|
return untilAborted(signal, async () => {
|
|
150
149
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
150
|
+
const scopedPaths = toPathList(pathInput);
|
|
151
|
+
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
151
152
|
const rawPatternInputs = this.#customOps
|
|
152
|
-
?
|
|
153
|
-
: await expandDelimitedPathEntries(
|
|
153
|
+
? effectivePaths
|
|
154
|
+
: await expandDelimitedPathEntries(effectivePaths, this.session.cwd, { splitter: parseFindPattern });
|
|
154
155
|
const rawPatterns = rawPatternInputs.map(input => normalizePathLikeInput(input).replace(/\\/g, "/"));
|
|
155
156
|
const internalRouter = InternalUrlRouter.instance();
|
|
156
157
|
const normalizedPatterns: string[] = [];
|
|
@@ -180,7 +181,7 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
180
181
|
normalizedPatterns.push(resource.sourcePath);
|
|
181
182
|
}
|
|
182
183
|
if (normalizedPatterns.some(pattern => pattern.length === 0)) {
|
|
183
|
-
throw new ToolError("`
|
|
184
|
+
throw new ToolError("`path` must contain non-empty globs or paths");
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
@@ -468,12 +469,15 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
|
|
|
468
469
|
// =============================================================================
|
|
469
470
|
|
|
470
471
|
interface GlobRenderArgs {
|
|
472
|
+
path?: string | string[];
|
|
473
|
+
/** Legacy pre-`path` argument name; kept so historical transcripts still render a scope. */
|
|
471
474
|
paths?: string | string[];
|
|
472
475
|
limit?: number;
|
|
473
476
|
}
|
|
474
477
|
|
|
475
|
-
function formatGlobRenderPaths(
|
|
476
|
-
|
|
478
|
+
function formatGlobRenderPaths(args: GlobRenderArgs | undefined): string | undefined {
|
|
479
|
+
const list = toPathList(args?.path ?? args?.paths);
|
|
480
|
+
return list.length > 0 ? list.join(", ") : undefined;
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
@@ -493,7 +497,7 @@ export const globToolRenderer = {
|
|
|
493
497
|
icon: "pending",
|
|
494
498
|
title: "Glob",
|
|
495
499
|
titleColor: "toolTitle",
|
|
496
|
-
description: formatGlobRenderPaths(args
|
|
500
|
+
description: formatGlobRenderPaths(args) || "*",
|
|
497
501
|
meta,
|
|
498
502
|
},
|
|
499
503
|
uiTheme,
|
|
@@ -533,7 +537,7 @@ export const globToolRenderer = {
|
|
|
533
537
|
iconOverride: globStatusIcon(uiTheme),
|
|
534
538
|
title: "Glob",
|
|
535
539
|
titleColor: "toolTitle",
|
|
536
|
-
description: formatGlobRenderPaths(args
|
|
540
|
+
description: formatGlobRenderPaths(args),
|
|
537
541
|
meta: [formatCount("file", lines.length)],
|
|
538
542
|
},
|
|
539
543
|
uiTheme,
|
|
@@ -573,7 +577,7 @@ export const globToolRenderer = {
|
|
|
573
577
|
icon: "warning",
|
|
574
578
|
title: "Glob",
|
|
575
579
|
titleColor: "toolTitle",
|
|
576
|
-
description: formatGlobRenderPaths(args
|
|
580
|
+
description: formatGlobRenderPaths(args),
|
|
577
581
|
meta: ["0 files"],
|
|
578
582
|
},
|
|
579
583
|
uiTheme,
|
|
@@ -590,7 +594,7 @@ export const globToolRenderer = {
|
|
|
590
594
|
...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
|
|
591
595
|
title: "Glob",
|
|
592
596
|
titleColor: "toolTitle",
|
|
593
|
-
description: formatGlobRenderPaths(args
|
|
597
|
+
description: formatGlobRenderPaths(args),
|
|
594
598
|
meta,
|
|
595
599
|
},
|
|
596
600
|
uiTheme,
|