pi-hashline-edit-pro 6.1.1 → 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.
Files changed (69) hide show
  1. package/README.md +22 -16
  2. package/index.ts +48 -25
  3. package/package.json +3 -2
  4. package/src/auto-read-all.ts +39 -21
  5. package/src/batch.ts +2 -2
  6. package/src/config-ui.ts +135 -55
  7. package/src/config.ts +48 -12
  8. package/src/copy-move.ts +4 -4
  9. package/src/grep.ts +3 -3
  10. package/src/insert.ts +3 -3
  11. package/src/outline/index.ts +45 -0
  12. package/src/outline/parsers.ts +42 -0
  13. package/src/outline/render.ts +166 -0
  14. package/src/outline/vendor/LICENSE +201 -0
  15. package/src/outline/vendor/README.md +17 -0
  16. package/src/outline/vendor/parsers/blade.ts +138 -0
  17. package/src/outline/vendor/parsers/index.ts +75 -0
  18. package/src/outline/vendor/parsers/java.ts +501 -0
  19. package/src/outline/vendor/parsers/manager.ts +1721 -0
  20. package/src/outline/vendor/parsers/php.ts +332 -0
  21. package/src/outline/vendor/parsers/sass.ts +416 -0
  22. package/src/outline/vendor/parsers/scss.ts +566 -0
  23. package/src/outline/vendor/types.ts +82 -0
  24. package/src/outline/vendor/wasm/LICENSES.md +37 -0
  25. package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
  26. package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
  27. package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
  28. package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
  29. package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
  30. package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
  31. package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
  32. package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
  33. package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
  34. package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
  35. package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
  36. package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
  37. package/src/payload-contract.ts +7 -2
  38. package/src/read.ts +108 -36
  39. package/src/replace-match.ts +8 -6
  40. package/src/replace-undo.ts +3 -3
  41. package/src/replace.ts +7 -6
  42. package/src/utils.ts +19 -3
  43. package/tool-prompts/read-guidelines.md +3 -0
  44. package/{prompts → tool-prompts}/read.md +2 -0
  45. package/{prompts → tool-prompts}/replace-match.md +1 -1
  46. package/{prompts → tool-prompts}/replace.md +1 -1
  47. package/prompts/read-guidelines.md +0 -1
  48. package/src/auto-read-all-state.ts +0 -27
  49. /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
  50. /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
  51. /package/{prompts → tool-prompts}/copy.md +0 -0
  52. /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
  53. /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
  54. /package/{prompts → tool-prompts}/grep.md +0 -0
  55. /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
  56. /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
  57. /package/{prompts → tool-prompts}/insert.md +0 -0
  58. /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
  59. /package/{prompts → tool-prompts}/move-snippet.md +0 -0
  60. /package/{prompts → tool-prompts}/move.md +0 -0
  61. /package/{prompts → tool-prompts}/read-snippet.md +0 -0
  62. /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
  63. /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
  64. /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
  65. /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
  66. /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
  67. /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
  68. /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
  69. /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" | "disableReadOnModels";
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
- return [
21
- { key: "autoRead", label: "Auto-read", hint: "Anchors after write + post-edit diffs", enabled: config.autoRead !== false },
22
- { key: "autoReadAll", label: "Auto-read all", hint: "Attach files on the first turn: off, on, git (git repos only)", enabled: (config.autoReadAll ?? "off") !== "off", mode: config.autoReadAll ?? "off", cycle: ["off", "on", "git"] },
23
- { key: "autoReadAllIgnore", label: "Ignore folders/files", hint: "Extra folders, files, or globs skipped by auto-read all (comma-separated)", enabled: (config.autoReadAllIgnore ?? []).length > 0, entries: config.autoReadAllIgnore ?? [] },
24
- { key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
25
- { key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
26
- { key: "copyMoveEnabled", label: "Copy/move", hint: "copy and move tools (both off while disabled)", enabled: config.copyMoveEnabled !== false },
27
- { key: "requirePath", label: "Require path", hint: "replace, insert, copy, move need path (RPC visibility)", enabled: config.requirePath === true },
28
- { key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
29
- { key: "replaceMatchEnabled", label: "Replace match", hint: "replace_match tool (off while disabled)", enabled: config.replaceMatchEnabled !== false },
30
- { key: "disableOnModels", label: "Disable on models", hint: "Model globs (provider/id, model id, or api) that turn off the anchored edit tools (comma-separated)", enabled: (config.disableOnModels ?? []).length > 0, entries: config.disableOnModels ?? [] },
31
- { key: "disableReadOnModels", label: "Disable read on models", hint: "Also turn off read for models matched by Disable on models (off keeps read active)", enabled: config.disableReadOnModels !== false },
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 padded = content + " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
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
- function listBox(theme: Theme, entries: string[]): string {
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
- function formatList(entries: string[]): string {
54
- if (entries.length === 0) return "(empty)";
55
- const joined = entries.join(", ");
56
- return joined.length > 40 ? `${joined.slice(0, 37)}...` : joined;
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
- const next = row.cycle[(row.cycle.indexOf(row.mode) + 1) % row.cycle.length] ?? row.cycle[0];
117
- if (next === undefined) return;
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 || row.value === undefined) return;
129
- row.value += delta;
130
- this.runToggle(row, delta);
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 lines: string[] = [];
197
- lines.push(theme.fg("border", `╭${"─".repeat(innerWidth)}╮`));
198
- lines.push(padRow(theme, innerWidth, ` ${theme.fg("accent", theme.bold("Hashline Config"))}`));
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
- const cursor = index === this.selected ? theme.fg("accent", "> ") : " ";
202
- const box = row.entries !== undefined ? listBox(theme, row.entries) : row.value !== undefined ? numberBox(theme, row.value, row.disabled) : row.mode !== undefined ? modeBox(theme, row.mode) : row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
203
- const label = row.disabled ? theme.fg("dim", row.label) : index === this.selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
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
- lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
211
- const footer = this.editingList ? " type to edit · Enter save · Esc cancel" : " ↑↓ navigate · space toggle · ←/→ or -/+ adjust · e edit list · q close";
212
- lines.push(padRow(theme, innerWidth, theme.fg("dim", footer)));
213
- lines.push(theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
214
- return lines;
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,11 @@ 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" | "on" | "git";
7
- const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "on", "git"];
6
+ export type AutoReadAllMode = "off" | "outline" | "full";
7
+ const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "outline", "full"];
8
+
9
+ export type ReadOnDisabledModels = "remove" | "vanilla";
10
+ const READ_ON_DISABLED_MODELS: ReadOnDisabledModels[] = ["remove", "vanilla"];
8
11
 
9
12
  export const DEFAULT_DIFF_CONTEXT_LINES = 1;
10
13
  export const MIN_DIFF_CONTEXT_LINES = 0;
@@ -16,12 +19,13 @@ export interface Config {
16
19
  copyMoveEnabled?: boolean;
17
20
  replaceMatchEnabled?: boolean;
18
21
  autoReadAll?: AutoReadAllMode;
22
+ autoReadAllRequireGit?: boolean;
19
23
  autoReadAllIgnore?: string[];
20
24
  requirePath?: boolean;
21
25
  strictInput?: boolean;
22
26
  diffContextLines?: number;
23
27
  disableOnModels?: string[];
24
- disableReadOnModels?: boolean;
28
+ readOnDisabledModels?: ReadOnDisabledModels;
25
29
  }
26
30
 
27
31
  const DEFAULT_CONFIG: Config = {
@@ -30,21 +34,37 @@ const DEFAULT_CONFIG: Config = {
30
34
  copyMoveEnabled: true,
31
35
  replaceMatchEnabled: true,
32
36
  autoReadAll: "off",
37
+ autoReadAllRequireGit: true,
33
38
  autoReadAllIgnore: [],
34
39
  requirePath: false,
35
40
  strictInput: false,
36
41
  diffContextLines: DEFAULT_DIFF_CONTEXT_LINES,
37
42
  disableOnModels: [],
38
- disableReadOnModels: true,
43
+ readOnDisabledModels: "vanilla",
39
44
  };
40
45
 
41
46
  function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
42
- if (value === "off" || value === "on" || value === "git") return value;
43
- if (value === true) return "on";
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";
44
51
  if (value === false) return "off";
45
52
  return DEFAULT_CONFIG.autoReadAll ?? "off";
46
53
  }
47
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
+
62
+ function parseReadOnDisabledModels(value: unknown, legacy: unknown): ReadOnDisabledModels {
63
+ if (value === "remove" || value === "vanilla") return value;
64
+ if (typeof legacy === "boolean") return legacy ? "remove" : "vanilla";
65
+ return DEFAULT_CONFIG.readOnDisabledModels ?? "vanilla";
66
+ }
67
+
48
68
  export function normalizeAutoReadAllIgnoreEntry(entry: string): string {
49
69
  return entry.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\/{2,}/g, "/");
50
70
  }
@@ -91,24 +111,27 @@ function parseConfig(content: string): Config {
91
111
  const copyMoveEnabled = parsed.copyMoveEnabled;
92
112
  const replaceMatchEnabled = parsed.replaceMatchEnabled;
93
113
  const autoReadAll = parsed.autoReadAll;
114
+ const autoReadAllRequireGit = parsed.autoReadAllRequireGit;
94
115
  const requirePath = parsed.requirePath;
95
116
  const strictInput = parsed.strictInput;
96
117
  const diffContextLines = parsed.diffContextLines;
97
118
  const autoReadAllIgnore = parsed.autoReadAllIgnore;
98
119
  const disableOnModels = parsed.disableOnModels;
99
- const disableReadOnModels = parsed.disableReadOnModels;
120
+ const readOnDisabledModels = parsed.readOnDisabledModels;
121
+ const legacyDisableReadOnModels = parsed.disableReadOnModels;
100
122
  return {
101
123
  autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
102
124
  anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
103
125
  copyMoveEnabled: typeof copyMoveEnabled === "boolean" ? copyMoveEnabled : DEFAULT_CONFIG.copyMoveEnabled,
104
126
  replaceMatchEnabled: typeof replaceMatchEnabled === "boolean" ? replaceMatchEnabled : DEFAULT_CONFIG.replaceMatchEnabled,
105
127
  autoReadAll: parseAutoReadAllMode(autoReadAll),
128
+ autoReadAllRequireGit: parseAutoReadAllRequireGit(autoReadAllRequireGit, autoReadAll),
106
129
  requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
107
130
  strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
108
131
  diffContextLines: normalizeDiffContextLines(diffContextLines),
109
132
  autoReadAllIgnore: parseAutoReadAllIgnore(autoReadAllIgnore),
110
133
  disableOnModels: parseDisableOnModels(disableOnModels),
111
- disableReadOnModels: typeof disableReadOnModels === "boolean" ? disableReadOnModels : DEFAULT_CONFIG.disableReadOnModels,
134
+ readOnDisabledModels: parseReadOnDisabledModels(readOnDisabledModels, legacyDisableReadOnModels),
112
135
  };
113
136
  }
114
137
 
@@ -216,22 +239,35 @@ export async function writeConfig(config: Config): Promise<void> {
216
239
  }
217
240
 
218
241
 
219
- type ToggleKey = "autoRead" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput" | "disableReadOnModels";
242
+ type ToggleKey = "autoRead" | "autoReadAllRequireGit" | "anchorGrepEnabled" | "copyMoveEnabled" | "replaceMatchEnabled" | "requirePath" | "strictInput";
220
243
 
221
244
  async function toggleFlag(key: ToggleKey): Promise<boolean> {
222
245
  const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
223
246
  return config[key] === true;
224
247
  }
225
248
  export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
249
+ export const toggleAutoReadAllRequireGit = (): Promise<boolean> => toggleFlag("autoReadAllRequireGit");
226
250
  export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
227
251
  export const toggleCopyMove = (): Promise<boolean> => toggleFlag("copyMoveEnabled");
228
252
  export const toggleReplaceMatch = (): Promise<boolean> => toggleFlag("replaceMatchEnabled");
229
- export const toggleDisableReadOnModels = (): Promise<boolean> => toggleFlag("disableReadOnModels");
230
- export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
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> {
258
+ let next: ReadOnDisabledModels = "vanilla";
259
+ await updateConfig((c) => {
260
+ const current = c.readOnDisabledModels ?? "vanilla";
261
+ next = READ_ON_DISABLED_MODELS[steppedIndex(READ_ON_DISABLED_MODELS.length, READ_ON_DISABLED_MODELS.indexOf(current), delta)] ?? "remove";
262
+ c.readOnDisabledModels = next;
263
+ });
264
+ return next;
265
+ }
266
+ export async function cycleAutoReadAllMode(delta = 1): Promise<AutoReadAllMode> {
231
267
  let next: AutoReadAllMode = "off";
232
268
  await updateConfig((c) => {
233
269
  const current = c.autoReadAll ?? "off";
234
- next = AUTO_READ_ALL_MODES[(AUTO_READ_ALL_MODES.indexOf(current) + 1) % AUTO_READ_ALL_MODES.length] ?? "off";
270
+ next = AUTO_READ_ALL_MODES[steppedIndex(AUTO_READ_ALL_MODES.length, AUTO_READ_ALL_MODES.indexOf(current), delta)] ?? "off";
235
271
  c.autoReadAll = next;
236
272
  });
237
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
+ }