@oh-my-pi/pi-coding-agent 15.7.5 → 15.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -182
- 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 +5 -0
- package/dist/types/config/settings-schema.d.ts +11 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/eval/__tests__/heartbeat.test.d.ts +1 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/heartbeat.d.ts +45 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +24 -5
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/lsp/diagnostics-ledger.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +2 -0
- package/dist/types/lsp/utils.d.ts +4 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/custom-editor.d.ts +3 -2
- package/dist/types/modes/components/hook-selector.d.ts +10 -1
- 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 +3 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -3
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +5 -3
- 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 +12 -3
- 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/ask.d.ts +8 -6
- package/dist/types/tools/eval-backends.d.ts +12 -0
- package/dist/types/tools/eval-render.d.ts +52 -0
- package/dist/types/tools/eval.d.ts +2 -35
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +5 -12
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tui/output-block.d.ts +4 -3
- package/dist/types/utils/clipboard.d.ts +4 -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/examples/extensions/README.md +1 -0
- package/examples/extensions/thinking-note.ts +13 -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 +6 -0
- package/src/config/model-registry.ts +33 -4
- package/src/config/settings-schema.ts +12 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/discovery/claude.ts +41 -22
- package/src/edit/index.ts +23 -3
- package/src/eval/__tests__/agent-bridge.test.ts +148 -4
- package/src/eval/__tests__/heartbeat.test.ts +84 -0
- package/src/eval/__tests__/llm-bridge.test.ts +30 -0
- package/src/eval/agent-bridge.ts +44 -38
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/heartbeat.ts +74 -0
- package/src/eval/js/executor.ts +13 -9
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/llm-bridge.ts +20 -14
- package/src/eval/py/executor.ts +14 -18
- package/src/eval/py/prelude.py +23 -15
- package/src/exec/bash-executor.ts +31 -5
- package/src/extensibility/extensions/loader.ts +16 -18
- package/src/extensibility/extensions/runner.ts +22 -17
- package/src/extensibility/extensions/types.ts +39 -5
- 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 +14 -13
- package/src/lsp/diagnostics-ledger.ts +51 -0
- package/src/lsp/index.ts +9 -22
- package/src/lsp/utils.ts +21 -0
- package/src/main.ts +92 -24
- package/src/modes/acp/acp-agent.ts +8 -4
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/assistant-message.ts +28 -1
- package/src/modes/components/custom-editor.ts +19 -7
- package/src/modes/components/hook-selector.ts +229 -44
- 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 +36 -7
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/event-controller.ts +5 -2
- package/src/modes/controllers/extension-ui-controller.ts +6 -4
- package/src/modes/controllers/input-controller.ts +19 -16
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +127 -16
- package/src/modes/rpc/rpc-mode.ts +17 -6
- package/src/modes/theme/theme-schema.json +30 -0
- package/src/modes/theme/theme.ts +39 -2
- package/src/modes/types.ts +7 -3
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/ask.md +2 -1
- package/src/prompts/tools/eval.md +6 -6
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- 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 +127 -57
- 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/ask.ts +74 -32
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/eval-backends.ts +38 -0
- package/src/tools/eval-render.ts +750 -0
- package/src/tools/eval.ts +27 -754
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +8 -38
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/renderers.ts +1 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +9 -1
- package/src/tui/output-block.ts +5 -4
- package/src/utils/clipboard.ts +38 -1
- 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;
|
|
@@ -122,6 +123,10 @@ export const KEYBINDINGS = {
|
|
|
122
123
|
defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v",
|
|
123
124
|
description: "Paste image from clipboard",
|
|
124
125
|
},
|
|
126
|
+
"app.clipboard.pasteTextRaw": {
|
|
127
|
+
defaultKeys: ["ctrl+shift+v", "alt+shift+v"],
|
|
128
|
+
description: "Paste text from clipboard as raw text (no collapse)",
|
|
129
|
+
},
|
|
125
130
|
"app.clipboard.copyLine": {
|
|
126
131
|
defaultKeys: "alt+shift+l",
|
|
127
132
|
description: "Copy current line",
|
|
@@ -214,6 +219,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
|
|
|
214
219
|
followUp: "app.message.followUp",
|
|
215
220
|
dequeue: "app.message.dequeue",
|
|
216
221
|
pasteImage: "app.clipboard.pasteImage",
|
|
222
|
+
pasteTextRaw: "app.clipboard.pasteTextRaw",
|
|
217
223
|
copyLine: "app.clipboard.copyLine",
|
|
218
224
|
copyPrompt: "app.clipboard.copyPrompt",
|
|
219
225
|
newSession: "app.session.new",
|
|
@@ -27,6 +27,32 @@ import {
|
|
|
27
27
|
// any provider module at startup. Must match `DEFAULT_LOCAL_TOKEN` in oauth/lm-studio.ts.
|
|
28
28
|
const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
29
29
|
|
|
30
|
+
// Default cap on `max_tokens` for auto-discovered models that do not advertise
|
|
31
|
+
// their own output limit (OpenAI-models-list, Ollama, llama.cpp, new-api/
|
|
32
|
+
// one-api proxies). 32K matches the upper end of what mainstream
|
|
33
|
+
// OpenAI-compatible providers (DeepSeek, MiMo, OpenRouter, etc.) actually
|
|
34
|
+
// accept and keeps `min(contextWindow, …)` honoring smaller local windows.
|
|
35
|
+
// Conservative caps below this caused providers to drop the connection
|
|
36
|
+
// mid-stream when models hit the cap on legitimate large tool calls (see
|
|
37
|
+
// issue #1528: `write` payloads >~5KB on deepseek-v4-pro surfaced as
|
|
38
|
+
// "socket connection was closed unexpectedly").
|
|
39
|
+
const DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
40
|
+
|
|
41
|
+
// Anthropic-safe variant of the discovery cap. The Anthropic stream converter
|
|
42
|
+
// in `packages/ai/src/providers/anthropic.ts` derives the request limit as
|
|
43
|
+
// `(model.maxTokens / 3) | 0`, so the 32K default would surface as 10,922
|
|
44
|
+
// requested output tokens — above the 8,192 hard cap on classic Claude 3.x
|
|
45
|
+
// Sonnet/Haiku/Opus endpoints. Discovered models routed through
|
|
46
|
+
// `anthropic-messages` (proxy `supported_endpoint_types: ["anthropic"]` or a
|
|
47
|
+
// custom provider with `api: anthropic-messages` + openai-models-list
|
|
48
|
+
// discovery) fall back to this conservative value.
|
|
49
|
+
const DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC = 8_192;
|
|
50
|
+
|
|
51
|
+
/** Routes discovered-model `maxTokens` defaults around Anthropic's 3× output divisor. */
|
|
52
|
+
function discoveryDefaultMaxTokens(api: Api | undefined): number {
|
|
53
|
+
return api === "anthropic-messages" ? DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC : DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
31
57
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
32
58
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -1690,7 +1716,7 @@ export class ModelRegistry {
|
|
|
1690
1716
|
input: metadata?.input ?? ["text"],
|
|
1691
1717
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1692
1718
|
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1693
|
-
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
1719
|
+
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1694
1720
|
headers: providerConfig.headers,
|
|
1695
1721
|
});
|
|
1696
1722
|
});
|
|
@@ -1760,7 +1786,10 @@ export class ModelRegistry {
|
|
|
1760
1786
|
input: serverMetadata?.input ?? ["text"],
|
|
1761
1787
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1762
1788
|
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
1763
|
-
maxTokens: Math.min(
|
|
1789
|
+
maxTokens: Math.min(
|
|
1790
|
+
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
1791
|
+
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
1792
|
+
),
|
|
1764
1793
|
headers,
|
|
1765
1794
|
compat: {
|
|
1766
1795
|
supportsStore: false,
|
|
@@ -1807,7 +1836,7 @@ export class ModelRegistry {
|
|
|
1807
1836
|
input: ["text"],
|
|
1808
1837
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1809
1838
|
contextWindow: 128000,
|
|
1810
|
-
maxTokens:
|
|
1839
|
+
maxTokens: discoveryDefaultMaxTokens(providerConfig.api),
|
|
1811
1840
|
headers,
|
|
1812
1841
|
compat: {
|
|
1813
1842
|
supportsStore: false,
|
|
@@ -1890,7 +1919,7 @@ export class ModelRegistry {
|
|
|
1890
1919
|
// we successfully recover the upstream model identity.
|
|
1891
1920
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1892
1921
|
contextWindow: reference?.contextWindow ?? 128000,
|
|
1893
|
-
maxTokens: reference?.maxTokens ??
|
|
1922
|
+
maxTokens: reference?.maxTokens ?? discoveryDefaultMaxTokens(api),
|
|
1894
1923
|
headers,
|
|
1895
1924
|
// OpenAI-compat fields are no-ops on anthropic models; the
|
|
1896
1925
|
// Anthropic SDK ignores them. Provider-level disableStrictTools
|
|
@@ -1966,6 +1966,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1966
1966
|
},
|
|
1967
1967
|
},
|
|
1968
1968
|
|
|
1969
|
+
"lsp.diagnosticsDeduplicate": {
|
|
1970
|
+
type: "boolean",
|
|
1971
|
+
default: true,
|
|
1972
|
+
ui: {
|
|
1973
|
+
tab: "editing",
|
|
1974
|
+
label: "Deduplicate Diagnostics",
|
|
1975
|
+
description: "Suppress post-edit LSP diagnostics already shown for a file; only surface new or changed ones",
|
|
1976
|
+
},
|
|
1977
|
+
},
|
|
1978
|
+
|
|
1969
1979
|
// Bash interceptor
|
|
1970
1980
|
"bashInterceptor.enabled": {
|
|
1971
1981
|
type: "boolean",
|
|
@@ -2884,7 +2894,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2884
2894
|
label: "Auto",
|
|
2885
2895
|
description: "Preferred web-search provider",
|
|
2886
2896
|
},
|
|
2887
|
-
{ value: "exa", label: "Exa", description: "
|
|
2897
|
+
{ value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
|
|
2888
2898
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
2889
2899
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
2890
2900
|
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
@@ -2910,7 +2920,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2910
2920
|
},
|
|
2911
2921
|
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
2912
2922
|
{ value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
|
|
2913
|
-
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY
|
|
2923
|
+
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY (Kagi V1 Search API)" },
|
|
2914
2924
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
2915
2925
|
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
2916
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
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -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/discovery/claude.ts
CHANGED
|
@@ -269,6 +269,29 @@ function readClaudeCommandToggles(): { enableUser: boolean; enableProject: boole
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function getClaudeRelativeCommandName(commandsDir: string, filePath: string): string {
|
|
273
|
+
return path.relative(commandsDir, filePath).replace(/\.md$/, "");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function addClaudeCommandNamespaceAliases(commands: SlashCommand[], commandsDir: string): SlashCommand[] {
|
|
277
|
+
const rootCommands: SlashCommand[] = [];
|
|
278
|
+
const nestedCommands: SlashCommand[] = [];
|
|
279
|
+
const aliases: SlashCommand[] = [];
|
|
280
|
+
|
|
281
|
+
for (const command of commands) {
|
|
282
|
+
const relativeName = getClaudeRelativeCommandName(commandsDir, command.path);
|
|
283
|
+
if (!/[\\/]/.test(relativeName)) {
|
|
284
|
+
rootCommands.push(command);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
nestedCommands.push(command);
|
|
289
|
+
aliases.push({ ...command, name: relativeName.replace(/[\\/]+/g, ":") });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return nestedCommands.length === 0 ? commands : [...rootCommands, ...nestedCommands, ...aliases];
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
|
|
273
296
|
const items: SlashCommand[] = [];
|
|
274
297
|
const warnings: string[] = [];
|
|
@@ -280,19 +303,17 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
280
303
|
|
|
281
304
|
const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
|
|
282
305
|
extensions: ["md"],
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
};
|
|
292
|
-
},
|
|
306
|
+
recursive: true,
|
|
307
|
+
transform: (name, content, filePath, source) => ({
|
|
308
|
+
name: name.replace(/\.md$/, ""),
|
|
309
|
+
path: filePath,
|
|
310
|
+
content,
|
|
311
|
+
level: "user",
|
|
312
|
+
_source: source,
|
|
313
|
+
}),
|
|
293
314
|
});
|
|
294
315
|
|
|
295
|
-
items.push(...userResult.items);
|
|
316
|
+
items.push(...addClaudeCommandNamespaceAliases(userResult.items, userCommandsDir));
|
|
296
317
|
if (userResult.warnings) warnings.push(...userResult.warnings);
|
|
297
318
|
}
|
|
298
319
|
|
|
@@ -301,19 +322,17 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
301
322
|
|
|
302
323
|
const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
|
|
303
324
|
extensions: ["md"],
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
};
|
|
313
|
-
},
|
|
325
|
+
recursive: true,
|
|
326
|
+
transform: (name, content, filePath, source) => ({
|
|
327
|
+
name: name.replace(/\.md$/, ""),
|
|
328
|
+
path: filePath,
|
|
329
|
+
content,
|
|
330
|
+
level: "project",
|
|
331
|
+
_source: source,
|
|
332
|
+
}),
|
|
314
333
|
});
|
|
315
334
|
|
|
316
|
-
items.push(...projectResult.items);
|
|
335
|
+
items.push(...addClaudeCommandNamespaceAliases(projectResult.items, projectCommandsDir));
|
|
317
336
|
if (projectResult.warnings) warnings.push(...projectResult.warnings);
|
|
318
337
|
}
|
|
319
338
|
|
package/src/edit/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type WritethroughDeferredHandle,
|
|
11
11
|
writethroughNoop,
|
|
12
12
|
} from "../lsp";
|
|
13
|
+
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
13
14
|
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
14
15
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
15
16
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
@@ -102,7 +103,16 @@ function createEditWritethrough(session: ToolSession): WritethroughCallback {
|
|
|
102
103
|
const enableLsp = session.enableLsp ?? true;
|
|
103
104
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnEdit");
|
|
104
105
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
105
|
-
|
|
106
|
+
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
107
|
+
return enableLsp
|
|
108
|
+
? createLspWritethrough(session.cwd, {
|
|
109
|
+
enableFormat,
|
|
110
|
+
enableDiagnostics,
|
|
111
|
+
transformDiagnostics: dedup
|
|
112
|
+
? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
|
|
113
|
+
: undefined,
|
|
114
|
+
})
|
|
115
|
+
: writethroughNoop;
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
/** Run apply_patch file operations and aggregate their multi-file result. */
|
|
@@ -294,6 +304,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
294
304
|
readonly #fuzzyThreshold: number;
|
|
295
305
|
readonly #writethrough: WritethroughCallback;
|
|
296
306
|
readonly #editMode?: EditMode;
|
|
307
|
+
readonly #dedupDiagnostics: boolean;
|
|
297
308
|
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
298
309
|
|
|
299
310
|
constructor(private readonly session: ToolSession) {
|
|
@@ -306,6 +317,10 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
306
317
|
this.#editMode = resolveConfiguredEditMode(envEditVariant);
|
|
307
318
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
308
319
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
320
|
+
this.#dedupDiagnostics =
|
|
321
|
+
(session.enableLsp ?? true) &&
|
|
322
|
+
session.settings.get("lsp.diagnosticsOnEdit") &&
|
|
323
|
+
session.settings.get("lsp.diagnosticsDeduplicate");
|
|
309
324
|
this.#writethrough = createEditWritethrough(session);
|
|
310
325
|
}
|
|
311
326
|
|
|
@@ -495,8 +510,13 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
495
510
|
}
|
|
496
511
|
|
|
497
512
|
#injectLateDiagnostics(path: string, diagnostics: FileDiagnosticsResult): void {
|
|
498
|
-
const
|
|
499
|
-
|
|
513
|
+
const effective = this.#dedupDiagnostics
|
|
514
|
+
? getDiagnosticsLedger(this.session).reduce(path, diagnostics)
|
|
515
|
+
: diagnostics;
|
|
516
|
+
if (this.#dedupDiagnostics && effective.messages.length === 0) return;
|
|
517
|
+
|
|
518
|
+
const summary = effective.summary ?? "";
|
|
519
|
+
const lines = effective.messages ?? [];
|
|
500
520
|
const body = [`Late LSP diagnostics for ${path} (arrived after the edit tool returned):`, summary, ...lines]
|
|
501
521
|
.filter(Boolean)
|
|
502
522
|
.join("\n");
|