@oh-my-pi/pi-coding-agent 16.3.12 → 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 +16 -0
- package/dist/cli.js +3244 -3246
- package/dist/types/config/keybindings.d.ts +9 -4
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- 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/read-tool-group.d.ts +1 -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/session/agent-session.d.ts +3 -4
- package/dist/types/tools/read.d.ts +1 -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/package.json +12 -12
- package/src/config/keybindings.ts +62 -10
- package/src/config/model-registry.ts +85 -20
- package/src/config/settings.ts +48 -21
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- 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/read-tool-group.ts +5 -1
- package/src/modes/components/tool-execution.ts +28 -24
- package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +5 -55
- package/src/modes/interactive-mode.ts +43 -2
- 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/prompts/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/session/agent-session.ts +8 -5
- package/src/tools/grep.ts +58 -13
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/read.ts +33 -13
- 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
|
@@ -88,6 +88,7 @@ export type RpcSkillCommandResult = { agentInvoked: true };
|
|
|
88
88
|
export async function tryRunRpcSkillCommand(
|
|
89
89
|
session: RpcSkillCommandSession,
|
|
90
90
|
text: string,
|
|
91
|
+
streamingBehavior: "steer" | "followUp" = "steer",
|
|
91
92
|
): Promise<RpcSkillCommandResult | false> {
|
|
92
93
|
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
93
94
|
const parsed = parseSkillInvocation(text);
|
|
@@ -95,13 +96,16 @@ export async function tryRunRpcSkillCommand(
|
|
|
95
96
|
const skill = session.skills.find(candidate => candidate.name === parsed.name);
|
|
96
97
|
if (!skill) return false;
|
|
97
98
|
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
|
98
|
-
await session.promptCustomMessage(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
await session.promptCustomMessage(
|
|
100
|
+
{
|
|
101
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
102
|
+
content: built.message,
|
|
103
|
+
display: true,
|
|
104
|
+
details: built.details,
|
|
105
|
+
attribution: "user",
|
|
106
|
+
},
|
|
107
|
+
{ streamingBehavior },
|
|
108
|
+
);
|
|
105
109
|
return { agentInvoked: true };
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -755,6 +759,10 @@ export async function runRpcMode(
|
|
|
755
759
|
return requestRpcEditor(this.pendingRequests, this.output, title, prefill, dialogOptions, editorOptions);
|
|
756
760
|
}
|
|
757
761
|
|
|
762
|
+
addAutocompleteProvider(): void {
|
|
763
|
+
// Autocomplete provider composition is not supported in RPC mode
|
|
764
|
+
}
|
|
765
|
+
|
|
758
766
|
get theme(): Theme {
|
|
759
767
|
return theme;
|
|
760
768
|
}
|
|
@@ -842,7 +850,7 @@ export async function runRpcMode(
|
|
|
842
850
|
// =================================================================
|
|
843
851
|
|
|
844
852
|
case "prompt": {
|
|
845
|
-
const skillResult = await tryRunRpcSkillCommand(session, command.message);
|
|
853
|
+
const skillResult = await tryRunRpcSkillCommand(session, command.message, command.streamingBehavior);
|
|
846
854
|
if (skillResult) {
|
|
847
855
|
return success(id, "prompt", skillResult);
|
|
848
856
|
}
|
|
@@ -1198,11 +1206,9 @@ export async function runRpcMode(
|
|
|
1198
1206
|
return error(id, "login", `Unknown OAuth provider: ${command.providerId}`);
|
|
1199
1207
|
}
|
|
1200
1208
|
const uiCtx = new RpcExtensionUIContext(pendingExtensionRequests, output);
|
|
1201
|
-
// Track whether onAuth has fired. Providers that
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
// GitHub Enterprise URL, device-code entry) call onPrompt before onAuth.
|
|
1205
|
-
// We use this ordering to self-classify at runtime — no static allowlist.
|
|
1209
|
+
// Track whether onAuth has fired. Providers that require interactive
|
|
1210
|
+
// input before a browser URL cannot be satisfied headlessly; after
|
|
1211
|
+
// onAuth, prompt input is the pasted OAuth code/redirect URL path.
|
|
1206
1212
|
let authEmitted = false;
|
|
1207
1213
|
try {
|
|
1208
1214
|
await session.modelRegistry.authStorage.login(command.providerId, {
|
|
@@ -1220,7 +1226,7 @@ export async function runRpcMode(
|
|
|
1220
1226
|
onProgress: message => {
|
|
1221
1227
|
uiCtx.notify(message, "info");
|
|
1222
1228
|
},
|
|
1223
|
-
onPrompt:
|
|
1229
|
+
onPrompt: async prompt => {
|
|
1224
1230
|
if (!authEmitted) {
|
|
1225
1231
|
// onPrompt called before any auth URL — provider requires
|
|
1226
1232
|
// interactive input that cannot be satisfied headlessly.
|
|
@@ -1231,11 +1237,7 @@ export async function runRpcMode(
|
|
|
1231
1237
|
),
|
|
1232
1238
|
);
|
|
1233
1239
|
}
|
|
1234
|
-
|
|
1235
|
-
// manual-redirect fallback race. Returning a never-settling promise
|
|
1236
|
-
// lets the race block until the callback server wins; a rejection
|
|
1237
|
-
// would be caught as null and spin the while(true) loop.
|
|
1238
|
-
return new Promise<string>(() => {});
|
|
1240
|
+
return (await uiCtx.input(prompt.message, prompt.placeholder, { timeout: 600_000 })) ?? "";
|
|
1239
1241
|
},
|
|
1240
1242
|
});
|
|
1241
1243
|
await session.modelRegistry.refresh();
|
package/src/modes/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { CollabHost } from "../collab/host";
|
|
|
7
7
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
8
8
|
import type { Settings } from "../config/settings";
|
|
9
9
|
import type {
|
|
10
|
+
AutocompleteProviderFactory,
|
|
10
11
|
ExtensionUIContext,
|
|
11
12
|
ExtensionUIDialogOptions,
|
|
12
13
|
ExtensionUISelectItem,
|
|
@@ -218,6 +219,8 @@ export interface InteractiveModeContext {
|
|
|
218
219
|
// Extension UI integration
|
|
219
220
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
220
221
|
initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
222
|
+
/** Stack extension autocomplete behavior on top of the built-in editor provider. */
|
|
223
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
|
221
224
|
setEditorComponent(
|
|
222
225
|
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
|
|
223
226
|
): void;
|
|
@@ -4,7 +4,6 @@ description: Software architect for complex multi-file architectural decisions.
|
|
|
4
4
|
tools: read, grep, glob, bash, lsp, web_search, ast_grep
|
|
5
5
|
spawns: explore
|
|
6
6
|
model: pi/plan, pi/slow
|
|
7
|
-
thinking-level: high
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
Analyze the codebase and the user's request. Produce a detailed implementation plan.
|
|
@@ -2,7 +2,8 @@ Greps files using regex.
|
|
|
2
2
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Rust regex (RE2-style): alternation is `foo|bar`, not GNU BRE-style `foo\|bar`; Rust word boundaries like `\bword\b` are supported. Use line anchors or post-filters instead of lookaround/backreferences.
|
|
5
|
-
- `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`).
|
|
5
|
+
- `path`: SHOULD scope to a known path (e.g. `src`); pass several as a delimited list (`src; tests`). Use `selector` only for line-number filtering, never path/root selection (`"/"` belongs in `path`).
|
|
6
|
+
- Literal colon filename + line range? Use `selector` (e.g. `{"path":"test:1-2","selector":"1-2"}`), not recursive `path:"test:1-2:1-2"`.
|
|
6
7
|
- Cross-line patterns detected from literal `\n` or `\\n` in `pattern`.
|
|
7
8
|
</instruction>
|
|
8
9
|
|
|
@@ -5,6 +5,8 @@ Use only with ids returned by the `recall` tool. Operations:
|
|
|
5
5
|
- `forget`: permanently delete a working memory.
|
|
6
6
|
- `invalidate`: softly supersede a working or episodic memory, optionally pointing at `replacement_id`.
|
|
7
7
|
|
|
8
|
+
Fact ids (recall results marked `[facts]`) are read-only: inspect them with `read memory://<id>`; every edit op on a fact id returns `not_editable`.
|
|
9
|
+
|
|
8
10
|
Prefer `invalidate` when a memory became stale but its history may still be useful. Use `forget` only for content that should be hard-deleted.
|
|
9
11
|
|
|
10
12
|
**Always read the full memory before `update`.** Recall results are clipped previews (the trailing `…` marks a truncation and `full_length` reports the original size); `update` replaces content wholesale, so overwriting the preview would delete the unseen tail. Fetch the row first with `read memory://<id>`, then pass the merged content in `content`.
|
|
@@ -1180,6 +1180,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
1180
1180
|
pasteToEditor: () => {},
|
|
1181
1181
|
getEditorText: () => "",
|
|
1182
1182
|
editor: async () => undefined,
|
|
1183
|
+
addAutocompleteProvider: () => {},
|
|
1183
1184
|
get theme() {
|
|
1184
1185
|
return theme;
|
|
1185
1186
|
},
|
|
@@ -8309,11 +8310,10 @@ export class AgentSession {
|
|
|
8309
8310
|
}
|
|
8310
8311
|
|
|
8311
8312
|
/**
|
|
8312
|
-
* Send a user message
|
|
8313
|
-
* When deliverAs is set, queue the message instead of starting a new turn.
|
|
8313
|
+
* Send a user message through the prompt flow.
|
|
8314
8314
|
*
|
|
8315
|
-
*
|
|
8316
|
-
*
|
|
8315
|
+
* Omitted `deliverAs` starts a turn when idle and queues as a steer while streaming.
|
|
8316
|
+
* Explicit `deliverAs` queues without starting a turn in either state.
|
|
8317
8317
|
*/
|
|
8318
8318
|
async sendUserMessage(
|
|
8319
8319
|
content: string | (TextContent | ImageContent)[],
|
|
@@ -8348,10 +8348,13 @@ export class AgentSession {
|
|
|
8348
8348
|
return;
|
|
8349
8349
|
}
|
|
8350
8350
|
|
|
8351
|
-
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
|
|
8351
|
+
// Use prompt() with expandPromptTemplates: false to skip command handling and template expansion.
|
|
8352
|
+
// `streamingBehavior: "steer"` preserves prompt-flow side effects during streaming while
|
|
8353
|
+
// covering the narrow race where a stream starts before prompt() acquires the turn.
|
|
8352
8354
|
await this.prompt(text, {
|
|
8353
8355
|
expandPromptTemplates: false,
|
|
8354
8356
|
images,
|
|
8357
|
+
streamingBehavior: "steer",
|
|
8355
8358
|
});
|
|
8356
8359
|
}
|
|
8357
8360
|
|
package/src/tools/grep.ts
CHANGED
|
@@ -80,7 +80,7 @@ const searchSchema = type({
|
|
|
80
80
|
'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
81
|
),
|
|
82
82
|
"selector?": type("string").describe(
|
|
83
|
-
'line selector
|
|
83
|
+
'line selector applied to every searched file (e.g. "50-100", "50+10", "50-100,200-300"); never a path like "/"',
|
|
84
84
|
),
|
|
85
85
|
"case?": type("boolean").describe("case-sensitive search"),
|
|
86
86
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
@@ -126,6 +126,7 @@ interface GrepPathSpec {
|
|
|
126
126
|
clean: string;
|
|
127
127
|
literalFilesystemMatch?: boolean;
|
|
128
128
|
ranges?: [LineRange, ...LineRange[]];
|
|
129
|
+
rangeSource?: "explicit" | "path";
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/**
|
|
@@ -158,11 +159,11 @@ async function parsePathSpecs(
|
|
|
158
159
|
cwd: string,
|
|
159
160
|
explicitSelector?: string,
|
|
160
161
|
): Promise<GrepPathSpec[]> {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
if (
|
|
162
|
+
const normalizedSelector = explicitSelector?.trim() || undefined;
|
|
163
|
+
const explicitRanges = normalizedSelector === undefined ? undefined : parseLineRanges(normalizedSelector);
|
|
164
|
+
if (normalizedSelector !== undefined && !explicitRanges) {
|
|
164
165
|
throw new ToolError(
|
|
165
|
-
`selector "${
|
|
166
|
+
`selector "${normalizedSelector}" is invalid — use line ranges like "50-100", "50+10", or "50-100,200-300" without a leading colon`,
|
|
166
167
|
);
|
|
167
168
|
}
|
|
168
169
|
const specs: GrepPathSpec[] = [];
|
|
@@ -182,6 +183,7 @@ async function parsePathSpecs(
|
|
|
182
183
|
clean: literalMatch && !rawPathHasScheme ? resolveReadPath(entry, cwd) : entry,
|
|
183
184
|
literalFilesystemMatch: literalMatch,
|
|
184
185
|
ranges: explicitRanges,
|
|
186
|
+
rangeSource: "explicit",
|
|
185
187
|
});
|
|
186
188
|
continue;
|
|
187
189
|
}
|
|
@@ -210,6 +212,7 @@ async function parsePathSpecs(
|
|
|
210
212
|
const literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;
|
|
211
213
|
let clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;
|
|
212
214
|
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
215
|
+
let rangeSource: "path" | undefined;
|
|
213
216
|
if (!literalFilesystemMatch && split.sel) {
|
|
214
217
|
const parsed = parseLineRanges(split.sel);
|
|
215
218
|
if (!parsed) {
|
|
@@ -222,8 +225,15 @@ async function parsePathSpecs(
|
|
|
222
225
|
}
|
|
223
226
|
clean = split.path;
|
|
224
227
|
ranges = parsed;
|
|
228
|
+
rangeSource = "path";
|
|
225
229
|
}
|
|
226
|
-
specs.push({
|
|
230
|
+
specs.push({
|
|
231
|
+
original: entry,
|
|
232
|
+
clean,
|
|
233
|
+
literalFilesystemMatch,
|
|
234
|
+
ranges,
|
|
235
|
+
rangeSource: ranges ? rangeSource : undefined,
|
|
236
|
+
});
|
|
227
237
|
}
|
|
228
238
|
return specs;
|
|
229
239
|
}
|
|
@@ -400,6 +410,27 @@ function lineAllowed(lineNumber: number, ranges: readonly LineRange[] | undefine
|
|
|
400
410
|
return !ranges || isLineInRanges(lineNumber, ranges);
|
|
401
411
|
}
|
|
402
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Per-file native fetch budget that guarantees the JS range filter can still
|
|
415
|
+
* surface `perFileKeep` in-range hits. Matches arrive one entry per matched
|
|
416
|
+
* line in line order, so a bounded range's hits all sit within the first
|
|
417
|
+
* `endLine` entries, and an open-ended range starting at S is preceded by at
|
|
418
|
+
* most S-1 out-of-range entries — S-1+perFileKeep entries cover the kept
|
|
419
|
+
* window or exhaust the file. Clamped to the native file-size ceiling (a
|
|
420
|
+
* ≤4 MiB file cannot have more matched lines than bytes), which also keeps
|
|
421
|
+
* the scaled global budget inside the native layer's u32 bounds.
|
|
422
|
+
*/
|
|
423
|
+
function lineRangeFetchCap(pathSpecs: readonly GrepPathSpec[], perFileKeep: number): number {
|
|
424
|
+
let cap = 0;
|
|
425
|
+
for (const spec of pathSpecs) {
|
|
426
|
+
if (!spec.ranges) continue;
|
|
427
|
+
for (const range of spec.ranges) {
|
|
428
|
+
cap = Math.max(cap, range.endLine ?? range.startLine - 1 + perFileKeep);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return Math.min(cap, NATIVE_GREP_MAX_FILE_BYTES);
|
|
432
|
+
}
|
|
433
|
+
|
|
403
434
|
/** Binary search for the index of the line containing byte `offset`. */
|
|
404
435
|
function findLineIndex(starts: readonly number[], offset: number): number {
|
|
405
436
|
if (starts.length === 0) return -1;
|
|
@@ -971,6 +1002,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
971
1002
|
const searchablePaths = internalResolution.paths;
|
|
972
1003
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
973
1004
|
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
1005
|
+
const globalRanges = pathSpecs.find(spec => spec.rangeSource === "explicit")?.ranges;
|
|
974
1006
|
|
|
975
1007
|
if (
|
|
976
1008
|
archiveUnreadable.length > 0 &&
|
|
@@ -1030,6 +1062,7 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1030
1062
|
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
1031
1063
|
const spec = pathSpecs[idx];
|
|
1032
1064
|
if (!spec.ranges) continue;
|
|
1065
|
+
if (spec.rangeSource === "explicit") continue;
|
|
1033
1066
|
if (virtualInputIndexes.has(idx)) continue;
|
|
1034
1067
|
const resolved = internalResolution.resolvedPathsByInput[idx];
|
|
1035
1068
|
if (!resolved) continue;
|
|
@@ -1095,6 +1128,18 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1095
1128
|
Boolean(multiTargets) ||
|
|
1096
1129
|
(virtualResources.length > 0 && (virtualResources.length > 1 || searchablePaths.length > 0));
|
|
1097
1130
|
const perFileMatchCap = isMultiScope ? MULTI_FILE_PER_FILE_MATCHES : SINGLE_FILE_MATCHES;
|
|
1131
|
+
// Range filtering happens in JS after the native fetch, so out-of-range
|
|
1132
|
+
// matches consume fetch budget. Widen the per-file budget just enough
|
|
1133
|
+
// that filtering can still yield `perFileMatchCap` in-range hits, and
|
|
1134
|
+
// scale the global safety ceiling by the same amplification so ranged
|
|
1135
|
+
// searches keep the baseline file coverage while staying finite.
|
|
1136
|
+
const hasLineRangeFilters = pathSpecs.some(spec => spec.ranges);
|
|
1137
|
+
const nativeMaxCountPerFile = hasLineRangeFilters
|
|
1138
|
+
? Math.max(perFileMatchCap + 1, lineRangeFetchCap(pathSpecs, perFileMatchCap + 1))
|
|
1139
|
+
: perFileMatchCap + 1;
|
|
1140
|
+
const nativeMaxCount = hasLineRangeFilters
|
|
1141
|
+
? Math.ceil(INTERNAL_TOTAL_CAP / (perFileMatchCap + 1)) * nativeMaxCountPerFile
|
|
1142
|
+
: INTERNAL_TOTAL_CAP;
|
|
1098
1143
|
|
|
1099
1144
|
// Run grep
|
|
1100
1145
|
let result: GrepResult = {
|
|
@@ -1129,12 +1174,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1129
1174
|
multiline: effectiveMultiline,
|
|
1130
1175
|
hidden: true,
|
|
1131
1176
|
gitignore: useGitignore,
|
|
1132
|
-
maxCount:
|
|
1177
|
+
maxCount: nativeMaxCount,
|
|
1133
1178
|
contextBefore: normalizedContextBefore,
|
|
1134
1179
|
contextAfter: normalizedContextAfter,
|
|
1135
1180
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
1136
1181
|
mode: effectiveOutputMode,
|
|
1137
|
-
maxCountPerFile:
|
|
1182
|
+
maxCountPerFile: nativeMaxCountPerFile,
|
|
1138
1183
|
signal,
|
|
1139
1184
|
timeoutMs: SEARCH_GREP_TIMEOUT_MS,
|
|
1140
1185
|
},
|
|
@@ -1176,12 +1221,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1176
1221
|
multiline: effectiveMultiline,
|
|
1177
1222
|
hidden: true,
|
|
1178
1223
|
gitignore: useGitignore,
|
|
1179
|
-
maxCount:
|
|
1224
|
+
maxCount: nativeMaxCount,
|
|
1180
1225
|
contextBefore: normalizedContextBefore,
|
|
1181
1226
|
contextAfter: normalizedContextAfter,
|
|
1182
1227
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
1183
1228
|
mode: effectiveOutputMode,
|
|
1184
|
-
maxCountPerFile:
|
|
1229
|
+
maxCountPerFile: nativeMaxCountPerFile,
|
|
1185
1230
|
signal,
|
|
1186
1231
|
timeoutMs: SEARCH_GREP_TIMEOUT_MS,
|
|
1187
1232
|
},
|
|
@@ -1222,12 +1267,12 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
|
|
|
1222
1267
|
}
|
|
1223
1268
|
throw err;
|
|
1224
1269
|
}
|
|
1225
|
-
result = mergeGrepResults(result, virtualResult,
|
|
1226
|
-
if (rangesByAbsPath.size > 0) {
|
|
1270
|
+
result = mergeGrepResults(result, virtualResult, nativeMaxCount);
|
|
1271
|
+
if (rangesByAbsPath.size > 0 || globalRanges) {
|
|
1227
1272
|
const filteredMatches: GrepMatch[] = [];
|
|
1228
1273
|
for (const match of result.matches) {
|
|
1229
1274
|
const abs = matchAbsolutePath(match.path, searchPath);
|
|
1230
|
-
const ranges = rangesByAbsPath.get(abs);
|
|
1275
|
+
const ranges = rangesByAbsPath.get(abs) ?? globalRanges;
|
|
1231
1276
|
if (!ranges) {
|
|
1232
1277
|
// Path has no line-range constraint (e.g. a peer entry without `:N-M`).
|
|
1233
1278
|
filteredMatches.push(match);
|
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/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/read.ts
CHANGED
|
@@ -2118,13 +2118,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2118
2118
|
_toolContext?: AgentToolContext,
|
|
2119
2119
|
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
2120
2120
|
let { path: readPath } = params;
|
|
2121
|
-
let explicitSelector = params.selector?.trim();
|
|
2121
|
+
let explicitSelector = params.selector?.trim() || undefined;
|
|
2122
2122
|
let explicitParsedSelector = explicitSelector === undefined ? undefined : parseSel(explicitSelector);
|
|
2123
|
-
if (
|
|
2124
|
-
|
|
2125
|
-
(explicitSelector === undefined || explicitSelector.length === 0 || explicitParsedSelector?.kind === "none")
|
|
2126
|
-
) {
|
|
2127
|
-
throw invalidSelector(params.selector);
|
|
2123
|
+
if (explicitSelector !== undefined && explicitParsedSelector?.kind === "none") {
|
|
2124
|
+
throw invalidSelector(explicitSelector);
|
|
2128
2125
|
}
|
|
2129
2126
|
if (readPath.startsWith("file://")) {
|
|
2130
2127
|
readPath = expandPath(readPath);
|
|
@@ -3314,6 +3311,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
3314
3311
|
interface ReadRenderArgs {
|
|
3315
3312
|
path?: unknown;
|
|
3316
3313
|
file_path?: unknown;
|
|
3314
|
+
selector?: unknown;
|
|
3317
3315
|
sel?: string;
|
|
3318
3316
|
// Legacy fields from old schema — tolerated for in-flight tool calls during transition
|
|
3319
3317
|
offset?: number;
|
|
@@ -3378,10 +3376,20 @@ function formatReadPathLink(
|
|
|
3378
3376
|
|
|
3379
3377
|
export const readToolRenderer = {
|
|
3380
3378
|
renderCall(args: ReadRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
3381
|
-
const
|
|
3379
|
+
const baseRawPath =
|
|
3382
3380
|
typeof args.file_path === "string" ? args.file_path : typeof args.path === "string" ? args.path : "";
|
|
3383
|
-
|
|
3384
|
-
|
|
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
|
+
);
|
|
3385
3393
|
}
|
|
3386
3394
|
|
|
3387
3395
|
const offset = args.offset;
|
|
@@ -3405,9 +3413,9 @@ export const readToolRenderer = {
|
|
|
3405
3413
|
args?: ReadRenderArgs,
|
|
3406
3414
|
): Component {
|
|
3407
3415
|
const urlDetails = result.details as ReadUrlToolDetails | undefined;
|
|
3408
|
-
const
|
|
3416
|
+
const baseRawPathForKind =
|
|
3409
3417
|
typeof args?.file_path === "string" ? args.file_path : typeof args?.path === "string" ? args.path : "";
|
|
3410
|
-
if (urlDetails?.kind === "url" || isReadableUrlPath(
|
|
3418
|
+
if (urlDetails?.kind === "url" || isReadableUrlPath(baseRawPathForKind)) {
|
|
3411
3419
|
return renderReadUrlResult(
|
|
3412
3420
|
result as {
|
|
3413
3421
|
content: Array<{ type: string; text?: string }>;
|
|
@@ -3422,8 +3430,14 @@ export const readToolRenderer = {
|
|
|
3422
3430
|
if (result.isError) {
|
|
3423
3431
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
3424
3432
|
const errorText = (rawErrorText || "Unknown error").replace(/^Error:\s*/, "");
|
|
3425
|
-
const
|
|
3433
|
+
const baseRawPath =
|
|
3426
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;
|
|
3427
3441
|
const filePath =
|
|
3428
3442
|
formatReadPathLink(rawPath, { offset: args?.offset, sourcePath: readSourceFsPath(result.details) }) ||
|
|
3429
3443
|
shortenPath(rawPath);
|
|
@@ -3450,8 +3464,14 @@ export const readToolRenderer = {
|
|
|
3450
3464
|
// echo next to the styled warning line below.
|
|
3451
3465
|
const contentText = details?.displayContent?.text ?? stripOutputNotice(rawText, details?.meta);
|
|
3452
3466
|
const imageContent = result.content?.find(c => c.type === "image");
|
|
3453
|
-
const
|
|
3467
|
+
const baseRawPath =
|
|
3454
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;
|
|
3455
3475
|
const renderPath = splitReadRenderPath(rawPath);
|
|
3456
3476
|
const lang = getLanguageFromPath(renderPath.path);
|
|
3457
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
|
};
|