@oh-my-pi/pi-coding-agent 17.2.3 → 17.2.4
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 +41 -0
- package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-bpmhv26t.md} +41 -0
- package/dist/cli.js +3270 -3270
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/paths.d.ts +2 -1
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +9 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/index.d.ts +6 -8
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +7 -0
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +11 -5
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/collab/host.ts +1 -1
- package/src/config/keybindings.ts +14 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/launch/paths.ts +2 -5
- package/src/main.ts +24 -22
- package/src/mcp/config.ts +1 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +26 -0
- package/src/modes/components/tool-execution.ts +77 -20
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +182 -10
- package/src/modes/controllers/selector-controller.ts +27 -6
- package/src/modes/types.ts +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +68 -29
- package/src/secrets/index.ts +15 -17
- package/src/session/agent-session.ts +10 -1
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/session-advisors.ts +19 -1
- package/src/session/session-maintenance.ts +3 -0
- package/src/session/session-manager.ts +128 -76
- package/src/session/session-tools.ts +25 -23
- package/src/slash-commands/builtin-registry.ts +2 -2
- package/src/system-prompt.ts +6 -1
- package/src/task/executor.ts +8 -1
- package/src/tiny/text.ts +12 -0
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/index.ts +10 -6
- package/src/web/search/providers/anthropic.ts +3 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Registry read/write operations for the marketplace plugin system.
|
|
3
3
|
*
|
|
4
4
|
* Two registries:
|
|
5
|
-
* - marketplaces.json
|
|
5
|
+
* - marketplaces.json at getMarketplacesRegistryPath() — which catalogs the user has added
|
|
6
6
|
* - installed_plugins.json under getPluginsDir() — which plugins are installed
|
|
7
7
|
*
|
|
8
8
|
* Read/write functions accept explicit file paths so callers control the
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import * as fs from "node:fs/promises";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { getPluginsDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
19
|
+
|
|
20
|
+
export { getMarketplacesRegistryPath } from "@oh-my-pi/pi-utils";
|
|
19
21
|
|
|
20
22
|
import type {
|
|
21
23
|
InstalledPluginEntry,
|
|
@@ -24,12 +26,6 @@ import type {
|
|
|
24
26
|
MarketplacesRegistry,
|
|
25
27
|
} from "./types";
|
|
26
28
|
|
|
27
|
-
// ── Path helpers ─────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export function getMarketplacesRegistryPath(): string {
|
|
30
|
-
return path.join(getConfigRootDir(), "marketplaces.json");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
export function getInstalledPluginsRegistryPath(): string {
|
|
34
30
|
return path.join(getPluginsDir(), "installed_plugins.json");
|
|
35
31
|
}
|
package/src/launch/paths.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { getDaemonRuntimeDir } from "@oh-my-pi/pi-utils";
|
|
3
3
|
|
|
4
4
|
/** Resolve the private runtime directory shared by omp processes in one project directory. */
|
|
5
|
-
export
|
|
6
|
-
const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0");
|
|
7
|
-
return path.join(configRoot, "run", "daemons", key);
|
|
8
|
-
}
|
|
5
|
+
export { getDaemonRuntimeDir as daemonRuntimeDir };
|
|
9
6
|
|
|
10
7
|
/** Resolve the Unix socket or Windows named pipe used by one project broker. */
|
|
11
8
|
export function daemonBrokerEndpoint(projectDir: string, runtimeDir: string): string {
|
package/src/main.ts
CHANGED
|
@@ -1110,14 +1110,13 @@ export async function buildSessionOptions(
|
|
|
1110
1110
|
}
|
|
1111
1111
|
|
|
1112
1112
|
// Additional extension paths from CLI
|
|
1113
|
-
const cliExtensionPaths =
|
|
1113
|
+
const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
|
|
1114
1114
|
if (cliExtensionPaths.length > 0) {
|
|
1115
1115
|
options.additionalExtensionPaths = cliExtensionPaths;
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
1118
|
if (parsed.noExtensions) {
|
|
1119
1119
|
options.disableExtensionDiscovery = true;
|
|
1120
|
-
options.additionalExtensionPaths = [];
|
|
1121
1120
|
}
|
|
1122
1121
|
|
|
1123
1122
|
return options;
|
|
@@ -1151,10 +1150,6 @@ export async function runRootCommand(
|
|
|
1151
1150
|
|
|
1152
1151
|
const notifs: (InteractiveModeNotify | null)[] = [];
|
|
1153
1152
|
|
|
1154
|
-
// Create AuthStorage and ModelRegistry upfront
|
|
1155
|
-
const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
|
|
1156
|
-
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
1157
|
-
|
|
1158
1153
|
if (parsedArgs.version) {
|
|
1159
1154
|
writeStartupNotice(parsedArgs, `${VERSION}\n`);
|
|
1160
1155
|
process.exit(0);
|
|
@@ -1197,15 +1192,31 @@ export async function runRootCommand(
|
|
|
1197
1192
|
// Register CLI-provided extension package paths (`--extension`, `--hook`) so
|
|
1198
1193
|
// the `omp-plugins` discovery provider can surface their `skills/`, `hooks/`,
|
|
1199
1194
|
// `tools/`, `commands/`, `rules/`, `prompts/`, and `.mcp.json` sub-trees.
|
|
1200
|
-
//
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
}
|
|
1195
|
+
// Explicit roots remain authorized under `--no-extensions`; only ambient
|
|
1196
|
+
// extension discovery is disabled.
|
|
1197
|
+
const cliExtensions = [...(parsedArgs.extensions ?? []), ...(parsedArgs.hooks ?? [])];
|
|
1198
|
+
injectOmpExtensionCliRoots(cliExtensions, home, getProjectDir(), {
|
|
1199
|
+
mode: parsedArgs.noExtensions ? "explicit-only" : "merge",
|
|
1200
|
+
replace: true,
|
|
1201
|
+
});
|
|
1207
1202
|
|
|
1208
1203
|
let cwd = getProjectDir();
|
|
1204
|
+
// Classify the host before opening auth or settings storage so every
|
|
1205
|
+
// session-critical database connection picks the right busy timeout.
|
|
1206
|
+
// See getDbBusyTimeoutMs().
|
|
1207
|
+
const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
|
|
1208
|
+
// Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
|
|
1209
|
+
const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
|
|
1210
|
+
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
1211
|
+
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
1212
|
+
// Only the interactive host renders a focusable Agent Hub / subagent session
|
|
1213
|
+
// tree; declare it so headless subagent optimizations (e.g. skipping replan
|
|
1214
|
+
// title refresh) can tell a focusable process from a print/RPC/eval one.
|
|
1215
|
+
setInteractiveHost(isInteractive);
|
|
1216
|
+
// Create AuthStorage and ModelRegistry upfront
|
|
1217
|
+
const authStorage = await logger.time("discoverAuthStorage", deps.discoverAuthStorage ?? discoverAuthStorage);
|
|
1218
|
+
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
1219
|
+
|
|
1209
1220
|
const settingsInstance =
|
|
1210
1221
|
deps.settings ?? (await logger.time("settings:init", Settings.init, { cwd, configFiles: parsedArgs.config }));
|
|
1211
1222
|
if (parsedArgs.approvalMode) {
|
|
@@ -1228,15 +1239,6 @@ export async function runRootCommand(
|
|
|
1228
1239
|
if (parsedArgs.noTitle || parsedArgs.mode === "rpc" || parsedArgs.mode === "rpc-ui" || parsedArgs.mode === "acp") {
|
|
1229
1240
|
Bun.env.PI_NO_TITLE = "1";
|
|
1230
1241
|
}
|
|
1231
|
-
const isProtocolMode = mode === "rpc" || mode === "rpc-ui" || mode === "acp";
|
|
1232
|
-
// Protocol modes own stdin; treating it as prompt text would consume JSON-RPC frames before their transports start.
|
|
1233
|
-
const pipedInput = isProtocolMode ? undefined : await logger.time("readPipedInput", readPipedInput);
|
|
1234
|
-
const autoPrint = pipedInput !== undefined && !parsedArgs.print && parsedArgs.mode === undefined;
|
|
1235
|
-
const isInteractive = !parsedArgs.print && !autoPrint && parsedArgs.mode === undefined;
|
|
1236
|
-
// Only the interactive host renders a focusable Agent Hub / subagent session
|
|
1237
|
-
// tree; declare it so headless subagent optimizations (e.g. skipping replan
|
|
1238
|
-
// title refresh) can tell a focusable process from a print/RPC/eval one.
|
|
1239
|
-
setInteractiveHost(isInteractive);
|
|
1240
1242
|
|
|
1241
1243
|
// Initialize discovery system with settings for provider persistence
|
|
1242
1244
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
package/src/mcp/config.ts
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import type { MCPRequestIdFormat } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-transport allocator for outgoing JSON-RPC request ids.
|
|
6
|
+
*
|
|
7
|
+
* JSON-RPC 2.0 permits String and Number ids equally, but the wider MCP
|
|
8
|
+
* ecosystem (reference SDKs included) issues incrementing integers, so servers
|
|
9
|
+
* are best tested against numbers — Apple's `xcrun mcpbridge` even logs
|
|
10
|
+
* `mcpbridge.DecodeError Code=1` and never answers a string id. Integers are
|
|
11
|
+
* therefore the default. `"string"` opts a server back into collision-resistant
|
|
12
|
+
* snowflake ids.
|
|
13
|
+
*
|
|
14
|
+
* The counter is per instance and never reset, so ids stay unique for the life
|
|
15
|
+
* of the transport even though a reconnect rejects and clears every pending
|
|
16
|
+
* request.
|
|
17
|
+
*/
|
|
18
|
+
export class RequestIdAllocator {
|
|
19
|
+
#previousNumeric = 0;
|
|
20
|
+
|
|
21
|
+
next(format: MCPRequestIdFormat | undefined): string | number {
|
|
22
|
+
return format === "string" ? Snowflake.next() : ++this.#previousNumeric;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Based on MCP spec 2025-03-26.
|
|
6
6
|
*/
|
|
7
7
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
8
|
-
import { logger, readSseJson
|
|
8
|
+
import { logger, readSseJson } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import type {
|
|
10
10
|
JsonRpcError,
|
|
11
11
|
JsonRpcMessage,
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
MCPTransport,
|
|
18
18
|
} from "../../mcp/types";
|
|
19
19
|
import { toJsonRpcError } from "../../mcp/types";
|
|
20
|
+
import { RequestIdAllocator } from "../request-id";
|
|
20
21
|
import { createMCPTimeout, getNeverAbortSignal, isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
21
22
|
|
|
22
23
|
const HTTP_SSE_CONNECT_TIMEOUT_MS = 1_000;
|
|
@@ -42,6 +43,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
42
43
|
#connected = false;
|
|
43
44
|
#sessionId: string | null = null;
|
|
44
45
|
#sseConnection: AbortController | null = null;
|
|
46
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
45
47
|
|
|
46
48
|
onClose?: () => void;
|
|
47
49
|
onError?: (error: Error) => void;
|
|
@@ -209,7 +211,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
209
211
|
throw new Error("Transport not connected");
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
const id =
|
|
214
|
+
const id = this.#requestIds.next(this.config.requestIdFormat);
|
|
213
215
|
const body = {
|
|
214
216
|
jsonrpc: "2.0" as const,
|
|
215
217
|
id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
2
|
-
import { logger, readSseEvents
|
|
2
|
+
import { logger, readSseEvents } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type {
|
|
4
4
|
JsonRpcError,
|
|
5
5
|
JsonRpcMessage,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
MCPTransport,
|
|
11
11
|
} from "../../mcp/types";
|
|
12
12
|
import { toJsonRpcError } from "../../mcp/types";
|
|
13
|
+
import { RequestIdAllocator } from "../request-id";
|
|
13
14
|
import { createMCPTimeout, getNeverAbortSignal, resolveMCPTimeoutMs } from "../timeout";
|
|
14
15
|
|
|
15
16
|
interface MCPTimeoutOperation {
|
|
@@ -32,6 +33,7 @@ export class LegacySseTransport implements MCPTransport {
|
|
|
32
33
|
#sseConnection: AbortController | null = null;
|
|
33
34
|
#pending = new Map<string | number, PendingLegacySseRequest>();
|
|
34
35
|
#config: MCPSseServerConfig;
|
|
36
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
35
37
|
|
|
36
38
|
onClose?: () => void;
|
|
37
39
|
onError?: (error: Error) => void;
|
|
@@ -194,7 +196,7 @@ export class LegacySseTransport implements MCPTransport {
|
|
|
194
196
|
throw new Error("Transport not connected");
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
const id =
|
|
199
|
+
const id = this.#requestIds.next(this.#config.requestIdFormat);
|
|
198
200
|
const body = {
|
|
199
201
|
jsonrpc: "2.0" as const,
|
|
200
202
|
id,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import { getProjectDir, readJsonl
|
|
10
|
+
import { getProjectDir, readJsonl } from "@oh-my-pi/pi-utils";
|
|
11
11
|
import type { Subprocess } from "bun";
|
|
12
12
|
import { hostHasInheritableConsole } from "../../eval/py/spawn-options";
|
|
13
13
|
import type {
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
MCPTransport,
|
|
21
21
|
} from "../../mcp/types";
|
|
22
22
|
import { toJsonRpcError } from "../../mcp/types";
|
|
23
|
+
import { RequestIdAllocator } from "../request-id";
|
|
23
24
|
import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
24
25
|
|
|
25
26
|
/** Subprocess argv and platform-derived spawn flags for an MCP stdio server. */
|
|
@@ -551,6 +552,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
551
552
|
* actually spawned into its own session may target it.
|
|
552
553
|
*/
|
|
553
554
|
#detached = false;
|
|
555
|
+
readonly #requestIds = new RequestIdAllocator();
|
|
554
556
|
|
|
555
557
|
onClose?: () => void;
|
|
556
558
|
onError?: (error: Error) => void;
|
|
@@ -734,7 +736,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
734
736
|
throw new Error("Transport not connected");
|
|
735
737
|
}
|
|
736
738
|
|
|
737
|
-
const id =
|
|
739
|
+
const id = this.#requestIds.next(this.config.requestIdFormat);
|
|
738
740
|
const request = {
|
|
739
741
|
jsonrpc: "2.0" as const,
|
|
740
742
|
id,
|
package/src/mcp/types.ts
CHANGED
|
@@ -59,12 +59,27 @@ export interface MCPAuthConfig {
|
|
|
59
59
|
resource?: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Encoding used for outgoing JSON-RPC request ids. */
|
|
63
|
+
export type MCPRequestIdFormat = "string" | "number";
|
|
64
|
+
|
|
62
65
|
/** Base server config with shared options */
|
|
63
66
|
interface MCPServerConfigBase {
|
|
64
67
|
/** Whether this server is enabled (default: true) */
|
|
65
68
|
enabled?: boolean;
|
|
66
69
|
/** MCP request timeout in milliseconds (default: 30000, 0 to disable) */
|
|
67
70
|
timeout?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Encoding for outgoing JSON-RPC request ids (default: `"number"`).
|
|
73
|
+
*
|
|
74
|
+
* Set `"string"` for servers that need collision-resistant snowflake string
|
|
75
|
+
* ids instead of per-transport integers. See `RequestIdAllocator` in
|
|
76
|
+
* `./request-id`.
|
|
77
|
+
*
|
|
78
|
+
* OMP-specific, so only the OMP-owned discovery providers parse it (native,
|
|
79
|
+
* standalone `mcp.json`, OMP plugins). Providers that translate another
|
|
80
|
+
* tool's config do not, since the key is not part of those formats.
|
|
81
|
+
*/
|
|
82
|
+
requestIdFormat?: MCPRequestIdFormat;
|
|
68
83
|
/** Authentication configuration (optional) */
|
|
69
84
|
auth?: MCPAuthConfig;
|
|
70
85
|
/** OAuth configuration for servers requiring explicit client credentials */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MemoryBackendId } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fallback text for `/memory stats` and `/memory diagnose` when the active
|
|
5
|
+
* backend's hook is missing or returns nothing.
|
|
6
|
+
*
|
|
7
|
+
* The `off` backend isn't a real backend a user picked among several
|
|
8
|
+
* stats-capable options — it's the no-op state memory falls back to by
|
|
9
|
+
* default — so it gets its own message instead of the generic
|
|
10
|
+
* "not available for the X backend" template, which would otherwise read as
|
|
11
|
+
* the self-contradictory "not available for the off backend".
|
|
12
|
+
*
|
|
13
|
+
* Shared by both the TUI (`CommandController.handleMemoryCommand`) and the
|
|
14
|
+
* ACP/RPC slash-command handler so the two surfaces stay consistent.
|
|
15
|
+
*/
|
|
16
|
+
export function memoryStatsUnavailableMessage(backendId: MemoryBackendId, action: "stats" | "diagnose"): string {
|
|
17
|
+
if (backendId === "off") return "Memory backend is off — there is nothing to show.";
|
|
18
|
+
return `Memory ${action} is not available for the ${backendId} backend.`;
|
|
19
|
+
}
|
|
@@ -636,7 +636,7 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
636
636
|
if (ref.status === "running" && ref.session) {
|
|
637
637
|
await ref.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
638
638
|
}
|
|
639
|
-
await this.#lifecycle().release(ref.id, ref);
|
|
639
|
+
await this.#lifecycle().release(ref.id, ref, { tombstone: true });
|
|
640
640
|
} catch (error) {
|
|
641
641
|
logger.warn("Agent hub: kill failed", { id: ref.id, error: String(error) });
|
|
642
642
|
this.#notice = error instanceof Error ? error.message : String(error);
|
|
@@ -56,7 +56,7 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
56
56
|
"app.thinking.toggle": ["ctrl+t"],
|
|
57
57
|
"app.editor.external": ["ctrl+g"],
|
|
58
58
|
"app.history.search": ["ctrl+r"],
|
|
59
|
-
"app.message.dequeue": ["alt+up"],
|
|
59
|
+
"app.message.dequeue": ["alt+up", "shift+up"],
|
|
60
60
|
"app.retry": ["alt+r"],
|
|
61
61
|
"app.clipboard.pasteImage": ["ctrl+v"],
|
|
62
62
|
"app.clipboard.pasteTextRaw": ["ctrl+shift+v", "alt+shift+v"],
|
|
@@ -314,10 +314,10 @@ function padLeftVisible(text: string, width: number): string {
|
|
|
314
314
|
export interface ModelBrowserOptions {
|
|
315
315
|
/** Render the dim `provider/` prefix before model ids. Default true. */
|
|
316
316
|
showProvider?: boolean;
|
|
317
|
-
/** Session token count used to
|
|
317
|
+
/** Session token count used to flag models whose context window is exceeded. */
|
|
318
318
|
currentContextTokens?: number;
|
|
319
|
-
/** When true, rows
|
|
320
|
-
|
|
319
|
+
/** When true, over-context rows render grayed; picking one compacts first (session-switch mode). */
|
|
320
|
+
markOverContext?: boolean;
|
|
321
321
|
/** Host-provided empty-state text (e.g. provider discovery status). */
|
|
322
322
|
emptyText?: () => string | undefined;
|
|
323
323
|
}
|
|
@@ -351,7 +351,7 @@ export class ModelBrowser implements Component {
|
|
|
351
351
|
#maxVisible = 10;
|
|
352
352
|
#showProvider: boolean;
|
|
353
353
|
#currentContextTokens: number;
|
|
354
|
-
#
|
|
354
|
+
#markOverContext: boolean;
|
|
355
355
|
#emptyText?: () => string | undefined;
|
|
356
356
|
/** Keep role-like virtual rows in their host-defined order during search. */
|
|
357
357
|
#preserveQueryOrder = false;
|
|
@@ -375,7 +375,7 @@ export class ModelBrowser implements Component {
|
|
|
375
375
|
this.#showProvider = options.showProvider ?? true;
|
|
376
376
|
const tokens = options.currentContextTokens ?? 0;
|
|
377
377
|
this.#currentContextTokens = Number.isFinite(tokens) && tokens > 0 ? Math.floor(tokens) : 0;
|
|
378
|
-
this.#
|
|
378
|
+
this.#markOverContext = options.markOverContext ?? false;
|
|
379
379
|
this.#emptyText = options.emptyText;
|
|
380
380
|
}
|
|
381
381
|
|
|
@@ -420,9 +420,9 @@ export class ModelBrowser implements Component {
|
|
|
420
420
|
setPreserveQueryOrder(preserve: boolean): void {
|
|
421
421
|
this.#preserveQueryOrder = preserve;
|
|
422
422
|
}
|
|
423
|
-
/** Allow hosts to toggle context-window
|
|
424
|
-
|
|
425
|
-
this.#
|
|
423
|
+
/** Allow hosts to toggle context-window flagging between browser modes. */
|
|
424
|
+
setMarkOverContext(mark: boolean): void {
|
|
425
|
+
this.#markOverContext = mark;
|
|
426
426
|
}
|
|
427
427
|
/** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
|
|
428
428
|
setFocused(focused: boolean): void {
|
|
@@ -461,8 +461,13 @@ export class ModelBrowser implements Component {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
#isDisabled(item: ModelBrowserItem): boolean {
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
return item.id === "separator";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** True when `item`'s context window is smaller than the live session token count (grayed row; hosts compact before switching). */
|
|
468
|
+
isOverContext(item: ModelBrowserItem): boolean {
|
|
469
|
+
if (item.id === "separator") return false;
|
|
470
|
+
if (!this.#markOverContext || this.#currentContextTokens <= 0) return false;
|
|
466
471
|
const contextWindow = item.model.contextWindow ?? 0;
|
|
467
472
|
return contextWindow > 0 && this.#currentContextTokens > contextWindow;
|
|
468
473
|
}
|
|
@@ -718,7 +723,7 @@ export class ModelBrowser implements Component {
|
|
|
718
723
|
const line = theme.fg("muted", "─".repeat(dashCount));
|
|
719
724
|
return ` ${line} `;
|
|
720
725
|
}
|
|
721
|
-
const
|
|
726
|
+
const overContext = this.isOverContext(item);
|
|
722
727
|
const prefix = selected && this.#focused ? `${theme.fg("accent", theme.nav.cursor)} ` : " ";
|
|
723
728
|
const providerPrefix = this.#showProvider ? theme.fg("dim", `${item.provider}/`) : "";
|
|
724
729
|
const name = item.labelColor
|
|
@@ -728,7 +733,7 @@ export class ModelBrowser implements Component {
|
|
|
728
733
|
: item.id;
|
|
729
734
|
const currentMark =
|
|
730
735
|
item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
|
|
731
|
-
const overLimit =
|
|
736
|
+
const overLimit = overContext
|
|
732
737
|
? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
|
|
733
738
|
: "";
|
|
734
739
|
let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
|
|
@@ -743,12 +748,15 @@ export class ModelBrowser implements Component {
|
|
|
743
748
|
const gap = Math.max(0, available - visibleWidth(left));
|
|
744
749
|
|
|
745
750
|
let line = `${left}${" ".repeat(gap)} ${meta}`;
|
|
746
|
-
if (
|
|
747
|
-
|
|
751
|
+
if (overContext) {
|
|
752
|
+
// Gray the whole row but keep the selection cursor visible: over-context
|
|
753
|
+
// models stay selectable (the host compacts before switching).
|
|
754
|
+
const plainPrefix = Bun.stripANSI(prefix);
|
|
755
|
+
line = `${prefix}${theme.fg("dim", Bun.stripANSI(line).slice(plainPrefix.length))}`;
|
|
748
756
|
}
|
|
749
757
|
// The bg band is reserved for the mouse: it marks hover, nothing else.
|
|
750
758
|
// Keyboard selection is the cursor glyph + accent name.
|
|
751
|
-
if (hovered
|
|
759
|
+
if (hovered) {
|
|
752
760
|
line = theme.bg("selectedBg", line);
|
|
753
761
|
}
|
|
754
762
|
return line;
|
|
@@ -772,8 +780,8 @@ export class ModelBrowser implements Component {
|
|
|
772
780
|
}
|
|
773
781
|
const line1 = truncateToWidth(theme.fg("muted", ` ${facts.join(" · ")}`), width);
|
|
774
782
|
|
|
775
|
-
if (this
|
|
776
|
-
const warning = ` ${theme.status.disabled}
|
|
783
|
+
if (this.isOverContext(selected)) {
|
|
784
|
+
const warning = ` ${theme.status.disabled} context ${formatNumber(this.#currentContextTokens).toLowerCase()} exceeds ${formatNumber(model.contextWindow ?? 0).toLowerCase()} limit · compacts with current model, then switches`;
|
|
777
785
|
return [line1, truncateToWidth(theme.fg("warning", warning), width)];
|
|
778
786
|
}
|
|
779
787
|
|
|
@@ -22,8 +22,12 @@ import { bottomBorder, row, topBorder } from "./overlay-box";
|
|
|
22
22
|
import { resolveSegmentPalette } from "./segment-track";
|
|
23
23
|
|
|
24
24
|
export interface ModelPickerCallbacks {
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A model was chosen for a session-only switch. `selector` is `provider/id`.
|
|
27
|
+
* `overContext` is true when the session transcript exceeds the model's
|
|
28
|
+
* context window — the host must compact before switching.
|
|
29
|
+
*/
|
|
30
|
+
onPick: (model: Model, selector: string, meta: { overContext: boolean }) => void;
|
|
27
31
|
/** A configured ctrl+p quick role was chosen. */
|
|
28
32
|
onPickRole?: (entry: ResolvedRoleModel) => void;
|
|
29
33
|
/** The picker was dismissed. */
|
|
@@ -31,7 +35,7 @@ export interface ModelPickerCallbacks {
|
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ModelPickerOptions {
|
|
34
|
-
/** Session token count; models with smaller context windows are
|
|
38
|
+
/** Session token count; models with smaller context windows are grayed and compact-first on pick. */
|
|
35
39
|
currentContextTokens?: number;
|
|
36
40
|
/** `provider/id` of the session's active model; highlighted and preselected. */
|
|
37
41
|
currentSelector?: string;
|
|
@@ -97,7 +101,7 @@ export class ModelPickerComponent implements Component {
|
|
|
97
101
|
|
|
98
102
|
this.#browser = new ModelBrowser(settings, {
|
|
99
103
|
currentContextTokens: options.currentContextTokens,
|
|
100
|
-
|
|
104
|
+
markOverContext: true,
|
|
101
105
|
emptyText: () => (this.#roleMode ? " No quick roles in the Ctrl+P cycle" : undefined),
|
|
102
106
|
});
|
|
103
107
|
this.#browser.onActivate = item => {
|
|
@@ -106,7 +110,7 @@ export class ModelPickerComponent implements Component {
|
|
|
106
110
|
callbacks.onPickRole?.(quickRole);
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
|
-
callbacks.onPick(item.model, item.selector);
|
|
113
|
+
callbacks.onPick(item.model, item.selector, { overContext: this.#browser.isOverContext(item) });
|
|
110
114
|
};
|
|
111
115
|
this.#browser.onCancel = () => callbacks.onCancel();
|
|
112
116
|
this.#browser.onQueryChange = query => this.#syncItemsForQuery(query);
|
|
@@ -193,7 +197,7 @@ export class ModelPickerComponent implements Component {
|
|
|
193
197
|
|
|
194
198
|
this.#roleMode = roleMode;
|
|
195
199
|
this.#browser.setShowProvider(!roleMode);
|
|
196
|
-
this.#browser.
|
|
200
|
+
this.#browser.setMarkOverContext(!roleMode);
|
|
197
201
|
this.#browser.setPreserveQueryOrder(roleMode);
|
|
198
202
|
const currentSelector = roleMode ? this.#currentQuickRoleSelector : this.#currentSelector;
|
|
199
203
|
this.#browser.setCurrentSelector(currentSelector);
|
|
@@ -397,6 +397,32 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
397
397
|
this.#updateDisplay();
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Re-key an entry whose streamed tool-call id changed mid-stream (a provider
|
|
402
|
+
* rewriting the id across deltas; see EventController's
|
|
403
|
+
* `#streamedToolCallIdByIndex`). Preserves row order so a sibling read run is
|
|
404
|
+
* not visibly reshuffled, and no-ops when the rename would collide.
|
|
405
|
+
*/
|
|
406
|
+
renameEntry(oldId: string, newId: string): void {
|
|
407
|
+
if (oldId === newId || !newId) return;
|
|
408
|
+
const entry = this.#entries.get(oldId);
|
|
409
|
+
if (!entry || this.#entries.has(newId)) return;
|
|
410
|
+
entry.toolCallId = newId;
|
|
411
|
+
const reordered = [...this.#entries].map(([key, value]): [string, ReadEntry] => [
|
|
412
|
+
key === oldId ? newId : key,
|
|
413
|
+
value,
|
|
414
|
+
]);
|
|
415
|
+
this.#entries.clear();
|
|
416
|
+
for (const [key, value] of reordered) this.#entries.set(key, value);
|
|
417
|
+
this.#updateDisplay();
|
|
418
|
+
}
|
|
419
|
+
/** Remove one call without discarding successful siblings in the shared group. */
|
|
420
|
+
removeEntry(toolCallId: string): boolean {
|
|
421
|
+
if (!this.#entries.delete(toolCallId)) return this.#entries.size === 0;
|
|
422
|
+
this.#updateDisplay();
|
|
423
|
+
return this.#entries.size === 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
400
426
|
updateResult(
|
|
401
427
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
402
428
|
isPartial = false,
|