@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2
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 +172 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +14 -1
- package/src/config/model-registry.ts +18 -7
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +12 -11
- package/src/main.ts +92 -24
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +209 -54
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +6 -0
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { SessionSelectorComponent } from "../modes/components/session-selector";
|
|
3
|
-
import
|
|
4
|
+
import { HistoryStorage } from "../session/history-storage";
|
|
5
|
+
import { type SessionInfo, SessionManager } from "../session/session-manager";
|
|
4
6
|
import { FileSessionStorage } from "../session/session-storage";
|
|
5
7
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Show the TUI session selector and return the selected session, or null if
|
|
10
|
+
* cancelled. Tab toggles between current-folder and all-projects scope; the
|
|
11
|
+
* all-projects list is loaded lazily via `SessionManager.listAll`.
|
|
12
|
+
*/
|
|
13
|
+
export async function selectSession(
|
|
14
|
+
sessions: SessionInfo[],
|
|
15
|
+
options?: { allSessions?: SessionInfo[]; startInAllScope?: boolean },
|
|
16
|
+
): Promise<SessionInfo | null> {
|
|
17
|
+
const { promise, resolve } = Promise.withResolvers<SessionInfo | null>();
|
|
9
18
|
const ui = new TUI(new ProcessTerminal());
|
|
10
19
|
let resolved = false;
|
|
11
20
|
const storage = new FileSessionStorage();
|
|
12
21
|
|
|
22
|
+
// Rank sessions with prompt-history matches too, recovering prompts the 4KB
|
|
23
|
+
// session-list prefix never sees. Best-effort: a missing/locked history.db
|
|
24
|
+
// must not break the picker.
|
|
25
|
+
let historyMatcher: ((query: string) => string[]) | undefined;
|
|
26
|
+
try {
|
|
27
|
+
const history = HistoryStorage.open();
|
|
28
|
+
historyMatcher = (query: string) => history.matchingSessionIds(query);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.warn("History storage unavailable for session ranking", { error: String(error) });
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
const showSelector = () => {
|
|
14
34
|
const selector = new SessionSelectorComponent(
|
|
15
35
|
sessions,
|
|
16
|
-
(
|
|
36
|
+
(session: SessionInfo) => {
|
|
17
37
|
if (!resolved) {
|
|
18
38
|
resolved = true;
|
|
19
39
|
ui.stop();
|
|
20
|
-
resolve(
|
|
40
|
+
resolve(session);
|
|
21
41
|
}
|
|
22
42
|
},
|
|
23
43
|
() => {
|
|
@@ -34,10 +54,16 @@ export async function selectSession(sessions: SessionInfo[]): Promise<string | n
|
|
|
34
54
|
process.exit(0);
|
|
35
55
|
}
|
|
36
56
|
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
{
|
|
58
|
+
onDelete: async (session: SessionInfo) => {
|
|
59
|
+
// Delete handler - SessionList will show confirmation internally
|
|
60
|
+
await storage.deleteSessionWithArtifacts(session.path);
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
historyMatcher,
|
|
64
|
+
loadAllSessions: () => SessionManager.listAll(storage),
|
|
65
|
+
allSessions: options?.allSessions,
|
|
66
|
+
startInAllScope: options?.startInAllScope,
|
|
41
67
|
},
|
|
42
68
|
);
|
|
43
69
|
return selector;
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -14,6 +14,18 @@ import { theme } from "../modes/theme/theme";
|
|
|
14
14
|
|
|
15
15
|
const REPO = "can1357/oh-my-pi";
|
|
16
16
|
const PACKAGE = "@oh-my-pi/pi-coding-agent";
|
|
17
|
+
/**
|
|
18
|
+
* Official npm registry origin.
|
|
19
|
+
*
|
|
20
|
+
* Pinned across both the version check and the bun install step so the two
|
|
21
|
+
* agree on which catalog they are talking to. A user's bun may be pointed at
|
|
22
|
+
* an unofficial mirror (corporate proxy, Taobao, etc.) that lags the upstream
|
|
23
|
+
* registry by minutes-to-hours, in which case `getLatestRelease` would resolve
|
|
24
|
+
* a version the mirror has not yet replicated and the install would fail with
|
|
25
|
+
* `No version matching "X" found for specifier "<pkg>" (but package exists)`.
|
|
26
|
+
* See #1686.
|
|
27
|
+
*/
|
|
28
|
+
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
17
29
|
|
|
18
30
|
interface ReleaseInfo {
|
|
19
31
|
tag: string;
|
|
@@ -130,7 +142,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
130
142
|
* Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
|
|
131
143
|
*/
|
|
132
144
|
async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
133
|
-
const response = await fetch(
|
|
145
|
+
const response = await fetch(`${NPM_REGISTRY}${PACKAGE}/latest`);
|
|
134
146
|
if (!response.ok) {
|
|
135
147
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
|
136
148
|
}
|
|
@@ -292,12 +304,33 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
292
304
|
}
|
|
293
305
|
}
|
|
294
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Build the bun argv used to globally install a specific omp version.
|
|
309
|
+
*
|
|
310
|
+
* The version is selected by hitting {@link NPM_REGISTRY} directly in
|
|
311
|
+
* {@link getLatestRelease}, so the install MUST observe the same catalog:
|
|
312
|
+
*
|
|
313
|
+
* - `--registry=${NPM_REGISTRY}` pins the install to the official registry
|
|
314
|
+
* regardless of the user's bunfig/`.npmrc`. A mirror (corporate proxy,
|
|
315
|
+
* Taobao, …) that hasn't yet replicated the release would otherwise reject
|
|
316
|
+
* a version the upstream registry already advertises.
|
|
317
|
+
* - `--no-cache` tells bun to ignore its on-disk manifest snapshot so it
|
|
318
|
+
* re-fetches metadata from that registry on every invocation.
|
|
319
|
+
*
|
|
320
|
+
* Together these two flags make `omp update` produce exactly the registry
|
|
321
|
+
* lookup the version check just performed. See #1686.
|
|
322
|
+
*/
|
|
323
|
+
export function buildBunInstallArgs(expectedVersion: string): string[] {
|
|
324
|
+
return ["install", "-g", "--no-cache", `--registry=${NPM_REGISTRY}`, `${PACKAGE}@${expectedVersion}`];
|
|
325
|
+
}
|
|
326
|
+
|
|
295
327
|
/**
|
|
296
328
|
* Update via bun package manager.
|
|
297
329
|
*/
|
|
298
330
|
async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
299
331
|
console.log(chalk.dim("Updating via bun..."));
|
|
300
|
-
const
|
|
332
|
+
const args = buildBunInstallArgs(expectedVersion);
|
|
333
|
+
const result = await $`bun ${args}`.nothrow();
|
|
301
334
|
if (result.exitCode !== 0) {
|
|
302
335
|
throw new Error(`bun install failed with exit code ${result.exitCode}`);
|
|
303
336
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -90,6 +90,9 @@ export default class Index extends Command {
|
|
|
90
90
|
description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
|
|
91
91
|
options: [...THINKING_EFFORTS],
|
|
92
92
|
}),
|
|
93
|
+
"hide-thinking": Flags.boolean({
|
|
94
|
+
description: "Hide thinking blocks in TUI output (display only, does not disable model thinking)",
|
|
95
|
+
}),
|
|
93
96
|
hook: Flags.string({
|
|
94
97
|
description: "Load a hook/extension file (can be used multiple times)",
|
|
95
98
|
multiple: true,
|
|
@@ -32,6 +32,7 @@ interface AppKeybindings {
|
|
|
32
32
|
"app.message.followUp": true;
|
|
33
33
|
"app.message.dequeue": true;
|
|
34
34
|
"app.clipboard.pasteImage": true;
|
|
35
|
+
"app.clipboard.pasteTextRaw": true;
|
|
35
36
|
"app.clipboard.copyLine": true;
|
|
36
37
|
"app.clipboard.copyPrompt": true;
|
|
37
38
|
"app.session.new": true;
|
|
@@ -57,6 +58,13 @@ declare module "@oh-my-pi/pi-tui" {
|
|
|
57
58
|
interface Keybindings extends AppKeybindings {}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Resolve default image-paste shortcuts for the current terminal platform.
|
|
63
|
+
*/
|
|
64
|
+
export function getDefaultPasteImageKeys(platform: NodeJS.Platform = process.platform): KeyId[] {
|
|
65
|
+
return platform === "win32" ? ["ctrl+v", "alt+v"] : ["ctrl+v"];
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
69
|
* All keybindings definitions: TUI + app-specific.
|
|
62
70
|
*/
|
|
@@ -119,9 +127,13 @@ export const KEYBINDINGS = {
|
|
|
119
127
|
description: "Dequeue message",
|
|
120
128
|
},
|
|
121
129
|
"app.clipboard.pasteImage": {
|
|
122
|
-
defaultKeys:
|
|
130
|
+
defaultKeys: getDefaultPasteImageKeys(),
|
|
123
131
|
description: "Paste image from clipboard",
|
|
124
132
|
},
|
|
133
|
+
"app.clipboard.pasteTextRaw": {
|
|
134
|
+
defaultKeys: ["ctrl+shift+v", "alt+shift+v"],
|
|
135
|
+
description: "Paste text from clipboard as raw text (no collapse)",
|
|
136
|
+
},
|
|
125
137
|
"app.clipboard.copyLine": {
|
|
126
138
|
defaultKeys: "alt+shift+l",
|
|
127
139
|
description: "Copy current line",
|
|
@@ -214,6 +226,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
|
|
|
214
226
|
followUp: "app.message.followUp",
|
|
215
227
|
dequeue: "app.message.dequeue",
|
|
216
228
|
pasteImage: "app.clipboard.pasteImage",
|
|
229
|
+
pasteTextRaw: "app.clipboard.pasteTextRaw",
|
|
217
230
|
copyLine: "app.clipboard.copyLine",
|
|
218
231
|
copyPrompt: "app.clipboard.copyPrompt",
|
|
219
232
|
newSession: "app.session.new",
|
|
@@ -53,6 +53,17 @@ function discoveryDefaultMaxTokens(api: Api | undefined): number {
|
|
|
53
53
|
return api === "anthropic-messages" ? DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC : DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
|
|
57
|
+
"google-antigravity",
|
|
58
|
+
"google-gemini-cli",
|
|
59
|
+
"openai-codex",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
|
|
63
|
+
...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
|
|
64
|
+
...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
|
|
65
|
+
];
|
|
66
|
+
|
|
56
67
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
57
68
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
58
69
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -1133,28 +1144,28 @@ export class ModelRegistry {
|
|
|
1133
1144
|
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
|
|
1134
1145
|
const cachedModels: Model<Api>[] = [];
|
|
1135
1146
|
const authoritativeFreshProviders = new Set<string>();
|
|
1136
|
-
for (const
|
|
1137
|
-
if (configuredDiscoveryProviders.has(
|
|
1147
|
+
for (const providerId of STARTUP_MODEL_CACHE_PROVIDER_IDS) {
|
|
1148
|
+
if (configuredDiscoveryProviders.has(providerId)) {
|
|
1138
1149
|
continue;
|
|
1139
1150
|
}
|
|
1140
|
-
const cache = readModelCache<Api>(
|
|
1151
|
+
const cache = readModelCache<Api>(providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1141
1152
|
if (!cache) {
|
|
1142
1153
|
continue;
|
|
1143
1154
|
}
|
|
1144
1155
|
if (cache.fresh && cache.authoritative) {
|
|
1145
|
-
authoritativeFreshProviders.add(
|
|
1156
|
+
authoritativeFreshProviders.add(providerId);
|
|
1146
1157
|
}
|
|
1147
1158
|
const models = cache.models.map(model =>
|
|
1148
|
-
model.provider ===
|
|
1159
|
+
model.provider === providerId ? model : { ...model, provider: providerId },
|
|
1149
1160
|
);
|
|
1150
|
-
const providerOverride = this.#providerOverrides.get(
|
|
1161
|
+
const providerOverride = this.#providerOverrides.get(providerId);
|
|
1151
1162
|
const withTransport = providerOverride
|
|
1152
1163
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1153
1164
|
: models;
|
|
1154
1165
|
const withCompat = providerOverride?.compat
|
|
1155
1166
|
? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) }))
|
|
1156
1167
|
: withTransport;
|
|
1157
|
-
cachedModels.push(...this.#applyProviderModelOverrides(
|
|
1168
|
+
cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
|
|
1158
1169
|
}
|
|
1159
1170
|
return { models: cachedModels, authoritativeFreshProviders };
|
|
1160
1171
|
}
|
|
@@ -2894,7 +2894,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2894
2894
|
label: "Auto",
|
|
2895
2895
|
description: "Preferred web-search provider",
|
|
2896
2896
|
},
|
|
2897
|
-
{ value: "exa", label: "Exa", description: "
|
|
2897
|
+
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
2898
2898
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
2899
2899
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
2900
2900
|
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
@@ -2920,7 +2920,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2920
2920
|
},
|
|
2921
2921
|
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
2922
2922
|
{ value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
|
|
2923
|
-
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY
|
|
2923
|
+
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY (Kagi V1 Search API)" },
|
|
2924
2924
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
2925
2925
|
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
2926
2926
|
{ value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
|
package/src/config/settings.ts
CHANGED
|
@@ -365,6 +365,29 @@ export class Settings {
|
|
|
365
365
|
return cloned;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Re-scope this instance to a new working directory *in place*: reload the
|
|
370
|
+
* project layer (`.claude/settings.yml` etc.) from `cwd`, re-resolve
|
|
371
|
+
* path-scoped settings against it, and re-fire side-effect hooks (theme,
|
|
372
|
+
* symbols, tab width, …). Global settings and runtime overrides are preserved.
|
|
373
|
+
*
|
|
374
|
+
* Unlike {@link cloneForCwd}, this mutates the live instance, so every holder
|
|
375
|
+
* (the `settings` proxy, the active session, controllers) observes the new
|
|
376
|
+
* project scope without swapping references — used when the process changes
|
|
377
|
+
* directory mid-run (`/move`, cross-project resume). No-op when `cwd` is
|
|
378
|
+
* already the current scope.
|
|
379
|
+
*/
|
|
380
|
+
async reloadForCwd(cwd: string): Promise<void> {
|
|
381
|
+
const normalized = path.normalize(cwd);
|
|
382
|
+
if (normalized === this.#cwd) return;
|
|
383
|
+
this.#cwd = normalized;
|
|
384
|
+
if (this.#persist) {
|
|
385
|
+
this.#project = await this.#loadProjectSettings();
|
|
386
|
+
}
|
|
387
|
+
this.#rebuildMerged();
|
|
388
|
+
this.#fireAllHooks();
|
|
389
|
+
}
|
|
390
|
+
|
|
368
391
|
// ─────────────────────────────────────────────────────────────────────────
|
|
369
392
|
// Accessors
|
|
370
393
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -17,6 +17,7 @@ import rsResultType from "./rs-result-type.md" with { type: "text" };
|
|
|
17
17
|
import tsBareCatch from "./ts-bare-catch.md" with { type: "text" };
|
|
18
18
|
import tsImportType from "./ts-import-type.md" with { type: "text" };
|
|
19
19
|
import tsNoAny from "./ts-no-any.md" with { type: "text" };
|
|
20
|
+
import tsNoDeprecatedLeftovers from "./ts-no-deprecated-leftovers.md" with { type: "text" };
|
|
20
21
|
import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
|
|
21
22
|
import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
|
|
22
23
|
import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
|
|
@@ -40,6 +41,7 @@ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
|
|
|
40
41
|
{ name: "ts-bare-catch", content: tsBareCatch },
|
|
41
42
|
{ name: "ts-import-type", content: tsImportType },
|
|
42
43
|
{ name: "ts-no-any", content: tsNoAny },
|
|
44
|
+
{ name: "ts-no-deprecated-leftovers", content: tsNoDeprecatedLeftovers },
|
|
43
45
|
{ name: "ts-no-dynamic-import", content: tsNoDynamicImport },
|
|
44
46
|
{ name: "ts-no-return-type", content: tsNoReturnType },
|
|
45
47
|
{ name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Do not leave `@deprecated` shims behind after refactors — update call sites and remove the old API"
|
|
3
|
+
condition: "@deprecated"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Do not use `@deprecated` as a substitute for finishing a refactor. If an API is obsolete inside the code you control, update every call site and remove the old name in the same change.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Deprecated aliases keep two contracts alive.
|
|
12
|
+
- Future maintainers must preserve behavior nobody should call.
|
|
13
|
+
- Tests can pass while production code keeps using the old path.
|
|
14
|
+
- The next refactor has to unwind both the real API and the compatibility layer.
|
|
15
|
+
|
|
16
|
+
## Avoid
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Bad — leaves a stale compatibility name instead of finishing the cutover.
|
|
20
|
+
/** @deprecated Use loadSettings instead. */
|
|
21
|
+
export const loadConfig = loadSettings;
|
|
22
|
+
|
|
23
|
+
// Bad — preserves an obsolete wrapper after callers can be updated.
|
|
24
|
+
/** @deprecated Use createClient instead. */
|
|
25
|
+
export function makeClient(options: ClientOptions): Client {
|
|
26
|
+
return createClient(options);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Update all imports and call sites to the durable name.
|
|
34
|
+
export function loadSettings(path: string): Settings { ... }
|
|
35
|
+
export function createClient(options: ClientOptions): Client { ... }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Exceptions
|
|
39
|
+
|
|
40
|
+
- Public package APIs with a documented migration window.
|
|
41
|
+
- Third-party declarations where the deprecated marker reflects an external contract.
|
|
42
|
+
- Tests that intentionally verify deprecated API behavior during a supported transition.
|
|
43
|
+
|
|
44
|
+
If an exception applies, state the external compatibility requirement. Otherwise, finish the refactor and delete the deprecated symbol.
|
|
@@ -99,10 +99,8 @@ async function resolvePluginDir(
|
|
|
99
99
|
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
100
100
|
const items: Skill[] = [];
|
|
101
101
|
const warnings: string[] = [];
|
|
102
|
-
|
|
103
102
|
const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
|
|
104
103
|
warnings.push(...rootWarnings);
|
|
105
|
-
|
|
106
104
|
const results = await Promise.all(
|
|
107
105
|
roots.map(async root => {
|
|
108
106
|
const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
|
|
@@ -114,16 +112,16 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
114
112
|
return { root, result, warning };
|
|
115
113
|
}),
|
|
116
114
|
);
|
|
117
|
-
|
|
118
|
-
for (const { root, result, warning } of results) {
|
|
115
|
+
for (const { result, warning } of results) {
|
|
119
116
|
if (warning) warnings.push(warning);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
// Intentionally do NOT prefix skill names with `root.plugin`.
|
|
118
|
+
// The `plugin:name` format breaks skill:// URL parsing (colons are
|
|
119
|
+
// ambiguous with port separators) and is unintuitive for callers.
|
|
120
|
+
// Dedup-by-key in the capability layer already handles name collisions
|
|
121
|
+
// across providers using priority ordering.
|
|
122
|
+
items.push(...result.items);
|
|
124
123
|
if (result.warnings) warnings.push(...result.warnings);
|
|
125
124
|
}
|
|
126
|
-
|
|
127
125
|
return { items, warnings };
|
|
128
126
|
}
|
|
129
127
|
|
package/src/edit/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
|
24
24
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
25
25
|
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
26
26
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
27
|
+
import { EDIT_MODE_STRATEGIES } from "./streaming";
|
|
27
28
|
|
|
28
29
|
export * from "@oh-my-pi/hashline";
|
|
29
30
|
export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
@@ -360,6 +361,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
360
361
|
return "apply_patch";
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Normalize streamed args into the source text this edit introduces, so
|
|
366
|
+
* stream matchers (TTSR rules) run against real file content instead of the
|
|
367
|
+
* mode-specific patch grammar.
|
|
368
|
+
*/
|
|
369
|
+
matcherDigest(args: unknown): string | undefined {
|
|
370
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherDigest(args);
|
|
371
|
+
}
|
|
372
|
+
|
|
363
373
|
async execute(
|
|
364
374
|
_toolCallId: string,
|
|
365
375
|
params: EditParams,
|
package/src/edit/streaming.ts
CHANGED
|
@@ -69,6 +69,14 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
69
69
|
* compute returned `null` because args are still too partial).
|
|
70
70
|
*/
|
|
71
71
|
renderStreamingFallback(args: Args, uiTheme: Theme): string;
|
|
72
|
+
/**
|
|
73
|
+
* Project the (potentially partial) args onto the plain text the edit
|
|
74
|
+
* introduces into files — added lines without patch grammar — so stream
|
|
75
|
+
* matchers (TTSR rules) can run source-level patterns against real content
|
|
76
|
+
* instead of the mode-specific wire format. Returns `undefined` when the
|
|
77
|
+
* args don't yet carry any content.
|
|
78
|
+
*/
|
|
79
|
+
matcherDigest(args: Args): string | undefined;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
// -----------------------------------------------------------------------------
|
|
@@ -161,6 +169,28 @@ function groupApplyPatchEntriesByPath(entries: readonly ApplyPatchEntry[]): Map<
|
|
|
161
169
|
return groups;
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Extract the lines a patch-style payload adds (`+` prefix, excluding `+++ `
|
|
174
|
+
* file headers), stripped of the prefix. When the text carries no added lines,
|
|
175
|
+
* returns the whole text if `fallbackToWhole` (full-content payloads such as a
|
|
176
|
+
* `create` op), otherwise an empty string (grammar-only payloads).
|
|
177
|
+
*/
|
|
178
|
+
function extractAddedLines(text: string, fallbackToWhole: boolean): string {
|
|
179
|
+
let added: string | undefined;
|
|
180
|
+
let lineStart = 0;
|
|
181
|
+
while (lineStart <= text.length) {
|
|
182
|
+
let lineEnd = text.indexOf("\n", lineStart);
|
|
183
|
+
if (lineEnd === -1) lineEnd = text.length;
|
|
184
|
+
if (text.charCodeAt(lineStart) === 43 /* + */ && !text.startsWith("+++ ", lineStart)) {
|
|
185
|
+
const line = text.slice(lineStart + 1, lineEnd);
|
|
186
|
+
added = added === undefined ? line : `${added}\n${line}`;
|
|
187
|
+
}
|
|
188
|
+
lineStart = lineEnd + 1;
|
|
189
|
+
}
|
|
190
|
+
if (added === undefined) return fallbackToWhole ? text : "";
|
|
191
|
+
return added;
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
// -----------------------------------------------------------------------------
|
|
165
195
|
// Strategies
|
|
166
196
|
// -----------------------------------------------------------------------------
|
|
@@ -196,6 +226,16 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
|
|
|
196
226
|
renderStreamingFallback() {
|
|
197
227
|
return "";
|
|
198
228
|
},
|
|
229
|
+
matcherDigest(args) {
|
|
230
|
+
const edits = args?.edits;
|
|
231
|
+
if (!Array.isArray(edits)) return undefined;
|
|
232
|
+
let digest: string | undefined;
|
|
233
|
+
for (const edit of edits) {
|
|
234
|
+
if (typeof edit?.new_text !== "string") continue;
|
|
235
|
+
digest = digest === undefined ? edit.new_text : `${digest}\n${edit.new_text}`;
|
|
236
|
+
}
|
|
237
|
+
return digest;
|
|
238
|
+
},
|
|
199
239
|
};
|
|
200
240
|
|
|
201
241
|
interface PatchArgs {
|
|
@@ -225,6 +265,19 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
225
265
|
renderStreamingFallback() {
|
|
226
266
|
return "";
|
|
227
267
|
},
|
|
268
|
+
matcherDigest(args) {
|
|
269
|
+
const edits = args?.edits;
|
|
270
|
+
if (!Array.isArray(edits)) return undefined;
|
|
271
|
+
let digest: string | undefined;
|
|
272
|
+
for (const edit of edits) {
|
|
273
|
+
if (typeof edit?.diff !== "string") continue;
|
|
274
|
+
// `create` ops carry full file content in `diff` with no +/- markers;
|
|
275
|
+
// pass that content through whole.
|
|
276
|
+
const added = extractAddedLines(edit.diff, true);
|
|
277
|
+
digest = digest === undefined ? added : `${digest}\n${added}`;
|
|
278
|
+
}
|
|
279
|
+
return digest;
|
|
280
|
+
},
|
|
228
281
|
};
|
|
229
282
|
|
|
230
283
|
interface HashlineArgs {
|
|
@@ -378,6 +431,12 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
378
431
|
// than a sigil dump.
|
|
379
432
|
return "";
|
|
380
433
|
},
|
|
434
|
+
matcherDigest(args) {
|
|
435
|
+
const input = args?.input;
|
|
436
|
+
if (typeof input !== "string") return undefined;
|
|
437
|
+
// Body rows are `+TEXT`; headers and op lines are grammar, never content.
|
|
438
|
+
return extractAddedLines(input, false);
|
|
439
|
+
},
|
|
381
440
|
};
|
|
382
441
|
|
|
383
442
|
interface ApplyPatchArgs {
|
|
@@ -430,6 +489,12 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
430
489
|
renderStreamingFallback() {
|
|
431
490
|
return "";
|
|
432
491
|
},
|
|
492
|
+
matcherDigest(args) {
|
|
493
|
+
const input = args?.input;
|
|
494
|
+
if (typeof input !== "string") return undefined;
|
|
495
|
+
// Envelope markers and `@@` hunk headers are grammar, never content.
|
|
496
|
+
return extractAddedLines(input, false);
|
|
497
|
+
},
|
|
433
498
|
};
|
|
434
499
|
export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>> = {
|
|
435
500
|
replace: replaceStrategy as EditStreamingStrategy<unknown>,
|
|
@@ -106,6 +106,7 @@ function singleResult(options: ExecutorOptions, overrides: Partial<SingleResult>
|
|
|
106
106
|
function makeEvalSession(
|
|
107
107
|
tempDir: TempDir,
|
|
108
108
|
prefix: string,
|
|
109
|
+
settings?: Settings,
|
|
109
110
|
): { session: ToolSession; sessionFile: string; sessionId: string } {
|
|
110
111
|
const sessionFile = path.join(tempDir.path(), "session.jsonl");
|
|
111
112
|
const artifactsDir = sessionFile.slice(0, -6);
|
|
@@ -113,6 +114,7 @@ function makeEvalSession(
|
|
|
113
114
|
cwd: tempDir.path(),
|
|
114
115
|
sessionFile,
|
|
115
116
|
artifactsDir,
|
|
117
|
+
settings,
|
|
116
118
|
outputManager: new AgentOutputManager(() => artifactsDir),
|
|
117
119
|
});
|
|
118
120
|
return { session, sessionFile, sessionId: `${prefix}:${crypto.randomUUID()}` };
|
|
@@ -261,9 +263,15 @@ describe("agent() through eval runtimes", () => {
|
|
|
261
263
|
expect(JSON.parse(result.output.trim())).toEqual(["hello from agent", { ok: true, n: 3 }]);
|
|
262
264
|
});
|
|
263
265
|
|
|
264
|
-
it("
|
|
266
|
+
it("bounds JavaScript parallel() by the task.maxConcurrency setting while preserving order", async () => {
|
|
265
267
|
using tempDir = TempDir.createSync("@omp-eval-agent-js-parallel-");
|
|
266
|
-
const
|
|
268
|
+
const settings = Settings.isolated({
|
|
269
|
+
"async.enabled": false,
|
|
270
|
+
"task.isolation.mode": "none",
|
|
271
|
+
"task.enableLsp": true,
|
|
272
|
+
"task.maxConcurrency": 2,
|
|
273
|
+
});
|
|
274
|
+
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "js-agent-parallel", settings);
|
|
267
275
|
mockAgents();
|
|
268
276
|
let inFlight = 0;
|
|
269
277
|
let maxInFlight = 0;
|
|
@@ -279,7 +287,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
279
287
|
});
|
|
280
288
|
|
|
281
289
|
const result = await executeJs(
|
|
282
|
-
'const values = await parallel(["a", "b", "c", "d"].map(name => () => agent(name))
|
|
290
|
+
'const values = await parallel(["a", "b", "c", "d"].map(name => () => agent(name))); return JSON.stringify(values);',
|
|
283
291
|
{ cwd: tempDir.path(), sessionId, session, sessionFile },
|
|
284
292
|
);
|
|
285
293
|
|
|
@@ -300,7 +308,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
300
308
|
return singleResult(options, { output: options.assignment ?? "" });
|
|
301
309
|
});
|
|
302
310
|
|
|
303
|
-
const result = await executeJs('await parallel([() => agent("ok"), () => agent("bad")]
|
|
311
|
+
const result = await executeJs('await parallel([() => agent("ok"), () => agent("bad")]);', {
|
|
304
312
|
cwd: tempDir.path(),
|
|
305
313
|
sessionId,
|
|
306
314
|
session,
|
|
@@ -343,6 +351,52 @@ describe("agent() through eval runtimes", () => {
|
|
|
343
351
|
expect(result.output.trim()).toBe("hello from python");
|
|
344
352
|
});
|
|
345
353
|
|
|
354
|
+
it("bounds Python parallel() by the task.maxConcurrency setting while preserving order", async () => {
|
|
355
|
+
using tempDir = TempDir.createSync("@omp-eval-agent-py-parallel-");
|
|
356
|
+
const settings = Settings.isolated({
|
|
357
|
+
"async.enabled": false,
|
|
358
|
+
"task.isolation.mode": "none",
|
|
359
|
+
"task.enableLsp": true,
|
|
360
|
+
"task.maxConcurrency": 2,
|
|
361
|
+
});
|
|
362
|
+
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent-parallel", settings);
|
|
363
|
+
mockAgents();
|
|
364
|
+
let inFlight = 0;
|
|
365
|
+
let maxInFlight = 0;
|
|
366
|
+
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
367
|
+
inFlight++;
|
|
368
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
369
|
+
try {
|
|
370
|
+
await Bun.sleep(options.assignment === "a" ? 30 : 10);
|
|
371
|
+
return singleResult(options, { output: options.assignment ?? "" });
|
|
372
|
+
} finally {
|
|
373
|
+
inFlight--;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const probe = await executePython('print("probe")', {
|
|
378
|
+
cwd: tempDir.path(),
|
|
379
|
+
sessionId: `${sessionId}:probe`,
|
|
380
|
+
sessionFile,
|
|
381
|
+
kernelMode: "per-call",
|
|
382
|
+
});
|
|
383
|
+
if (probe.exitCode === undefined && probe.cancelled) {
|
|
384
|
+
expect(probe.output).toBe("");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
expect(probe.exitCode).toBe(0);
|
|
388
|
+
|
|
389
|
+
const result = await executePython(
|
|
390
|
+
'import json\nprint(json.dumps(parallel([lambda n=n: agent(n) for n in ["a", "b", "c", "d"]])))',
|
|
391
|
+
{ cwd: tempDir.path(), sessionId, sessionFile, kernelMode: "per-call", toolSession: session },
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(result.exitCode).toBe(0);
|
|
395
|
+
expect(JSON.parse(result.output.trim())).toEqual(["a", "b", "c", "d"]);
|
|
396
|
+
expect(maxInFlight).toBeGreaterThan(1);
|
|
397
|
+
expect(maxInFlight).toBeLessThanOrEqual(2);
|
|
398
|
+
});
|
|
399
|
+
|
|
346
400
|
it("streams enriched agent progress through onStatus before the cell finishes", async () => {
|
|
347
401
|
using tempDir = TempDir.createSync("@omp-eval-agent-progress-");
|
|
348
402
|
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "js-agent-progress");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side handler for the eval `parallel()` / `pipeline()` worker pool.
|
|
3
|
+
*
|
|
4
|
+
* The pool ceiling is not a kernel-side knob: it tracks the `task.maxConcurrency`
|
|
5
|
+
* setting so an eval fan-out runs as wide as a `task` tool batch would. `0` means
|
|
6
|
+
* unbounded — run every item at once, exactly like `task.maxConcurrency = 0`.
|
|
7
|
+
*/
|
|
8
|
+
import type { ToolSession } from "../tools";
|
|
9
|
+
import type { JsStatusEvent } from "./js/shared/types";
|
|
10
|
+
|
|
11
|
+
/** Synthetic bridge name reserved for the parallel-pool ceiling across both runtimes. */
|
|
12
|
+
export const EVAL_CONCURRENCY_BRIDGE_NAME = "__concurrency__";
|
|
13
|
+
|
|
14
|
+
export interface EvalConcurrencyBridgeOptions {
|
|
15
|
+
session: ToolSession;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
emitStatus?: (event: JsStatusEvent) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EvalConcurrencyResult {
|
|
21
|
+
/** Worker-pool ceiling; `0` means unbounded (run every item at once). */
|
|
22
|
+
limit: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the worker-pool ceiling for an eval cell's `parallel()`/`pipeline()`
|
|
27
|
+
* helpers from the live `task.maxConcurrency` setting. Negative/non-finite
|
|
28
|
+
* values collapse to `0` (unbounded), matching the `task` tool's own handling.
|
|
29
|
+
*/
|
|
30
|
+
export function runEvalConcurrency(_args: unknown, options: EvalConcurrencyBridgeOptions): EvalConcurrencyResult {
|
|
31
|
+
const raw = options.session.settings.get("task.maxConcurrency");
|
|
32
|
+
const limit = Number.isFinite(raw) ? Math.trunc(raw) : 0;
|
|
33
|
+
return { limit: limit > 0 ? limit : 0 };
|
|
34
|
+
}
|