@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.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.
- package/CHANGELOG.md +65 -0
- package/dist/cli.js +3176 -3087
- package/dist/types/advisor/runtime.d.ts +11 -0
- package/dist/types/config/keybindings.d.ts +9 -4
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +6 -0
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
- package/dist/types/mnemopi/state.d.ts +7 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -1
- package/dist/types/modes/components/read-tool-group.d.ts +1 -0
- package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +0 -12
- package/dist/types/modes/workflow.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +8 -4
- package/dist/types/system-prompt.d.ts +1 -1
- package/dist/types/tools/bash-interactive.d.ts +1 -1
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +2 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +24 -0
- package/dist/types/tools/read.d.ts +3 -0
- package/dist/types/tools/renderers.d.ts +12 -5
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tools/write.d.ts +1 -0
- package/dist/types/utils/local-date.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +145 -0
- package/src/advisor/runtime.ts +19 -0
- package/src/config/api-key-resolver.ts +7 -2
- package/src/config/keybindings.ts +62 -10
- package/src/config/model-registry.ts +94 -20
- package/src/config/settings-schema.ts +11 -1
- package/src/config/settings.ts +59 -21
- package/src/discovery/builtin.ts +2 -1
- package/src/discovery/claude-plugins.ts +167 -46
- package/src/discovery/helpers.ts +16 -1
- package/src/edit/renderer.ts +20 -6
- package/src/eval/js/worker-core.ts +163 -6
- package/src/exec/bash-executor.ts +14 -9
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/registry-helpers.ts +9 -6
- package/src/mnemopi/state.ts +19 -5
- package/src/modes/acp/acp-agent.ts +69 -8
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/model-selector.ts +30 -6
- package/src/modes/components/read-tool-group.ts +5 -1
- package/src/modes/components/settings-defs.ts +1 -1
- package/src/modes/components/status-line/component.ts +14 -2
- package/src/modes/components/tool-execution.ts +28 -24
- package/src/modes/controllers/command-controller.ts +13 -23
- package/src/modes/controllers/event-controller.ts +12 -12
- package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -35
- package/src/modes/controllers/input-controller.ts +23 -57
- package/src/modes/controllers/mcp-command-controller.ts +10 -9
- package/src/modes/controllers/selector-controller.ts +16 -5
- package/src/modes/github-ref-autocomplete.ts +75 -0
- package/src/modes/interactive-mode.ts +97 -12
- package/src/modes/prompt-action-autocomplete.ts +35 -0
- package/src/modes/rpc/rpc-client.ts +42 -13
- package/src/modes/rpc/rpc-mode.ts +21 -19
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/context-usage.ts +58 -5
- package/src/modes/utils/hotkeys-markdown.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/modes/workflow.ts +14 -8
- package/src/prompts/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/system/plan-mode-active.md +5 -2
- package/src/prompts/system/system-prompt.md +1 -2
- package/src/prompts/system/workflow-notice.md +69 -50
- package/src/prompts/tools/bash.md +18 -7
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/prompts/tools/read.md +4 -3
- package/src/sdk.ts +11 -0
- package/src/session/agent-session.ts +136 -19
- package/src/system-prompt.ts +3 -2
- package/src/tools/bash-interactive.ts +1 -1
- package/src/tools/bash-skill-urls.ts +39 -7
- package/src/tools/bash.ts +69 -39
- package/src/tools/browser/launch.ts +31 -4
- package/src/tools/grep.ts +105 -21
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/index.ts +11 -0
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/path-utils.ts +46 -1
- package/src/tools/read.ts +135 -57
- package/src/tools/renderers.ts +13 -5
- package/src/tools/ssh.ts +10 -3
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +26 -0
- package/src/utils/local-date.ts +7 -0
- package/src/utils/open.ts +36 -10
package/src/tools/image-gen.ts
CHANGED
|
@@ -1276,7 +1276,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1276
1276
|
const xaiCreds = await resolveXAIHttpCredentials(ctx.modelRegistry, resolvedModel);
|
|
1277
1277
|
if (!xaiCreds) {
|
|
1278
1278
|
throw new Error(
|
|
1279
|
-
"No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok
|
|
1279
|
+
"No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok or X Premium+) or set XAI_API_KEY.",
|
|
1280
1280
|
);
|
|
1281
1281
|
}
|
|
1282
1282
|
|
package/src/tools/index.ts
CHANGED
|
@@ -224,6 +224,10 @@ export interface ToolSession {
|
|
|
224
224
|
getAgentId?: () => string | null;
|
|
225
225
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|
|
226
226
|
getToolByName?: (name: string) => AgentTool | undefined;
|
|
227
|
+
/** Return whether a built-in tool is active in this turn's tool set. */
|
|
228
|
+
isToolActive?: (name: string) => boolean;
|
|
229
|
+
/** Update the active built-in tool predicate when a session changes tools mid-run. */
|
|
230
|
+
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
227
231
|
/** Agent registry for IRC routing across live sessions. */
|
|
228
232
|
agentRegistry?: AgentRegistry;
|
|
229
233
|
/** Get artifacts directory for artifact:// URLs */
|
|
@@ -647,6 +651,13 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
647
651
|
...(goalModeActive ? ([["goal", HIDDEN_TOOLS.goal]] as const) : []),
|
|
648
652
|
];
|
|
649
653
|
|
|
654
|
+
const activeToolNames = new Set(baseEntries.map(([name]) => name));
|
|
655
|
+
if (session.setActiveToolNames) {
|
|
656
|
+
session.setActiveToolNames(activeToolNames);
|
|
657
|
+
} else {
|
|
658
|
+
session.isToolActive = name => activeToolNames.has(name);
|
|
659
|
+
}
|
|
660
|
+
|
|
650
661
|
const baseResults = await Promise.all(
|
|
651
662
|
baseEntries.map(async ([name, factory]) => {
|
|
652
663
|
const tool = await logger.time(`createTools:${name}`, factory as ToolFactory, session);
|
package/src/tools/memory-edit.ts
CHANGED
|
@@ -50,7 +50,9 @@ export class MemoryEditTool implements AgentTool<typeof memoryEditSchema> {
|
|
|
50
50
|
const text =
|
|
51
51
|
result.status === "not_found"
|
|
52
52
|
? `Memory ${params.id} was not found${location}.`
|
|
53
|
-
:
|
|
53
|
+
: result.status === "not_editable"
|
|
54
|
+
? `Memory ${params.id} is a read-only fact${location}; it cannot be edited. Read it with memory://${params.id}.`
|
|
55
|
+
: `Memory ${params.id} ${result.status}${location}.`;
|
|
54
56
|
return {
|
|
55
57
|
content: [{ type: "text", text }],
|
|
56
58
|
details: result,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -315,6 +315,46 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
|
|
|
315
315
|
return { path: basePath, sel };
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Three-way probe for whether the exact filesystem entry named by `filePath`
|
|
320
|
+
* exists. `stat` (used earlier) failed for reasons other than "no such file"
|
|
321
|
+
* (dangling symlink, `EACCES` on a parent, transient I/O), and each of those
|
|
322
|
+
* silently reinterpreted a real literal path such as `test:1-2` as `test`
|
|
323
|
+
* plus selector `1-2` (issue #4618). `lstat` inspects the entry itself, so a
|
|
324
|
+
* dangling symlink is still detected as present; ambiguous errors resolve to
|
|
325
|
+
* `"unknown"` so callers keep the raw path instead of guessing.
|
|
326
|
+
*/
|
|
327
|
+
export async function probeLiteralPathExists(filePath: string, cwd: string): Promise<"exists" | "missing" | "unknown"> {
|
|
328
|
+
const resolved = resolveReadPath(filePath, cwd);
|
|
329
|
+
try {
|
|
330
|
+
await fs.promises.lstat(resolved);
|
|
331
|
+
return "exists";
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (isEnoent(err) || isEnotdir(err)) return "missing";
|
|
334
|
+
return "unknown";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Async sibling of {@link splitPathAndSel} that prefers a literal filesystem
|
|
340
|
+
* path over selector interpretation. Filenames whose tail matches the selector
|
|
341
|
+
* grammar (e.g. `test:1-2`, `log:raw`) are legal on POSIX; without this the
|
|
342
|
+
* strict splitter peels the tail and both `read` and `grep` refuse to open the
|
|
343
|
+
* real file (issue #4618). The literal wins on a confirmed `lstat`, and also
|
|
344
|
+
* on `"unknown"` (`EACCES` on a parent, transient I/O), so an unreachable
|
|
345
|
+
* literal is never silently reinterpreted as `path + selector`. Only a
|
|
346
|
+
* definitive `ENOENT`/`ENOTDIR` falls back to the strict split.
|
|
347
|
+
*/
|
|
348
|
+
export async function splitPathAndSelPreferringLiteral(
|
|
349
|
+
rawPath: string,
|
|
350
|
+
cwd: string,
|
|
351
|
+
): Promise<{ path: string; sel?: string }> {
|
|
352
|
+
const strict = splitPathAndSel(rawPath);
|
|
353
|
+
if (strict.sel === undefined) return strict;
|
|
354
|
+
const probe = await probeLiteralPathExists(rawPath, cwd);
|
|
355
|
+
return probe === "missing" ? strict : { path: rawPath };
|
|
356
|
+
}
|
|
357
|
+
|
|
318
358
|
/**
|
|
319
359
|
* Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
|
|
320
360
|
*
|
|
@@ -669,7 +709,12 @@ export async function splitDelimitedPathEntry(
|
|
|
669
709
|
const normalizedEntry = normalizePathLikeInput(entry);
|
|
670
710
|
if (!hasTopLevelPathDelimiter(normalizedEntry)) return null;
|
|
671
711
|
if (isInternalUrlPath(normalizedEntry)) return null;
|
|
672
|
-
|
|
712
|
+
// A real POSIX file may contain the delimiter and a selector-shaped tail
|
|
713
|
+
// (`a;b:1-2`, `a b:1-2`). Preserve the raw entry whenever the full literal
|
|
714
|
+
// resolves — or is only ambiguous — so downstream literal-preferring
|
|
715
|
+
// splitters see it before delimiter expansion peels or splits (issue #4618
|
|
716
|
+
// reviewer feedback: delimited expansion ran before the literal check).
|
|
717
|
+
if ((await probeLiteralPathExists(normalizedEntry, cwd)) !== "missing") return null;
|
|
673
718
|
const splitter = options.splitter ?? parseSearchPath;
|
|
674
719
|
const peeledEntry = splitPathAndSel(normalizedEntry).path;
|
|
675
720
|
if (!hasGlobPathChars(peeledEntry) && (await delimitedPathPartResolves(normalizedEntry, cwd, splitter))) {
|
package/src/tools/read.ts
CHANGED
|
@@ -99,10 +99,12 @@ import {
|
|
|
99
99
|
type LineRange,
|
|
100
100
|
parseLineRanges,
|
|
101
101
|
pathTargetsSsh,
|
|
102
|
+
probeLiteralPathExists,
|
|
102
103
|
resolveReadPath,
|
|
103
104
|
splitDelimitedPathEntry,
|
|
104
105
|
splitInternalUrlSel,
|
|
105
106
|
splitPathAndSel,
|
|
107
|
+
splitPathAndSelPreferringLiteral,
|
|
106
108
|
} from "./path-utils";
|
|
107
109
|
import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
|
|
108
110
|
import {
|
|
@@ -746,7 +748,10 @@ function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; membe
|
|
|
746
748
|
|
|
747
749
|
const readSchema = type({
|
|
748
750
|
path: type("string").describe(
|
|
749
|
-
'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL
|
|
751
|
+
'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL. Inline :<sel> is still accepted for compatibility.',
|
|
752
|
+
),
|
|
753
|
+
"selector?": type("string").describe(
|
|
754
|
+
'selector without a leading colon (e.g. "50-100", "raw", "raw:50-100", "conflicts"); keeps `path` literal when filenames contain colons',
|
|
750
755
|
),
|
|
751
756
|
});
|
|
752
757
|
|
|
@@ -2113,6 +2118,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2113
2118
|
_toolContext?: AgentToolContext,
|
|
2114
2119
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2115
2120
|
let { path: readPath } = params;
|
|
2121
|
+
let explicitSelector = params.selector?.trim() || undefined;
|
|
2122
|
+
let explicitParsedSelector = explicitSelector === undefined ? undefined : parseSel(explicitSelector);
|
|
2123
|
+
if (explicitSelector !== undefined && explicitParsedSelector?.kind === "none") {
|
|
2124
|
+
throw invalidSelector(explicitSelector);
|
|
2125
|
+
}
|
|
2116
2126
|
if (readPath.startsWith("file://")) {
|
|
2117
2127
|
readPath = expandPath(readPath);
|
|
2118
2128
|
}
|
|
@@ -2133,40 +2143,55 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2133
2143
|
if (!this.session.settings.get("fetch.enabled")) {
|
|
2134
2144
|
throw new ToolError("URL reads are disabled by settings.");
|
|
2135
2145
|
}
|
|
2136
|
-
if (
|
|
2146
|
+
if (explicitParsedSelector?.kind === "conflicts") {
|
|
2147
|
+
throw new ToolError("The explicit read selector `conflicts` is only supported for local files.");
|
|
2148
|
+
}
|
|
2149
|
+
const urlRaw =
|
|
2150
|
+
explicitParsedSelector === undefined ? parsedUrlTarget.raw : isRawSelector(explicitParsedSelector);
|
|
2151
|
+
const urlRanges =
|
|
2152
|
+
explicitParsedSelector?.kind === "lines" ? explicitParsedSelector.ranges : parsedUrlTarget.ranges;
|
|
2153
|
+
if (urlRanges !== undefined && urlRanges.length > 1) {
|
|
2137
2154
|
const cached = await loadReadUrlCacheEntry(
|
|
2138
2155
|
this.session,
|
|
2139
|
-
{ path: parsedUrlTarget.path, raw:
|
|
2156
|
+
{ path: parsedUrlTarget.path, raw: urlRaw },
|
|
2140
2157
|
signal,
|
|
2141
2158
|
{ ensureArtifact: true, preferCached: true },
|
|
2142
2159
|
);
|
|
2143
|
-
return this.#buildInMemoryMultiRangeResult(cached.output,
|
|
2160
|
+
return this.#buildInMemoryMultiRangeResult(cached.output, urlRanges, {
|
|
2144
2161
|
details: { ...cached.details },
|
|
2145
2162
|
sourceUrl: cached.details.finalUrl,
|
|
2146
2163
|
entityLabel: "URL output",
|
|
2147
|
-
raw:
|
|
2164
|
+
raw: urlRaw,
|
|
2148
2165
|
immutable: true,
|
|
2149
2166
|
});
|
|
2150
2167
|
}
|
|
2151
|
-
|
|
2168
|
+
const urlRange = urlRanges?.[0];
|
|
2169
|
+
const urlOffset = explicitParsedSelector?.kind === "lines" ? urlRange?.startLine : parsedUrlTarget.offset;
|
|
2170
|
+
const urlLimit =
|
|
2171
|
+
explicitParsedSelector?.kind === "lines" && urlRange
|
|
2172
|
+
? urlRange.endLine !== undefined
|
|
2173
|
+
? urlRange.endLine - urlRange.startLine + 1
|
|
2174
|
+
: undefined
|
|
2175
|
+
: parsedUrlTarget.limit;
|
|
2176
|
+
if (urlOffset !== undefined || urlLimit !== undefined) {
|
|
2152
2177
|
const cached = await loadReadUrlCacheEntry(
|
|
2153
2178
|
this.session,
|
|
2154
|
-
{ path: parsedUrlTarget.path, raw:
|
|
2179
|
+
{ path: parsedUrlTarget.path, raw: urlRaw },
|
|
2155
2180
|
signal,
|
|
2156
2181
|
{
|
|
2157
2182
|
ensureArtifact: true,
|
|
2158
2183
|
preferCached: true,
|
|
2159
2184
|
},
|
|
2160
2185
|
);
|
|
2161
|
-
return this.#buildInMemoryTextResult(cached.output,
|
|
2186
|
+
return this.#buildInMemoryTextResult(cached.output, urlOffset, urlLimit, {
|
|
2162
2187
|
details: { ...cached.details },
|
|
2163
2188
|
sourceUrl: cached.details.finalUrl,
|
|
2164
2189
|
entityLabel: "URL output",
|
|
2165
|
-
raw:
|
|
2190
|
+
raw: urlRaw,
|
|
2166
2191
|
immutable: true,
|
|
2167
2192
|
});
|
|
2168
2193
|
}
|
|
2169
|
-
return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw:
|
|
2194
|
+
return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal);
|
|
2170
2195
|
}
|
|
2171
2196
|
|
|
2172
2197
|
// Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://, omp://, issue://, pr://).
|
|
@@ -2174,8 +2199,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2174
2199
|
// off the URL and surfaced via parseSel rather than confusing handlers.
|
|
2175
2200
|
const internalRouter = InternalUrlRouter.instance();
|
|
2176
2201
|
if (internalRouter.canHandle(readPath)) {
|
|
2177
|
-
const internalTarget =
|
|
2178
|
-
|
|
2202
|
+
const internalTarget =
|
|
2203
|
+
explicitSelector === undefined ? splitInternalUrlSel(readPath) : { path: readPath, sel: explicitSelector };
|
|
2204
|
+
const parsed = explicitParsedSelector ?? parseSel(internalTarget.sel);
|
|
2179
2205
|
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
2180
2206
|
throw new ToolError(
|
|
2181
2207
|
`Invalid selector ':${internalTarget.sel}' on '${internalTarget.path}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
|
|
@@ -2192,7 +2218,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2192
2218
|
skills: this.session.skills,
|
|
2193
2219
|
});
|
|
2194
2220
|
if (localFile) {
|
|
2195
|
-
readPath =
|
|
2221
|
+
readPath = localFile.path;
|
|
2222
|
+
// Promote the URL-embedded selector into the explicit-selector state so
|
|
2223
|
+
// downstream literal-preferring routing does NOT re-split the synthesized
|
|
2224
|
+
// `${localFile.path}:${sel}` string — a sibling literal file at that name
|
|
2225
|
+
// would otherwise shadow the intended local:// URL selector semantics
|
|
2226
|
+
// (issue #4618 reviewer feedback on c493d12).
|
|
2227
|
+
if (explicitSelector === undefined && internalTarget.sel !== undefined) {
|
|
2228
|
+
explicitSelector = internalTarget.sel;
|
|
2229
|
+
explicitParsedSelector = parsed;
|
|
2230
|
+
}
|
|
2196
2231
|
} else {
|
|
2197
2232
|
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2198
2233
|
}
|
|
@@ -2205,48 +2240,68 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2205
2240
|
// resolution share misses instead of re-globbing the workspace.
|
|
2206
2241
|
const suffixCache: SuffixMatchCache = new Map();
|
|
2207
2242
|
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2243
|
+
// Prefer a literal filesystem match over selector interpretation so real
|
|
2244
|
+
// POSIX filenames containing selector-looking suffixes win over structured
|
|
2245
|
+
// archive / sqlite / pdf-image dispatch. With explicit `selector`, `path`
|
|
2246
|
+
// is exact: `path: "test:1-2", selector: "1-2"` means "lines 1-2 from
|
|
2247
|
+
// the literal file test:1-2", without recursively depending on whether a
|
|
2248
|
+
// longer `test:1-2:1-2` filename also exists (issue #4618).
|
|
2249
|
+
const literalSplit =
|
|
2250
|
+
explicitSelector === undefined
|
|
2251
|
+
? await splitPathAndSelPreferringLiteral(readPath, this.session.cwd)
|
|
2252
|
+
: { path: readPath, sel: explicitSelector };
|
|
2253
|
+
const rawPathIsLiteral =
|
|
2254
|
+
explicitSelector !== undefined
|
|
2255
|
+
? readPath.includes(":") && (await probeLiteralPathExists(readPath, this.session.cwd)) !== "missing"
|
|
2256
|
+
: literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
|
|
2257
|
+
|
|
2258
|
+
if (!rawPathIsLiteral) {
|
|
2259
|
+
const archivePath = await this.#resolveArchiveReadPath(readPath, suffixCache, signal);
|
|
2260
|
+
if (archivePath) {
|
|
2261
|
+
const archiveSubPath =
|
|
2262
|
+
explicitSelector === undefined
|
|
2263
|
+
? splitPathAndSel(archivePath.archiveSubPath)
|
|
2264
|
+
: { path: archivePath.archiveSubPath, sel: explicitSelector };
|
|
2265
|
+
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
2266
|
+
return this.#readArchive(
|
|
2267
|
+
readPath,
|
|
2268
|
+
archiveParsed,
|
|
2269
|
+
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
2270
|
+
signal,
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2219
2273
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2274
|
+
const sqlitePath = await this.#resolveSqliteReadPath(readPath, suffixCache, signal);
|
|
2275
|
+
if (sqlitePath) {
|
|
2276
|
+
return this.#readSqlite(sqlitePath, signal);
|
|
2277
|
+
}
|
|
2224
2278
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2279
|
+
const pdfImageMemberPath = splitPdfImageMemberReadPath(readPath);
|
|
2280
|
+
if (pdfImageMemberPath) {
|
|
2281
|
+
let absolutePdfPath = resolveReadPath(pdfImageMemberPath.pdfPath, this.session.cwd);
|
|
2282
|
+
let suffixResolution: { from: string; to: string } | undefined;
|
|
2283
|
+
try {
|
|
2284
|
+
const stat = await Bun.file(absolutePdfPath).stat();
|
|
2285
|
+
if (stat.isDirectory())
|
|
2286
|
+
throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
|
|
2289
|
+
const suffixMatch = await this.#findSuffixMatchCached(suffixCache, pdfImageMemberPath.pdfPath, signal);
|
|
2290
|
+
if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
|
|
2291
|
+
absolutePdfPath = suffixMatch.absolutePath;
|
|
2292
|
+
suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
|
|
2293
|
+
}
|
|
2294
|
+
return this.#readPdfImageMember(
|
|
2295
|
+
absolutePdfPath,
|
|
2296
|
+
pdfImageMemberPath.pdfPath,
|
|
2297
|
+
pdfImageMemberPath.member,
|
|
2298
|
+
suffixResolution,
|
|
2299
|
+
signal,
|
|
2300
|
+
);
|
|
2239
2301
|
}
|
|
2240
|
-
return this.#readPdfImageMember(
|
|
2241
|
-
absolutePdfPath,
|
|
2242
|
-
pdfImageMemberPath.pdfPath,
|
|
2243
|
-
pdfImageMemberPath.member,
|
|
2244
|
-
suffixResolution,
|
|
2245
|
-
signal,
|
|
2246
|
-
);
|
|
2247
2302
|
}
|
|
2248
2303
|
|
|
2249
|
-
const localTarget =
|
|
2304
|
+
const localTarget = literalSplit;
|
|
2250
2305
|
const localReadPath = localTarget.path;
|
|
2251
2306
|
const parsed = parseSel(localTarget.sel);
|
|
2252
2307
|
|
|
@@ -3256,6 +3311,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3256
3311
|
interface ReadRenderArgs {
|
|
3257
3312
|
path?: unknown;
|
|
3258
3313
|
file_path?: unknown;
|
|
3314
|
+
selector?: unknown;
|
|
3259
3315
|
sel?: string;
|
|
3260
3316
|
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
3261
3317
|
offset?: number;
|
|
@@ -3320,10 +3376,20 @@ function formatReadPathLink(
|
|
|
3320
3376
|
|
|
3321
3377
|
export const readToolRenderer = {
|
|
3322
3378
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
3323
|
-
const
|
|
3379
|
+
const baseRawPath =
|
|
3324
3380
|
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3325
|
-
|
|
3326
|
-
|
|
3381
|
+
const explicitSelector =
|
|
3382
|
+
typeof args.selector === "string"
|
|
3383
|
+
? args.selector.trim().replace(/^:+/, "")
|
|
3384
|
+
: args.sel?.trim().replace(/^:+/, "");
|
|
3385
|
+
const rawPath =
|
|
3386
|
+
explicitSelector && explicitSelector.length > 0 ? `${baseRawPath}:${explicitSelector}` : baseRawPath;
|
|
3387
|
+
if (isReadableUrlPath(baseRawPath)) {
|
|
3388
|
+
return renderReadUrlCall(
|
|
3389
|
+
{ path: rawPath, raw: args.raw || explicitSelector?.toLowerCase() === "raw" },
|
|
3390
|
+
_options,
|
|
3391
|
+
uiTheme,
|
|
3392
|
+
);
|
|
3327
3393
|
}
|
|
3328
3394
|
|
|
3329
3395
|
const offset = args.offset;
|
|
@@ -3347,9 +3413,9 @@ export const readToolRenderer = {
|
|
|
3347
3413
|
args?: ReadRenderArgs,
|
|
3348
3414
|
): Component {
|
|
3349
3415
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
3350
|
-
const
|
|
3416
|
+
const baseRawPathForKind =
|
|
3351
3417
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3352
|
-
if (urlDetails?.kind === "url" || isReadableUrlPath(
|
|
3418
|
+
if (urlDetails?.kind === "url" || isReadableUrlPath(baseRawPathForKind)) {
|
|
3353
3419
|
return renderReadUrlResult(
|
|
3354
3420
|
result as {
|
|
3355
3421
|
content: Array<{ type: string; text?: string }>;
|
|
@@ -3364,8 +3430,14 @@ export const readToolRenderer = {
|
|
|
3364
3430
|
if (result.isError) {
|
|
3365
3431
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3366
3432
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
3367
|
-
const
|
|
3433
|
+
const baseRawPath =
|
|
3368
3434
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3435
|
+
const explicitSelector =
|
|
3436
|
+
typeof args?.selector === "string"
|
|
3437
|
+
? args.selector.trim().replace(/^:+/, "")
|
|
3438
|
+
: args?.sel?.trim().replace(/^:+/, "");
|
|
3439
|
+
const rawPath =
|
|
3440
|
+
explicitSelector && explicitSelector.length > 0 ? `${baseRawPath}:${explicitSelector}` : baseRawPath;
|
|
3369
3441
|
const filePath =
|
|
3370
3442
|
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
3371
3443
|
shortenPath(rawPath);
|
|
@@ -3392,8 +3464,14 @@ export const readToolRenderer = {
|
|
|
3392
3464
|
// echo next to the styled warning line below.
|
|
3393
3465
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
3394
3466
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
3395
|
-
const
|
|
3467
|
+
const baseRawPath =
|
|
3396
3468
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3469
|
+
const explicitSelector =
|
|
3470
|
+
typeof args?.selector === "string"
|
|
3471
|
+
? args.selector.trim().replace(/^:+/, "")
|
|
3472
|
+
: args?.sel?.trim().replace(/^:+/, "");
|
|
3473
|
+
const rawPath =
|
|
3474
|
+
explicitSelector && explicitSelector.length > 0 ? `${baseRawPath}:${explicitSelector}` : baseRawPath;
|
|
3397
3475
|
const renderPath = splitReadRenderPath(rawPath);
|
|
3398
3476
|
const lang = getLanguageFromPath(renderPath.path);
|
|
3399
3477
|
|
package/src/tools/renderers.ts
CHANGED
|
@@ -32,6 +32,15 @@ import { sshToolRenderer } from "./ssh";
|
|
|
32
32
|
import { todoToolRenderer } from "./todo";
|
|
33
33
|
import { writeToolRenderer } from "./write";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Per-renderer opt-in for a full viewport replay when the first result
|
|
37
|
+
* replaces a painted pending-call render. A predicate receives the painted
|
|
38
|
+
* call args and render options so the repaint stays scoped to the pending
|
|
39
|
+
* shapes that actually re-anchor (an over-eager replay wipes native
|
|
40
|
+
* scrollback on direct terminals).
|
|
41
|
+
*/
|
|
42
|
+
export type FirstResultViewportRepaint = boolean | ((args: unknown, options: RenderResultOptions) => boolean);
|
|
43
|
+
|
|
35
44
|
export type ToolRenderer = {
|
|
36
45
|
renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
|
|
37
46
|
renderResult: (
|
|
@@ -55,12 +64,11 @@ export type ToolRenderer = {
|
|
|
55
64
|
*/
|
|
56
65
|
animatedPartialResult?: boolean | ((args: unknown) => boolean);
|
|
57
66
|
/**
|
|
58
|
-
* Whether replacing a
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* re-anchors instead of preserving.
|
|
67
|
+
* Whether replacing a pending call render with the first result requires a
|
|
68
|
+
* full viewport repaint. Use for merged renderers whose pending rows can be
|
|
69
|
+
* re-anchored instead of preserved by the result render.
|
|
62
70
|
*/
|
|
63
|
-
forceFirstResultViewportRepaint?:
|
|
71
|
+
forceFirstResultViewportRepaint?: FirstResultViewportRepaint;
|
|
64
72
|
/**
|
|
65
73
|
* Whether settling a provisional partial result into the final render requires
|
|
66
74
|
* a full viewport repaint. Use when the result renderer changes chrome or
|
package/src/tools/ssh.ts
CHANGED
|
@@ -244,6 +244,13 @@ interface SshRenderArgs {
|
|
|
244
244
|
timeout?: number;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/** Whether the painted call args still carry the streamed raw-JSON buffer —
|
|
248
|
+
* the shape that renders the `⏳ SSH: […]` / `$ …` placeholder. */
|
|
249
|
+
function hasStreamedRenderArgs(args: unknown): boolean {
|
|
250
|
+
if (args == null || typeof args !== "object" || !("__partialJson" in args)) return false;
|
|
251
|
+
return typeof args.__partialJson === "string";
|
|
252
|
+
}
|
|
253
|
+
|
|
247
254
|
interface SshRenderContext {
|
|
248
255
|
/** Visual lines for truncated output (pre-computed by tool-execution) */
|
|
249
256
|
visualLines?: string[];
|
|
@@ -388,9 +395,9 @@ export const sshToolRenderer = {
|
|
|
388
395
|
mergeCallAndResult: true,
|
|
389
396
|
// Streamed args can initially render the SSH placeholder (`⏳ SSH: […]` /
|
|
390
397
|
// `$ …`), then the first partial result inserts the `Output` section and
|
|
391
|
-
// re-anchors the frame. Force a full repaint at that
|
|
392
|
-
// do not survive in viewport/native scrollback.
|
|
393
|
-
forceFirstResultViewportRepaint:
|
|
398
|
+
// re-anchors the frame. Force a full repaint only at that streamed-placeholder
|
|
399
|
+
// seam so placeholder rows do not survive in viewport/native scrollback.
|
|
400
|
+
forceFirstResultViewportRepaint: hasStreamedRenderArgs,
|
|
394
401
|
// The provisional pending-result frame settles into the final `⇄ SSH: [host]`
|
|
395
402
|
// frame, so clear/replay the viewport at that topology flip too.
|
|
396
403
|
forceResultViewportRepaintOnSettle: true,
|
package/src/tools/tts.ts
CHANGED
|
@@ -103,7 +103,7 @@ async function synthesizeXai(
|
|
|
103
103
|
content: [
|
|
104
104
|
{
|
|
105
105
|
type: "text",
|
|
106
|
-
text: "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok
|
|
106
|
+
text: "No xAI credentials. Run /login → xAI Grok OAuth (SuperGrok or X Premium+) or set XAI_API_KEY.",
|
|
107
107
|
},
|
|
108
108
|
],
|
|
109
109
|
};
|
package/src/tools/write.ts
CHANGED
|
@@ -985,6 +985,24 @@ function countLines(text: string): number {
|
|
|
985
985
|
return text.split("\n").length;
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
+
/** Bounded newline scan: whether `text` spans more than `maxLines` lines.
|
|
989
|
+
* Runs on every live compose (the repaint predicate below), so it must not
|
|
990
|
+
* materialize the split the way `countLines` does. */
|
|
991
|
+
function exceedsLineCount(text: string, maxLines: number): boolean {
|
|
992
|
+
if (!text) return false;
|
|
993
|
+
let lines = 1;
|
|
994
|
+
for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) {
|
|
995
|
+
if (++lines > maxLines) return true;
|
|
996
|
+
}
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function writeContentOf(args: unknown): string {
|
|
1001
|
+
if (args == null || typeof args !== "object" || !("content" in args)) return "";
|
|
1002
|
+
const content = args.content;
|
|
1003
|
+
return typeof content === "string" ? content : "";
|
|
1004
|
+
}
|
|
1005
|
+
|
|
988
1006
|
function formatLineCountSuffix(lineCount: number, uiTheme: Theme): string {
|
|
989
1007
|
if (lineCount <= 0) return "";
|
|
990
1008
|
return uiTheme.fg("dim", ` · ${lineCount} line${lineCount === 1 ? "" : "s"}`);
|
|
@@ -1218,4 +1236,12 @@ export const writeToolRenderer = {
|
|
|
1218
1236
|
});
|
|
1219
1237
|
},
|
|
1220
1238
|
mergeCallAndResult: true,
|
|
1239
|
+
// The collapsed pending preview follows the streaming edge with a tail
|
|
1240
|
+
// window once the content outgrows it (`… (N earlier lines)` + last rows);
|
|
1241
|
+
// the first partial result re-anchors the frame to the top of the file, so
|
|
1242
|
+
// tail rows already committed to viewport/native scrollback would survive
|
|
1243
|
+
// as stale content above the new frame without a full replay. Expanded and
|
|
1244
|
+
// short previews stay top-anchored and skip the (scrollback-wiping) reset.
|
|
1245
|
+
forceFirstResultViewportRepaint: (args: unknown, options: RenderResultOptions) =>
|
|
1246
|
+
!options.expanded && exceedsLineCount(writeContentOf(args), WRITE_STREAMING_PREVIEW_LINES),
|
|
1221
1247
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** formatLocalCalendarDate formats a Date as YYYY-MM-DD in the host local timezone. */
|
|
2
|
+
export function formatLocalCalendarDate(date: Date = new Date()): string {
|
|
3
|
+
const year = date.getFullYear();
|
|
4
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
5
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
6
|
+
return `${year}-${month}-${day}`;
|
|
7
|
+
}
|
package/src/utils/open.ts
CHANGED
|
@@ -32,22 +32,48 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Resolve the Windows
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* Resolve the Windows opener used to hand a URL/path to the user's registered
|
|
36
|
+
* protocol handler. PowerShell's `Start-Process` goes through ShellExecute
|
|
37
|
+
* like the previous `rundll32 url.dll,FileProtocolHandler`, with two
|
|
38
|
+
* advantages that make the delayed-failure telemetry in {@link openPath}
|
|
39
|
+
* actually observable on Windows:
|
|
40
|
+
*
|
|
41
|
+
* - `rundll32` exits 0 unconditionally, so no launch failure ever reaches the
|
|
42
|
+
* non-zero-exit logging below. `Start-Process` surfaces the failures
|
|
43
|
+
* ShellExecute itself reports — missing target file, no handler executable,
|
|
44
|
+
* access denied — as exit code 1 (verified live: a nonexistent file path
|
|
45
|
+
* exits 1; `$ErrorActionPreference='Stop'` additionally promotes any
|
|
46
|
+
* non-terminating error classes). Known limitation shared by every opener:
|
|
47
|
+
* an unregistered URL scheme exits 0 because Windows "handles" it by
|
|
48
|
+
* offering the app-picker.
|
|
49
|
+
* - `-EncodedCommand` carries the target as a UTF-16LE/base64 payload, so no
|
|
50
|
+
* cmd/PowerShell metacharacter parsing ever sees it (OAuth authorize URLs
|
|
51
|
+
* carry `&`); inside the decoded script the target is a single-quoted
|
|
52
|
+
* literal (no `$` expansion) with embedded quotes doubled.
|
|
53
|
+
*
|
|
54
|
+
* PowerShell is anchored to `%SystemRoot%\System32` for the same reason the
|
|
55
|
+
* previous revision anchored `rundll32`: machine PATHs that dropped
|
|
56
|
+
* `System32` are a real-world occurrence, and bare names throw
|
|
57
|
+
* `Executable not found in $PATH` from `Bun.spawn`. A bare-name fallback
|
|
58
|
+
* remains for exotic SystemRoot layouts.
|
|
43
59
|
*/
|
|
44
60
|
function windowsOpenerCommand(target: string): string[] {
|
|
45
61
|
const systemRoot = process.env.SystemRoot?.trim() || process.env.SYSTEMROOT?.trim() || "C:\\Windows";
|
|
46
62
|
// `path.win32` (not the platform-adaptive `path.join`) keeps Windows path
|
|
47
63
|
// separators when tests run under a POSIX host and matches Windows call
|
|
48
64
|
// conventions on the real target.
|
|
49
|
-
const
|
|
50
|
-
|
|
65
|
+
const absolute = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
66
|
+
const powershell = fs.existsSync(absolute) ? absolute : "powershell.exe";
|
|
67
|
+
const script = `$ErrorActionPreference='Stop';Start-Process '${target.replaceAll("'", "''")}'`;
|
|
68
|
+
return [
|
|
69
|
+
powershell,
|
|
70
|
+
"-NoProfile",
|
|
71
|
+
"-NonInteractive",
|
|
72
|
+
"-WindowStyle",
|
|
73
|
+
"Hidden",
|
|
74
|
+
"-EncodedCommand",
|
|
75
|
+
Buffer.from(script, "utf16le").toString("base64"),
|
|
76
|
+
];
|
|
51
77
|
}
|
|
52
78
|
/** Open a URL or file path in the default browser/application. Best-effort, never throws. */
|
|
53
79
|
export function openPath(urlOrPath: string): void {
|