@oh-my-pi/pi-coding-agent 14.8.0 → 14.9.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 (60) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/package.json +16 -7
  3. package/src/config/model-resolver.ts +92 -35
  4. package/src/config/prompt-templates.ts +1 -1
  5. package/src/debug/index.ts +21 -0
  6. package/src/debug/raw-sse-buffer.ts +229 -0
  7. package/src/debug/raw-sse.ts +213 -0
  8. package/src/edit/index.ts +9 -10
  9. package/src/edit/streaming.ts +6 -5
  10. package/src/eval/js/context-manager.ts +91 -47
  11. package/src/extensibility/extensions/loader.ts +9 -3
  12. package/src/extensibility/extensions/types.ts +10 -3
  13. package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
  14. package/src/hashline/anchors.ts +113 -0
  15. package/src/hashline/apply.ts +732 -0
  16. package/src/hashline/bigrams.json +649 -0
  17. package/src/hashline/constants.ts +8 -0
  18. package/src/hashline/diff-preview.ts +43 -0
  19. package/src/hashline/diff.ts +56 -0
  20. package/src/hashline/execute.ts +268 -0
  21. package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
  22. package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
  23. package/src/hashline/index.ts +14 -0
  24. package/src/hashline/input.ts +110 -0
  25. package/src/hashline/parser.ts +220 -0
  26. package/src/hashline/prefixes.ts +101 -0
  27. package/src/hashline/recovery.ts +72 -0
  28. package/src/hashline/stream.ts +123 -0
  29. package/src/hashline/types.ts +69 -0
  30. package/src/hashline/utils.ts +3 -0
  31. package/src/index.ts +1 -1
  32. package/src/lsp/index.ts +1 -1
  33. package/src/lsp/render.ts +4 -0
  34. package/src/memories/index.ts +13 -4
  35. package/src/modes/components/assistant-message.ts +55 -9
  36. package/src/modes/components/welcome.ts +114 -38
  37. package/src/modes/controllers/event-controller.ts +3 -1
  38. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  39. package/src/modes/controllers/input-controller.ts +8 -1
  40. package/src/modes/interactive-mode.ts +50 -11
  41. package/src/modes/prompt-action-autocomplete.ts +3 -0
  42. package/src/modes/rpc/rpc-client.ts +53 -2
  43. package/src/modes/rpc/rpc-mode.ts +67 -1
  44. package/src/modes/rpc/rpc-types.ts +17 -2
  45. package/src/modes/types.ts +4 -1
  46. package/src/modes/utils/ui-helpers.ts +3 -1
  47. package/src/prompts/agents/reviewer.md +14 -0
  48. package/src/prompts/tools/hashline.md +57 -10
  49. package/src/sdk.ts +4 -3
  50. package/src/session/agent-session.ts +195 -30
  51. package/src/session/compaction/branch-summarization.ts +4 -2
  52. package/src/session/compaction/compaction.ts +22 -3
  53. package/src/task/executor.ts +21 -2
  54. package/src/task/index.ts +4 -1
  55. package/src/tools/ast-edit.ts +1 -1
  56. package/src/tools/match-line-format.ts +1 -1
  57. package/src/tools/read.ts +1 -1
  58. package/src/utils/file-mentions.ts +1 -1
  59. package/src/utils/title-generator.ts +11 -0
  60. package/src/edit/modes/hashline.ts +0 -2039
@@ -0,0 +1,213 @@
1
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
2
+ import { type Component, matchesKey, padding, replaceTabs, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
3
+ import { theme } from "../modes/theme/theme";
4
+ import { copyToClipboard } from "../utils/clipboard";
5
+ import { formatRawSseIsoTime, type RawSseDebugBuffer, rawSseRecordLines } from "./raw-sse-buffer";
6
+
7
+ const MIN_VIEWER_WIDTH = 20;
8
+ const VIEWER_FRAME_LINES = 5;
9
+
10
+ function sanitizeFrameLine(line: string, width: number): string {
11
+ return truncateToWidth(replaceTabs(sanitizeText(line)), width);
12
+ }
13
+
14
+ export interface RawSseViewerOptions {
15
+ buffer: RawSseDebugBuffer;
16
+ terminalRows: number;
17
+ onExit: () => void;
18
+ onStatus?: (message: string) => void;
19
+ onUpdate?: () => void;
20
+ }
21
+
22
+ export class RawSseViewerComponent implements Component {
23
+ readonly #buffer: RawSseDebugBuffer;
24
+ readonly #terminalRows: number;
25
+ readonly #onExit: () => void;
26
+ readonly #onStatus?: (message: string) => void;
27
+ readonly #onUpdate?: () => void;
28
+ readonly #unsubscribe: () => void;
29
+ #scrollOffset = 0;
30
+ #followTail = true;
31
+ #lastRenderWidth = MIN_VIEWER_WIDTH;
32
+ #statusMessage: string | undefined;
33
+
34
+ constructor(options: RawSseViewerOptions) {
35
+ this.#buffer = options.buffer;
36
+ this.#terminalRows = options.terminalRows;
37
+ this.#onExit = options.onExit;
38
+ this.#onStatus = options.onStatus;
39
+ this.#onUpdate = options.onUpdate;
40
+ this.#unsubscribe = this.#buffer.subscribe(() => {
41
+ this.#followIfNeeded();
42
+ this.#onUpdate?.();
43
+ });
44
+ }
45
+
46
+ handleInput(keyData: string): void {
47
+ if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
48
+ this.#unsubscribe();
49
+ this.#onExit();
50
+ return;
51
+ }
52
+
53
+ if (matchesKey(keyData, "ctrl+c")) {
54
+ this.#copyAll();
55
+ return;
56
+ }
57
+
58
+ if (matchesKey(keyData, "up")) {
59
+ this.#followTail = false;
60
+ this.#scrollOffset = Math.max(0, this.#scrollOffset - 1);
61
+ this.#onUpdate?.();
62
+ return;
63
+ }
64
+
65
+ if (matchesKey(keyData, "down")) {
66
+ this.#followTail = false;
67
+ this.#scrollOffset = Math.min(this.#maxScrollOffset(), this.#scrollOffset + 1);
68
+ this.#onUpdate?.();
69
+ return;
70
+ }
71
+
72
+ if (matchesKey(keyData, "pageUp")) {
73
+ this.#followTail = false;
74
+ this.#scrollOffset = Math.max(0, this.#scrollOffset - this.#bodyHeight());
75
+ this.#onUpdate?.();
76
+ return;
77
+ }
78
+
79
+ if (matchesKey(keyData, "pageDown")) {
80
+ this.#followTail = false;
81
+ this.#scrollOffset = Math.min(this.#maxScrollOffset(), this.#scrollOffset + this.#bodyHeight());
82
+ this.#onUpdate?.();
83
+ return;
84
+ }
85
+
86
+ if (matchesKey(keyData, "end")) {
87
+ this.#followTail = true;
88
+ this.#scrollToTail();
89
+ this.#onUpdate?.();
90
+ }
91
+ }
92
+
93
+ invalidate(): void {}
94
+
95
+ render(width: number): string[] {
96
+ this.#lastRenderWidth = Math.max(MIN_VIEWER_WIDTH, width);
97
+ this.#followIfNeeded();
98
+
99
+ const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
100
+ const bodyHeight = this.#bodyHeight();
101
+ const rawLines = this.#renderRawLines(innerWidth);
102
+ const body = rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight);
103
+ while (body.length < bodyHeight) body.push("");
104
+
105
+ return [
106
+ this.#frameTop(innerWidth),
107
+ this.#frameLine(this.#summaryText(), innerWidth),
108
+ this.#frameSeparator(innerWidth),
109
+ ...body.map(line => this.#frameLine(line, innerWidth)),
110
+ this.#frameLine(this.#statusText(), innerWidth),
111
+ this.#frameBottom(innerWidth),
112
+ ];
113
+ }
114
+
115
+ #renderRawLines(innerWidth: number): string[] {
116
+ const snapshot = this.#buffer.snapshot();
117
+ if (snapshot.records.length === 0) {
118
+ return [
119
+ theme.fg("muted", "No raw SSE frames captured yet."),
120
+ theme.fg("muted", "HTTP SSE providers populate this view while a model response is streaming."),
121
+ ];
122
+ }
123
+
124
+ const lines: string[] = [];
125
+ if (snapshot.droppedRecords > 0) {
126
+ lines.push(
127
+ theme.fg(
128
+ "warning",
129
+ `: omp-debug-dropped records=${snapshot.droppedRecords} chars=${snapshot.droppedChars}`,
130
+ ),
131
+ );
132
+ lines.push("");
133
+ }
134
+ for (const record of snapshot.records) {
135
+ for (const line of rawSseRecordLines(record)) {
136
+ lines.push(sanitizeFrameLine(line, innerWidth));
137
+ }
138
+ if (record.kind === "event" && record.truncated) {
139
+ lines.push(theme.fg("warning", `: omp-debug-event-truncated originalChars=${record.originalChars}`));
140
+ }
141
+ lines.push("");
142
+ }
143
+ return lines;
144
+ }
145
+
146
+ #summaryText(): string {
147
+ const snapshot = this.#buffer.snapshot();
148
+ const last = snapshot.lastUpdatedAt ? ` last=${formatRawSseIsoTime(snapshot.lastUpdatedAt)}` : "";
149
+ const follow = this.#followTail ? "follow:on" : "follow:off";
150
+ return ` # raw SSE | events=${snapshot.totalEvents} records=${snapshot.records.length}${last} | ${follow} | Esc back Ctrl+C copy End follow`;
151
+ }
152
+
153
+ #statusText(): string {
154
+ return this.#statusMessage ?? " Up/Down scroll PgUp/PgDn page";
155
+ }
156
+
157
+ #bodyHeight(): number {
158
+ return Math.max(3, this.#terminalRows - VIEWER_FRAME_LINES);
159
+ }
160
+
161
+ #followIfNeeded(): void {
162
+ if (this.#followTail) this.#scrollToTail();
163
+ }
164
+
165
+ #scrollToTail(): void {
166
+ this.#scrollOffset = this.#maxScrollOffset();
167
+ }
168
+
169
+ #maxScrollOffset(): number {
170
+ const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
171
+ return Math.max(0, this.#renderRawLines(innerWidth).length - this.#bodyHeight());
172
+ }
173
+
174
+ #copyAll(): void {
175
+ const payload = this.#buffer.toRawText();
176
+ if (payload.trim().length === 0) {
177
+ const message = "No raw SSE frames to copy";
178
+ this.#statusMessage = message;
179
+ this.#onStatus?.(message);
180
+ this.#onUpdate?.();
181
+ return;
182
+ }
183
+
184
+ try {
185
+ copyToClipboard(payload);
186
+ const message = "Copied raw SSE stream";
187
+ this.#statusMessage = message;
188
+ this.#onStatus?.(message);
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ this.#statusMessage = `Copy failed: ${message}`;
192
+ }
193
+ this.#onUpdate?.();
194
+ }
195
+
196
+ #frameTop(innerWidth: number): string {
197
+ return `${theme.boxSharp.topLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.topRight}`;
198
+ }
199
+
200
+ #frameSeparator(innerWidth: number): string {
201
+ return `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.teeLeft}`;
202
+ }
203
+
204
+ #frameBottom(innerWidth: number): string {
205
+ return `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.bottomRight}`;
206
+ }
207
+
208
+ #frameLine(content: string, innerWidth: number): string {
209
+ const truncated = truncateToWidth(content, innerWidth);
210
+ const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
211
+ return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
212
+ }
213
+ }
package/src/edit/index.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
2
  import { prompt } from "@oh-my-pi/pi-utils";
3
3
  import type { Static } from "@sinclair/typebox";
4
+ import {
5
+ executeHashlineSingle,
6
+ HashlineMismatchError,
7
+ type HashlineParams,
8
+ hashlineEditParamsSchema,
9
+ } from "../hashline";
10
+ import hashlineGrammarTemplate from "../hashline/grammar.lark" with { type: "text" };
11
+ import { resolveHashlineGrammarPlaceholders } from "../hashline/hash";
4
12
  import {
5
13
  createLspWritethrough,
6
14
  type FileDiagnosticsResult,
@@ -16,16 +24,8 @@ import type { ToolSession } from "../tools";
16
24
  import { VimTool, vimSchema } from "../tools/vim";
17
25
  import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
18
26
  import type { VimToolDetails } from "../vim/types";
19
- import { resolveHashlineGrammarPlaceholders } from "./line-hash";
20
27
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
21
28
  import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
22
- import {
23
- executeHashlineSingle,
24
- HashlineMismatchError,
25
- type HashlineParams,
26
- hashlineEditParamsSchema,
27
- } from "./modes/hashline";
28
- import hashlineGrammarTemplate from "./modes/hashline.lark" with { type: "text" };
29
29
  import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
30
30
  import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
31
31
  import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
@@ -34,13 +34,12 @@ export { DEFAULT_EDIT_MODE, type EditMode, normalizeEditMode } from "../utils/ed
34
34
  export * from "./apply-patch";
35
35
  export * from "./diff";
36
36
  export * from "./file-read-cache";
37
- export * from "./line-hash";
38
37
 
39
38
  // Resolve the `$HFMT$` and `$HSEP$` placeholders in the hashline Lark grammar.
40
39
  const hashlineGrammar = resolveHashlineGrammarPlaceholders(hashlineGrammarTemplate);
41
40
 
41
+ export * from "../hashline";
42
42
  export * from "./modes/apply-patch";
43
- export * from "./modes/hashline";
44
43
  export * from "./modes/patch";
45
44
  export * from "./modes/replace";
46
45
  export * from "./normalize";
@@ -12,17 +12,18 @@
12
12
  * The shared renderer / `ToolExecutionComponent` consult the strategy via
13
13
  * the injected `editMode` rather than probing argument shape.
14
14
  */
15
- import type { Theme } from "../modes/theme/theme";
16
- import { type EditMode, resolveEditMode } from "../utils/edit-mode";
17
- import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
18
- import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
15
+
19
16
  import {
20
17
  computeHashlineDiff,
21
18
  computeHashlineSectionDiff,
22
19
  containsRecognizableHashlineOperations,
23
20
  type HashlineInputSection,
24
21
  splitHashlineInputs,
25
- } from "./modes/hashline";
22
+ } from "../hashline";
23
+ import type { Theme } from "../modes/theme/theme";
24
+ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
25
+ import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
26
+ import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
26
27
  import { computePatchDiff, type PatchEditEntry } from "./modes/patch";
27
28
  import type { ReplaceEditEntry } from "./modes/replace";
28
29
 
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
5
5
  import * as util from "node:util";
6
6
  import * as vm from "node:vm";
7
7
 
8
+ import { parse as babelParse } from "@babel/parser";
8
9
  import * as Diff from "diff";
9
10
  import type { ToolSession } from "../../tools";
10
11
  import { ToolError } from "../../tools/tool-errors";
@@ -488,65 +489,108 @@ function buildRequire(cwd: string): NodeJS.Require {
488
489
  return createRequire(pathToFileURL(path.join(cwd, "[eval]")).href);
489
490
  }
490
491
 
491
- // Static `import ... from "x"` is not valid inside vm.runInContext. Rewrite the common
492
- // forms to dynamic `await import(...)` so users can paste ESM-style imports verbatim.
493
- const STATIC_IMPORT_RE = /^[ \t]*import\b(?:[ \t]+([^'"\n]+?)[ \t]+from)?[ \t]*(['"])([^'"\n]+)\2[ \t]*;?[ \t]*$/gm;
494
-
495
- function splitTopLevel(clause: string): string[] {
496
- const out: string[] = [];
497
- let depth = 0;
498
- let buf = "";
499
- for (const ch of clause) {
500
- if (ch === "{") depth++;
501
- else if (ch === "}") depth--;
502
- if (ch === "," && depth === 0) {
503
- if (buf.trim()) out.push(buf.trim());
504
- buf = "";
505
- } else {
506
- buf += ch;
507
- }
508
- }
509
- if (buf.trim()) out.push(buf.trim());
510
- return out;
492
+ // Static `import ... from "x"` is not valid inside vm.runInContext (script-mode parsing).
493
+ // Rewrite top-level static imports to dynamic `await import(...)` so users can paste ESM
494
+ // source verbatim. We use a real parser instead of regex matching so imports embedded in
495
+ // string literals, template literals, or comments — common in codemods — stay intact.
496
+
497
+ type BabelImportDeclaration = {
498
+ type: "ImportDeclaration";
499
+ start: number;
500
+ end: number;
501
+ source: { value: string };
502
+ specifiers: ReadonlyArray<{
503
+ type: "ImportDefaultSpecifier" | "ImportNamespaceSpecifier" | "ImportSpecifier";
504
+ local: { name: string };
505
+ imported?: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
506
+ }>;
507
+ attributes?: ReadonlyArray<{
508
+ key: { type: "Identifier"; name: string } | { type: "StringLiteral"; value: string };
509
+ value: { value: string };
510
+ }>;
511
+ };
512
+
513
+ function buildDynamicImportCall(sourceLiteral: string, withClause: string | undefined): string {
514
+ return withClause ? `import(${sourceLiteral}, { with: ${withClause} })` : `import(${sourceLiteral})`;
511
515
  }
512
516
 
513
- function rewriteImportClause(clause: string, sourceLiteral: string): string {
517
+ function buildWithClause(node: BabelImportDeclaration): string | undefined {
518
+ const attrs = node.attributes;
519
+ if (!attrs || attrs.length === 0) return undefined;
520
+ const pairs = attrs.map(attr => {
521
+ const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
522
+ return `${key}: ${JSON.stringify(attr.value.value)}`;
523
+ });
524
+ return `{ ${pairs.join(", ")} }`;
525
+ }
526
+
527
+ function rewriteImportNode(node: BabelImportDeclaration): string {
528
+ const sourceLiteral = JSON.stringify(node.source.value);
529
+ const withClause = buildWithClause(node);
530
+ const importCall = buildDynamicImportCall(sourceLiteral, withClause);
531
+
514
532
  let defaultName: string | undefined;
515
533
  let namespaceName: string | undefined;
516
- let namedBlock: string | undefined;
517
- for (const part of splitTopLevel(clause)) {
518
- if (part.startsWith("{")) {
519
- namedBlock = part;
520
- } else if (part.startsWith("*")) {
521
- const m = part.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/);
522
- if (!m) return `await import(${sourceLiteral}); /* unrewritten import: ${clause} */`;
523
- namespaceName = m[1];
524
- } else if (/^[A-Za-z_$][\w$]*$/.test(part)) {
525
- defaultName = part;
526
- } else {
527
- return `await import(${sourceLiteral}); /* unrewritten import: ${clause} */`;
534
+ const namedPairs: Array<[string, string]> = [];
535
+ for (const spec of node.specifiers) {
536
+ if (spec.type === "ImportDefaultSpecifier") {
537
+ defaultName = spec.local.name;
538
+ } else if (spec.type === "ImportNamespaceSpecifier") {
539
+ namespaceName = spec.local.name;
540
+ } else if (spec.type === "ImportSpecifier" && spec.imported) {
541
+ const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
542
+ namedPairs.push([imported, spec.local.name]);
528
543
  }
529
544
  }
530
- if (namedBlock) {
531
- const inner = namedBlock.slice(1, -1).trim();
532
- const renamed = inner.replace(/([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g, "$1: $2");
533
- const props = defaultName ? `default: ${defaultName}, ${renamed}` : renamed;
534
- return `const { ${props} } = await import(${sourceLiteral});`;
545
+
546
+ if (namedPairs.length > 0) {
547
+ const inner = namedPairs.map(([imp, loc]) => (imp === loc ? imp : `${imp}: ${loc}`)).join(", ");
548
+ const props = defaultName ? `default: ${defaultName}, ${inner}` : inner;
549
+ return `const { ${props} } = await ${importCall};`;
535
550
  }
536
551
  if (namespaceName && defaultName) {
537
- return `const ${namespaceName} = await import(${sourceLiteral}); const ${defaultName} = ${namespaceName}.default;`;
552
+ return `const ${namespaceName} = await ${importCall}; const ${defaultName} = ${namespaceName}.default;`;
538
553
  }
539
- if (namespaceName) return `const ${namespaceName} = await import(${sourceLiteral});`;
540
- if (defaultName) return `const ${defaultName} = (await import(${sourceLiteral})).default;`;
541
- return `await import(${sourceLiteral});`;
554
+ if (namespaceName) return `const ${namespaceName} = await ${importCall};`;
555
+ if (defaultName) return `const ${defaultName} = (await ${importCall}).default;`;
556
+ return `await ${importCall};`;
542
557
  }
543
558
 
544
559
  export function rewriteStaticImports(code: string): string {
545
- return code.replace(STATIC_IMPORT_RE, (_match, clause: string | undefined, _quote, source: string) => {
546
- const literal = JSON.stringify(source);
547
- if (!clause) return `await import(${literal});`;
548
- return rewriteImportClause(clause.trim(), literal);
549
- });
560
+ if (!code.includes("import")) return code;
561
+
562
+ let ast: { program: { body: ReadonlyArray<{ type: string }> } };
563
+ try {
564
+ ast = babelParse(code, {
565
+ sourceType: "module",
566
+ allowAwaitOutsideFunction: true,
567
+ allowReturnOutsideFunction: true,
568
+ allowImportExportEverywhere: true,
569
+ allowNewTargetOutsideFunction: true,
570
+ allowSuperOutsideMethod: true,
571
+ allowUndeclaredExports: true,
572
+ errorRecovery: true,
573
+ }) as unknown as typeof ast;
574
+ } catch {
575
+ // Parser bailed entirely — let the VM surface the real syntax error.
576
+ return code;
577
+ }
578
+
579
+ // Only rewrite top-level imports. Anything nested deeper is invalid JS anyway and the
580
+ // VM will report it.
581
+ const imports: BabelImportDeclaration[] = [];
582
+ for (const node of ast.program.body) {
583
+ if (node.type === "ImportDeclaration") imports.push(node as unknown as BabelImportDeclaration);
584
+ }
585
+ if (imports.length === 0) return code;
586
+
587
+ // Splice from the back so earlier offsets stay valid.
588
+ imports.sort((a, b) => b.start - a.start);
589
+ let result = code;
590
+ for (const node of imports) {
591
+ result = result.slice(0, node.start) + rewriteImportNode(node) + result.slice(node.end);
592
+ }
593
+ return result;
550
594
  }
551
595
 
552
596
  function wrapCode(code: string): { source: string; asyncWrapped: boolean } {
@@ -17,7 +17,7 @@ import type { ExecOptions } from "../../exec/exec";
17
17
  import { execCommand } from "../../exec/exec";
18
18
  import type { CustomMessage } from "../../session/messages";
19
19
  import { EventBus } from "../../utils/event-bus";
20
- import { installLegacyPiSpecifierShim } from "../plugins/legacy-pi-compat";
20
+ import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
21
21
  import { getAllPluginExtensionPaths } from "../plugins/loader";
22
22
 
23
23
  import { resolvePath } from "../utils";
@@ -36,6 +36,12 @@ import type {
36
36
  installLegacyPiSpecifierShim();
37
37
 
38
38
  type HandlerFn = (...args: unknown[]) => Promise<unknown>;
39
+ type LoadedExtensionModule = ExtensionFactory | { default?: ExtensionFactory };
40
+
41
+ function getExtensionFactory(module: LoadedExtensionModule): ExtensionFactory | null {
42
+ const candidate = typeof module === "function" ? module : module.default;
43
+ return typeof candidate === "function" ? candidate : null;
44
+ }
39
45
 
40
46
  export class ExtensionRuntimeNotInitializedError extends Error {
41
47
  constructor() {
@@ -272,8 +278,8 @@ async function loadExtension(
272
278
  ): Promise<{ extension: Extension | null; error: string | null }> {
273
279
  const resolvedPath = resolvePath(extensionPath, cwd);
274
280
  try {
275
- const module = await import(`omp-legacy-pi-file:${resolvedPath}`);
276
- const factory = (module.default ?? module) as ExtensionFactory;
281
+ const module = (await loadLegacyPiModule(resolvedPath)) as LoadedExtensionModule;
282
+ const factory = getExtensionFactory(module);
277
283
 
278
284
  if (typeof factory !== "function") {
279
285
  return {
@@ -22,7 +22,7 @@ import type {
22
22
  } from "@oh-my-pi/pi-ai";
23
23
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
24
24
  import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
25
- import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
25
+ import type { AutocompleteItem, Component, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
26
26
  import type { Static, TSchema } from "@sinclair/typebox";
27
27
  import type { Rule } from "../../capability/rule";
28
28
  import type { KeybindingsManager } from "../../config/keybindings";
@@ -31,6 +31,7 @@ import type { EditToolDetails } from "../../edit";
31
31
  import type { PythonResult } from "../../eval/py/executor";
32
32
  import type { BashResult } from "../../exec/bash-executor";
33
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
34
+ import type { CustomEditor } from "../../modes/components/custom-editor";
34
35
  import type { Theme } from "../../modes/theme/theme";
35
36
  import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
36
37
  import type { CustomMessage } from "../../session/messages";
@@ -170,9 +171,15 @@ export interface ExtensionUIContext {
170
171
  editorOptions?: { promptStyle?: boolean },
171
172
  ): Promise<string | undefined>;
172
173
 
173
- /** Set a custom editor component via factory function, or undefined to restore the default editor. */
174
+ /**
175
+ * Set a custom editor component via factory function, or `undefined` to restore the default editor.
176
+ *
177
+ * The factory must return a {@link CustomEditor} subclass. Plain `EditorComponent`/`Editor`
178
+ * instances do not implement the action-keys, escape callbacks, and custom-key-handler surface
179
+ * required by interactive mode.
180
+ */
174
181
  setEditorComponent(
175
- factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent) | undefined,
182
+ factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
176
183
  ): void;
177
184
 
178
185
  /** Get the current theme for styling. */