pi-hashline-edit-pro 6.1.2 → 6.2.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/README.md +14 -12
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/auto-read-all.ts +39 -21
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +4 -4
- package/src/grep.ts +3 -3
- package/src/insert.ts +3 -3
- package/src/outline/index.ts +45 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +166 -0
- package/src/outline/vendor/LICENSE +201 -0
- package/src/outline/vendor/README.md +17 -0
- package/src/outline/vendor/parsers/blade.ts +138 -0
- package/src/outline/vendor/parsers/index.ts +75 -0
- package/src/outline/vendor/parsers/java.ts +501 -0
- package/src/outline/vendor/parsers/manager.ts +1721 -0
- package/src/outline/vendor/parsers/php.ts +332 -0
- package/src/outline/vendor/parsers/sass.ts +416 -0
- package/src/outline/vendor/parsers/scss.ts +566 -0
- package/src/outline/vendor/types.ts +82 -0
- package/src/outline/vendor/wasm/LICENSES.md +37 -0
- package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/read.ts +108 -36
- package/src/replace-match.ts +3 -3
- package/src/replace-undo.ts +3 -3
- package/src/replace.ts +3 -3
- package/tool-prompts/read-guidelines.md +3 -0
- package/{prompts → tool-prompts}/read.md +2 -0
- package/prompts/read-guidelines.md +0 -1
- package/src/auto-read-all-state.ts +0 -27
- /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
- /package/{prompts → tool-prompts}/copy.md +0 -0
- /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
- /package/{prompts → tool-prompts}/grep.md +0 -0
- /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
- /package/{prompts → tool-prompts}/insert.md +0 -0
- /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/move-snippet.md +0 -0
- /package/{prompts → tool-prompts}/move.md +0 -0
- /package/{prompts → tool-prompts}/read-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-match.md +0 -0
- /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace.md +0 -0
- /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
package/src/config-ui.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { readConfig, type Config } from "./config";
|
|
4
4
|
|
|
5
|
-
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput" | "diffContextLines" | "disableOnModels" | "readOnDisabledModels";
|
|
5
|
+
export type ConfigToggleKey = "autoRead" | "autoReadAll" | "autoReadAllRequireGit" | "autoReadAllIgnore" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput" | "diffContextLines" | "disableOnModels" | "readOnDisabledModels";
|
|
6
6
|
|
|
7
7
|
export interface ConfigRow {
|
|
8
8
|
key: ConfigToggleKey;
|
|
9
9
|
label: string;
|
|
10
10
|
hint: string;
|
|
11
|
+
group: string;
|
|
12
|
+
depth: number;
|
|
11
13
|
enabled: boolean;
|
|
14
|
+
dependsOn?: ConfigToggleKey;
|
|
15
|
+
gated?: boolean;
|
|
12
16
|
mode?: string;
|
|
13
17
|
cycle?: string[];
|
|
14
18
|
value?: number;
|
|
@@ -16,53 +20,79 @@ export interface ConfigRow {
|
|
|
16
20
|
disabled?: boolean;
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
function rowActive(row: ConfigRow): boolean {
|
|
24
|
+
if (row.entries !== undefined) return row.entries.length > 0;
|
|
25
|
+
if (row.mode !== undefined) return row.mode !== "off";
|
|
26
|
+
return row.enabled;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export function configRows(config: Config): ConfigRow[] {
|
|
20
|
-
|
|
21
|
-
{ key: "autoRead", label: "Auto-read", hint: "
|
|
22
|
-
{ key: "
|
|
23
|
-
{ key: "
|
|
24
|
-
{ key: "
|
|
25
|
-
{ key: "
|
|
26
|
-
{ key: "
|
|
27
|
-
{ key: "
|
|
28
|
-
{ key: "
|
|
29
|
-
{ key: "
|
|
30
|
-
{ key: "
|
|
31
|
-
{ key: "
|
|
30
|
+
const rows: ConfigRow[] = [
|
|
31
|
+
{ key: "autoRead", group: "Auto-read", depth: 0, label: "Auto-read", hint: "Show fresh anchors after write and post-edit diffs", enabled: config.autoRead !== false },
|
|
32
|
+
{ key: "diffContextLines", group: "Auto-read", depth: 1, dependsOn: "autoRead", gated: true, label: "Diff context", hint: "Context lines around each change in post-edit diffs", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1 },
|
|
33
|
+
{ key: "autoReadAll", group: "Auto-read all", depth: 0, label: "Auto-read all", hint: "Attach project files when a session starts", enabled: (config.autoReadAll ?? "off") !== "off", mode: config.autoReadAll ?? "off", cycle: ["off", "outline", "full"] },
|
|
34
|
+
{ key: "autoReadAllRequireGit", group: "Auto-read all", depth: 1, dependsOn: "autoReadAll", gated: true, label: "Git repos only", hint: "Attach only inside a git repository", enabled: config.autoReadAllRequireGit !== false },
|
|
35
|
+
{ key: "autoReadAllIgnore", group: "Auto-read all", depth: 1, dependsOn: "autoReadAll", label: "Ignore folders/files", hint: "Folders, files, or globs that auto-read all skips (comma-separated)", enabled: (config.autoReadAllIgnore ?? []).length > 0, entries: config.autoReadAllIgnore ?? [] },
|
|
36
|
+
{ key: "anchorGrepEnabled", group: "Tools", depth: 0, label: "Anchor grep", hint: "Use anchor_grep instead of the built-in grep", enabled: config.anchorGrepEnabled === true },
|
|
37
|
+
{ key: "copyMoveEnabled", group: "Tools", depth: 0, label: "Copy/move", hint: "Enable the copy and move tools", enabled: config.copyMoveEnabled !== false },
|
|
38
|
+
{ key: "replaceMatchEnabled", group: "Tools", depth: 0, label: "Replace match", hint: "Enable the replace_match tool", enabled: config.replaceMatchEnabled !== false },
|
|
39
|
+
{ key: "requirePath", group: "Edit behavior", depth: 0, label: "Require path", hint: "Edit tools must also send a matching path", enabled: config.requirePath === true },
|
|
40
|
+
{ key: "strictInput", group: "Edit behavior", depth: 0, label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
|
|
41
|
+
{ key: "disableOnModels", group: "Model gating", depth: 0, label: "Disable on models", hint: "Model globs that turn off anchored tools (comma-separated)", enabled: (config.disableOnModels ?? []).length > 0, entries: config.disableOnModels ?? [] },
|
|
42
|
+
{ key: "readOnDisabledModels", group: "Model gating", depth: 1, dependsOn: "disableOnModels", gated: true, label: "Read on disabled models", hint: "Keep read available for models disabled above", enabled: (config.readOnDisabledModels ?? "vanilla") !== "remove", mode: config.readOnDisabledModels ?? "vanilla", cycle: ["remove", "vanilla"] },
|
|
32
43
|
];
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
if (row.gated !== true || row.dependsOn === undefined) continue;
|
|
46
|
+
const parent = rows.find((candidate) => candidate.key === row.dependsOn);
|
|
47
|
+
row.disabled = parent !== undefined && !rowActive(parent);
|
|
48
|
+
}
|
|
49
|
+
return rows;
|
|
33
50
|
}
|
|
34
51
|
|
|
35
52
|
function padRow(theme: Theme, innerWidth: number, content: string): string {
|
|
36
|
-
const
|
|
53
|
+
const clipped = visibleWidth(content) > innerWidth ? truncateToWidth(content, innerWidth) : content;
|
|
54
|
+
const padded = clipped + " ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)));
|
|
37
55
|
return theme.fg("border", "│") + padded + theme.fg("border", "│");
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
function modeBox(theme: Theme, mode: string): string {
|
|
41
|
-
if (mode === "off") return theme.fg("dim", `[${mode}]`);
|
|
58
|
+
function modeBox(theme: Theme, mode: string, disabled?: boolean): string {
|
|
59
|
+
if (disabled === true || mode === "off") return theme.fg("dim", `[${mode}]`);
|
|
42
60
|
if (mode === "strict") return theme.fg("accent", `[${mode}]`);
|
|
43
61
|
return theme.fg("success", `[${mode}]`);
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
function numberBox(theme: Theme, value: number, disabled?: boolean): string {
|
|
47
|
-
if (disabled) return theme.fg("dim", `[${value}]`);
|
|
65
|
+
if (disabled === true) return theme.fg("dim", `[${value}]`);
|
|
48
66
|
return theme.fg("accent", `[${value}]`);
|
|
49
67
|
}
|
|
50
|
-
|
|
68
|
+
|
|
69
|
+
function listBox(theme: Theme, entries: string[], disabled?: boolean): string {
|
|
70
|
+
if (disabled === true) return theme.fg("dim", `[${entries.length}]`);
|
|
51
71
|
return theme.fg("accent", `[${entries.length}]`);
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
|
|
74
|
+
function rowBox(theme: Theme, row: ConfigRow): string {
|
|
75
|
+
if (row.entries !== undefined) return listBox(theme, row.entries, row.disabled);
|
|
76
|
+
if (row.value !== undefined) return numberBox(theme, row.value, row.disabled);
|
|
77
|
+
if (row.mode !== undefined) return modeBox(theme, row.mode, row.disabled);
|
|
78
|
+
if (row.disabled === true) return theme.fg("dim", row.enabled ? "[x]" : "[ ]");
|
|
79
|
+
return row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rowDetail(row: ConfigRow): string {
|
|
83
|
+
if (row.entries === undefined) return row.hint;
|
|
84
|
+
const joined = row.entries.length > 0 ? row.entries.join(", ") : "(empty)";
|
|
85
|
+
return `${row.hint}: ${joined}`;
|
|
57
86
|
}
|
|
58
87
|
|
|
59
88
|
export class HashlineConfigOverlay {
|
|
60
89
|
private rows: ConfigRow[];
|
|
61
90
|
private selected = 0;
|
|
91
|
+
private scrollTop = 0;
|
|
62
92
|
private editingList = false;
|
|
63
93
|
private editBuffer = "";
|
|
64
94
|
|
|
65
|
-
constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; onToggle: (key: ConfigToggleKey, delta?: number, value?: string) => Promise<void> }) {
|
|
95
|
+
constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; maxHeight?: () => number | undefined; onToggle: (key: ConfigToggleKey, delta?: number, value?: string) => Promise<void> }) {
|
|
66
96
|
this.rows = [];
|
|
67
97
|
}
|
|
68
98
|
|
|
@@ -100,6 +130,16 @@ export class HashlineConfigOverlay {
|
|
|
100
130
|
this.opts.tui.requestRender(true);
|
|
101
131
|
}
|
|
102
132
|
|
|
133
|
+
private applyCycle(row: ConfigRow, step: number): void {
|
|
134
|
+
if (row.cycle === undefined || row.mode === undefined) return;
|
|
135
|
+
const index = row.cycle.indexOf(row.mode);
|
|
136
|
+
const next = row.cycle[(index + step + row.cycle.length) % row.cycle.length];
|
|
137
|
+
if (next === undefined) return;
|
|
138
|
+
row.mode = next;
|
|
139
|
+
row.enabled = next !== "off";
|
|
140
|
+
this.runToggle(row, step);
|
|
141
|
+
}
|
|
142
|
+
|
|
103
143
|
private toggleSelected(): void {
|
|
104
144
|
const row = this.rows[this.selected];
|
|
105
145
|
if (!row || row.disabled) return;
|
|
@@ -112,22 +152,31 @@ export class HashlineConfigOverlay {
|
|
|
112
152
|
this.runToggle(row, 1);
|
|
113
153
|
return;
|
|
114
154
|
}
|
|
115
|
-
if (row.cycle && row.mode !== undefined) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
row.mode = next;
|
|
119
|
-
row.enabled = next !== "off";
|
|
120
|
-
} else {
|
|
121
|
-
row.enabled = !row.enabled;
|
|
155
|
+
if (row.cycle !== undefined && row.mode !== undefined) {
|
|
156
|
+
this.applyCycle(row, 1);
|
|
157
|
+
return;
|
|
122
158
|
}
|
|
159
|
+
row.enabled = !row.enabled;
|
|
123
160
|
this.runToggle(row);
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
private adjustSelected(delta: number): void {
|
|
127
164
|
const row = this.rows[this.selected];
|
|
128
|
-
if (!row || row.disabled
|
|
129
|
-
row.value
|
|
130
|
-
|
|
165
|
+
if (!row || row.disabled) return;
|
|
166
|
+
if (row.value !== undefined) {
|
|
167
|
+
row.value += delta;
|
|
168
|
+
this.runToggle(row, delta);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (row.cycle !== undefined && row.mode !== undefined) {
|
|
172
|
+
this.applyCycle(row, delta > 0 ? 1 : -1);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (row.entries !== undefined) return;
|
|
176
|
+
const wanted = delta > 0;
|
|
177
|
+
if (row.enabled === wanted) return;
|
|
178
|
+
row.enabled = wanted;
|
|
179
|
+
this.runToggle(row);
|
|
131
180
|
}
|
|
132
181
|
|
|
133
182
|
handleInput(data: string): void {
|
|
@@ -177,11 +226,6 @@ export class HashlineConfigOverlay {
|
|
|
177
226
|
this.toggleSelected();
|
|
178
227
|
return;
|
|
179
228
|
}
|
|
180
|
-
const row = this.rows[this.selected];
|
|
181
|
-
if ((data === "e" || data === "E") && row && row.entries !== undefined && !row.disabled) {
|
|
182
|
-
this.startListEdit(row);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
229
|
if (matchesKey(data, Key.escape) || data === "q") {
|
|
186
230
|
this.opts.done();
|
|
187
231
|
}
|
|
@@ -193,24 +237,60 @@ export class HashlineConfigOverlay {
|
|
|
193
237
|
render(width: number): string[] {
|
|
194
238
|
const theme = this.opts.theme;
|
|
195
239
|
const innerWidth = width - 2;
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
|
|
240
|
+
const body: string[] = [];
|
|
241
|
+
const spans: Array<{ start: number; end: number }> = [];
|
|
242
|
+
let group = "";
|
|
200
243
|
this.rows.forEach((row, index) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const suffix = row.entries !== undefined ? ` — ${row.hint}: ${formatList(row.entries)}` : ` — ${row.hint}`;
|
|
205
|
-
lines.push(padRow(theme, innerWidth, `${cursor}${box} ${label} ${theme.fg("dim", suffix)}`));
|
|
206
|
-
if (row.entries !== undefined && index === this.selected && this.editingList) {
|
|
207
|
-
lines.push(padRow(theme, innerWidth, ` ${theme.fg("accent", "edit:")} ${this.editBuffer}█ ${theme.fg("dim", "(Enter save · Esc cancel · Ctrl-U clear)")}`));
|
|
244
|
+
if (row.group !== group) {
|
|
245
|
+
group = row.group;
|
|
246
|
+
body.push(padRow(theme, innerWidth, theme.fg("dim", theme.bold(` ${group}`))));
|
|
208
247
|
}
|
|
248
|
+
const start = body.length;
|
|
249
|
+
const selected = index === this.selected;
|
|
250
|
+
const indent = " ".repeat(row.depth);
|
|
251
|
+
const cursor = selected ? theme.fg("accent", "> ") : " ";
|
|
252
|
+
const box = rowBox(theme, row);
|
|
253
|
+
const label = row.disabled === true ? theme.fg("dim", row.label) : selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
|
|
254
|
+
const headWidth = 2 + indent.length + visibleWidth(box) + 1;
|
|
255
|
+
const contentWidth = Math.max(1, innerWidth - headWidth);
|
|
256
|
+
wrapTextWithAnsi(label, contentWidth).forEach((segment, part) => {
|
|
257
|
+
body.push(padRow(theme, innerWidth, (part === 0 ? `${cursor}${indent}${box} ` : " ".repeat(headWidth)) + segment));
|
|
258
|
+
});
|
|
259
|
+
for (const segment of wrapTextWithAnsi(theme.fg("dim", rowDetail(row)), contentWidth)) {
|
|
260
|
+
body.push(padRow(theme, innerWidth, " ".repeat(headWidth) + segment));
|
|
261
|
+
}
|
|
262
|
+
if (row.entries !== undefined && selected && this.editingList) {
|
|
263
|
+
const edit = `${theme.fg("accent", "edit:")} ${this.editBuffer}█ ${theme.fg("dim", "(Enter save · Esc cancel · Ctrl-U clear)")}`;
|
|
264
|
+
for (const segment of wrapTextWithAnsi(edit, Math.max(1, innerWidth - 2))) {
|
|
265
|
+
body.push(padRow(theme, innerWidth, ` ${segment}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
spans.push({ start, end: body.length - 1 });
|
|
209
269
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
270
|
+
const head = [
|
|
271
|
+
theme.fg("border", `╭${"─".repeat(innerWidth)}╮`),
|
|
272
|
+
padRow(theme, innerWidth, ` ${theme.fg("accent", theme.bold("Hashline Config"))}`),
|
|
273
|
+
theme.fg("border", `├${"─".repeat(innerWidth)}┤`),
|
|
274
|
+
];
|
|
275
|
+
const footer = this.editingList ? " type to edit · Enter save · Esc cancel" : " ↑↓ navigate · space toggle/edit · ←/→ or -/+ adjust · q close";
|
|
276
|
+
const tail = [
|
|
277
|
+
theme.fg("border", `├${"─".repeat(innerWidth)}┤`),
|
|
278
|
+
padRow(theme, innerWidth, theme.fg("dim", footer)),
|
|
279
|
+
theme.fg("border", `╰${"─".repeat(innerWidth)}╯`),
|
|
280
|
+
];
|
|
281
|
+
const limit = this.opts.maxHeight?.();
|
|
282
|
+
if (limit === undefined || body.length + head.length + tail.length <= limit) {
|
|
283
|
+
this.scrollTop = 0;
|
|
284
|
+
return [...head, ...body, ...tail];
|
|
285
|
+
}
|
|
286
|
+
const viewport = Math.max(1, limit - head.length - tail.length);
|
|
287
|
+
const span = spans[this.selected] ?? { start: 0, end: 0 };
|
|
288
|
+
let top = this.scrollTop;
|
|
289
|
+
if (span.end - span.start + 1 > viewport) top = span.start;
|
|
290
|
+
else if (span.start < top) top = span.start;
|
|
291
|
+
else if (span.end >= top + viewport) top = span.end - viewport + 1;
|
|
292
|
+
top = Math.max(0, Math.min(top, Math.max(0, body.length - viewport)));
|
|
293
|
+
this.scrollTop = top;
|
|
294
|
+
return [...head, ...body.slice(top, top + viewport), ...tail];
|
|
215
295
|
}
|
|
216
296
|
}
|
package/src/config.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { dirname } from "node:path";
|
|
|
3
3
|
import { configPath } from "./paths";
|
|
4
4
|
import { errCode, isRec } from "./utils";
|
|
5
5
|
import { writeAtomic } from "./fs-write";
|
|
6
|
-
export type AutoReadAllMode = "off" | "
|
|
7
|
-
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "
|
|
6
|
+
export type AutoReadAllMode = "off" | "outline" | "full";
|
|
7
|
+
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "outline", "full"];
|
|
8
8
|
|
|
9
9
|
export type ReadOnDisabledModels = "remove" | "vanilla";
|
|
10
10
|
const READ_ON_DISABLED_MODELS: ReadOnDisabledModels[] = ["remove", "vanilla"];
|
|
@@ -19,6 +19,7 @@ export interface Config {
|
|
|
19
19
|
copyMoveEnabled?: boolean;
|
|
20
20
|
replaceMatchEnabled?: boolean;
|
|
21
21
|
autoReadAll?: AutoReadAllMode;
|
|
22
|
+
autoReadAllRequireGit?: boolean;
|
|
22
23
|
autoReadAllIgnore?: string[];
|
|
23
24
|
requirePath?: boolean;
|
|
24
25
|
strictInput?: boolean;
|
|
@@ -33,6 +34,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
33
34
|
copyMoveEnabled: true,
|
|
34
35
|
replaceMatchEnabled: true,
|
|
35
36
|
autoReadAll: "off",
|
|
37
|
+
autoReadAllRequireGit: true,
|
|
36
38
|
autoReadAllIgnore: [],
|
|
37
39
|
requirePath: false,
|
|
38
40
|
strictInput: false,
|
|
@@ -42,12 +44,21 @@ const DEFAULT_CONFIG: Config = {
|
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
45
|
-
if (value === "off" || value === "
|
|
46
|
-
if (value ===
|
|
47
|
+
if (value === "off" || value === "outline" || value === "full") return value;
|
|
48
|
+
if (value === "outline(git)") return "outline";
|
|
49
|
+
if (value === "git") return "full";
|
|
50
|
+
if (value === "on" || value === true) return "full";
|
|
47
51
|
if (value === false) return "off";
|
|
48
52
|
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
function parseAutoReadAllRequireGit(value: unknown, legacyMode: unknown): boolean {
|
|
56
|
+
if (typeof value === "boolean") return value;
|
|
57
|
+
if (legacyMode === "git" || legacyMode === "outline(git)") return true;
|
|
58
|
+
if (legacyMode === "full" || legacyMode === "outline" || legacyMode === "on" || legacyMode === true) return false;
|
|
59
|
+
return DEFAULT_CONFIG.autoReadAllRequireGit ?? true;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
function parseReadOnDisabledModels(value: unknown, legacy: unknown): ReadOnDisabledModels {
|
|
52
63
|
if (value === "remove" || value === "vanilla") return value;
|
|
53
64
|
if (typeof legacy === "boolean") return legacy ? "remove" : "vanilla";
|
|
@@ -100,6 +111,7 @@ function parseConfig(content: string): Config {
|
|
|
100
111
|
const copyMoveEnabled = parsed.copyMoveEnabled;
|
|
101
112
|
const replaceMatchEnabled = parsed.replaceMatchEnabled;
|
|
102
113
|
const autoReadAll = parsed.autoReadAll;
|
|
114
|
+
const autoReadAllRequireGit = parsed.autoReadAllRequireGit;
|
|
103
115
|
const requirePath = parsed.requirePath;
|
|
104
116
|
const strictInput = parsed.strictInput;
|
|
105
117
|
const diffContextLines = parsed.diffContextLines;
|
|
@@ -113,6 +125,7 @@ function parseConfig(content: string): Config {
|
|
|
113
125
|
copyMoveEnabled: typeof copyMoveEnabled === "boolean" ? copyMoveEnabled : DEFAULT_CONFIG.copyMoveEnabled,
|
|
114
126
|
replaceMatchEnabled: typeof replaceMatchEnabled === "boolean" ? replaceMatchEnabled : DEFAULT_CONFIG.replaceMatchEnabled,
|
|
115
127
|
autoReadAll: parseAutoReadAllMode(autoReadAll),
|
|
128
|
+
autoReadAllRequireGit: parseAutoReadAllRequireGit(autoReadAllRequireGit, autoReadAll),
|
|
116
129
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
117
130
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
118
131
|
diffContextLines: normalizeDiffContextLines(diffContextLines),
|
|
@@ -226,30 +239,35 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
226
239
|
}
|
|
227
240
|
|
|
228
241
|
|
|
229
|
-
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
|
|
242
|
+
type ToggleKey = "autoRead" | "autoReadAllRequireGit" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
|
|
230
243
|
|
|
231
244
|
async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
232
245
|
const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
|
|
233
246
|
return config[key] === true;
|
|
234
247
|
}
|
|
235
248
|
export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
249
|
+
export const toggleAutoReadAllRequireGit = (): Promise<boolean> => toggleFlag("autoReadAllRequireGit");
|
|
236
250
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
237
251
|
export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
|
|
238
252
|
export const toggleReplaceMatch = (): Promise<boolean> => toggleFlag("replaceMatchEnabled");
|
|
239
|
-
|
|
253
|
+
function steppedIndex(length: number, index: number, delta: number): number {
|
|
254
|
+
return ((index + delta) % length + length) % length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function cycleReadOnDisabledModels(delta = 1): Promise<ReadOnDisabledModels> {
|
|
240
258
|
let next: ReadOnDisabledModels = "vanilla";
|
|
241
259
|
await updateConfig((c) => {
|
|
242
260
|
const current = c.readOnDisabledModels ?? "vanilla";
|
|
243
|
-
next = READ_ON_DISABLED_MODELS[(READ_ON_DISABLED_MODELS.indexOf(current)
|
|
261
|
+
next = READ_ON_DISABLED_MODELS[steppedIndex(READ_ON_DISABLED_MODELS.length, READ_ON_DISABLED_MODELS.indexOf(current), delta)] ?? "remove";
|
|
244
262
|
c.readOnDisabledModels = next;
|
|
245
263
|
});
|
|
246
264
|
return next;
|
|
247
265
|
}
|
|
248
|
-
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
266
|
+
export async function cycleAutoReadAllMode(delta = 1): Promise<AutoReadAllMode> {
|
|
249
267
|
let next: AutoReadAllMode = "off";
|
|
250
268
|
await updateConfig((c) => {
|
|
251
269
|
const current = c.autoReadAll ?? "off";
|
|
252
|
-
next = AUTO_READ_ALL_MODES[(AUTO_READ_ALL_MODES.indexOf(current)
|
|
270
|
+
next = AUTO_READ_ALL_MODES[steppedIndex(AUTO_READ_ALL_MODES.length, AUTO_READ_ALL_MODES.indexOf(current), delta)] ?? "off";
|
|
253
271
|
c.autoReadAll = next;
|
|
254
272
|
});
|
|
255
273
|
return next;
|
package/src/copy-move.ts
CHANGED
|
@@ -900,11 +900,11 @@ type TransferToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { renderSh
|
|
|
900
900
|
export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): TransferToolDef {
|
|
901
901
|
const prompted = withTransferPrompts(
|
|
902
902
|
{
|
|
903
|
-
description: loadP(`../prompts/${kind}.md`),
|
|
904
|
-
snippet: loadP(`../prompts/${kind}-snippet.md`),
|
|
903
|
+
description: loadP(`../tool-prompts/${kind}.md`),
|
|
904
|
+
snippet: loadP(`../tool-prompts/${kind}-snippet.md`),
|
|
905
905
|
guidelines: [
|
|
906
|
-
...loadGuide(`../prompts/${kind}-guidelines.md`),
|
|
907
|
-
...loadGuide("../prompts/transfer-guidelines.md"),
|
|
906
|
+
...loadGuide(`../tool-prompts/${kind}-guidelines.md`),
|
|
907
|
+
...loadGuide("../tool-prompts/transfer-guidelines.md"),
|
|
908
908
|
],
|
|
909
909
|
},
|
|
910
910
|
flags,
|
package/src/grep.ts
CHANGED
|
@@ -535,9 +535,9 @@ export function renderGrepResult(result: { content?: Array<{ type: string; text?
|
|
|
535
535
|
}
|
|
536
536
|
export function regGrep(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
|
|
537
537
|
const prompted = withGrepPrompts({
|
|
538
|
-
description: loadP("../prompts/grep.md"),
|
|
539
|
-
snippet: loadP("../prompts/grep-snippet.md"),
|
|
540
|
-
guidelines: loadGuide("../prompts/grep-guidelines.md"),
|
|
538
|
+
description: loadP("../tool-prompts/grep.md"),
|
|
539
|
+
snippet: loadP("../tool-prompts/grep-snippet.md"),
|
|
540
|
+
guidelines: loadGuide("../tool-prompts/grep-guidelines.md"),
|
|
541
541
|
}, flags);
|
|
542
542
|
pi.registerTool({
|
|
543
543
|
name: "anchor_grep",
|
package/src/insert.ts
CHANGED
|
@@ -159,9 +159,9 @@ type InsertToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { renderShel
|
|
|
159
159
|
|
|
160
160
|
export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): InsertToolDef {
|
|
161
161
|
const prompted = withInsertPrompts({
|
|
162
|
-
description: loadP("../prompts/insert.md"),
|
|
163
|
-
snippet: loadP("../prompts/insert-snippet.md"),
|
|
164
|
-
guidelines: loadGuide("../prompts/insert-guidelines.md"),
|
|
162
|
+
description: loadP("../tool-prompts/insert.md"),
|
|
163
|
+
snippet: loadP("../tool-prompts/insert-snippet.md"),
|
|
164
|
+
guidelines: loadGuide("../tool-prompts/insert-guidelines.md"),
|
|
165
165
|
}, flags);
|
|
166
166
|
return {
|
|
167
167
|
name: "insert",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { visLines } from "../utils";
|
|
2
|
+
import { loadOutlineParser } from "./parsers";
|
|
3
|
+
import { renderPreviewOutline, renderSymbolOutline, type OutlineRender } from "./render";
|
|
4
|
+
|
|
5
|
+
export interface FileOutlineInput {
|
|
6
|
+
displayPath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
hashes: string[];
|
|
9
|
+
maxRows?: number;
|
|
10
|
+
maxDepth?: number;
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
previewHeadLines?: number;
|
|
13
|
+
previewTailLines?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function buildFileOutline(input: FileOutlineInput): Promise<OutlineRender> {
|
|
17
|
+
const lines = visLines(input.content);
|
|
18
|
+
const parser = await loadOutlineParser();
|
|
19
|
+
const parsed = parser === undefined ? undefined : await parser.parse(input.displayPath, input.content);
|
|
20
|
+
if (parsed !== undefined && parsed.symbols.length > 0) {
|
|
21
|
+
return renderSymbolOutline({
|
|
22
|
+
displayPath: input.displayPath,
|
|
23
|
+
languageName: parsed.languageName,
|
|
24
|
+
totalLines: lines.length,
|
|
25
|
+
hashes: input.hashes,
|
|
26
|
+
symbols: parsed.symbols,
|
|
27
|
+
...(input.maxRows !== undefined ? { maxRows: input.maxRows } : {}),
|
|
28
|
+
...(input.maxDepth !== undefined ? { maxDepth: input.maxDepth } : {}),
|
|
29
|
+
...(input.maxBytes !== undefined ? { maxBytes: input.maxBytes } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return renderPreviewOutline({
|
|
33
|
+
displayPath: input.displayPath,
|
|
34
|
+
lines,
|
|
35
|
+
hashes: input.hashes,
|
|
36
|
+
...(input.previewHeadLines !== undefined ? { headLines: input.previewHeadLines } : {}),
|
|
37
|
+
...(input.previewTailLines !== undefined ? { tailLines: input.previewTailLines } : {}),
|
|
38
|
+
...(input.maxBytes !== undefined ? { maxBytes: input.maxBytes } : {}),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { loadOutlineParser } from "./parsers";
|
|
43
|
+
export { renderPreviewOutline, renderSymbolOutline } from "./render";
|
|
44
|
+
export type { OutlineParse, OutlineParser, OutlineSymbol } from "./parsers";
|
|
45
|
+
export type { OutlineRender, OutlineRow } from "./render";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface OutlineSymbol {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
children?: OutlineSymbol[];
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OutlineParse {
|
|
11
|
+
symbols: OutlineSymbol[];
|
|
12
|
+
languageName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OutlineParser {
|
|
16
|
+
languageFor(path: string): string | null;
|
|
17
|
+
parse(path: string, content: string): Promise<OutlineParse | undefined>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function load(): Promise<OutlineParser | undefined> {
|
|
21
|
+
try {
|
|
22
|
+
const parsers = await import("./vendor/parsers/index.js");
|
|
23
|
+
return {
|
|
24
|
+
languageFor: (path) => parsers.detectLanguage(path),
|
|
25
|
+
parse: async (path, content) => {
|
|
26
|
+
if (parsers.detectLanguage(path) === null) return undefined;
|
|
27
|
+
const parsed = await parsers.parseSourceFile(path, content);
|
|
28
|
+
return { symbols: parsed.symbols, languageName: parsed.languageName };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error("Failed to load the vendored outline parsers:", error);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let parserPromise: Promise<OutlineParser | undefined> | undefined;
|
|
38
|
+
|
|
39
|
+
export function loadOutlineParser(): Promise<OutlineParser | undefined> {
|
|
40
|
+
parserPromise ??= load();
|
|
41
|
+
return parserPromise;
|
|
42
|
+
}
|