@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.0
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 +39 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/session/session-manager.ts +17 -13
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +17 -0
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
import type { ToolDefinition } from "../../extensibility/extensions";
|
|
4
|
+
import type { Theme } from "../../modes/theme/theme";
|
|
5
|
+
import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
|
|
6
|
+
import * as git from "../../utils/git";
|
|
7
|
+
import { buildExperimentState } from "../state";
|
|
8
|
+
import { openAutoresearchStorageIfExists } from "../storage";
|
|
9
|
+
import type { AutoresearchToolFactoryOptions } from "../types";
|
|
10
|
+
|
|
11
|
+
const updateNotesSchema = Type.Object({
|
|
12
|
+
body: Type.String({
|
|
13
|
+
description: "Replacement markdown body for the active autoresearch session's notes (your durable playbook).",
|
|
14
|
+
}),
|
|
15
|
+
append_idea: Type.Optional(
|
|
16
|
+
Type.String({
|
|
17
|
+
description:
|
|
18
|
+
"When set, append this string as a new bullet under an Ideas section instead of replacing the body. `body` is ignored.",
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
interface UpdateNotesDetails {
|
|
24
|
+
notes: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createUpdateNotesTool(
|
|
28
|
+
options: AutoresearchToolFactoryOptions,
|
|
29
|
+
): ToolDefinition<typeof updateNotesSchema, UpdateNotesDetails> {
|
|
30
|
+
return {
|
|
31
|
+
name: "update_notes",
|
|
32
|
+
label: "Update Notes",
|
|
33
|
+
description:
|
|
34
|
+
"Persist the durable autoresearch playbook (goal, scope notes, hypotheses, ideas backlog) on the active session. Pass `body` to replace the entire notes blob, or `append_idea` to append a single bullet under an `## Ideas` section.",
|
|
35
|
+
parameters: updateNotesSchema,
|
|
36
|
+
defaultInactive: true,
|
|
37
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
38
|
+
const storage = await openAutoresearchStorageIfExists(ctx.cwd);
|
|
39
|
+
const currentBranch = (await git.branch.current(ctx.cwd)) ?? null;
|
|
40
|
+
const session = storage?.getActiveSessionForBranch(currentBranch) ?? null;
|
|
41
|
+
if (!storage || !session) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: "Error: no active autoresearch session for the current branch. Call init_experiment first.",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const nextNotes =
|
|
53
|
+
params.append_idea !== undefined && params.append_idea.trim().length > 0
|
|
54
|
+
? appendIdea(session.notes, params.append_idea.trim())
|
|
55
|
+
: params.body;
|
|
56
|
+
|
|
57
|
+
storage.updateSession(session.id, { notes: nextNotes });
|
|
58
|
+
const refreshed = storage.getSessionById(session.id);
|
|
59
|
+
const loggedRuns = storage.listLoggedRuns(session.id);
|
|
60
|
+
const runtime = options.getRuntime(ctx);
|
|
61
|
+
if (refreshed) {
|
|
62
|
+
runtime.state = buildExperimentState(refreshed, loggedRuns);
|
|
63
|
+
}
|
|
64
|
+
options.dashboard.updateWidget(ctx, runtime);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: "text",
|
|
70
|
+
text:
|
|
71
|
+
params.append_idea !== undefined
|
|
72
|
+
? `Appended idea (${nextNotes.length} chars total).`
|
|
73
|
+
: `Notes updated (${nextNotes.length} chars).`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
details: { notes: nextNotes },
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
renderCall(args, _options, theme): Text {
|
|
80
|
+
const preview = args.append_idea ?? args.body.slice(0, 100);
|
|
81
|
+
return new Text(
|
|
82
|
+
`${theme.fg("toolTitle", theme.bold("update_notes"))} ${theme.fg("muted", truncateToWidth(replaceTabs(preview), 100))}`,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
renderResult(result, _options, theme: Theme): Text {
|
|
88
|
+
const text = replaceTabs(result.content.find(part => part.type === "text")?.text ?? "");
|
|
89
|
+
return new Text(theme.fg("muted", text), 0, 0);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const IDEAS_HEADING = "## Ideas";
|
|
95
|
+
|
|
96
|
+
function appendIdea(currentNotes: string, idea: string): string {
|
|
97
|
+
const trimmed = currentNotes.trimEnd();
|
|
98
|
+
if (trimmed.length === 0) {
|
|
99
|
+
return `${IDEAS_HEADING}\n- ${idea}\n`;
|
|
100
|
+
}
|
|
101
|
+
if (trimmed.includes(IDEAS_HEADING)) {
|
|
102
|
+
const lines = trimmed.split("\n");
|
|
103
|
+
const ideasIndex = lines.findIndex(line => line.trim() === IDEAS_HEADING);
|
|
104
|
+
// find end of ideas section (next heading or end of file)
|
|
105
|
+
let insertAt = lines.length;
|
|
106
|
+
for (let i = ideasIndex + 1; i < lines.length; i += 1) {
|
|
107
|
+
if (/^#{1,6}\s/.test(lines[i] ?? "")) {
|
|
108
|
+
insertAt = i;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
lines.splice(insertAt, 0, `- ${idea}`);
|
|
113
|
+
return `${lines.join("\n")}\n`;
|
|
114
|
+
}
|
|
115
|
+
return `${trimmed}\n\n${IDEAS_HEADING}\n- ${idea}\n`;
|
|
116
|
+
}
|
|
@@ -21,21 +21,6 @@ export interface MetricDef {
|
|
|
21
21
|
unit: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export interface AutoresearchBenchmarkContract {
|
|
25
|
-
command: string | null;
|
|
26
|
-
primaryMetric: string | null;
|
|
27
|
-
metricUnit: string;
|
|
28
|
-
direction: MetricDirection | null;
|
|
29
|
-
secondaryMetrics: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AutoresearchContract {
|
|
33
|
-
benchmark: AutoresearchBenchmarkContract;
|
|
34
|
-
scopePaths: string[];
|
|
35
|
-
offLimits: string[];
|
|
36
|
-
constraints: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
24
|
export interface ExperimentResult {
|
|
40
25
|
runNumber: number | null;
|
|
41
26
|
commit: string;
|
|
@@ -47,6 +32,11 @@ export interface ExperimentResult {
|
|
|
47
32
|
segment: number;
|
|
48
33
|
confidence: number | null;
|
|
49
34
|
asi?: ASIData;
|
|
35
|
+
modifiedPaths: string[];
|
|
36
|
+
scopeDeviations: string[];
|
|
37
|
+
justification: string | null;
|
|
38
|
+
flagged: boolean;
|
|
39
|
+
flaggedReason: string | null;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
42
|
export interface ExperimentState {
|
|
@@ -57,13 +47,17 @@ export interface ExperimentState {
|
|
|
57
47
|
metricUnit: string;
|
|
58
48
|
secondaryMetrics: MetricDef[];
|
|
59
49
|
name: string | null;
|
|
50
|
+
goal: string | null;
|
|
60
51
|
currentSegment: number;
|
|
61
52
|
maxExperiments: number | null;
|
|
62
53
|
confidence: number | null;
|
|
63
|
-
benchmarkCommand: string | null;
|
|
64
54
|
scopePaths: string[];
|
|
65
55
|
offLimits: string[];
|
|
66
56
|
constraints: string[];
|
|
57
|
+
notes: string;
|
|
58
|
+
branch: string | null;
|
|
59
|
+
baselineCommit: string | null;
|
|
60
|
+
sessionId: number | null;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
63
|
export interface RunExperimentProgressDetails {
|
|
@@ -78,7 +72,6 @@ export interface RunDetails {
|
|
|
78
72
|
runNumber: number;
|
|
79
73
|
runDirectory: string;
|
|
80
74
|
benchmarkLogPath: string;
|
|
81
|
-
checksLogPath?: string;
|
|
82
75
|
command: string;
|
|
83
76
|
exitCode: number | null;
|
|
84
77
|
durationSeconds: number;
|
|
@@ -86,16 +79,13 @@ export interface RunDetails {
|
|
|
86
79
|
crashed: boolean;
|
|
87
80
|
timedOut: boolean;
|
|
88
81
|
tailOutput: string;
|
|
89
|
-
checksPass: boolean | null;
|
|
90
|
-
checksTimedOut: boolean;
|
|
91
|
-
checksOutput: string;
|
|
92
|
-
checksDuration: number;
|
|
93
82
|
parsedMetrics: NumericMetricMap | null;
|
|
94
83
|
parsedPrimary: number | null;
|
|
95
84
|
parsedAsi: ASIData | null;
|
|
96
85
|
metricName: string;
|
|
97
86
|
metricUnit: string;
|
|
98
87
|
preRunDirtyPaths: string[];
|
|
88
|
+
abandonedPriorRun: number | null;
|
|
99
89
|
truncation?: TruncationResult;
|
|
100
90
|
fullOutputPath?: string;
|
|
101
91
|
}
|
|
@@ -104,18 +94,12 @@ export interface LogDetails {
|
|
|
104
94
|
experiment: ExperimentResult;
|
|
105
95
|
state: ExperimentState;
|
|
106
96
|
wallClockSeconds: number | null;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
pass: boolean;
|
|
111
|
-
output: string;
|
|
112
|
-
duration: number;
|
|
97
|
+
scopeDeviations: string[];
|
|
98
|
+
justification: string | null;
|
|
99
|
+
flaggedRuns: Array<{ runId: number; reason: string }>;
|
|
113
100
|
}
|
|
114
101
|
|
|
115
102
|
export interface PendingRunSummary {
|
|
116
|
-
checksDurationSeconds: number | null;
|
|
117
|
-
checksPass: boolean | null;
|
|
118
|
-
checksTimedOut: boolean;
|
|
119
103
|
command: string;
|
|
120
104
|
durationSeconds: number | null;
|
|
121
105
|
parsedAsi: ASIData | null;
|
|
@@ -125,6 +109,8 @@ export interface PendingRunSummary {
|
|
|
125
109
|
preRunDirtyPaths: string[];
|
|
126
110
|
runDirectory: string;
|
|
127
111
|
runNumber: number;
|
|
112
|
+
exitCode: number | null;
|
|
113
|
+
timedOut: boolean;
|
|
128
114
|
}
|
|
129
115
|
|
|
130
116
|
export interface RunningExperiment {
|
|
@@ -139,7 +125,6 @@ export interface AutoresearchRuntime {
|
|
|
139
125
|
autoResumeArmed: boolean;
|
|
140
126
|
dashboardExpanded: boolean;
|
|
141
127
|
lastAutoResumePendingRunNumber: number | null;
|
|
142
|
-
lastRunChecks: ChecksResult | null;
|
|
143
128
|
lastRunDuration: number | null;
|
|
144
129
|
lastRunAsi: ASIData | null;
|
|
145
130
|
lastRunArtifactDir: string | null;
|
|
@@ -150,41 +135,6 @@ export interface AutoresearchRuntime {
|
|
|
150
135
|
goal: string | null;
|
|
151
136
|
}
|
|
152
137
|
|
|
153
|
-
export interface AutoresearchConfig {
|
|
154
|
-
maxIterations?: number;
|
|
155
|
-
workingDir?: string;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export interface AutoresearchJsonConfigEntry {
|
|
159
|
-
type: "config";
|
|
160
|
-
name?: string;
|
|
161
|
-
metricName?: string;
|
|
162
|
-
metricUnit?: string;
|
|
163
|
-
bestDirection?: MetricDirection;
|
|
164
|
-
benchmarkCommand?: string;
|
|
165
|
-
secondaryMetrics?: string[];
|
|
166
|
-
scopePaths?: string[];
|
|
167
|
-
offLimits?: string[];
|
|
168
|
-
constraints?: string[];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface AutoresearchJsonRunEntry {
|
|
172
|
-
run?: number;
|
|
173
|
-
commit?: string;
|
|
174
|
-
metric?: number;
|
|
175
|
-
metrics?: NumericMetricMap;
|
|
176
|
-
status?: ExperimentStatus;
|
|
177
|
-
description?: string;
|
|
178
|
-
timestamp?: number;
|
|
179
|
-
confidence?: number | null;
|
|
180
|
-
asi?: ASIData;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export interface ReconstructedExperimentData {
|
|
184
|
-
hasLog: boolean;
|
|
185
|
-
state: ExperimentState;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
138
|
export interface AutoresearchControlEntryData {
|
|
189
139
|
mode: "on" | "off" | "clear";
|
|
190
140
|
goal?: string;
|
|
@@ -973,7 +973,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
973
973
|
ui: {
|
|
974
974
|
tab: "editing",
|
|
975
975
|
label: "Edit Mode",
|
|
976
|
-
description: "Select the edit tool variant (replace, patch, hashline,
|
|
976
|
+
description: "Select the edit tool variant (replace, patch, hashline, vim, or apply_patch)",
|
|
977
977
|
},
|
|
978
978
|
},
|
|
979
979
|
|
package/src/config/settings.ts
CHANGED
|
@@ -326,7 +326,7 @@ export class Settings {
|
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
328
|
* Get the edit variant for a specific model.
|
|
329
|
-
* Returns "patch", "replace", "hashline", "
|
|
329
|
+
* Returns "patch", "replace", "hashline", "vim", "apply_patch", or null (use global default).
|
|
330
330
|
*/
|
|
331
331
|
getEditVariantForModel(model: string | undefined): EditMode | null {
|
|
332
332
|
if (!model) return null;
|
|
@@ -533,6 +533,25 @@ export class Settings {
|
|
|
533
533
|
delete isolationObj.enabled;
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
// edit.mode: removed "atom" variant is now "hashline"
|
|
537
|
+
const editObj = raw.edit as Record<string, unknown> | undefined;
|
|
538
|
+
if (editObj) {
|
|
539
|
+
if (editObj.mode === "atom") {
|
|
540
|
+
editObj.mode = "hashline";
|
|
541
|
+
}
|
|
542
|
+
const modelVariants = editObj.modelVariants as Record<string, unknown> | undefined;
|
|
543
|
+
if (modelVariants && typeof modelVariants === "object" && !Array.isArray(modelVariants)) {
|
|
544
|
+
for (const [pattern, variant] of Object.entries(modelVariants)) {
|
|
545
|
+
if (variant === "atom") {
|
|
546
|
+
modelVariants[pattern] = "hashline";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (raw["edit.mode"] === "atom") {
|
|
552
|
+
raw["edit.mode"] = "hashline";
|
|
553
|
+
}
|
|
554
|
+
|
|
536
555
|
// statusLine: rename "plan_mode" segment to "mode"
|
|
537
556
|
const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
|
|
538
557
|
if (statusLineObj) {
|
package/src/cursor.ts
CHANGED
|
@@ -180,7 +180,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
180
180
|
const searchPath = args.glob ? `${args.path || "."}/${args.glob}` : args.path || ".";
|
|
181
181
|
const toolResultMessage = await executeTool(this.options, "search", toolCallId, {
|
|
182
182
|
pattern: args.pattern,
|
|
183
|
-
|
|
183
|
+
paths: [searchPath],
|
|
184
184
|
i: args.caseInsensitive || undefined,
|
|
185
185
|
});
|
|
186
186
|
return toolResultMessage;
|
package/src/edit/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
writethroughNoop,
|
|
10
10
|
} from "../lsp";
|
|
11
11
|
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
12
|
-
import atomDescription from "../prompts/tools/atom.md" with { type: "text" };
|
|
13
12
|
import hashlineDescription from "../prompts/tools/hashline.md" with { type: "text" };
|
|
14
13
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
15
14
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
@@ -17,17 +16,16 @@ import type { ToolSession } from "../tools";
|
|
|
17
16
|
import { VimTool, vimSchema } from "../tools/vim";
|
|
18
17
|
import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
|
|
19
18
|
import type { VimToolDetails } from "../vim/types";
|
|
19
|
+
import { resolveLarkLidPlaceholders } from "./line-hash";
|
|
20
20
|
import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
|
|
21
21
|
import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
|
|
22
|
-
import { type AtomParams, atomEditParamsSchema, executeAtomSingle } from "./modes/atom";
|
|
23
|
-
import atomGrammar from "./modes/atom.lark" with { type: "text" };
|
|
24
22
|
import {
|
|
25
23
|
executeHashlineSingle,
|
|
26
24
|
HashlineMismatchError,
|
|
27
25
|
type HashlineParams,
|
|
28
|
-
type HashlineToolEdit,
|
|
29
26
|
hashlineEditParamsSchema,
|
|
30
27
|
} from "./modes/hashline";
|
|
28
|
+
import hashlineGrammarTemplate from "./modes/hashline.lark" with { type: "text" };
|
|
31
29
|
import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
|
|
32
30
|
import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
|
|
33
31
|
import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
|
|
@@ -36,8 +34,11 @@ export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/ed
|
|
|
36
34
|
export * from "./apply-patch";
|
|
37
35
|
export * from "./diff";
|
|
38
36
|
export * from "./line-hash";
|
|
37
|
+
|
|
38
|
+
// Resolve the `$HASHFMT$` placeholder in the hashline Lark grammar.
|
|
39
|
+
const hashlineGrammar = resolveLarkLidPlaceholders(hashlineGrammarTemplate);
|
|
40
|
+
|
|
39
41
|
export * from "./modes/apply-patch";
|
|
40
|
-
export * from "./modes/atom";
|
|
41
42
|
export * from "./modes/hashline";
|
|
42
43
|
export * from "./modes/patch";
|
|
43
44
|
export * from "./modes/replace";
|
|
@@ -49,12 +50,11 @@ type TInput =
|
|
|
49
50
|
| typeof replaceEditSchema
|
|
50
51
|
| typeof patchEditSchema
|
|
51
52
|
| typeof hashlineEditParamsSchema
|
|
52
|
-
| typeof atomEditParamsSchema
|
|
53
53
|
| typeof vimSchema
|
|
54
54
|
| typeof applyPatchSchema;
|
|
55
55
|
|
|
56
56
|
type VimParams = Static<typeof vimSchema>;
|
|
57
|
-
type EditParams = ReplaceParams | PatchParams | HashlineParams |
|
|
57
|
+
type EditParams = ReplaceParams | PatchParams | HashlineParams | VimParams | ApplyPatchParams;
|
|
58
58
|
type EditToolResultDetails = EditToolDetails | VimToolDetails;
|
|
59
59
|
|
|
60
60
|
type EditModeDefinition = {
|
|
@@ -292,7 +292,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
292
292
|
*/
|
|
293
293
|
get customFormat(): { syntax: "lark"; definition: string } | undefined {
|
|
294
294
|
if (this.mode === "apply_patch") return { syntax: "lark", definition: applyPatchGrammar };
|
|
295
|
-
if (this.mode === "
|
|
295
|
+
if (this.mode === "hashline") return { syntax: "lark", definition: hashlineGrammar };
|
|
296
296
|
return undefined;
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -390,30 +390,8 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
390
390
|
batchRequest: LspBatchRequest | undefined,
|
|
391
391
|
_onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
392
392
|
) => {
|
|
393
|
-
const {
|
|
393
|
+
const { input, path } = params as HashlineParams & { path?: string };
|
|
394
394
|
return executeHashlineSingle({
|
|
395
|
-
session: tool.session,
|
|
396
|
-
path,
|
|
397
|
-
edits: edits as HashlineToolEdit[],
|
|
398
|
-
signal,
|
|
399
|
-
batchRequest,
|
|
400
|
-
writethrough: tool.#writethrough,
|
|
401
|
-
beginDeferredDiagnosticsForPath: p => tool.#beginDeferredDiagnosticsForPath(p),
|
|
402
|
-
});
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
atom: {
|
|
406
|
-
description: () => prompt.render(atomDescription),
|
|
407
|
-
parameters: atomEditParamsSchema,
|
|
408
|
-
execute: (
|
|
409
|
-
tool: EditTool,
|
|
410
|
-
params: EditParams,
|
|
411
|
-
signal: AbortSignal | undefined,
|
|
412
|
-
batchRequest: LspBatchRequest | undefined,
|
|
413
|
-
_onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
414
|
-
) => {
|
|
415
|
-
const { input, path } = params as AtomParams & { path?: string };
|
|
416
|
-
return executeAtomSingle({
|
|
417
395
|
session: tool.session,
|
|
418
396
|
input,
|
|
419
397
|
path,
|
package/src/edit/line-hash.ts
CHANGED
|
@@ -668,66 +668,93 @@ export const HASHLINE_BIGRAMS = [
|
|
|
668
668
|
export const HASHLINE_BIGRAMS_COUNT = HASHLINE_BIGRAMS.length;
|
|
669
669
|
|
|
670
670
|
/**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
671
|
+
* Decoration prefix that may precede a `LINE+HASH` anchor in tool output:
|
|
672
|
+
* `>` (context line in grep), `+` (added line in diff), `-` (removed line),
|
|
673
|
+
* `*` (match line). Any combination, in any order, surrounded by optional
|
|
674
|
+
* whitespace. Output formatters emit at most one decoration per anchor; the
|
|
675
|
+
* regex stays liberal because anchor-ref parsers accept whatever the model
|
|
676
|
+
* echoes back.
|
|
673
677
|
*/
|
|
674
|
-
export const
|
|
678
|
+
export const HASHLINE_ANCHOR_DECORATION_RE_SRC = `\\s*[>+\\-*]*\\s*`;
|
|
675
679
|
|
|
676
|
-
|
|
680
|
+
/**
|
|
681
|
+
* Capture-group regex source for a decorated `LINE+HASH` anchor. Group 1
|
|
682
|
+
* captures the line number (digits only); group 2 captures the hash. The
|
|
683
|
+
* source is intentionally unanchored — anchoring with `^` (or composing into a
|
|
684
|
+
* larger pattern) is the caller's responsibility.
|
|
685
|
+
*/
|
|
686
|
+
export const HASHLINE_ANCHOR_RE_SRC = `${HASHLINE_ANCHOR_DECORATION_RE_SRC}(\\d+)([a-z]{2})`;
|
|
677
687
|
|
|
678
|
-
|
|
679
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Bare `LINE+HASH` Lid (no decorations, no captures, no anchors). Use for
|
|
690
|
+
* embedding inside larger patterns where the line+hash unit appears as a
|
|
691
|
+
* literal (e.g. range bounds, alternation arms, op-line heuristics).
|
|
692
|
+
*/
|
|
693
|
+
export const HASHLINE_LID_RE_SRC = `[1-9]\\d*[a-z]{2}`;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Capture-group form of {@link HASHLINE_LID_RE_SRC}: group 1 captures the
|
|
697
|
+
* line number, group 2 captures the hash.
|
|
698
|
+
*/
|
|
699
|
+
export const HASHLINE_LID_CAPTURE_RE_SRC = `([1-9]\\d*)([a-z]{2})`;
|
|
700
|
+
|
|
701
|
+
/** Width of a hash in display characters. */
|
|
702
|
+
export const HASHLINE_HASH_WIDTH = 2;
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Representative hash suffixes for use in user-facing error messages and
|
|
706
|
+
* prompt examples.
|
|
707
|
+
*/
|
|
708
|
+
export const HASHLINE_HASH_EXAMPLES = ["sr", "ab", "th"] as const;
|
|
680
709
|
|
|
681
710
|
/**
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
* `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
|
|
685
|
-
* line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
|
|
686
|
-
* `100th`, …). Brace-only lines therefore cost one token for the whole
|
|
687
|
-
* `LINE+ID` anchor instead of two.
|
|
711
|
+
* Format a comma-separated list of example anchors with an optional line-number
|
|
712
|
+
* prefix, quoted for inclusion in error messages: `"160sr", "160ab", "160th"`.
|
|
688
713
|
*/
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
if (mod100 >= 11 && mod100 <= 13) return "th";
|
|
692
|
-
switch (line % 10) {
|
|
693
|
-
case 1:
|
|
694
|
-
return "st";
|
|
695
|
-
case 2:
|
|
696
|
-
return "nd";
|
|
697
|
-
case 3:
|
|
698
|
-
return "rd";
|
|
699
|
-
default:
|
|
700
|
-
return "th";
|
|
701
|
-
}
|
|
714
|
+
export function describeAnchorExamples(linePrefix = ""): string {
|
|
715
|
+
return HASHLINE_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", ");
|
|
702
716
|
}
|
|
703
717
|
|
|
704
718
|
/**
|
|
705
|
-
*
|
|
719
|
+
* Sentinel token that the hashline Lark grammar uses for the hash
|
|
720
|
+
* regex source. Replaced at module-load time by {@link resolveLarkLidPlaceholders}
|
|
721
|
+
* so the grammar is re-derived from a single source of truth alongside its
|
|
722
|
+
* TypeScript consumers. Update the placeholder name here and in the grammar together.
|
|
723
|
+
*/
|
|
724
|
+
export const LARK_LID_HASH_PLACEHOLDER = "$HASHFMT$";
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Substitute the LID hash placeholder in a Lark grammar text with the
|
|
728
|
+
* `[a-z]{2}` hash regex source. Grammars that don't reference Lids pass
|
|
729
|
+
* through unchanged.
|
|
730
|
+
*/
|
|
731
|
+
export function resolveLarkLidPlaceholders(grammar: string): string {
|
|
732
|
+
return grammar.replaceAll(LARK_LID_HASH_PLACEHOLDER, "[a-z]{2}");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export const HASHLINE_CONTENT_SEPARATOR = "|";
|
|
736
|
+
|
|
737
|
+
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Compute a 2-character hash of a single line via xxHash32 mod 647 over
|
|
741
|
+
* {@link HASHLINE_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`,
|
|
742
|
+
* bare `{`) mix the line number into the seed so adjacent identical
|
|
743
|
+
* brace-only lines get distinct hashes; lines with significant content stay
|
|
744
|
+
* line-number-independent so a line is identifiable across small shifts.
|
|
706
745
|
*
|
|
707
|
-
* Uses xxHash32 on a trailing-whitespace-trimmed, CR-stripped line, mapped into
|
|
708
|
-
* {@link HASHLINE_BIGRAMS} via modulo. Lines that contain only whitespace and
|
|
709
|
-
* `{`/`}` collapse to an ordinal-suffix bigram (see {@link structuralBigram})
|
|
710
|
-
* so brace-only structure shares one merged ordinal token. For other lines
|
|
711
|
-
* containing no alphanumeric characters, the line number is mixed in to reduce hash collisions.
|
|
712
746
|
* The line input should not include a trailing newline.
|
|
713
747
|
*/
|
|
714
748
|
export function computeLineHash(idx: number, line: string): string {
|
|
715
749
|
line = line.replace(/\r/g, "").trimEnd();
|
|
716
|
-
|
|
717
|
-
if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
|
|
718
|
-
return structuralBigram(idx);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
let seed = 0;
|
|
722
|
-
if (!RE_SIGNIFICANT.test(line)) {
|
|
723
|
-
seed = idx;
|
|
724
|
-
}
|
|
750
|
+
const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
|
|
725
751
|
return HASHLINE_BIGRAMS[Bun.hash.xxHash32(line, seed) % HASHLINE_BIGRAMS_COUNT];
|
|
726
752
|
}
|
|
727
753
|
|
|
728
754
|
/**
|
|
729
755
|
* Formats an anchor reference given a line number and its text.
|
|
730
|
-
* Returns `LINE+ID` (e.g., `
|
|
756
|
+
* Returns `LINE+ID` (e.g., `42sr`) — no separator between
|
|
757
|
+
* number and hash.
|
|
731
758
|
*/
|
|
732
759
|
export function formatLineHash(line: number, lines: string): string {
|
|
733
760
|
return `${line}${computeLineHash(line, lines)}`;
|
|
@@ -735,7 +762,7 @@ export function formatLineHash(line: number, lines: string): string {
|
|
|
735
762
|
|
|
736
763
|
/**
|
|
737
764
|
* Formats a single line with a hashline anchor.
|
|
738
|
-
* Returns `LINE+ID|TEXT` (e.g., `
|
|
765
|
+
* Returns `LINE+ID|TEXT` (e.g., `42sr|function hi() {`, `3ab|}`).
|
|
739
766
|
*/
|
|
740
767
|
export function formatHashLine(lineNumber: number, line: string): string {
|
|
741
768
|
return `${lineNumber}${computeLineHash(lineNumber, line)}${HASHLINE_CONTENT_SEPARATOR}${line}`;
|
|
@@ -754,7 +781,7 @@ export function formatHashLine(lineNumber: number, line: string): string {
|
|
|
754
781
|
* @example
|
|
755
782
|
* ```
|
|
756
783
|
* formatHashLines("function hi() {\n return;\n}")
|
|
757
|
-
* // "
|
|
784
|
+
* // "1bm|function hi() {\n2er| return;\n3ab|}"
|
|
758
785
|
* ```
|
|
759
786
|
*/
|
|
760
787
|
export function formatHashLines(text: string, startLine = 1): string {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
%import common.LF
|
|
2
|
+
|
|
3
|
+
start: section+
|
|
4
|
+
|
|
5
|
+
section: file_header line_op*
|
|
6
|
+
|
|
7
|
+
file_header: "@" path LF
|
|
8
|
+
|
|
9
|
+
line_op: insert_before_op payload+
|
|
10
|
+
| insert_after_op payload+
|
|
11
|
+
| replace_op payload*
|
|
12
|
+
| delete_op
|
|
13
|
+
| blank
|
|
14
|
+
|
|
15
|
+
insert_before_op: "<" insert_target LF
|
|
16
|
+
insert_after_op: "+" insert_target LF
|
|
17
|
+
replace_op: "=" range LF
|
|
18
|
+
delete_op: "-" range LF
|
|
19
|
+
payload: "|" /[^\r\n]*/ LF
|
|
20
|
+
|
|
21
|
+
insert_target: LID | "EOF" | "BOF"
|
|
22
|
+
range: LID (".." LID)?
|
|
23
|
+
|
|
24
|
+
path: /(?:[^\s\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
|
|
25
|
+
LID: /[1-9][0-9]*$HASHFMT$/
|
|
26
|
+
blank: LF
|