@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- 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/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- 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/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -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/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -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 +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- 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/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- 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/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- 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/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- 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/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -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/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- 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
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -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;
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
searchSmitheryRegistry,
|
|
48
48
|
toConfigName,
|
|
49
49
|
} from "../../mcp/smithery-registry";
|
|
50
|
-
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
50
|
+
import type { MCPAuthChallenge, MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
51
51
|
import { shortenPath } from "../../tools/render-utils";
|
|
52
52
|
import { urlHyperlinkAlways } from "../../tui";
|
|
53
53
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
@@ -1042,7 +1042,10 @@ export class MCPCommandController {
|
|
|
1042
1042
|
return next;
|
|
1043
1043
|
}
|
|
1044
1044
|
|
|
1045
|
-
async #resolveOAuthEndpointsFromServer(
|
|
1045
|
+
async #resolveOAuthEndpointsFromServer(
|
|
1046
|
+
config: MCPServerConfig,
|
|
1047
|
+
authChallenge?: MCPAuthChallenge,
|
|
1048
|
+
): Promise<OAuthEndpoints> {
|
|
1046
1049
|
// Stdio servers manage credentials inside the child process; OMP's OAuth
|
|
1047
1050
|
// flow only applies to http/sse transports. Without this guard the
|
|
1048
1051
|
// unauthenticated preflight below spawns the child, which happily reuses
|
|
@@ -1068,13 +1071,20 @@ export class MCPCommandController {
|
|
|
1068
1071
|
connectionError = error as Error;
|
|
1069
1072
|
}
|
|
1070
1073
|
|
|
1071
|
-
// Server connected fine without auth — reauth is not needed
|
|
1072
|
-
|
|
1074
|
+
// Server connected fine without auth — reauth is not needed. A tool-level
|
|
1075
|
+
// challenge overrides this: servers may allow the anonymous handshake yet
|
|
1076
|
+
// protect individual tool calls with `_meta["mcp/www_authenticate"]`.
|
|
1077
|
+
if (connectionSucceeded && !authChallenge) {
|
|
1073
1078
|
throw new Error("Server connection succeeded without OAuth; reauthorization is not required.");
|
|
1074
1079
|
}
|
|
1075
1080
|
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1081
|
+
// Tool calls can carry richer RFC 6750/RFC 9728 hints than the original
|
|
1082
|
+
// connection error. Feed those hints through the same analyzer so
|
|
1083
|
+
// resource_metadata and scope reach protected-resource discovery.
|
|
1084
|
+
const authError = authChallenge
|
|
1085
|
+
? new Error(`${connectionError?.message ?? "HTTP 401"}\n${authChallenge.wwwAuthenticate.join("\n")}`)
|
|
1086
|
+
: connectionError!;
|
|
1087
|
+
const authResult = analyzeAuthError(authError, "url" in config ? config.url : undefined);
|
|
1078
1088
|
let oauth = authResult.authType === "oauth" ? (authResult.oauth ?? null) : null;
|
|
1079
1089
|
|
|
1080
1090
|
if (!oauth && (config.type === "http" || config.type === "sse") && config.url) {
|
|
@@ -1678,21 +1688,29 @@ export class MCPCommandController {
|
|
|
1678
1688
|
}
|
|
1679
1689
|
}
|
|
1680
1690
|
|
|
1681
|
-
|
|
1691
|
+
/** Reauthorize a server after a tool-level OAuth challenge. */
|
|
1692
|
+
async handleMCPAuthChallenge(name: string, challenge: MCPAuthChallenge): Promise<MCPServerConfig | undefined> {
|
|
1693
|
+
return this.#handleReauth(name, { silent: true, reload: false, authChallenge: challenge });
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async #handleReauth(
|
|
1697
|
+
name: string | undefined,
|
|
1698
|
+
options: { silent?: boolean; reload?: boolean; authChallenge?: MCPAuthChallenge } = {},
|
|
1699
|
+
): Promise<MCPServerConfig | undefined> {
|
|
1682
1700
|
if (!name) {
|
|
1683
|
-
this.ctx.showError("Server name required. Usage: /mcp reauth <name>");
|
|
1701
|
+
if (!options.silent) this.ctx.showError("Server name required. Usage: /mcp reauth <name>");
|
|
1684
1702
|
return;
|
|
1685
1703
|
}
|
|
1686
1704
|
|
|
1687
1705
|
try {
|
|
1688
1706
|
const found = await this.#resolveServerForAuth(name);
|
|
1689
1707
|
if (!found) {
|
|
1690
|
-
this.ctx.showError(`Server "${name}" not found.`);
|
|
1708
|
+
if (!options.silent) this.ctx.showError(`Server "${name}" not found.`);
|
|
1691
1709
|
return;
|
|
1692
1710
|
}
|
|
1693
1711
|
|
|
1694
1712
|
if (found.config.enabled === false) {
|
|
1695
|
-
this.ctx.showError(`Server "${name}" is disabled. Run /mcp enable ${name} first.`);
|
|
1713
|
+
if (!options.silent) this.ctx.showError(`Server "${name}" is disabled. Run /mcp enable ${name} first.`);
|
|
1696
1714
|
return;
|
|
1697
1715
|
}
|
|
1698
1716
|
|
|
@@ -1705,7 +1723,7 @@ export class MCPCommandController {
|
|
|
1705
1723
|
// happened yet if the server turns out not to need (or support) OAuth.
|
|
1706
1724
|
// Use the same env-expanded config shape runtime discovery passes to
|
|
1707
1725
|
// MCPManager; the raw file value may contain `${...}` placeholders.
|
|
1708
|
-
const oauth = await this.#resolveOAuthEndpointsFromServer(runtimeBaseConfig);
|
|
1726
|
+
const oauth = await this.#resolveOAuthEndpointsFromServer(runtimeBaseConfig, options.authChallenge);
|
|
1709
1727
|
const serverUrl =
|
|
1710
1728
|
runtimeBaseConfig.type === "http" || runtimeBaseConfig.type === "sse" ? runtimeBaseConfig.url : undefined;
|
|
1711
1729
|
// A user-supplied client secret may live in either block (the wizard
|
|
@@ -1719,7 +1737,9 @@ export class MCPCommandController {
|
|
|
1719
1737
|
const userClientSecret = found.config.oauth?.clientSecret ?? currentAuth?.clientSecret;
|
|
1720
1738
|
const flowClientSecret = userClientSecret ?? storedClientSecret ?? "";
|
|
1721
1739
|
|
|
1722
|
-
|
|
1740
|
+
if (!options.silent) {
|
|
1741
|
+
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1742
|
+
}
|
|
1723
1743
|
|
|
1724
1744
|
const currentAuthResource = currentAuth?.resource ? expandEnvVarsDeep(currentAuth.resource) : undefined;
|
|
1725
1745
|
const oauthResource =
|
|
@@ -1755,39 +1775,49 @@ export class MCPCommandController {
|
|
|
1755
1775
|
// Definition-only entries resolve through the url-keyed binding alone;
|
|
1756
1776
|
// skip the write-back so a committed project mcp.json stays clean.
|
|
1757
1777
|
const urlKeyedId = serverUrl ? mcpOAuthCredentialId(serverUrl) : undefined;
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1778
|
+
const shouldPersist = currentAuth || oauthResult.credentialId !== urlKeyedId;
|
|
1779
|
+
const updatedConfig = shouldPersist
|
|
1780
|
+
? this.#persistOAuthResult(baseConfig, oauthResult, {
|
|
1781
|
+
tokenUrl: oauth.tokenUrl,
|
|
1782
|
+
clientId: oauth.clientId,
|
|
1783
|
+
userClientSecret,
|
|
1784
|
+
resource: oauthResource,
|
|
1785
|
+
stripSameOriginResource: oauthResourceIsFallback,
|
|
1786
|
+
})
|
|
1787
|
+
: baseConfig;
|
|
1788
|
+
if (shouldPersist) {
|
|
1789
|
+
await updateMCPServer(found.filePath, name, updatedConfig);
|
|
1790
|
+
}
|
|
1791
|
+
if (options.reload !== false) {
|
|
1792
|
+
await this.#reloadMCP();
|
|
1793
|
+
const state = await this.#waitForServerConnectionWithAnimation(name);
|
|
1770
1794
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1795
|
+
const lines = [
|
|
1796
|
+
"",
|
|
1797
|
+
theme.fg("success", `✓ Reauthorized "${name}" (${found.scope} config)`),
|
|
1798
|
+
"",
|
|
1799
|
+
` Status: ${
|
|
1800
|
+
state === "connected"
|
|
1801
|
+
? theme.fg("success", "connected")
|
|
1802
|
+
: state === "connecting"
|
|
1803
|
+
? theme.fg("muted", "connecting")
|
|
1804
|
+
: theme.fg("warning", "not connected")
|
|
1805
|
+
}`,
|
|
1806
|
+
"",
|
|
1807
|
+
];
|
|
1808
|
+
this.#showMessage(lines.join("\n"));
|
|
1809
|
+
}
|
|
1810
|
+
return updatedConfig;
|
|
1785
1811
|
} catch (error) {
|
|
1786
1812
|
if (error instanceof MCPOAuthCancelledError) {
|
|
1787
|
-
this.ctx.showStatus(`Reauthorization cancelled for "${name}"`);
|
|
1813
|
+
if (!options.silent) this.ctx.showStatus(`Reauthorization cancelled for "${name}"`);
|
|
1788
1814
|
return;
|
|
1789
1815
|
}
|
|
1790
|
-
|
|
1816
|
+
if (!options.silent) {
|
|
1817
|
+
this.ctx.showError(
|
|
1818
|
+
`Failed to reauthorize server: ${error instanceof Error ? error.message : String(error)}`,
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1791
1821
|
}
|
|
1792
1822
|
}
|
|
1793
1823
|
|
|
@@ -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;
|
|
@@ -654,6 +656,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
654
656
|
this.#toolUiContextSetter = setToolUIContext;
|
|
655
657
|
this.lspServers = lspServers;
|
|
656
658
|
this.mcpManager = mcpManager;
|
|
659
|
+
this.mcpManager?.setAuthHandler((serverName, challenge) =>
|
|
660
|
+
new MCPCommandController(this).handleMCPAuthChallenge(serverName, challenge),
|
|
661
|
+
);
|
|
657
662
|
this.#eventBus = eventBus;
|
|
658
663
|
if (eventBus) {
|
|
659
664
|
this.#eventBusUnsubscribers.push(
|
|
@@ -1657,7 +1662,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1657
1662
|
if (options.requestRender !== false) this.ui.requestRender();
|
|
1658
1663
|
}
|
|
1659
1664
|
|
|
1660
|
-
rebuildChatFromMessages(): void {
|
|
1665
|
+
rebuildChatFromMessages(options: { reuseSettledComponents?: boolean } = {}): void {
|
|
1661
1666
|
// Mid-stream rebuilds (e.g. `/shake`, theme/setting changes that touch the
|
|
1662
1667
|
// transcript) replay only committed `state.messages`. The agent's in-flight
|
|
1663
1668
|
// `streamMessage` and its still-pending tool calls live OUTSIDE
|
|
@@ -1690,7 +1695,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1690
1695
|
const context = this.viewSession.buildTranscriptSessionContext({
|
|
1691
1696
|
collapseCompactedHistory: settings.get("display.collapseCompacted"),
|
|
1692
1697
|
});
|
|
1693
|
-
this
|
|
1698
|
+
// Prune the settled-component cache to the messages this rebuild will
|
|
1699
|
+
// actually render. Message objects stay strongly reachable through
|
|
1700
|
+
// session entries for the whole session, so entries for compacted-away
|
|
1701
|
+
// history would otherwise pin their components' rendered layout caches
|
|
1702
|
+
// forever — exactly the memory a collapsed compaction used to release.
|
|
1703
|
+
const retained = new WeakMap<AgentMessage, Component>();
|
|
1704
|
+
for (const message of context.messages) {
|
|
1705
|
+
const component = this.transcriptMessageComponents.get(message);
|
|
1706
|
+
if (component) retained.set(message, component);
|
|
1707
|
+
}
|
|
1708
|
+
this.transcriptMessageComponents = retained;
|
|
1709
|
+
this.renderSessionContext(context, { reuseSettledComponents: options.reuseSettledComponents });
|
|
1694
1710
|
for (const child of liveComponents) {
|
|
1695
1711
|
this.chatContainer.addChild(child);
|
|
1696
1712
|
}
|
|
@@ -2642,6 +2658,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2642
2658
|
settled = true;
|
|
2643
2659
|
resolve(choice);
|
|
2644
2660
|
};
|
|
2661
|
+
this.#planReviewCancel = () => finish(undefined);
|
|
2645
2662
|
const overlay = new PlanReviewOverlay(
|
|
2646
2663
|
planContent,
|
|
2647
2664
|
{
|
|
@@ -2678,11 +2695,19 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2678
2695
|
}
|
|
2679
2696
|
|
|
2680
2697
|
#hidePlanReview(): void {
|
|
2698
|
+
this.#planReviewCancel = undefined;
|
|
2681
2699
|
this.#planReviewOverlayHandle?.hide();
|
|
2682
2700
|
this.#planReviewOverlayHandle = undefined;
|
|
2683
2701
|
this.#planReviewOverlay = undefined;
|
|
2684
2702
|
}
|
|
2685
2703
|
|
|
2704
|
+
#dismissPlanReview(): void {
|
|
2705
|
+
const cancel = this.#planReviewCancel;
|
|
2706
|
+
this.#planReviewCancel = undefined;
|
|
2707
|
+
cancel?.();
|
|
2708
|
+
this.#hidePlanReview();
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2686
2711
|
#getEditorTerminalPath(): string | null {
|
|
2687
2712
|
if (process.platform === "win32") {
|
|
2688
2713
|
return null;
|
|
@@ -3912,6 +3937,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3912
3937
|
}
|
|
3913
3938
|
|
|
3914
3939
|
resetTranscript(): void {
|
|
3940
|
+
this.transcriptMessageComponents = new WeakMap<AgentMessage, Component>();
|
|
3915
3941
|
this.chatContainer.dispose();
|
|
3916
3942
|
this.chatContainer.clear();
|
|
3917
3943
|
}
|
|
@@ -3931,6 +3957,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3931
3957
|
}
|
|
3932
3958
|
|
|
3933
3959
|
showPinnedError(message: string): void {
|
|
3960
|
+
this.#dismissPlanReview();
|
|
3934
3961
|
this.errorBannerContainer.clear();
|
|
3935
3962
|
this.errorBannerContainer.addChild(new ErrorBannerComponent(message));
|
|
3936
3963
|
this.ui.requestRender();
|
|
@@ -4137,14 +4164,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4137
4164
|
|
|
4138
4165
|
addMessageToChat(
|
|
4139
4166
|
message: AgentMessage,
|
|
4140
|
-
options?: {
|
|
4167
|
+
options?: {
|
|
4168
|
+
populateHistory?: boolean;
|
|
4169
|
+
imageLinks?: readonly (string | undefined)[];
|
|
4170
|
+
reuseSettledComponent?: boolean;
|
|
4171
|
+
},
|
|
4141
4172
|
): Component[] {
|
|
4142
4173
|
return this.#uiHelpers.addMessageToChat(message, options);
|
|
4143
4174
|
}
|
|
4144
4175
|
|
|
4145
4176
|
renderSessionContext(
|
|
4146
4177
|
sessionContext: SessionContext,
|
|
4147
|
-
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
|
4178
|
+
options?: { updateFooter?: boolean; populateHistory?: boolean; reuseSettledComponents?: boolean },
|
|
4148
4179
|
): void {
|
|
4149
4180
|
for (const message of sessionContext.messages) {
|
|
4150
4181
|
this.noteDisplayableThinkingContent(message);
|
|
@@ -4635,6 +4666,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4635
4666
|
return this.#extensionUiController.initHooksAndCustomTools();
|
|
4636
4667
|
}
|
|
4637
4668
|
|
|
4669
|
+
getToolUIContext(): ExtensionUIContext | undefined {
|
|
4670
|
+
return this.#extensionUiController.getToolUIContext();
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4638
4673
|
emitCustomToolSessionEvent(
|
|
4639
4674
|
reason: "start" | "switch" | "branch" | "tree" | "shutdown",
|
|
4640
4675
|
previousSessionFile?: string,
|