@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.8
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 +59 -0
- package/dist/cli.js +4967 -4948
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +16 -0
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/mcp/tool-bridge.d.ts +9 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +78 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/package.json +12 -13
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/config/model-registry.ts +47 -24
- package/src/config/model-resolver.ts +56 -31
- package/src/config/settings-schema.ts +5 -0
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +15 -5
- package/src/mcp/tool-bridge.ts +9 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +22 -5
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/sdk.ts +134 -48
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +300 -26
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +41 -27
- package/src/task/index.ts +11 -0
- package/src/task/prewalk.ts +6 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
|
@@ -172,6 +172,7 @@ export class InputController {
|
|
|
172
172
|
|
|
173
173
|
#enhancedPaste?: EnhancedPasteController;
|
|
174
174
|
#focusedLeftTapListenerInstalled = false;
|
|
175
|
+
#focusedPasteListenerInstalled = false;
|
|
175
176
|
#btwBranchListenerInstalled = false;
|
|
176
177
|
#btwCopyListenerInstalled = false;
|
|
177
178
|
// Tap counter for the double-← gesture; reset whenever a quiet gap
|
|
@@ -265,6 +266,16 @@ export class InputController {
|
|
|
265
266
|
return { consume: true };
|
|
266
267
|
});
|
|
267
268
|
}
|
|
269
|
+
if (!this.#focusedPasteListenerInstalled) {
|
|
270
|
+
this.#focusedPasteListenerInstalled = true;
|
|
271
|
+
this.ctx.ui.addInputListener(data => {
|
|
272
|
+
const focused = this.ctx.ui.getFocused();
|
|
273
|
+
if (!focused || focused === this.ctx.editor || !hasPasteText(focused)) return undefined;
|
|
274
|
+
if (!this.ctx.keybindings.matches(data, "app.clipboard.pasteImage")) return undefined;
|
|
275
|
+
void this.handleImagePaste();
|
|
276
|
+
return { consume: true };
|
|
277
|
+
});
|
|
278
|
+
}
|
|
268
279
|
this.ctx.editor.onEscape = () => {
|
|
269
280
|
// Side-channel panels are the topmost view. Esc dismisses them before
|
|
270
281
|
// touching loop mode, maintenance, or the underlying main turn.
|
|
@@ -308,6 +319,14 @@ export class InputController {
|
|
|
308
319
|
if (aborted) return;
|
|
309
320
|
}
|
|
310
321
|
|
|
322
|
+
if (vocalizer.isSpeaking()) {
|
|
323
|
+
// Playback from the completed response can overlap the next agent
|
|
324
|
+
// turn. Silence it before interrupting any ongoing main-turn work.
|
|
325
|
+
vocalizer.clear();
|
|
326
|
+
this.ctx.lastEscapeTime = 0;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
311
330
|
if (this.ctx.loopModeEnabled) {
|
|
312
331
|
this.ctx.pauseLoop();
|
|
313
332
|
if (this.ctx.session.isStreaming) {
|
|
@@ -360,13 +379,6 @@ export class InputController {
|
|
|
360
379
|
} else if (this.ctx.editor.getText().trim()) {
|
|
361
380
|
// Esc must not destroy an in-progress draft.
|
|
362
381
|
this.ctx.lastEscapeTime = 0;
|
|
363
|
-
} else if (vocalizer.isSpeaking()) {
|
|
364
|
-
// TTS buffers seconds of PCM past the streaming abort, so an Esc
|
|
365
|
-
// arriving after the model stopped would otherwise fall through to
|
|
366
|
-
// the double-Esc gesture while Kokoro reads on. Silence first;
|
|
367
|
-
// tree/branch stays reachable via a second Esc.
|
|
368
|
-
vocalizer.clear();
|
|
369
|
-
this.ctx.lastEscapeTime = 0;
|
|
370
382
|
} else {
|
|
371
383
|
// Double-interrupt with empty editor triggers /tree, /branch, or nothing based on setting
|
|
372
384
|
const action = settings.get("doubleEscapeAction");
|
|
@@ -822,12 +834,25 @@ export class InputController {
|
|
|
822
834
|
// First, move any pending bash components to chat
|
|
823
835
|
this.ctx.flushPendingBashComponents();
|
|
824
836
|
|
|
837
|
+
// AgentSession.prompt() consumes registered extension commands locally.
|
|
838
|
+
// Classify them here because title generation starts before prompt dispatch.
|
|
839
|
+
const extensionCommandSpace = text.indexOf(" ");
|
|
840
|
+
const isLocalExtensionCommand =
|
|
841
|
+
text.startsWith("/") &&
|
|
842
|
+
runner?.getCommand(extensionCommandSpace === -1 ? text.slice(1) : text.slice(1, extensionCommandSpace)) !==
|
|
843
|
+
undefined;
|
|
844
|
+
|
|
825
845
|
// Auto-generate a session title while the session is still unnamed.
|
|
826
846
|
// Greetings / acknowledgements / empty input carry no task, so they are
|
|
827
847
|
// skipped deterministically (no model invoked, no download-progress UI)
|
|
828
848
|
// and the session stays unnamed — the next user message gets a fresh
|
|
829
849
|
// chance, so titling defers past "hi" instead of latching onto it.
|
|
830
|
-
if (
|
|
850
|
+
if (
|
|
851
|
+
!isLocalExtensionCommand &&
|
|
852
|
+
!this.ctx.sessionManager.getSessionName() &&
|
|
853
|
+
!$env.PI_NO_TITLE &&
|
|
854
|
+
!isLowSignalTitleInput(text)
|
|
855
|
+
) {
|
|
831
856
|
this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
|
|
832
857
|
this.ctx.session
|
|
833
858
|
.generateTitle(text)
|
|
@@ -1558,8 +1583,19 @@ export class InputController {
|
|
|
1558
1583
|
|
|
1559
1584
|
async handleImagePaste(): Promise<boolean> {
|
|
1560
1585
|
try {
|
|
1586
|
+
// When a modal paste-capable prompt (login/API-key Input) owns focus,
|
|
1587
|
+
// only clipboard text may land there. Image payloads must not mutate
|
|
1588
|
+
// the hidden main editor — mirror the enhanced-paste `pasteImage`
|
|
1589
|
+
// behavior and surface the unsupported-status instead (#6057).
|
|
1590
|
+
const focusedNow = this.ctx.ui.getFocused();
|
|
1591
|
+
const promptTarget =
|
|
1592
|
+
focusedNow && focusedNow !== this.ctx.editor && hasPasteText(focusedNow) ? focusedNow : null;
|
|
1561
1593
|
const image = await this.clipboard.readImage();
|
|
1562
1594
|
if (image) {
|
|
1595
|
+
if (promptTarget) {
|
|
1596
|
+
this.ctx.showStatus("Image paste is not supported in this prompt");
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1563
1599
|
return await this.#normalizeAndInsertPastedImage(
|
|
1564
1600
|
{
|
|
1565
1601
|
type: "image",
|
|
@@ -1581,7 +1617,7 @@ export class InputController {
|
|
|
1581
1617
|
// selections must not silently drop after the first attach.
|
|
1582
1618
|
// `readMacFileUrls` returns an empty list off Darwin, so the
|
|
1583
1619
|
// check is free on every other platform.
|
|
1584
|
-
const fileUrls = (await this.clipboard.readMacFileUrls?.()) ?? [];
|
|
1620
|
+
const fileUrls = promptTarget ? [] : ((await this.clipboard.readMacFileUrls?.()) ?? []);
|
|
1585
1621
|
let attachedFromFileUrls = false;
|
|
1586
1622
|
for (const url of fileUrls) {
|
|
1587
1623
|
const candidate = extractImagePathFromText(url);
|
|
@@ -1606,15 +1642,14 @@ export class InputController {
|
|
|
1606
1642
|
// text. Covers terminals that paste the Finder file path as
|
|
1607
1643
|
// plain text rather than as a `public.file-url` (most macOS
|
|
1608
1644
|
// terminals do this for image clipboards).
|
|
1609
|
-
const imagePath = extractImagePathFromText(text);
|
|
1645
|
+
const imagePath = promptTarget ? null : extractImagePathFromText(text);
|
|
1610
1646
|
if (imagePath) {
|
|
1611
1647
|
await this.handleImagePathPaste(imagePath);
|
|
1612
1648
|
return true;
|
|
1613
1649
|
}
|
|
1614
1650
|
// Route to the focused component when it accepts pastes (modal
|
|
1615
1651
|
// Input prompts), matching the enhanced-paste text path (#2127).
|
|
1616
|
-
const
|
|
1617
|
-
const target = focused && focused !== this.ctx.editor && hasPasteText(focused) ? focused : this.ctx.editor;
|
|
1652
|
+
const target = promptTarget ?? this.ctx.editor;
|
|
1618
1653
|
target.pasteText(text);
|
|
1619
1654
|
this.ctx.ui.requestRender();
|
|
1620
1655
|
return true;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
1
|
+
import { type AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
4
4
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
@@ -63,8 +63,11 @@ import {
|
|
|
63
63
|
setExcludedSearchProviders,
|
|
64
64
|
setPreferredImageProvider,
|
|
65
65
|
setPreferredSearchProvider,
|
|
66
|
+
type ToolSession,
|
|
66
67
|
} from "../../tools";
|
|
68
|
+
import { AskTool, type AskToolDetails, type AskToolInput } from "../../tools/ask";
|
|
67
69
|
import { shortenPath } from "../../tools/render-utils";
|
|
70
|
+
import { ToolAbortError } from "../../tools/tool-errors";
|
|
68
71
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
69
72
|
import { repo } from "../../utils/git";
|
|
70
73
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
@@ -1138,11 +1141,21 @@ export class SelectorController {
|
|
|
1138
1141
|
realLeafId,
|
|
1139
1142
|
this.ctx.ui.terminal.rows,
|
|
1140
1143
|
async (entryId, options) => {
|
|
1141
|
-
// Selecting the current leaf is a no-op (already there)
|
|
1144
|
+
// Selecting the current leaf is normally a no-op (already there) —
|
|
1145
|
+
// unless it's an `ask` toolResult, in which case the re-answer flow
|
|
1146
|
+
// must still be allowed to reopen the picker even though the leaf
|
|
1147
|
+
// doesn't move (chatgpt-codex review on #5895).
|
|
1142
1148
|
if (entryId === realLeafId) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1149
|
+
const currentEntry = this.ctx.sessionManager.getEntry(entryId);
|
|
1150
|
+
const currentIsAskResult =
|
|
1151
|
+
currentEntry?.type === "message" &&
|
|
1152
|
+
currentEntry.message.role === "toolResult" &&
|
|
1153
|
+
currentEntry.message.toolName === "ask";
|
|
1154
|
+
if (!currentIsAskResult) {
|
|
1155
|
+
done();
|
|
1156
|
+
this.ctx.showStatus("Already at this point");
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1146
1159
|
}
|
|
1147
1160
|
|
|
1148
1161
|
// Ask about summarization
|
|
@@ -1204,11 +1217,29 @@ export class SelectorController {
|
|
|
1204
1217
|
}
|
|
1205
1218
|
|
|
1206
1219
|
try {
|
|
1207
|
-
|
|
1220
|
+
let result = await this.ctx.session.navigateTree(entryId, {
|
|
1208
1221
|
summarize: wantsSummary,
|
|
1209
1222
|
customInstructions,
|
|
1223
|
+
allowAskReopen: true,
|
|
1210
1224
|
});
|
|
1211
1225
|
|
|
1226
|
+
// Selecting an `ask` toolResult doesn't land the leaf directly —
|
|
1227
|
+
// re-open the picker with the original questions first, then
|
|
1228
|
+
// complete the navigation as a new sibling branch (issue #5642).
|
|
1229
|
+
if (result.reopenAsk) {
|
|
1230
|
+
const reanswer = await this.#reanswerAsk(result.reopenAsk.questions);
|
|
1231
|
+
if (!reanswer) {
|
|
1232
|
+
this.ctx.showStatus("Re-answer cancelled");
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
result = await this.ctx.session.navigateTree(entryId, {
|
|
1236
|
+
summarize: wantsSummary,
|
|
1237
|
+
customInstructions,
|
|
1238
|
+
allowAskReopen: true,
|
|
1239
|
+
reanswerAskResult: reanswer,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1212
1243
|
if (result.aborted) {
|
|
1213
1244
|
// Summarization aborted - re-show tree selector
|
|
1214
1245
|
this.ctx.showStatus("Branch summarization cancelled");
|
|
@@ -1252,6 +1283,51 @@ export class SelectorController {
|
|
|
1252
1283
|
});
|
|
1253
1284
|
}
|
|
1254
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Re-open the `ask` picker with the original `questions` (issue #5642):
|
|
1288
|
+
* runs a standalone `AskTool.execute()` outside a normal agent turn,
|
|
1289
|
+
* reusing the same picker/dialog primitives a live `ask` tool call gets.
|
|
1290
|
+
* Returns `undefined` when the user cancels — mirrors `navigateTree`'s
|
|
1291
|
+
* cancellation contract instead of throwing.
|
|
1292
|
+
*/
|
|
1293
|
+
async #reanswerAsk(questions: AskToolInput["questions"]): Promise<AgentToolResult<AskToolDetails> | undefined> {
|
|
1294
|
+
const uiContext = this.ctx.getToolUIContext();
|
|
1295
|
+
if (!uiContext) {
|
|
1296
|
+
this.ctx.showError("Ask tool UI is not ready");
|
|
1297
|
+
return undefined;
|
|
1298
|
+
}
|
|
1299
|
+
const toolSession: ToolSession = {
|
|
1300
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
1301
|
+
hasUI: true,
|
|
1302
|
+
settings: this.ctx.settings,
|
|
1303
|
+
getSessionFile: () => this.ctx.sessionManager.getSessionFile() ?? null,
|
|
1304
|
+
getSessionSpawns: () => null,
|
|
1305
|
+
getPlanModeState: () => this.ctx.session.getPlanModeState(),
|
|
1306
|
+
};
|
|
1307
|
+
const askTool = new AskTool(toolSession);
|
|
1308
|
+
const context = this.ctx.session.buildAskReanswerContext(uiContext);
|
|
1309
|
+
let result: AgentToolResult<AskToolDetails>;
|
|
1310
|
+
try {
|
|
1311
|
+
result = await askTool.execute("tree-reanswer", { questions }, undefined, undefined, context);
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
if (error instanceof ToolAbortError) return undefined;
|
|
1314
|
+
throw error;
|
|
1315
|
+
}
|
|
1316
|
+
// The rich ask dialog can race a collab guest choosing "Chat about this"
|
|
1317
|
+
// (`AskTool`'s `chatRedirect` result); that's meaningful inside a live
|
|
1318
|
+
// agent turn, where the model sees the redirect and starts a
|
|
1319
|
+
// conversation, but this standalone re-answer has no turn to hand it
|
|
1320
|
+
// to — completing the navigation with it would silently drop the
|
|
1321
|
+
// user's intent to chat (roboomp review on #5895).
|
|
1322
|
+
if (result.details?.chatRedirect) {
|
|
1323
|
+
this.ctx.showError(
|
|
1324
|
+
"Chat about this isn't available when re-answering from the tree — pick an option or type a custom answer instead.",
|
|
1325
|
+
);
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
return result;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1255
1331
|
async showSessionSelector(): Promise<void> {
|
|
1256
1332
|
const sessions = await SessionManager.list(
|
|
1257
1333
|
this.ctx.sessionManager.getCwd(),
|
|
@@ -481,6 +481,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
481
481
|
proseOnlyThinking = true;
|
|
482
482
|
compactionQueuedMessages: CompactionQueuedMessage[] = [];
|
|
483
483
|
pendingTools = new Map<string, ToolExecutionHandle>();
|
|
484
|
+
transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
484
485
|
pendingBashComponents: BashExecutionComponent[] = [];
|
|
485
486
|
bashComponent: BashExecutionComponent | undefined = undefined;
|
|
486
487
|
pendingPythonComponents: EvalExecutionComponent[] = [];
|
|
@@ -553,6 +554,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
553
554
|
#planModeHasEntered = false;
|
|
554
555
|
#planReviewOverlay: PlanReviewOverlay | undefined;
|
|
555
556
|
#planReviewOverlayHandle: OverlayHandle | undefined;
|
|
557
|
+
#planReviewCancel: (() => void) | undefined;
|
|
556
558
|
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
|
|
557
559
|
mcpManager?: MCPManager;
|
|
558
560
|
readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
@@ -1657,7 +1659,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1657
1659
|
if (options.requestRender !== false) this.ui.requestRender();
|
|
1658
1660
|
}
|
|
1659
1661
|
|
|
1660
|
-
rebuildChatFromMessages(): void {
|
|
1662
|
+
rebuildChatFromMessages(options: { reuseSettledComponents?: boolean } = {}): void {
|
|
1661
1663
|
// Mid-stream rebuilds (e.g. `/shake`, theme/setting changes that touch the
|
|
1662
1664
|
// transcript) replay only committed `state.messages`. The agent's in-flight
|
|
1663
1665
|
// `streamMessage` and its still-pending tool calls live OUTSIDE
|
|
@@ -1690,7 +1692,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1690
1692
|
const context = this.viewSession.buildTranscriptSessionContext({
|
|
1691
1693
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
1692
1694
|
});
|
|
1693
|
-
this
|
|
1695
|
+
// Prune the settled-component cache to the messages this rebuild will
|
|
1696
|
+
// actually render. Message objects stay strongly reachable through
|
|
1697
|
+
// session entries for the whole session, so entries for compacted-away
|
|
1698
|
+
// history would otherwise pin their components' rendered layout caches
|
|
1699
|
+
// forever — exactly the memory a collapsed compaction used to release.
|
|
1700
|
+
const retained = new WeakMap<AgentMessage, Component>();
|
|
1701
|
+
for (const message of context.messages) {
|
|
1702
|
+
const component = this.transcriptMessageComponents.get(message);
|
|
1703
|
+
if (component) retained.set(message, component);
|
|
1704
|
+
}
|
|
1705
|
+
this.transcriptMessageComponents = retained;
|
|
1706
|
+
this.renderSessionContext(context, { reuseSettledComponents: options.reuseSettledComponents });
|
|
1694
1707
|
for (const child of liveComponents) {
|
|
1695
1708
|
this.chatContainer.addChild(child);
|
|
1696
1709
|
}
|
|
@@ -2642,6 +2655,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2642
2655
|
settled = true;
|
|
2643
2656
|
resolve(choice);
|
|
2644
2657
|
};
|
|
2658
|
+
this.#planReviewCancel = () => finish(undefined);
|
|
2645
2659
|
const overlay = new PlanReviewOverlay(
|
|
2646
2660
|
planContent,
|
|
2647
2661
|
{
|
|
@@ -2678,11 +2692,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2678
2692
|
}
|
|
2679
2693
|
|
|
2680
2694
|
#hidePlanReview(): void {
|
|
2695
|
+
this.#planReviewCancel = undefined;
|
|
2681
2696
|
this.#planReviewOverlayHandle?.hide();
|
|
2682
2697
|
this.#planReviewOverlayHandle = undefined;
|
|
2683
2698
|
this.#planReviewOverlay = undefined;
|
|
2684
2699
|
}
|
|
2685
2700
|
|
|
2701
|
+
#dismissPlanReview(): void {
|
|
2702
|
+
const cancel = this.#planReviewCancel;
|
|
2703
|
+
this.#planReviewCancel = undefined;
|
|
2704
|
+
cancel?.();
|
|
2705
|
+
this.#hidePlanReview();
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2686
2708
|
#getEditorTerminalPath(): string | null {
|
|
2687
2709
|
if (process.platform === "win32") {
|
|
2688
2710
|
return null;
|
|
@@ -3912,6 +3934,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3912
3934
|
}
|
|
3913
3935
|
|
|
3914
3936
|
resetTranscript(): void {
|
|
3937
|
+
this.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
3915
3938
|
this.chatContainer.dispose();
|
|
3916
3939
|
this.chatContainer.clear();
|
|
3917
3940
|
}
|
|
@@ -3931,6 +3954,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3931
3954
|
}
|
|
3932
3955
|
|
|
3933
3956
|
showPinnedError(message: string): void {
|
|
3957
|
+
this.#dismissPlanReview();
|
|
3934
3958
|
this.errorBannerContainer.clear();
|
|
3935
3959
|
this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
|
|
3936
3960
|
this.ui.requestRender();
|
|
@@ -4137,14 +4161,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4137
4161
|
|
|
4138
4162
|
addMessageToChat(
|
|
4139
4163
|
message: AgentMessage,
|
|
4140
|
-
options?: {
|
|
4164
|
+
options?: {
|
|
4165
|
+
populateHistory?: boolean;
|
|
4166
|
+
imageLinks?: readonly (string | undefined)[];
|
|
4167
|
+
reuseSettledComponent?: boolean;
|
|
4168
|
+
},
|
|
4141
4169
|
): Component[] {
|
|
4142
4170
|
return this.#uiHelpers.addMessageToChat(message, options);
|
|
4143
4171
|
}
|
|
4144
4172
|
|
|
4145
4173
|
renderSessionContext(
|
|
4146
4174
|
sessionContext: SessionContext,
|
|
4147
|
-
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
4175
|
+
options?: { updateFooter?: boolean; populateHistory?: boolean; reuseSettledComponents?: boolean },
|
|
4148
4176
|
): void {
|
|
4149
4177
|
for (const message of sessionContext.messages) {
|
|
4150
4178
|
this.noteDisplayableThinkingContent(message);
|
|
@@ -4635,6 +4663,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4635
4663
|
return this.#extensionUiController.initHooksAndCustomTools();
|
|
4636
4664
|
}
|
|
4637
4665
|
|
|
4666
|
+
getToolUIContext(): ExtensionUIContext | undefined {
|
|
4667
|
+
return this.#extensionUiController.getToolUIContext();
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4638
4670
|
emitCustomToolSessionEvent(
|
|
4639
4671
|
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
4640
4672
|
previousSessionFile?: string,
|
package/src/modes/types.ts
CHANGED
|
@@ -176,6 +176,8 @@ export interface InteractiveModeContext {
|
|
|
176
176
|
noteDisplayableThinkingContent(message: AgentMessage): boolean;
|
|
177
177
|
proseOnlyThinking: boolean;
|
|
178
178
|
compactionQueuedMessages: CompactionQueuedMessage[];
|
|
179
|
+
/** Settled user/assistant components reusable across post-compaction transcript rebuilds. */
|
|
180
|
+
transcriptMessageComponents: WeakMap<AgentMessage, Component>;
|
|
179
181
|
pendingTools: Map<string, ToolExecutionHandle>;
|
|
180
182
|
pendingBashComponents: BashExecutionComponent[];
|
|
181
183
|
bashComponent: BashExecutionComponent | undefined;
|
|
@@ -302,11 +304,15 @@ export interface InteractiveModeContext {
|
|
|
302
304
|
isKnownSlashCommand(text: string): boolean;
|
|
303
305
|
addMessageToChat(
|
|
304
306
|
message: AgentMessage,
|
|
305
|
-
options?: {
|
|
307
|
+
options?: {
|
|
308
|
+
populateHistory?: boolean;
|
|
309
|
+
imageLinks?: readonly (string | undefined)[];
|
|
310
|
+
reuseSettledComponent?: boolean;
|
|
311
|
+
},
|
|
306
312
|
): Component[];
|
|
307
313
|
renderSessionContext(
|
|
308
314
|
sessionContext: SessionContext,
|
|
309
|
-
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
315
|
+
options?: { updateFooter?: boolean; populateHistory?: boolean; reuseSettledComponents?: boolean },
|
|
310
316
|
): void;
|
|
311
317
|
renderInitialMessages(options?: { preserveExistingChat?: boolean; clearTerminalHistory?: boolean }): void;
|
|
312
318
|
getUserMessageText(message: Message): string;
|
|
@@ -315,7 +321,7 @@ export interface InteractiveModeContext {
|
|
|
315
321
|
/** Refresh the running-subagents status badge from the active local or collab registry. */
|
|
316
322
|
syncRunningSubagentBadge(): void;
|
|
317
323
|
updateEditorBorderColor(): void;
|
|
318
|
-
rebuildChatFromMessages(): void;
|
|
324
|
+
rebuildChatFromMessages(options?: { reuseSettledComponents?: boolean }): void;
|
|
319
325
|
setTodos(todos: TodoItem[] | TodoPhase[]): void;
|
|
320
326
|
reloadTodos(): Promise<void>;
|
|
321
327
|
toggleTodoExpansion(): void;
|
|
@@ -422,6 +428,13 @@ export interface InteractiveModeContext {
|
|
|
422
428
|
|
|
423
429
|
// Hook UI methods
|
|
424
430
|
initHooksAndCustomTools(): Promise<void>;
|
|
431
|
+
/**
|
|
432
|
+
* The live `ExtensionUIContext` (picker/dialog primitives) used for tool
|
|
433
|
+
* execution, `undefined` before hooks have initialized. `/tree` `ask`
|
|
434
|
+
* re-answer (issue #5642) reuses it to drive a standalone
|
|
435
|
+
* `AskTool.execute()` call.
|
|
436
|
+
*/
|
|
437
|
+
getToolUIContext(): ExtensionUIContext | undefined;
|
|
425
438
|
emitCustomToolSessionEvent(
|
|
426
439
|
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
427
440
|
previousSessionFile?: string,
|
|
@@ -66,6 +66,17 @@ type QueuedMessages = {
|
|
|
66
66
|
steering: string[];
|
|
67
67
|
followUp: string[];
|
|
68
68
|
};
|
|
69
|
+
type AddMessageOptions = {
|
|
70
|
+
populateHistory?: boolean;
|
|
71
|
+
imageLinks?: readonly (string | undefined)[];
|
|
72
|
+
reuseSettledComponent?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type RenderSessionContextOptions = {
|
|
76
|
+
updateFooter?: boolean;
|
|
77
|
+
populateHistory?: boolean;
|
|
78
|
+
reuseSettledComponents?: boolean;
|
|
79
|
+
};
|
|
69
80
|
|
|
70
81
|
function imageLinksForMessage(
|
|
71
82
|
message: Extract<AgentMessage, { role: "developer" | "user" }>,
|
|
@@ -118,10 +129,7 @@ export class UiHelpers {
|
|
|
118
129
|
this.ctx.lastStatusText = text;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
addMessageToChat(
|
|
122
|
-
message: AgentMessage,
|
|
123
|
-
options?: { populateHistory?: boolean; imageLinks?: readonly (string | undefined)[] },
|
|
124
|
-
): Component[] {
|
|
132
|
+
addMessageToChat(message: AgentMessage, options?: AddMessageOptions): Component[] {
|
|
125
133
|
switch (message.role) {
|
|
126
134
|
case "bashExecution": {
|
|
127
135
|
const component = new BashExecutionComponent(message.command, this.ctx.ui, message.excludeFromContext);
|
|
@@ -233,13 +241,22 @@ export class UiHelpers {
|
|
|
233
241
|
const textContent = this.ctx.getUserMessageText(message);
|
|
234
242
|
if (textContent) {
|
|
235
243
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
const cached = options?.reuseSettledComponent
|
|
245
|
+
? this.ctx.transcriptMessageComponents.get(message)
|
|
246
|
+
: undefined;
|
|
247
|
+
let userComponent: UserMessageComponent;
|
|
248
|
+
if (cached instanceof UserMessageComponent) {
|
|
249
|
+
userComponent = cached;
|
|
250
|
+
} else {
|
|
251
|
+
const imageLinks =
|
|
252
|
+
options?.imageLinks ??
|
|
253
|
+
imageLinksForMessage(
|
|
254
|
+
message,
|
|
255
|
+
this.ctx.viewSession.sessionManager.putBlobSync.bind(this.ctx.viewSession.sessionManager),
|
|
256
|
+
);
|
|
257
|
+
userComponent = new UserMessageComponent(textContent, isSynthetic, imageLinks);
|
|
258
|
+
this.ctx.transcriptMessageComponents.set(message, userComponent);
|
|
259
|
+
}
|
|
243
260
|
this.ctx.chatContainer.addChild(userComponent);
|
|
244
261
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
245
262
|
this.ctx.editor.addToHistory(textContent);
|
|
@@ -248,10 +265,16 @@ export class UiHelpers {
|
|
|
248
265
|
break;
|
|
249
266
|
}
|
|
250
267
|
case "assistant": {
|
|
251
|
-
const
|
|
252
|
-
this.ctx
|
|
253
|
-
|
|
254
|
-
|
|
268
|
+
const cached = options?.reuseSettledComponent
|
|
269
|
+
? this.ctx.transcriptMessageComponents.get(message)
|
|
270
|
+
: undefined;
|
|
271
|
+
const assistantComponent =
|
|
272
|
+
cached instanceof AssistantMessageComponent
|
|
273
|
+
? cached
|
|
274
|
+
: createAssistantMessageComponent(this.ctx, splitAssistantMessageToolTimeline(message).beforeTools);
|
|
275
|
+
if (cached !== assistantComponent) {
|
|
276
|
+
this.ctx.transcriptMessageComponents.set(message, assistantComponent);
|
|
277
|
+
}
|
|
255
278
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
256
279
|
break;
|
|
257
280
|
}
|
|
@@ -272,10 +295,7 @@ export class UiHelpers {
|
|
|
272
295
|
* @param options.updateFooter Update footer state
|
|
273
296
|
* @param options.populateHistory Add user messages to editor history
|
|
274
297
|
*/
|
|
275
|
-
renderSessionContext(
|
|
276
|
-
sessionContext: SessionContext,
|
|
277
|
-
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
|
278
|
-
): void {
|
|
298
|
+
renderSessionContext(sessionContext: SessionContext, options: RenderSessionContextOptions = {}): void {
|
|
279
299
|
// Preserved: message_start handler owns this lifecycle (see #783)
|
|
280
300
|
this.ctx.pendingTools.clear();
|
|
281
301
|
// Reseed the cache-invalidation baseline: this rebuild re-derives every
|
|
@@ -362,7 +382,7 @@ export class UiHelpers {
|
|
|
362
382
|
// Assistant messages need special handling for tool calls
|
|
363
383
|
if (message.role === "assistant") {
|
|
364
384
|
const timeline = splitAssistantMessageToolTimeline(message);
|
|
365
|
-
this.ctx.addMessageToChat(message);
|
|
385
|
+
this.ctx.addMessageToChat(message, { reuseSettledComponent: options.reuseSettledComponents });
|
|
366
386
|
const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
|
|
367
387
|
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
|
368
388
|
if (assistantComponent) {
|
|
@@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
|
|
|
13
13
|
<helpers>
|
|
14
14
|
State persists across eval calls, so scout in one call and fan out in the next. Every eval call has:
|
|
15
15
|
|
|
16
|
-
- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("
|
|
16
|
+
- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("scout", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes. Recursion follows `task.maxRecursionDepth` (default 2; a negative value disables the cap): main agent depth = 0, each `agent()` child increments depth by 1, and, when the cap is non-negative, a spawner may call `agent()` only while its current `taskDepth < cap`. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts.
|
|
17
17
|
- `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
|
|
18
18
|
- `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
|
|
19
19
|
- `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
|