@oh-my-pi/pi-coding-agent 16.5.1 → 16.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/dist/cli.js +3442 -3408
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +136 -49
- package/src/advisor/runtime.ts +16 -34
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/config-cli.ts +15 -3
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/backend.ts +3 -3
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/prelude.py +28 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +7 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +4 -3
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +9 -2
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +28 -31
- package/src/session/agent-session.ts +44 -22
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -277,6 +277,8 @@ export interface MCPOAuthConfig {
|
|
|
277
277
|
authorizationUrl: string;
|
|
278
278
|
/** Token endpoint URL */
|
|
279
279
|
tokenUrl: string;
|
|
280
|
+
/** Dynamic client registration endpoint advertised by the authorization server. */
|
|
281
|
+
registrationUrl?: string;
|
|
280
282
|
/** Client ID (optional when already embedded in authorization URL) */
|
|
281
283
|
clientId?: string;
|
|
282
284
|
/** Client secret (optional for PKCE flows) */
|
|
@@ -561,7 +563,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
561
563
|
* accept the later authorize request for the same scope set.
|
|
562
564
|
*/
|
|
563
565
|
async #tryRegisterClient(redirectUri: string): Promise<void> {
|
|
564
|
-
const registrationEndpoint = await this.#resolveRegistrationEndpoint();
|
|
566
|
+
const registrationEndpoint = this.config.registrationUrl ?? (await this.#resolveRegistrationEndpoint());
|
|
565
567
|
if (!registrationEndpoint) return;
|
|
566
568
|
|
|
567
569
|
try {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { getThemeByName, setThemeInstance, theme } from "../../theme/theme";
|
|
6
|
+
import { DynamicBorder } from "../dynamic-border";
|
|
7
|
+
|
|
8
|
+
const componentPath = path.resolve(import.meta.dir, "../dynamic-border.ts");
|
|
9
|
+
|
|
10
|
+
describe("DynamicBorder", () => {
|
|
11
|
+
// Regression for #5366: extensions importing legacy pi UI components get a
|
|
12
|
+
// second `src` module graph whose module-level `theme` is never assigned by
|
|
13
|
+
// host startup. `render()` must degrade to plain glyphs instead of throwing
|
|
14
|
+
// "undefined is not an object (evaluating 'theme.boxRound')" and killing the
|
|
15
|
+
// TUI. Other test files in the same process initialize the theme module, so
|
|
16
|
+
// the uninitialized state is only reachable in a fresh module graph — a
|
|
17
|
+
// subprocess reproduces the actual reported scenario deterministically.
|
|
18
|
+
it("renders plain glyphs when the module-level theme is uninitialized", async () => {
|
|
19
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-dynamic-border-"));
|
|
20
|
+
try {
|
|
21
|
+
const script = [
|
|
22
|
+
`import { DynamicBorder } from ${JSON.stringify(componentPath)};`,
|
|
23
|
+
'const custom = new DynamicBorder(str => "<" + str + ">");',
|
|
24
|
+
`if (JSON.stringify(custom.render(4)) !== JSON.stringify(["<────>"])) process.exit(1);`,
|
|
25
|
+
"const plain = new DynamicBorder();",
|
|
26
|
+
`if (JSON.stringify(plain.render(3)) !== JSON.stringify(["───"])) process.exit(2);`,
|
|
27
|
+
"",
|
|
28
|
+
].join("\n");
|
|
29
|
+
const file = path.join(dir, "uninitialized-theme.ts");
|
|
30
|
+
await Bun.write(file, script);
|
|
31
|
+
const proc = Bun.spawn(["bun", file], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
|
|
32
|
+
const [exitCode, stderr] = await Promise.all([
|
|
33
|
+
proc.exited,
|
|
34
|
+
new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
35
|
+
]);
|
|
36
|
+
expect(stderr).toBe("");
|
|
37
|
+
expect(exitCode).toBe(0);
|
|
38
|
+
} finally {
|
|
39
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("paints with theme.boxRound.horizontal once the theme is initialized", async () => {
|
|
44
|
+
const previous = theme;
|
|
45
|
+
const loaded = await getThemeByName("dark");
|
|
46
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
47
|
+
setThemeInstance(loaded);
|
|
48
|
+
try {
|
|
49
|
+
const border = new DynamicBorder(str => `<${str}>`);
|
|
50
|
+
expect(border.render(3)).toEqual([`<${loaded.boxRound.horizontal.repeat(3)}>`]);
|
|
51
|
+
} finally {
|
|
52
|
+
if (previous) setThemeInstance(previous);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1142,11 +1142,11 @@ export class AgentDashboard extends Container {
|
|
|
1142
1142
|
return;
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
-
if (matchesSelectUp(data) || data
|
|
1145
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
1146
1146
|
this.#moveSelection(-1);
|
|
1147
1147
|
return;
|
|
1148
1148
|
}
|
|
1149
|
-
if (matchesSelectDown(data) || data
|
|
1149
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
1150
1150
|
this.#moveSelection(1);
|
|
1151
1151
|
return;
|
|
1152
1152
|
}
|
|
@@ -340,6 +340,19 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
340
340
|
this.#handleTableInput(keyData);
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Seed the table's left-left close detector with the current time so a single
|
|
345
|
+
* subsequent `←` (within {@link LEFT_TAP_WINDOW_MS}) dismisses the hub.
|
|
346
|
+
*
|
|
347
|
+
* The editor's own double-tap detector consumes the `←←` that opens the hub,
|
|
348
|
+
* leaving this detector at its fresh `0` — without this handoff the user would
|
|
349
|
+
* have to press `←←` a second time to escape. Called by the opener when the hub
|
|
350
|
+
* was raised by that gesture.
|
|
351
|
+
*/
|
|
352
|
+
armCloseTap(): void {
|
|
353
|
+
this.#lastLeftTap = Date.now();
|
|
354
|
+
}
|
|
355
|
+
|
|
343
356
|
/**
|
|
344
357
|
* Open the fullscreen transcript viewer for an agent id (public for table Enter
|
|
345
358
|
* and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
|
|
@@ -573,14 +586,14 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
573
586
|
}
|
|
574
587
|
return;
|
|
575
588
|
}
|
|
576
|
-
if (keyData
|
|
589
|
+
if (matchesKey(keyData, "j") || matchesSelectDown(keyData)) {
|
|
577
590
|
if (this.#rows.length > 0) {
|
|
578
591
|
this.#selectedRow = Math.min(this.#selectedRow + 1, this.#rows.length - 1);
|
|
579
592
|
}
|
|
580
593
|
this.#requestRender();
|
|
581
594
|
return;
|
|
582
595
|
}
|
|
583
|
-
if (keyData
|
|
596
|
+
if (matchesKey(keyData, "k") || matchesSelectUp(keyData)) {
|
|
584
597
|
if (this.#rows.length > 0) {
|
|
585
598
|
this.#selectedRow = Math.max(this.#selectedRow - 1, 0);
|
|
586
599
|
}
|
|
@@ -496,9 +496,9 @@ export class AgentTranscriptViewer implements Component {
|
|
|
496
496
|
this.deps.requestRender();
|
|
497
497
|
return true;
|
|
498
498
|
}
|
|
499
|
-
if (data
|
|
499
|
+
if (matchesKey(data, "j") || matchesSelectDown(data)) {
|
|
500
500
|
this.#scrollView.scroll(1);
|
|
501
|
-
} else if (data
|
|
501
|
+
} else if (matchesKey(data, "k") || matchesSelectUp(data)) {
|
|
502
502
|
this.#scrollView.scroll(-1);
|
|
503
503
|
} else if (data === "g") {
|
|
504
504
|
this.#scrollView.scrollToTop();
|
|
@@ -280,7 +280,7 @@ export class ChatTranscriptBuilder {
|
|
|
280
280
|
hideThinkingBlock,
|
|
281
281
|
() => this.deps.requestRender(),
|
|
282
282
|
this.deps.getMessageRenderer ? undefined : [], // placeholder for thinkingRenderers
|
|
283
|
-
|
|
283
|
+
this.deps.ui.imageBudget,
|
|
284
284
|
proseOnlyThinking,
|
|
285
285
|
);
|
|
286
286
|
this.container.addChild(assistantComponent);
|
|
@@ -352,8 +352,9 @@ export class ChatTranscriptBuilder {
|
|
|
352
352
|
content.name,
|
|
353
353
|
content.arguments,
|
|
354
354
|
{
|
|
355
|
-
//
|
|
356
|
-
|
|
355
|
+
// Stable ids and Kitty placeholder cells keep images anchored
|
|
356
|
+
// while the transcript viewport scrolls and reflows.
|
|
357
|
+
showImages: settings.get("terminal.showImages"),
|
|
357
358
|
editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
|
|
358
359
|
editAllowFuzzy: settings.get("edit.fuzzyMatch"),
|
|
359
360
|
liveRegion: this.container,
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
|
-
import { theme } from "../../modes/theme/theme";
|
|
2
|
+
import { fgOrPlain, theme } from "../../modes/theme/theme";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Dynamic border component that adjusts to viewport width.
|
|
6
6
|
*
|
|
7
|
-
* Note:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Note: the module-level `theme` may be `undefined` — when loaded through jiti
|
|
8
|
+
* (separate module cache) or from a second `src` module graph in npm-package
|
|
9
|
+
* installs, where the host bundle assigns `theme` but this copy never sees it
|
|
10
|
+
* (issue #5366). Both the default color and `render()` degrade to plain,
|
|
11
|
+
* unstyled output instead of crashing the TUI.
|
|
10
12
|
*/
|
|
11
13
|
export class DynamicBorder implements Component {
|
|
12
14
|
#color: (str: string) => string;
|
|
13
15
|
#cachedWidth = -1;
|
|
14
16
|
#cachedLines: string[] | undefined;
|
|
15
17
|
|
|
16
|
-
constructor(color: (str: string) => string = str =>
|
|
18
|
+
constructor(color: (str: string) => string = str => fgOrPlain("border", str)) {
|
|
17
19
|
this.#color = color;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -26,7 +28,8 @@ export class DynamicBorder implements Component {
|
|
|
26
28
|
if (this.#cachedLines && this.#cachedWidth === width) {
|
|
27
29
|
return this.#cachedLines;
|
|
28
30
|
}
|
|
29
|
-
const
|
|
31
|
+
const horizontal = typeof theme === "undefined" ? "─" : theme.boxRound.horizontal;
|
|
32
|
+
const lines = [this.#color(horizontal.repeat(Math.max(1, width)))];
|
|
30
33
|
this.#cachedWidth = width;
|
|
31
34
|
this.#cachedLines = lines;
|
|
32
35
|
return lines;
|
|
@@ -449,12 +449,12 @@ export class ExtensionList implements Component {
|
|
|
449
449
|
|
|
450
450
|
handleInput(data: string): void {
|
|
451
451
|
// Navigation
|
|
452
|
-
if (matchesSelectUp(data) || data
|
|
452
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
453
453
|
this.#moveSelectionUp();
|
|
454
454
|
return;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
if (matchesSelectDown(data) || data
|
|
457
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
458
458
|
this.#moveSelectionDown();
|
|
459
459
|
return;
|
|
460
460
|
}
|
|
@@ -652,17 +652,23 @@ export class HookSelectorComponent extends Container {
|
|
|
652
652
|
return;
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
-
if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && keyData
|
|
655
|
+
if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "k"))) {
|
|
656
656
|
this.#moveSelection(-1);
|
|
657
|
-
} else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && keyData
|
|
657
|
+
} else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && matchesKey(keyData, "j"))) {
|
|
658
658
|
this.#moveSelection(1);
|
|
659
659
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
660
660
|
const selected = this.#filteredOptions[this.#selectedIndex];
|
|
661
661
|
if (selected && !this.#isDisabled(selected.index)) this.#onSelectCallback(selected.option.label);
|
|
662
|
-
} else if (
|
|
662
|
+
} else if (
|
|
663
|
+
matchesKey(keyData, "left") ||
|
|
664
|
+
(this.#slider && !this.#isSearchEnabled() && matchesKey(keyData, "h"))
|
|
665
|
+
) {
|
|
663
666
|
if (this.#slider) this.#moveSlider(-1);
|
|
664
667
|
else this.#onLeftCallback?.();
|
|
665
|
-
} else if (
|
|
668
|
+
} else if (
|
|
669
|
+
matchesKey(keyData, "right") ||
|
|
670
|
+
(this.#slider && !this.#isSearchEnabled() && matchesKey(keyData, "l"))
|
|
671
|
+
) {
|
|
666
672
|
if (this.#slider) this.#moveSlider(1);
|
|
667
673
|
else this.#onRightCallback?.();
|
|
668
674
|
} else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
|
|
@@ -164,6 +164,11 @@ export class LoginDialogComponent extends Container {
|
|
|
164
164
|
this.#tui.requestRender();
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** Route non-bracketed paste transports into the active login input. */
|
|
168
|
+
pasteText(text: string): void {
|
|
169
|
+
this.#input.pasteText(text);
|
|
170
|
+
}
|
|
171
|
+
|
|
167
172
|
handleInput(data: string): void {
|
|
168
173
|
const kb = getKeybindings();
|
|
169
174
|
|
|
@@ -63,6 +63,7 @@ export interface MCPAddWizardOAuthResult {
|
|
|
63
63
|
interface MCPAddWizardOAuthOptions {
|
|
64
64
|
serverUrl?: string;
|
|
65
65
|
resource?: string;
|
|
66
|
+
registrationUrl?: string;
|
|
66
67
|
/**
|
|
67
68
|
* External cancellation source. Aborting it tears down the in-flight OAuth
|
|
68
69
|
* flow and surfaces a neutral cancellation error. The wizard wires its own
|
|
@@ -82,6 +83,7 @@ interface WizardState {
|
|
|
82
83
|
authMethod: AuthMethod;
|
|
83
84
|
oauthAuthUrl: string;
|
|
84
85
|
oauthTokenUrl: string;
|
|
86
|
+
oauthRegistrationUrl: string;
|
|
85
87
|
oauthClientId: string;
|
|
86
88
|
oauthClientSecret: string;
|
|
87
89
|
oauthScopes: string;
|
|
@@ -113,6 +115,7 @@ export class MCPAddWizard extends Container {
|
|
|
113
115
|
authMethod: "none",
|
|
114
116
|
oauthAuthUrl: "",
|
|
115
117
|
oauthTokenUrl: "",
|
|
118
|
+
oauthRegistrationUrl: "",
|
|
116
119
|
oauthClientId: "",
|
|
117
120
|
oauthClientSecret: "",
|
|
118
121
|
oauthScopes: "",
|
|
@@ -1026,6 +1029,7 @@ export class MCPAddWizard extends Container {
|
|
|
1026
1029
|
if (oauth) {
|
|
1027
1030
|
this.#state.oauthAuthUrl = oauth.authorizationUrl;
|
|
1028
1031
|
this.#state.oauthTokenUrl = oauth.tokenUrl;
|
|
1032
|
+
this.#state.oauthRegistrationUrl = oauth.registrationUrl || "";
|
|
1029
1033
|
this.#state.oauthClientId = oauth.clientId || "";
|
|
1030
1034
|
this.#state.oauthScopes = oauth.scopes || "";
|
|
1031
1035
|
this.#state.oauthResource = oauth.resource || (this.#state.transport === "stdio" ? "" : this.#state.url);
|
|
@@ -1196,6 +1200,7 @@ export class MCPAddWizard extends Container {
|
|
|
1196
1200
|
this.#state.oauthScopes,
|
|
1197
1201
|
{
|
|
1198
1202
|
serverUrl: this.#state.url || undefined,
|
|
1203
|
+
registrationUrl: this.#state.oauthRegistrationUrl || undefined,
|
|
1199
1204
|
resource: oauthResource || undefined,
|
|
1200
1205
|
abortSignal: this.#oauthAbort.signal,
|
|
1201
1206
|
},
|
|
@@ -400,8 +400,8 @@ export class PlanReviewOverlay implements Component {
|
|
|
400
400
|
// Left/right always drive the slider. The sidebar sits beside the body
|
|
401
401
|
// (above this row), not the slider, so stealing left for it would strand
|
|
402
402
|
// the operator unable to step the model tier back — reach the ToC via Tab.
|
|
403
|
-
const isLeft = matchesKey(data, "left") || (this.#slider !== undefined && data
|
|
404
|
-
const isRight = matchesKey(data, "right") || (this.#slider !== undefined && data
|
|
403
|
+
const isLeft = matchesKey(data, "left") || (this.#slider !== undefined && matchesKey(data, "h"));
|
|
404
|
+
const isRight = matchesKey(data, "right") || (this.#slider !== undefined && matchesKey(data, "l"));
|
|
405
405
|
if (isLeft) {
|
|
406
406
|
this.#moveSlider(-1);
|
|
407
407
|
return;
|
|
@@ -410,12 +410,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
410
410
|
this.#moveSlider(1);
|
|
411
411
|
return;
|
|
412
412
|
}
|
|
413
|
-
if (matchesSelectUp(data) || data
|
|
413
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
414
414
|
if (this.#selectedIndex === this.#firstEnabledIndex()) this.#setFocus("body");
|
|
415
415
|
else this.#moveSelection(-1);
|
|
416
416
|
return;
|
|
417
417
|
}
|
|
418
|
-
if (matchesSelectDown(data) || data
|
|
418
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
419
419
|
this.#moveSelection(1);
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
@@ -427,13 +427,13 @@ export class PlanReviewOverlay implements Component {
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
#handleBody(data: string): void {
|
|
430
|
-
if (matchesKey(data, "left") || data
|
|
430
|
+
if (matchesKey(data, "left") || matchesKey(data, "h")) {
|
|
431
431
|
if (this.#sidebarShown) this.#setFocus("toc");
|
|
432
432
|
return;
|
|
433
433
|
}
|
|
434
434
|
if (
|
|
435
435
|
matchesKey(data, "right") ||
|
|
436
|
-
data
|
|
436
|
+
matchesKey(data, "l") ||
|
|
437
437
|
matchesKey(data, "enter") ||
|
|
438
438
|
matchesKey(data, "return") ||
|
|
439
439
|
data === "\n"
|
|
@@ -444,12 +444,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
444
444
|
// Vertical nav flows between regions at the edges: scrolling off the bottom
|
|
445
445
|
// drops into the actions ("next step"); scrolling off the top steps back up
|
|
446
446
|
// to the ToC.
|
|
447
|
-
if (matchesSelectUp(data) || data
|
|
447
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
448
448
|
if (this.#scrollView.getScrollOffset() <= 0 && this.#sidebarShown) this.#setFocus("toc");
|
|
449
449
|
else this.#scrollView.scroll(-1);
|
|
450
450
|
return;
|
|
451
451
|
}
|
|
452
|
-
if (matchesSelectDown(data) || data
|
|
452
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
453
453
|
if (this.#scrollView.getScrollOffset() >= this.#scrollView.getMaxScrollOffset()) this.#setFocus("actions");
|
|
454
454
|
else this.#scrollView.scroll(1);
|
|
455
455
|
return;
|
|
@@ -470,11 +470,11 @@ export class PlanReviewOverlay implements Component {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
#handleToc(data: string): void {
|
|
473
|
-
if (matchesSelectUp(data) || data
|
|
473
|
+
if (matchesSelectUp(data) || matchesKey(data, "k")) {
|
|
474
474
|
this.#moveTocCursor(-1);
|
|
475
475
|
return;
|
|
476
476
|
}
|
|
477
|
-
if (matchesSelectDown(data) || data
|
|
477
|
+
if (matchesSelectDown(data) || matchesKey(data, "j")) {
|
|
478
478
|
// Past the last section, fall through to the actions ("next step").
|
|
479
479
|
if (this.#tocCursor >= this.#toc.length - 1) this.#setFocus("actions");
|
|
480
480
|
else this.#moveTocCursor(1);
|
|
@@ -482,7 +482,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
482
482
|
}
|
|
483
483
|
if (
|
|
484
484
|
matchesKey(data, "right") ||
|
|
485
|
-
data
|
|
485
|
+
matchesKey(data, "l") ||
|
|
486
486
|
matchesKey(data, "enter") ||
|
|
487
487
|
matchesKey(data, "return") ||
|
|
488
488
|
data === "\n"
|
|
@@ -37,9 +37,6 @@ export function readArgsTargetInternalUrl(args: unknown): boolean {
|
|
|
37
37
|
type ReadRenderArgs = {
|
|
38
38
|
path?: string;
|
|
39
39
|
file_path?: string;
|
|
40
|
-
selector?: string;
|
|
41
|
-
// Legacy field from the old schema; tolerated for rebuilt transcripts.
|
|
42
|
-
sel?: string;
|
|
43
40
|
};
|
|
44
41
|
|
|
45
42
|
type ReadToolSuffixResolution = {
|
|
@@ -344,11 +341,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
344
341
|
|
|
345
342
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
346
343
|
if (!toolCallId) return;
|
|
347
|
-
const
|
|
348
|
-
const rawSelector =
|
|
349
|
-
typeof args.selector === "string" ? args.selector : typeof args.sel === "string" ? args.sel : undefined;
|
|
350
|
-
const selector = rawSelector?.trim().replace(/^:+/, "");
|
|
351
|
-
const rawPath = selector && selector.length > 0 ? `${basePath}:${selector}` : basePath;
|
|
344
|
+
const rawPath = args.file_path || args.path || "";
|
|
352
345
|
const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
|
|
353
346
|
toolCallId,
|
|
354
347
|
path: rawPath,
|
|
@@ -488,14 +488,16 @@ export class InputController {
|
|
|
488
488
|
// main session, or returns the focused subagent view to the main session.
|
|
489
489
|
// Focused ←← intentionally matches Esc. From the main session the gesture
|
|
490
490
|
// stays inert when there are no subagents (requireContent); the explicit
|
|
491
|
-
// hub key still opens the empty roster.
|
|
491
|
+
// hub key still opens the empty roster. `armCloseTap` hands this gesture's
|
|
492
|
+
// tap state to the hub so the same ←← that opened it also arms its close —
|
|
493
|
+
// otherwise the hub's fresh detector demands a second ←← (issue #4780).
|
|
492
494
|
this.ctx.editor.onLeftAtStart = () => {
|
|
493
495
|
if (this.ctx.focusedAgentId) {
|
|
494
496
|
this.#handleFocusedLeftTap();
|
|
495
497
|
return;
|
|
496
498
|
}
|
|
497
499
|
if (this.#detectLeftDoubleTap()) {
|
|
498
|
-
this.ctx.showAgentHub({ requireContent: true });
|
|
500
|
+
this.ctx.showAgentHub({ requireContent: true, armCloseTap: true });
|
|
499
501
|
}
|
|
500
502
|
};
|
|
501
503
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
fetchResourceMetadataScopes,
|
|
15
15
|
loadAllMCPConfigs,
|
|
16
16
|
MCPManager,
|
|
17
|
+
type OAuthEndpoints,
|
|
17
18
|
} from "../../mcp";
|
|
18
19
|
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
|
|
19
20
|
import {
|
|
@@ -603,6 +604,7 @@ export class MCPCommandController {
|
|
|
603
604
|
callbackPath: finalConfig.oauth?.callbackPath,
|
|
604
605
|
redirectUri: finalConfig.oauth?.redirectUri,
|
|
605
606
|
prompt: finalConfig.oauth?.prompt,
|
|
607
|
+
registrationUrl: oauth.registrationUrl,
|
|
606
608
|
serverUrl: finalConfig.url,
|
|
607
609
|
resource: oauthResource,
|
|
608
610
|
stripSameOriginResource: oauthResourceIsFallback,
|
|
@@ -684,6 +686,7 @@ export class MCPCommandController {
|
|
|
684
686
|
redirectUri?: string;
|
|
685
687
|
prompt?: string;
|
|
686
688
|
serverUrl?: string;
|
|
689
|
+
registrationUrl?: string;
|
|
687
690
|
resource?: string;
|
|
688
691
|
stripSameOriginResource?: boolean;
|
|
689
692
|
/**
|
|
@@ -747,6 +750,7 @@ export class MCPCommandController {
|
|
|
747
750
|
{
|
|
748
751
|
authorizationUrl: authUrl,
|
|
749
752
|
tokenUrl: tokenUrl,
|
|
753
|
+
registrationUrl: opts?.registrationUrl,
|
|
750
754
|
clientId: resolvedClientId,
|
|
751
755
|
clientSecret: resolvedClientSecret,
|
|
752
756
|
scopes: scopes || undefined,
|
|
@@ -1038,13 +1042,7 @@ export class MCPCommandController {
|
|
|
1038
1042
|
return next;
|
|
1039
1043
|
}
|
|
1040
1044
|
|
|
1041
|
-
async #resolveOAuthEndpointsFromServer(config: MCPServerConfig): Promise<{
|
|
1042
|
-
authorizationUrl: string;
|
|
1043
|
-
tokenUrl: string;
|
|
1044
|
-
clientId?: string;
|
|
1045
|
-
scopes?: string;
|
|
1046
|
-
resource?: string;
|
|
1047
|
-
}> {
|
|
1045
|
+
async #resolveOAuthEndpointsFromServer(config: MCPServerConfig): Promise<OAuthEndpoints> {
|
|
1048
1046
|
// Stdio servers manage credentials inside the child process; OMP's OAuth
|
|
1049
1047
|
// flow only applies to http/sse transports. Without this guard the
|
|
1050
1048
|
// unauthenticated preflight below spawns the child, which happily reuses
|
|
@@ -1739,6 +1737,7 @@ export class MCPCommandController {
|
|
|
1739
1737
|
callbackPath: found.config.oauth?.callbackPath,
|
|
1740
1738
|
redirectUri: found.config.oauth?.redirectUri,
|
|
1741
1739
|
prompt: found.config.oauth?.prompt,
|
|
1740
|
+
registrationUrl: oauth.registrationUrl,
|
|
1742
1741
|
serverUrl,
|
|
1743
1742
|
resource: oauthResource,
|
|
1744
1743
|
stripSameOriginResource: oauthResourceIsFallback,
|
|
@@ -719,7 +719,7 @@ export class SelectorController {
|
|
|
719
719
|
if (role === "default") {
|
|
720
720
|
const { switched } = await this.ctx.session.setModel(model, role, {
|
|
721
721
|
selector,
|
|
722
|
-
thinkingLevel: concreteThinking,
|
|
722
|
+
thinkingLevel: isAuto ? ThinkingLevel.Inherit : concreteThinking,
|
|
723
723
|
persist: true,
|
|
724
724
|
currentContextTokens,
|
|
725
725
|
});
|
|
@@ -1544,7 +1544,10 @@ export class SelectorController {
|
|
|
1544
1544
|
});
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
-
showAgentHub(
|
|
1547
|
+
showAgentHub(
|
|
1548
|
+
observers: SessionObserverRegistry,
|
|
1549
|
+
options?: { requireContent?: boolean; armCloseTap?: boolean },
|
|
1550
|
+
): void {
|
|
1548
1551
|
const hubKeys = [
|
|
1549
1552
|
...this.ctx.keybindings.getKeys("app.agents.hub"),
|
|
1550
1553
|
...this.ctx.keybindings.getKeys("app.session.observe"),
|
|
@@ -1599,6 +1602,10 @@ export class SelectorController {
|
|
|
1599
1602
|
this.ctx.editorContainer.clear();
|
|
1600
1603
|
this.ctx.editorContainer.addChild(hub);
|
|
1601
1604
|
this.ctx.ui.setFocus(hub);
|
|
1605
|
+
// When the hub was raised by the editor's double-← gesture, prime its own
|
|
1606
|
+
// close detector so the *next* single ← dismisses it — the two taps that
|
|
1607
|
+
// opened it were consumed by the editor's detector (issue #4780).
|
|
1608
|
+
if (options?.armCloseTap) hub.armCloseTap();
|
|
1602
1609
|
this.ctx.ui.requestRender();
|
|
1603
1610
|
};
|
|
1604
1611
|
|
|
@@ -116,16 +116,18 @@ function findTaskFuzzy(phases: TodoPhase[], query: string): { task: TodoItem; ph
|
|
|
116
116
|
// Build system reminder
|
|
117
117
|
// =============================================================================
|
|
118
118
|
|
|
119
|
-
function buildSystemReminder(action: string, phases: TodoPhase[]): string {
|
|
119
|
+
function buildSystemReminder(action: string, phases: TodoPhase[], removed = false): string {
|
|
120
120
|
const md = phases.length === 0 ? "(empty)" : phasesToMarkdown(phases).trimEnd();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
const lines = ["<system-reminder>", `The user manually modified the todo list (${action}).`];
|
|
122
|
+
if (removed) {
|
|
123
|
+
lines.push(
|
|
124
|
+
phases.length === 0
|
|
125
|
+
? "The user intentionally cleared the todo list. Do NOT recreate or re-populate it unless the user explicitly asks; continue the current request without a todo list."
|
|
126
|
+
: "The user intentionally removed the entries no longer shown below. Do NOT re-add them unless the user explicitly asks.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
lines.push("Current todo list:", "", md, "</system-reminder>");
|
|
130
|
+
return lines.join("\n");
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
export class TodoCommandController {
|
|
@@ -366,7 +368,7 @@ export class TodoCommandController {
|
|
|
366
368
|
const current = this.#currentPhases();
|
|
367
369
|
const trimmed = rest.trim();
|
|
368
370
|
if (!trimmed) {
|
|
369
|
-
this.#commit([], "/todo rm (all)");
|
|
371
|
+
this.#commit([], "/todo rm (all)", { removed: true });
|
|
370
372
|
this.ctx.showStatus("Cleared all todos.");
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
@@ -377,7 +379,7 @@ export class TodoCommandController {
|
|
|
377
379
|
this.ctx.showError(errors.join("; "));
|
|
378
380
|
return;
|
|
379
381
|
}
|
|
380
|
-
this.#commit(phases, `/todo rm ${taskHit.task.content}
|
|
382
|
+
this.#commit(phases, `/todo rm ${taskHit.task.content}`, { removed: true });
|
|
381
383
|
this.ctx.showStatus(`Removed: ${taskHit.task.content}`);
|
|
382
384
|
return;
|
|
383
385
|
}
|
|
@@ -388,7 +390,7 @@ export class TodoCommandController {
|
|
|
388
390
|
this.ctx.showError(errors.join("; "));
|
|
389
391
|
return;
|
|
390
392
|
}
|
|
391
|
-
this.#commit(phases, `/todo rm ${phaseHit.name}
|
|
393
|
+
this.#commit(phases, `/todo rm ${phaseHit.name}`, { removed: true });
|
|
392
394
|
this.ctx.showStatus(`Removed phase: ${phaseHit.name}`);
|
|
393
395
|
return;
|
|
394
396
|
}
|
|
@@ -454,7 +456,7 @@ export class TodoCommandController {
|
|
|
454
456
|
}
|
|
455
457
|
}
|
|
456
458
|
|
|
457
|
-
#commit(nextPhases: TodoPhase[], action: string): void {
|
|
459
|
+
#commit(nextPhases: TodoPhase[], action: string, opts?: { removed?: boolean }): void {
|
|
458
460
|
// 1. In-memory + UI state
|
|
459
461
|
this.ctx.session.setTodoPhases(nextPhases);
|
|
460
462
|
this.ctx.setTodos(nextPhases);
|
|
@@ -463,7 +465,9 @@ export class TodoCommandController {
|
|
|
463
465
|
this.ctx.sessionManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases: nextPhases });
|
|
464
466
|
|
|
465
467
|
// 3. Inject system reminder so the agent learns about the change next turn.
|
|
466
|
-
|
|
468
|
+
// Removals carry explicit intent so the agent does not rebuild the
|
|
469
|
+
// cleared/removed items on its next turn (issue #5258).
|
|
470
|
+
const reminderText = buildSystemReminder(action, nextPhases, opts?.removed ?? false);
|
|
467
471
|
const message = {
|
|
468
472
|
role: "developer" as const,
|
|
469
473
|
content: [{ type: "text" as const, text: reminderText }],
|
|
@@ -71,7 +71,7 @@ import type {
|
|
|
71
71
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
72
72
|
import type { Skill } from "../extensibility/skills";
|
|
73
73
|
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
74
|
-
import { type GuidedGoalMessage, runGuidedGoalTurn } from "../goals/guided-setup";
|
|
74
|
+
import { type GuidedGoalMessage, newGuidedGoalSessionId, runGuidedGoalTurn } from "../goals/guided-setup";
|
|
75
75
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
76
76
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
77
77
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
@@ -3115,8 +3115,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3115
3115
|
|
|
3116
3116
|
const messages: GuidedGoalMessage[] = [{ role: "user", content: initial }];
|
|
3117
3117
|
let latestDraftObjective: string | undefined;
|
|
3118
|
+
// One Codex side session for the whole interview: every follow-up turn
|
|
3119
|
+
// reuses it so a multi-question interview shares a single websocket-only
|
|
3120
|
+
// Codex socket instead of leaking one per turn (#5471 review).
|
|
3121
|
+
const guidedGoalSessionId = newGuidedGoalSessionId(this.session);
|
|
3118
3122
|
for (let turn = 0; turn < 6; turn++) {
|
|
3119
|
-
const result = await runGuidedGoalTurn(this.session, { messages });
|
|
3123
|
+
const result = await runGuidedGoalTurn(this.session, { messages, sideSessionId: guidedGoalSessionId });
|
|
3120
3124
|
if (result.objective?.trim()) latestDraftObjective = result.objective.trim();
|
|
3121
3125
|
if (result.kind === "question") {
|
|
3122
3126
|
messages.push({ role: "assistant", content: result.question });
|
|
@@ -4156,7 +4160,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4156
4160
|
await this.#selectorController.showDebugSelector();
|
|
4157
4161
|
}
|
|
4158
4162
|
|
|
4159
|
-
showAgentHub(options?: { requireContent?: boolean }): void {
|
|
4163
|
+
showAgentHub(options?: { requireContent?: boolean; armCloseTap?: boolean }): void {
|
|
4160
4164
|
this.#selectorController.showAgentHub(this.#observerRegistry, options);
|
|
4161
4165
|
}
|
|
4162
4166
|
|
|
@@ -156,7 +156,12 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
156
156
|
const commandName = commandText.slice(1, spaceIndex);
|
|
157
157
|
const command = this.#commands.find(cmd => cmd.name === commandName || cmd.aliases?.includes(commandName));
|
|
158
158
|
if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
|
|
159
|
-
|
|
159
|
+
const argumentSuggestions = await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol);
|
|
160
|
+
if (argumentSuggestions) return argumentSuggestions;
|
|
161
|
+
// No slash-argument completion for this input: fall through to
|
|
162
|
+
// internal-url completion only. `#` prompt-action tokens stay
|
|
163
|
+
// literal text inside slash command arguments.
|
|
164
|
+
return getInternalUrlSuggestions(textBeforeCursor, this.#basePath);
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
167
|
|
package/src/modes/types.ts
CHANGED
|
@@ -368,7 +368,7 @@ export interface InteractiveModeContext {
|
|
|
368
368
|
showProviderSetup(): Promise<void>;
|
|
369
369
|
showHookConfirm(title: string, message: string): Promise<boolean>;
|
|
370
370
|
showDebugSelector(): Promise<void>;
|
|
371
|
-
showAgentHub(options?: { requireContent?: boolean }): void;
|
|
371
|
+
showAgentHub(options?: { requireContent?: boolean; armCloseTap?: boolean }): void;
|
|
372
372
|
resetObserverRegistry(): void;
|
|
373
373
|
|
|
374
374
|
// Input handling
|
|
@@ -57,6 +57,7 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
|
|
|
57
57
|
- `memory://root`: project memory summary
|
|
58
58
|
{{/if}}
|
|
59
59
|
- `agent://<id>`: agent output artifact; `/<path>` extracts a JSON field
|
|
60
|
+
- `history://<id>`: read-only markdown transcript of an agent (live, parked, or released); bare `history://` lists all agents. Serves any agent whose session file persists on disk, not just registered peers.
|
|
60
61
|
- `artifact://<id>`: artifact content
|
|
61
62
|
- `local://<name>.md`: plan artifacts or shared content for subagents
|
|
62
63
|
{{#if hasObsidian}}
|