@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.
- package/CHANGELOG.md +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/extensions/types.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +50 -11
- package/src/modes/prompt-action-autocomplete.ts +3 -0
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- 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";
|
package/src/edit/streaming.ts
CHANGED
|
@@ -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
|
-
|
|
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 "
|
|
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
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
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
|
-
|
|
517
|
-
for (const
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
} else if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
const props = defaultName ? `default: ${defaultName}, ${
|
|
534
|
-
return `const { ${props} } = await
|
|
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
|
|
552
|
+
return `const ${namespaceName} = await ${importCall}; const ${defaultName} = ${namespaceName}.default;`;
|
|
538
553
|
}
|
|
539
|
-
if (namespaceName) return `const ${namespaceName} = await
|
|
540
|
-
if (defaultName) return `const ${defaultName} = (await
|
|
541
|
-
return `await
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
276
|
-
const factory = (module
|
|
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,
|
|
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
|
-
/**
|
|
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) =>
|
|
182
|
+
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => CustomEditor) | undefined,
|
|
176
183
|
): void;
|
|
177
184
|
|
|
178
185
|
/** Get the current theme for styling. */
|