@oh-my-pi/pi-coding-agent 12.1.1 → 12.2.1
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 +30 -0
- package/examples/sdk/11-sessions.ts +1 -1
- package/package.json +7 -7
- package/src/capability/index.ts +2 -1
- package/src/capability/types.ts +1 -1
- package/src/cli/file-processor.ts +2 -1
- package/src/cli/shell-cli.ts +2 -2
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +2 -1
- package/src/config/prompt-templates.ts +3 -3
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +2 -2
- package/src/config.ts +6 -6
- package/src/debug/system-info.ts +2 -2
- package/src/extensibility/custom-commands/loader.ts +5 -5
- package/src/extensibility/plugins/installer.ts +2 -2
- package/src/extensibility/plugins/manager.ts +2 -1
- package/src/extensibility/skills.ts +3 -2
- package/src/extensibility/slash-commands.ts +1 -1
- package/src/ipy/executor.ts +2 -2
- package/src/ipy/modules.ts +3 -3
- package/src/main.ts +9 -7
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/modes/components/footer.ts +3 -2
- package/src/modes/components/oauth-selector.ts +96 -21
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +2 -1
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +60 -2
- package/src/modes/controllers/mcp-command-controller.ts +8 -8
- package/src/modes/controllers/selector-controller.ts +35 -11
- package/src/modes/interactive-mode.ts +2 -2
- package/src/sdk.ts +37 -11
- package/src/session/agent-session.ts +12 -0
- package/src/session/session-manager.ts +7 -4
- package/src/system-prompt.ts +41 -28
- package/src/tools/bash-normalize.ts +2 -2
- package/src/tools/bash.ts +2 -1
- package/src/tools/fetch.ts +3 -3
- package/src/tools/python.ts +2 -1
|
@@ -3,9 +3,8 @@ import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { theme } from "../../modes/theme/theme";
|
|
4
4
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
5
5
|
import { DynamicBorder } from "./dynamic-border";
|
|
6
|
-
|
|
7
6
|
/**
|
|
8
|
-
* Component that renders an OAuth provider selector
|
|
7
|
+
* Component that renders an OAuth provider selector.
|
|
9
8
|
*/
|
|
10
9
|
export class OAuthSelectorComponent extends Container {
|
|
11
10
|
#listContainer: Container;
|
|
@@ -16,62 +15,139 @@ export class OAuthSelectorComponent extends Container {
|
|
|
16
15
|
#onSelectCallback: (providerId: string) => void;
|
|
17
16
|
#onCancelCallback: () => void;
|
|
18
17
|
#statusMessage: string | undefined;
|
|
19
|
-
|
|
18
|
+
#validateAuthCallback?: (providerId: string) => Promise<boolean>;
|
|
19
|
+
#requestRenderCallback?: () => void;
|
|
20
|
+
#authState: Map<string, "checking" | "valid" | "invalid"> = new Map();
|
|
21
|
+
#spinnerFrame: number = 0;
|
|
22
|
+
#spinnerInterval?: NodeJS.Timeout;
|
|
23
|
+
#validationGeneration: number = 0;
|
|
20
24
|
constructor(
|
|
21
25
|
mode: "login" | "logout",
|
|
22
26
|
authStorage: AuthStorage,
|
|
23
27
|
onSelect: (providerId: string) => void,
|
|
24
28
|
onCancel: () => void,
|
|
29
|
+
options?: {
|
|
30
|
+
validateAuth?: (providerId: string) => Promise<boolean>;
|
|
31
|
+
requestRender?: () => void;
|
|
32
|
+
},
|
|
25
33
|
) {
|
|
26
34
|
super();
|
|
27
|
-
|
|
28
35
|
this.#mode = mode;
|
|
29
36
|
this.#authStorage = authStorage;
|
|
30
37
|
this.#onSelectCallback = onSelect;
|
|
31
38
|
this.#onCancelCallback = onCancel;
|
|
32
|
-
|
|
39
|
+
this.#validateAuthCallback = options?.validateAuth;
|
|
40
|
+
this.#requestRenderCallback = options?.requestRender;
|
|
33
41
|
// Load all OAuth providers
|
|
34
42
|
this.#loadProviders();
|
|
35
|
-
|
|
36
|
-
// Add top border
|
|
37
43
|
this.addChild(new DynamicBorder());
|
|
38
44
|
this.addChild(new Spacer(1));
|
|
39
|
-
|
|
40
45
|
// Add title
|
|
41
46
|
const title = mode === "login" ? "Select provider to login:" : "Select provider to logout:";
|
|
42
47
|
this.addChild(new TruncatedText(theme.bold(title)));
|
|
43
48
|
this.addChild(new Spacer(1));
|
|
44
|
-
|
|
45
49
|
// Create list container
|
|
46
50
|
this.#listContainer = new Container();
|
|
47
51
|
this.addChild(this.#listContainer);
|
|
48
|
-
|
|
49
52
|
this.addChild(new Spacer(1));
|
|
50
|
-
|
|
51
53
|
// Add bottom border
|
|
52
54
|
this.addChild(new DynamicBorder());
|
|
53
|
-
|
|
54
55
|
// Initial render
|
|
55
56
|
this.#updateList();
|
|
57
|
+
this.#startValidation();
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
stopValidation(): void {
|
|
61
|
+
this.#validationGeneration += 1;
|
|
62
|
+
this.#stopSpinner();
|
|
63
|
+
}
|
|
58
64
|
#loadProviders(): void {
|
|
59
65
|
this.#allProviders = getOAuthProviders();
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
#startValidation(): void {
|
|
69
|
+
if (!this.#validateAuthCallback) return;
|
|
70
|
+
const generation = this.#validationGeneration + 1;
|
|
71
|
+
this.#validationGeneration = generation;
|
|
72
|
+
|
|
73
|
+
let pending = 0;
|
|
74
|
+
for (const provider of this.#allProviders) {
|
|
75
|
+
if (!this.#authStorage.hasAuth(provider.id)) {
|
|
76
|
+
this.#authState.delete(provider.id);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
this.#authState.set(provider.id, "checking");
|
|
80
|
+
pending += 1;
|
|
81
|
+
void this.#validateProvider(provider.id, generation);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (pending > 0) {
|
|
85
|
+
this.#startSpinner();
|
|
86
|
+
this.#updateList();
|
|
87
|
+
this.#requestRenderCallback?.();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async #validateProvider(providerId: string, generation: number): Promise<void> {
|
|
92
|
+
if (!this.#validateAuthCallback) return;
|
|
93
|
+
let isValid = false;
|
|
94
|
+
try {
|
|
95
|
+
isValid = await this.#validateAuthCallback(providerId);
|
|
96
|
+
} catch {
|
|
97
|
+
isValid = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (generation !== this.#validationGeneration) return;
|
|
101
|
+
this.#authState.set(providerId, isValid ? "valid" : "invalid");
|
|
102
|
+
if (![...this.#authState.values()].includes("checking")) {
|
|
103
|
+
this.#stopSpinner();
|
|
104
|
+
}
|
|
105
|
+
this.#updateList();
|
|
106
|
+
this.#requestRenderCallback?.();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#startSpinner(): void {
|
|
110
|
+
if (this.#spinnerInterval) return;
|
|
111
|
+
this.#spinnerInterval = setInterval(() => {
|
|
112
|
+
const frameCount = theme.spinnerFrames.length;
|
|
113
|
+
if (frameCount > 0) {
|
|
114
|
+
this.#spinnerFrame = (this.#spinnerFrame + 1) % frameCount;
|
|
115
|
+
}
|
|
116
|
+
this.#updateList();
|
|
117
|
+
this.#requestRenderCallback?.();
|
|
118
|
+
}, 80);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#stopSpinner(): void {
|
|
122
|
+
if (this.#spinnerInterval) {
|
|
123
|
+
clearInterval(this.#spinnerInterval);
|
|
124
|
+
this.#spinnerInterval = undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#getStatusIndicator(providerId: string): string {
|
|
129
|
+
const state = this.#authState.get(providerId);
|
|
130
|
+
if (state === "checking") {
|
|
131
|
+
const frameCount = theme.spinnerFrames.length;
|
|
132
|
+
const spinner = frameCount > 0 ? theme.spinnerFrames[this.#spinnerFrame % frameCount] : theme.status.pending;
|
|
133
|
+
return theme.fg("warning", ` ${spinner} checking`);
|
|
134
|
+
}
|
|
135
|
+
if (state === "invalid") {
|
|
136
|
+
return theme.fg("error", ` ${theme.status.error} invalid`);
|
|
137
|
+
}
|
|
138
|
+
if (state === "valid") {
|
|
139
|
+
return theme.fg("success", ` ${theme.status.success} logged in`);
|
|
140
|
+
}
|
|
141
|
+
return this.#authStorage.hasAuth(providerId) ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
|
|
142
|
+
}
|
|
62
143
|
#updateList(): void {
|
|
63
144
|
this.#listContainer.clear();
|
|
64
|
-
|
|
65
145
|
for (let i = 0; i < this.#allProviders.length; i++) {
|
|
66
146
|
const provider = this.#allProviders[i];
|
|
67
147
|
if (!provider) continue;
|
|
68
|
-
|
|
69
148
|
const isSelected = i === this.#selectedIndex;
|
|
70
149
|
const isAvailable = provider.available;
|
|
71
|
-
|
|
72
|
-
// Check if user is logged in for this provider
|
|
73
|
-
const isLoggedIn = this.#authStorage.hasOAuth(provider.id);
|
|
74
|
-
const statusIndicator = isLoggedIn ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
|
|
150
|
+
const statusIndicator = this.#getStatusIndicator(provider.id);
|
|
75
151
|
|
|
76
152
|
let line = "";
|
|
77
153
|
if (isSelected) {
|
|
@@ -82,7 +158,6 @@ export class OAuthSelectorComponent extends Container {
|
|
|
82
158
|
const text = isAvailable ? ` ${provider.name}` : theme.fg("dim", ` ${provider.name}`);
|
|
83
159
|
line = text + statusIndicator;
|
|
84
160
|
}
|
|
85
|
-
|
|
86
161
|
this.#listContainer.addChild(new TruncatedText(line, 0, 0));
|
|
87
162
|
}
|
|
88
163
|
|
|
@@ -92,13 +167,11 @@ export class OAuthSelectorComponent extends Container {
|
|
|
92
167
|
this.#mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
|
|
93
168
|
this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
|
|
94
169
|
}
|
|
95
|
-
|
|
96
170
|
if (this.#statusMessage) {
|
|
97
171
|
this.#listContainer.addChild(new Spacer(1));
|
|
98
172
|
this.#listContainer.addChild(new TruncatedText(theme.fg("warning", ` ${this.#statusMessage}`), 0, 0));
|
|
99
173
|
}
|
|
100
174
|
}
|
|
101
|
-
|
|
102
175
|
handleInput(keyData: string): void {
|
|
103
176
|
// Up arrow
|
|
104
177
|
if (matchesKey(keyData, "up")) {
|
|
@@ -121,6 +194,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
121
194
|
const selectedProvider = this.#allProviders[this.#selectedIndex];
|
|
122
195
|
if (selectedProvider?.available) {
|
|
123
196
|
this.#statusMessage = undefined;
|
|
197
|
+
this.stopValidation();
|
|
124
198
|
this.#onSelectCallback(selectedProvider.id);
|
|
125
199
|
} else if (selectedProvider) {
|
|
126
200
|
this.#statusMessage = "Provider unavailable in this environment.";
|
|
@@ -129,6 +203,7 @@ export class OAuthSelectorComponent extends Container {
|
|
|
129
203
|
}
|
|
130
204
|
// Escape or Ctrl+C
|
|
131
205
|
else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
|
206
|
+
this.stopValidation();
|
|
132
207
|
this.#onCancelCallback();
|
|
133
208
|
}
|
|
134
209
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
2
3
|
import { theme } from "../../../modes/theme/theme";
|
|
3
4
|
import { shortenPath } from "../../../tools/render-utils";
|
|
4
5
|
import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
|
|
@@ -91,7 +92,7 @@ const pathSegment: StatusLineSegment = {
|
|
|
91
92
|
render(ctx) {
|
|
92
93
|
const opts = ctx.options.path ?? {};
|
|
93
94
|
|
|
94
|
-
let pwd =
|
|
95
|
+
let pwd = getProjectDir();
|
|
95
96
|
|
|
96
97
|
if (opts.abbreviate !== false) {
|
|
97
98
|
pwd = shortenPath(pwd);
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
5
6
|
import { $ } from "bun";
|
|
6
7
|
import { settings } from "../../config/settings";
|
|
7
8
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
|
|
@@ -41,7 +42,7 @@ function sanitizeStatusText(text: string): string {
|
|
|
41
42
|
|
|
42
43
|
/** Find the git root directory by walking up from cwd */
|
|
43
44
|
function findGitHeadPath(): string | null {
|
|
44
|
-
let dir =
|
|
45
|
+
let dir = getProjectDir();
|
|
45
46
|
while (true) {
|
|
46
47
|
const gitHeadPath = path.join(dir, ".git", "HEAD");
|
|
47
48
|
if (fs.existsSync(gitHeadPath)) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type TUI,
|
|
14
14
|
} from "@oh-my-pi/pi-tui";
|
|
15
15
|
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
16
17
|
import type { Theme } from "../../modes/theme/theme";
|
|
17
18
|
import { theme } from "../../modes/theme/theme";
|
|
18
19
|
import {
|
|
@@ -129,7 +130,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
129
130
|
options: ToolExecutionOptions = {},
|
|
130
131
|
tool: AgentTool | undefined,
|
|
131
132
|
ui: TUI,
|
|
132
|
-
cwd: string =
|
|
133
|
+
cwd: string = getProjectDir(),
|
|
133
134
|
) {
|
|
134
135
|
super();
|
|
135
136
|
this.#toolName = toolName;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
getEnvApiKey,
|
|
6
|
+
getProviderDetails,
|
|
7
|
+
type ProviderDetails,
|
|
8
|
+
type UsageLimit,
|
|
9
|
+
type UsageReport,
|
|
10
|
+
} from "@oh-my-pi/pi-ai";
|
|
5
11
|
import { copyToClipboard } from "@oh-my-pi/pi-natives";
|
|
6
12
|
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
7
13
|
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { setProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
8
15
|
import { $ } from "bun";
|
|
9
16
|
import { reset as resetCapabilities } from "../../capability";
|
|
10
17
|
import { loadCustomShare } from "../../export/custom-share";
|
|
@@ -16,6 +23,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
|
|
|
16
23
|
import { PythonExecutionComponent } from "../../modes/components/python-execution";
|
|
17
24
|
import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
18
25
|
import type { InteractiveModeContext } from "../../modes/types";
|
|
26
|
+
import type { AuthStorage } from "../../session/auth-storage";
|
|
19
27
|
import { createCompactionSummaryMessage } from "../../session/messages";
|
|
20
28
|
import { outputMeta } from "../../tools/output-meta";
|
|
21
29
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
@@ -206,6 +214,25 @@ export class CommandController {
|
|
|
206
214
|
let info = `${theme.bold("Session Info")}\n\n`;
|
|
207
215
|
info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`;
|
|
208
216
|
info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`;
|
|
217
|
+
info += `\n${theme.bold("Provider")}\n`;
|
|
218
|
+
const model = this.ctx.session.model;
|
|
219
|
+
if (!model) {
|
|
220
|
+
info += `${theme.fg("dim", "No model selected")}\n`;
|
|
221
|
+
} else {
|
|
222
|
+
const authMode = resolveProviderAuthMode(this.ctx.session.modelRegistry.authStorage, model.provider);
|
|
223
|
+
const openaiWebsocketSetting = this.ctx.settings.get("providers.openaiWebsockets") ?? "auto";
|
|
224
|
+
const preferOpenAICodexWebsockets =
|
|
225
|
+
openaiWebsocketSetting === "on" ? true : openaiWebsocketSetting === "off" ? false : undefined;
|
|
226
|
+
const providerDetails = getProviderDetails({
|
|
227
|
+
model,
|
|
228
|
+
sessionId: stats.sessionId,
|
|
229
|
+
authMode,
|
|
230
|
+
preferWebsockets: preferOpenAICodexWebsockets,
|
|
231
|
+
providerSessionState: this.ctx.session.providerSessionState,
|
|
232
|
+
});
|
|
233
|
+
info += renderProviderSection(providerDetails, theme);
|
|
234
|
+
}
|
|
235
|
+
info += `\n`;
|
|
209
236
|
info += `${theme.bold("Messages")}\n`;
|
|
210
237
|
info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`;
|
|
211
238
|
info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`;
|
|
@@ -388,6 +415,12 @@ export class CommandController {
|
|
|
388
415
|
}
|
|
389
416
|
this.ctx.statusContainer.clear();
|
|
390
417
|
|
|
418
|
+
if (this.ctx.session.isCompacting) {
|
|
419
|
+
this.ctx.session.abortCompaction();
|
|
420
|
+
while (this.ctx.session.isCompacting) {
|
|
421
|
+
await Bun.sleep(10);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
391
424
|
await this.ctx.session.newSession();
|
|
392
425
|
|
|
393
426
|
this.ctx.statusLine.invalidate();
|
|
@@ -460,7 +493,7 @@ export class CommandController {
|
|
|
460
493
|
try {
|
|
461
494
|
await this.ctx.sessionManager.flush();
|
|
462
495
|
await this.ctx.sessionManager.moveTo(resolvedPath);
|
|
463
|
-
|
|
496
|
+
setProjectDir(resolvedPath);
|
|
464
497
|
resetCapabilities();
|
|
465
498
|
await this.ctx.refreshSlashCommandState(resolvedPath);
|
|
466
499
|
|
|
@@ -733,6 +766,31 @@ function formatDurationShort(ms: number): string {
|
|
|
733
766
|
return `${totalSeconds}s`;
|
|
734
767
|
}
|
|
735
768
|
|
|
769
|
+
function resolveProviderAuthMode(authStorage: AuthStorage, provider: string): string {
|
|
770
|
+
if (authStorage.hasOAuth(provider)) {
|
|
771
|
+
return "oauth";
|
|
772
|
+
}
|
|
773
|
+
if (authStorage.has(provider)) {
|
|
774
|
+
return "api key";
|
|
775
|
+
}
|
|
776
|
+
if (getEnvApiKey(provider)) {
|
|
777
|
+
return "env api key";
|
|
778
|
+
}
|
|
779
|
+
if (authStorage.hasAuth(provider)) {
|
|
780
|
+
return "runtime/fallback";
|
|
781
|
+
}
|
|
782
|
+
return "unknown";
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function renderProviderSection(details: ProviderDetails, uiTheme: Pick<typeof theme, "fg">): string {
|
|
786
|
+
const lines: string[] = [];
|
|
787
|
+
lines.push(`${uiTheme.fg("dim", "Name:")} ${details.provider}`);
|
|
788
|
+
for (const field of details.fields) {
|
|
789
|
+
lines.push(`${uiTheme.fg("dim", `${field.label}:`)} ${field.value}`);
|
|
790
|
+
}
|
|
791
|
+
return `${lines.join("\n")}\n`;
|
|
792
|
+
}
|
|
793
|
+
|
|
736
794
|
function resolveFraction(limit: UsageLimit): number | undefined {
|
|
737
795
|
const amount = limit.amount;
|
|
738
796
|
if (amount.usedFraction !== undefined) return amount.usedFraction;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Handles /mcp subcommands for managing MCP servers.
|
|
5
5
|
*/
|
|
6
6
|
import { Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs";
|
|
7
|
+
import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
8
8
|
import type { SourceMeta } from "../../capability/types";
|
|
9
9
|
import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
|
|
10
10
|
import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
|
|
@@ -528,7 +528,7 @@ export class MCPCommandController {
|
|
|
528
528
|
if (this.ctx.mcpManager) {
|
|
529
529
|
resolvedConfig = await this.ctx.mcpManager.prepareConfig(config);
|
|
530
530
|
} else {
|
|
531
|
-
const tempManager = new MCPManager(
|
|
531
|
+
const tempManager = new MCPManager(getProjectDir());
|
|
532
532
|
tempManager.setAuthStorage(this.ctx.session.modelRegistry.authStorage);
|
|
533
533
|
resolvedConfig = await tempManager.prepareConfig(config);
|
|
534
534
|
}
|
|
@@ -540,7 +540,7 @@ export class MCPCommandController {
|
|
|
540
540
|
async #findConfiguredServer(
|
|
541
541
|
name: string,
|
|
542
542
|
): Promise<{ filePath: string; scope: "user" | "project"; config: MCPServerConfig } | null> {
|
|
543
|
-
const cwd =
|
|
543
|
+
const cwd = getProjectDir();
|
|
544
544
|
const userPath = getMCPConfigPath("user", cwd);
|
|
545
545
|
const projectPath = getMCPConfigPath("project", cwd);
|
|
546
546
|
|
|
@@ -665,7 +665,7 @@ export class MCPCommandController {
|
|
|
665
665
|
async #handleWizardComplete(name: string, config: MCPServerConfig, scope: "user" | "project"): Promise<void> {
|
|
666
666
|
try {
|
|
667
667
|
// Determine file path
|
|
668
|
-
const cwd =
|
|
668
|
+
const cwd = getProjectDir();
|
|
669
669
|
const filePath = getMCPConfigPath(scope, cwd);
|
|
670
670
|
|
|
671
671
|
// Add server to config
|
|
@@ -747,7 +747,7 @@ export class MCPCommandController {
|
|
|
747
747
|
*/
|
|
748
748
|
async #handleList(): Promise<void> {
|
|
749
749
|
try {
|
|
750
|
-
const cwd =
|
|
750
|
+
const cwd = getProjectDir();
|
|
751
751
|
|
|
752
752
|
// Load from both user and project configs
|
|
753
753
|
const userPath = getMCPConfigPath("user", cwd);
|
|
@@ -913,7 +913,7 @@ export class MCPCommandController {
|
|
|
913
913
|
}
|
|
914
914
|
|
|
915
915
|
try {
|
|
916
|
-
const cwd =
|
|
916
|
+
const cwd = getProjectDir();
|
|
917
917
|
const userPath = getMCPConfigPath("user", cwd);
|
|
918
918
|
const projectPath = getMCPConfigPath("project", cwd);
|
|
919
919
|
const filePath = scope === "user" ? userPath : projectPath;
|
|
@@ -957,7 +957,7 @@ export class MCPCommandController {
|
|
|
957
957
|
|
|
958
958
|
let connection: MCPServerConnection | undefined;
|
|
959
959
|
try {
|
|
960
|
-
const cwd =
|
|
960
|
+
const cwd = getProjectDir();
|
|
961
961
|
const userPath = getMCPConfigPath("user", cwd);
|
|
962
962
|
const projectPath = getMCPConfigPath("project", cwd);
|
|
963
963
|
|
|
@@ -989,7 +989,7 @@ export class MCPCommandController {
|
|
|
989
989
|
if (this.ctx.mcpManager) {
|
|
990
990
|
resolvedConfig = await this.ctx.mcpManager.prepareConfig(config);
|
|
991
991
|
} else {
|
|
992
|
-
const tempManager = new MCPManager(
|
|
992
|
+
const tempManager = new MCPManager(getProjectDir());
|
|
993
993
|
tempManager.setAuthStorage(this.ctx.session.modelRegistry.authStorage);
|
|
994
994
|
resolvedConfig = await tempManager.prepareConfig(config);
|
|
995
995
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type
|
|
2
|
+
import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
|
|
5
|
+
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
6
6
|
import { MODEL_ROLES } from "../../config/model-registry";
|
|
7
7
|
import { settings } from "../../config/settings";
|
|
8
8
|
import { DebugSelectorComponent } from "../../debug";
|
|
@@ -32,6 +32,16 @@ import { setPreferredImageProvider, setPreferredSearchProvider } from "../../too
|
|
|
32
32
|
export class SelectorController {
|
|
33
33
|
constructor(private ctx: InteractiveModeContext) {}
|
|
34
34
|
|
|
35
|
+
async #refreshOAuthProviderAuthState(): Promise<void> {
|
|
36
|
+
const oauthProviders = getOAuthProviders();
|
|
37
|
+
await Promise.all(
|
|
38
|
+
oauthProviders.map(provider =>
|
|
39
|
+
this.ctx.session.modelRegistry
|
|
40
|
+
.getApiKeyForProvider(provider.id, this.ctx.session.sessionId)
|
|
41
|
+
.catch(() => undefined),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Shows a selector component in place of the editor.
|
|
37
47
|
* @param create Factory that receives a `done` callback and returns the component and focus target
|
|
@@ -57,7 +67,7 @@ export class SelectorController {
|
|
|
57
67
|
availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
|
|
58
68
|
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
59
69
|
availableThemes,
|
|
60
|
-
cwd:
|
|
70
|
+
cwd: getProjectDir(),
|
|
61
71
|
},
|
|
62
72
|
{
|
|
63
73
|
onChange: (id, value) => this.handleSettingChange(id, value),
|
|
@@ -136,7 +146,7 @@ export class SelectorController {
|
|
|
136
146
|
* Replaces /status with a unified view of all providers and extensions.
|
|
137
147
|
*/
|
|
138
148
|
async showExtensionsDashboard(): Promise<void> {
|
|
139
|
-
const dashboard = await ExtensionDashboard.create(
|
|
149
|
+
const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows);
|
|
140
150
|
this.showSelector(done => {
|
|
141
151
|
dashboard.onClose = () => {
|
|
142
152
|
done();
|
|
@@ -536,8 +546,11 @@ export class SelectorController {
|
|
|
536
546
|
|
|
537
547
|
async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
|
|
538
548
|
if (mode === "logout") {
|
|
539
|
-
|
|
540
|
-
const
|
|
549
|
+
await this.#refreshOAuthProviderAuthState();
|
|
550
|
+
const oauthProviders = getOAuthProviders();
|
|
551
|
+
const loggedInProviders = oauthProviders.filter(provider =>
|
|
552
|
+
this.ctx.session.modelRegistry.authStorage.hasAuth(provider.id),
|
|
553
|
+
);
|
|
541
554
|
if (loggedInProviders.length === 0) {
|
|
542
555
|
this.ctx.showStatus("No OAuth providers logged in. Use /login first.");
|
|
543
556
|
return;
|
|
@@ -545,15 +558,15 @@ export class SelectorController {
|
|
|
545
558
|
}
|
|
546
559
|
|
|
547
560
|
this.showSelector(done => {
|
|
548
|
-
|
|
561
|
+
let selector: OAuthSelectorComponent;
|
|
562
|
+
selector = new OAuthSelectorComponent(
|
|
549
563
|
mode,
|
|
550
564
|
this.ctx.session.modelRegistry.authStorage,
|
|
551
565
|
async (providerId: string) => {
|
|
566
|
+
selector.stopValidation();
|
|
552
567
|
done();
|
|
553
|
-
|
|
554
568
|
if (mode === "login") {
|
|
555
569
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
556
|
-
|
|
557
570
|
try {
|
|
558
571
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
559
572
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -567,7 +580,6 @@ export class SelectorController {
|
|
|
567
580
|
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
568
581
|
}
|
|
569
582
|
this.ctx.ui.requestRender();
|
|
570
|
-
|
|
571
583
|
this.ctx.openInBrowser(info.url);
|
|
572
584
|
},
|
|
573
585
|
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
|
|
@@ -577,7 +589,6 @@ export class SelectorController {
|
|
|
577
589
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", prompt.placeholder), 1, 0));
|
|
578
590
|
}
|
|
579
591
|
this.ctx.ui.requestRender();
|
|
580
|
-
|
|
581
592
|
return new Promise<string>(resolve => {
|
|
582
593
|
const codeInput = new Input();
|
|
583
594
|
codeInput.onSubmit = () => {
|
|
@@ -638,9 +649,22 @@ export class SelectorController {
|
|
|
638
649
|
}
|
|
639
650
|
},
|
|
640
651
|
() => {
|
|
652
|
+
selector.stopValidation();
|
|
641
653
|
done();
|
|
642
654
|
this.ctx.ui.requestRender();
|
|
643
655
|
},
|
|
656
|
+
{
|
|
657
|
+
validateAuth: async (providerId: string) => {
|
|
658
|
+
const apiKey = await this.ctx.session.modelRegistry.getApiKeyForProvider(
|
|
659
|
+
providerId,
|
|
660
|
+
this.ctx.session.sessionId,
|
|
661
|
+
);
|
|
662
|
+
return !!apiKey;
|
|
663
|
+
},
|
|
664
|
+
requestRender: () => {
|
|
665
|
+
this.ctx.ui.requestRender();
|
|
666
|
+
},
|
|
667
|
+
},
|
|
644
668
|
);
|
|
645
669
|
return { component: selector, focus: selector };
|
|
646
670
|
});
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
TUI,
|
|
17
17
|
} from "@oh-my-pi/pi-tui";
|
|
18
18
|
import { $env, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
19
|
-
import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
|
|
19
|
+
import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
20
20
|
import chalk from "chalk";
|
|
21
21
|
import { KeybindingsManager } from "../config/keybindings";
|
|
22
22
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -249,7 +249,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
249
249
|
this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
|
|
250
250
|
debugStartup("InteractiveMode.init:cleanupRegistered");
|
|
251
251
|
|
|
252
|
-
await this.refreshSlashCommandState(
|
|
252
|
+
await this.refreshSlashCommandState(getProjectDir());
|
|
253
253
|
debugStartup("InteractiveMode.init:slashCommands");
|
|
254
254
|
|
|
255
255
|
// Get current model info for welcome screen
|