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.
Files changed (66) hide show
  1. package/README.md +14 -12
  2. package/index.ts +20 -12
  3. package/package.json +3 -2
  4. package/src/auto-read-all.ts +39 -21
  5. package/src/config-ui.ts +135 -55
  6. package/src/config.ts +27 -9
  7. package/src/copy-move.ts +4 -4
  8. package/src/grep.ts +3 -3
  9. package/src/insert.ts +3 -3
  10. package/src/outline/index.ts +45 -0
  11. package/src/outline/parsers.ts +42 -0
  12. package/src/outline/render.ts +166 -0
  13. package/src/outline/vendor/LICENSE +201 -0
  14. package/src/outline/vendor/README.md +17 -0
  15. package/src/outline/vendor/parsers/blade.ts +138 -0
  16. package/src/outline/vendor/parsers/index.ts +75 -0
  17. package/src/outline/vendor/parsers/java.ts +501 -0
  18. package/src/outline/vendor/parsers/manager.ts +1721 -0
  19. package/src/outline/vendor/parsers/php.ts +332 -0
  20. package/src/outline/vendor/parsers/sass.ts +416 -0
  21. package/src/outline/vendor/parsers/scss.ts +566 -0
  22. package/src/outline/vendor/types.ts +82 -0
  23. package/src/outline/vendor/wasm/LICENSES.md +37 -0
  24. package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
  25. package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
  26. package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
  27. package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
  28. package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
  29. package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
  30. package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
  31. package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
  32. package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
  33. package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
  34. package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
  35. package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
  36. package/src/read.ts +108 -36
  37. package/src/replace-match.ts +3 -3
  38. package/src/replace-undo.ts +3 -3
  39. package/src/replace.ts +3 -3
  40. package/tool-prompts/read-guidelines.md +3 -0
  41. package/{prompts → tool-prompts}/read.md +2 -0
  42. package/prompts/read-guidelines.md +0 -1
  43. package/src/auto-read-all-state.ts +0 -27
  44. /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
  45. /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
  46. /package/{prompts → tool-prompts}/copy.md +0 -0
  47. /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
  48. /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
  49. /package/{prompts → tool-prompts}/grep.md +0 -0
  50. /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
  51. /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
  52. /package/{prompts → tool-prompts}/insert.md +0 -0
  53. /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
  54. /package/{prompts → tool-prompts}/move-snippet.md +0 -0
  55. /package/{prompts → tool-prompts}/move.md +0 -0
  56. /package/{prompts → tool-prompts}/read-snippet.md +0 -0
  57. /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
  58. /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
  59. /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
  60. /package/{prompts → tool-prompts}/replace-match.md +0 -0
  61. /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
  62. /package/{prompts → tool-prompts}/replace.md +0 -0
  63. /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
  64. /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
  65. /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
  66. /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
- 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: "readOnDisabledModels", label: "Read on disabled models", hint: "Read for models matched by Disable on models: vanilla (plain pi read), or remove it", enabled: (config.readOnDisabledModels ?? "vanilla") !== "remove", mode: config.readOnDisabledModels ?? "vanilla", cycle: ["remove", "vanilla"] },
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,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" | "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
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 === "on" || value === "git") return value;
46
- 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";
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
- export async function cycleReadOnDisabledModels(): Promise<ReadOnDisabledModels> {
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) + 1) % READ_ON_DISABLED_MODELS.length] ?? "remove";
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) + 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";
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
+ }