@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.0
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 +100 -0
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/hooks/status-line.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +8 -8
- package/src/cli/args.ts +9 -6
- package/src/cli/update-cli.ts +2 -2
- package/src/commands/index/index.ts +2 -5
- package/src/commit/agentic/agent.ts +1 -1
- package/src/commit/changelog/index.ts +2 -2
- package/src/config/keybindings.ts +16 -1
- package/src/config/model-registry.ts +25 -20
- package/src/config/model-resolver.ts +8 -8
- package/src/config/resolve-config-value.ts +92 -0
- package/src/config/settings-schema.ts +9 -0
- package/src/config.ts +14 -1
- package/src/export/html/template.css +7 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +33 -16
- package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
- package/src/extensibility/extensions/index.ts +18 -0
- package/src/extensibility/extensions/loader.ts +15 -0
- package/src/extensibility/extensions/runner.ts +78 -1
- package/src/extensibility/extensions/types.ts +131 -5
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/plugins/git-url.ts +270 -0
- package/src/extensibility/plugins/index.ts +2 -0
- package/src/extensibility/slash-commands.ts +45 -0
- package/src/index.ts +7 -0
- package/src/lsp/render.ts +50 -43
- package/src/lsp/utils.ts +2 -2
- package/src/main.ts +11 -10
- package/src/mcp/transports/stdio.ts +3 -5
- package/src/modes/components/custom-message.ts +0 -8
- package/src/modes/components/diff.ts +1 -7
- package/src/modes/components/footer.ts +4 -4
- package/src/modes/components/model-selector.ts +4 -0
- package/src/modes/components/todo-display.ts +13 -3
- package/src/modes/components/tool-execution.ts +30 -16
- package/src/modes/components/tree-selector.ts +50 -19
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +34 -2
- package/src/modes/controllers/input-controller.ts +47 -33
- package/src/modes/controllers/selector-controller.ts +10 -15
- package/src/modes/interactive-mode.ts +50 -38
- package/src/modes/print-mode.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +17 -2
- package/src/modes/rpc/rpc-types.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/patch/applicator.ts +2 -3
- package/src/patch/fuzzy.ts +1 -1
- package/src/patch/shared.ts +74 -61
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/sdk.ts +15 -11
- package/src/session/agent-session.ts +72 -23
- package/src/session/auth-storage.ts +2 -1
- package/src/session/blob-store.ts +105 -0
- package/src/session/session-manager.ts +107 -44
- package/src/task/executor.ts +19 -9
- package/src/task/render.ts +80 -58
- package/src/tools/ask.ts +28 -5
- package/src/tools/bash.ts +47 -39
- package/src/tools/browser.ts +248 -26
- package/src/tools/calculator.ts +42 -23
- package/src/tools/fetch.ts +33 -16
- package/src/tools/find.ts +57 -22
- package/src/tools/grep.ts +54 -25
- package/src/tools/index.ts +5 -5
- package/src/tools/notebook.ts +19 -6
- package/src/tools/path-utils.ts +26 -1
- package/src/tools/python.ts +20 -14
- package/src/tools/read.ts +21 -8
- package/src/tools/render-utils.ts +5 -45
- package/src/tools/ssh.ts +59 -53
- package/src/tools/submit-result.ts +2 -2
- package/src/tools/todo-write.ts +32 -14
- package/src/tools/truncate.ts +1 -1
- package/src/tools/write.ts +39 -24
- package/src/tui/output-block.ts +61 -3
- package/src/tui/tree-list.ts +4 -4
- package/src/tui/utils.ts +71 -1
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/utils/tools-manager.ts +18 -2
- package/src/web/scrapers/osv.ts +4 -1
- package/src/web/scrapers/youtube.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +96 -90
|
@@ -286,6 +286,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
286
286
|
return Promise.resolve({ success: false, error: "Theme switching not supported in RPC mode" });
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
getToolsExpanded() {
|
|
290
|
+
// Tool expansion not supported in RPC mode - no TUI
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setToolsExpanded(_expanded: boolean) {
|
|
295
|
+
// Tool expansion not supported in RPC mode - no TUI
|
|
296
|
+
}
|
|
297
|
+
|
|
289
298
|
setEditorComponent(): void {
|
|
290
299
|
// Custom editor components not supported in RPC mode
|
|
291
300
|
}
|
|
@@ -316,6 +325,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
316
325
|
getActiveTools: () => session.getActiveToolNames(),
|
|
317
326
|
getAllTools: () => session.getAllToolNames(),
|
|
318
327
|
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
328
|
+
getCommands: () => [],
|
|
319
329
|
setModel: async model => {
|
|
320
330
|
const key = await session.modelRegistry.getApiKey(model);
|
|
321
331
|
if (!key) return false;
|
|
@@ -335,6 +345,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
335
345
|
shutdownState.requested = true;
|
|
336
346
|
},
|
|
337
347
|
getContextUsage: () => session.getContextUsage(),
|
|
348
|
+
getSystemPrompt: () => session.systemPrompt,
|
|
338
349
|
compact: async instructionsOrOptions => {
|
|
339
350
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
340
351
|
const options =
|
|
@@ -364,6 +375,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
364
375
|
const result = await session.navigateTree(targetId, { summarize: options?.summarize });
|
|
365
376
|
return { cancelled: result.cancelled };
|
|
366
377
|
},
|
|
378
|
+
switchSession: async sessionPath => {
|
|
379
|
+
const success = await session.switchSession(sessionPath);
|
|
380
|
+
return { cancelled: !success };
|
|
381
|
+
},
|
|
367
382
|
compact: async instructionsOrOptions => {
|
|
368
383
|
const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
|
|
369
384
|
const options =
|
|
@@ -412,12 +427,12 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
412
427
|
}
|
|
413
428
|
|
|
414
429
|
case "steer": {
|
|
415
|
-
await session.steer(command.message);
|
|
430
|
+
await session.steer(command.message, command.images);
|
|
416
431
|
return success(id, "steer");
|
|
417
432
|
}
|
|
418
433
|
|
|
419
434
|
case "follow_up": {
|
|
420
|
-
await session.followUp(command.message);
|
|
435
|
+
await session.followUp(command.message, command.images);
|
|
421
436
|
return success(id, "follow_up");
|
|
422
437
|
}
|
|
423
438
|
|
|
@@ -17,8 +17,8 @@ import type { CompactionResult } from "../../session/compaction";
|
|
|
17
17
|
export type RpcCommand =
|
|
18
18
|
// Prompting
|
|
19
19
|
| { id?: string; type: "prompt"; message: string; images?: ImageContent[]; streamingBehavior?: "steer" | "followUp" }
|
|
20
|
-
| { id?: string; type: "steer"; message: string }
|
|
21
|
-
| { id?: string; type: "follow_up"; message: string }
|
|
20
|
+
| { id?: string; type: "steer"; message: string; images?: ImageContent[] }
|
|
21
|
+
| { id?: string; type: "follow_up"; message: string; images?: ImageContent[] }
|
|
22
22
|
| { id?: string; type: "abort" }
|
|
23
23
|
| { id?: string; type: "new_session"; parentSession?: string }
|
|
24
24
|
|
package/src/modes/types.ts
CHANGED
|
@@ -176,6 +176,7 @@ export interface InteractiveModeContext {
|
|
|
176
176
|
cycleThinkingLevel(): void;
|
|
177
177
|
cycleRoleModel(options?: { temporary?: boolean }): Promise<void>;
|
|
178
178
|
toggleToolOutputExpansion(): void;
|
|
179
|
+
setToolsExpanded(expanded: boolean): void;
|
|
179
180
|
toggleThinkingBlockVisibility(): void;
|
|
180
181
|
openExternalEditor(): void;
|
|
181
182
|
registerExtensionShortcuts(): void;
|
|
@@ -203,7 +203,9 @@ export class UiHelpers {
|
|
|
203
203
|
|
|
204
204
|
// Render tool call components
|
|
205
205
|
for (const content of message.content) {
|
|
206
|
-
if (content.type !== "toolCall")
|
|
206
|
+
if (content.type !== "toolCall") {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
207
209
|
|
|
208
210
|
if (content.name === "read") {
|
|
209
211
|
if (!readGroup) {
|
package/src/patch/applicator.ts
CHANGED
|
@@ -434,8 +434,7 @@ function formatSequenceMatchPreview(lines: string[], startIdx: number): string {
|
|
|
434
434
|
return previewLines
|
|
435
435
|
.map((line, i) => {
|
|
436
436
|
const num = start + i + 1;
|
|
437
|
-
const truncated =
|
|
438
|
-
line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 3)}...` : line;
|
|
437
|
+
const truncated = line.length > MATCH_PREVIEW_MAX_LEN ? `${line.slice(0, MATCH_PREVIEW_MAX_LEN - 1)}…` : line;
|
|
439
438
|
return ` ${num} | ${truncated}`;
|
|
440
439
|
})
|
|
441
440
|
.join("\n");
|
|
@@ -1103,7 +1102,7 @@ function computeReplacements(
|
|
|
1103
1102
|
return lines
|
|
1104
1103
|
.map((line, i) => {
|
|
1105
1104
|
const num = start + i + 1;
|
|
1106
|
-
const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength -
|
|
1105
|
+
const truncated = line.length > maxLineLength ? `${line.slice(0, maxLineLength - 1)}…` : line;
|
|
1107
1106
|
return ` ${num} | ${truncated}`;
|
|
1108
1107
|
})
|
|
1109
1108
|
.join("\n");
|
package/src/patch/fuzzy.ts
CHANGED
|
@@ -241,7 +241,7 @@ export function findMatch(
|
|
|
241
241
|
const preview = previewLines
|
|
242
242
|
.map((line, idx) => {
|
|
243
243
|
const num = start + idx + 1;
|
|
244
|
-
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN -
|
|
244
|
+
return ` ${num} | ${line.length > OCCURRENCE_PREVIEW_MAX_LEN ? `${line.slice(0, OCCURRENCE_PREVIEW_MAX_LEN - 1)}…` : line}`;
|
|
245
245
|
})
|
|
246
246
|
.join("\n");
|
|
247
247
|
occurrencePreviews.push(preview);
|
package/src/patch/shared.ts
CHANGED
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
formatExpandHint,
|
|
14
14
|
formatStatusIcon,
|
|
15
15
|
getDiffStats,
|
|
16
|
+
PREVIEW_LIMITS,
|
|
16
17
|
shortenPath,
|
|
17
18
|
ToolUIKit,
|
|
18
19
|
truncateDiffByHunk,
|
|
19
20
|
} from "../tools/render-utils";
|
|
20
21
|
import type { RenderCallOptions } from "../tools/renderers";
|
|
21
|
-
import { renderStatusLine } from "../tui";
|
|
22
|
+
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
|
|
22
23
|
import type { DiffError, DiffResult, Operation } from "./types";
|
|
23
24
|
|
|
24
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -85,8 +86,6 @@ export interface EditRenderContext {
|
|
|
85
86
|
renderDiff?: (diffText: string, options?: { filePath?: string }) => string;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
const EDIT_DIFF_PREVIEW_HUNKS = 2;
|
|
89
|
-
const EDIT_DIFF_PREVIEW_LINES = 24;
|
|
90
89
|
const EDIT_STREAMING_PREVIEW_LINES = 12;
|
|
91
90
|
|
|
92
91
|
function countLines(text: string): number {
|
|
@@ -140,7 +139,7 @@ function renderDiffSection(
|
|
|
140
139
|
hiddenLines,
|
|
141
140
|
} = expanded
|
|
142
141
|
? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
|
|
143
|
-
: truncateDiffByHunk(diff,
|
|
142
|
+
: truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
|
|
144
143
|
|
|
145
144
|
text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
|
|
146
145
|
if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
|
|
@@ -209,75 +208,89 @@ export const editToolRenderer = {
|
|
|
209
208
|
args?: EditRenderArgs,
|
|
210
209
|
): Component {
|
|
211
210
|
const ui = new ToolUIKit(uiTheme);
|
|
212
|
-
const { expanded, renderContext } = options;
|
|
213
211
|
const rawPath = args?.file_path || args?.path || "";
|
|
214
212
|
const filePath = shortenPath(rawPath);
|
|
215
213
|
const editLanguage = getLanguageFromPath(rawPath) ?? "text";
|
|
216
214
|
const editIcon = uiTheme.fg("muted", uiTheme.getLangIcon(editLanguage));
|
|
217
|
-
const editDiffPreview = renderContext?.editDiffPreview;
|
|
218
|
-
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
219
215
|
|
|
220
|
-
// Get op and rename from args or details
|
|
221
216
|
const op = args?.op || result.details?.op;
|
|
222
217
|
const rename = args?.rename || result.details?.rename;
|
|
218
|
+
const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
223
219
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (firstChangedLine) {
|
|
230
|
-
pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
|
|
231
|
-
}
|
|
220
|
+
// Pre-compute metadata line (static across renders)
|
|
221
|
+
const metadataLine =
|
|
222
|
+
op !== "delete"
|
|
223
|
+
? `\n${formatMetadataLine(countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? ""), editLanguage, uiTheme)}`
|
|
224
|
+
: "";
|
|
232
225
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
|
|
236
|
-
}
|
|
226
|
+
// Pre-compute error text (static)
|
|
227
|
+
const errorText = result.isError ? (result.content?.find(c => c.type === "text")?.text ?? "") : "";
|
|
237
228
|
|
|
238
|
-
|
|
239
|
-
const opTitle = op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
|
|
240
|
-
const header = renderStatusLine(
|
|
241
|
-
{
|
|
242
|
-
icon: result.isError ? "error" : "success",
|
|
243
|
-
title: opTitle,
|
|
244
|
-
description: `${editIcon} ${pathDisplay}`,
|
|
245
|
-
},
|
|
246
|
-
uiTheme,
|
|
247
|
-
);
|
|
248
|
-
let text = header;
|
|
249
|
-
|
|
250
|
-
// Skip metadata line for delete operations
|
|
251
|
-
if (op !== "delete") {
|
|
252
|
-
const editLineCount = countLines(args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch ?? "");
|
|
253
|
-
text += `\n${formatMetadataLine(editLineCount, editLanguage, uiTheme)}`;
|
|
254
|
-
}
|
|
229
|
+
let cached: RenderCache | undefined;
|
|
255
230
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// Prefer actual diff after execution
|
|
264
|
-
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
265
|
-
} else if (editDiffPreview) {
|
|
266
|
-
// Use cached diff preview when no actual diff is available
|
|
267
|
-
if ("error" in editDiffPreview) {
|
|
268
|
-
text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
|
|
269
|
-
} else if (editDiffPreview.diff) {
|
|
270
|
-
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
231
|
+
return {
|
|
232
|
+
render(width) {
|
|
233
|
+
const { expanded, renderContext } = options;
|
|
234
|
+
const editDiffPreview = renderContext?.editDiffPreview;
|
|
235
|
+
const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
|
|
236
|
+
const key = new Hasher().bool(expanded).u32(width).digest();
|
|
237
|
+
if (cached?.key === key) return cached.lines;
|
|
273
238
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
239
|
+
// Build path display with line number
|
|
240
|
+
let pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
241
|
+
const firstChangedLine =
|
|
242
|
+
(editDiffPreview && "firstChangedLine" in editDiffPreview
|
|
243
|
+
? editDiffPreview.firstChangedLine
|
|
244
|
+
: undefined) || (result.details && !result.isError ? result.details.firstChangedLine : undefined);
|
|
245
|
+
if (firstChangedLine) {
|
|
246
|
+
pathDisplay += uiTheme.fg("warning", `:${firstChangedLine}`);
|
|
247
|
+
}
|
|
280
248
|
|
|
281
|
-
|
|
249
|
+
// Add arrow for rename operations
|
|
250
|
+
if (rename) {
|
|
251
|
+
pathDisplay += ` ${uiTheme.fg("dim", "→")} ${uiTheme.fg("accent", shortenPath(rename))}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const header = renderStatusLine(
|
|
255
|
+
{
|
|
256
|
+
icon: result.isError ? "error" : "success",
|
|
257
|
+
title: opTitle,
|
|
258
|
+
description: `${editIcon} ${pathDisplay}`,
|
|
259
|
+
},
|
|
260
|
+
uiTheme,
|
|
261
|
+
);
|
|
262
|
+
let text = header;
|
|
263
|
+
text += metadataLine;
|
|
264
|
+
|
|
265
|
+
if (result.isError) {
|
|
266
|
+
if (errorText) {
|
|
267
|
+
text += `\n\n${uiTheme.fg("error", errorText)}`;
|
|
268
|
+
}
|
|
269
|
+
} else if (result.details?.diff) {
|
|
270
|
+
text += renderDiffSection(result.details.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
271
|
+
} else if (editDiffPreview) {
|
|
272
|
+
if ("error" in editDiffPreview) {
|
|
273
|
+
text += `\n\n${uiTheme.fg("error", editDiffPreview.error)}`;
|
|
274
|
+
} else if (editDiffPreview.diff) {
|
|
275
|
+
text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, ui, renderDiffFn);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Show LSP diagnostics if available
|
|
280
|
+
if (result.details?.diagnostics) {
|
|
281
|
+
text += ui.formatDiagnostics(result.details.diagnostics, expanded, (fp: string) =>
|
|
282
|
+
uiTheme.getLangIcon(getLanguageFromPath(fp)),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const lines =
|
|
287
|
+
width > 0 ? text.split("\n").map(line => truncateToWidth(line, width, Ellipsis.Omit)) : text.split("\n");
|
|
288
|
+
cached = { key, lines };
|
|
289
|
+
return lines;
|
|
290
|
+
},
|
|
291
|
+
invalidate() {
|
|
292
|
+
cached = undefined;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
282
295
|
},
|
|
283
296
|
};
|
|
@@ -221,6 +221,7 @@ Main branch: {{git.mainBranch}}
|
|
|
221
221
|
{{#if skills.length}}
|
|
222
222
|
<skills>
|
|
223
223
|
Scan descriptions vs domain. Skill covers output? Read `skill://<name>` first.
|
|
224
|
+
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.
|
|
224
225
|
|
|
225
226
|
{{#list skills join="\n"}}
|
|
226
227
|
<skill name="{{name}}">
|
|
@@ -70,6 +70,10 @@ Run in isolated git worktree; returns patches. Use when tasks edit overlapping f
|
|
|
70
70
|
### `schema` (optional — recommended for structured output)
|
|
71
71
|
|
|
72
72
|
JTD schema defining expected response structure. Use typed properties. If you care about parsing result, define here — **never describe output format in `context` or `assignment`**.
|
|
73
|
+
|
|
74
|
+
<caution>
|
|
75
|
+
**Schema vs agent mismatch causes null output.** Agents with `output="structured"` (e.g., `explore`) have a built-in schema. If you also pass `schema`, yours takes precedence — but if you describe output format in `context`/`assignment` instead, the agent's built-in schema wins. The agent gets confused trying to fit your requested format into its schema shape and submits `null`. Either: (1) use `schema` to override the built-in one, (2) use `task` agent which has no built-in schema, or (3) match your instructions to the agent's expected output shape.
|
|
76
|
+
</caution>
|
|
73
77
|
---
|
|
74
78
|
|
|
75
79
|
## Writing an assignment
|
|
@@ -115,6 +119,8 @@ Use structure every assignment:
|
|
|
115
119
|
- "No WASM."
|
|
116
120
|
|
|
117
121
|
If tempted to write above, expand using templates.
|
|
122
|
+
**Output format in prose instead of `schema`** — agent returns null:
|
|
123
|
+
Structured agents (`explore`, `reviewer`) have built-in output schemas. Describing a different output format in `context`/`assignment` without overriding via `schema` creates a mismatch — the agent can't reconcile your prose instructions with its schema and submits null data. Always use `schema` for output structure, or pick an agent whose built-in schema matches your needs.
|
|
118
124
|
**Test/lint commands in parallel tasks** — edit wars:
|
|
119
125
|
Parallel agents share working tree. If two agents run `bun check` or `bun test` concurrently, they see each other's half-finished edits, "fix" phantom errors, loop. **Never tell parallel tasks run project-wide build/test/lint commands.** Each task edits, stops. Caller verifies after all tasks complete.
|
|
120
126
|
**If you can’t specify scope yet**, create **Discovery task** first: enumerate files, find callsites, list candidates. Then fan out with explicit paths.
|
package/src/sdk.ts
CHANGED
|
@@ -387,7 +387,7 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
|
|
|
387
387
|
label: tool.label,
|
|
388
388
|
description: tool.description,
|
|
389
389
|
parameters: tool.parameters,
|
|
390
|
-
execute: (toolCallId, params, onUpdate, ctx
|
|
390
|
+
execute: (toolCallId, params, signal, onUpdate, ctx) =>
|
|
391
391
|
tool.execute(toolCallId, params, onUpdate, createCustomToolContext(ctx), signal),
|
|
392
392
|
onSession: tool.onSession ? (event, ctx) => tool.onSession?.(event, createCustomToolContext(ctx)) : undefined,
|
|
393
393
|
renderCall: tool.renderCall,
|
|
@@ -506,6 +506,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
506
506
|
const existingSession = sessionManager.buildSessionContext();
|
|
507
507
|
time("loadSession");
|
|
508
508
|
const hasExistingSession = existingSession.messages.length > 0;
|
|
509
|
+
const hasThinkingEntry = sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
509
510
|
|
|
510
511
|
const hasExplicitModel = options.model !== undefined;
|
|
511
512
|
let model = options.model;
|
|
@@ -563,7 +564,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
563
564
|
|
|
564
565
|
// If session has data, restore thinking level from it
|
|
565
566
|
if (thinkingLevel === undefined && hasExistingSession) {
|
|
566
|
-
thinkingLevel =
|
|
567
|
+
thinkingLevel = hasThinkingEntry
|
|
568
|
+
? (existingSession.thinkingLevel as ThinkingLevel)
|
|
569
|
+
: ((settingsInstance.get("defaultThinkingLevel") ?? "off") as ThinkingLevel);
|
|
567
570
|
}
|
|
568
571
|
|
|
569
572
|
// Fall back to settings default
|
|
@@ -698,7 +701,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
698
701
|
onConnecting: serverNames => {
|
|
699
702
|
if (options.hasUI && serverNames.length > 0) {
|
|
700
703
|
process.stderr.write(
|
|
701
|
-
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}
|
|
704
|
+
chalk.gray(`Connecting to MCP servers: ${serverNames.join(", ")}…
|
|
702
705
|
`),
|
|
703
706
|
);
|
|
704
707
|
}
|
|
@@ -1016,14 +1019,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1016
1019
|
thinkingBudgets: settingsInstance.getGroup("thinkingBudgets"),
|
|
1017
1020
|
kimiApiFormat: settingsInstance.get("providers.kimiApiFormat") ?? "anthropic",
|
|
1018
1021
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
1019
|
-
getApiKey: async
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
const key = await modelRegistry.getApiKey(currentModel, sessionId);
|
|
1022
|
+
getApiKey: async provider => {
|
|
1023
|
+
// Use the provider argument from the in-flight request;
|
|
1024
|
+
// agent.state.model may already be switched mid-turn.
|
|
1025
|
+
const key = await modelRegistry.getApiKeyForProvider(provider, sessionId);
|
|
1025
1026
|
if (!key) {
|
|
1026
|
-
throw new Error(`No API key found for provider "${
|
|
1027
|
+
throw new Error(`No API key found for provider "${provider}"`);
|
|
1027
1028
|
}
|
|
1028
1029
|
return key;
|
|
1029
1030
|
},
|
|
@@ -1036,6 +1037,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1036
1037
|
// Restore messages if session has existing data
|
|
1037
1038
|
if (hasExistingSession) {
|
|
1038
1039
|
agent.replaceMessages(existingSession.messages);
|
|
1040
|
+
if (!hasThinkingEntry) {
|
|
1041
|
+
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
1042
|
+
}
|
|
1039
1043
|
} else {
|
|
1040
1044
|
// Save initial model and thinking level for new sessions so they can be restored on resume
|
|
1041
1045
|
if (model) {
|
|
@@ -1072,7 +1076,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1072
1076
|
const result = await warmupLspServers(cwd, {
|
|
1073
1077
|
onConnecting: serverNames => {
|
|
1074
1078
|
if (options.hasUI && serverNames.length > 0) {
|
|
1075
|
-
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}
|
|
1079
|
+
process.stderr.write(chalk.gray(`Starting LSP servers: ${serverNames.join(", ")}…\n`));
|
|
1076
1080
|
}
|
|
1077
1081
|
},
|
|
1078
1082
|
});
|
|
@@ -237,6 +237,8 @@ const noOpUIContext: ExtensionUIContext = {
|
|
|
237
237
|
setFooter: () => {},
|
|
238
238
|
setHeader: () => {},
|
|
239
239
|
setEditorComponent: () => {},
|
|
240
|
+
getToolsExpanded: () => false,
|
|
241
|
+
setToolsExpanded: () => {},
|
|
240
242
|
};
|
|
241
243
|
|
|
242
244
|
async function cleanupSshResources(): Promise<void> {
|
|
@@ -906,6 +908,11 @@ export class AgentSession {
|
|
|
906
908
|
return this.agent.state.isStreaming || this._promptInFlight;
|
|
907
909
|
}
|
|
908
910
|
|
|
911
|
+
/** Current effective system prompt (includes any per-turn extension modifications) */
|
|
912
|
+
get systemPrompt(): string {
|
|
913
|
+
return this.agent.state.systemPrompt;
|
|
914
|
+
}
|
|
915
|
+
|
|
909
916
|
/** Current retry attempt (0 if not retrying) */
|
|
910
917
|
get retryAttempt(): number {
|
|
911
918
|
return this._retryAttempt;
|
|
@@ -1426,6 +1433,11 @@ export class AgentSession {
|
|
|
1426
1433
|
instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
|
|
1427
1434
|
await this.compact(instructions, options);
|
|
1428
1435
|
},
|
|
1436
|
+
switchSession: async sessionPath => {
|
|
1437
|
+
const success = await this.switchSession(sessionPath);
|
|
1438
|
+
return { cancelled: !success };
|
|
1439
|
+
},
|
|
1440
|
+
getSystemPrompt: () => this.systemPrompt,
|
|
1429
1441
|
};
|
|
1430
1442
|
}
|
|
1431
1443
|
|
|
@@ -1477,25 +1489,25 @@ export class AgentSession {
|
|
|
1477
1489
|
/**
|
|
1478
1490
|
* Queue a steering message to interrupt the agent mid-run.
|
|
1479
1491
|
*/
|
|
1480
|
-
async steer(text: string): Promise<void> {
|
|
1492
|
+
async steer(text: string, images?: ImageContent[]): Promise<void> {
|
|
1481
1493
|
if (text.startsWith("/")) {
|
|
1482
1494
|
this._throwIfExtensionCommand(text);
|
|
1483
1495
|
}
|
|
1484
1496
|
|
|
1485
1497
|
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
1486
|
-
await this._queueSteer(expandedText);
|
|
1498
|
+
await this._queueSteer(expandedText, images);
|
|
1487
1499
|
}
|
|
1488
1500
|
|
|
1489
1501
|
/**
|
|
1490
1502
|
* Queue a follow-up message to process after the agent would otherwise stop.
|
|
1491
1503
|
*/
|
|
1492
|
-
async followUp(text: string): Promise<void> {
|
|
1504
|
+
async followUp(text: string, images?: ImageContent[]): Promise<void> {
|
|
1493
1505
|
if (text.startsWith("/")) {
|
|
1494
1506
|
this._throwIfExtensionCommand(text);
|
|
1495
1507
|
}
|
|
1496
1508
|
|
|
1497
1509
|
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
|
1498
|
-
await this._queueFollowUp(expandedText);
|
|
1510
|
+
await this._queueFollowUp(expandedText, images);
|
|
1499
1511
|
}
|
|
1500
1512
|
|
|
1501
1513
|
/**
|
|
@@ -1733,6 +1745,9 @@ export class AgentSession {
|
|
|
1733
1745
|
this._steeringMessages = [];
|
|
1734
1746
|
this._followUpMessages = [];
|
|
1735
1747
|
this._pendingNextTurnMessages = [];
|
|
1748
|
+
|
|
1749
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
1750
|
+
|
|
1736
1751
|
this._todoReminderCount = 0;
|
|
1737
1752
|
this._planReferenceSent = false;
|
|
1738
1753
|
this._reconnectToAgent();
|
|
@@ -1934,22 +1949,39 @@ export class AgentSession {
|
|
|
1934
1949
|
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
|
1935
1950
|
}
|
|
1936
1951
|
|
|
1952
|
+
private async _getScopedModelsWithApiKey(): Promise<Array<{ model: Model; thinkingLevel: ThinkingLevel }>> {
|
|
1953
|
+
const apiKeysByProvider = new Map<string, string | undefined>();
|
|
1954
|
+
const result: Array<{ model: Model; thinkingLevel: ThinkingLevel }> = [];
|
|
1955
|
+
|
|
1956
|
+
for (const scoped of this._scopedModels) {
|
|
1957
|
+
const provider = scoped.model.provider;
|
|
1958
|
+
let apiKey: string | undefined;
|
|
1959
|
+
if (apiKeysByProvider.has(provider)) {
|
|
1960
|
+
apiKey = apiKeysByProvider.get(provider);
|
|
1961
|
+
} else {
|
|
1962
|
+
apiKey = await this._modelRegistry.getApiKeyForProvider(provider, this.sessionId);
|
|
1963
|
+
apiKeysByProvider.set(provider, apiKey);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
if (apiKey) {
|
|
1967
|
+
result.push(scoped);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
return result;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1937
1974
|
private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | undefined> {
|
|
1938
|
-
|
|
1975
|
+
const scopedModels = await this._getScopedModelsWithApiKey();
|
|
1976
|
+
if (scopedModels.length <= 1) return undefined;
|
|
1939
1977
|
|
|
1940
1978
|
const currentModel = this.model;
|
|
1941
|
-
let currentIndex =
|
|
1979
|
+
let currentIndex = scopedModels.findIndex(sm => modelsAreEqual(sm.model, currentModel));
|
|
1942
1980
|
|
|
1943
1981
|
if (currentIndex === -1) currentIndex = 0;
|
|
1944
|
-
const len =
|
|
1982
|
+
const len = scopedModels.length;
|
|
1945
1983
|
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
|
|
1946
|
-
const next =
|
|
1947
|
-
|
|
1948
|
-
// Validate API key
|
|
1949
|
-
const apiKey = await this._modelRegistry.getApiKey(next.model, this.sessionId);
|
|
1950
|
-
if (!apiKey) {
|
|
1951
|
-
throw new Error(`No API key for ${next.model.provider}/${next.model.id}`);
|
|
1952
|
-
}
|
|
1984
|
+
const next = scopedModels[nextIndex];
|
|
1953
1985
|
|
|
1954
1986
|
// Apply model
|
|
1955
1987
|
this.agent.setModel(next.model);
|
|
@@ -2005,15 +2037,22 @@ export class AgentSession {
|
|
|
2005
2037
|
/**
|
|
2006
2038
|
* Set thinking level.
|
|
2007
2039
|
* Clamps to model capabilities based on available thinking levels.
|
|
2008
|
-
* Saves to session
|
|
2040
|
+
* Saves to session and settings only if the level actually changes.
|
|
2009
2041
|
*/
|
|
2010
2042
|
setThinkingLevel(level: ThinkingLevel, persist: boolean = false): void {
|
|
2011
2043
|
const availableLevels = this.getAvailableThinkingLevels();
|
|
2012
2044
|
const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
|
|
2045
|
+
|
|
2046
|
+
// Only persist if actually changing
|
|
2047
|
+
const isChanging = effectiveLevel !== this.agent.state.thinkingLevel;
|
|
2048
|
+
|
|
2013
2049
|
this.agent.setThinkingLevel(effectiveLevel);
|
|
2014
|
-
|
|
2015
|
-
if (
|
|
2016
|
-
this.
|
|
2050
|
+
|
|
2051
|
+
if (isChanging) {
|
|
2052
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
2053
|
+
if (persist) {
|
|
2054
|
+
this.settings.set("defaultThinkingLevel", effectiveLevel);
|
|
2055
|
+
}
|
|
2017
2056
|
}
|
|
2018
2057
|
}
|
|
2019
2058
|
|
|
@@ -2903,8 +2942,8 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2903
2942
|
}
|
|
2904
2943
|
|
|
2905
2944
|
private _isRetryableErrorMessage(errorMessage: string): boolean {
|
|
2906
|
-
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed
|
|
2907
|
-
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|fetch failed/i.test(
|
|
2945
|
+
// Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed, retry delay exceeded
|
|
2946
|
+
return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|fetch failed|retry delay/i.test(
|
|
2908
2947
|
errorMessage,
|
|
2909
2948
|
);
|
|
2910
2949
|
}
|
|
@@ -3354,9 +3393,19 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3354
3393
|
}
|
|
3355
3394
|
}
|
|
3356
3395
|
|
|
3357
|
-
|
|
3358
|
-
|
|
3396
|
+
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
3397
|
+
const defaultThinkingLevel = (this.settings.get("defaultThinkingLevel") ?? "off") as ThinkingLevel;
|
|
3398
|
+
|
|
3399
|
+
if (hasThinkingEntry) {
|
|
3400
|
+
// Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
|
|
3359
3401
|
this.setThinkingLevel(sessionContext.thinkingLevel as ThinkingLevel);
|
|
3402
|
+
} else {
|
|
3403
|
+
const availableLevels = this.getAvailableThinkingLevels();
|
|
3404
|
+
const effectiveLevel = availableLevels.includes(defaultThinkingLevel)
|
|
3405
|
+
? defaultThinkingLevel
|
|
3406
|
+
: this._clampThinkingLevel(defaultThinkingLevel, availableLevels);
|
|
3407
|
+
this.agent.setThinkingLevel(effectiveLevel);
|
|
3408
|
+
this.sessionManager.appendThinkingLevelChange(effectiveLevel);
|
|
3360
3409
|
}
|
|
3361
3410
|
|
|
3362
3411
|
this._reconnectToAgent();
|
|
@@ -3404,7 +3453,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3404
3453
|
await this.sessionManager.flush();
|
|
3405
3454
|
|
|
3406
3455
|
if (!selectedEntry.parentId) {
|
|
3407
|
-
this.sessionManager.newSession();
|
|
3456
|
+
this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
3408
3457
|
} else {
|
|
3409
3458
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
3410
3459
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "@oh-my-pi/pi-ai";
|
|
35
35
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
36
36
|
import { getAgentDbPath } from "../config";
|
|
37
|
+
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
37
38
|
import { AgentStorage } from "./agent-storage";
|
|
38
39
|
|
|
39
40
|
export type ApiKeyCredential = {
|
|
@@ -1294,7 +1295,7 @@ export class AuthStorage {
|
|
|
1294
1295
|
const apiKeySelection = this.selectCredentialByType(provider, "api_key", sessionId);
|
|
1295
1296
|
if (apiKeySelection) {
|
|
1296
1297
|
this.recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
1297
|
-
return apiKeySelection.credential.key;
|
|
1298
|
+
return resolveConfigValue(apiKeySelection.credential.key);
|
|
1298
1299
|
}
|
|
1299
1300
|
|
|
1300
1301
|
const oauthKey = await this.resolveOAuthApiKey(provider, sessionId, options);
|