@oh-my-pi/pi-coding-agent 12.5.1 → 12.7.0
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 +25 -0
- package/docs/extensions.md +31 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +14 -9
- package/src/cli/web-search-cli.ts +2 -0
- package/src/commands/web-search.ts +1 -0
- package/src/config/model-registry.ts +168 -0
- package/src/config/model-resolver.ts +102 -3
- package/src/config/settings-schema.ts +1 -1
- package/src/extensibility/extensions/index.ts +12 -0
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +180 -6
- package/src/extensibility/plugins/git-url.ts +18 -7
- package/src/index.ts +2 -0
- package/src/main.ts +57 -21
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/settings-defs.ts +7 -1
- package/src/modes/components/welcome.ts +44 -14
- package/src/modes/controllers/extension-ui-controller.ts +22 -0
- package/src/modes/controllers/selector-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/sdk.ts +63 -30
- package/src/session/agent-session.ts +84 -14
- package/src/session/auth-storage.ts +43 -11
- package/src/task/render.ts +1 -1
- package/src/web/search/index.ts +15 -5
- package/src/web/search/provider.ts +3 -1
- package/src/web/search/providers/zai.ts +352 -0
- package/src/web/search/types.ts +1 -1
|
@@ -8,7 +8,19 @@
|
|
|
8
8
|
* - Interact with the user via UI primitives
|
|
9
9
|
*/
|
|
10
10
|
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
Api,
|
|
13
|
+
AssistantMessageEvent,
|
|
14
|
+
AssistantMessageEventStream,
|
|
15
|
+
Context,
|
|
16
|
+
ImageContent,
|
|
17
|
+
Model,
|
|
18
|
+
OAuthCredentials,
|
|
19
|
+
OAuthLoginCallbacks,
|
|
20
|
+
SimpleStreamOptions,
|
|
21
|
+
TextContent,
|
|
22
|
+
ToolResultMessage,
|
|
23
|
+
} from "@oh-my-pi/pi-ai";
|
|
12
24
|
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
13
25
|
import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
14
26
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
@@ -64,6 +76,9 @@ export interface ExtensionUIDialogOptions {
|
|
|
64
76
|
outline?: boolean;
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
/** Raw terminal input listener for extensions. */
|
|
80
|
+
export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
83
|
* UI context for extensions to request interactive UI.
|
|
69
84
|
* Each mode (interactive, RPC, print) provides its own implementation.
|
|
@@ -81,6 +96,9 @@ export interface ExtensionUIContext {
|
|
|
81
96
|
/** Show a notification to the user. */
|
|
82
97
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
83
98
|
|
|
99
|
+
/** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
|
|
100
|
+
onTerminalInput(handler: TerminalInputHandler): () => void;
|
|
101
|
+
|
|
84
102
|
/** Set status text in the footer/status bar. Pass undefined to clear. */
|
|
85
103
|
setStatus(key: string, text: string | undefined): void;
|
|
86
104
|
|
|
@@ -157,12 +175,11 @@ export interface ExtensionUIContext {
|
|
|
157
175
|
// ============================================================================
|
|
158
176
|
|
|
159
177
|
export interface ContextUsage {
|
|
160
|
-
tokens
|
|
178
|
+
/** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
|
|
179
|
+
tokens: number | null;
|
|
161
180
|
contextWindow: number;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
trailingTokens: number;
|
|
165
|
-
lastUsageIndex: number | null;
|
|
181
|
+
/** Context usage as percentage of context window, or null if tokens is unknown. */
|
|
182
|
+
percent: number | null;
|
|
166
183
|
}
|
|
167
184
|
|
|
168
185
|
export interface CompactOptions {
|
|
@@ -461,6 +478,51 @@ export interface TurnEndEvent {
|
|
|
461
478
|
toolResults: ToolResultMessage[];
|
|
462
479
|
}
|
|
463
480
|
|
|
481
|
+
/** Fired when a message starts (user, assistant, or toolResult) */
|
|
482
|
+
export interface MessageStartEvent {
|
|
483
|
+
type: "message_start";
|
|
484
|
+
message: AgentMessage;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Fired during assistant message streaming with token-by-token updates */
|
|
488
|
+
export interface MessageUpdateEvent {
|
|
489
|
+
type: "message_update";
|
|
490
|
+
message: AgentMessage;
|
|
491
|
+
assistantMessageEvent: AssistantMessageEvent;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Fired when a message ends */
|
|
495
|
+
export interface MessageEndEvent {
|
|
496
|
+
type: "message_end";
|
|
497
|
+
message: AgentMessage;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Fired when a tool starts executing */
|
|
501
|
+
export interface ToolExecutionStartEvent {
|
|
502
|
+
type: "tool_execution_start";
|
|
503
|
+
toolCallId: string;
|
|
504
|
+
toolName: string;
|
|
505
|
+
args: unknown;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** Fired during tool execution with partial/streaming output */
|
|
509
|
+
export interface ToolExecutionUpdateEvent {
|
|
510
|
+
type: "tool_execution_update";
|
|
511
|
+
toolCallId: string;
|
|
512
|
+
toolName: string;
|
|
513
|
+
args: unknown;
|
|
514
|
+
partialResult: unknown;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Fired when a tool finishes executing */
|
|
518
|
+
export interface ToolExecutionEndEvent {
|
|
519
|
+
type: "tool_execution_end";
|
|
520
|
+
toolCallId: string;
|
|
521
|
+
toolName: string;
|
|
522
|
+
result: unknown;
|
|
523
|
+
isError: boolean;
|
|
524
|
+
}
|
|
525
|
+
|
|
464
526
|
/** Fired when auto-compaction starts */
|
|
465
527
|
export interface AutoCompactionStartEvent {
|
|
466
528
|
type: "auto_compaction_start";
|
|
@@ -700,6 +762,12 @@ export type ExtensionEvent =
|
|
|
700
762
|
| AgentEndEvent
|
|
701
763
|
| TurnStartEvent
|
|
702
764
|
| TurnEndEvent
|
|
765
|
+
| MessageStartEvent
|
|
766
|
+
| MessageUpdateEvent
|
|
767
|
+
| MessageEndEvent
|
|
768
|
+
| ToolExecutionStartEvent
|
|
769
|
+
| ToolExecutionUpdateEvent
|
|
770
|
+
| ToolExecutionEndEvent
|
|
703
771
|
| AutoCompactionStartEvent
|
|
704
772
|
| AutoCompactionEndEvent
|
|
705
773
|
| AutoRetryStartEvent
|
|
@@ -868,6 +936,12 @@ export interface ExtensionAPI {
|
|
|
868
936
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
869
937
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
870
938
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
939
|
+
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|
|
940
|
+
on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
|
|
941
|
+
on(event: "message_end", handler: ExtensionHandler<MessageEndEvent>): void;
|
|
942
|
+
on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
|
|
943
|
+
on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
|
|
944
|
+
on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
|
|
871
945
|
on(event: "auto_compaction_start", handler: ExtensionHandler<AutoCompactionStartEvent>): void;
|
|
872
946
|
on(event: "auto_compaction_end", handler: ExtensionHandler<AutoCompactionEndEvent>): void;
|
|
873
947
|
on(event: "auto_retry_start", handler: ExtensionHandler<AutoRetryStartEvent>): void;
|
|
@@ -976,10 +1050,108 @@ export interface ExtensionAPI {
|
|
|
976
1050
|
/** Set thinking level (clamped to model capabilities). */
|
|
977
1051
|
setThinkingLevel(level: ThinkingLevel): void;
|
|
978
1052
|
|
|
1053
|
+
// =========================================================================
|
|
1054
|
+
// Provider Registration
|
|
1055
|
+
// =========================================================================
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Register or override a model provider.
|
|
1059
|
+
*
|
|
1060
|
+
* If `models` is provided: replaces all existing models for this provider.
|
|
1061
|
+
* If only `baseUrl` is provided: overrides the URL for existing models.
|
|
1062
|
+
* If `streamSimple` is provided: registers a custom API stream handler.
|
|
1063
|
+
*
|
|
1064
|
+
* @example
|
|
1065
|
+
* // Register a new provider with custom models and streaming
|
|
1066
|
+
* pi.registerProvider("google-vertex-claude", {
|
|
1067
|
+
* baseUrl: "https://us-east5-aiplatform.googleapis.com",
|
|
1068
|
+
* apiKey: "GOOGLE_CLOUD_PROJECT",
|
|
1069
|
+
* api: "vertex-claude-api",
|
|
1070
|
+
* streamSimple: myStreamFunction,
|
|
1071
|
+
* models: [
|
|
1072
|
+
* {
|
|
1073
|
+
* id: "claude-sonnet-4@20250514",
|
|
1074
|
+
* name: "Claude Sonnet 4 (Vertex)",
|
|
1075
|
+
* reasoning: true,
|
|
1076
|
+
* input: ["text", "image"],
|
|
1077
|
+
* cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
1078
|
+
* contextWindow: 200000,
|
|
1079
|
+
* maxTokens: 64000,
|
|
1080
|
+
* }
|
|
1081
|
+
* ]
|
|
1082
|
+
* });
|
|
1083
|
+
*
|
|
1084
|
+
* @example
|
|
1085
|
+
* // Override baseUrl for an existing provider
|
|
1086
|
+
* pi.registerProvider("anthropic", {
|
|
1087
|
+
* baseUrl: "https://proxy.example.com"
|
|
1088
|
+
* });
|
|
1089
|
+
*/
|
|
1090
|
+
registerProvider(name: string, config: ProviderConfig): void;
|
|
1091
|
+
|
|
979
1092
|
/** Shared event bus for extension communication. */
|
|
980
1093
|
events: EventBus;
|
|
981
1094
|
}
|
|
982
1095
|
|
|
1096
|
+
// ============================================================================
|
|
1097
|
+
// Provider Registration Types
|
|
1098
|
+
// ============================================================================
|
|
1099
|
+
|
|
1100
|
+
/** Configuration for registering a provider via pi.registerProvider(). */
|
|
1101
|
+
export interface ProviderConfig {
|
|
1102
|
+
/** Base URL for the API endpoint. Required when defining models. */
|
|
1103
|
+
baseUrl?: string;
|
|
1104
|
+
/** API key or environment variable name. Required when defining models unless oauth is provided. */
|
|
1105
|
+
apiKey?: string;
|
|
1106
|
+
/** API type identifier. Required when registering streamSimple or when models don't specify one. */
|
|
1107
|
+
api?: Api;
|
|
1108
|
+
/** Custom streaming function for non-built-in APIs. */
|
|
1109
|
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
1110
|
+
/** Custom headers to include in requests. */
|
|
1111
|
+
headers?: Record<string, string>;
|
|
1112
|
+
/** If true, adds Authorization: Bearer header with the resolved API key. */
|
|
1113
|
+
authHeader?: boolean;
|
|
1114
|
+
/** Models to register. If provided, replaces all existing models for this provider. */
|
|
1115
|
+
models?: ProviderModelConfig[];
|
|
1116
|
+
/** OAuth provider for /login support. */
|
|
1117
|
+
oauth?: {
|
|
1118
|
+
/** Display name in login UI. */
|
|
1119
|
+
name: string;
|
|
1120
|
+
/** Run the provider login flow and return credentials (or a plain API key) to persist. */
|
|
1121
|
+
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials | string>;
|
|
1122
|
+
/** Refresh expired credentials. */
|
|
1123
|
+
refreshToken?(credentials: OAuthCredentials): Promise<OAuthCredentials>;
|
|
1124
|
+
/** Convert credentials to an API key string for requests. */
|
|
1125
|
+
getApiKey?(credentials: OAuthCredentials): string;
|
|
1126
|
+
/** Optional model rewrite hook for credential-aware routing (e.g., enterprise URLs). */
|
|
1127
|
+
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/** Configuration for a model within a provider. */
|
|
1132
|
+
export interface ProviderModelConfig {
|
|
1133
|
+
/** Model ID (e.g., "claude-sonnet-4@20250514"). */
|
|
1134
|
+
id: string;
|
|
1135
|
+
/** Display name (e.g., "Claude Sonnet 4 (Vertex)"). */
|
|
1136
|
+
name: string;
|
|
1137
|
+
/** API type override for this model. */
|
|
1138
|
+
api?: Api;
|
|
1139
|
+
/** Whether the model supports extended thinking. */
|
|
1140
|
+
reasoning: boolean;
|
|
1141
|
+
/** Supported input types. */
|
|
1142
|
+
input: ("text" | "image")[];
|
|
1143
|
+
/** Cost per million tokens. */
|
|
1144
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
1145
|
+
/** Maximum context window size in tokens. */
|
|
1146
|
+
contextWindow: number;
|
|
1147
|
+
/** Maximum output tokens. */
|
|
1148
|
+
maxTokens: number;
|
|
1149
|
+
/** Custom headers for this model. */
|
|
1150
|
+
headers?: Record<string, string>;
|
|
1151
|
+
/** OpenAI compatibility settings. */
|
|
1152
|
+
compat?: Model<Api>["compat"];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
983
1155
|
/** Extension factory function type. Supports both sync and async initialization. */
|
|
984
1156
|
export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
|
|
985
1157
|
|
|
@@ -1038,6 +1210,8 @@ export type SetThinkingLevelHandler = (level: ThinkingLevel, persist?: boolean)
|
|
|
1038
1210
|
/** Shared state created by loader, used during registration and runtime. */
|
|
1039
1211
|
export interface ExtensionRuntimeState {
|
|
1040
1212
|
flagValues: Map<string, boolean | string>;
|
|
1213
|
+
/** Provider registrations queued during extension loading, processed during session initialization */
|
|
1214
|
+
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }>;
|
|
1041
1215
|
}
|
|
1042
1216
|
|
|
1043
1217
|
/** Action implementations for ExtensionAPI methods. */
|
|
@@ -172,7 +172,7 @@ function parseGenericGitUrl(url: string): GitSource | null {
|
|
|
172
172
|
if (scpLikeMatch) {
|
|
173
173
|
host = scpLikeMatch[1] ?? "";
|
|
174
174
|
repoPath = scpLikeMatch[2] ?? "";
|
|
175
|
-
} else if (/^https?:\/\/|^ssh:\/\//.test(repoWithoutRef)) {
|
|
175
|
+
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(repoWithoutRef)) {
|
|
176
176
|
try {
|
|
177
177
|
const parsed = new URL(repoWithoutRef);
|
|
178
178
|
if (parsed.hash) {
|
|
@@ -210,20 +210,30 @@ function parseGenericGitUrl(url: string): GitSource | null {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
* Parse
|
|
213
|
+
* Parse git source into a GitSource.
|
|
214
|
+
*
|
|
215
|
+
* Rules:
|
|
216
|
+
* - With `git:` prefix, accept shorthand forms.
|
|
217
|
+
* - Without `git:` prefix, only accept explicit protocol URLs.
|
|
214
218
|
*
|
|
215
219
|
* Handles:
|
|
216
220
|
* - `git:` prefixed URLs (`git:github.com/user/repo`)
|
|
217
|
-
* - SSH SCP-like URLs (`git@github.com:user/repo`)
|
|
218
|
-
* - HTTPS/HTTP/SSH protocol URLs
|
|
219
|
-
* - Bare `host/user/repo` shorthand
|
|
221
|
+
* - SSH SCP-like URLs (`git:git@github.com:user/repo`)
|
|
222
|
+
* - HTTPS/HTTP/SSH/git protocol URLs
|
|
220
223
|
* - Ref pinning via `@ref` suffix
|
|
221
224
|
*
|
|
222
225
|
* Recognizes GitHub, GitLab, Bitbucket, Sourcehut, and Codeberg natively.
|
|
223
226
|
* Falls back to generic URL parsing for other hosts.
|
|
224
227
|
*/
|
|
225
228
|
export function parseGitUrl(source: string): GitSource | null {
|
|
226
|
-
const
|
|
229
|
+
const trimmed = source.trim();
|
|
230
|
+
const hasGitPrefix = /^git:(?!\/\/)/i.test(trimmed);
|
|
231
|
+
const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed;
|
|
232
|
+
|
|
233
|
+
if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
227
237
|
const hashIndex = url.indexOf("#");
|
|
228
238
|
if (hashIndex >= 0) {
|
|
229
239
|
const hash = url.slice(hashIndex + 1);
|
|
@@ -244,7 +254,7 @@ export function parseGitUrl(source: string): GitSource | null {
|
|
|
244
254
|
const directCandidates: string[] = [];
|
|
245
255
|
if (scpMatch) {
|
|
246
256
|
directCandidates.push(`https://${scpMatch[1]}/${scpMatch[2]}`);
|
|
247
|
-
} else if (/^https?:\/\/|^ssh:\/\//.test(split.repo)) {
|
|
257
|
+
} else if (/^https?:\/\/|^ssh:\/\/|^git:\/\//.test(split.repo)) {
|
|
248
258
|
directCandidates.push(split.repo);
|
|
249
259
|
}
|
|
250
260
|
|
|
@@ -254,6 +264,7 @@ export function parseGitUrl(source: string): GitSource | null {
|
|
|
254
264
|
!split.repo.startsWith("http://") &&
|
|
255
265
|
!split.repo.startsWith("https://") &&
|
|
256
266
|
!split.repo.startsWith("ssh://") &&
|
|
267
|
+
!split.repo.startsWith("git://") &&
|
|
257
268
|
!split.repo.startsWith("git@");
|
|
258
269
|
const result = tryKnownHostSource(split, withRef, needsHttps ? `https://${split.repo}` : split.repo);
|
|
259
270
|
if (result) return result;
|
package/src/index.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { listModels } from "./cli/list-models";
|
|
|
20
20
|
import { selectSession } from "./cli/session-picker";
|
|
21
21
|
import { findConfigFile } from "./config";
|
|
22
22
|
import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
|
|
23
|
-
import {
|
|
23
|
+
import { parseModelString, resolveCliModel, resolveModelScope, type ScopedModel } from "./config/model-resolver";
|
|
24
24
|
import { Settings, settings } from "./config/settings";
|
|
25
25
|
import { initializeWithSettings } from "./discovery";
|
|
26
26
|
import { exportFromFile } from "./export/html";
|
|
@@ -355,10 +355,11 @@ async function buildSessionOptions(
|
|
|
355
355
|
scopedModels: ScopedModel[],
|
|
356
356
|
sessionManager: SessionManager | undefined,
|
|
357
357
|
modelRegistry: ModelRegistry,
|
|
358
|
-
): Promise<CreateAgentSessionOptions> {
|
|
358
|
+
): Promise<{ options: CreateAgentSessionOptions; cliThinkingFromModel: boolean }> {
|
|
359
359
|
const options: CreateAgentSessionOptions = {
|
|
360
360
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
361
361
|
};
|
|
362
|
+
let cliThinkingFromModel = false;
|
|
362
363
|
|
|
363
364
|
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
|
364
365
|
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
@@ -370,22 +371,39 @@ async function buildSessionOptions(
|
|
|
370
371
|
options.sessionManager = sessionManager;
|
|
371
372
|
}
|
|
372
373
|
|
|
373
|
-
// Model from CLI
|
|
374
|
+
// Model from CLI
|
|
375
|
+
// - supports --provider <name> --model <pattern>
|
|
376
|
+
// - supports --model <provider>/<pattern>
|
|
374
377
|
if (parsed.model) {
|
|
375
|
-
const available = modelRegistry.getAvailable();
|
|
376
378
|
const modelMatchPreferences = {
|
|
377
379
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
378
380
|
};
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
381
|
+
const resolved = resolveCliModel({
|
|
382
|
+
cliProvider: parsed.provider,
|
|
383
|
+
cliModel: parsed.model,
|
|
384
|
+
modelRegistry,
|
|
385
|
+
preferences: modelMatchPreferences,
|
|
386
|
+
});
|
|
387
|
+
if (resolved.warning) {
|
|
388
|
+
writeStderr(chalk.yellow(`Warning: ${resolved.warning}`));
|
|
382
389
|
}
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
390
|
+
if (resolved.error) {
|
|
391
|
+
if (!parsed.provider && !parsed.model.includes(":")) {
|
|
392
|
+
// Model not found in built-in registry — defer resolution to after extensions load
|
|
393
|
+
// (extensions may register additional providers/models via registerProvider)
|
|
394
|
+
options.modelPattern = parsed.model;
|
|
395
|
+
} else {
|
|
396
|
+
writeStderr(chalk.red(resolved.error));
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
} else if (resolved.model) {
|
|
400
|
+
options.model = resolved.model;
|
|
401
|
+
settings.overrideModelRoles({ default: `${resolved.model.provider}/${resolved.model.id}` });
|
|
402
|
+
if (!parsed.thinking && resolved.thinkingLevel) {
|
|
403
|
+
options.thinkingLevel = resolved.thinkingLevel;
|
|
404
|
+
cliThinkingFromModel = true;
|
|
405
|
+
}
|
|
386
406
|
}
|
|
387
|
-
options.model = model;
|
|
388
|
-
settings.overrideModelRoles({ default: `${model.provider}/${model.id}` });
|
|
389
407
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
|
390
408
|
const remembered = settings.getModelRole("default");
|
|
391
409
|
if (remembered) {
|
|
@@ -470,7 +488,7 @@ async function buildSessionOptions(
|
|
|
470
488
|
options.additionalExtensionPaths = [];
|
|
471
489
|
}
|
|
472
490
|
|
|
473
|
-
return options;
|
|
491
|
+
return { options, cliThinkingFromModel };
|
|
474
492
|
}
|
|
475
493
|
|
|
476
494
|
export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<void> {
|
|
@@ -602,7 +620,12 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
602
620
|
sessionManager = await SessionManager.open(selectedPath);
|
|
603
621
|
}
|
|
604
622
|
|
|
605
|
-
const sessionOptions = await buildSessionOptions(
|
|
623
|
+
const { options: sessionOptions, cliThinkingFromModel } = await buildSessionOptions(
|
|
624
|
+
parsedArgs,
|
|
625
|
+
scopedModels,
|
|
626
|
+
sessionManager,
|
|
627
|
+
modelRegistry,
|
|
628
|
+
);
|
|
606
629
|
debugStartup("main:buildSessionOptions");
|
|
607
630
|
sessionOptions.authStorage = authStorage;
|
|
608
631
|
sessionOptions.modelRegistry = modelRegistry;
|
|
@@ -610,11 +633,15 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
610
633
|
|
|
611
634
|
// Handle CLI --api-key as runtime override (not persisted)
|
|
612
635
|
if (parsedArgs.apiKey) {
|
|
613
|
-
if (!sessionOptions.model) {
|
|
614
|
-
writeStderr(
|
|
636
|
+
if (!sessionOptions.model && !sessionOptions.modelPattern) {
|
|
637
|
+
writeStderr(
|
|
638
|
+
chalk.red("--api-key requires a model to be specified via --model, --provider/--model, or --models"),
|
|
639
|
+
);
|
|
615
640
|
process.exit(1);
|
|
616
641
|
}
|
|
617
|
-
|
|
642
|
+
if (sessionOptions.model) {
|
|
643
|
+
authStorage.setRuntimeApiKey(sessionOptions.model.provider, parsedArgs.apiKey);
|
|
644
|
+
}
|
|
618
645
|
}
|
|
619
646
|
|
|
620
647
|
time("buildSessionOptions");
|
|
@@ -622,6 +649,9 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
622
649
|
await createAgentSession(sessionOptions);
|
|
623
650
|
debugStartup("main:createAgentSession");
|
|
624
651
|
time("createAgentSession");
|
|
652
|
+
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
|
653
|
+
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
|
654
|
+
}
|
|
625
655
|
|
|
626
656
|
if (modelFallbackMessage) {
|
|
627
657
|
notifs.push({ kind: "warn", message: modelFallbackMessage });
|
|
@@ -660,16 +690,22 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
|
|
660
690
|
debugStartup("main:applyExtensionFlags");
|
|
661
691
|
|
|
662
692
|
if (!isInteractive && !session.model) {
|
|
663
|
-
|
|
693
|
+
if (modelFallbackMessage) {
|
|
694
|
+
writeStderr(chalk.red(modelFallbackMessage));
|
|
695
|
+
} else {
|
|
696
|
+
writeStderr(chalk.red("No models available."));
|
|
697
|
+
}
|
|
664
698
|
writeStderr(chalk.yellow("\nSet an API key environment variable:"));
|
|
665
699
|
writeStderr(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
|
|
666
700
|
writeStderr(chalk.yellow(`\nOr create ${ModelsConfigFile.path()}`));
|
|
667
701
|
process.exit(1);
|
|
668
702
|
}
|
|
669
703
|
|
|
670
|
-
// Clamp thinking level to model capabilities
|
|
671
|
-
|
|
672
|
-
|
|
704
|
+
// Clamp thinking level to model capabilities for CLI-provided thinking levels.
|
|
705
|
+
// This covers both --thinking <level> and --model <pattern>:<thinking>.
|
|
706
|
+
const cliThinkingOverride = parsedArgs.thinking !== undefined || cliThinkingFromModel;
|
|
707
|
+
if (session.model && cliThinkingOverride) {
|
|
708
|
+
let effectiveThinking = session.thinkingLevel;
|
|
673
709
|
if (!session.model.reasoning) {
|
|
674
710
|
effectiveThinking = "off";
|
|
675
711
|
} else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
3
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
4
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
5
|
import { getProjectDir } from "@oh-my-pi/pi-utils/dirs";
|
|
@@ -175,22 +174,12 @@ export class FooterComponent implements Component {
|
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Calculate context percentage from last message (input + output + cacheRead + cacheWrite)
|
|
185
|
-
const contextTokens = lastAssistantMessage
|
|
186
|
-
? lastAssistantMessage.usage.input +
|
|
187
|
-
lastAssistantMessage.usage.output +
|
|
188
|
-
lastAssistantMessage.usage.cacheRead +
|
|
189
|
-
lastAssistantMessage.usage.cacheWrite
|
|
190
|
-
: 0;
|
|
191
|
-
const contextWindow = state.model?.contextWindow || 0;
|
|
192
|
-
const contextPercentValue = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
|
|
193
|
-
const contextPercent = contextPercentValue.toFixed(1);
|
|
177
|
+
// Calculate context usage from session (handles compaction correctly).
|
|
178
|
+
// After compaction, tokens are unknown until the next LLM response.
|
|
179
|
+
const contextUsage = this.session.getContextUsage();
|
|
180
|
+
const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
|
|
181
|
+
const contextPercentValue = contextUsage?.percent ?? 0;
|
|
182
|
+
const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
|
|
194
183
|
|
|
195
184
|
// Format token counts (similar to web-ui)
|
|
196
185
|
const formatTokens = (count: number): string => {
|
|
@@ -239,7 +228,10 @@ export class FooterComponent implements Component {
|
|
|
239
228
|
// Colorize context percentage based on usage
|
|
240
229
|
let contextPercentStr: string;
|
|
241
230
|
const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
|
|
242
|
-
const contextPercentDisplay =
|
|
231
|
+
const contextPercentDisplay =
|
|
232
|
+
contextPercent === "?"
|
|
233
|
+
? `?/${formatTokens(contextWindow)}${autoIndicator}`
|
|
234
|
+
: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
243
235
|
if (contextPercentValue > 90) {
|
|
244
236
|
contextPercentStr = theme.fg("error", contextPercentDisplay);
|
|
245
237
|
} else if (contextPercentValue > 70) {
|
|
@@ -146,10 +146,16 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
146
146
|
],
|
|
147
147
|
// Provider options
|
|
148
148
|
"providers.webSearch": [
|
|
149
|
-
{
|
|
149
|
+
{
|
|
150
|
+
value: "auto",
|
|
151
|
+
label: "Auto",
|
|
152
|
+
description: "Priority: Exa > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI",
|
|
153
|
+
},
|
|
150
154
|
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
155
|
+
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
151
156
|
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
|
|
152
157
|
{ value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
|
|
158
|
+
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
153
159
|
],
|
|
154
160
|
"providers.image": [
|
|
155
161
|
{ value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
|
|
@@ -41,12 +41,31 @@ export class WelcomeComponent implements Component {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
render(termWidth: number): string[] {
|
|
44
|
-
// Box dimensions - responsive with
|
|
45
|
-
const minWidth = 80;
|
|
44
|
+
// Box dimensions - responsive with max width and small-terminal support
|
|
46
45
|
const maxWidth = 100;
|
|
47
|
-
const boxWidth = Math.
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const boxWidth = Math.min(maxWidth, Math.max(0, termWidth - 2));
|
|
47
|
+
if (boxWidth < 4) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const dualContentWidth = boxWidth - 3; // 3 = │ + │ + │
|
|
51
|
+
const preferredLeftCol = 26;
|
|
52
|
+
const minLeftCol = 14; // logo width
|
|
53
|
+
const minRightCol = 20;
|
|
54
|
+
const leftMinContentWidth = Math.max(
|
|
55
|
+
minLeftCol,
|
|
56
|
+
visibleWidth("Welcome back!"),
|
|
57
|
+
visibleWidth(this.modelName),
|
|
58
|
+
visibleWidth(this.providerName),
|
|
59
|
+
);
|
|
60
|
+
const desiredLeftCol = Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.35)));
|
|
61
|
+
const dualLeftCol =
|
|
62
|
+
dualContentWidth >= minRightCol + 1
|
|
63
|
+
? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
|
|
64
|
+
: Math.max(1, dualContentWidth - 1);
|
|
65
|
+
const dualRightCol = Math.max(1, dualContentWidth - dualLeftCol);
|
|
66
|
+
const showRightColumn = dualLeftCol >= leftMinContentWidth && dualRightCol >= minRightCol;
|
|
67
|
+
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
68
|
+
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
50
69
|
|
|
51
70
|
// Block-based OMP logo (gradient: magenta → cyan)
|
|
52
71
|
// biome-ignore format: preserve ASCII art layout
|
|
@@ -67,7 +86,7 @@ export class WelcomeComponent implements Component {
|
|
|
67
86
|
];
|
|
68
87
|
|
|
69
88
|
// Right column separator
|
|
70
|
-
const separatorWidth = rightCol - 2; // padding on each side
|
|
89
|
+
const separatorWidth = Math.max(0, rightCol - 2); // padding on each side
|
|
71
90
|
const separator = ` ${theme.fg("dim", theme.boxRound.horizontal.repeat(separatorWidth))}`;
|
|
72
91
|
|
|
73
92
|
// Recent sessions content
|
|
@@ -131,20 +150,31 @@ export class WelcomeComponent implements Component {
|
|
|
131
150
|
const titlePrefixRaw = hChar.repeat(3);
|
|
132
151
|
const titleStyled = theme.fg("dim", titlePrefixRaw) + theme.fg("muted", title);
|
|
133
152
|
const titleVisLen = visibleWidth(titlePrefixRaw) + visibleWidth(title);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
const titleSpace = boxWidth - 2;
|
|
154
|
+
if (titleVisLen >= titleSpace) {
|
|
155
|
+
lines.push(tl + truncateToWidth(titleStyled, titleSpace) + tr);
|
|
156
|
+
} else {
|
|
157
|
+
const afterTitle = titleSpace - titleVisLen;
|
|
158
|
+
lines.push(tl + titleStyled + theme.fg("dim", hChar.repeat(afterTitle)) + tr);
|
|
159
|
+
}
|
|
137
160
|
|
|
138
161
|
// Content rows
|
|
139
|
-
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
162
|
+
const maxRows = showRightColumn ? Math.max(leftLines.length, rightLines.length) : leftLines.length;
|
|
140
163
|
for (let i = 0; i < maxRows; i++) {
|
|
141
164
|
const left = this.#fitToWidth(leftLines[i] ?? "", leftCol);
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
if (showRightColumn) {
|
|
166
|
+
const right = this.#fitToWidth(rightLines[i] ?? "", rightCol);
|
|
167
|
+
lines.push(v + left + v + right + v);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(v + left + v);
|
|
170
|
+
}
|
|
144
171
|
}
|
|
145
|
-
|
|
146
172
|
// Bottom border
|
|
147
|
-
|
|
173
|
+
if (showRightColumn) {
|
|
174
|
+
lines.push(bl + h.repeat(leftCol) + theme.fg("dim", theme.boxSharp.teeUp) + h.repeat(rightCol) + br);
|
|
175
|
+
} else {
|
|
176
|
+
lines.push(bl + h.repeat(leftCol) + br);
|
|
177
|
+
}
|
|
148
178
|
|
|
149
179
|
return lines;
|
|
150
180
|
}
|