@oh-my-pi/pi-coding-agent 16.5.1 → 16.5.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.
- package/CHANGELOG.md +65 -0
- package/dist/cli.js +3442 -3408
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +136 -49
- package/src/advisor/runtime.ts +16 -34
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/config-cli.ts +15 -3
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/backend.ts +3 -3
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/prelude.py +28 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +7 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +4 -3
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +9 -2
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +28 -31
- package/src/session/agent-session.ts +44 -22
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
package/src/tools/grep.ts
CHANGED
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
type LineRange,
|
|
49
49
|
parseLineRanges,
|
|
50
50
|
pathTargetsSsh,
|
|
51
|
-
probeLiteralPathExists,
|
|
52
51
|
type ResolvedSearchTarget,
|
|
53
52
|
resolveReadPath,
|
|
54
53
|
resolveToolSearchScope,
|
|
@@ -79,9 +78,6 @@ const searchSchema = type({
|
|
|
79
78
|
"path?": searchPathEntry.describe(
|
|
80
79
|
'file, directory, glob, internal URL, or "<file>:<lines>" selector to search; pass several as a semicolon-delimited list ("src; tests"). Omitted -> searches the workspace root (".")',
|
|
81
80
|
),
|
|
82
|
-
"selector?": type("string").describe(
|
|
83
|
-
'line selector applied to every searched file (e.g. "50-100", "50+10", "50-100,200-300"); never a path like "/"',
|
|
84
|
-
),
|
|
85
81
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
86
82
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
87
83
|
"skip?": type("number")
|
|
@@ -126,7 +122,6 @@ interface GrepPathSpec {
|
|
|
126
122
|
clean: string;
|
|
127
123
|
literalFilesystemMatch?: boolean;
|
|
128
124
|
ranges?: [LineRange, ...LineRange[]];
|
|
129
|
-
rangeSource?: "explicit" | "path";
|
|
130
125
|
}
|
|
131
126
|
|
|
132
127
|
/**
|
|
@@ -154,39 +149,9 @@ function isReadSelectorGrammar(sel: string): boolean {
|
|
|
154
149
|
return lower === "raw" || lower === "conflicts" || parseLineRanges(sel) !== null;
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
async function parsePathSpecs(
|
|
158
|
-
rawEntries: readonly string[],
|
|
159
|
-
cwd: string,
|
|
160
|
-
explicitSelector?: string,
|
|
161
|
-
): Promise<GrepPathSpec[]> {
|
|
162
|
-
const normalizedSelector = explicitSelector?.trim() || undefined;
|
|
163
|
-
const explicitRanges = normalizedSelector === undefined ? undefined : parseLineRanges(normalizedSelector);
|
|
164
|
-
if (normalizedSelector !== undefined && !explicitRanges) {
|
|
165
|
-
throw new ToolError(
|
|
166
|
-
`selector "${normalizedSelector}" is invalid — use line ranges like "50-100", "50+10", or "50-100,200-300" without a leading colon`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
152
|
+
async function parsePathSpecs(rawEntries: readonly string[], cwd: string): Promise<GrepPathSpec[]> {
|
|
169
153
|
const specs: GrepPathSpec[] = [];
|
|
170
154
|
for (const entry of rawEntries) {
|
|
171
|
-
if (explicitRanges) {
|
|
172
|
-
// Separate selector parameter makes `path` deterministic: first try the
|
|
173
|
-
// exact local filesystem path (with read-path normalization), then let
|
|
174
|
-
// archive/internal/URL resolution handle non-literal structured paths.
|
|
175
|
-
const rawPathHasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(entry);
|
|
176
|
-
const probe = rawPathHasScheme ? "missing" : await probeLiteralPathExists(entry, cwd);
|
|
177
|
-
// `"unknown"` covers EACCES/IO where we cannot confirm existence — treat
|
|
178
|
-
// it as a literal so a real file such as `test:1-2` under an unreadable
|
|
179
|
-
// parent is never silently reinterpreted as `test` + selector.
|
|
180
|
-
const literalMatch = probe !== "missing";
|
|
181
|
-
specs.push({
|
|
182
|
-
original: entry,
|
|
183
|
-
clean: literalMatch && !rawPathHasScheme ? resolveReadPath(entry, cwd) : entry,
|
|
184
|
-
literalFilesystemMatch: literalMatch,
|
|
185
|
-
ranges: explicitRanges,
|
|
186
|
-
rangeSource: "explicit",
|
|
187
|
-
});
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
155
|
// Internal URLs (`artifact://`, `skill://`, …) use the URL-aware splitter,
|
|
191
156
|
// which peels selector-shaped tails only for selector-capable schemes and
|
|
192
157
|
// leaves opaque ones (`mcp://`) intact. Unlike filesystem paths, their
|
|
@@ -212,7 +177,6 @@ async function parsePathSpecs(
|
|
|
212
177
|
const literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;
|
|
213
178
|
let clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;
|
|
214
179
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
215
|
-
let rangeSource: "path" | undefined;
|
|
216
180
|
if (!literalFilesystemMatch && split.sel) {
|
|
217
181
|
const parsed = parseLineRanges(split.sel);
|
|
218
182
|
if (!parsed) {
|
|
@@ -225,14 +189,12 @@ async function parsePathSpecs(
|
|
|
225
189
|
}
|
|
226
190
|
clean = split.path;
|
|
227
191
|
ranges = parsed;
|
|
228
|
-
rangeSource = "path";
|
|
229
192
|
}
|
|
230
193
|
specs.push({
|
|
231
194
|
original: entry,
|
|
232
195
|
clean,
|
|
233
196
|
literalFilesystemMatch,
|
|
234
197
|
ranges,
|
|
235
|
-
rangeSource: ranges ? rangeSource : undefined,
|
|
236
198
|
});
|
|
237
199
|
}
|
|
238
200
|
return specs;
|
|
@@ -950,7 +912,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
950
912
|
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
951
913
|
_toolContext?: AgentToolContext,
|
|
952
914
|
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
953
|
-
const { pattern, path: rawPath,
|
|
915
|
+
const { pattern, path: rawPath, case: caseSensitive, gitignore, skip } = params;
|
|
954
916
|
|
|
955
917
|
return untilAborted(signal, async () => {
|
|
956
918
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -968,7 +930,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
968
930
|
const scopedPaths = toPathList(rawPath);
|
|
969
931
|
const effectivePaths = scopedPaths.length > 0 ? scopedPaths : ["."];
|
|
970
932
|
const rawEntries = await expandDelimitedPathEntries(effectivePaths, this.session.cwd);
|
|
971
|
-
const pathSpecs = await parsePathSpecs(rawEntries, this.session.cwd
|
|
933
|
+
const pathSpecs = await parsePathSpecs(rawEntries, this.session.cwd);
|
|
972
934
|
const materializedExternalPaths = new Map<string, string>();
|
|
973
935
|
const materializeExternalUrlForSearch = async (rawPath: string) => {
|
|
974
936
|
const target = parseReadUrlTarget(rawPath);
|
|
@@ -1002,7 +964,6 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1002
964
|
const searchablePaths = internalResolution.paths;
|
|
1003
965
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
1004
966
|
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
1005
|
-
const globalRanges = pathSpecs.find(spec => spec.rangeSource === "explicit")?.ranges;
|
|
1006
967
|
|
|
1007
968
|
if (
|
|
1008
969
|
archiveUnreadable.length > 0 &&
|
|
@@ -1062,7 +1023,6 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1062
1023
|
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
1063
1024
|
const spec = pathSpecs[idx];
|
|
1064
1025
|
if (!spec.ranges) continue;
|
|
1065
|
-
if (spec.rangeSource === "explicit") continue;
|
|
1066
1026
|
if (virtualInputIndexes.has(idx)) continue;
|
|
1067
1027
|
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
1068
1028
|
if (!resolved) continue;
|
|
@@ -1268,11 +1228,11 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1268
1228
|
throw err;
|
|
1269
1229
|
}
|
|
1270
1230
|
result = mergeGrepResults(result, virtualResult, nativeMaxCount);
|
|
1271
|
-
if (rangesByAbsPath.size > 0
|
|
1231
|
+
if (rangesByAbsPath.size > 0) {
|
|
1272
1232
|
const filteredMatches: GrepMatch[] = [];
|
|
1273
1233
|
for (const match of result.matches) {
|
|
1274
1234
|
const abs = matchAbsolutePath(match.path, searchPath);
|
|
1275
|
-
const ranges = rangesByAbsPath.get(abs)
|
|
1235
|
+
const ranges = rangesByAbsPath.get(abs);
|
|
1276
1236
|
if (!ranges) {
|
|
1277
1237
|
// Path has no line-range constraint (e.g. a peer entry without `:N-M`).
|
|
1278
1238
|
filteredMatches.push(match);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -147,8 +147,14 @@ export function expandTilde(filePath: string, home?: string): string {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function expandPath(filePath: string): string {
|
|
150
|
+
// Some models intermittently prefix an otherwise-valid path with a stray
|
|
151
|
+
// `:` (e.g. `:/abs/path`, `:../rel`). No real path starts with `:` and it
|
|
152
|
+
// never begins a selector against an absolute/relative path, so strip it
|
|
153
|
+
// before resolution — mirroring the `@`-prefix normalization above and the
|
|
154
|
+
// implicit stripping `write` already tolerates (issue #5508).
|
|
155
|
+
const deColoned = /^:(?=[/~]|\.\.?\/)/.test(filePath) ? filePath.slice(1) : filePath;
|
|
150
156
|
const normalized = stripWindowsExtendedLengthPathPrefix(
|
|
151
|
-
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(
|
|
157
|
+
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(deColoned))),
|
|
152
158
|
);
|
|
153
159
|
return expandTilde(normalized);
|
|
154
160
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -748,10 +748,7 @@ function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; membe
|
|
|
748
748
|
|
|
749
749
|
const readSchema = type({
|
|
750
750
|
path: type("string").describe(
|
|
751
|
-
|
|
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',
|
|
751
|
+
"Local path, internal URI (e.g. memory://, skill://), or URL. Inline selectors are supported.",
|
|
755
752
|
),
|
|
756
753
|
});
|
|
757
754
|
|
|
@@ -2122,11 +2119,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2122
2119
|
_toolContext?: AgentToolContext,
|
|
2123
2120
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2124
2121
|
let { path: readPath } = params;
|
|
2125
|
-
let explicitSelector = params.selector?.trim() || undefined;
|
|
2126
|
-
let explicitParsedSelector = explicitSelector === undefined ? undefined : parseSel(explicitSelector);
|
|
2127
|
-
if (explicitSelector !== undefined && explicitParsedSelector?.kind === "none") {
|
|
2128
|
-
throw invalidSelector(explicitSelector);
|
|
2129
|
-
}
|
|
2130
2122
|
if (readPath.startsWith("file://")) {
|
|
2131
2123
|
readPath = expandPath(readPath);
|
|
2132
2124
|
}
|
|
@@ -2147,13 +2139,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2147
2139
|
if (!this.session.settings.get("fetch.enabled")) {
|
|
2148
2140
|
throw new ToolError("URL reads are disabled by settings.");
|
|
2149
2141
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
const urlRaw =
|
|
2154
|
-
explicitParsedSelector === undefined ? parsedUrlTarget.raw : isRawSelector(explicitParsedSelector);
|
|
2155
|
-
const urlRanges =
|
|
2156
|
-
explicitParsedSelector?.kind === "lines" ? explicitParsedSelector.ranges : parsedUrlTarget.ranges;
|
|
2142
|
+
const urlRaw = parsedUrlTarget.raw;
|
|
2143
|
+
const urlRanges = parsedUrlTarget.ranges;
|
|
2157
2144
|
if (urlRanges !== undefined && urlRanges.length > 1) {
|
|
2158
2145
|
const cached = await loadReadUrlCacheEntry(
|
|
2159
2146
|
this.session,
|
|
@@ -2169,14 +2156,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2169
2156
|
immutable: true,
|
|
2170
2157
|
});
|
|
2171
2158
|
}
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
const urlLimit =
|
|
2175
|
-
explicitParsedSelector?.kind === "lines" && urlRange
|
|
2176
|
-
? urlRange.endLine !== undefined
|
|
2177
|
-
? urlRange.endLine - urlRange.startLine + 1
|
|
2178
|
-
: undefined
|
|
2179
|
-
: parsedUrlTarget.limit;
|
|
2159
|
+
const urlOffset = parsedUrlTarget.offset;
|
|
2160
|
+
const urlLimit = parsedUrlTarget.limit;
|
|
2180
2161
|
if (urlOffset !== undefined || urlLimit !== undefined) {
|
|
2181
2162
|
const cached = await loadReadUrlCacheEntry(
|
|
2182
2163
|
this.session,
|
|
@@ -2202,10 +2183,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2202
2183
|
// Use the internal-URL-aware splitter so malformed selectors are peeled
|
|
2203
2184
|
// off the URL and surfaced via parseSel rather than confusing handlers.
|
|
2204
2185
|
const internalRouter = InternalUrlRouter.instance();
|
|
2186
|
+
let promotedSelector: string | undefined;
|
|
2205
2187
|
if (internalRouter.canHandle(readPath)) {
|
|
2206
|
-
const internalTarget =
|
|
2207
|
-
|
|
2208
|
-
const parsed = explicitParsedSelector ?? parseSel(internalTarget.sel);
|
|
2188
|
+
const internalTarget = splitInternalUrlSel(readPath);
|
|
2189
|
+
const parsed = parseSel(internalTarget.sel);
|
|
2209
2190
|
if (internalTarget.sel !== undefined && parsed.kind === "none") {
|
|
2210
2191
|
throw new ToolError(
|
|
2211
2192
|
`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).`,
|
|
@@ -2223,15 +2204,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2223
2204
|
});
|
|
2224
2205
|
if (localFile) {
|
|
2225
2206
|
readPath = localFile.path;
|
|
2226
|
-
//
|
|
2227
|
-
//
|
|
2228
|
-
|
|
2229
|
-
// would otherwise shadow the intended local:// URL selector semantics
|
|
2230
|
-
// (issue #4618 reviewer feedback on c493d12).
|
|
2231
|
-
if (explicitSelector === undefined && internalTarget.sel !== undefined) {
|
|
2232
|
-
explicitSelector = internalTarget.sel;
|
|
2233
|
-
explicitParsedSelector = parsed;
|
|
2234
|
-
}
|
|
2207
|
+
// Preserve a local:// selector separately so a sibling literal file
|
|
2208
|
+
// cannot shadow the URL's selector semantics during filesystem routing.
|
|
2209
|
+
promotedSelector = internalTarget.sel;
|
|
2235
2210
|
} else {
|
|
2236
2211
|
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2237
2212
|
}
|
|
@@ -2246,16 +2221,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2246
2221
|
|
|
2247
2222
|
// Prefer a literal filesystem match over selector interpretation so real
|
|
2248
2223
|
// POSIX filenames containing selector-looking suffixes win over structured
|
|
2249
|
-
// archive / sqlite / pdf-image dispatch.
|
|
2250
|
-
//
|
|
2251
|
-
// the literal file test:1-2", without recursively depending on whether a
|
|
2252
|
-
// longer `test:1-2:1-2` filename also exists (issue #4618).
|
|
2224
|
+
// archive / sqlite / pdf-image dispatch. A selector promoted from local://
|
|
2225
|
+
// remains separate so it cannot be mistaken for part of the resolved path.
|
|
2253
2226
|
const literalSplit =
|
|
2254
|
-
|
|
2227
|
+
promotedSelector === undefined
|
|
2255
2228
|
? await splitPathAndSelPreferringLiteral(readPath, this.session.cwd)
|
|
2256
|
-
: { path: readPath, sel:
|
|
2229
|
+
: { path: readPath, sel: promotedSelector };
|
|
2257
2230
|
const rawPathIsLiteral =
|
|
2258
|
-
|
|
2231
|
+
promotedSelector !== undefined
|
|
2259
2232
|
? readPath.includes(":") && (await probeLiteralPathExists(readPath, this.session.cwd)) !== "missing"
|
|
2260
2233
|
: literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
|
|
2261
2234
|
|
|
@@ -2263,9 +2236,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2263
2236
|
const archivePath = await this.#resolveArchiveReadPath(readPath, suffixCache, signal);
|
|
2264
2237
|
if (archivePath) {
|
|
2265
2238
|
const archiveSubPath =
|
|
2266
|
-
|
|
2239
|
+
promotedSelector === undefined
|
|
2267
2240
|
? splitPathAndSel(archivePath.archiveSubPath)
|
|
2268
|
-
: { path: archivePath.archiveSubPath, sel:
|
|
2241
|
+
: { path: archivePath.archiveSubPath, sel: promotedSelector };
|
|
2269
2242
|
const archiveParsed = parseSel(archiveSubPath.sel);
|
|
2270
2243
|
return this.#readArchive(
|
|
2271
2244
|
readPath,
|
|
@@ -3319,8 +3292,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3319
3292
|
interface ReadRenderArgs {
|
|
3320
3293
|
path?: unknown;
|
|
3321
3294
|
file_path?: unknown;
|
|
3322
|
-
selector?: unknown;
|
|
3323
|
-
sel?: string;
|
|
3324
3295
|
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
3325
3296
|
offset?: number;
|
|
3326
3297
|
limit?: number;
|
|
@@ -3384,20 +3355,10 @@ function formatReadPathLink(
|
|
|
3384
3355
|
|
|
3385
3356
|
export const readToolRenderer = {
|
|
3386
3357
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
3387
|
-
const baseRawPath =
|
|
3388
|
-
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3389
|
-
const explicitSelector =
|
|
3390
|
-
typeof args.selector === "string"
|
|
3391
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3392
|
-
: args.sel?.trim().replace(/^:+/, "");
|
|
3393
3358
|
const rawPath =
|
|
3394
|
-
|
|
3395
|
-
if (isReadableUrlPath(
|
|
3396
|
-
return renderReadUrlCall(
|
|
3397
|
-
{ path: rawPath, raw: args.raw || explicitSelector?.toLowerCase() === "raw" },
|
|
3398
|
-
_options,
|
|
3399
|
-
uiTheme,
|
|
3400
|
-
);
|
|
3359
|
+
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3360
|
+
if (isReadableUrlPath(rawPath)) {
|
|
3361
|
+
return renderReadUrlCall({ path: rawPath, raw: args.raw }, _options, uiTheme);
|
|
3401
3362
|
}
|
|
3402
3363
|
|
|
3403
3364
|
const offset = args.offset;
|
|
@@ -3438,14 +3399,8 @@ export const readToolRenderer = {
|
|
|
3438
3399
|
if (result.isError) {
|
|
3439
3400
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3440
3401
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
3441
|
-
const baseRawPath =
|
|
3442
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3443
|
-
const explicitSelector =
|
|
3444
|
-
typeof args?.selector === "string"
|
|
3445
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3446
|
-
: args?.sel?.trim().replace(/^:+/, "");
|
|
3447
3402
|
const rawPath =
|
|
3448
|
-
|
|
3403
|
+
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3449
3404
|
const filePath =
|
|
3450
3405
|
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
3451
3406
|
shortenPath(rawPath);
|
|
@@ -3472,14 +3427,8 @@ export const readToolRenderer = {
|
|
|
3472
3427
|
// echo next to the styled warning line below.
|
|
3473
3428
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
3474
3429
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
3475
|
-
const baseRawPath =
|
|
3476
|
-
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3477
|
-
const explicitSelector =
|
|
3478
|
-
typeof args?.selector === "string"
|
|
3479
|
-
? args.selector.trim().replace(/^:+/, "")
|
|
3480
|
-
: args?.sel?.trim().replace(/^:+/, "");
|
|
3481
3430
|
const rawPath =
|
|
3482
|
-
|
|
3431
|
+
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3483
3432
|
const renderPath = splitReadRenderPath(rawPath);
|
|
3484
3433
|
const lang = getLanguageFromPath(renderPath.path);
|
|
3485
3434
|
|
|
@@ -40,6 +40,8 @@ const THINKING_TAG_ENVELOPE_RE = /<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>/
|
|
|
40
40
|
const THINKING_FENCE_ENVELOPE_RE = /```(?:thinking|reasoning)\b[\s\S]*?```/gi;
|
|
41
41
|
const LEADING_THINKING_TAG_RE = /^\s*<(think|thinking|reasoning)>\s*[\s\S]*?<\/\1>\s*/i;
|
|
42
42
|
const LEADING_THINKING_FENCE_RE = /^\s*```(?:thinking|reasoning)\b[\s\S]*?```\s*/i;
|
|
43
|
+
const LEADING_PROSE_THINKING_PREAMBLE_RE =
|
|
44
|
+
/^[ \t]*(?:(?:here(?:['’]s| is)[ \t]+(?:a|the|my)[ \t]+)|my[ \t]+)?(?:thinking|thought|reasoning)[ \t]+process[ \t]*:?[ \t]*(?:\r?\n|$)/i;
|
|
43
45
|
|
|
44
46
|
function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel?: Model<Api>): Model<Api> | undefined {
|
|
45
47
|
const availableModels = registry.getAvailable();
|
|
@@ -250,13 +252,15 @@ function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): stri
|
|
|
250
252
|
}
|
|
251
253
|
// Stay lenient: prefer the first closed title marker in visible text, then
|
|
252
254
|
// fall back to a plain sentence after stripping only known leading leaked
|
|
253
|
-
// thinking envelopes plus any stray/unclosed title tag fragment.
|
|
255
|
+
// thinking envelopes plus any stray/unclosed title tag fragment. Reject a
|
|
256
|
+
// prose thinking preamble only on the markerless path: a later marked title
|
|
257
|
+
// remains authoritative.
|
|
254
258
|
const markedTitle = extractVisibleMarkedTitle(textTitle);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
259
|
+
if (markedTitle !== undefined) return unwrapJsonTitle(markedTitle);
|
|
260
|
+
const cleanedTextTitle = stripLeadingLeakedThinkingMarkup(textTitle)
|
|
261
|
+
.replace(/<\/?title>/gi, "")
|
|
262
|
+
.trim();
|
|
263
|
+
if (LEADING_PROSE_THINKING_PREAMBLE_RE.test(cleanedTextTitle)) return "";
|
|
260
264
|
return unwrapJsonTitle(cleanedTextTitle);
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -44,17 +44,26 @@ export async function getApiConfigs(
|
|
|
44
44
|
const useResponses = $env.PI_PERPLEXITY_RESPONSES === "1";
|
|
45
45
|
const configs: ApiConfig[] = [];
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
// A Perplexity OAuth session and a real API key are mutually exclusive here:
|
|
48
|
+
// when the active credential origin is OAuth, `getApiKey("perplexity")`
|
|
49
|
+
// returns the OAuth session JWT (OAuth wins in AuthStorage.getApiKey), not an
|
|
50
|
+
// api.perplexity.ai key. Emitting it as a direct api-key config makes the
|
|
51
|
+
// search loop send the session token as a Bearer to the direct API endpoint,
|
|
52
|
+
// which rejects it with 401 and masks the real (transport) failure — see #5315.
|
|
53
|
+
// Skip the direct config in that case; the OAuth ask-endpoint method covers it.
|
|
54
|
+
if (authStorage.getCredentialOrigin("perplexity")?.kind !== "oauth") {
|
|
55
|
+
const perplexityKey = await authStorage.getApiKey("perplexity", sessionId, options);
|
|
56
|
+
if (perplexityKey) {
|
|
57
|
+
configs.push({
|
|
58
|
+
type: "api_key",
|
|
59
|
+
apiKey: perplexityKey,
|
|
60
|
+
provider: "perplexity",
|
|
61
|
+
chatBaseUrl: PERPLEXITY_CHAT_BASE_URL,
|
|
62
|
+
responsesBaseUrl: PERPLEXITY_RESPONSES_BASE_URL,
|
|
63
|
+
modelPrefix: "",
|
|
64
|
+
useResponses,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
const openrouterKey = await authStorage.getApiKey("openrouter", sessionId, options);
|
|
@@ -604,7 +604,7 @@ async function callPerplexityAsk(
|
|
|
604
604
|
requestParams.send_back_text_in_streaming_api = true;
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
-
const
|
|
607
|
+
const requestInit = {
|
|
608
608
|
method: "POST",
|
|
609
609
|
headers,
|
|
610
610
|
body: JSON.stringify({
|
|
@@ -612,7 +612,19 @@ async function callPerplexityAsk(
|
|
|
612
612
|
params: requestParams,
|
|
613
613
|
}),
|
|
614
614
|
signal: withHardTimeout(params.signal),
|
|
615
|
-
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// The consumer ask endpoint intermittently drops the socket before sending an
|
|
618
|
+
// HTTP response (#5315). Retry the transport exactly once; once we hold an
|
|
619
|
+
// HTTP response (handled below) the outcome — including non-2xx — is final and
|
|
620
|
+
// never retried, so a real 401/429 is never papered over by a second attempt.
|
|
621
|
+
let response: Response;
|
|
622
|
+
try {
|
|
623
|
+
response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, requestInit);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
if (params.signal?.aborted) throw error;
|
|
626
|
+
response = await (params.fetch ?? fetch)(PERPLEXITY_OAUTH_ASK_URL, requestInit);
|
|
627
|
+
}
|
|
616
628
|
|
|
617
629
|
if (!response.ok) {
|
|
618
630
|
const errorText = await response.text();
|