@oh-my-pi/pi-coding-agent 15.8.0 → 15.8.2
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 +26 -0
- package/dist/types/config/keybindings.d.ts +5 -1
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/package.json +9 -9
- package/src/config/keybindings.ts +8 -1
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/session/agent-session.ts +147 -8
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/write.ts +6 -0
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.8.2] - 2026-06-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a bundled TypeScript rule that warns against leaving `@deprecated` compatibility shims behind instead of finishing a refactor.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed `/review`'s uncommitted-change mode in Jujutsu repositories to read `jj diff --git` from the current workspace, so non-default JJ workspaces include their working-copy changes instead of falling back to the colocated Git checkout.
|
|
14
|
+
- Fixed empty assistant stop retry continuations preserving auto-retry state until a non-empty assistant turn completes or recovery reaches its retry cap.
|
|
15
|
+
- Fixed TTSR rule conditions never matching streamed `edit`/`write` tool calls whose wire format obscures the real content (hashline `+` body rows, apply_patch envelopes, JSON-escaped `write` content). The edit and write tools now expose a `matcherDigest` normalization and TTSR matches against the introduced source text, so rule regexes stay universal regardless of the active edit mode.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Changed the JJ utility API to mirror Git's scoped helpers: repository operations now live under `jj.repo` (`root`, `resolve`, `is`, `clearRootCache`), and diff file listing is available as `jj.diff.changedFiles`.
|
|
20
|
+
|
|
21
|
+
- Changed the `search_tool_bm25` tool description to name the hidden discoverable built-in tools (e.g. `write`, `find`, `search`, `lsp`, `task`) when `tools.discoveryMode: "all"` is active, so a model can form a targeted discovery query by name instead of guessing or falling back to shell. `mcp-only` mode is unchanged (no built-ins are advertised) and the `Total discoverable tools available: N` count still includes them.
|
|
22
|
+
|
|
23
|
+
## [15.8.1] - 2026-06-02
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed an unhandled `EPIPE` rejection when an MCP stdio server exits between returning the `initialize` response and the client's `notifications/initialized` send. `StdioTransport.notify()` and `#sendResponse()` now route stdin writes through a shared helper that catches synchronous sink failures: `notify()` tears the transport down (firing `onClose`) and surfaces a `Transport closed while sending notification` rejection so `connectToServer()` treats the handshake as a failed connection instead of returning a "connected" handle wrapping a dead transport; `#sendResponse()` stays silent because a dead subprocess has no use for the response. `StdioTransport.close()` is now the authoritative resource teardown — it no longer early-returns when `#handleClose()` has already flipped `#connected`, so the subprocess and read loop are always cleaned up (including in the `connectToServer()` failure path) ([#1710](https://github.com/can1357/oh-my-pi/issues/1710)).
|
|
28
|
+
- Fixed startup model resolution ignoring cached discovery rows for special built-in providers (`google-antigravity`, `google-gemini-cli`, `openai-codex`) until the background refresh completed ([#1721](https://github.com/can1357/oh-my-pi/issues/1721)).
|
|
29
|
+
- Fixed Windows clipboard-image paste keeping `Ctrl+V` unregistered by default. The TUI now registers `Ctrl+V` plus the Windows Terminal-safe `Alt+V` fallback, and the keybinding docs call out when to use the fallback ([#1708](https://github.com/can1357/oh-my-pi/issues/1708)).
|
|
30
|
+
|
|
5
31
|
## [15.8.0] - 2026-06-02
|
|
6
32
|
|
|
7
33
|
### Added
|
|
@@ -43,6 +43,10 @@ declare module "@oh-my-pi/pi-tui" {
|
|
|
43
43
|
interface Keybindings extends AppKeybindings {
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve default image-paste shortcuts for the current terminal platform.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getDefaultPasteImageKeys(platform?: NodeJS.Platform): KeyId[];
|
|
46
50
|
/**
|
|
47
51
|
* All keybindings definitions: TUI + app-specific.
|
|
48
52
|
*/
|
|
@@ -228,7 +232,7 @@ export declare const KEYBINDINGS: {
|
|
|
228
232
|
readonly description: "Dequeue message";
|
|
229
233
|
};
|
|
230
234
|
readonly "app.clipboard.pasteImage": {
|
|
231
|
-
readonly defaultKeys:
|
|
235
|
+
readonly defaultKeys: KeyId[];
|
|
232
236
|
readonly description: "Paste image from clipboard";
|
|
233
237
|
};
|
|
234
238
|
readonly "app.clipboard.pasteTextRaw": {
|
|
@@ -52,5 +52,11 @@ export declare class EditTool implements AgentTool<TInput> {
|
|
|
52
52
|
* internal `name` and `customWireName`, so returned calls route correctly.
|
|
53
53
|
*/
|
|
54
54
|
get customWireName(): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Normalize streamed args into the source text this edit introduces, so
|
|
57
|
+
* stream matchers (TTSR rules) run against real file content instead of the
|
|
58
|
+
* mode-specific patch grammar.
|
|
59
|
+
*/
|
|
60
|
+
matcherDigest(args: unknown): string | undefined;
|
|
55
61
|
execute(_toolCallId: string, params: EditParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<EditToolDetails, TInput>, context?: AgentToolContext): Promise<AgentToolResult<EditToolDetails, TInput>>;
|
|
56
62
|
}
|
|
@@ -52,6 +52,14 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
52
52
|
* compute returned `null` because args are still too partial).
|
|
53
53
|
*/
|
|
54
54
|
renderStreamingFallback(args: Args, uiTheme: Theme): string;
|
|
55
|
+
/**
|
|
56
|
+
* Project the (potentially partial) args onto the plain text the edit
|
|
57
|
+
* introduces into files — added lines without patch grammar — so stream
|
|
58
|
+
* matchers (TTSR rules) can run source-level patterns against real content
|
|
59
|
+
* instead of the mode-specific wire format. Returns `undefined` when the
|
|
60
|
+
* args don't yet carry any content.
|
|
61
|
+
*/
|
|
62
|
+
matcherDigest(args: Args): string | undefined;
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
57
65
|
* Given an edits array parsed from partial JSON, drop the last entry when the
|
|
@@ -23,6 +23,15 @@ export declare class TtsrManager {
|
|
|
23
23
|
* assistant prose, thinking text, and unrelated tool argument streams.
|
|
24
24
|
*/
|
|
25
25
|
checkDelta(delta: string, context: TtsrMatchContext): Rule[];
|
|
26
|
+
/**
|
|
27
|
+
* Replace the scoped buffer with a tool-provided normalized snapshot and
|
|
28
|
+
* return matching rules.
|
|
29
|
+
*
|
|
30
|
+
* Used for tools exposing `matcherDigest`: the digest is recomputed from the
|
|
31
|
+
* full (partial) arguments on every delta, so it replaces the buffer instead
|
|
32
|
+
* of being appended to it.
|
|
33
|
+
*/
|
|
34
|
+
checkSnapshot(snapshot: string, context: TtsrMatchContext): Rule[];
|
|
26
35
|
/** Mark rules as injected (won't trigger again until conditions allow). */
|
|
27
36
|
markInjected(rulesToMark: Rule[]): void;
|
|
28
37
|
/** Mark rule names as injected (won't trigger again until conditions allow). */
|
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
* Messages are newline-delimited JSON.
|
|
6
6
|
*/
|
|
7
7
|
import type { MCPRequestOptions, MCPStdioServerConfig, MCPTransport } from "../../mcp/types";
|
|
8
|
+
/** Minimal write surface of `Subprocess.stdin` we need for framed sends. */
|
|
9
|
+
interface FrameSink {
|
|
10
|
+
write(chunk: string): unknown;
|
|
11
|
+
flush(): unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Write a newline-delimited JSON-RPC frame to the subprocess's stdin sink,
|
|
15
|
+
* swallowing synchronous errors so the caller can decide how to react.
|
|
16
|
+
*
|
|
17
|
+
* Bun's `FileSink` may throw synchronously (most reliably on Windows) when
|
|
18
|
+
* the read end of the pipe has been closed by a subprocess that exited
|
|
19
|
+
* between read-loop ticks. Letting that throw escape an `async` method
|
|
20
|
+
* surfaces as an unhandled promise rejection at the call site.
|
|
21
|
+
*
|
|
22
|
+
* Returns `true` when the frame was accepted by the sink, `false` when the
|
|
23
|
+
* sink threw — callers signal transport closure on `false`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeFrame(stdin: FrameSink, frame: string): boolean;
|
|
8
26
|
/**
|
|
9
27
|
* Stdio transport for MCP servers.
|
|
10
28
|
* Spawns a subprocess and communicates via stdin/stdout.
|
|
@@ -30,3 +48,4 @@ export declare class StdioTransport implements MCPTransport {
|
|
|
30
48
|
* Create and connect a stdio transport.
|
|
31
49
|
*/
|
|
32
50
|
export declare function createStdioTransport(config: MCPStdioServerConfig): Promise<StdioTransport>;
|
|
51
|
+
export {};
|
|
@@ -41,6 +41,8 @@ export declare class WriteTool implements AgentTool<typeof writeSchema, WriteToo
|
|
|
41
41
|
readonly concurrency = "exclusive";
|
|
42
42
|
readonly loadMode = "discoverable";
|
|
43
43
|
readonly summary = "Write content to a file (creates or overwrites)";
|
|
44
|
+
/** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
|
|
45
|
+
matcherDigest(args: unknown): string | undefined;
|
|
44
46
|
constructor(session: ToolSession);
|
|
45
47
|
execute(_toolCallId: string, { path, content }: WriteParams, signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<WriteToolDetails>, context?: AgentToolContext): Promise<AgentToolResult<WriteToolDetails>>;
|
|
46
48
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Result from a completed `jj` subprocess invocation. */
|
|
2
|
+
export interface JjCommandResult {
|
|
3
|
+
/** Process exit code reported by `jj`. */
|
|
4
|
+
exitCode: number;
|
|
5
|
+
/** Captured standard output as UTF-8 text. */
|
|
6
|
+
stdout: string;
|
|
7
|
+
/** Captured standard error as UTF-8 text. */
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
/** Resolved Jujutsu workspace metadata. */
|
|
11
|
+
export interface JjRepository {
|
|
12
|
+
/** Root directory containing the `.jj` workspace metadata. */
|
|
13
|
+
repoRoot: string;
|
|
14
|
+
/** Path to the workspace store directory used to verify a real JJ checkout. */
|
|
15
|
+
storeDir: string;
|
|
16
|
+
}
|
|
17
|
+
/** Options for `jj diff` invocations. */
|
|
18
|
+
export interface DiffOptions {
|
|
19
|
+
/** Optional file paths to restrict the diff with `-- <files>`. */
|
|
20
|
+
readonly files?: readonly string[];
|
|
21
|
+
/** Return only changed file names instead of Git-format diff text. */
|
|
22
|
+
readonly nameOnly?: boolean;
|
|
23
|
+
/** Optional abort signal passed to the spawned `jj` process. */
|
|
24
|
+
readonly signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/** Error thrown when a checked `jj` command exits non-zero. */
|
|
27
|
+
export declare class JjCommandError extends Error {
|
|
28
|
+
/** Arguments passed after the common `jj --no-pager --color=never` prefix. */
|
|
29
|
+
readonly args: readonly string[];
|
|
30
|
+
/** Captured command result that caused the failure. */
|
|
31
|
+
readonly result: JjCommandResult;
|
|
32
|
+
/** Create an error for a failed checked `jj` command. */
|
|
33
|
+
constructor(args: readonly string[], result: JjCommandResult);
|
|
34
|
+
}
|
|
35
|
+
/** Run `jj diff --git` for the current workspace commit and return the raw Git-format diff text. */
|
|
36
|
+
export declare const diff: ((cwd: string, options?: DiffOptions) => Promise<string>) & {
|
|
37
|
+
/** List changed file paths. */
|
|
38
|
+
changedFiles(cwd: string, options?: Pick<DiffOptions, "files" | "signal">): Promise<string[]>;
|
|
39
|
+
};
|
|
40
|
+
export declare const repo: {
|
|
41
|
+
/** Clear cached workspace roots. Intended for tests that mutate JJ metadata under an existing path. */
|
|
42
|
+
clearRootCache(): void;
|
|
43
|
+
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
|
44
|
+
root(cwd: string): Promise<string | null>;
|
|
45
|
+
/** Full Jujutsu workspace metadata. */
|
|
46
|
+
resolve(cwd: string): Promise<JjRepository | null>;
|
|
47
|
+
/** Check whether `cwd` is inside a Jujutsu repository. */
|
|
48
|
+
is(cwd: string): Promise<boolean>;
|
|
49
|
+
};
|
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.8.
|
|
4
|
+
"version": "15.8.2",
|
|
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,14 +47,14 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.22.1",
|
|
48
48
|
"@babel/parser": "^7.29.7",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "15.8.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.8.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.8.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.8.
|
|
54
|
-
"@oh-my-pi/pi-mnemopi": "15.8.
|
|
55
|
-
"@oh-my-pi/pi-natives": "15.8.
|
|
56
|
-
"@oh-my-pi/pi-tui": "15.8.
|
|
57
|
-
"@oh-my-pi/pi-utils": "15.8.
|
|
50
|
+
"@oh-my-pi/hashline": "15.8.2",
|
|
51
|
+
"@oh-my-pi/omp-stats": "15.8.2",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "15.8.2",
|
|
53
|
+
"@oh-my-pi/pi-ai": "15.8.2",
|
|
54
|
+
"@oh-my-pi/pi-mnemopi": "15.8.2",
|
|
55
|
+
"@oh-my-pi/pi-natives": "15.8.2",
|
|
56
|
+
"@oh-my-pi/pi-tui": "15.8.2",
|
|
57
|
+
"@oh-my-pi/pi-utils": "15.8.2",
|
|
58
58
|
"@puppeteer/browsers": "^3.0.4",
|
|
59
59
|
"@types/turndown": "5.0.6",
|
|
60
60
|
"@xterm/headless": "^6.0.0",
|
|
@@ -58,6 +58,13 @@ declare module "@oh-my-pi/pi-tui" {
|
|
|
58
58
|
interface Keybindings extends AppKeybindings {}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Resolve default image-paste shortcuts for the current terminal platform.
|
|
63
|
+
*/
|
|
64
|
+
export function getDefaultPasteImageKeys(platform: NodeJS.Platform = process.platform): KeyId[] {
|
|
65
|
+
return platform === "win32" ? ["ctrl+v", "alt+v"] : ["ctrl+v"];
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
/**
|
|
62
69
|
* All keybindings definitions: TUI + app-specific.
|
|
63
70
|
*/
|
|
@@ -120,7 +127,7 @@ export const KEYBINDINGS = {
|
|
|
120
127
|
description: "Dequeue message",
|
|
121
128
|
},
|
|
122
129
|
"app.clipboard.pasteImage": {
|
|
123
|
-
defaultKeys:
|
|
130
|
+
defaultKeys: getDefaultPasteImageKeys(),
|
|
124
131
|
description: "Paste image from clipboard",
|
|
125
132
|
},
|
|
126
133
|
"app.clipboard.pasteTextRaw": {
|
|
@@ -53,6 +53,17 @@ function discoveryDefaultMaxTokens(api: Api | undefined): number {
|
|
|
53
53
|
return api === "anthropic-messages" ? DISCOVERY_DEFAULT_MAX_TOKENS_ANTHROPIC : DISCOVERY_DEFAULT_MAX_TOKENS;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const SPECIAL_MODEL_MANAGER_PROVIDER_IDS: readonly string[] = [
|
|
57
|
+
"google-antigravity",
|
|
58
|
+
"google-gemini-cli",
|
|
59
|
+
"openai-codex",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
|
|
63
|
+
...PROVIDER_DESCRIPTORS.map(descriptor => descriptor.providerId),
|
|
64
|
+
...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
|
|
65
|
+
];
|
|
66
|
+
|
|
56
67
|
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
|
|
57
68
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
|
|
58
69
|
import { isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
@@ -1133,28 +1144,28 @@ export class ModelRegistry {
|
|
|
1133
1144
|
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
|
|
1134
1145
|
const cachedModels: Model<Api>[] = [];
|
|
1135
1146
|
const authoritativeFreshProviders = new Set<string>();
|
|
1136
|
-
for (const
|
|
1137
|
-
if (configuredDiscoveryProviders.has(
|
|
1147
|
+
for (const providerId of STARTUP_MODEL_CACHE_PROVIDER_IDS) {
|
|
1148
|
+
if (configuredDiscoveryProviders.has(providerId)) {
|
|
1138
1149
|
continue;
|
|
1139
1150
|
}
|
|
1140
|
-
const cache = readModelCache<Api>(
|
|
1151
|
+
const cache = readModelCache<Api>(providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1141
1152
|
if (!cache) {
|
|
1142
1153
|
continue;
|
|
1143
1154
|
}
|
|
1144
1155
|
if (cache.fresh && cache.authoritative) {
|
|
1145
|
-
authoritativeFreshProviders.add(
|
|
1156
|
+
authoritativeFreshProviders.add(providerId);
|
|
1146
1157
|
}
|
|
1147
1158
|
const models = cache.models.map(model =>
|
|
1148
|
-
model.provider ===
|
|
1159
|
+
model.provider === providerId ? model : { ...model, provider: providerId },
|
|
1149
1160
|
);
|
|
1150
|
-
const providerOverride = this.#providerOverrides.get(
|
|
1161
|
+
const providerOverride = this.#providerOverrides.get(providerId);
|
|
1151
1162
|
const withTransport = providerOverride
|
|
1152
1163
|
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1153
1164
|
: models;
|
|
1154
1165
|
const withCompat = providerOverride?.compat
|
|
1155
1166
|
? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) }))
|
|
1156
1167
|
: withTransport;
|
|
1157
|
-
cachedModels.push(...this.#applyProviderModelOverrides(
|
|
1168
|
+
cachedModels.push(...this.#applyProviderModelOverrides(providerId, withCompat));
|
|
1158
1169
|
}
|
|
1159
1170
|
return { models: cachedModels, authoritativeFreshProviders };
|
|
1160
1171
|
}
|
|
@@ -17,6 +17,7 @@ import rsResultType from "./rs-result-type.md" with { type: "text" };
|
|
|
17
17
|
import tsBareCatch from "./ts-bare-catch.md" with { type: "text" };
|
|
18
18
|
import tsImportType from "./ts-import-type.md" with { type: "text" };
|
|
19
19
|
import tsNoAny from "./ts-no-any.md" with { type: "text" };
|
|
20
|
+
import tsNoDeprecatedLeftovers from "./ts-no-deprecated-leftovers.md" with { type: "text" };
|
|
20
21
|
import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
|
|
21
22
|
import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
|
|
22
23
|
import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
|
|
@@ -40,6 +41,7 @@ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
|
|
|
40
41
|
{ name: "ts-bare-catch", content: tsBareCatch },
|
|
41
42
|
{ name: "ts-import-type", content: tsImportType },
|
|
42
43
|
{ name: "ts-no-any", content: tsNoAny },
|
|
44
|
+
{ name: "ts-no-deprecated-leftovers", content: tsNoDeprecatedLeftovers },
|
|
43
45
|
{ name: "ts-no-dynamic-import", content: tsNoDynamicImport },
|
|
44
46
|
{ name: "ts-no-return-type", content: tsNoReturnType },
|
|
45
47
|
{ name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Do not leave `@deprecated` shims behind after refactors — update call sites and remove the old API"
|
|
3
|
+
condition: "@deprecated"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Do not use `@deprecated` as a substitute for finishing a refactor. If an API is obsolete inside the code you control, update every call site and remove the old name in the same change.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Deprecated aliases keep two contracts alive.
|
|
12
|
+
- Future maintainers must preserve behavior nobody should call.
|
|
13
|
+
- Tests can pass while production code keeps using the old path.
|
|
14
|
+
- The next refactor has to unwind both the real API and the compatibility layer.
|
|
15
|
+
|
|
16
|
+
## Avoid
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Bad — leaves a stale compatibility name instead of finishing the cutover.
|
|
20
|
+
/** @deprecated Use loadSettings instead. */
|
|
21
|
+
export const loadConfig = loadSettings;
|
|
22
|
+
|
|
23
|
+
// Bad — preserves an obsolete wrapper after callers can be updated.
|
|
24
|
+
/** @deprecated Use createClient instead. */
|
|
25
|
+
export function makeClient(options: ClientOptions): Client {
|
|
26
|
+
return createClient(options);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// Update all imports and call sites to the durable name.
|
|
34
|
+
export function loadSettings(path: string): Settings { ... }
|
|
35
|
+
export function createClient(options: ClientOptions): Client { ... }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Exceptions
|
|
39
|
+
|
|
40
|
+
- Public package APIs with a documented migration window.
|
|
41
|
+
- Third-party declarations where the deprecated marker reflects an external contract.
|
|
42
|
+
- Tests that intentionally verify deprecated API behavior during a supported transition.
|
|
43
|
+
|
|
44
|
+
If an exception applies, state the external compatibility requirement. Otherwise, finish the refactor and delete the deprecated symbol.
|
package/src/edit/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
|
24
24
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
25
25
|
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
26
26
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
27
|
+
import { EDIT_MODE_STRATEGIES } from "./streaming";
|
|
27
28
|
|
|
28
29
|
export * from "@oh-my-pi/hashline";
|
|
29
30
|
export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/edit-mode";
|
|
@@ -360,6 +361,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
360
361
|
return "apply_patch";
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Normalize streamed args into the source text this edit introduces, so
|
|
366
|
+
* stream matchers (TTSR rules) run against real file content instead of the
|
|
367
|
+
* mode-specific patch grammar.
|
|
368
|
+
*/
|
|
369
|
+
matcherDigest(args: unknown): string | undefined {
|
|
370
|
+
return EDIT_MODE_STRATEGIES[this.mode].matcherDigest(args);
|
|
371
|
+
}
|
|
372
|
+
|
|
363
373
|
async execute(
|
|
364
374
|
_toolCallId: string,
|
|
365
375
|
params: EditParams,
|
package/src/edit/streaming.ts
CHANGED
|
@@ -69,6 +69,14 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
69
69
|
* compute returned `null` because args are still too partial).
|
|
70
70
|
*/
|
|
71
71
|
renderStreamingFallback(args: Args, uiTheme: Theme): string;
|
|
72
|
+
/**
|
|
73
|
+
* Project the (potentially partial) args onto the plain text the edit
|
|
74
|
+
* introduces into files — added lines without patch grammar — so stream
|
|
75
|
+
* matchers (TTSR rules) can run source-level patterns against real content
|
|
76
|
+
* instead of the mode-specific wire format. Returns `undefined` when the
|
|
77
|
+
* args don't yet carry any content.
|
|
78
|
+
*/
|
|
79
|
+
matcherDigest(args: Args): string | undefined;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
// -----------------------------------------------------------------------------
|
|
@@ -161,6 +169,28 @@ function groupApplyPatchEntriesByPath(entries: readonly ApplyPatchEntry[]): Map<
|
|
|
161
169
|
return groups;
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Extract the lines a patch-style payload adds (`+` prefix, excluding `+++ `
|
|
174
|
+
* file headers), stripped of the prefix. When the text carries no added lines,
|
|
175
|
+
* returns the whole text if `fallbackToWhole` (full-content payloads such as a
|
|
176
|
+
* `create` op), otherwise an empty string (grammar-only payloads).
|
|
177
|
+
*/
|
|
178
|
+
function extractAddedLines(text: string, fallbackToWhole: boolean): string {
|
|
179
|
+
let added: string | undefined;
|
|
180
|
+
let lineStart = 0;
|
|
181
|
+
while (lineStart <= text.length) {
|
|
182
|
+
let lineEnd = text.indexOf("\n", lineStart);
|
|
183
|
+
if (lineEnd === -1) lineEnd = text.length;
|
|
184
|
+
if (text.charCodeAt(lineStart) === 43 /* + */ && !text.startsWith("+++ ", lineStart)) {
|
|
185
|
+
const line = text.slice(lineStart + 1, lineEnd);
|
|
186
|
+
added = added === undefined ? line : `${added}\n${line}`;
|
|
187
|
+
}
|
|
188
|
+
lineStart = lineEnd + 1;
|
|
189
|
+
}
|
|
190
|
+
if (added === undefined) return fallbackToWhole ? text : "";
|
|
191
|
+
return added;
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
// -----------------------------------------------------------------------------
|
|
165
195
|
// Strategies
|
|
166
196
|
// -----------------------------------------------------------------------------
|
|
@@ -196,6 +226,16 @@ const replaceStrategy: EditStreamingStrategy<ReplaceArgs> = {
|
|
|
196
226
|
renderStreamingFallback() {
|
|
197
227
|
return "";
|
|
198
228
|
},
|
|
229
|
+
matcherDigest(args) {
|
|
230
|
+
const edits = args?.edits;
|
|
231
|
+
if (!Array.isArray(edits)) return undefined;
|
|
232
|
+
let digest: string | undefined;
|
|
233
|
+
for (const edit of edits) {
|
|
234
|
+
if (typeof edit?.new_text !== "string") continue;
|
|
235
|
+
digest = digest === undefined ? edit.new_text : `${digest}\n${edit.new_text}`;
|
|
236
|
+
}
|
|
237
|
+
return digest;
|
|
238
|
+
},
|
|
199
239
|
};
|
|
200
240
|
|
|
201
241
|
interface PatchArgs {
|
|
@@ -225,6 +265,19 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
225
265
|
renderStreamingFallback() {
|
|
226
266
|
return "";
|
|
227
267
|
},
|
|
268
|
+
matcherDigest(args) {
|
|
269
|
+
const edits = args?.edits;
|
|
270
|
+
if (!Array.isArray(edits)) return undefined;
|
|
271
|
+
let digest: string | undefined;
|
|
272
|
+
for (const edit of edits) {
|
|
273
|
+
if (typeof edit?.diff !== "string") continue;
|
|
274
|
+
// `create` ops carry full file content in `diff` with no +/- markers;
|
|
275
|
+
// pass that content through whole.
|
|
276
|
+
const added = extractAddedLines(edit.diff, true);
|
|
277
|
+
digest = digest === undefined ? added : `${digest}\n${added}`;
|
|
278
|
+
}
|
|
279
|
+
return digest;
|
|
280
|
+
},
|
|
228
281
|
};
|
|
229
282
|
|
|
230
283
|
interface HashlineArgs {
|
|
@@ -378,6 +431,12 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
378
431
|
// than a sigil dump.
|
|
379
432
|
return "";
|
|
380
433
|
},
|
|
434
|
+
matcherDigest(args) {
|
|
435
|
+
const input = args?.input;
|
|
436
|
+
if (typeof input !== "string") return undefined;
|
|
437
|
+
// Body rows are `+TEXT`; headers and op lines are grammar, never content.
|
|
438
|
+
return extractAddedLines(input, false);
|
|
439
|
+
},
|
|
381
440
|
};
|
|
382
441
|
|
|
383
442
|
interface ApplyPatchArgs {
|
|
@@ -430,6 +489,12 @@ const applyPatchStrategy: EditStreamingStrategy<ApplyPatchArgs> = {
|
|
|
430
489
|
renderStreamingFallback() {
|
|
431
490
|
return "";
|
|
432
491
|
},
|
|
492
|
+
matcherDigest(args) {
|
|
493
|
+
const input = args?.input;
|
|
494
|
+
if (typeof input !== "string") return undefined;
|
|
495
|
+
// Envelope markers and `@@` hunk headers are grammar, never content.
|
|
496
|
+
return extractAddedLines(input, false);
|
|
497
|
+
},
|
|
433
498
|
};
|
|
434
499
|
export const EDIT_MODE_STRATEGIES: Record<EditMode, EditStreamingStrategy<unknown>> = {
|
|
435
500
|
replace: replaceStrategy as EditStreamingStrategy<unknown>,
|
package/src/export/ttsr.ts
CHANGED
|
@@ -339,7 +339,24 @@ export class TtsrManager {
|
|
|
339
339
|
const bufferKey = this.#bufferKey(context);
|
|
340
340
|
const nextBuffer = `${this.#buffers.get(bufferKey) ?? ""}${delta}`;
|
|
341
341
|
this.#buffers.set(bufferKey, nextBuffer);
|
|
342
|
+
return this.#matchBuffer(nextBuffer, context);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Replace the scoped buffer with a tool-provided normalized snapshot and
|
|
347
|
+
* return matching rules.
|
|
348
|
+
*
|
|
349
|
+
* Used for tools exposing `matcherDigest`: the digest is recomputed from the
|
|
350
|
+
* full (partial) arguments on every delta, so it replaces the buffer instead
|
|
351
|
+
* of being appended to it.
|
|
352
|
+
*/
|
|
353
|
+
checkSnapshot(snapshot: string, context: TtsrMatchContext): Rule[] {
|
|
354
|
+
const bufferKey = this.#bufferKey(context);
|
|
355
|
+
this.#buffers.set(bufferKey, snapshot);
|
|
356
|
+
return this.#matchBuffer(snapshot, context);
|
|
357
|
+
}
|
|
342
358
|
|
|
359
|
+
#matchBuffer(buffer: string, context: TtsrMatchContext): Rule[] {
|
|
343
360
|
const matches: Rule[] = [];
|
|
344
361
|
for (const [name, entry] of this.#rules) {
|
|
345
362
|
if (!this.#canTrigger(name)) {
|
|
@@ -351,7 +368,7 @@ export class TtsrManager {
|
|
|
351
368
|
if (!this.#matchesGlobalPaths(entry, context)) {
|
|
352
369
|
continue;
|
|
353
370
|
}
|
|
354
|
-
if (!this.#matchesCondition(entry,
|
|
371
|
+
if (!this.#matchesCondition(entry, buffer)) {
|
|
355
372
|
continue;
|
|
356
373
|
}
|
|
357
374
|
|