@oh-my-pi/pi-coding-agent 14.1.0 → 14.1.1

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 (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +8 -8
  3. package/src/async/job-manager.ts +43 -10
  4. package/src/commit/agentic/tools/analyze-file.ts +1 -2
  5. package/src/config/mcp-schema.json +1 -1
  6. package/src/config/model-equivalence.ts +1 -0
  7. package/src/config/model-registry.ts +63 -34
  8. package/src/config/model-resolver.ts +111 -15
  9. package/src/config/settings-schema.ts +4 -3
  10. package/src/config/settings.ts +1 -1
  11. package/src/cursor.ts +64 -23
  12. package/src/edit/index.ts +254 -89
  13. package/src/edit/modes/chunk.ts +336 -57
  14. package/src/edit/modes/hashline.ts +51 -26
  15. package/src/edit/modes/patch.ts +16 -10
  16. package/src/edit/modes/replace.ts +15 -7
  17. package/src/edit/renderer.ts +248 -94
  18. package/src/export/html/template.generated.ts +1 -1
  19. package/src/export/html/template.js +6 -4
  20. package/src/extensibility/custom-tools/types.ts +0 -3
  21. package/src/extensibility/extensions/loader.ts +16 -0
  22. package/src/extensibility/extensions/runner.ts +2 -7
  23. package/src/extensibility/extensions/types.ts +8 -4
  24. package/src/internal-urls/docs-index.generated.ts +3 -3
  25. package/src/ipy/executor.ts +447 -52
  26. package/src/ipy/kernel.ts +39 -13
  27. package/src/lsp/client.ts +54 -0
  28. package/src/lsp/index.ts +8 -0
  29. package/src/lsp/types.ts +6 -0
  30. package/src/main.ts +0 -1
  31. package/src/modes/acp/acp-agent.ts +4 -1
  32. package/src/modes/components/bash-execution.ts +16 -4
  33. package/src/modes/components/status-line/presets.ts +17 -6
  34. package/src/modes/components/status-line/segments.ts +15 -0
  35. package/src/modes/components/status-line-segment-editor.ts +1 -0
  36. package/src/modes/components/status-line.ts +7 -1
  37. package/src/modes/components/tool-execution.ts +145 -75
  38. package/src/modes/controllers/command-controller.ts +24 -1
  39. package/src/modes/controllers/event-controller.ts +4 -1
  40. package/src/modes/controllers/extension-ui-controller.ts +28 -5
  41. package/src/modes/controllers/input-controller.ts +9 -3
  42. package/src/modes/controllers/selector-controller.ts +4 -1
  43. package/src/modes/interactive-mode.ts +19 -3
  44. package/src/modes/print-mode.ts +13 -4
  45. package/src/modes/prompt-action-autocomplete.ts +3 -5
  46. package/src/modes/rpc/rpc-mode.ts +8 -2
  47. package/src/modes/shared.ts +2 -2
  48. package/src/modes/types.ts +1 -0
  49. package/src/modes/utils/ui-helpers.ts +1 -0
  50. package/src/prompts/tools/bash.md +2 -2
  51. package/src/prompts/tools/chunk-edit.md +191 -163
  52. package/src/prompts/tools/hashline.md +11 -11
  53. package/src/prompts/tools/patch.md +10 -5
  54. package/src/prompts/tools/{await.md → poll.md} +1 -1
  55. package/src/prompts/tools/read-chunk.md +3 -3
  56. package/src/prompts/tools/task.md +2 -2
  57. package/src/prompts/tools/vim.md +98 -0
  58. package/src/sdk.ts +754 -724
  59. package/src/session/agent-session.ts +164 -34
  60. package/src/session/session-manager.ts +50 -4
  61. package/src/slash-commands/builtin-registry.ts +17 -0
  62. package/src/task/executor.ts +4 -4
  63. package/src/task/index.ts +3 -5
  64. package/src/task/types.ts +2 -2
  65. package/src/tools/bash.ts +26 -8
  66. package/src/tools/find.ts +5 -2
  67. package/src/tools/grep.ts +77 -8
  68. package/src/tools/index.ts +48 -19
  69. package/src/tools/{await-tool.ts → poll-tool.ts} +36 -30
  70. package/src/tools/python.ts +293 -278
  71. package/src/tools/submit-result.ts +5 -2
  72. package/src/tools/todo-write.ts +8 -2
  73. package/src/tools/vim.ts +966 -0
  74. package/src/utils/edit-mode.ts +2 -1
  75. package/src/utils/session-color.ts +55 -0
  76. package/src/utils/title-generator.ts +15 -6
  77. package/src/vim/buffer.ts +309 -0
  78. package/src/vim/commands.ts +382 -0
  79. package/src/vim/engine.ts +2426 -0
  80. package/src/vim/parser.ts +151 -0
  81. package/src/vim/render.ts +252 -0
  82. package/src/vim/types.ts +197 -0
@@ -0,0 +1,382 @@
1
+ import type { VimExCommand, VimLineRange } from "./types";
2
+ import { VimInputError } from "./types";
3
+
4
+ export interface VimExParseContext {
5
+ currentLine: number;
6
+ lastLine: number;
7
+ }
8
+
9
+ interface ParsedLineAddress {
10
+ line: number;
11
+ nextIndex: number;
12
+ }
13
+
14
+ function clampLine(line: number, context: VimExParseContext): number {
15
+ return Math.min(Math.max(line, 1), Math.max(1, context.lastLine));
16
+ }
17
+
18
+ function readDigits(raw: string, start: number): { digits: string; nextIndex: number } {
19
+ let index = start;
20
+ let digits = "";
21
+ while (index < raw.length) {
22
+ const char = raw[index] ?? "";
23
+ if (!/^\d$/.test(char)) {
24
+ break;
25
+ }
26
+ digits += char;
27
+ index += 1;
28
+ }
29
+ return { digits, nextIndex: index };
30
+ }
31
+
32
+ function parseLineAddress(
33
+ raw: string,
34
+ start: number,
35
+ context: VimExParseContext,
36
+ relativeBase = context.currentLine,
37
+ ): ParsedLineAddress | undefined {
38
+ let index = start;
39
+ let line: number | undefined;
40
+ const first = raw[index] ?? "";
41
+
42
+ if (/^\d$/.test(first)) {
43
+ const { digits, nextIndex } = readDigits(raw, index);
44
+ line = Number.parseInt(digits, 10);
45
+ index = nextIndex;
46
+ } else if (first === ".") {
47
+ line = context.currentLine;
48
+ index += 1;
49
+ } else if (first === "$") {
50
+ line = context.lastLine;
51
+ index += 1;
52
+ } else if (first === "+" || first === "-") {
53
+ line = relativeBase;
54
+ } else {
55
+ return undefined;
56
+ }
57
+
58
+ while (index < raw.length) {
59
+ const sign = raw[index];
60
+ if (sign !== "+" && sign !== "-") {
61
+ break;
62
+ }
63
+ index += 1;
64
+ const { digits, nextIndex } = readDigits(raw, index);
65
+ index = nextIndex;
66
+ const offset = digits.length > 0 ? Number.parseInt(digits, 10) : 1;
67
+ line += sign === "+" ? offset : -offset;
68
+ }
69
+
70
+ return { line: clampLine(line, context), nextIndex: index };
71
+ }
72
+
73
+ function parseLineRange(raw: string, context?: VimExParseContext): { range?: VimLineRange | "all"; rest: string } {
74
+ if (raw.startsWith("%")) {
75
+ return { range: "all", rest: raw.slice(1).trimStart() };
76
+ }
77
+
78
+ if (!context) {
79
+ const match = raw.match(/^(\d+)(?:\s*,\s*(\d+))?/);
80
+ if (!match) {
81
+ return { rest: raw };
82
+ }
83
+
84
+ const start = Number.parseInt(match[1] ?? "", 10);
85
+ const end = Number.parseInt(match[2] ?? match[1] ?? "", 10);
86
+ return {
87
+ range: { start, end },
88
+ rest: raw.slice(match[0].length).trimStart(),
89
+ };
90
+ }
91
+
92
+ const first = parseLineAddress(raw, 0, context);
93
+ if (!first) {
94
+ return { rest: raw };
95
+ }
96
+
97
+ let index = first.nextIndex;
98
+ while (raw[index] === " ") {
99
+ index += 1;
100
+ }
101
+
102
+ const separator = raw[index];
103
+ if (separator !== "," && separator !== ";") {
104
+ return {
105
+ range: { start: first.line, end: first.line },
106
+ rest: raw.slice(index).trimStart(),
107
+ };
108
+ }
109
+
110
+ index += 1;
111
+ while (raw[index] === " ") {
112
+ index += 1;
113
+ }
114
+
115
+ const second = parseLineAddress(raw, index, context, separator === ";" ? first.line : context.currentLine);
116
+ if (!second) {
117
+ throw new VimInputError(`Missing line address after ${separator}`);
118
+ }
119
+
120
+ return {
121
+ range: { start: first.line, end: second.line },
122
+ rest: raw.slice(second.nextIndex).trimStart(),
123
+ };
124
+ }
125
+
126
+ function parseDelimitedSegments(raw: string): { pattern: string; replacement: string; flags: string } {
127
+ if (raw.length === 0) {
128
+ throw new VimInputError("Missing substitute delimiter");
129
+ }
130
+
131
+ const delimiter = raw[0] ?? "/";
132
+ const segments: string[] = [];
133
+ let current = "";
134
+ let escaped = false;
135
+
136
+ for (let index = 1; index < raw.length; index += 1) {
137
+ const char = raw[index] ?? "";
138
+ if (escaped) {
139
+ current += char;
140
+ escaped = false;
141
+ continue;
142
+ }
143
+ if (char === "\\") {
144
+ escaped = true;
145
+ current += char;
146
+ continue;
147
+ }
148
+ if (char === delimiter && segments.length < 2) {
149
+ segments.push(current);
150
+ current = "";
151
+ continue;
152
+ }
153
+ current += char;
154
+ }
155
+
156
+ if (segments.length !== 2) {
157
+ throw new VimInputError("Substitute command must look like :s/pattern/replacement/flags");
158
+ }
159
+
160
+ return {
161
+ pattern: segments[0] ?? "",
162
+ replacement: segments[1] ?? "",
163
+ flags: current.trim(),
164
+ };
165
+ }
166
+
167
+ function parseDestination(raw: string, context?: VimExParseContext): number {
168
+ const trimmed = raw.trim();
169
+ if (trimmed.length === 0) {
170
+ throw new VimInputError("Missing destination");
171
+ }
172
+
173
+ if (/^\d+$/.test(trimmed)) {
174
+ return Number.parseInt(trimmed, 10);
175
+ }
176
+
177
+ if (context) {
178
+ const address = parseLineAddress(trimmed, 0, context);
179
+ if (address && trimmed.slice(address.nextIndex).trim().length === 0) {
180
+ return address.line;
181
+ }
182
+ }
183
+
184
+ const destination = Number.parseInt(trimmed, 10);
185
+ if (Number.isNaN(destination)) {
186
+ throw new VimInputError("Invalid destination");
187
+ }
188
+ return destination;
189
+ }
190
+
191
+ function matchGlobalCommand(rest: string): { pattern: string; command: string; invert: boolean } | undefined {
192
+ const globalMatch = rest.match(/^(g|v|g!|global|global!|vglobal)\s*([/|#])(.+?)\2(.*)$/);
193
+ if (!globalMatch) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ invert: globalMatch[1] === "v" || globalMatch[1] === "vglobal" || globalMatch[1]?.endsWith("!") === true,
198
+ pattern: globalMatch[3] ?? "",
199
+ command: (globalMatch[4] ?? "d").trim() || "d",
200
+ };
201
+ }
202
+
203
+ function matchDestinationCommand(rest: string, prefixes: readonly string[]): string | undefined {
204
+ for (const prefix of prefixes) {
205
+ if (!rest.startsWith(prefix)) {
206
+ continue;
207
+ }
208
+ const suffix = rest.slice(prefix.length);
209
+ if (suffix.length === 0) {
210
+ return "";
211
+ }
212
+ if (/^\s/.test(suffix) || /^[\d.$+-]/.test(suffix)) {
213
+ return suffix.trim();
214
+ }
215
+ }
216
+ return undefined;
217
+ }
218
+
219
+ export function parseExCommand(input: string, context?: VimExParseContext): VimExCommand {
220
+ const trimmed = input.trim();
221
+ const normalized = trimmed.startsWith(":") ? trimmed.slice(1).trimStart() : trimmed;
222
+ if (normalized.length === 0) {
223
+ throw new VimInputError("Empty ex command");
224
+ }
225
+
226
+ if (/^\d+$/.test(normalized)) {
227
+ return {
228
+ kind: "goto-line",
229
+ line: Number.parseInt(normalized, 10),
230
+ };
231
+ }
232
+
233
+ if (normalized === "w" || normalized === "write") {
234
+ return { kind: "write", force: false };
235
+ }
236
+ if (normalized === "w!" || normalized === "write!") {
237
+ return { kind: "write", force: true };
238
+ }
239
+ if (normalized === "update" || normalized === "up") {
240
+ return { kind: "update", force: false };
241
+ }
242
+ if (normalized === "update!" || normalized === "up!") {
243
+ return { kind: "update", force: true };
244
+ }
245
+ if (normalized === "wq" || normalized === "x" || normalized === "xit" || normalized === "exit") {
246
+ return { kind: "write-quit", force: false };
247
+ }
248
+ if (normalized === "wq!" || normalized === "x!" || normalized === "xit!" || normalized === "exit!") {
249
+ return { kind: "write-quit", force: true };
250
+ }
251
+ if (normalized === "q" || normalized === "quit") {
252
+ return { kind: "quit", force: false };
253
+ }
254
+ if (normalized === "q!" || normalized === "quit!") {
255
+ return { kind: "quit", force: true };
256
+ }
257
+ if (normalized === "e" || normalized === "edit") {
258
+ return { kind: "edit", force: false };
259
+ }
260
+ if (normalized === "e!" || normalized === "edit!") {
261
+ return { kind: "edit", force: true };
262
+ }
263
+ if (normalized.startsWith("e ") || normalized.startsWith("edit ")) {
264
+ const path = normalized.startsWith("edit ") ? normalized.slice(5).trim() : normalized.slice(2).trim();
265
+ return { kind: "edit", force: false, path };
266
+ }
267
+ if (normalized.startsWith("e! ") || normalized.startsWith("edit! ")) {
268
+ const path = normalized.startsWith("edit! ") ? normalized.slice(6).trim() : normalized.slice(3).trim();
269
+ return { kind: "edit", force: true, path };
270
+ }
271
+
272
+ const global = matchGlobalCommand(normalized);
273
+ if (global) {
274
+ return { kind: "global", ...global };
275
+ }
276
+
277
+ const { range, rest } = parseLineRange(normalized, context);
278
+ if (range && rest.length === 0) {
279
+ if (range === "all") {
280
+ throw new VimInputError(":% requires a following command");
281
+ }
282
+ return {
283
+ kind: "goto-line",
284
+ line: range.start,
285
+ };
286
+ }
287
+
288
+ const rangedGlobal = matchGlobalCommand(rest);
289
+ if (rangedGlobal) {
290
+ return { kind: "global", range, ...rangedGlobal };
291
+ }
292
+
293
+ if (rest === "sort" || rest.startsWith("sort ") || rest.startsWith("sort!")) {
294
+ const flags = rest.slice(4).trim();
295
+ return { kind: "sort", range: range ?? undefined, flags };
296
+ }
297
+ if (rest === "j" || rest === "join" || rest === "j!" || rest === "join!") {
298
+ return { kind: "join", range: range ?? undefined, trimWhitespace: !rest.endsWith("!") };
299
+ }
300
+
301
+ if (rest.startsWith("substitute")) {
302
+ const segments = parseDelimitedSegments(rest.slice("substitute".length));
303
+ return {
304
+ kind: "substitute",
305
+ range,
306
+ pattern: segments.pattern,
307
+ replacement: segments.replacement,
308
+ flags: segments.flags,
309
+ };
310
+ }
311
+
312
+ if (/^s(?:\W|$)/.test(rest)) {
313
+ const segments = parseDelimitedSegments(rest.slice(1));
314
+ return {
315
+ kind: "substitute",
316
+ range,
317
+ pattern: segments.pattern,
318
+ replacement: segments.replacement,
319
+ flags: segments.flags,
320
+ };
321
+ }
322
+
323
+ if (
324
+ rest === "d" ||
325
+ rest === "del" ||
326
+ rest === "delete" ||
327
+ rest.startsWith("d ") ||
328
+ rest.startsWith("del ") ||
329
+ rest.startsWith("delete ")
330
+ ) {
331
+ return {
332
+ kind: "delete",
333
+ range,
334
+ };
335
+ }
336
+
337
+ if (
338
+ rest === "y" ||
339
+ rest === "ya" ||
340
+ rest === "yank" ||
341
+ rest.startsWith("y ") ||
342
+ rest.startsWith("ya ") ||
343
+ rest.startsWith("yank ")
344
+ ) {
345
+ return {
346
+ kind: "yank",
347
+ range,
348
+ };
349
+ }
350
+
351
+ if (rest === "pu" || rest === "put" || rest === "pu!" || rest === "put!") {
352
+ return {
353
+ kind: "put",
354
+ range,
355
+ before: rest.endsWith("!"),
356
+ };
357
+ }
358
+
359
+ const copyDestination = matchDestinationCommand(rest, ["copy", "co", "t"]);
360
+ if (copyDestination !== undefined) {
361
+ const destination = parseDestination(copyDestination, context);
362
+ return { kind: "copy", range, destination };
363
+ }
364
+
365
+ const moveDestination = matchDestinationCommand(rest, ["move", "mo", "m"]);
366
+ if (moveDestination !== undefined) {
367
+ const destination = parseDestination(moveDestination, context);
368
+ return { kind: "move", range, destination };
369
+ }
370
+
371
+ if (rest === "a" || rest === "append" || rest.startsWith("a ") || rest.startsWith("append ")) {
372
+ const text = rest.startsWith("append") ? rest.slice(6).trimStart() : rest.slice(1).trimStart();
373
+ return { kind: "append", range: range === "all" ? undefined : range, text };
374
+ }
375
+
376
+ if (rest === "i" || rest === "insert" || rest.startsWith("i ") || rest.startsWith("insert ")) {
377
+ const text = rest.startsWith("insert") ? rest.slice(6).trimStart() : rest.slice(1).trimStart();
378
+ return { kind: "insert-before", range: range === "all" ? undefined : range, text };
379
+ }
380
+
381
+ throw new VimInputError(`Unsupported ex command: ${input}.`);
382
+ }