@oh-my-pi/pi-coding-agent 15.1.6 → 15.1.8
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 +24 -0
- package/dist/types/config/settings-schema.d.ts +15 -7
- package/dist/types/edit/streaming.d.ts +7 -0
- 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/edit/streaming.ts +145 -4
- 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/modes/components/tool-execution.ts +46 -1
- package/src/modes/interactive-mode.ts +33 -7
- 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/src/web/search/providers/perplexity.ts +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.8] - 2026-05-20
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed streaming edit previews for `apply_patch` and `hashline` jittering as the model typed `+added` lines. Two root causes addressed: (1) the trailing partial line of the streaming text input is now trimmed at each tick so a half-typed `+added` line no longer flickers; (2) the preview is rendered in the model's input order during streaming instead of re-deriving a unified diff via `Diff.structuredPatch`, whose coalescing previously reshuffled existing `+added` lines downward each time a new `-removed` line arrived. Existing additions now stay put and the preview only grows at the bottom while streaming. A residual trailing `-removed`/hunk-header block whose matching `+added` companion has not yet arrived is also suppressed until the additions land.
|
|
10
|
+
- Fixed Perplexity web search appearing "logged out" roughly an hour after `omp auth login perplexity`. The search provider's `findOAuthToken` was honoring the bogus `expires = login_time + 1h` written by older logins (Perplexity JWTs typically omit `exp` because sessions are server-side) and silently dropping the credential. The loader now decodes the JWT's `exp` claim directly and only skips when the JWT itself is expired; tokens without an `exp` claim are treated as non-expiring.
|
|
11
|
+
|
|
12
|
+
## [15.1.7] - 2026-05-19
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- 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))
|
|
17
|
+
- 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))
|
|
18
|
+
- 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))
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
5
29
|
## [15.1.6] - 2026-05-19
|
|
6
30
|
|
|
7
31
|
### 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
|
};
|
|
@@ -26,6 +26,13 @@ export interface StreamingDiffContext {
|
|
|
26
26
|
fuzzyThreshold?: number;
|
|
27
27
|
allowFuzzy?: boolean;
|
|
28
28
|
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* True while the tool's arguments are still streaming in. Strategies that
|
|
31
|
+
* accept free-form text input (apply_patch, hashline) trim the trailing
|
|
32
|
+
* partial line so per-character growth of an in-flight `+added` line does
|
|
33
|
+
* not flicker in the preview.
|
|
34
|
+
*/
|
|
35
|
+
isStreaming?: boolean;
|
|
29
36
|
}
|
|
30
37
|
export interface EditStreamingStrategy<Args = unknown> {
|
|
31
38
|
/**
|
|
@@ -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.8",
|
|
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.8",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.1.8",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.1.8",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.1.8",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.1.8",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.1.8",
|
|
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") {
|
package/src/edit/streaming.ts
CHANGED
|
@@ -45,6 +45,13 @@ export interface StreamingDiffContext {
|
|
|
45
45
|
fuzzyThreshold?: number;
|
|
46
46
|
allowFuzzy?: boolean;
|
|
47
47
|
hashlineAutoDropPureInsertDuplicates?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* True while the tool's arguments are still streaming in. Strategies that
|
|
50
|
+
* accept free-form text input (apply_patch, hashline) trim the trailing
|
|
51
|
+
* partial line so per-character growth of an in-flight `+added` line does
|
|
52
|
+
* not flicker in the preview.
|
|
53
|
+
*/
|
|
54
|
+
isStreaming?: boolean;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
export interface EditStreamingStrategy<Args = unknown> {
|
|
@@ -274,21 +281,146 @@ interface HashlineArgs {
|
|
|
274
281
|
__partialJson?: string;
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
/**
|
|
285
|
+
* While streaming a free-form text payload (apply_patch envelope, hashline
|
|
286
|
+
* input), trim the trailing partial line so per-character growth of an
|
|
287
|
+
* in-flight `+added` line does not cause the diff preview to flicker. The
|
|
288
|
+
* full line will show on the next streaming tick once its `\n` arrives.
|
|
289
|
+
* Returns `text` unchanged when not streaming or when no newline is present.
|
|
290
|
+
*/
|
|
291
|
+
function trimTrailingPartialLine(text: string, isStreaming: boolean | undefined): string {
|
|
292
|
+
if (!isStreaming) return text;
|
|
293
|
+
const idx = text.lastIndexOf("\n");
|
|
294
|
+
if (idx === -1) return "";
|
|
295
|
+
return text.slice(0, idx + 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Build a per-file diff preview directly from a partial `apply_patch`
|
|
300
|
+
* envelope by emitting its body lines in *input order*. This bypasses the
|
|
301
|
+
* file-state re-diff (`computePatchDiff` → `Diff.structuredPatch`) whose
|
|
302
|
+
* coalescing reorders the model's `-old +new -old +new` stream into
|
|
303
|
+
* `-old -old +new +new` and visibly shifts existing `+added` lines
|
|
304
|
+
* downward each time a new `-` arrives. The preview therefore grows
|
|
305
|
+
* monotonically at the bottom while streaming and only becomes a real
|
|
306
|
+
* unified diff once the args are complete.
|
|
307
|
+
*/
|
|
308
|
+
function buildApplyPatchNaturalOrderPreviews(input: string): PerFileDiffPreview[] | null {
|
|
309
|
+
const lines = input.split("\n");
|
|
310
|
+
const groups = new Map<string, string[]>();
|
|
311
|
+
let currentPath: string | undefined;
|
|
312
|
+
const ensure = (path: string): string[] => {
|
|
313
|
+
let bucket = groups.get(path);
|
|
314
|
+
if (!bucket) {
|
|
315
|
+
bucket = [];
|
|
316
|
+
groups.set(path, bucket);
|
|
317
|
+
}
|
|
318
|
+
return bucket;
|
|
319
|
+
};
|
|
320
|
+
for (const raw of lines) {
|
|
321
|
+
const trimmedEnd = raw.trimEnd();
|
|
322
|
+
if (trimmedEnd === BEGIN_PATCH_MARKER || trimmedEnd === END_PATCH_MARKER || trimmedEnd === ABORT_MARKER) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (trimmedEnd.startsWith("*** Add File: ")) {
|
|
326
|
+
currentPath = trimmedEnd.slice("*** Add File: ".length);
|
|
327
|
+
ensure(currentPath);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (trimmedEnd.startsWith("*** Delete File: ")) {
|
|
331
|
+
currentPath = trimmedEnd.slice("*** Delete File: ".length);
|
|
332
|
+
ensure(currentPath);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (trimmedEnd.startsWith("*** Update File: ")) {
|
|
336
|
+
currentPath = trimmedEnd.slice("*** Update File: ".length);
|
|
337
|
+
ensure(currentPath);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (trimmedEnd.startsWith("*** Move to:") || trimmedEnd.startsWith("*** End of File")) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (!currentPath) continue;
|
|
344
|
+
// Diff body: keep `-/+/space`-prefixed lines and `@@` hunk headers in
|
|
345
|
+
// input order. parseDiffLine accepts the no-line-number legacy form so
|
|
346
|
+
// the renderer styles them as additions/removals/context naturally.
|
|
347
|
+
if (raw.startsWith("+") || raw.startsWith("-") || raw.startsWith(" ") || raw.startsWith("@@")) {
|
|
348
|
+
ensure(currentPath).push(raw);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (groups.size === 0) return null;
|
|
352
|
+
const previews: PerFileDiffPreview[] = [];
|
|
353
|
+
for (const [path, body] of groups) {
|
|
354
|
+
if (body.length === 0) continue;
|
|
355
|
+
previews.push({ path, diff: body.join("\n") });
|
|
356
|
+
}
|
|
357
|
+
return previews.length > 0 ? previews : null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Hashline equivalent: emit each section's `~payload` lines as `+added`
|
|
362
|
+
* lines in the order the model typed them. We deliberately omit op headers
|
|
363
|
+
* and removal targets from the streaming preview because their content
|
|
364
|
+
* lives in the file and would require a costly re-apply per tick; the
|
|
365
|
+
* complete unified diff is shown once streaming finishes.
|
|
366
|
+
*/
|
|
367
|
+
function buildHashlineNaturalOrderPreviews(
|
|
368
|
+
input: string,
|
|
369
|
+
defaultPath: string | undefined,
|
|
370
|
+
): PerFileDiffPreview[] | null {
|
|
371
|
+
const lines = input.split("\n");
|
|
372
|
+
const groups = new Map<string, string[]>();
|
|
373
|
+
let currentPath = defaultPath ?? "";
|
|
374
|
+
const ensure = (path: string): string[] => {
|
|
375
|
+
let bucket = groups.get(path);
|
|
376
|
+
if (!bucket) {
|
|
377
|
+
bucket = [];
|
|
378
|
+
groups.set(path, bucket);
|
|
379
|
+
}
|
|
380
|
+
return bucket;
|
|
381
|
+
};
|
|
382
|
+
for (const raw of lines) {
|
|
383
|
+
if (isHashlineEnvelopeMarkerLine(raw)) continue;
|
|
384
|
+
if (isHashlineHeaderLine(raw)) {
|
|
385
|
+
currentPath = raw.trimEnd().slice(1).trim();
|
|
386
|
+
if (currentPath) ensure(currentPath);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (raw.startsWith("~")) {
|
|
390
|
+
ensure(currentPath).push(`+${raw.slice(1)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (groups.size === 0) return null;
|
|
394
|
+
const previews: PerFileDiffPreview[] = [];
|
|
395
|
+
for (const [path, body] of groups) {
|
|
396
|
+
if (body.length === 0) continue;
|
|
397
|
+
previews.push({ path, diff: body.join("\n") });
|
|
398
|
+
}
|
|
399
|
+
return previews.length > 0 ? previews : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
277
402
|
const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
278
403
|
extractCompleteEdits(args) {
|
|
279
404
|
return args;
|
|
280
405
|
},
|
|
281
406
|
async computeDiffPreview(args, ctx) {
|
|
282
407
|
if (typeof args.input !== "string" || args.input.length === 0) return null;
|
|
408
|
+
const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
|
|
409
|
+
if (input.length === 0) return null;
|
|
410
|
+
if (ctx.isStreaming) {
|
|
411
|
+
// Skip the costly per-tick re-apply and avoid `Diff.structuredPatch`
|
|
412
|
+
// reordering by showing the model's `~payload` lines in input order.
|
|
413
|
+
return buildHashlineNaturalOrderPreviews(input, args.path);
|
|
414
|
+
}
|
|
283
415
|
ctx.signal.throwIfAborted();
|
|
284
416
|
|
|
285
417
|
let sections: HashlineInputSection[];
|
|
286
418
|
try {
|
|
287
|
-
sections = splitHashlineInputs(
|
|
419
|
+
sections = splitHashlineInputs(input, { cwd: ctx.cwd, path: args.path });
|
|
288
420
|
} catch {
|
|
289
421
|
// Single-section fallback keeps the original error rendering for the
|
|
290
422
|
// "haven't typed `@@ PATH` yet" case.
|
|
291
|
-
const result = await computeHashlineDiff({ input
|
|
423
|
+
const result = await computeHashlineDiff({ input, path: args.path }, ctx.cwd, {
|
|
292
424
|
autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
|
|
293
425
|
});
|
|
294
426
|
ctx.signal.throwIfAborted();
|
|
@@ -340,12 +472,21 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
340
472
|
},
|
|
341
473
|
async computeDiffPreview(args, ctx) {
|
|
342
474
|
if (typeof args.input !== "string" || args.input.length === 0) return null;
|
|
475
|
+
const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
|
|
476
|
+
if (input.length === 0) return null;
|
|
477
|
+
if (ctx.isStreaming) {
|
|
478
|
+
// Render the envelope's diff body in input order so newly streamed
|
|
479
|
+
// `+added` lines append at the bottom instead of being shuffled
|
|
480
|
+
// upward as later `-removed` lines arrive and reorder the unified
|
|
481
|
+
// diff that `Diff.structuredPatch` would otherwise produce.
|
|
482
|
+
return buildApplyPatchNaturalOrderPreviews(input);
|
|
483
|
+
}
|
|
343
484
|
let entries: ApplyPatchEntry[];
|
|
344
485
|
try {
|
|
345
|
-
entries = expandApplyPatchToEntries({ input
|
|
486
|
+
entries = expandApplyPatchToEntries({ input });
|
|
346
487
|
} catch {
|
|
347
488
|
try {
|
|
348
|
-
entries = expandApplyPatchToPreviewEntries({ input
|
|
489
|
+
entries = expandApplyPatchToPreviewEntries({ input });
|
|
349
490
|
} catch (err) {
|
|
350
491
|
return [{ path: "", error: err instanceof Error ? err.message : String(err) }];
|
|
351
492
|
}
|