@oh-my-pi/pi-coding-agent 15.1.6 → 15.1.7
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 +17 -0
- package/dist/types/config/settings-schema.d.ts +15 -7
- package/dist/types/hashline/hash.d.ts +4 -4
- package/dist/types/hashline/recovery.d.ts +5 -0
- package/dist/types/lsp/edits.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/session/client-bridge.d.ts +1 -0
- package/dist/types/tools/find.d.ts +4 -0
- package/dist/types/tools/resolve.d.ts +5 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/package.json +7 -7
- package/src/config/settings-schema.ts +22 -7
- package/src/dap/session.ts +58 -5
- package/src/edit/modes/patch.ts +46 -0
- package/src/eval/js/context-manager.ts +11 -7
- package/src/eval/js/shared/rewrite-imports.ts +21 -9
- package/src/eval/js/shared/runtime.ts +2 -1
- package/src/hashline/hash.ts +11 -8
- package/src/hashline/parser.ts +23 -6
- package/src/hashline/recovery.ts +44 -3
- package/src/lsp/edits.ts +92 -38
- package/src/lsp/index.ts +110 -7
- package/src/lsp/utils.ts +13 -0
- package/src/modes/acp/acp-client-bridge.ts +1 -0
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/prompts/tools/bash.md +14 -0
- package/src/prompts/tools/debug.md +4 -1
- package/src/prompts/tools/find.md +10 -0
- package/src/prompts/tools/hashline.md +5 -3
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +2 -1
- package/src/prompts/tools/task.md +4 -0
- package/src/prompts/tools/todo-write.md +2 -0
- package/src/session/agent-session.ts +116 -8
- package/src/session/client-bridge.ts +1 -0
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/index.ts +33 -5
- package/src/task/render.ts +4 -1
- package/src/tools/browser/tab-supervisor.ts +23 -3
- package/src/tools/browser/tab-worker.ts +4 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +19 -2
- package/src/tools/find.ts +80 -24
- package/src/tools/read.ts +3 -6
- package/src/tools/resolve.ts +54 -22
- package/src/tools/search.ts +31 -0
- package/src/tools/todo-write.ts +11 -4
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/tools-manager.ts +29 -22
- package/src/web/search/providers/codex.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.7] - 2026-05-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `debug` launch/attach failures so `configurationDone` no longer masks the underlying DAP launch error, early stop-outcome watchers cannot emit unhandled rejections, and directory-valued launch programs are rejected before adapter selection. ([#1187](https://github.com/can1357/oh-my-pi/issues/1187))
|
|
10
|
+
- Fixed hashline edit payloads that use a readability space after `~` by warning on separator-padding-shaped payload blocks and tightening the model prompt. ([#1166](https://github.com/can1357/oh-my-pi/issues/1166))
|
|
11
|
+
- Fixed ACP bash permission requests to include execute tool metadata and command content so clients can render command approval prompts consistently. ([#1189](https://github.com/can1357/oh-my-pi/issues/1189))
|
|
12
|
+
- Fixed the status-line fast-mode indicator (`⚡`) rendering for scoped service tiers (`openai-only`, `claude-only`) even when the active model's provider didn't realize them — e.g. `serviceTier: "openai-only"` would still show the indicator next to a Claude model the wire request couldn't apply fast mode to. The indicator now consults a new `AgentSession.isFastModeActive()` predicate that runs the configured tier through `resolveServiceTier(tier, model.provider)` and only lights up when the result is `"priority"` for the current model. `isFastModeEnabled()` keeps its scope-aware semantics so `/fast on|off|toggle` and `/fast status` continue to reflect the user's configured intent.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Added scoped service tier values to the `serviceTier` setting: `priority (OpenAI only)` and `priority (Claude only)`. They let you opt into premium processing on one provider family without paying premium costs on the other when switching models mid-session. `/fast on` continues to set the unscoped `"priority"` (active everywhere supported); `/fast status` and `isFastModeEnabled()` now report `on` for any scoped value too.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Changed `/fast` to be a single provider-agnostic toggle: enabling the command sets `serviceTier: "priority"` for every provider, and the anthropic-messages provider translates `priority` into `speed: "fast"` plus the `fast-mode-2026-02-01` beta. Anthropic fast mode is currently supported on Claude Opus 4.6 and 4.7; the server rejects other models, which triggers the provider's auto-fallback (request retried without the priority signal, `providerSessionState.fastModeDisabled` persisted for the rest of the session). The session listens for the `"priority"` marker in `AssistantMessage.disabledFeatures`, syncs `/fast` off, and emits a warning notice. Re-running `/fast on` clears the per-session disable so the next request actually re-tries priority.
|
|
21
|
+
|
|
5
22
|
## [15.1.6] - 2026-05-19
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -884,12 +884,12 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
884
884
|
};
|
|
885
885
|
readonly serviceTier: {
|
|
886
886
|
readonly type: "enum";
|
|
887
|
-
readonly values: readonly ["none", "auto", "default", "flex", "scale", "priority"];
|
|
887
|
+
readonly values: readonly ["none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"];
|
|
888
888
|
readonly default: "none";
|
|
889
889
|
readonly ui: {
|
|
890
890
|
readonly tab: "model";
|
|
891
891
|
readonly label: "Service Tier";
|
|
892
|
-
readonly description:
|
|
892
|
+
readonly description: 'Processing priority hint (none = omit). OpenAI accepts the tier values directly; Anthropic realizes `priority` as `speed: "fast"` on supported Opus models. Scoped values target one family.';
|
|
893
893
|
readonly options: readonly [{
|
|
894
894
|
readonly value: "none";
|
|
895
895
|
readonly label: "None";
|
|
@@ -897,23 +897,31 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
897
897
|
}, {
|
|
898
898
|
readonly value: "auto";
|
|
899
899
|
readonly label: "Auto";
|
|
900
|
-
readonly description: "Use provider default tier selection";
|
|
900
|
+
readonly description: "Use provider default tier selection (OpenAI)";
|
|
901
901
|
}, {
|
|
902
902
|
readonly value: "default";
|
|
903
903
|
readonly label: "Default";
|
|
904
|
-
readonly description: "Standard priority processing";
|
|
904
|
+
readonly description: "Standard priority processing (OpenAI)";
|
|
905
905
|
}, {
|
|
906
906
|
readonly value: "flex";
|
|
907
907
|
readonly label: "Flex";
|
|
908
|
-
readonly description: "
|
|
908
|
+
readonly description: "Flexible capacity tier when available (OpenAI)";
|
|
909
909
|
}, {
|
|
910
910
|
readonly value: "scale";
|
|
911
911
|
readonly label: "Scale";
|
|
912
|
-
readonly description: "
|
|
912
|
+
readonly description: "Scale Tier credits when available (OpenAI)";
|
|
913
913
|
}, {
|
|
914
914
|
readonly value: "priority";
|
|
915
915
|
readonly label: "Priority";
|
|
916
|
-
readonly description: "
|
|
916
|
+
readonly description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)";
|
|
917
|
+
}, {
|
|
918
|
+
readonly value: "openai-only";
|
|
919
|
+
readonly label: "Priority (OpenAI only)";
|
|
920
|
+
readonly description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere";
|
|
921
|
+
}, {
|
|
922
|
+
readonly value: "claude-only";
|
|
923
|
+
readonly label: "Priority (Claude only)";
|
|
924
|
+
readonly description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)";
|
|
917
925
|
}];
|
|
918
926
|
};
|
|
919
927
|
};
|
|
@@ -111,10 +111,10 @@ export declare const HL_BODY_SEP = "|";
|
|
|
111
111
|
export declare const HL_BODY_SEP_RE_RAW: string;
|
|
112
112
|
/**
|
|
113
113
|
* Compute a 2-character hash of a single line via xxHash32 mod 647 over
|
|
114
|
-
* {@link HL_BIGRAMS}.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* {@link HL_BIGRAMS}. The hash depends only on the line's content (after
|
|
115
|
+
* stripping CR and trailing whitespace); the `idx` parameter is accepted
|
|
116
|
+
* for call-site symmetry with line numbers but is intentionally unused so
|
|
117
|
+
* that anchors remain stable across line shifts caused by sibling edits.
|
|
118
118
|
*
|
|
119
119
|
* The line input should not include a trailing newline.
|
|
120
120
|
*/
|
|
@@ -18,5 +18,10 @@ export interface HashlineRecoveryResult {
|
|
|
18
18
|
* onto the current on-disk content. Returns `null` when no recovery is
|
|
19
19
|
* possible — callers should propagate the original mismatch error in that
|
|
20
20
|
* case.
|
|
21
|
+
*
|
|
22
|
+
* Recovery is gated on a strict precondition: every line the model anchored
|
|
23
|
+
* MUST be present in the cached snapshot AND its content MUST hash to the
|
|
24
|
+
* model-supplied hash. This prevents 3-way merges from silently sliding onto
|
|
25
|
+
* the wrong site when only tangential parts of the file went stale.
|
|
21
26
|
*/
|
|
22
27
|
export declare function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): HashlineRecoveryResult | null;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { TextEdit, WorkspaceEdit } from "./types";
|
|
1
|
+
import type { Range, TextEdit, WorkspaceEdit } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Apply text edits to a string in-memory.
|
|
4
4
|
* Edits are applied in reverse order (bottom-to-top) to preserve line/character indices.
|
|
5
5
|
*/
|
|
6
6
|
export declare function applyTextEditsToString(content: string, edits: TextEdit[]): string;
|
|
7
|
+
/** True when two ranges overlap (share any position other than a touching boundary). */
|
|
8
|
+
export declare function rangesOverlap(a: Range, b: Range): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Flatten a WorkspaceEdit's text edits into a Map<uri, TextEdit[]>.
|
|
11
|
+
* Resource operations (create/rename/delete) are ignored — callers handle them separately.
|
|
12
|
+
*/
|
|
13
|
+
export declare function flattenWorkspaceTextEdits(edit: WorkspaceEdit): Map<string, TextEdit[]>;
|
|
7
14
|
/**
|
|
8
15
|
* Apply text edits to a file.
|
|
9
16
|
* Edits are applied in reverse order (bottom-to-top) to preserve line/character indices.
|
|
@@ -618,7 +618,23 @@ export declare class AgentSession {
|
|
|
618
618
|
* @returns New level, or undefined if model doesn't support thinking
|
|
619
619
|
*/
|
|
620
620
|
cycleThinkingLevel(): ThinkingLevel | undefined;
|
|
621
|
+
/**
|
|
622
|
+
* True when *any* fast-mode-granting service tier is configured, regardless
|
|
623
|
+
* of whether the active model's provider actually realizes it. Used by the
|
|
624
|
+
* toggle (`/fast on|off`) so re-toggling a scoped tier (`openai-only`,
|
|
625
|
+
* `claude-only`) doesn't silently broaden it to unscoped `priority`.
|
|
626
|
+
*
|
|
627
|
+
* For "is fast mode actually applied to the next request?" use
|
|
628
|
+
* {@link isFastModeActive} instead — that one respects the model's provider.
|
|
629
|
+
*/
|
|
621
630
|
isFastModeEnabled(): boolean;
|
|
631
|
+
/**
|
|
632
|
+
* True when the configured `serviceTier` resolves to `"priority"` for the
|
|
633
|
+
* *currently selected model's provider*. Returns false for scoped tiers
|
|
634
|
+
* that don't match (e.g. `"openai-only"` on an anthropic model) and when
|
|
635
|
+
* no model is selected.
|
|
636
|
+
*/
|
|
637
|
+
isFastModeActive(): boolean;
|
|
622
638
|
setServiceTier(serviceTier: ServiceTier | undefined): void;
|
|
623
639
|
setFastMode(enabled: boolean): void;
|
|
624
640
|
toggleFastMode(): boolean;
|
|
@@ -9,7 +9,9 @@ import { type OutputMeta } from "./output-meta";
|
|
|
9
9
|
declare const findSchema: z.ZodObject<{
|
|
10
10
|
paths: z.ZodArray<z.ZodString>;
|
|
11
11
|
hidden: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
12
|
+
gitignore: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
12
13
|
limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
14
|
+
timeout: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
13
15
|
}, z.core.$strict>;
|
|
14
16
|
export type FindToolInput = z.infer<typeof findSchema>;
|
|
15
17
|
export interface FindToolDetails {
|
|
@@ -62,7 +64,9 @@ export declare class FindTool implements AgentTool<typeof findSchema, FindToolDe
|
|
|
62
64
|
readonly parameters: z.ZodObject<{
|
|
63
65
|
paths: z.ZodArray<z.ZodString>;
|
|
64
66
|
hidden: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
67
|
+
gitignore: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
65
68
|
limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
69
|
+
timeout: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
66
70
|
}, z.core.$strict>;
|
|
67
71
|
readonly strict = true;
|
|
68
72
|
constructor(session: ToolSession, options?: FindToolOptions);
|
|
@@ -49,6 +49,11 @@ export declare function runResolveInvocation(params: ResolveParams, options: {
|
|
|
49
49
|
label: string;
|
|
50
50
|
apply(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown>>;
|
|
51
51
|
reject?(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown> | undefined>;
|
|
52
|
+
/** Invoked synchronously when `apply()` throws, before the error is rethrown.
|
|
53
|
+
* The queued caller uses this to re-push the resolve directive so the
|
|
54
|
+
* pending preview survives a failed apply (e.g. overlapping ast_edit
|
|
55
|
+
* replacements) and the model can `discard` or fix-and-retry. */
|
|
56
|
+
onApplyError?(error: unknown): void;
|
|
52
57
|
}): Promise<AgentToolResult<ResolveToolDetails>>;
|
|
53
58
|
export declare class ResolveTool implements AgentTool<typeof resolveSchema, ResolveToolDetails> {
|
|
54
59
|
private readonly session;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.1.
|
|
4
|
+
"version": "15.1.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.1.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.1.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.1.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.1.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.1.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.1.7",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.1.7",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.1.7",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.1.7",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.1.7",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.1.7",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -755,19 +755,34 @@ export const SETTINGS_SCHEMA = {
|
|
|
755
755
|
|
|
756
756
|
serviceTier: {
|
|
757
757
|
type: "enum",
|
|
758
|
-
values: ["none", "auto", "default", "flex", "scale", "priority"] as const,
|
|
758
|
+
values: ["none", "auto", "default", "flex", "scale", "priority", "openai-only", "claude-only"] as const,
|
|
759
759
|
default: "none",
|
|
760
760
|
ui: {
|
|
761
761
|
tab: "model",
|
|
762
762
|
label: "Service Tier",
|
|
763
|
-
description:
|
|
763
|
+
description:
|
|
764
|
+
'Processing priority hint (none = omit). OpenAI accepts the tier values directly; Anthropic realizes `priority` as `speed: "fast"` on supported Opus models. Scoped values target one family.',
|
|
764
765
|
options: [
|
|
765
766
|
{ value: "none", label: "None", description: "Omit service_tier parameter" },
|
|
766
|
-
{ value: "auto", label: "Auto", description: "Use provider default tier selection" },
|
|
767
|
-
{ value: "default", label: "Default", description: "Standard priority processing" },
|
|
768
|
-
{ value: "flex", label: "Flex", description: "
|
|
769
|
-
{ value: "scale", label: "Scale", description: "
|
|
770
|
-
{
|
|
767
|
+
{ value: "auto", label: "Auto", description: "Use provider default tier selection (OpenAI)" },
|
|
768
|
+
{ value: "default", label: "Default", description: "Standard priority processing (OpenAI)" },
|
|
769
|
+
{ value: "flex", label: "Flex", description: "Flexible capacity tier when available (OpenAI)" },
|
|
770
|
+
{ value: "scale", label: "Scale", description: "Scale Tier credits when available (OpenAI)" },
|
|
771
|
+
{
|
|
772
|
+
value: "priority",
|
|
773
|
+
label: "Priority",
|
|
774
|
+
description: "Priority on every supported provider (OpenAI `service_tier`, Anthropic fast mode)",
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
value: "openai-only",
|
|
778
|
+
label: "Priority (OpenAI only)",
|
|
779
|
+
description: "Priority on OpenAI/OpenAI-Codex requests; ignored elsewhere",
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
value: "claude-only",
|
|
783
|
+
label: "Priority (Claude only)",
|
|
784
|
+
description: "Anthropic fast mode on direct Claude requests; ignored elsewhere (incl. Bedrock/Vertex)",
|
|
785
|
+
},
|
|
771
786
|
],
|
|
772
787
|
},
|
|
773
788
|
},
|
package/src/dap/session.ts
CHANGED
|
@@ -105,6 +105,41 @@ function toErrorMessage(value: unknown): string {
|
|
|
105
105
|
return String(value);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
interface DapStartRequestFailure {
|
|
109
|
+
rejected: boolean;
|
|
110
|
+
error?: unknown;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function trackDapStartRequest<T>(promise: Promise<T>, failure: DapStartRequestFailure): Promise<T> {
|
|
114
|
+
return promise.catch(error => {
|
|
115
|
+
failure.rejected = true;
|
|
116
|
+
failure.error = error;
|
|
117
|
+
throw error;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function combineDapStartErrors(command: "launch" | "attach", startError: unknown, configurationError: unknown): Error {
|
|
122
|
+
const startMessage = toErrorMessage(startError);
|
|
123
|
+
const configurationMessage = toErrorMessage(configurationError);
|
|
124
|
+
if (startMessage === configurationMessage) {
|
|
125
|
+
return startError instanceof Error ? startError : new Error(startMessage);
|
|
126
|
+
}
|
|
127
|
+
return new Error(
|
|
128
|
+
`DAP ${command} failed: ${startMessage}\nDAP configurationDone also failed: ${configurationMessage}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function throwPreferredDapStartError(
|
|
133
|
+
command: "launch" | "attach",
|
|
134
|
+
startFailure: DapStartRequestFailure,
|
|
135
|
+
configurationError: unknown,
|
|
136
|
+
): Promise<never> {
|
|
137
|
+
await Promise.resolve();
|
|
138
|
+
if (startFailure.rejected) {
|
|
139
|
+
throw combineDapStartErrors(command, startFailure.error, configurationError);
|
|
140
|
+
}
|
|
141
|
+
throw configurationError;
|
|
142
|
+
}
|
|
108
143
|
function normalizePath(filePath: string): string {
|
|
109
144
|
return path.resolve(filePath);
|
|
110
145
|
}
|
|
@@ -209,12 +244,20 @@ export class DapSessionManager {
|
|
|
209
244
|
// DAP spec: many adapters do not respond to launch until after
|
|
210
245
|
// configurationDone. Fire launch, complete the config handshake,
|
|
211
246
|
// then await the launch response.
|
|
212
|
-
const
|
|
247
|
+
const launchFailure: DapStartRequestFailure = { rejected: false };
|
|
248
|
+
const launchPromise = trackDapStartRequest(
|
|
249
|
+
client.sendRequest("launch", launchArguments, signal, timeoutMs),
|
|
250
|
+
launchFailure,
|
|
251
|
+
);
|
|
213
252
|
// Mark handled so a fast error response doesn't become an unhandled
|
|
214
253
|
// rejection while we await the config handshake. The actual error
|
|
215
254
|
// still propagates when we await launchPromise below.
|
|
216
255
|
launchPromise.catch(() => {});
|
|
217
|
-
|
|
256
|
+
try {
|
|
257
|
+
await this.#completeConfigurationHandshake(session, signal, timeoutMs);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
await throwPreferredDapStartError("launch", launchFailure, error);
|
|
260
|
+
}
|
|
218
261
|
await launchPromise;
|
|
219
262
|
// Try to capture initial stopped state (e.g. stopOnEntry).
|
|
220
263
|
// Timeout is acceptable — the program may simply be running.
|
|
@@ -262,9 +305,17 @@ export class DapSessionManager {
|
|
|
262
305
|
signal,
|
|
263
306
|
Math.min(timeoutMs, STOP_CAPTURE_TIMEOUT_MS),
|
|
264
307
|
);
|
|
265
|
-
const
|
|
308
|
+
const attachFailure: DapStartRequestFailure = { rejected: false };
|
|
309
|
+
const attachPromise = trackDapStartRequest(
|
|
310
|
+
client.sendRequest("attach", attachArguments, signal, timeoutMs),
|
|
311
|
+
attachFailure,
|
|
312
|
+
);
|
|
266
313
|
attachPromise.catch(() => {});
|
|
267
|
-
|
|
314
|
+
try {
|
|
315
|
+
await this.#completeConfigurationHandshake(session, signal, timeoutMs);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
await throwPreferredDapStartError("attach", attachFailure, error);
|
|
318
|
+
}
|
|
268
319
|
await attachPromise;
|
|
269
320
|
try {
|
|
270
321
|
await untilAborted(signal, initialStopPromise);
|
|
@@ -1085,7 +1136,9 @@ export class DapSessionManager {
|
|
|
1085
1136
|
for (const p of promises) {
|
|
1086
1137
|
p.catch(() => {});
|
|
1087
1138
|
}
|
|
1088
|
-
|
|
1139
|
+
const outcome = Promise.race(promises);
|
|
1140
|
+
outcome.catch(() => {});
|
|
1141
|
+
return outcome;
|
|
1089
1142
|
}
|
|
1090
1143
|
|
|
1091
1144
|
/**
|
package/src/edit/modes/patch.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { outputMeta } from "../../tools/output-meta";
|
|
27
27
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
28
28
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
29
|
+
import { ToolError } from "../../tools/tool-errors";
|
|
29
30
|
import {
|
|
30
31
|
ApplyPatchError,
|
|
31
32
|
type DiffHunk,
|
|
@@ -1714,6 +1715,24 @@ export async function executePatchSingle(
|
|
|
1714
1715
|
|
|
1715
1716
|
await assertEditableFile(resolvedPath, path);
|
|
1716
1717
|
|
|
1718
|
+
// Capture pre-edit content so we can verify the write actually hit disk.
|
|
1719
|
+
// `LspFileSystem.writeFile` delegates to a writethrough callback that, in
|
|
1720
|
+
// some host integrations, has been observed to report success without
|
|
1721
|
+
// persisting bytes — leaving the tool to claim "Updated <path>" while the
|
|
1722
|
+
// file on disk is byte-identical to before. After the write we re-read
|
|
1723
|
+
// the file and assert the bytes match the expected newContent; relying
|
|
1724
|
+
// on stat (mtime/size) is unreliable because filesystems with coarse
|
|
1725
|
+
// timestamp resolution can record an unchanged mtime even when the
|
|
1726
|
+
// content was rewritten, and same-length rewrites leave size unchanged.
|
|
1727
|
+
let preEditContent: Uint8Array | undefined;
|
|
1728
|
+
if (op === "update") {
|
|
1729
|
+
try {
|
|
1730
|
+
preEditContent = await fs.promises.readFile(resolvedPath);
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
if (!isEnoent(err)) throw err;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1717
1736
|
const input: PatchInput = { path: resolvedPath, op, rename: resolvedRename, diff };
|
|
1718
1737
|
const patchFileSystem = new LspFileSystem(writethrough, signal, batchRequest, beginDeferredDiagnosticsForPath);
|
|
1719
1738
|
const result = await applyPatch(input, {
|
|
@@ -1723,6 +1742,33 @@ export async function executePatchSingle(
|
|
|
1723
1742
|
allowFuzzy,
|
|
1724
1743
|
});
|
|
1725
1744
|
|
|
1745
|
+
// Post-write verification: only meaningful for in-place updates where the
|
|
1746
|
+
// patch actually changes content and the file is not being renamed away.
|
|
1747
|
+
if (
|
|
1748
|
+
result.change.type === "update" &&
|
|
1749
|
+
!result.change.newPath &&
|
|
1750
|
+
preEditContent !== undefined &&
|
|
1751
|
+
result.change.oldContent !== undefined &&
|
|
1752
|
+
result.change.newContent !== undefined &&
|
|
1753
|
+
result.change.oldContent !== result.change.newContent
|
|
1754
|
+
) {
|
|
1755
|
+
let postEditContent: Uint8Array | undefined;
|
|
1756
|
+
try {
|
|
1757
|
+
postEditContent = await fs.promises.readFile(resolvedPath);
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
if (!isEnoent(err)) throw err;
|
|
1760
|
+
}
|
|
1761
|
+
const unchanged =
|
|
1762
|
+
postEditContent !== undefined &&
|
|
1763
|
+
postEditContent.length === preEditContent.length &&
|
|
1764
|
+
postEditContent.every((b, i) => b === preEditContent[i]);
|
|
1765
|
+
if (unchanged) {
|
|
1766
|
+
throw new ToolError(`edit appeared successful but file content did not change on disk: ${resolvedPath}`, {
|
|
1767
|
+
path: resolvedPath,
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1726
1772
|
if (resolvedRename) {
|
|
1727
1773
|
invalidateFsScanAfterRename(resolvedPath, resolvedRename);
|
|
1728
1774
|
} else if (result.change.type === "delete") {
|
|
@@ -52,7 +52,7 @@ interface JsSession {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
const sessions = new Map<string, JsSession>();
|
|
55
|
-
const
|
|
55
|
+
const READY_TIMEOUT_MS_DEFAULT = 5_000;
|
|
56
56
|
|
|
57
57
|
export async function executeInVmContext(options: {
|
|
58
58
|
sessionKey: string;
|
|
@@ -68,10 +68,11 @@ export async function executeInVmContext(options: {
|
|
|
68
68
|
if (options.reset) {
|
|
69
69
|
await resetVmContext(options.sessionKey);
|
|
70
70
|
}
|
|
71
|
-
const session = await acquireSession(
|
|
72
|
-
|
|
73
|
-
sessionId: options.sessionId,
|
|
74
|
-
|
|
71
|
+
const session = await acquireSession(
|
|
72
|
+
options.sessionKey,
|
|
73
|
+
{ cwd: options.cwd, sessionId: options.sessionId },
|
|
74
|
+
options.timeoutMs,
|
|
75
|
+
);
|
|
75
76
|
return await runQueued(session, () => runOnce(session, options));
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -158,7 +159,7 @@ async function runOnce(
|
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
async function acquireSession(sessionKey: string, snapshot: SessionSnapshot): Promise<JsSession> {
|
|
162
|
+
async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, timeoutMs?: number): Promise<JsSession> {
|
|
162
163
|
const existing = sessions.get(sessionKey);
|
|
163
164
|
if (existing && existing.state === "alive") return existing;
|
|
164
165
|
|
|
@@ -186,7 +187,10 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot): Pr
|
|
|
186
187
|
handleSessionMessage(session, msg);
|
|
187
188
|
});
|
|
188
189
|
try {
|
|
189
|
-
|
|
190
|
+
// Cold-start can exceed 5s on slow hosts. Let the caller's per-cell timeout dominate so
|
|
191
|
+
// users can grant more headroom when they raise `timeout` on a cell.
|
|
192
|
+
const readyTimeoutMs = Math.max(READY_TIMEOUT_MS_DEFAULT, timeoutMs ?? 0);
|
|
193
|
+
await raceWithTimeout(readyPromise, readyTimeoutMs, "Timed out initializing JS eval worker");
|
|
190
194
|
} catch (error) {
|
|
191
195
|
unsubscribe();
|
|
192
196
|
await worker.terminate().catch(() => undefined);
|
|
@@ -303,15 +303,27 @@ function returnFinalExpression(code: string): { source: string; returned: boolea
|
|
|
303
303
|
let lastIndex = body.length - 1;
|
|
304
304
|
while (lastIndex >= 0 && body[lastIndex]?.type === "EmptyStatement") lastIndex--;
|
|
305
305
|
const last = lastIndex >= 0 ? body[lastIndex] : undefined;
|
|
306
|
-
if (last?.type
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
306
|
+
if (last?.type === "ExpressionStatement") {
|
|
307
|
+
const expression = last as BabelExpressionStatement;
|
|
308
|
+
const prefix = code.slice(0, expression.start);
|
|
309
|
+
const statement = code.slice(expression.start, expression.end);
|
|
310
|
+
const suffix = code.slice(expression.end);
|
|
311
|
+
const semicolonMatch = statement.match(/;\s*$/);
|
|
312
|
+
const trimmedStatement = semicolonMatch ? statement.slice(0, semicolonMatch.index) : statement;
|
|
313
|
+
return { source: `${prefix}__omp_set_final_expr__((${trimmedStatement}));${suffix}`, returned: true };
|
|
314
|
+
}
|
|
315
|
+
if (last?.type === "ReturnStatement") {
|
|
316
|
+
// Top-level `return value;` is otherwise swallowed: it forces the cell into an async IIFE
|
|
317
|
+
// wrapper that discards the returned value. Rewrite into `__omp_set_final_expr__((expr))`
|
|
318
|
+
// so the runtime can surface the value to the caller just like a trailing expression.
|
|
319
|
+
const ret = last as unknown as { start: number; end: number; argument?: { start: number; end: number } | null };
|
|
320
|
+
if (!ret.argument) return { source: code, returned: false };
|
|
321
|
+
const prefix = code.slice(0, ret.start);
|
|
322
|
+
const suffix = code.slice(ret.end);
|
|
323
|
+
const expr = code.slice(ret.argument.start, ret.argument.end);
|
|
324
|
+
return { source: `${prefix}__omp_set_final_expr__((${expr}));${suffix}`, returned: true };
|
|
325
|
+
}
|
|
326
|
+
return { source: code, returned: false };
|
|
315
327
|
}
|
|
316
328
|
|
|
317
329
|
function isExecutionBoundary(type: string): boolean {
|
|
@@ -165,7 +165,8 @@ export class JsRuntime {
|
|
|
165
165
|
const finalValue = this.#finalExpressionValue;
|
|
166
166
|
this.#finalExpressionSet = false;
|
|
167
167
|
this.#finalExpressionValue = undefined;
|
|
168
|
-
|
|
168
|
+
const resolved = await awaitMaybePromise(finalValue);
|
|
169
|
+
return resolved;
|
|
169
170
|
}
|
|
170
171
|
return awaited;
|
|
171
172
|
}
|
package/src/hashline/hash.ts
CHANGED
|
@@ -136,21 +136,24 @@ export const HL_BODY_SEP = "|";
|
|
|
136
136
|
/** Regex-escaped form of {@link HL_BODY_SEP}, safe for embedding inside a regex. */
|
|
137
137
|
export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
|
|
138
138
|
|
|
139
|
-
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
|
|
140
|
-
|
|
141
139
|
/**
|
|
142
140
|
* Compute a 2-character hash of a single line via xxHash32 mod 647 over
|
|
143
|
-
* {@link HL_BIGRAMS}.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
141
|
+
* {@link HL_BIGRAMS}. The hash depends only on the line's content (after
|
|
142
|
+
* stripping CR and trailing whitespace); the `idx` parameter is accepted
|
|
143
|
+
* for call-site symmetry with line numbers but is intentionally unused so
|
|
144
|
+
* that anchors remain stable across line shifts caused by sibling edits.
|
|
147
145
|
*
|
|
148
146
|
* The line input should not include a trailing newline.
|
|
149
147
|
*/
|
|
150
148
|
export function computeLineHash(idx: number, line: string): string {
|
|
149
|
+
void idx;
|
|
151
150
|
line = line.replace(/\r/g, "").trimEnd();
|
|
152
|
-
|
|
153
|
-
|
|
151
|
+
// Seed is fixed so the hash depends only on line content. Earlier we mixed
|
|
152
|
+
// in `idx` for blank/punctuation-only lines, but that meant any line shift
|
|
153
|
+
// (e.g. from a sibling edit in the same batch) invalidated anchors whose
|
|
154
|
+
// content had not changed. Identical blank lines are intentionally allowed
|
|
155
|
+
// to collide — the edit op's line number disambiguates them.
|
|
156
|
+
return HL_BIGRAMS[Bun.hash.xxHash32(line, 0) % HL_BIGRAMS_COUNT];
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
/**
|
package/src/hashline/parser.ts
CHANGED
|
@@ -74,19 +74,29 @@ export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
|
|
|
74
74
|
if (cursor.kind === "after_anchor") return { kind: "after_anchor", anchor: { ...cursor.anchor } };
|
|
75
75
|
return cursor;
|
|
76
76
|
}
|
|
77
|
+
/** Returns true when every non-empty payload line starts with `${sep} ` (sep + one space). */
|
|
78
|
+
function hasUniformSeparatorPadding(payload: string[]): boolean {
|
|
79
|
+
let any = false;
|
|
80
|
+
for (const text of payload) {
|
|
81
|
+
if (text.length === 0) continue;
|
|
82
|
+
if (!text.startsWith(" ")) return false;
|
|
83
|
+
any = true;
|
|
84
|
+
}
|
|
85
|
+
return any;
|
|
86
|
+
}
|
|
77
87
|
|
|
78
88
|
function collectPayload(
|
|
79
89
|
lines: string[],
|
|
80
90
|
startIndex: number,
|
|
81
91
|
opLineNum: number,
|
|
82
92
|
requirePayload: boolean,
|
|
83
|
-
): { payload: string[]; nextIndex: number } {
|
|
93
|
+
): { payload: string[]; nextIndex: number; paddingWarning?: string } {
|
|
84
94
|
const payload: string[] = [];
|
|
85
95
|
let index = startIndex;
|
|
86
96
|
while (index < lines.length) {
|
|
87
97
|
const line = lines[index];
|
|
88
98
|
if (line.startsWith(HL_EDIT_SEP)) {
|
|
89
|
-
payload.push(line.slice(
|
|
99
|
+
payload.push(line.slice(HL_EDIT_SEP.length).trimEnd());
|
|
90
100
|
index++;
|
|
91
101
|
continue;
|
|
92
102
|
}
|
|
@@ -115,7 +125,11 @@ function collectPayload(
|
|
|
115
125
|
if (payload.length === 0 && requirePayload) {
|
|
116
126
|
throw new Error(`line ${opLineNum}: + and < operations require at least one ${HL_EDIT_SEP}TEXT payload line.`);
|
|
117
127
|
}
|
|
118
|
-
|
|
128
|
+
const paddingWarning = hasUniformSeparatorPadding(payload)
|
|
129
|
+
? `line ${opLineNum}: all payload lines start with "${HL_EDIT_SEP} " (separator + space). ` +
|
|
130
|
+
`The space becomes file content. Remove it unless the target file requires leading spaces.`
|
|
131
|
+
: undefined;
|
|
132
|
+
return { payload, nextIndex: index, paddingWarning };
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
export function parseHashline(diff: string): HashlineEdit[] {
|
|
@@ -158,7 +172,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
158
172
|
const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line);
|
|
159
173
|
if (insertBeforeMatch) {
|
|
160
174
|
const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
|
|
161
|
-
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
|
|
175
|
+
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, true);
|
|
176
|
+
if (paddingWarning) warnings.push(paddingWarning);
|
|
162
177
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
163
178
|
i = nextIndex;
|
|
164
179
|
continue;
|
|
@@ -167,7 +182,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
167
182
|
const insertAfterMatch = INSERT_AFTER_OP_RE.exec(line);
|
|
168
183
|
if (insertAfterMatch) {
|
|
169
184
|
const cursor = parseInsertTarget(insertAfterMatch[1], lineNum, "after");
|
|
170
|
-
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
|
|
185
|
+
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, true);
|
|
186
|
+
if (paddingWarning) warnings.push(paddingWarning);
|
|
171
187
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
172
188
|
i = nextIndex;
|
|
173
189
|
continue;
|
|
@@ -185,7 +201,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
185
201
|
const replaceMatch = REPLACE_OP_RE.exec(line);
|
|
186
202
|
if (replaceMatch) {
|
|
187
203
|
const range = parseRange(replaceMatch[1], lineNum);
|
|
188
|
-
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
|
|
204
|
+
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, false);
|
|
205
|
+
if (paddingWarning) warnings.push(paddingWarning);
|
|
189
206
|
// `= A..B` with no payload blanks the range to a single empty line.
|
|
190
207
|
const replacement = payload.length === 0 ? [""] : payload;
|
|
191
208
|
for (const text of replacement) {
|