@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3
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 +91 -0
- package/dist/cli.js +3855 -3798
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +24 -0
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/utils/git.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli.ts +27 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +21 -0
- package/src/cursor.ts +1 -1
- package/src/dap/client.ts +134 -36
- package/src/discovery/helpers.ts +8 -0
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/irc/bus.ts +5 -4
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +119 -20
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +23 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +329 -79
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-history-format.ts +6 -2
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +46 -9
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +39 -37
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +23 -6
- package/src/tools/renderers.ts +24 -0
- package/src/tools/ssh.ts +25 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/git.ts +8 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
package/src/cli/update-cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { $which, APP_NAME, isEnoent, VERSION } from "@oh-my-pi/pi-utils";
|
|
|
12
12
|
import { $ } from "bun";
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
import { theme } from "../modes/theme/theme";
|
|
15
|
+
import { isTimeoutError, withTimeoutSignal } from "../utils/fetch-timeout";
|
|
15
16
|
|
|
16
17
|
const REPO = "can1357/oh-my-pi";
|
|
17
18
|
const PACKAGE = "@oh-my-pi/pi-coding-agent";
|
|
@@ -29,6 +30,8 @@ const MISE_TOOL = "github:can1357/oh-my-pi";
|
|
|
29
30
|
* See #1686.
|
|
30
31
|
*/
|
|
31
32
|
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
33
|
+
const RELEASE_METADATA_TIMEOUT_MS = 30_000;
|
|
34
|
+
const BINARY_DOWNLOAD_TIMEOUT_MS = 15 * 60_000;
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Core native addon package. Bumped in lock-step with {@link PACKAGE} so the
|
|
@@ -82,7 +85,7 @@ export interface BinaryReplacementOptions {
|
|
|
82
85
|
* Parse update subcommand arguments.
|
|
83
86
|
* Returns undefined if not an update command.
|
|
84
87
|
*/
|
|
85
|
-
export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean } | undefined {
|
|
88
|
+
export function parseUpdateArgs(args: string[]): { force: boolean; check: boolean; plugins: boolean } | undefined {
|
|
86
89
|
if (args.length === 0 || args[0] !== "update") {
|
|
87
90
|
return undefined;
|
|
88
91
|
}
|
|
@@ -90,6 +93,7 @@ export function parseUpdateArgs(args: string[]): { force: boolean; check: boolea
|
|
|
90
93
|
return {
|
|
91
94
|
force: args.includes("--force") || args.includes("-f"),
|
|
92
95
|
check: args.includes("--check") || args.includes("-c"),
|
|
96
|
+
plugins: args.includes("--plugins") || args.includes("-l"),
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -239,7 +243,17 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
239
243
|
* Uses npm instead of GitHub API to avoid unauthenticated rate limiting.
|
|
240
244
|
*/
|
|
241
245
|
async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
242
|
-
|
|
246
|
+
let response: Response;
|
|
247
|
+
try {
|
|
248
|
+
response = await fetch(`${NPM_REGISTRY}${PACKAGE}/latest`, {
|
|
249
|
+
signal: withTimeoutSignal(RELEASE_METADATA_TIMEOUT_MS),
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
if (isTimeoutError(err)) {
|
|
253
|
+
throw new Error("Timed out fetching release info after 30s", { cause: err });
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
243
257
|
if (!response.ok) {
|
|
244
258
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
|
245
259
|
}
|
|
@@ -832,7 +846,18 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
832
846
|
const backupPath = `${targetPath}.${Date.now()}.${process.pid}.bak`;
|
|
833
847
|
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
834
848
|
|
|
835
|
-
|
|
849
|
+
let response: Response;
|
|
850
|
+
try {
|
|
851
|
+
response = await fetch(url, {
|
|
852
|
+
redirect: "follow",
|
|
853
|
+
signal: withTimeoutSignal(BINARY_DOWNLOAD_TIMEOUT_MS),
|
|
854
|
+
});
|
|
855
|
+
} catch (err) {
|
|
856
|
+
if (isTimeoutError(err)) {
|
|
857
|
+
throw new Error("Timed out downloading release binary after 15 minutes", { cause: err });
|
|
858
|
+
}
|
|
859
|
+
throw err;
|
|
860
|
+
}
|
|
836
861
|
if (!response.ok || !response.body) {
|
|
837
862
|
throw new Error(`Download failed: ${response.statusText}`);
|
|
838
863
|
}
|
|
@@ -914,12 +939,14 @@ ${chalk.bold("Usage:")}
|
|
|
914
939
|
${APP_NAME} update [options]
|
|
915
940
|
|
|
916
941
|
${chalk.bold("Options:")}
|
|
917
|
-
-c, --check
|
|
918
|
-
-f, --force
|
|
942
|
+
-c, --check Check for updates without installing
|
|
943
|
+
-f, --force Force reinstall even if up to date
|
|
944
|
+
-l, --plugins Update installed plugins
|
|
919
945
|
|
|
920
946
|
${chalk.bold("Examples:")}
|
|
921
|
-
${APP_NAME} update
|
|
922
|
-
${APP_NAME} update --check
|
|
923
|
-
${APP_NAME} update --force
|
|
947
|
+
${APP_NAME} update Update to latest version
|
|
948
|
+
${APP_NAME} update --check Check if updates are available
|
|
949
|
+
${APP_NAME} update --force Force reinstall
|
|
950
|
+
${APP_NAME} update -l Update installed plugins
|
|
924
951
|
`);
|
|
925
952
|
}
|
package/src/cli.ts
CHANGED
|
@@ -173,14 +173,20 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
173
173
|
* worker is idle, and hard-kills the process on parent `disconnect`.
|
|
174
174
|
*/
|
|
175
175
|
async function runIpcSubprocessWorker<In, Out>(
|
|
176
|
-
start: (transport: {
|
|
176
|
+
start: (transport: {
|
|
177
|
+
send(message: Out): void;
|
|
178
|
+
sendAndFlush(message: Out): Promise<void>;
|
|
179
|
+
onMessage(handler: (message: In) => void): () => void;
|
|
180
|
+
}) => void,
|
|
177
181
|
): Promise<void> {
|
|
178
182
|
const { promise: shuttingDown, resolve: shutdown } = Promise.withResolvers<void>();
|
|
183
|
+
type IpcSend = (this: NodeJS.Process, message: unknown, callback?: (error: Error | null) => void) => boolean;
|
|
184
|
+
// `process.send` only exists when spawned with an IPC channel; the parent
|
|
185
|
+
// always spawns us that way. If it's missing, the parent vanished and
|
|
186
|
+
// there's no one to talk to.
|
|
187
|
+
const ipcSend = (): IpcSend | undefined => (process as NodeJS.Process & { send?: IpcSend }).send;
|
|
179
188
|
const send = (message: Out): void => {
|
|
180
|
-
|
|
181
|
-
// parent always spawns us that way. If it's missing, the parent
|
|
182
|
-
// vanished and there's no one to talk to.
|
|
183
|
-
const sender = (process as NodeJS.Process & { send?: (m: unknown) => boolean }).send;
|
|
189
|
+
const sender = ipcSend();
|
|
184
190
|
if (!sender) {
|
|
185
191
|
shutdown();
|
|
186
192
|
return;
|
|
@@ -191,8 +197,24 @@ async function runIpcSubprocessWorker<In, Out>(
|
|
|
191
197
|
shutdown();
|
|
192
198
|
}
|
|
193
199
|
};
|
|
200
|
+
const sendAndFlush = (message: Out): Promise<void> => {
|
|
201
|
+
const sender = ipcSend();
|
|
202
|
+
if (!sender) {
|
|
203
|
+
shutdown();
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
}
|
|
206
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
207
|
+
try {
|
|
208
|
+
sender.call(process, message, () => resolve());
|
|
209
|
+
} catch {
|
|
210
|
+
shutdown();
|
|
211
|
+
resolve();
|
|
212
|
+
}
|
|
213
|
+
return promise;
|
|
214
|
+
};
|
|
194
215
|
start({
|
|
195
216
|
send,
|
|
217
|
+
sendAndFlush,
|
|
196
218
|
onMessage(handler) {
|
|
197
219
|
const wrap = (data: unknown): void => handler(data as In);
|
|
198
220
|
process.on("message", wrap);
|
package/src/commands/update.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Check for and install updates.
|
|
3
3
|
*/
|
|
4
4
|
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
5
|
-
import
|
|
5
|
+
import * as pluginCli from "../cli/plugin-cli";
|
|
6
|
+
import * as updateCli from "../cli/update-cli";
|
|
6
7
|
import { initTheme } from "../modes/theme/theme";
|
|
7
8
|
|
|
8
9
|
export default class Update extends Command {
|
|
@@ -11,11 +12,16 @@ export default class Update extends Command {
|
|
|
11
12
|
static flags = {
|
|
12
13
|
force: Flags.boolean({ char: "f", description: "Force update", default: false }),
|
|
13
14
|
check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }),
|
|
15
|
+
plugins: Flags.boolean({ char: "l", description: "Update installed plugins", default: false }),
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
async run(): Promise<void> {
|
|
17
19
|
const { flags } = await this.parse(Update);
|
|
18
20
|
await initTheme();
|
|
19
|
-
|
|
21
|
+
if (flags.plugins) {
|
|
22
|
+
await pluginCli.runPluginCommand({ action: "upgrade", args: [], flags: {} });
|
|
23
|
+
} else {
|
|
24
|
+
await updateCli.runUpdateCommand({ force: flags.force, check: flags.check });
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import type
|
|
4
|
+
import { ArkErrors, type Type } from "arktype";
|
|
5
5
|
import { JSONC, YAML } from "bun";
|
|
6
6
|
|
|
7
7
|
/** Minimal subset of the AJV ConfigSchemaError shape this module actually relies on. */
|
|
@@ -220,11 +220,11 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const checked = this.schema(parsed);
|
|
223
|
-
if (checked instanceof
|
|
224
|
-
const schemaErrors: ConfigSchemaError[] =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
if (checked instanceof ArkErrors) {
|
|
224
|
+
const schemaErrors: ConfigSchemaError[] = checked.map(error => ({
|
|
225
|
+
instancePath: error.path.length === 0 ? "root" : error.path.join("."),
|
|
226
|
+
message: error.problem,
|
|
227
|
+
}));
|
|
228
228
|
const error = new ConfigError(this.id, schemaErrors);
|
|
229
229
|
logger.warn("Failed to parse config file", { path: this.path(), error });
|
|
230
230
|
return this.#storeCache({ error, status: "error" });
|
|
@@ -630,7 +630,8 @@ function matchModel(
|
|
|
630
630
|
// fuzzy matching so raw IDs that contain slashes (for example OpenRouter model
|
|
631
631
|
// IDs like "openai/gpt-4o:extended") still resolve as IDs instead of being
|
|
632
632
|
// misread as a provider-qualified selector.
|
|
633
|
-
const
|
|
633
|
+
const lowerPattern = modelPattern.toLowerCase();
|
|
634
|
+
const exactMatches = availableModels.filter(m => m.id.toLowerCase() === lowerPattern);
|
|
634
635
|
if (exactMatches.length > 0) {
|
|
635
636
|
return pickPreferredModel(exactMatches, context);
|
|
636
637
|
}
|
|
@@ -647,7 +648,8 @@ function matchModel(
|
|
|
647
648
|
const bareAlias = resolveBareVariantAlias(modelPattern);
|
|
648
649
|
const bareAliasTargetId = bareAlias?.id ?? stripThinkingVariantToken(modelPattern);
|
|
649
650
|
if (bareAliasTargetId) {
|
|
650
|
-
const
|
|
651
|
+
const lowerAliasTarget = bareAliasTargetId.toLowerCase();
|
|
652
|
+
const aliasMatches = availableModels.filter(m => m.id.toLowerCase() === lowerAliasTarget);
|
|
651
653
|
if (aliasMatches.length > 0) {
|
|
652
654
|
const preferred = bareAlias ? aliasMatches.filter(m => bareAlias.providers.includes(m.provider)) : [];
|
|
653
655
|
return pickPreferredModel(preferred.length > 0 ? preferred : aliasMatches, context);
|
|
@@ -658,7 +660,8 @@ function matchModel(
|
|
|
658
660
|
if (slashIndex !== -1) {
|
|
659
661
|
const provider = modelPattern.substring(0, slashIndex);
|
|
660
662
|
const modelId = modelPattern.substring(slashIndex + 1);
|
|
661
|
-
const
|
|
663
|
+
const lowerProvider = provider.toLowerCase();
|
|
664
|
+
const providerModels = availableModels.filter(m => m.provider.toLowerCase() === lowerProvider);
|
|
662
665
|
if (providerModels.length === 0) {
|
|
663
666
|
// The prefix is not a known provider in this candidate set, so treat the
|
|
664
667
|
// slash as part of the raw model ID and continue with generic matching.
|
|
@@ -699,9 +702,7 @@ function matchModel(
|
|
|
699
702
|
|
|
700
703
|
// No exact match - fall back to partial matching
|
|
701
704
|
const matches = availableModels.filter(
|
|
702
|
-
m =>
|
|
703
|
-
m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
704
|
-
m.name?.toLowerCase().includes(modelPattern.toLowerCase()),
|
|
705
|
+
m => m.id.toLowerCase().includes(lowerPattern) || m.name?.toLowerCase().includes(lowerPattern),
|
|
705
706
|
);
|
|
706
707
|
|
|
707
708
|
if (matches.length === 0) {
|
|
@@ -808,13 +809,15 @@ function parseModelPatternWithContext(
|
|
|
808
809
|
return result;
|
|
809
810
|
}
|
|
810
811
|
|
|
811
|
-
|
|
812
|
+
/** Match a single pattern with a pre-built preference context (direct match plus
|
|
813
|
+
* the `@upstream` routing fallback), so role resolution can reuse one context
|
|
814
|
+
* across every fallback pattern instead of rebuilding it per pattern. */
|
|
815
|
+
function matchPatternWithContext(
|
|
812
816
|
pattern: string,
|
|
813
817
|
availableModels: Model<Api>[],
|
|
814
|
-
|
|
818
|
+
context: ModelPreferenceContext,
|
|
815
819
|
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
816
820
|
): ParsedModelResult {
|
|
817
|
-
const context = buildPreferenceContext(availableModels, preferences);
|
|
818
821
|
const direct = parseModelPatternWithContext(pattern, availableModels, context, options);
|
|
819
822
|
if (direct.model) return direct;
|
|
820
823
|
|
|
@@ -831,6 +834,20 @@ export function parseModelPattern(
|
|
|
831
834
|
return direct;
|
|
832
835
|
}
|
|
833
836
|
|
|
837
|
+
export function parseModelPattern(
|
|
838
|
+
pattern: string,
|
|
839
|
+
availableModels: Model<Api>[],
|
|
840
|
+
preferences?: ModelMatchPreferences,
|
|
841
|
+
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
842
|
+
): ParsedModelResult {
|
|
843
|
+
return matchPatternWithContext(
|
|
844
|
+
pattern,
|
|
845
|
+
availableModels,
|
|
846
|
+
buildPreferenceContext(availableModels, preferences),
|
|
847
|
+
options,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
834
851
|
const PREFIX_MODEL_ROLE = "pi/";
|
|
835
852
|
const DEFAULT_MODEL_ROLE = "default";
|
|
836
853
|
|
|
@@ -1035,8 +1052,12 @@ export function resolveModelRoleValue(
|
|
|
1035
1052
|
|
|
1036
1053
|
let warning: string | undefined;
|
|
1037
1054
|
const matchPreferences = mergeModelMatchPreferences(options?.settings, options?.matchPreferences);
|
|
1055
|
+
// Build the O(n) preference context (model-order map over all available
|
|
1056
|
+
// models) once and reuse it across every fallback pattern instead of
|
|
1057
|
+
// rebuilding it per pattern inside parseModelPattern.
|
|
1058
|
+
const preferenceContext = buildPreferenceContext(availableModels, matchPreferences);
|
|
1038
1059
|
for (const effectivePattern of effectivePatterns) {
|
|
1039
|
-
const resolved =
|
|
1060
|
+
const resolved = matchPatternWithContext(effectivePattern, availableModels, preferenceContext);
|
|
1040
1061
|
if (resolved.model) {
|
|
1041
1062
|
return {
|
|
1042
1063
|
model: resolved.model,
|
|
@@ -4337,6 +4337,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
4337
4337
|
description: "Providers that web_search should never use, even as fallbacks",
|
|
4338
4338
|
},
|
|
4339
4339
|
},
|
|
4340
|
+
"providers.webSearchGeminiModel": {
|
|
4341
|
+
type: "string",
|
|
4342
|
+
default: undefined,
|
|
4343
|
+
ui: {
|
|
4344
|
+
tab: "providers",
|
|
4345
|
+
group: "Services",
|
|
4346
|
+
label: "Gemini web_search model",
|
|
4347
|
+
description: "Model ID for Gemini Google Search grounding. Defaults to gemini-2.5-flash.",
|
|
4348
|
+
},
|
|
4349
|
+
},
|
|
4340
4350
|
"providers.antigravityEndpoint": {
|
|
4341
4351
|
type: "enum",
|
|
4342
4352
|
values: ["auto", "production", "sandbox"] as const,
|
|
@@ -4491,6 +4501,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
4491
4501
|
],
|
|
4492
4502
|
},
|
|
4493
4503
|
},
|
|
4504
|
+
"speech.enhanced": {
|
|
4505
|
+
type: "boolean",
|
|
4506
|
+
default: false,
|
|
4507
|
+
ui: {
|
|
4508
|
+
tab: "providers",
|
|
4509
|
+
group: "Services",
|
|
4510
|
+
label: "Enhanced Speech Rewriting",
|
|
4511
|
+
description:
|
|
4512
|
+
"Rewrite assistant output into natural spoken prose with the tiny/smol model before synthesis (describes code, drops links and markdown). Falls back to mechanical cleanup on failure",
|
|
4513
|
+
},
|
|
4514
|
+
},
|
|
4494
4515
|
"speech.voice": {
|
|
4495
4516
|
type: "enum",
|
|
4496
4517
|
values: TTS_LOCAL_VOICE_VALUES,
|
package/src/cursor.ts
CHANGED
|
@@ -180,7 +180,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
180
180
|
const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
|
181
181
|
const toolResultMessage = await executeTool(this.options, "grep", toolCallId, {
|
|
182
182
|
pattern: args.pattern,
|
|
183
|
-
|
|
183
|
+
path: searchPath,
|
|
184
184
|
case: args.caseInsensitive === true ? false : undefined,
|
|
185
185
|
});
|
|
186
186
|
return toolResultMessage;
|
package/src/dap/client.ts
CHANGED
|
@@ -17,6 +17,14 @@ import type {
|
|
|
17
17
|
interface DapSpawnOptions {
|
|
18
18
|
adapter: DapResolvedAdapter;
|
|
19
19
|
cwd: string;
|
|
20
|
+
/**
|
|
21
|
+
* Cap on how long the socket-mode helpers wait for the adapter to open its
|
|
22
|
+
* socket (unix) or dial back into our listener (TCP). Exposed for tests;
|
|
23
|
+
* production callers rely on the default.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
socketReadyTimeoutMs?: number;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
/** Minimal write interface shared by Bun.FileSink and Bun TCP sockets. */
|
|
@@ -29,13 +37,14 @@ type DapEventHandler = (body: unknown, event: DapEventMessage) => void | Promise
|
|
|
29
37
|
type DapReverseRequestHandler = (args: unknown) => unknown | Promise<unknown>;
|
|
30
38
|
|
|
31
39
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Hard cap on a single message write. A wedged adapter stdin used to hang the
|
|
42
|
+
* whole client forever; on hitting this cap the client disposes itself so the
|
|
43
|
+
* next request fails fast instead of piling more work onto a broken adapter.
|
|
44
|
+
*/
|
|
45
|
+
const WRITE_MESSAGE_TIMEOUT_MS = 30_000;
|
|
46
|
+
/** Default wait for socket-mode adapters to become reachable. */
|
|
47
|
+
const SOCKET_READY_TIMEOUT_MS = 10_000;
|
|
39
48
|
|
|
40
49
|
function toErrorMessage(value: unknown): string {
|
|
41
50
|
if (value instanceof Error) return value.message;
|
|
@@ -62,6 +71,8 @@ export class DapClient {
|
|
|
62
71
|
#eventHandlers = new Map<string, Set<DapEventHandler>>();
|
|
63
72
|
#anyEventHandlers = new Set<DapEventHandler>();
|
|
64
73
|
#reverseRequestHandlers = new Map<string, DapReverseRequestHandler>();
|
|
74
|
+
#adapterExited = false;
|
|
75
|
+
#pendingWriteExitRejectors = new Set<() => void>();
|
|
65
76
|
|
|
66
77
|
constructor(
|
|
67
78
|
adapter: DapResolvedAdapter,
|
|
@@ -75,11 +86,15 @@ export class DapClient {
|
|
|
75
86
|
this.#readable = options?.readable ?? (proc.stdout as ReadableStream<Uint8Array>);
|
|
76
87
|
this.#writeSink = options?.writeSink ?? proc.stdin;
|
|
77
88
|
this.#socket = options?.socket;
|
|
89
|
+
this.proc.exited.then(
|
|
90
|
+
() => this.#rejectPendingWritesForExit(),
|
|
91
|
+
() => this.#rejectPendingWritesForExit(),
|
|
92
|
+
);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
static async spawn({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
95
|
+
static async spawn({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
|
|
81
96
|
if (adapter.connectMode === "socket") {
|
|
82
|
-
return DapClient.#spawnSocket({ adapter, cwd });
|
|
97
|
+
return DapClient.#spawnSocket({ adapter, cwd, socketReadyTimeoutMs });
|
|
83
98
|
}
|
|
84
99
|
// Merge non-interactive env and start in a new session (detached → setsid)
|
|
85
100
|
// so the adapter process tree has no controlling terminal. Without this,
|
|
@@ -108,17 +123,18 @@ export class DapClient {
|
|
|
108
123
|
* Linux: connect to a unix domain socket via --listen=unix:<path>
|
|
109
124
|
* macOS/other: the adapter dials into our TCP listener via --client-addr
|
|
110
125
|
*/
|
|
111
|
-
static async #spawnSocket({ adapter, cwd }: DapSpawnOptions): Promise<DapClient> {
|
|
126
|
+
static async #spawnSocket({ adapter, cwd, socketReadyTimeoutMs }: DapSpawnOptions): Promise<DapClient> {
|
|
112
127
|
const env = {
|
|
113
128
|
...Bun.env,
|
|
114
129
|
...NON_INTERACTIVE_ENV,
|
|
115
130
|
};
|
|
131
|
+
const timeoutMs = socketReadyTimeoutMs ?? SOCKET_READY_TIMEOUT_MS;
|
|
116
132
|
const isLinux = process.platform === "linux";
|
|
117
133
|
|
|
118
134
|
if (isLinux) {
|
|
119
|
-
return DapClient.#spawnSocketUnix({ adapter, cwd, env });
|
|
135
|
+
return DapClient.#spawnSocketUnix({ adapter, cwd, env, timeoutMs });
|
|
120
136
|
}
|
|
121
|
-
return DapClient.#spawnSocketClientAddr({ adapter, cwd, env });
|
|
137
|
+
return DapClient.#spawnSocketClientAddr({ adapter, cwd, env, timeoutMs });
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
/** Linux: spawn adapter with --listen=unix:<path>, then connect to the socket. */
|
|
@@ -126,10 +142,12 @@ export class DapClient {
|
|
|
126
142
|
adapter,
|
|
127
143
|
cwd,
|
|
128
144
|
env,
|
|
145
|
+
timeoutMs,
|
|
129
146
|
}: {
|
|
130
147
|
adapter: DapResolvedAdapter;
|
|
131
148
|
cwd: string;
|
|
132
149
|
env: Record<string, string | undefined>;
|
|
150
|
+
timeoutMs: number;
|
|
133
151
|
}): Promise<DapClient> {
|
|
134
152
|
const socketPath = `/tmp/dap-${adapter.name}-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`;
|
|
135
153
|
const proc = ptree.spawn([adapter.resolvedCommand, ...adapter.args, `--listen=unix:${socketPath}`], {
|
|
@@ -139,13 +157,23 @@ export class DapClient {
|
|
|
139
157
|
detached: true,
|
|
140
158
|
});
|
|
141
159
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
// If waitForCondition throws (timeout, or adapter exited early) or the
|
|
161
|
+
// socket connect fails, we must not leak the detached adapter process.
|
|
162
|
+
try {
|
|
163
|
+
await waitForCondition(() => isUnixSocketReady(socketPath), timeoutMs, proc);
|
|
164
|
+
const { readable, writeSink, socket } = await connectSocket({ unix: socketPath });
|
|
165
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
166
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
167
|
+
void client.#startMessageReader();
|
|
168
|
+
return client;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
try {
|
|
171
|
+
proc.kill();
|
|
172
|
+
} catch {
|
|
173
|
+
/* proc may already be dead */
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
/** macOS/other: listen on a random TCP port, spawn adapter with --client-addr, accept connection. */
|
|
@@ -153,10 +181,12 @@ export class DapClient {
|
|
|
153
181
|
adapter,
|
|
154
182
|
cwd,
|
|
155
183
|
env,
|
|
184
|
+
timeoutMs,
|
|
156
185
|
}: {
|
|
157
186
|
adapter: DapResolvedAdapter;
|
|
158
187
|
cwd: string;
|
|
159
188
|
env: Record<string, string | undefined>;
|
|
189
|
+
timeoutMs: number;
|
|
160
190
|
}): Promise<DapClient> {
|
|
161
191
|
const { promise: connPromise, resolve: resolveConn } = Promise.withResolvers<Bun.Socket<undefined>>();
|
|
162
192
|
|
|
@@ -182,25 +212,32 @@ export class DapClient {
|
|
|
182
212
|
detached: true,
|
|
183
213
|
});
|
|
184
214
|
|
|
185
|
-
// Wait for
|
|
186
|
-
|
|
215
|
+
// Wait for the adapter to dial back. On timeout (or any other failure
|
|
216
|
+
// before we've wired up the client) kill `proc` — otherwise the detached
|
|
217
|
+
// adapter process is orphaned.
|
|
187
218
|
const { promise: timeoutPromise, reject: rejectTimeout } = Promise.withResolvers<never>();
|
|
188
219
|
const connectTimeout = setTimeout(
|
|
189
|
-
() => rejectTimeout(new Error(`${adapter.name} did not connect within
|
|
190
|
-
|
|
220
|
+
() => rejectTimeout(new Error(`${adapter.name} did not connect within ${timeoutMs}ms`)),
|
|
221
|
+
timeoutMs,
|
|
191
222
|
);
|
|
192
223
|
try {
|
|
193
|
-
rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
224
|
+
const rawSocket = await Promise.race([connPromise, timeoutPromise]);
|
|
225
|
+
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
226
|
+
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
227
|
+
proc.exited.then(() => client.#handleProcessExit());
|
|
228
|
+
void client.#startMessageReader();
|
|
229
|
+
return client;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
try {
|
|
232
|
+
proc.kill();
|
|
233
|
+
} catch {
|
|
234
|
+
/* proc may already be dead */
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
194
237
|
} finally {
|
|
195
238
|
clearTimeout(connectTimeout);
|
|
196
239
|
server.stop();
|
|
197
240
|
}
|
|
198
|
-
|
|
199
|
-
const { readable, writeSink, socket } = wrapBunSocket(rawSocket);
|
|
200
|
-
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket });
|
|
201
|
-
proc.exited.then(() => client.#handleProcessExit());
|
|
202
|
-
void client.#startMessageReader();
|
|
203
|
-
return client;
|
|
204
241
|
}
|
|
205
242
|
|
|
206
243
|
get capabilities(): DapCapabilities | undefined {
|
|
@@ -309,6 +346,12 @@ export class DapClient {
|
|
|
309
346
|
arguments: args,
|
|
310
347
|
};
|
|
311
348
|
const { promise, resolve, reject } = Promise.withResolvers<TBody>();
|
|
349
|
+
// Suppress "unhandled rejection" if the request timer or abort fires
|
|
350
|
+
// before the caller's `await` subscribes — e.g. while #writeMessage is
|
|
351
|
+
// still racing a wedged stdin flush. The caller's own `await` still
|
|
352
|
+
// receives the rejection normally; this handler is a passive guard.
|
|
353
|
+
promise.catch(() => {});
|
|
354
|
+
|
|
312
355
|
let timeout: NodeJS.Timeout | undefined;
|
|
313
356
|
const cleanup = () => {
|
|
314
357
|
if (timeout) clearTimeout(timeout);
|
|
@@ -342,13 +385,15 @@ export class DapClient {
|
|
|
342
385
|
},
|
|
343
386
|
});
|
|
344
387
|
this.#lastActivity = Date.now();
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
388
|
+
// Fire the write in the background. Awaiting it here would let a wedged
|
|
389
|
+
// stdin flush block the caller's `timeoutMs`; if it fails, propagate the
|
|
390
|
+
// failure into `promise` — the timer or abort may still win the race.
|
|
391
|
+
void this.#writeMessage(request).catch(error => {
|
|
392
|
+
if (!this.#pendingRequests.has(requestSeq)) return;
|
|
348
393
|
this.#pendingRequests.delete(requestSeq);
|
|
349
394
|
cleanup();
|
|
350
|
-
|
|
351
|
-
}
|
|
395
|
+
reject(error);
|
|
396
|
+
});
|
|
352
397
|
return promise;
|
|
353
398
|
}
|
|
354
399
|
|
|
@@ -362,7 +407,60 @@ export class DapClient {
|
|
|
362
407
|
...(message ? { message } : {}),
|
|
363
408
|
...(body !== undefined ? { body } : {}),
|
|
364
409
|
};
|
|
365
|
-
await writeMessage(
|
|
410
|
+
await this.#writeMessage(response);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Framed write to the adapter, bounded by {@link WRITE_MESSAGE_TIMEOUT_MS}
|
|
415
|
+
* and by adapter exit. Without this bound a wedged adapter stdin used to
|
|
416
|
+
* hang the whole client forever. On timeout or exit-before-flush the client
|
|
417
|
+
* disposes itself and rethrows.
|
|
418
|
+
*/
|
|
419
|
+
async #writeMessage(message: DapRequestMessage | DapResponseMessage): Promise<void> {
|
|
420
|
+
const content = JSON.stringify(message);
|
|
421
|
+
this.#writeSink.write(`Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`);
|
|
422
|
+
this.#writeSink.write(content);
|
|
423
|
+
const flushResult = this.#writeSink.flush();
|
|
424
|
+
if (!(flushResult instanceof Promise)) return;
|
|
425
|
+
|
|
426
|
+
if (this.#adapterExited) {
|
|
427
|
+
throw new Error(`DAP adapter ${this.adapter.name} exited before write completed`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const { promise: guardPromise, reject: guardReject, resolve: guardResolve } = Promise.withResolvers<void>();
|
|
431
|
+
const timer = setTimeout(
|
|
432
|
+
() =>
|
|
433
|
+
guardReject(
|
|
434
|
+
new Error(`DAP adapter ${this.adapter.name} write timed out after ${WRITE_MESSAGE_TIMEOUT_MS}ms`),
|
|
435
|
+
),
|
|
436
|
+
WRITE_MESSAGE_TIMEOUT_MS,
|
|
437
|
+
);
|
|
438
|
+
const rejectOnExit = () => {
|
|
439
|
+
guardReject(new Error(`DAP adapter ${this.adapter.name} exited before write completed`));
|
|
440
|
+
};
|
|
441
|
+
this.#pendingWriteExitRejectors.add(rejectOnExit);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await Promise.race([flushResult, guardPromise]);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// The client is now known-broken. Kick off dispose in the background;
|
|
447
|
+
// callers will see subsequent sendRequest calls fail fast.
|
|
448
|
+
void this.dispose();
|
|
449
|
+
throw error;
|
|
450
|
+
} finally {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
this.#pendingWriteExitRejectors.delete(rejectOnExit);
|
|
453
|
+
// Release the guard so any late timeout callback becomes a no-op.
|
|
454
|
+
guardResolve();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#rejectPendingWritesForExit(): void {
|
|
459
|
+
this.#adapterExited = true;
|
|
460
|
+
for (const reject of this.#pendingWriteExitRejectors) {
|
|
461
|
+
reject();
|
|
462
|
+
}
|
|
463
|
+
this.#pendingWriteExitRejectors.clear();
|
|
366
464
|
}
|
|
367
465
|
|
|
368
466
|
async dispose(): Promise<void> {
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -830,6 +830,13 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
|
|
|
830
830
|
|
|
831
831
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
832
832
|
|
|
833
|
+
const pluginCacheInvalidators = new Set<() => void>();
|
|
834
|
+
|
|
835
|
+
/** Register a process-global plugin cache invalidator called whenever plugin roots are cleared. */
|
|
836
|
+
export function registerPluginCacheInvalidator(invalidator: () => void): void {
|
|
837
|
+
pluginCacheInvalidators.add(invalidator);
|
|
838
|
+
}
|
|
839
|
+
|
|
833
840
|
/**
|
|
834
841
|
* List all installed Claude Code plugin roots from the plugin cache.
|
|
835
842
|
* Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
|
|
@@ -1009,6 +1016,7 @@ export async function listClaudePluginRoots(
|
|
|
1009
1016
|
*/
|
|
1010
1017
|
export function clearClaudePluginRootsCache(): void {
|
|
1011
1018
|
pluginRootsCache.clear();
|
|
1019
|
+
for (const invalidate of pluginCacheInvalidators) invalidate();
|
|
1012
1020
|
preloadedPluginRoots = [...injectedPluginDirRoots];
|
|
1013
1021
|
// Re-warm preloaded roots asynchronously so sync LSP config reads stay valid
|
|
1014
1022
|
if (lastPreloadHome) {
|
|
@@ -132,12 +132,23 @@ export function recordSeenLines(
|
|
|
132
132
|
* can reject edits anchored on lines the model never saw. Best-effort: a no-op
|
|
133
133
|
* when the body has no numbered rows or the snapshot already aged out. `tag`
|
|
134
134
|
* must be the tag returned when this exact content was recorded.
|
|
135
|
+
*
|
|
136
|
+
* `excludedLines` prunes 1-indexed line numbers whose displayed text was
|
|
137
|
+
* column-truncated (or otherwise not shown in full). A column-clipped row
|
|
138
|
+
* still carries a `NN:` prefix — the parser sees the number and would
|
|
139
|
+
* otherwise mark the line "seen" even though only its prefix ever reached
|
|
140
|
+
* the model. Producers that apply per-line column truncation MUST supply
|
|
141
|
+
* the clipped line set so the patcher's seen-line guard keeps rejecting
|
|
142
|
+
* edits against those lines until a full-width read of them occurs.
|
|
135
143
|
*/
|
|
136
144
|
export function recordSeenLinesFromBody(
|
|
137
145
|
session: FileSnapshotStoreOwner,
|
|
138
146
|
absolutePath: string,
|
|
139
147
|
tag: string,
|
|
140
148
|
body: string,
|
|
149
|
+
excludedLines?: ReadonlySet<number>,
|
|
141
150
|
): void {
|
|
142
|
-
|
|
151
|
+
const parsed = parseSeenLinesFromHashlineBody(body);
|
|
152
|
+
const filtered = excludedLines && excludedLines.size > 0 ? parsed.filter(line => !excludedLines.has(line)) : parsed;
|
|
153
|
+
recordSeenLines(session, absolutePath, tag, filtered);
|
|
143
154
|
}
|