@oh-my-pi/pi-coding-agent 16.3.12 → 16.3.13
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/dist/cli.js +3244 -3246
- package/dist/types/config/keybindings.d.ts +9 -4
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -2
- package/dist/types/mnemopi/state.d.ts +7 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +11 -5
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +3 -4
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tools/renderers.d.ts +12 -5
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tools/write.d.ts +1 -0
- package/package.json +12 -12
- package/src/config/keybindings.ts +62 -10
- package/src/config/model-registry.ts +85 -20
- package/src/config/settings.ts +48 -21
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/extensions/types.ts +13 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mnemopi/state.ts +19 -5
- package/src/modes/acp/acp-agent.ts +69 -8
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/read-tool-group.ts +5 -1
- package/src/modes/components/tool-execution.ts +28 -24
- package/src/modes/controllers/extension-ui-controller.test.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +5 -55
- package/src/modes/interactive-mode.ts +43 -2
- package/src/modes/rpc/rpc-client.ts +42 -13
- package/src/modes/rpc/rpc-mode.ts +21 -19
- package/src/modes/types.ts +3 -0
- package/src/prompts/agents/plan.md +0 -1
- package/src/prompts/agents/reviewer.md +0 -1
- package/src/prompts/tools/grep.md +2 -1
- package/src/prompts/tools/memory-edit.md +2 -0
- package/src/session/agent-session.ts +8 -5
- package/src/tools/grep.ts +58 -13
- package/src/tools/image-gen.ts +1 -1
- package/src/tools/memory-edit.ts +3 -1
- package/src/tools/read.ts +33 -13
- package/src/tools/renderers.ts +13 -5
- package/src/tools/ssh.ts +10 -3
- package/src/tools/tts.ts +1 -1
- package/src/tools/write.ts +26 -0
|
@@ -323,6 +323,11 @@ export declare const KEYBINDINGS: {
|
|
|
323
323
|
readonly description: "Toggle speech-to-text (default gesture: hold Space)";
|
|
324
324
|
};
|
|
325
325
|
};
|
|
326
|
+
/** Controls inherited keybinding lookup when creating a manager for a named profile. */
|
|
327
|
+
export interface KeybindingsCreateOptions {
|
|
328
|
+
/** Default-profile agent directory whose keybindings are merged before profile-specific bindings. */
|
|
329
|
+
inheritedAgentDir?: string;
|
|
330
|
+
}
|
|
326
331
|
declare function migrateKeybindingsConfigFile(agentDir: string): void;
|
|
327
332
|
/**
|
|
328
333
|
* Manages all keybindings (app + TUI).
|
|
@@ -330,18 +335,18 @@ declare function migrateKeybindingsConfigFile(agentDir: string): void;
|
|
|
330
335
|
*/
|
|
331
336
|
export declare class KeybindingsManager extends TuiKeybindingsManager {
|
|
332
337
|
#private;
|
|
333
|
-
constructor(userBindings?: KeybindingsConfig, configPath?: string);
|
|
338
|
+
constructor(userBindings?: KeybindingsConfig, configPath?: string, inheritedConfigPath?: string);
|
|
334
339
|
/**
|
|
335
|
-
* Create from config
|
|
340
|
+
* Create from config files at agentDir/keybindings.yml and the default profile.
|
|
336
341
|
* Legacy keybindings.json is migrated to keybindings.yml on load.
|
|
337
342
|
*/
|
|
338
|
-
static create(agentDir?: string): KeybindingsManager;
|
|
343
|
+
static create(agentDir?: string, options?: KeybindingsCreateOptions): KeybindingsManager;
|
|
339
344
|
/**
|
|
340
345
|
* Create an in-memory keybindings manager without file persistence.
|
|
341
346
|
*/
|
|
342
347
|
static inMemory(userBindings?: KeybindingsConfig): KeybindingsManager;
|
|
343
348
|
/**
|
|
344
|
-
* Reload keybindings from the config
|
|
349
|
+
* Reload keybindings from the config files.
|
|
345
350
|
*/
|
|
346
351
|
reload(): void;
|
|
347
352
|
setUserBindings(userBindings: KeybindingsConfig): void;
|
|
@@ -24,7 +24,7 @@ export interface RawSettings {
|
|
|
24
24
|
export interface SettingsOptions {
|
|
25
25
|
/** Current working directory for project settings discovery */
|
|
26
26
|
cwd?: string;
|
|
27
|
-
/** Agent directory for config.yml storage */
|
|
27
|
+
/** Agent directory for config.yml/config.yaml storage */
|
|
28
28
|
agentDir?: string;
|
|
29
29
|
/** Don't persist to disk (for tests) */
|
|
30
30
|
inMemory?: boolean;
|
|
@@ -11,7 +11,7 @@ import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLe
|
|
|
11
11
|
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
12
12
|
import type { Api, AssistantMessageEvent, AssistantMessageEventStream, Context, ImageContent, Model, ModelSpec, ProviderResponseMetadata, SimpleStreamOptions, Static, TextContent, TSchema } from "@oh-my-pi/pi-ai";
|
|
13
13
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
|
|
14
|
-
import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
14
|
+
import type { AutocompleteItem, AutocompleteProvider, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
15
15
|
import type { logger as PiLogger } from "@oh-my-pi/pi-utils";
|
|
16
16
|
import type { Type as arktype } from "arktype";
|
|
17
17
|
import type * as zod from "zod/v4";
|
|
@@ -89,6 +89,8 @@ export type ExtensionUiComponent = Component & {
|
|
|
89
89
|
};
|
|
90
90
|
export type ExtensionUiComponentFactory = (tui: TUI, theme: Theme) => ExtensionUiComponent;
|
|
91
91
|
export type ExtensionWidgetContent = string[] | ExtensionUiComponentFactory | undefined;
|
|
92
|
+
/** Wrap the current autocomplete provider with additional behavior (pi-compatible). */
|
|
93
|
+
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
|
|
92
94
|
/**
|
|
93
95
|
* UI context for extensions to request interactive UI.
|
|
94
96
|
* Each mode (interactive, RPC, print) provides its own implementation.
|
|
@@ -135,6 +137,13 @@ export interface ExtensionUIContext {
|
|
|
135
137
|
editor(title: string, prefill?: string, dialogOptions?: ExtensionUIDialogOptions, editorOptions?: {
|
|
136
138
|
promptStyle?: boolean;
|
|
137
139
|
}): Promise<string | undefined>;
|
|
140
|
+
/**
|
|
141
|
+
* Stack additional autocomplete behavior on top of the built-in provider
|
|
142
|
+
* (pi-compatible). Interactive mode rebuilds the editor's provider through
|
|
143
|
+
* every registered factory, in registration order; headless modes (print,
|
|
144
|
+
* RPC, ACP, subagents) accept and ignore the factory.
|
|
145
|
+
*/
|
|
146
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
|
138
147
|
/**
|
|
139
148
|
* Set a custom editor component via factory function, or `undefined` to restore the default editor.
|
|
140
149
|
*
|
|
@@ -707,7 +716,7 @@ export interface ExtensionAPI {
|
|
|
707
716
|
triggerTurn?: boolean;
|
|
708
717
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
709
718
|
}): void;
|
|
710
|
-
/** Send a user
|
|
719
|
+
/** Send a user prompt: idle starts a turn; streaming queues as steer unless deliverAs is set. */
|
|
711
720
|
sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
|
|
712
721
|
deliverAs?: "steer" | "followUp";
|
|
713
722
|
}): void;
|
|
@@ -33,10 +33,14 @@ export interface MnemopiMemoryEditOptions {
|
|
|
33
33
|
replacementId?: string;
|
|
34
34
|
}
|
|
35
35
|
export interface MnemopiMemoryEditResult {
|
|
36
|
-
status: "updated" | "deleted" | "invalidated" | "not_found";
|
|
36
|
+
status: "updated" | "deleted" | "invalidated" | "not_found" | "not_editable";
|
|
37
37
|
bank?: string;
|
|
38
|
-
store?:
|
|
38
|
+
store?: MnemopiMemoryStore;
|
|
39
39
|
}
|
|
40
|
+
/** Which mnemopi table a resolved memory id lives in. `fact` rows are
|
|
41
|
+
* read-only projections of fact extraction (issue #4725): resolvable for
|
|
42
|
+
* reads, never editable. */
|
|
43
|
+
export type MnemopiMemoryStore = "working" | "episodic" | "fact";
|
|
40
44
|
/**
|
|
41
45
|
* Full-row lookup result produced by {@link MnemopiSessionState.getScopedMemory}.
|
|
42
46
|
* Mirrors the shape stored in mnemopi's working/episodic tables, tagged with
|
|
@@ -45,7 +49,7 @@ export interface MnemopiMemoryEditResult {
|
|
|
45
49
|
*/
|
|
46
50
|
export interface MnemopiScopedMemoryHit {
|
|
47
51
|
bank: string;
|
|
48
|
-
store:
|
|
52
|
+
store: MnemopiMemoryStore;
|
|
49
53
|
row: {
|
|
50
54
|
id: string;
|
|
51
55
|
content: string;
|
|
@@ -7,7 +7,7 @@ import type { CollabGuestLink } from "../collab/guest.js";
|
|
|
7
7
|
import type { CollabHost } from "../collab/host.js";
|
|
8
8
|
import { KeybindingsManager } from "../config/keybindings.js";
|
|
9
9
|
import { Settings } from "../config/settings.js";
|
|
10
|
-
import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions/index.js";
|
|
10
|
+
import type { AutocompleteProviderFactory, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions/index.js";
|
|
11
11
|
import type { CompactOptions } from "../extensibility/extensions/types.js";
|
|
12
12
|
import type { Skill } from "../extensibility/skills.js";
|
|
13
13
|
import type { MCPManager } from "../mcp/index.js";
|
|
@@ -180,6 +180,8 @@ export declare class InteractiveMode implements InteractiveModeContext {
|
|
|
180
180
|
refreshTitleSystemPrompt(cwd?: string): Promise<void>;
|
|
181
181
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
182
182
|
refreshSlashCommandState(cwd?: string): Promise<void>;
|
|
183
|
+
/** Stack extension autocomplete behavior on top of the built-in editor provider (#4919). */
|
|
184
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
|
183
185
|
/**
|
|
184
186
|
* Re-point the process and every cwd-derived cache at `newCwd` after the
|
|
185
187
|
* active session's working directory changed (`/move` relocation or resuming
|
|
@@ -269,17 +269,23 @@ export declare class RpcClient {
|
|
|
269
269
|
/**
|
|
270
270
|
* Trigger OAuth login for the given provider.
|
|
271
271
|
* The server will emit an `open_url` extension_ui_request for the auth URL.
|
|
272
|
+
* Providers that require pasted-code completion may then emit an `input`
|
|
273
|
+
* extension_ui_request; pass `onManualCodeInput` to satisfy it.
|
|
272
274
|
* Resolves when login completes or rejects on failure.
|
|
273
275
|
*
|
|
274
276
|
* @param onOpenUrl Called when the server emits the auth URL. The host must
|
|
275
|
-
* open `url` in a browser
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
277
|
+
* open `url` in a browser. When the flow's callback server hosts a
|
|
278
|
+
* `/launch` redirect, `launchUrl` is a short loopback URL that 302s to
|
|
279
|
+
* `url` — hosts SHOULD surface it as the truncation-safe copy target so
|
|
280
|
+
* terminal viewport clipping cannot corrupt trailing OAuth query
|
|
281
|
+
* parameters (e.g. `code_challenge_method=S256`).
|
|
280
282
|
*/
|
|
281
283
|
login(providerId: string, options?: {
|
|
282
284
|
onOpenUrl?: (url: string, instructions?: string, launchUrl?: string) => void;
|
|
285
|
+
onManualCodeInput?: (prompt: {
|
|
286
|
+
title: string;
|
|
287
|
+
placeholder?: string;
|
|
288
|
+
}) => string | Promise<string>;
|
|
283
289
|
}): Promise<{
|
|
284
290
|
providerId: string;
|
|
285
291
|
}>;
|
|
@@ -38,7 +38,7 @@ export type RpcSkillCommandSession = Pick<AgentSession, "promptCustomMessage" |
|
|
|
38
38
|
export type RpcSkillCommandResult = {
|
|
39
39
|
agentInvoked: true;
|
|
40
40
|
};
|
|
41
|
-
export declare function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string): Promise<RpcSkillCommandResult | false>;
|
|
41
|
+
export declare function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string, streamingBehavior?: "steer" | "followUp"): Promise<RpcSkillCommandResult | false>;
|
|
42
42
|
export declare function reportLocalOnlyPromptResult(input: {
|
|
43
43
|
id: string | undefined;
|
|
44
44
|
prompt: Promise<boolean>;
|
|
@@ -6,7 +6,7 @@ import type { CollabGuestLink } from "../collab/guest.js";
|
|
|
6
6
|
import type { CollabHost } from "../collab/host.js";
|
|
7
7
|
import type { KeybindingsManager } from "../config/keybindings.js";
|
|
8
8
|
import type { Settings } from "../config/settings.js";
|
|
9
|
-
import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions/index.js";
|
|
9
|
+
import type { AutocompleteProviderFactory, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions/index.js";
|
|
10
10
|
import type { CompactOptions } from "../extensibility/extensions/types.js";
|
|
11
11
|
import type { Skill } from "../extensibility/skills.js";
|
|
12
12
|
import type { MCPManager } from "../mcp/index.js";
|
|
@@ -195,6 +195,8 @@ export interface InteractiveModeContext {
|
|
|
195
195
|
checkShutdownRequested(): Promise<void>;
|
|
196
196
|
setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
197
197
|
initializeHookRunner(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
198
|
+
/** Stack extension autocomplete behavior on top of the built-in editor provider. */
|
|
199
|
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
|
198
200
|
setEditorComponent(factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined): void;
|
|
199
201
|
/**
|
|
200
202
|
* Mount transcript content and repaint once. The single sink for "show this in
|
|
@@ -814,11 +814,10 @@ export declare class AgentSession {
|
|
|
814
814
|
queueChipText?: string;
|
|
815
815
|
}): Promise<boolean>;
|
|
816
816
|
/**
|
|
817
|
-
* Send a user message
|
|
818
|
-
* When deliverAs is set, queue the message instead of starting a new turn.
|
|
817
|
+
* Send a user message through the prompt flow.
|
|
819
818
|
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
819
|
+
* Omitted `deliverAs` starts a turn when idle and queues as a steer while streaming.
|
|
820
|
+
* Explicit `deliverAs` queues without starting a turn in either state.
|
|
822
821
|
*/
|
|
823
822
|
sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
|
|
824
823
|
deliverAs?: "steer" | "followUp";
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types.js";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme.js";
|
|
9
|
+
/**
|
|
10
|
+
* Per-renderer opt-in for a full viewport replay when the first result
|
|
11
|
+
* replaces a painted pending-call render. A predicate receives the painted
|
|
12
|
+
* call args and render options so the repaint stays scoped to the pending
|
|
13
|
+
* shapes that actually re-anchor (an over-eager replay wipes native
|
|
14
|
+
* scrollback on direct terminals).
|
|
15
|
+
*/
|
|
16
|
+
export type FirstResultViewportRepaint = boolean | ((args: unknown, options: RenderResultOptions) => boolean);
|
|
9
17
|
export type ToolRenderer = {
|
|
10
18
|
renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
|
|
11
19
|
renderResult: (result: {
|
|
@@ -33,12 +41,11 @@ export type ToolRenderer = {
|
|
|
33
41
|
*/
|
|
34
42
|
animatedPartialResult?: boolean | ((args: unknown) => boolean);
|
|
35
43
|
/**
|
|
36
|
-
* Whether replacing a
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* re-anchors instead of preserving.
|
|
44
|
+
* Whether replacing a pending call render with the first result requires a
|
|
45
|
+
* full viewport repaint. Use for merged renderers whose pending rows can be
|
|
46
|
+
* re-anchored instead of preserved by the result render.
|
|
40
47
|
*/
|
|
41
|
-
forceFirstResultViewportRepaint?:
|
|
48
|
+
forceFirstResultViewportRepaint?: FirstResultViewportRepaint;
|
|
42
49
|
/**
|
|
43
50
|
* Whether settling a provisional partial result into the final render requires
|
|
44
51
|
* a full viewport repaint. Use when the result renderer changes chrome or
|
|
@@ -46,6 +46,9 @@ interface SshRenderArgs {
|
|
|
46
46
|
command?: string;
|
|
47
47
|
timeout?: number;
|
|
48
48
|
}
|
|
49
|
+
/** Whether the painted call args still carry the streamed raw-JSON buffer —
|
|
50
|
+
* the shape that renders the `⏳ SSH: […]` / `$ …` placeholder. */
|
|
51
|
+
declare function hasStreamedRenderArgs(args: unknown): boolean;
|
|
49
52
|
interface SshRenderContext {
|
|
50
53
|
/** Visual lines for truncated output (pre-computed by tool-execution) */
|
|
51
54
|
visualLines?: string[];
|
|
@@ -68,7 +71,7 @@ export declare const sshToolRenderer: {
|
|
|
68
71
|
renderContext?: SshRenderContext;
|
|
69
72
|
}, uiTheme: Theme, args?: SshRenderArgs): Component;
|
|
70
73
|
mergeCallAndResult: boolean;
|
|
71
|
-
forceFirstResultViewportRepaint:
|
|
74
|
+
forceFirstResultViewportRepaint: typeof hasStreamedRenderArgs;
|
|
72
75
|
forceResultViewportRepaintOnSettle: boolean;
|
|
73
76
|
};
|
|
74
77
|
export {};
|
|
@@ -62,5 +62,6 @@ export declare const writeToolRenderer: {
|
|
|
62
62
|
isError?: boolean;
|
|
63
63
|
}, options: RenderResultOptions, uiTheme: Theme, args?: WriteRenderArgs): Component;
|
|
64
64
|
mergeCallAndResult: boolean;
|
|
65
|
+
forceFirstResultViewportRepaint: (args: unknown, options: RenderResultOptions) => boolean;
|
|
65
66
|
};
|
|
66
67
|
export {};
|
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": "16.3.
|
|
4
|
+
"version": "16.3.13",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
57
57
|
"@babel/parser": "^7.29.7",
|
|
58
58
|
"@mozilla/readability": "^0.6.0",
|
|
59
|
-
"@oh-my-pi/hashline": "16.3.
|
|
60
|
-
"@oh-my-pi/omp-stats": "16.3.
|
|
61
|
-
"@oh-my-pi/pi-agent-core": "16.3.
|
|
62
|
-
"@oh-my-pi/pi-ai": "16.3.
|
|
63
|
-
"@oh-my-pi/pi-catalog": "16.3.
|
|
64
|
-
"@oh-my-pi/pi-mnemopi": "16.3.
|
|
65
|
-
"@oh-my-pi/pi-natives": "16.3.
|
|
66
|
-
"@oh-my-pi/pi-tui": "16.3.
|
|
67
|
-
"@oh-my-pi/pi-utils": "16.3.
|
|
68
|
-
"@oh-my-pi/pi-wire": "16.3.
|
|
69
|
-
"@oh-my-pi/snapcompact": "16.3.
|
|
59
|
+
"@oh-my-pi/hashline": "16.3.13",
|
|
60
|
+
"@oh-my-pi/omp-stats": "16.3.13",
|
|
61
|
+
"@oh-my-pi/pi-agent-core": "16.3.13",
|
|
62
|
+
"@oh-my-pi/pi-ai": "16.3.13",
|
|
63
|
+
"@oh-my-pi/pi-catalog": "16.3.13",
|
|
64
|
+
"@oh-my-pi/pi-mnemopi": "16.3.13",
|
|
65
|
+
"@oh-my-pi/pi-natives": "16.3.13",
|
|
66
|
+
"@oh-my-pi/pi-tui": "16.3.13",
|
|
67
|
+
"@oh-my-pi/pi-utils": "16.3.13",
|
|
68
|
+
"@oh-my-pi/pi-wire": "16.3.13",
|
|
69
|
+
"@oh-my-pi/snapcompact": "16.3.13",
|
|
70
70
|
"@opentelemetry/api": "^1.9.1",
|
|
71
71
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
72
72
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
TUI_KEYBINDINGS,
|
|
10
10
|
KeybindingsManager as TuiKeybindingsManager,
|
|
11
11
|
} from "@oh-my-pi/pi-tui";
|
|
12
|
-
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { getActiveProfile, getAgentDir, getProfileRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import { JSONC, YAML } from "bun";
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -375,6 +375,12 @@ interface KeybindingsConfigPaths {
|
|
|
375
375
|
writeBackPath: string;
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
/** Controls inherited keybinding lookup when creating a manager for a named profile. */
|
|
379
|
+
export interface KeybindingsCreateOptions {
|
|
380
|
+
/** Default-profile agent directory whose keybindings are merged before profile-specific bindings. */
|
|
381
|
+
inheritedAgentDir?: string;
|
|
382
|
+
}
|
|
383
|
+
|
|
378
384
|
/**
|
|
379
385
|
* Load raw config from a file synchronously.
|
|
380
386
|
* Returns parsed JSON/YAML or null if file doesn't exist or is invalid.
|
|
@@ -428,6 +434,48 @@ function resolveKeybindingsConfigPaths(agentDir: string): KeybindingsConfigPaths
|
|
|
428
434
|
return { readPath: ymlPath, writeBackPath: ymlPath };
|
|
429
435
|
}
|
|
430
436
|
|
|
437
|
+
function mergeKeybindingsConfig(
|
|
438
|
+
inheritedConfig: KeybindingsConfig,
|
|
439
|
+
profileConfig: KeybindingsConfig,
|
|
440
|
+
): KeybindingsConfig {
|
|
441
|
+
return { ...inheritedConfig, ...profileConfig };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveInheritedAgentDir(agentDir: string, options: KeybindingsCreateOptions): string | undefined {
|
|
445
|
+
const inheritedAgentDir =
|
|
446
|
+
options.inheritedAgentDir ?? (getActiveProfile() ? path.join(getProfileRootDir(undefined), "agent") : undefined);
|
|
447
|
+
if (!inheritedAgentDir) return undefined;
|
|
448
|
+
if (path.resolve(inheritedAgentDir) === path.resolve(agentDir)) return undefined;
|
|
449
|
+
return inheritedAgentDir;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function loadMergedKeybindingsConfig(
|
|
453
|
+
agentDir: string,
|
|
454
|
+
options: KeybindingsCreateOptions,
|
|
455
|
+
): {
|
|
456
|
+
config: KeybindingsConfig;
|
|
457
|
+
profilePath: string;
|
|
458
|
+
inheritedPath: string | undefined;
|
|
459
|
+
} {
|
|
460
|
+
const profilePaths = resolveKeybindingsConfigPaths(agentDir);
|
|
461
|
+
const profile = loadKeybindingsConfig(profilePaths.readPath, profilePaths.writeBackPath);
|
|
462
|
+
const inheritedAgentDir = resolveInheritedAgentDir(agentDir, options);
|
|
463
|
+
if (!inheritedAgentDir) {
|
|
464
|
+
return { config: profile.config, profilePath: profile.persistedPath, inheritedPath: undefined };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const inheritedPaths = resolveKeybindingsConfigPaths(inheritedAgentDir);
|
|
468
|
+
// Read-only: a named-profile process must never write migration output into
|
|
469
|
+
// the default profile's agent dir. Name migration still applies in-memory;
|
|
470
|
+
// the on-disk migration happens when the default profile itself launches.
|
|
471
|
+
const inherited = loadKeybindingsConfig(inheritedPaths.readPath, undefined);
|
|
472
|
+
return {
|
|
473
|
+
config: mergeKeybindingsConfig(inherited.config, profile.config),
|
|
474
|
+
profilePath: profile.persistedPath,
|
|
475
|
+
inheritedPath: inherited.persistedPath,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
431
479
|
/**
|
|
432
480
|
* Load and migrate keybindings config.
|
|
433
481
|
* Legacy JSON is read for compatibility, but successful write-back goes to YAML.
|
|
@@ -499,22 +547,23 @@ function keyConfigValue(keys: KeyId[]): KeyId | KeyId[] {
|
|
|
499
547
|
*/
|
|
500
548
|
export class KeybindingsManager extends TuiKeybindingsManager {
|
|
501
549
|
#configPath: string | undefined;
|
|
550
|
+
#inheritedConfigPath: string | undefined;
|
|
502
551
|
#userBindings: KeybindingsConfig;
|
|
503
552
|
|
|
504
|
-
constructor(userBindings: KeybindingsConfig = {}, configPath?: string) {
|
|
553
|
+
constructor(userBindings: KeybindingsConfig = {}, configPath?: string, inheritedConfigPath?: string) {
|
|
505
554
|
super(KEYBINDINGS, userBindings);
|
|
506
555
|
this.#configPath = configPath;
|
|
556
|
+
this.#inheritedConfigPath = inheritedConfigPath;
|
|
507
557
|
this.#userBindings = userBindings;
|
|
508
558
|
}
|
|
509
559
|
|
|
510
560
|
/**
|
|
511
|
-
* Create from config
|
|
561
|
+
* Create from config files at agentDir/keybindings.yml and the default profile.
|
|
512
562
|
* Legacy keybindings.json is migrated to keybindings.yml on load.
|
|
513
563
|
*/
|
|
514
|
-
static create(agentDir: string = getAgentDir()): KeybindingsManager {
|
|
515
|
-
const {
|
|
516
|
-
const
|
|
517
|
-
const manager = new KeybindingsManager(userBindings, persistedPath);
|
|
564
|
+
static create(agentDir: string = getAgentDir(), options: KeybindingsCreateOptions = {}): KeybindingsManager {
|
|
565
|
+
const { config: userBindings, profilePath, inheritedPath } = loadMergedKeybindingsConfig(agentDir, options);
|
|
566
|
+
const manager = new KeybindingsManager(userBindings, profilePath, inheritedPath);
|
|
518
567
|
// Set globally so getKeybindings() returns this manager
|
|
519
568
|
setKeybindings(manager);
|
|
520
569
|
return manager;
|
|
@@ -528,12 +577,15 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
528
577
|
}
|
|
529
578
|
|
|
530
579
|
/**
|
|
531
|
-
* Reload keybindings from the config
|
|
580
|
+
* Reload keybindings from the config files.
|
|
532
581
|
*/
|
|
533
582
|
reload(): void {
|
|
534
583
|
if (!this.#configPath) return;
|
|
535
|
-
const { config } =
|
|
536
|
-
|
|
584
|
+
const { config: inheritedConfig } = this.#inheritedConfigPath
|
|
585
|
+
? KeybindingsManager.#loadFromFile(this.#inheritedConfigPath)
|
|
586
|
+
: { config: {} };
|
|
587
|
+
const { config: profileConfig } = KeybindingsManager.#loadFromFile(this.#configPath);
|
|
588
|
+
this.setUserBindings(mergeKeybindingsConfig(inheritedConfig, profileConfig));
|
|
537
589
|
}
|
|
538
590
|
|
|
539
591
|
setUserBindings(userBindings: KeybindingsConfig): void {
|
|
@@ -54,6 +54,12 @@ const LOCAL_PROVIDER_PLACEHOLDERS = new Set<string>(["llama-cpp-local", "lm-stud
|
|
|
54
54
|
* so a successful fast path does not leave an armed timeout signal for concurrent GC.
|
|
55
55
|
*/
|
|
56
56
|
const RUNTIME_DYNAMIC_MODEL_FETCH_TIMEOUT_MS = 15_000;
|
|
57
|
+
// Built-in discovery preflight mirror of the catalog model-manager's private
|
|
58
|
+
// cache timings (model-manager.ts: DEFAULT_CACHE_TTL_MS / NON_AUTHORITATIVE_RETRY_MS).
|
|
59
|
+
// Built-in descriptors never override cacheTtlMs, so agreeing with these values
|
|
60
|
+
// makes the OAuth-refresh preflight fire exactly when the manager will fetch.
|
|
61
|
+
const BUILT_IN_DISCOVERY_CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
62
|
+
const BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
|
|
57
63
|
|
|
58
64
|
import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
59
65
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
@@ -1560,12 +1566,11 @@ export class ModelRegistry {
|
|
|
1560
1566
|
): Promise<BuiltInDiscoveryResult> {
|
|
1561
1567
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
1562
1568
|
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
|
|
1563
|
-
const managerOptions =
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
});
|
|
1569
|
+
const managerOptions = await this.#collectBuiltInModelManagerOptions(
|
|
1570
|
+
strategy,
|
|
1571
|
+
providerFilter,
|
|
1572
|
+
configuredDiscoveryProviders,
|
|
1573
|
+
);
|
|
1569
1574
|
if (managerOptions.length === 0) {
|
|
1570
1575
|
return { models: [], authoritativeProviders: new Set() };
|
|
1571
1576
|
}
|
|
@@ -1583,7 +1588,49 @@ export class ModelRegistry {
|
|
|
1583
1588
|
return { models, authoritativeProviders };
|
|
1584
1589
|
}
|
|
1585
1590
|
|
|
1586
|
-
async #
|
|
1591
|
+
async #resolveBuiltInDiscoveryApiKey(
|
|
1592
|
+
providerId: string,
|
|
1593
|
+
strategy: ModelRefreshStrategy,
|
|
1594
|
+
cacheProviderId: string,
|
|
1595
|
+
): Promise<string | undefined> {
|
|
1596
|
+
const peekedKey = await this.#peekApiKeyForProvider(providerId);
|
|
1597
|
+
if (isAuthenticated(peekedKey) || strategy === "offline") {
|
|
1598
|
+
return peekedKey;
|
|
1599
|
+
}
|
|
1600
|
+
const oauthCredentials = getOAuthCredentialsForProvider(this.authStorage, providerId);
|
|
1601
|
+
if (oauthCredentials.length === 0) {
|
|
1602
|
+
return peekedKey;
|
|
1603
|
+
}
|
|
1604
|
+
if (strategy === "online-if-uncached") {
|
|
1605
|
+
// Mirror shouldFetchRemoteSources: built-in managers use the catalog's
|
|
1606
|
+
// default TTL, so only refresh when the manager will actually fetch.
|
|
1607
|
+
const cache = readModelCache<Api>(
|
|
1608
|
+
cacheProviderId,
|
|
1609
|
+
BUILT_IN_DISCOVERY_CACHE_TTL_MS,
|
|
1610
|
+
Date.now,
|
|
1611
|
+
this.#cacheDbPath,
|
|
1612
|
+
);
|
|
1613
|
+
const cacheAgeMs = cache ? Date.now() - cache.updatedAt : Number.POSITIVE_INFINITY;
|
|
1614
|
+
if (cache?.fresh && (cache.authoritative || cacheAgeMs < BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS)) {
|
|
1615
|
+
return peekedKey;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
return await this.getApiKeyForProvider(providerId);
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
logger.debug("OAuth refresh failed during model discovery preflight", {
|
|
1622
|
+
provider: providerId,
|
|
1623
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1624
|
+
});
|
|
1625
|
+
return peekedKey;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
async #collectBuiltInModelManagerOptions(
|
|
1630
|
+
strategy: ModelRefreshStrategy,
|
|
1631
|
+
providerFilter: ReadonlySet<string> | undefined,
|
|
1632
|
+
configuredDiscoveryProviders: ReadonlySet<string>,
|
|
1633
|
+
): Promise<ModelManagerOptions<Api>[]> {
|
|
1587
1634
|
const specialProviderDescriptors: Array<{
|
|
1588
1635
|
providerId: string;
|
|
1589
1636
|
resolveKey: (value: string | undefined) => string | undefined;
|
|
@@ -1622,20 +1669,33 @@ export class ModelRegistry {
|
|
|
1622
1669
|
},
|
|
1623
1670
|
];
|
|
1624
1671
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
1625
|
-
const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(
|
|
1626
|
-
|
|
1672
|
+
const standardProviderDescriptors = PROVIDER_DESCRIPTORS.filter(descriptor => {
|
|
1673
|
+
if (disabledProviders.has(descriptor.providerId)) return false;
|
|
1674
|
+
if (configuredDiscoveryProviders.has(descriptor.providerId)) return false;
|
|
1675
|
+
return providerFilter ? providerFilter.has(descriptor.providerId) : true;
|
|
1676
|
+
});
|
|
1677
|
+
const enabledSpecialProviderDescriptors = specialProviderDescriptors.filter(descriptor => {
|
|
1678
|
+
if (disabledProviders.has(descriptor.providerId)) return false;
|
|
1679
|
+
if (configuredDiscoveryProviders.has(descriptor.providerId)) return false;
|
|
1680
|
+
return providerFilter ? providerFilter.has(descriptor.providerId) : true;
|
|
1681
|
+
});
|
|
1682
|
+
const standardProviderKeys = await Promise.all(
|
|
1683
|
+
standardProviderDescriptors.map(descriptor => {
|
|
1684
|
+
const discoveryBaseUrl =
|
|
1685
|
+
this.#runtimeProviderOverrides.get(descriptor.providerId)?.baseUrl ??
|
|
1686
|
+
this.#providerOverrides.get(descriptor.providerId)?.baseUrl ??
|
|
1687
|
+
this.getProviderBaseUrl(descriptor.providerId);
|
|
1688
|
+
const cacheProviderId =
|
|
1689
|
+
descriptor.createModelManagerOptions({ baseUrl: discoveryBaseUrl, fetch: this.#fetch })
|
|
1690
|
+
.cacheProviderId ?? descriptor.providerId;
|
|
1691
|
+
return this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, cacheProviderId);
|
|
1692
|
+
}),
|
|
1627
1693
|
);
|
|
1628
|
-
const
|
|
1629
|
-
descriptor =>
|
|
1694
|
+
const specialKeys = await Promise.all(
|
|
1695
|
+
enabledSpecialProviderDescriptors.map(descriptor =>
|
|
1696
|
+
this.#resolveBuiltInDiscoveryApiKey(descriptor.providerId, strategy, descriptor.providerId),
|
|
1697
|
+
),
|
|
1630
1698
|
);
|
|
1631
|
-
// Use peekApiKey to avoid OAuth token refresh during discovery.
|
|
1632
|
-
// The token is only needed if the dynamic fetch fires (cache miss),
|
|
1633
|
-
// and failures there are handled gracefully.
|
|
1634
|
-
const peekKey = (descriptor: { providerId: string }) => this.#peekApiKeyForProvider(descriptor.providerId);
|
|
1635
|
-
const [standardProviderKeys, specialKeys] = await Promise.all([
|
|
1636
|
-
Promise.all(standardProviderDescriptors.map(peekKey)),
|
|
1637
|
-
Promise.all(enabledSpecialProviderDescriptors.map(peekKey)),
|
|
1638
|
-
]);
|
|
1639
1699
|
const options: ModelManagerOptions<Api>[] = [];
|
|
1640
1700
|
for (let i = 0; i < standardProviderDescriptors.length; i++) {
|
|
1641
1701
|
const descriptor = standardProviderDescriptors[i];
|
|
@@ -1670,7 +1730,12 @@ export class ModelRegistry {
|
|
|
1670
1730
|
}
|
|
1671
1731
|
// Append runtime model managers registered by extensions via fetchDynamicModels.
|
|
1672
1732
|
for (const { options: managerOpts } of this.#runtimeModelManagers.values()) {
|
|
1673
|
-
|
|
1733
|
+
if (
|
|
1734
|
+
!configuredDiscoveryProviders.has(managerOpts.providerId) &&
|
|
1735
|
+
(!providerFilter || providerFilter.has(managerOpts.providerId))
|
|
1736
|
+
) {
|
|
1737
|
+
options.push(managerOpts);
|
|
1738
|
+
}
|
|
1674
1739
|
}
|
|
1675
1740
|
return options;
|
|
1676
1741
|
}
|