@oh-my-pi/pi-coding-agent 13.12.8 → 13.12.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 +16 -0
- package/package.json +7 -7
- package/src/capability/mcp.ts +7 -1
- package/src/cli/session-picker.ts +38 -28
- package/src/discovery/builtin.ts +18 -2
- package/src/discovery/mcp-json.ts +12 -2
- package/src/mcp/oauth-flow.ts +91 -1
- package/src/mcp/types.ts +3 -0
- package/src/modes/components/session-selector.ts +113 -13
- package/src/modes/controllers/mcp-command-controller.ts +20 -15
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/types.ts +1 -0
- package/src/sdk.ts +17 -4
- package/src/session/agent-session.ts +116 -26
- package/src/session/agent-storage.ts +48 -8
- package/src/session/history-storage.ts +44 -3
- package/src/session/session-manager.ts +49 -1
- package/src/session/session-storage.ts +27 -0
- package/src/slash-commands/builtin-registry.ts +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.12.9] - 2026-03-17
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `/session delete` command to delete current session with confirmation and return to session selector
|
|
9
|
+
- Added session deletion in session selector via Delete key with confirmation dialog
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Changed session deletion callback to return a boolean indicating success, allowing callers to distinguish between failed deletions and upstream cancellations
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed OAuth redirect URI validation to preserve exact configured values without adding trailing slashes
|
|
18
|
+
- Fixed session deletion error handling to display error messages in the session selector UI instead of silently failing
|
|
19
|
+
- Added `oauth.redirectUri`, `oauth.clientSecret`, and `oauth.callbackPath` support for MCP server OAuth config so providers can use exact registered redirect URIs while preserving local callback listener settings ([#445](https://github.com/can1357/oh-my-pi/issues/445))
|
|
20
|
+
|
|
5
21
|
## [13.12.8] - 2026-03-16
|
|
6
22
|
|
|
7
23
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.12.
|
|
4
|
+
"version": "13.12.9",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.12.9",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.12.9",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.12.9",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.12.9",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.12.9",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.12.9",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
package/src/capability/mcp.ts
CHANGED
|
@@ -31,11 +31,17 @@ export interface MCPServer {
|
|
|
31
31
|
auth?: {
|
|
32
32
|
type: "oauth" | "apikey";
|
|
33
33
|
credentialId?: string;
|
|
34
|
+
tokenUrl?: string;
|
|
35
|
+
clientId?: string;
|
|
36
|
+
clientSecret?: string;
|
|
34
37
|
};
|
|
35
|
-
/** OAuth configuration (clientId, callbackPort) for servers requiring explicit client credentials */
|
|
38
|
+
/** OAuth configuration (clientId, clientSecret, redirectUri, callbackPort, callbackPath) for servers requiring explicit client credentials */
|
|
36
39
|
oauth?: {
|
|
37
40
|
clientId?: string;
|
|
41
|
+
clientSecret?: string;
|
|
42
|
+
redirectUri?: string;
|
|
38
43
|
callbackPort?: number;
|
|
44
|
+
callbackPath?: string;
|
|
39
45
|
};
|
|
40
46
|
/** Transport type */
|
|
41
47
|
transport?: "stdio" | "sse" | "http";
|
|
@@ -1,42 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI session selector for --resume flag
|
|
3
|
-
*/
|
|
4
1
|
import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
|
|
5
2
|
import { SessionSelectorComponent } from "../modes/components/session-selector";
|
|
6
3
|
import type { SessionInfo } from "../session/session-manager";
|
|
4
|
+
import { FileSessionStorage } from "../session/session-storage";
|
|
7
5
|
|
|
8
6
|
/** Show TUI session selector and return selected session path or null if cancelled */
|
|
9
7
|
export async function selectSession(sessions: SessionInfo[]): Promise<string | null> {
|
|
10
8
|
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
11
9
|
const ui = new TUI(new ProcessTerminal());
|
|
12
10
|
let resolved = false;
|
|
13
|
-
const
|
|
14
|
-
sessions,
|
|
15
|
-
(path: string) => {
|
|
16
|
-
if (!resolved) {
|
|
17
|
-
resolved = true;
|
|
18
|
-
ui.stop();
|
|
19
|
-
resolve(path);
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
() => {
|
|
23
|
-
if (!resolved) {
|
|
24
|
-
resolved = true;
|
|
25
|
-
ui.stop();
|
|
26
|
-
resolve(null);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
() => {
|
|
30
|
-
if (!resolved) {
|
|
31
|
-
resolved = true;
|
|
32
|
-
ui.stop();
|
|
33
|
-
process.exit(0);
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
);
|
|
11
|
+
const storage = new FileSessionStorage();
|
|
37
12
|
|
|
13
|
+
const showSelector = () => {
|
|
14
|
+
const selector = new SessionSelectorComponent(
|
|
15
|
+
sessions,
|
|
16
|
+
(path: string) => {
|
|
17
|
+
if (!resolved) {
|
|
18
|
+
resolved = true;
|
|
19
|
+
ui.stop();
|
|
20
|
+
resolve(path);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
() => {
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
resolved = true;
|
|
26
|
+
ui.stop();
|
|
27
|
+
resolve(null);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
() => {
|
|
31
|
+
if (!resolved) {
|
|
32
|
+
resolved = true;
|
|
33
|
+
ui.stop();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
async (session: SessionInfo) => {
|
|
38
|
+
// Delete handler - SessionList will show confirmation internally
|
|
39
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
40
|
+
return true;
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
return selector;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const selector = showSelector();
|
|
47
|
+
selector.setOnRequestRender(() => ui.requestRender());
|
|
38
48
|
ui.addChild(selector);
|
|
39
|
-
ui.setFocus(selector
|
|
49
|
+
ui.setFocus(selector);
|
|
40
50
|
ui.start();
|
|
41
51
|
return promise;
|
|
42
52
|
}
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -160,8 +160,24 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
160
160
|
env: serverConfig.env as Record<string, string> | undefined,
|
|
161
161
|
url: serverConfig.url as string | undefined,
|
|
162
162
|
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
163
|
-
auth: serverConfig.auth as
|
|
164
|
-
|
|
163
|
+
auth: serverConfig.auth as
|
|
164
|
+
| {
|
|
165
|
+
type: "oauth" | "apikey";
|
|
166
|
+
credentialId?: string;
|
|
167
|
+
tokenUrl?: string;
|
|
168
|
+
clientId?: string;
|
|
169
|
+
clientSecret?: string;
|
|
170
|
+
}
|
|
171
|
+
| undefined,
|
|
172
|
+
oauth: serverConfig.oauth as
|
|
173
|
+
| {
|
|
174
|
+
clientId?: string;
|
|
175
|
+
clientSecret?: string;
|
|
176
|
+
redirectUri?: string;
|
|
177
|
+
callbackPort?: number;
|
|
178
|
+
callbackPath?: string;
|
|
179
|
+
}
|
|
180
|
+
| undefined,
|
|
165
181
|
transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
|
|
166
182
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
167
183
|
});
|
|
@@ -34,9 +34,18 @@ interface MCPConfigFile {
|
|
|
34
34
|
auth?: {
|
|
35
35
|
type: "oauth" | "apikey";
|
|
36
36
|
credentialId?: string;
|
|
37
|
+
tokenUrl?: string;
|
|
38
|
+
clientId?: string;
|
|
39
|
+
clientSecret?: string;
|
|
37
40
|
};
|
|
38
41
|
type?: "stdio" | "sse" | "http";
|
|
39
|
-
oauth?: {
|
|
42
|
+
oauth?: {
|
|
43
|
+
clientId?: string;
|
|
44
|
+
clientSecret?: string;
|
|
45
|
+
redirectUri?: string;
|
|
46
|
+
callbackPort?: number;
|
|
47
|
+
callbackPath?: string;
|
|
48
|
+
};
|
|
40
49
|
}
|
|
41
50
|
>;
|
|
42
51
|
}
|
|
@@ -93,7 +102,8 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
|
|
|
93
102
|
if (server.env) server.env = expandEnvVarsDeep(server.env);
|
|
94
103
|
if (server.url) server.url = expandEnvVarsDeep(server.url);
|
|
95
104
|
if (server.headers) server.headers = expandEnvVarsDeep(server.headers);
|
|
96
|
-
|
|
105
|
+
if (server.auth) server.auth = expandEnvVarsDeep(server.auth);
|
|
106
|
+
if (server.oauth) server.oauth = expandEnvVarsDeep(server.oauth);
|
|
97
107
|
servers.push(server);
|
|
98
108
|
}
|
|
99
109
|
}
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -6,11 +6,97 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { OAuthCallbackFlowOptions } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
9
10
|
import { OAuthCallbackFlow } from "@oh-my-pi/pi-ai/utils/oauth/callback-server";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_PORT = 3000;
|
|
12
13
|
const CALLBACK_PATH = "/callback";
|
|
13
14
|
|
|
15
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
16
|
+
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveRedirectUri(redirectUri: string | undefined): string | undefined {
|
|
20
|
+
const configured = redirectUri;
|
|
21
|
+
const trimmed = configured?.trim();
|
|
22
|
+
if (!trimmed) return undefined;
|
|
23
|
+
if (trimmed !== configured) {
|
|
24
|
+
throw new Error("OAuth redirect URI must not include surrounding whitespace");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const parsed = new URL(configured);
|
|
28
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
29
|
+
throw new Error("OAuth redirect URI must use http or https");
|
|
30
|
+
}
|
|
31
|
+
return configured;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseRedirectUri(redirectUri: string | undefined): URL | undefined {
|
|
35
|
+
return redirectUri ? new URL(redirectUri) : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getUriPort(uri: URL): number {
|
|
39
|
+
if (uri.port !== "") return Number(uri.port);
|
|
40
|
+
return uri.protocol === "https:" ? 443 : 80;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateRedirectConfig(config: MCPOAuthConfig, redirectUri: string | undefined): void {
|
|
44
|
+
const parsed = parseRedirectUri(redirectUri);
|
|
45
|
+
if (!parsed || parsed.protocol !== "https:" || !isLoopbackHostname(parsed.hostname)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.callbackPort === undefined) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"HTTPS loopback redirect URIs require oauth.callbackPort to point at the local HTTP callback listener behind your TLS terminator",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (config.callbackPort === getUriPort(parsed)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"HTTPS loopback redirect URIs cannot reuse the same local port; terminate TLS separately and forward to oauth.callbackPort",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveCallbackPort(callbackPort: number | undefined, redirectUri: string | undefined): number {
|
|
63
|
+
if (callbackPort !== undefined) return callbackPort;
|
|
64
|
+
|
|
65
|
+
const parsed = parseRedirectUri(redirectUri);
|
|
66
|
+
if (!parsed || parsed.protocol !== "http:" || !isLoopbackHostname(parsed.hostname)) {
|
|
67
|
+
return DEFAULT_PORT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const port = getUriPort(parsed);
|
|
71
|
+
return Number.isFinite(port) && port > 0 ? port : DEFAULT_PORT;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveCallbackPath(callbackPath: string | undefined, redirectUri: string | undefined): string {
|
|
75
|
+
const trimmed = callbackPath?.trim();
|
|
76
|
+
if (trimmed) return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
77
|
+
|
|
78
|
+
const parsed = parseRedirectUri(redirectUri);
|
|
79
|
+
if (parsed?.pathname) return parsed.pathname;
|
|
80
|
+
return CALLBACK_PATH;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveCallbackHostname(redirectUri: string | undefined): string | undefined {
|
|
84
|
+
const parsed = parseRedirectUri(redirectUri);
|
|
85
|
+
if (!parsed || !isLoopbackHostname(parsed.hostname)) return undefined;
|
|
86
|
+
return parsed.hostname;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOptions {
|
|
90
|
+
const redirectUri = resolveRedirectUri(config.redirectUri);
|
|
91
|
+
validateRedirectConfig(config, redirectUri);
|
|
92
|
+
return {
|
|
93
|
+
preferredPort: resolveCallbackPort(config.callbackPort, redirectUri),
|
|
94
|
+
callbackPath: resolveCallbackPath(config.callbackPath, redirectUri),
|
|
95
|
+
callbackHostname: resolveCallbackHostname(redirectUri),
|
|
96
|
+
redirectUri,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
14
100
|
export interface MCPOAuthConfig {
|
|
15
101
|
/** Authorization endpoint URL */
|
|
16
102
|
authorizationUrl: string;
|
|
@@ -22,8 +108,12 @@ export interface MCPOAuthConfig {
|
|
|
22
108
|
clientSecret?: string;
|
|
23
109
|
/** OAuth scopes (space-separated) */
|
|
24
110
|
scopes?: string;
|
|
111
|
+
/** Exact redirect URI to advertise to the provider */
|
|
112
|
+
redirectUri?: string;
|
|
25
113
|
/** Custom callback port (default: 3000) */
|
|
26
114
|
callbackPort?: number;
|
|
115
|
+
/** Custom callback path (default: /callback or redirectUri pathname) */
|
|
116
|
+
callbackPath?: string;
|
|
27
117
|
}
|
|
28
118
|
|
|
29
119
|
/**
|
|
@@ -39,7 +129,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
39
129
|
private config: MCPOAuthConfig,
|
|
40
130
|
ctrl: OAuthController,
|
|
41
131
|
) {
|
|
42
|
-
super(ctrl, config
|
|
132
|
+
super(ctrl, resolveCallbackOptions(config));
|
|
43
133
|
this.#resolvedClientId = this.#resolveClientId(config);
|
|
44
134
|
}
|
|
45
135
|
|
package/src/mcp/types.ts
CHANGED
|
@@ -68,7 +68,10 @@ interface MCPServerConfigBase {
|
|
|
68
68
|
/** OAuth configuration for servers requiring explicit client credentials */
|
|
69
69
|
oauth?: {
|
|
70
70
|
clientId?: string;
|
|
71
|
+
clientSecret?: string;
|
|
72
|
+
redirectUri?: string;
|
|
71
73
|
callbackPort?: number;
|
|
74
|
+
callbackPath?: string;
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Input,
|
|
5
5
|
matchesKey,
|
|
6
6
|
padding,
|
|
7
|
+
replaceTabs,
|
|
7
8
|
Spacer,
|
|
8
9
|
Text,
|
|
9
10
|
truncateToWidth,
|
|
@@ -13,6 +14,7 @@ import { theme } from "../../modes/theme/theme";
|
|
|
13
14
|
import type { SessionInfo } from "../../session/session-manager";
|
|
14
15
|
import { fuzzyFilter } from "../../utils/fuzzy";
|
|
15
16
|
import { DynamicBorder } from "./dynamic-border";
|
|
17
|
+
import { HookSelectorComponent } from "./hook-selector";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Custom session list component with multi-line items and search
|
|
@@ -26,6 +28,8 @@ class SessionList implements Component {
|
|
|
26
28
|
onExit: () => void = () => {};
|
|
27
29
|
#maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
|
|
28
30
|
|
|
31
|
+
onDeleteRequest?: (session: SessionInfo) => void;
|
|
32
|
+
|
|
29
33
|
constructor(
|
|
30
34
|
private readonly allSessions: SessionInfo[],
|
|
31
35
|
private readonly showCwd = false,
|
|
@@ -59,6 +63,18 @@ class SessionList implements Component {
|
|
|
59
63
|
this.#selectedIndex = Math.min(this.#selectedIndex, Math.max(0, this.#filteredSessions.length - 1));
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
removeSession(sessionPath: string): void {
|
|
67
|
+
const index = this.allSessions.findIndex(s => s.path === sessionPath);
|
|
68
|
+
if (index === -1) return;
|
|
69
|
+
this.allSessions.splice(index, 1);
|
|
70
|
+
// Re-filter to update filteredSessions
|
|
71
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
72
|
+
// Adjust selectedIndex if we deleted the last item or beyond
|
|
73
|
+
if (this.#selectedIndex >= this.#filteredSessions.length) {
|
|
74
|
+
this.#selectedIndex = Math.max(0, this.#filteredSessions.length - 1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
invalidate(): void {
|
|
63
79
|
// No cached state to invalidate currently
|
|
64
80
|
}
|
|
@@ -157,78 +173,105 @@ class SessionList implements Component {
|
|
|
157
173
|
lines.push(scrollInfo);
|
|
158
174
|
}
|
|
159
175
|
|
|
176
|
+
// Add keybinding hint
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push(theme.fg("muted", " [Del to delete, Enter to select, Esc to cancel]"));
|
|
179
|
+
|
|
160
180
|
return lines;
|
|
161
181
|
}
|
|
162
182
|
|
|
163
183
|
handleInput(keyData: string): void {
|
|
184
|
+
// Delete key - request delete confirmation from parent
|
|
185
|
+
if (matchesKey(keyData, "delete")) {
|
|
186
|
+
const selected = this.#filteredSessions[this.#selectedIndex];
|
|
187
|
+
if (selected && this.onDeleteRequest) {
|
|
188
|
+
this.onDeleteRequest(selected);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
164
193
|
// Up arrow
|
|
165
194
|
if (matchesKey(keyData, "up")) {
|
|
166
195
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
196
|
+
return;
|
|
167
197
|
}
|
|
168
198
|
// Down arrow
|
|
169
|
-
|
|
199
|
+
if (matchesKey(keyData, "down")) {
|
|
170
200
|
this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + 1);
|
|
201
|
+
return;
|
|
171
202
|
}
|
|
172
203
|
// Page up - jump up by maxVisible items
|
|
173
|
-
|
|
204
|
+
if (matchesKey(keyData, "pageUp")) {
|
|
174
205
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.#maxVisible);
|
|
206
|
+
return;
|
|
175
207
|
}
|
|
176
208
|
// Page down - jump down by maxVisible items
|
|
177
|
-
|
|
209
|
+
if (matchesKey(keyData, "pageDown")) {
|
|
178
210
|
this.#selectedIndex = Math.min(this.#filteredSessions.length - 1, this.#selectedIndex + this.#maxVisible);
|
|
211
|
+
return;
|
|
179
212
|
}
|
|
180
213
|
// Enter
|
|
181
|
-
|
|
214
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
182
215
|
const selected = this.#filteredSessions[this.#selectedIndex];
|
|
183
216
|
if (selected && this.onSelect) {
|
|
184
217
|
this.onSelect(selected.path);
|
|
185
218
|
}
|
|
219
|
+
return;
|
|
186
220
|
}
|
|
187
221
|
// Escape - cancel
|
|
188
|
-
|
|
222
|
+
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
189
223
|
if (this.onCancel) {
|
|
190
224
|
this.onCancel();
|
|
191
225
|
}
|
|
226
|
+
return;
|
|
192
227
|
}
|
|
193
228
|
// Ctrl+C - exit
|
|
194
|
-
|
|
229
|
+
if (matchesKey(keyData, "ctrl+c")) {
|
|
195
230
|
this.onExit();
|
|
231
|
+
return;
|
|
196
232
|
}
|
|
197
233
|
// Pass everything else to search input
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.#filterSessions(this.#searchInput.getValue());
|
|
201
|
-
}
|
|
234
|
+
this.#searchInput.handleInput(keyData);
|
|
235
|
+
this.#filterSessions(this.#searchInput.getValue());
|
|
202
236
|
}
|
|
203
237
|
}
|
|
204
238
|
|
|
205
239
|
/**
|
|
206
|
-
* Component that renders a session selector
|
|
240
|
+
* Component that renders a session selector with optional confirmation dialog
|
|
207
241
|
*/
|
|
208
242
|
export class SessionSelectorComponent extends Container {
|
|
209
243
|
#sessionList: SessionList;
|
|
244
|
+
#confirmationDialog: HookSelectorComponent | null = null;
|
|
245
|
+
#messageContainer: Container;
|
|
246
|
+
#onDelete?: (session: SessionInfo) => Promise<boolean>;
|
|
247
|
+
#onRequestRender?: () => void;
|
|
210
248
|
|
|
211
249
|
constructor(
|
|
212
250
|
sessions: SessionInfo[],
|
|
213
251
|
onSelect: (sessionPath: string) => void,
|
|
214
252
|
onCancel: () => void,
|
|
215
253
|
onExit: () => void,
|
|
254
|
+
onDelete?: (session: SessionInfo) => Promise<boolean>,
|
|
216
255
|
) {
|
|
217
256
|
super();
|
|
218
257
|
|
|
258
|
+
this.#messageContainer = new Container();
|
|
259
|
+
this.#onDelete = onDelete;
|
|
219
260
|
// Add header
|
|
220
261
|
this.addChild(new Spacer(1));
|
|
221
262
|
this.addChild(new Text(theme.bold("Resume Session"), 1, 0));
|
|
222
263
|
this.addChild(new Spacer(1));
|
|
223
264
|
this.addChild(new DynamicBorder());
|
|
224
265
|
this.addChild(new Spacer(1));
|
|
225
|
-
|
|
266
|
+
this.addChild(this.#messageContainer);
|
|
226
267
|
// Create session list
|
|
227
268
|
this.#sessionList = new SessionList(sessions);
|
|
228
269
|
this.#sessionList.onSelect = onSelect;
|
|
229
270
|
this.#sessionList.onCancel = onCancel;
|
|
230
271
|
this.#sessionList.onExit = onExit;
|
|
231
|
-
|
|
272
|
+
this.#sessionList.onDeleteRequest = (session: SessionInfo) => {
|
|
273
|
+
this.#showDeleteConfirmation(session);
|
|
274
|
+
};
|
|
232
275
|
this.addChild(this.#sessionList);
|
|
233
276
|
|
|
234
277
|
// Add bottom border
|
|
@@ -236,6 +279,63 @@ export class SessionSelectorComponent extends Container {
|
|
|
236
279
|
this.addChild(new DynamicBorder());
|
|
237
280
|
}
|
|
238
281
|
|
|
282
|
+
setOnRequestRender(callback: () => void): void {
|
|
283
|
+
this.#onRequestRender = callback;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#clearError(): void {
|
|
287
|
+
this.#messageContainer.clear();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
#showError(message: string): void {
|
|
291
|
+
this.#messageContainer.clear();
|
|
292
|
+
this.#messageContainer.addChild(new Text(theme.fg("error", `Error: ${replaceTabs(message)}`), 1, 0));
|
|
293
|
+
this.#messageContainer.addChild(new Spacer(1));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#showDeleteConfirmation(session: SessionInfo): void {
|
|
297
|
+
const displayName = session.title || session.firstMessage.slice(0, 40) || session.id;
|
|
298
|
+
this.#confirmationDialog = new HookSelectorComponent(
|
|
299
|
+
`Delete session?\n${displayName}`,
|
|
300
|
+
["Yes", "No"],
|
|
301
|
+
async (option: string) => {
|
|
302
|
+
if (option === "Yes" && this.#onDelete) {
|
|
303
|
+
this.#clearError();
|
|
304
|
+
try {
|
|
305
|
+
const deleted = await this.#onDelete(session);
|
|
306
|
+
if (deleted) {
|
|
307
|
+
this.#sessionList.removeSession(session.path);
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
this.#showError(err instanceof Error ? err.message : String(err));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Close confirmation dialog
|
|
314
|
+
this.removeChild(this.#confirmationDialog!);
|
|
315
|
+
this.#confirmationDialog = null;
|
|
316
|
+
// Request rerender
|
|
317
|
+
this.#onRequestRender?.();
|
|
318
|
+
},
|
|
319
|
+
() => {
|
|
320
|
+
// Cancel - close confirmation dialog
|
|
321
|
+
this.removeChild(this.#confirmationDialog!);
|
|
322
|
+
this.#confirmationDialog = null;
|
|
323
|
+
// Request rerender
|
|
324
|
+
this.#onRequestRender?.();
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
// Show confirmation dialog
|
|
328
|
+
this.addChild(this.#confirmationDialog);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
handleInput(keyData: string): void {
|
|
332
|
+
if (this.#confirmationDialog) {
|
|
333
|
+
this.#confirmationDialog.handleInput(keyData);
|
|
334
|
+
} else {
|
|
335
|
+
this.#sessionList.handleInput(keyData);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
239
339
|
getSessionList(): SessionList {
|
|
240
340
|
return this.#sessionList;
|
|
241
341
|
}
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
searchSmitheryRegistry,
|
|
33
33
|
toConfigName,
|
|
34
34
|
} from "../../mcp/smithery-registry";
|
|
35
|
-
import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
35
|
+
import type { MCPAuthConfig, MCPServerConfig, MCPServerConnection } from "../../mcp/types";
|
|
36
36
|
import type { OAuthCredential } from "../../session/auth-storage";
|
|
37
37
|
import { shortenPath } from "../../tools/render-utils";
|
|
38
38
|
import { openPath } from "../../utils/open";
|
|
@@ -400,13 +400,16 @@ export class MCPCommandController {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
try {
|
|
403
|
+
const oauthClientSecret = finalConfig.oauth?.clientSecret ?? "";
|
|
403
404
|
const credentialId = await this.#handleOAuthFlow(
|
|
404
405
|
oauth.authorizationUrl,
|
|
405
406
|
oauth.tokenUrl,
|
|
406
407
|
oauth.clientId ?? finalConfig.oauth?.clientId ?? "",
|
|
407
|
-
|
|
408
|
+
oauthClientSecret,
|
|
408
409
|
oauth.scopes ?? "",
|
|
409
410
|
finalConfig.oauth?.callbackPort,
|
|
411
|
+
finalConfig.oauth?.callbackPath,
|
|
412
|
+
finalConfig.oauth?.redirectUri,
|
|
410
413
|
);
|
|
411
414
|
finalConfig = {
|
|
412
415
|
...finalConfig,
|
|
@@ -415,7 +418,7 @@ export class MCPCommandController {
|
|
|
415
418
|
credentialId,
|
|
416
419
|
tokenUrl: oauth.tokenUrl,
|
|
417
420
|
clientId: oauth.clientId ?? finalConfig.oauth?.clientId,
|
|
418
|
-
clientSecret:
|
|
421
|
+
clientSecret: finalConfig.oauth?.clientSecret,
|
|
419
422
|
},
|
|
420
423
|
};
|
|
421
424
|
} catch (oauthError) {
|
|
@@ -478,6 +481,8 @@ export class MCPCommandController {
|
|
|
478
481
|
clientSecret: string,
|
|
479
482
|
scopes: string,
|
|
480
483
|
callbackPort?: number,
|
|
484
|
+
callbackPath?: string,
|
|
485
|
+
redirectUri?: string,
|
|
481
486
|
): Promise<string> {
|
|
482
487
|
const authStorage = this.ctx.session.modelRegistry.authStorage;
|
|
483
488
|
let parsedAuthUrl: URL;
|
|
@@ -493,6 +498,7 @@ export class MCPCommandController {
|
|
|
493
498
|
}
|
|
494
499
|
|
|
495
500
|
const resolvedClientId = clientId.trim() || parsedAuthUrl.searchParams.get("client_id") || undefined;
|
|
501
|
+
const resolvedClientSecret = clientSecret.trim() || undefined;
|
|
496
502
|
|
|
497
503
|
try {
|
|
498
504
|
// Create OAuth flow
|
|
@@ -501,9 +507,11 @@ export class MCPCommandController {
|
|
|
501
507
|
authorizationUrl: authUrl,
|
|
502
508
|
tokenUrl: tokenUrl,
|
|
503
509
|
clientId: resolvedClientId,
|
|
504
|
-
clientSecret:
|
|
510
|
+
clientSecret: resolvedClientSecret,
|
|
505
511
|
scopes: scopes || undefined,
|
|
512
|
+
redirectUri,
|
|
506
513
|
callbackPort,
|
|
514
|
+
callbackPath,
|
|
507
515
|
},
|
|
508
516
|
{
|
|
509
517
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -653,7 +661,7 @@ export class MCPCommandController {
|
|
|
653
661
|
}
|
|
654
662
|
|
|
655
663
|
#stripOAuthAuth(config: MCPServerConfig): MCPServerConfig {
|
|
656
|
-
const next = { ...config } as MCPServerConfig & { auth?:
|
|
664
|
+
const next = { ...config } as MCPServerConfig & { auth?: MCPAuthConfig };
|
|
657
665
|
delete next.auth;
|
|
658
666
|
return next;
|
|
659
667
|
}
|
|
@@ -1261,9 +1269,7 @@ export class MCPCommandController {
|
|
|
1261
1269
|
return;
|
|
1262
1270
|
}
|
|
1263
1271
|
|
|
1264
|
-
const currentAuth = (
|
|
1265
|
-
found.config as MCPServerConfig & { auth?: { type: "oauth" | "apikey"; credentialId?: string } }
|
|
1266
|
-
).auth;
|
|
1272
|
+
const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
|
|
1267
1273
|
if (currentAuth?.type === "oauth") {
|
|
1268
1274
|
await this.#removeManagedOAuthCredential(currentAuth.credentialId);
|
|
1269
1275
|
}
|
|
@@ -1298,17 +1304,14 @@ export class MCPCommandController {
|
|
|
1298
1304
|
return;
|
|
1299
1305
|
}
|
|
1300
1306
|
|
|
1301
|
-
const currentAuth = (
|
|
1302
|
-
found.config as MCPServerConfig & {
|
|
1303
|
-
auth?: { type: "oauth" | "apikey"; credentialId?: string; clientSecret?: string };
|
|
1304
|
-
}
|
|
1305
|
-
).auth;
|
|
1307
|
+
const currentAuth = (found.config as MCPServerConfig & { auth?: MCPAuthConfig }).auth;
|
|
1306
1308
|
if (currentAuth?.type === "oauth") {
|
|
1307
1309
|
await this.#removeManagedOAuthCredential(currentAuth.credentialId);
|
|
1308
1310
|
}
|
|
1309
1311
|
|
|
1310
1312
|
const baseConfig = this.#stripOAuthAuth(found.config);
|
|
1311
1313
|
const oauth = await this.#resolveOAuthEndpointsFromServer(baseConfig);
|
|
1314
|
+
const oauthClientSecret = found.config.oauth?.clientSecret ?? currentAuth?.clientSecret ?? "";
|
|
1312
1315
|
|
|
1313
1316
|
this.#showMessage(["", theme.fg("muted", `Reauthorizing "${name}"...`), ""].join("\n"));
|
|
1314
1317
|
|
|
@@ -1316,9 +1319,11 @@ export class MCPCommandController {
|
|
|
1316
1319
|
oauth.authorizationUrl,
|
|
1317
1320
|
oauth.tokenUrl,
|
|
1318
1321
|
oauth.clientId ?? found.config.oauth?.clientId ?? "",
|
|
1319
|
-
|
|
1322
|
+
oauthClientSecret,
|
|
1320
1323
|
oauth.scopes ?? "",
|
|
1321
1324
|
found.config.oauth?.callbackPort,
|
|
1325
|
+
found.config.oauth?.callbackPath,
|
|
1326
|
+
found.config.oauth?.redirectUri,
|
|
1322
1327
|
);
|
|
1323
1328
|
|
|
1324
1329
|
const updated: MCPServerConfig = {
|
|
@@ -1328,7 +1333,7 @@ export class MCPCommandController {
|
|
|
1328
1333
|
credentialId,
|
|
1329
1334
|
tokenUrl: oauth.tokenUrl,
|
|
1330
1335
|
clientId: oauth.clientId ?? found.config.oauth?.clientId,
|
|
1331
|
-
clientSecret:
|
|
1336
|
+
clientSecret: oauthClientSecret || undefined,
|
|
1332
1337
|
},
|
|
1333
1338
|
};
|
|
1334
1339
|
await updateMCPServer(found.filePath, name, updated);
|