@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.17
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 +78 -0
- package/dist/cli.js +3531 -3812
- package/dist/types/auto-thinking/classifier.d.ts +4 -2
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/model-discovery.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +22 -7
- package/dist/types/extensibility/plugins/loader.d.ts +16 -9
- package/dist/types/extensibility/tool-event-input.d.ts +2 -0
- package/dist/types/hindsight/content.d.ts +2 -10
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -0
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +6 -6
- package/dist/types/session/provider-image-budget.d.ts +3 -0
- package/dist/types/session/session-context.d.ts +6 -5
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/parallel.d.ts +4 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +23 -1
- package/dist/types/tiny/title-client.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/tools-manager.d.ts +2 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/auto-thinking/classifier.ts +7 -2
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/profile-alias.ts +38 -7
- package/src/cli/session-picker.ts +17 -3
- package/src/cli/usage-cli.ts +5 -1
- package/src/commands/launch.ts +3 -3
- package/src/config/model-discovery.ts +59 -8
- package/src/config/model-registry.ts +74 -3
- package/src/config/settings-schema.ts +13 -1
- package/src/discovery/omp-extension-roots.ts +1 -3
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/extensibility/extensions/wrapper.ts +3 -2
- package/src/extensibility/hooks/tool-wrapper.ts +4 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +40 -8
- package/src/extensibility/plugins/loader.ts +71 -23
- package/src/extensibility/plugins/marketplace/manager.ts +134 -0
- package/src/extensibility/tool-event-input.ts +57 -0
- package/src/hindsight/content.ts +21 -12
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/tool-bridge.ts +27 -2
- package/src/mnemopi/state.ts +5 -2
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/agent-transcript-viewer.ts +193 -41
- package/src/modes/components/chat-transcript-builder.ts +6 -0
- package/src/modes/components/custom-editor.test.ts +18 -1
- package/src/modes/components/custom-editor.ts +77 -45
- package/src/modes/components/hook-editor.ts +15 -2
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/component.ts +52 -8
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/welcome.ts +12 -14
- package/src/modes/controllers/command-controller.ts +21 -5
- package/src/modes/controllers/input-controller.ts +115 -3
- package/src/modes/controllers/selector-controller.ts +19 -1
- package/src/modes/interactive-mode.ts +3 -3
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/modes/utils/ui-helpers.ts +3 -3
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +16 -0
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/todo.md +1 -1
- package/src/sdk.ts +8 -10
- package/src/session/agent-session.ts +422 -97
- package/src/session/provider-image-budget.ts +86 -0
- package/src/session/session-context.ts +14 -7
- package/src/session/session-storage.ts +24 -2
- package/src/session/snapcompact-inline.ts +19 -3
- package/src/slash-commands/builtin-registry.ts +0 -22
- package/src/slash-commands/helpers/usage-report.ts +9 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/parallel.ts +6 -1
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +29 -2
- package/src/tiny/title-client.ts +75 -21
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/tools-manager.ts +67 -10
- package/src/web/search/providers/exa.ts +85 -1
|
@@ -59,31 +59,23 @@ export const shellFixtures: Record<string, GalleryFixture> = {
|
|
|
59
59
|
eval: {
|
|
60
60
|
label: "Eval",
|
|
61
61
|
streamingArgs: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
code: 'import json\nfrom pathlib import Path\n\ndata = json.loads(Path("package.js',
|
|
66
|
-
title: "load config",
|
|
67
|
-
},
|
|
68
|
-
],
|
|
62
|
+
language: "py",
|
|
63
|
+
code: 'import json\nfrom pathlib import Path\n\ndata = json.loads(Path("package.js',
|
|
64
|
+
title: "load config",
|
|
69
65
|
},
|
|
70
66
|
args: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"display(sorted(deps)[:3])",
|
|
84
|
-
].join("\n"),
|
|
85
|
-
},
|
|
86
|
-
],
|
|
67
|
+
language: "py",
|
|
68
|
+
title: "load config",
|
|
69
|
+
code: [
|
|
70
|
+
"import json",
|
|
71
|
+
"from pathlib import Path",
|
|
72
|
+
"",
|
|
73
|
+
'data = json.loads(Path("package.json").read_text())',
|
|
74
|
+
'deps = data.get("dependencies", {})',
|
|
75
|
+
'print(f"{data[\\"name\\"]} v{data[\\"version\\"]}")',
|
|
76
|
+
'print(f"{len(deps)} dependencies")',
|
|
77
|
+
"display(sorted(deps)[:3])",
|
|
78
|
+
].join("\n"),
|
|
87
79
|
},
|
|
88
80
|
result: {
|
|
89
81
|
content: [
|
package/src/cli/profile-alias.ts
CHANGED
|
@@ -198,37 +198,68 @@ export function resolveProfileAliasCommandFromProcess(
|
|
|
198
198
|
if (!runtime || !script || !/\.[cm]?[jt]s$/.test(script)) return DEFAULT_ALIAS_COMMAND;
|
|
199
199
|
|
|
200
200
|
const scriptPath = path.resolve(cwd, script);
|
|
201
|
-
|
|
201
|
+
// Normalize to forward slashes for POSIX shell fields — bash/zsh/fish
|
|
202
|
+
// can't resolve backslash-separated paths, even on Windows (Git Bash, WSL).
|
|
203
|
+
const posixScriptPath = scriptPath.replace(/\\/g, "/");
|
|
204
|
+
const posixRuntime = runtime.replace(/\\/g, "/");
|
|
205
|
+
const posix = `${quoteForShell(posixRuntime)} ${quoteForShell(posixScriptPath)}`;
|
|
202
206
|
return {
|
|
203
|
-
display: `${
|
|
207
|
+
display: `${posixRuntime} ${posixScriptPath}`,
|
|
204
208
|
posix,
|
|
205
209
|
fish: posix,
|
|
206
210
|
powerShell: `${quoteForPowerShell(runtime)} ${quoteForPowerShell(scriptPath)}`,
|
|
207
211
|
};
|
|
208
212
|
}
|
|
209
213
|
|
|
214
|
+
/** Normalize backslashes to forward slashes for POSIX-shell paths.
|
|
215
|
+
* path.posix.join only adds / separators — it preserves existing backslashes
|
|
216
|
+
* in input segments like homeDir ("C:\Users\me"), producing mixed paths.
|
|
217
|
+
* Windows UNC paths (\\server\share) become //server/share — path.posix.join
|
|
218
|
+
* would collapse the leading // to /, so we restore it after joining. */
|
|
219
|
+
function toPosix(p: string): string {
|
|
220
|
+
return p.replace(/\\/g, "/");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Like path.posix.join, but preserves leading // (UNC roots) which
|
|
224
|
+
* path.posix.join collapses to a single /. */
|
|
225
|
+
function posixJoinUnc(...segments: string[]): string {
|
|
226
|
+
const joined = path.posix.join(...segments);
|
|
227
|
+
// path.posix.join normalizes // at the start to /, breaking UNC roots.
|
|
228
|
+
// Restore it if any input segment started with // (a toPosix'd UNC path).
|
|
229
|
+
if (segments.some(s => s.startsWith("//") && !s.startsWith("///"))) {
|
|
230
|
+
return `/${joined}`;
|
|
231
|
+
}
|
|
232
|
+
return joined;
|
|
233
|
+
}
|
|
234
|
+
|
|
210
235
|
function resolveShellConfigPath(
|
|
211
236
|
shell: ProfileAliasShell,
|
|
212
237
|
homeDir: string,
|
|
213
238
|
platform: NodeJS.Platform,
|
|
214
239
|
env: NodeJS.ProcessEnv,
|
|
215
240
|
): string {
|
|
241
|
+
// POSIX shells (bash/zsh/fish) always need forward-slash config paths,
|
|
242
|
+
// even on Windows. path.posix.join adds / separators but preserves existing
|
|
243
|
+
// backslashes in input segments, so we normalize each component with toPosix.
|
|
244
|
+
// PowerShell profiles use the platform-native path.join (backslashes on
|
|
245
|
+
// Windows, forward slashes elsewhere).
|
|
246
|
+
const posixHome = toPosix(homeDir);
|
|
216
247
|
switch (shell) {
|
|
217
248
|
case "zsh":
|
|
218
|
-
return
|
|
249
|
+
return posixJoinUnc(env.ZDOTDIR ? toPosix(env.ZDOTDIR) : posixHome, ".zshrc");
|
|
219
250
|
case "bash":
|
|
220
|
-
return platform === "darwin" ?
|
|
251
|
+
return platform === "darwin" ? posixJoinUnc(posixHome, ".bash_profile") : posixJoinUnc(posixHome, ".bashrc");
|
|
221
252
|
case "fish": {
|
|
222
253
|
// fish sources conf.d from $XDG_CONFIG_HOME/fish (default ~/.config/fish);
|
|
223
254
|
// a hard-coded ~/.config would be silently ignored when the user relocates
|
|
224
255
|
// their XDG config root, leaving the alias unsourced after a restart.
|
|
225
|
-
const configHome = env.XDG_CONFIG_HOME
|
|
226
|
-
return
|
|
256
|
+
const configHome = env.XDG_CONFIG_HOME ? toPosix(env.XDG_CONFIG_HOME) : posixJoinUnc(posixHome, ".config");
|
|
257
|
+
return posixJoinUnc(configHome, "fish", "conf.d", "omp-profiles.fish");
|
|
227
258
|
}
|
|
228
259
|
case "pwsh":
|
|
229
260
|
return platform === "win32"
|
|
230
261
|
? path.join(homeDir, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
|
|
231
|
-
:
|
|
262
|
+
: posixJoinUnc(posixHome, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
|
232
263
|
case "powershell":
|
|
233
264
|
return path.join(homeDir, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
234
265
|
}
|
|
@@ -8,8 +8,10 @@ import { FileSessionStorage } from "../session/session-storage";
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Show the TUI session selector and return the selected session, or null if
|
|
11
|
-
* cancelled.
|
|
12
|
-
*
|
|
11
|
+
* cancelled. Rendered as a fullscreen overlay on the terminal's alternate
|
|
12
|
+
* screen, so the list scrolls and rows are clickable with the mouse. Tab
|
|
13
|
+
* toggles between current-folder and all-projects scope; the all-projects list
|
|
14
|
+
* is loaded lazily via `SessionManager.listAll`.
|
|
13
15
|
*/
|
|
14
16
|
export async function selectSession(
|
|
15
17
|
sessions: SessionInfo[],
|
|
@@ -65,6 +67,7 @@ export async function selectSession(
|
|
|
65
67
|
loadAllSessions: () => SessionManager.listAll(storage),
|
|
66
68
|
allSessions: options?.allSessions,
|
|
67
69
|
getTerminalRows: () => ui.terminal.rows,
|
|
70
|
+
fillHeight: true,
|
|
68
71
|
},
|
|
69
72
|
);
|
|
70
73
|
return selector;
|
|
@@ -72,7 +75,18 @@ export async function selectSession(
|
|
|
72
75
|
|
|
73
76
|
const selector = showSelector();
|
|
74
77
|
selector.setOnRequestRender(() => ui.requestRender());
|
|
75
|
-
|
|
78
|
+
// Present as a fullscreen overlay so the picker borrows the terminal's
|
|
79
|
+
// alternate screen buffer (vim/less idiom): the list scrolls and rows are
|
|
80
|
+
// clickable via the mouse tracking the overlay enables for its lifetime.
|
|
81
|
+
// Anchored top-left at full size so a mouse row maps directly to a rendered
|
|
82
|
+
// line (the overlay paints from screen row 0).
|
|
83
|
+
ui.showOverlay(selector, {
|
|
84
|
+
anchor: "top-left",
|
|
85
|
+
width: "100%",
|
|
86
|
+
maxHeight: "100%",
|
|
87
|
+
margin: 0,
|
|
88
|
+
fullscreen: true,
|
|
89
|
+
});
|
|
76
90
|
ui.setFocus(selector);
|
|
77
91
|
ui.start();
|
|
78
92
|
return promise;
|
package/src/cli/usage-cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type UsageReport,
|
|
16
16
|
type UsageUnit,
|
|
17
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { formatDuration, formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import chalk from "chalk";
|
|
20
20
|
import { ModelRegistry } from "../config/model-registry";
|
|
21
21
|
import { discoverAuthStorage } from "../sdk";
|
|
@@ -450,6 +450,10 @@ export function formatUsageBreakdown(
|
|
|
450
450
|
lines.push(
|
|
451
451
|
`${chalk.bold.cyan(formatProviderName(provider))} ${chalk.dim(`— ${accountCount} ${accountCount === 1 ? "account" : "accounts"}`)}`,
|
|
452
452
|
);
|
|
453
|
+
// Provider-wide disclaimers render once per provider, not per limit.
|
|
454
|
+
const providerNotes = [...new Set(providerReports.flatMap(report => report.notes ?? []))];
|
|
455
|
+
for (const note of providerNotes)
|
|
456
|
+
lines.push(` ${chalk.dim(sanitizeText(note.replace(/[\r\n]+/g, " ").replace(/\t/g, " ")))}`);
|
|
453
457
|
|
|
454
458
|
const labelWidth = providerReports
|
|
455
459
|
.flatMap(report => report.limits)
|
package/src/commands/launch.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* Root command for the coding agent CLI.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
|
|
6
5
|
import { APP_NAME } from "@oh-my-pi/pi-utils";
|
|
7
6
|
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
8
7
|
import { parseArgs } from "../cli/args";
|
|
9
8
|
import { runRootCommand } from "../main";
|
|
10
9
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
10
|
+
import { CLI_THINKING_LEVELS } from "../thinking";
|
|
11
11
|
|
|
12
12
|
export default class Index extends Command {
|
|
13
13
|
static description = "AI coding assistant";
|
|
@@ -100,8 +100,8 @@ export default class Index extends Command {
|
|
|
100
100
|
description: "Comma-separated list of tools to enable (default: all)",
|
|
101
101
|
}),
|
|
102
102
|
thinking: Flags.string({
|
|
103
|
-
description: `Set thinking level: ${
|
|
104
|
-
options: [...
|
|
103
|
+
description: `Set thinking level: ${CLI_THINKING_LEVELS.join(", ")}`,
|
|
104
|
+
options: [...CLI_THINKING_LEVELS],
|
|
105
105
|
}),
|
|
106
106
|
"hide-thinking": Flags.boolean({
|
|
107
107
|
description: "Hide thinking blocks in TUI output (display only, does not disable model thinking)",
|
|
@@ -117,6 +117,11 @@ type LlamaCppDiscoveredServerMetadata = {
|
|
|
117
117
|
input?: ("text" | "image")[];
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
+
type LlamaCppModelListEntry = {
|
|
121
|
+
id: string;
|
|
122
|
+
contextWindow?: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
120
125
|
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
121
126
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
122
127
|
return value;
|
|
@@ -162,6 +167,26 @@ function extractLlamaCppContextWindow(payload: Record<string, unknown>): number
|
|
|
162
167
|
return toPositiveNumberOrUndefined(payload.n_ctx);
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
function extractLlamaCppModelContextWindow(item: Record<string, unknown>): number | undefined {
|
|
171
|
+
const meta = item.meta;
|
|
172
|
+
if (!isRecord(meta)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return toPositiveNumberOrUndefined(meta.n_ctx) ?? toPositiveNumberOrUndefined(meta.n_ctx_train);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseLlamaCppModelList(payload: unknown): LlamaCppModelListEntry[] {
|
|
179
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
return payload.data.flatMap(item => {
|
|
183
|
+
if (!isRecord(item) || typeof item.id !== "string" || !item.id) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
return [{ id: item.id, contextWindow: extractLlamaCppModelContextWindow(item) }];
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
function extractLlamaCppInputCapabilities(payload: Record<string, unknown>): ("text" | "image")[] | undefined {
|
|
166
191
|
const modalities = payload.modalities;
|
|
167
192
|
if (!isRecord(modalities)) {
|
|
@@ -338,12 +363,13 @@ export async function discoverLlamaCppModels(
|
|
|
338
363
|
const [response, serverMetadata] = apiKey
|
|
339
364
|
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
340
365
|
: await attempt(baseHeaders);
|
|
341
|
-
const payload = (await response.json()) as
|
|
342
|
-
const models = payload
|
|
366
|
+
const payload = (await response.json()) as unknown;
|
|
367
|
+
const models = parseLlamaCppModelList(payload);
|
|
343
368
|
const discovered: Model<Api>[] = [];
|
|
344
369
|
for (const item of models) {
|
|
345
|
-
const id = item
|
|
370
|
+
const { id } = item;
|
|
346
371
|
if (!id) continue;
|
|
372
|
+
const contextWindow = item.contextWindow ?? serverMetadata?.contextWindow ?? 128000;
|
|
347
373
|
discovered.push(
|
|
348
374
|
buildModel({
|
|
349
375
|
id,
|
|
@@ -355,11 +381,8 @@ export async function discoverLlamaCppModels(
|
|
|
355
381
|
input: serverMetadata?.input ?? ["text"],
|
|
356
382
|
imageInputDecoder: "stb",
|
|
357
383
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
358
|
-
contextWindow
|
|
359
|
-
maxTokens: Math.min(
|
|
360
|
-
serverMetadata?.contextWindow ?? Number.POSITIVE_INFINITY,
|
|
361
|
-
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
362
|
-
),
|
|
384
|
+
contextWindow,
|
|
385
|
+
maxTokens: Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
363
386
|
headers,
|
|
364
387
|
compat: {
|
|
365
388
|
supportsStore: false,
|
|
@@ -372,6 +395,34 @@ export async function discoverLlamaCppModels(
|
|
|
372
395
|
return discovered;
|
|
373
396
|
}
|
|
374
397
|
|
|
398
|
+
export async function discoverLlamaCppModelContextWindow(
|
|
399
|
+
model: Pick<Model<Api>, "provider" | "id" | "baseUrl" | "headers">,
|
|
400
|
+
ctx: DiscoveryContext,
|
|
401
|
+
): Promise<number | undefined> {
|
|
402
|
+
const baseUrl = normalizeLlamaCppBaseUrl(model.baseUrl);
|
|
403
|
+
const modelsUrl = `${baseUrl}/models`;
|
|
404
|
+
const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
|
|
405
|
+
const attempt = async (headers: Record<string, string>) => {
|
|
406
|
+
const response = await ctx.fetch(modelsUrl, {
|
|
407
|
+
headers,
|
|
408
|
+
signal: AbortSignal.timeout(250),
|
|
409
|
+
});
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const entries = parseLlamaCppModelList(await response.json());
|
|
414
|
+
return entries.find(entry => entry.id === model.id)?.contextWindow;
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
const apiKey = await ctx.getBearerApiKeyResolver(model.provider);
|
|
418
|
+
return apiKey
|
|
419
|
+
? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
|
|
420
|
+
: await attempt(baseHeaders);
|
|
421
|
+
} catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
375
426
|
export async function discoverOpenAIModelsList(
|
|
376
427
|
providerConfig: DiscoveryProviderConfig,
|
|
377
428
|
ctx: DiscoveryContext,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
DISCOVERY_DEFAULT_MAX_TOKENS,
|
|
68
68
|
type DiscoveryContext,
|
|
69
69
|
type DiscoveryProviderConfig,
|
|
70
|
+
discoverLlamaCppModelContextWindow,
|
|
70
71
|
discoverModelsByProviderType,
|
|
71
72
|
getImplicitOllamaBaseUrl,
|
|
72
73
|
getOllamaContextLengthOverride,
|
|
@@ -759,6 +760,50 @@ export class ModelRegistry {
|
|
|
759
760
|
}
|
|
760
761
|
}
|
|
761
762
|
|
|
763
|
+
/**
|
|
764
|
+
* Refresh dynamic metadata that can appear only after a local model loads.
|
|
765
|
+
*/
|
|
766
|
+
async refreshSelectedModelMetadata(model: Model<Api>): Promise<Model<Api>> {
|
|
767
|
+
const isLlamaCppDiscovery = this.#discoverableProviders.some(
|
|
768
|
+
providerConfig => providerConfig.provider === model.provider && providerConfig.discovery.type === "llama.cpp",
|
|
769
|
+
);
|
|
770
|
+
if (!isLlamaCppDiscovery) {
|
|
771
|
+
return model;
|
|
772
|
+
}
|
|
773
|
+
const contextWindow = await discoverLlamaCppModelContextWindow(model, this.#nonResolvingDiscoveryContext());
|
|
774
|
+
if (contextWindow === undefined) {
|
|
775
|
+
return this.find(model.provider, model.id) ?? model;
|
|
776
|
+
}
|
|
777
|
+
const current = this.find(model.provider, model.id) ?? model;
|
|
778
|
+
const override = this.#resolveLiveModelOverride(current);
|
|
779
|
+
const customModel = this.#resolveLiveCustomModelOverlay(current);
|
|
780
|
+
const patch: ModelPatch = {};
|
|
781
|
+
if (
|
|
782
|
+
override?.contextWindow === undefined &&
|
|
783
|
+
customModel?.contextWindow === undefined &&
|
|
784
|
+
current.contextWindow !== contextWindow
|
|
785
|
+
) {
|
|
786
|
+
patch.contextWindow = contextWindow;
|
|
787
|
+
}
|
|
788
|
+
const maxTokens = Math.min(contextWindow, DISCOVERY_DEFAULT_MAX_TOKENS);
|
|
789
|
+
if (
|
|
790
|
+
override?.maxTokens === undefined &&
|
|
791
|
+
customModel?.maxTokens === undefined &&
|
|
792
|
+
current.maxTokens !== maxTokens
|
|
793
|
+
) {
|
|
794
|
+
patch.maxTokens = maxTokens;
|
|
795
|
+
}
|
|
796
|
+
if (patch.contextWindow === undefined && patch.maxTokens === undefined) {
|
|
797
|
+
return current;
|
|
798
|
+
}
|
|
799
|
+
const patched = applyModelPatch(current, patch, "merge");
|
|
800
|
+
this.#models = this.#models.map(candidate =>
|
|
801
|
+
candidate.provider === current.provider && candidate.id === current.id ? patched : candidate,
|
|
802
|
+
);
|
|
803
|
+
this.#rebuildCanonicalIndex();
|
|
804
|
+
return patched;
|
|
805
|
+
}
|
|
806
|
+
|
|
762
807
|
/**
|
|
763
808
|
* Discover models for providers registered at runtime via `fetchDynamicModels`
|
|
764
809
|
* (extension providers). Merges the discovered catalog into the existing model
|
|
@@ -1008,7 +1053,7 @@ export class ModelRegistry {
|
|
|
1008
1053
|
provider: providerConfig.provider,
|
|
1009
1054
|
status: "cached",
|
|
1010
1055
|
optional: providerConfig.optional ?? false,
|
|
1011
|
-
stale: !cache.fresh || !cache.authoritative || configStale,
|
|
1056
|
+
stale: providerConfig.discovery.type === "llama.cpp" || !cache.fresh || !cache.authoritative || configStale,
|
|
1012
1057
|
fetchedAt: cache.updatedAt,
|
|
1013
1058
|
models: models.map(model => model.id),
|
|
1014
1059
|
});
|
|
@@ -1272,7 +1317,9 @@ export class ModelRegistry {
|
|
|
1272
1317
|
const cacheProviderId = this.#configuredDiscoveryCacheProviderId(providerConfig);
|
|
1273
1318
|
const cached = readModelCache<Api>(cacheProviderId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1274
1319
|
const cacheOlderThanConfig = cached !== null && this.#isDiscoveryCacheOlderThanModelsConfig(cached.updatedAt);
|
|
1275
|
-
const
|
|
1320
|
+
const bypassFreshCache = providerConfig.discovery.type === "llama.cpp" && strategy === "online-if-uncached";
|
|
1321
|
+
const effectiveStrategy =
|
|
1322
|
+
strategy === "online-if-uncached" && (cacheOlderThanConfig || bypassFreshCache) ? "online" : strategy;
|
|
1276
1323
|
const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
|
|
1277
1324
|
if (requiresAuth) {
|
|
1278
1325
|
const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
|
|
@@ -1335,7 +1382,7 @@ export class ModelRegistry {
|
|
|
1335
1382
|
provider: providerId,
|
|
1336
1383
|
status,
|
|
1337
1384
|
optional: providerConfig.optional ?? false,
|
|
1338
|
-
stale: result.stale || status === "cached" || (cacheOlderThanConfig && status !== "ok"),
|
|
1385
|
+
stale: result.stale || status === "cached" || ((cacheOlderThanConfig || bypassFreshCache) && status !== "ok"),
|
|
1339
1386
|
fetchedAt: discoveryError ? cached?.updatedAt : Date.now(),
|
|
1340
1387
|
models: result.models.map(model => model.id),
|
|
1341
1388
|
error: discoveryError,
|
|
@@ -1365,6 +1412,13 @@ export class ModelRegistry {
|
|
|
1365
1412
|
};
|
|
1366
1413
|
}
|
|
1367
1414
|
|
|
1415
|
+
#nonResolvingDiscoveryContext(): DiscoveryContext {
|
|
1416
|
+
return {
|
|
1417
|
+
fetch: this.#fetch,
|
|
1418
|
+
getBearerApiKeyResolver: async () => undefined,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1368
1422
|
#warnProviderDiscoveryFailure(providerConfig: DiscoveryProviderConfig, error: string): void {
|
|
1369
1423
|
const previous = this.#lastDiscoveryWarnings.get(providerConfig.provider);
|
|
1370
1424
|
if (previous === error) {
|
|
@@ -1574,6 +1628,23 @@ export class ModelRegistry {
|
|
|
1574
1628
|
return this.#applyProviderTransportOverride(model, override);
|
|
1575
1629
|
});
|
|
1576
1630
|
}
|
|
1631
|
+
#resolveLiveModelOverride(model: Model<Api>): ModelOverride | undefined {
|
|
1632
|
+
const providerOverrides = this.#modelOverrides.get(model.provider);
|
|
1633
|
+
if (!providerOverrides) return undefined;
|
|
1634
|
+
return resolveModelOverrideWithAliases(
|
|
1635
|
+
providerOverrides,
|
|
1636
|
+
model,
|
|
1637
|
+
(provider, id) => this.find(provider, id) !== undefined,
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
#resolveLiveCustomModelOverlay(model: Model<Api>): CustomModelOverlay | undefined {
|
|
1642
|
+
return (
|
|
1643
|
+
this.#customModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id) ??
|
|
1644
|
+
this.#runtimeModelOverlays.find(overlay => overlay.provider === model.provider && overlay.id === model.id)
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1577
1648
|
#applyModelOverrides(models: Model<Api>[], overrides: Map<string, Map<string, ModelOverride>>): Model<Api>[] {
|
|
1578
1649
|
if (overrides.size === 0) return models;
|
|
1579
1650
|
let liveKeys: Set<string> | null = null;
|
|
@@ -3572,7 +3572,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
3572
3572
|
group: "Discovery & MCP",
|
|
3573
3573
|
label: "Essential Tools Override",
|
|
3574
3574
|
description:
|
|
3575
|
-
"Override the always-loaded built-in tools (default: read, bash, edit). Leave empty to use defaults.",
|
|
3575
|
+
"Override the always-loaded built-in tools (default: read, bash, edit, write, find, eval). Leave empty to use defaults.",
|
|
3576
3576
|
},
|
|
3577
3577
|
},
|
|
3578
3578
|
|
|
@@ -4479,6 +4479,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
4479
4479
|
},
|
|
4480
4480
|
},
|
|
4481
4481
|
|
|
4482
|
+
"exa.searchDelayMs": {
|
|
4483
|
+
type: "number",
|
|
4484
|
+
default: 1_000,
|
|
4485
|
+
ui: {
|
|
4486
|
+
tab: "providers",
|
|
4487
|
+
group: "Services",
|
|
4488
|
+
label: "Exa Search Delay",
|
|
4489
|
+
description: "Minimum delay between Exa web search requests in milliseconds; set 0 to disable pacing",
|
|
4490
|
+
},
|
|
4491
|
+
},
|
|
4492
|
+
|
|
4482
4493
|
"exa.enableResearcher": {
|
|
4483
4494
|
type: "boolean",
|
|
4484
4495
|
default: false,
|
|
@@ -4785,6 +4796,7 @@ export interface TtsrSettings {
|
|
|
4785
4796
|
export interface ExaSettings {
|
|
4786
4797
|
enabled: boolean;
|
|
4787
4798
|
enableSearch: boolean;
|
|
4799
|
+
searchDelayMs: number;
|
|
4788
4800
|
enableResearcher: boolean;
|
|
4789
4801
|
enableWebsets: boolean;
|
|
4790
4802
|
}
|
|
@@ -180,9 +180,7 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
|
|
|
180
180
|
async function listInstalledPluginRoots(ctx: LoadContext): Promise<InjectedRoot[]> {
|
|
181
181
|
try {
|
|
182
182
|
const plugins = await getEnabledPlugins(ctx.cwd, { home: ctx.home });
|
|
183
|
-
|
|
184
|
-
// honored by `getEnabledPlugins` via `loadProjectOverrides`.
|
|
185
|
-
return plugins.map(({ path: p }) => ({ path: p, level: "user" }));
|
|
183
|
+
return plugins.map(({ path: p, scope }) => ({ path: p, level: scope }));
|
|
186
184
|
} catch (err) {
|
|
187
185
|
logger.debug("listInstalledPluginRoots: enumeration failed", { error: String(err) });
|
|
188
186
|
return [];
|
package/src/edit/renderer.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
invalidateRenderedStringCache,
|
|
23
23
|
type LspBatchRequest,
|
|
24
24
|
PREVIEW_LIMITS,
|
|
25
|
+
previewWindowRows,
|
|
25
26
|
type RenderedStringCache,
|
|
26
27
|
replaceTabs,
|
|
27
28
|
shortenPath,
|
|
@@ -340,6 +341,7 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, _filePath?: string
|
|
|
340
341
|
function formatStreamingDiff(
|
|
341
342
|
diff: string,
|
|
342
343
|
rawPath: string,
|
|
344
|
+
width: number,
|
|
343
345
|
uiTheme: Theme,
|
|
344
346
|
expanded: boolean,
|
|
345
347
|
label = "streaming",
|
|
@@ -347,15 +349,32 @@ function formatStreamingDiff(
|
|
|
347
349
|
cache?: RenderedStringCache,
|
|
348
350
|
): string {
|
|
349
351
|
if (!diff) return "";
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
// Clamp the collapsed tail to the viewport so a tall or fast-growing diff
|
|
353
|
+
// cannot outgrow the live window. Otherwise its mutating tail scrolls above
|
|
354
|
+
// the native-scrollback commit boundary and the engine re-commits a fresh
|
|
355
|
+
// snapshot every streamed frame, stacking duplicate "… more lines above"
|
|
356
|
+
// previews in history. The budget is VISUAL rows (a long wrapped line counts
|
|
357
|
+
// for more than one) at the framed block's inner width (border only —
|
|
358
|
+
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so the
|
|
359
|
+
// cheap raw-line wrap walk keeps the per-chunk cost bounded. innerWidth/budget
|
|
360
|
+
// are in the cache salt so a resize re-slices.
|
|
361
|
+
const innerWidth = Math.max(1, width - 2);
|
|
362
|
+
const budget = expanded ? Number.POSITIVE_INFINITY : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
|
|
363
|
+
let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
|
|
364
|
+
// "Cursor" tail window: pin the last rows to the bottom so freshly streamed
|
|
365
|
+
// changes stay on screen. The whole-file diff is recomputed every chunk and
|
|
366
|
+
// its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
367
|
+
// window stutters as rows move between hunks. Expanded lifts the cap.
|
|
357
368
|
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
|
358
|
-
|
|
369
|
+
let visualUsed = 0;
|
|
370
|
+
let cut = allLines.length;
|
|
371
|
+
for (let i = allLines.length - 1; i >= 0; i--) {
|
|
372
|
+
const lineRows = Math.max(1, wrapTextWithAnsi(replaceTabs(allLines[i]!), innerWidth).length);
|
|
373
|
+
if (visualUsed + lineRows > budget && visualUsed > 0) break;
|
|
374
|
+
visualUsed += lineRows;
|
|
375
|
+
cut = i;
|
|
376
|
+
}
|
|
377
|
+
const hiddenLines = cut;
|
|
359
378
|
const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
|
|
360
379
|
let rendered = "\n\n";
|
|
361
380
|
if (hiddenLines > 0) {
|
|
@@ -384,6 +403,7 @@ function formatStreamingDiff(
|
|
|
384
403
|
|
|
385
404
|
function formatMultiFileStreamingDiff(
|
|
386
405
|
previews: PerFileDiffPreview[],
|
|
406
|
+
width: number,
|
|
387
407
|
uiTheme: Theme,
|
|
388
408
|
expanded: boolean,
|
|
389
409
|
spinnerFrame?: number,
|
|
@@ -405,7 +425,7 @@ function formatMultiFileStreamingDiff(
|
|
|
405
425
|
const isLast = index === previews.length - 1;
|
|
406
426
|
const cache = previewCacheAt(caches, index);
|
|
407
427
|
parts.push(
|
|
408
|
-
`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
|
|
428
|
+
`${header}${formatStreamingDiff(preview.diff, preview.path, width, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
|
|
409
429
|
);
|
|
410
430
|
}
|
|
411
431
|
}
|
|
@@ -415,6 +435,7 @@ function formatMultiFileStreamingDiff(
|
|
|
415
435
|
function getCallPreview(
|
|
416
436
|
args: EditRenderArgs,
|
|
417
437
|
rawPath: string,
|
|
438
|
+
width: number,
|
|
418
439
|
uiTheme: Theme,
|
|
419
440
|
renderContext: EditRenderContext | undefined,
|
|
420
441
|
expanded: boolean,
|
|
@@ -423,14 +444,14 @@ function getCallPreview(
|
|
|
423
444
|
): string {
|
|
424
445
|
const multi = renderContext?.perFileDiffPreview;
|
|
425
446
|
if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
|
|
426
|
-
return formatMultiFileStreamingDiff(multi, uiTheme, expanded, spinnerFrame, caches);
|
|
447
|
+
return formatMultiFileStreamingDiff(multi, width, uiTheme, expanded, spinnerFrame, caches);
|
|
427
448
|
}
|
|
428
449
|
const cache = previewCacheAt(caches, 0);
|
|
429
450
|
if (args.previewDiff) {
|
|
430
|
-
return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview", spinnerFrame, cache);
|
|
451
|
+
return formatStreamingDiff(args.previewDiff, rawPath, width, uiTheme, expanded, "preview", spinnerFrame, cache);
|
|
431
452
|
}
|
|
432
453
|
if (args.diff && args.op) {
|
|
433
|
-
return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded, "streaming", spinnerFrame, cache);
|
|
454
|
+
return formatStreamingDiff(args.diff, rawPath, width, uiTheme, expanded, "streaming", spinnerFrame, cache);
|
|
434
455
|
}
|
|
435
456
|
if (args.diff) {
|
|
436
457
|
return renderPlainTextPreview(args.diff, uiTheme, rawPath);
|
|
@@ -628,6 +649,7 @@ export const editToolRenderer = {
|
|
|
628
649
|
let body = getCallPreview(
|
|
629
650
|
editArgs,
|
|
630
651
|
rawPath,
|
|
652
|
+
width,
|
|
631
653
|
uiTheme,
|
|
632
654
|
renderContext,
|
|
633
655
|
options.expanded,
|