@plannotator/pi-extension 0.19.10 → 0.19.11
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/generated/agent-review-message.ts +148 -0
- package/generated/claude-review.ts +5 -1
- package/generated/codex-review.ts +0 -82
- package/generated/jj-core.ts +329 -0
- package/generated/review-args.ts +86 -0
- package/generated/review-core.ts +73 -1
- package/generated/tour-review.ts +17 -33
- package/generated/vcs-core.ts +452 -0
- package/index.ts +9 -4
- package/package.json +1 -1
- package/plannotator-browser.ts +143 -133
- package/plannotator-events.ts +5 -1
- package/plannotator.html +3 -3
- package/review-editor.html +98 -51
- package/server/serverReview.ts +30 -102
- package/server/vcs.ts +120 -0
- package/server.ts +17 -3
package/plannotator-browser.ts
CHANGED
|
@@ -6,13 +6,13 @@ import { createWorktreePool, type WorktreePool } from "./generated/worktree-pool
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
prepareLocalReviewDiff,
|
|
10
10
|
reviewRuntime,
|
|
11
|
-
runGitDiff,
|
|
12
11
|
startAnnotateServer,
|
|
13
12
|
startPlanReviewServer,
|
|
14
13
|
startReviewServer,
|
|
15
14
|
type DiffType,
|
|
15
|
+
type VcsSelection,
|
|
16
16
|
} from "./server.js";
|
|
17
17
|
import { openBrowser, isRemoteSession } from "./server/network.js";
|
|
18
18
|
import { parsePRUrl, checkPRAuth, fetchPR } from "./server/pr.js";
|
|
@@ -190,9 +190,13 @@ export async function openPlanReviewBrowser(
|
|
|
190
190
|
return session.waitForDecision();
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
export function shouldUseLocalPrCheckout(options: { useLocal?: boolean }): boolean {
|
|
194
|
+
return options.useLocal !== false;
|
|
195
|
+
}
|
|
196
|
+
|
|
193
197
|
export async function openCodeReview(
|
|
194
198
|
ctx: ExtensionContext,
|
|
195
|
-
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
|
|
199
|
+
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string; vcsType?: VcsSelection; useLocal?: boolean } = {},
|
|
196
200
|
): Promise<{ approved: boolean; feedback?: string; annotations?: unknown[]; agentSwitch?: string; exit?: boolean }> {
|
|
197
201
|
const session = await startCodeReviewBrowserSession(ctx, options);
|
|
198
202
|
return session.waitForDecision();
|
|
@@ -200,7 +204,7 @@ export async function openCodeReview(
|
|
|
200
204
|
|
|
201
205
|
export async function startCodeReviewBrowserSession(
|
|
202
206
|
ctx: ExtensionContext,
|
|
203
|
-
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
|
|
207
|
+
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string; vcsType?: VcsSelection; useLocal?: boolean } = {},
|
|
204
208
|
): Promise<
|
|
205
209
|
BrowserDecisionSession<{
|
|
206
210
|
approved: boolean;
|
|
@@ -220,7 +224,7 @@ export async function startCodeReviewBrowserSession(
|
|
|
220
224
|
let rawPatch: string;
|
|
221
225
|
let gitRef: string;
|
|
222
226
|
let diffError: string | undefined;
|
|
223
|
-
let gitCtx: Awaited<ReturnType<typeof
|
|
227
|
+
let gitCtx: Awaited<ReturnType<typeof prepareLocalReviewDiff>>["gitContext"] | undefined;
|
|
224
228
|
let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;
|
|
225
229
|
let diffType: DiffType | undefined;
|
|
226
230
|
let agentCwd: string | undefined;
|
|
@@ -260,152 +264,158 @@ export async function startCodeReviewBrowserSession(
|
|
|
260
264
|
gitRef = `${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}`;
|
|
261
265
|
prMetadata = pr.metadata;
|
|
262
266
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const repoDir = options.cwd ?? ctx.cwd;
|
|
268
|
-
const identifier = prMetadata.platform === "github"
|
|
269
|
-
? `${prMetadata.owner}-${prMetadata.repo}-${prMetadata.number}`
|
|
270
|
-
: `${prMetadata.projectPath.replace(/\//g, "-")}-${prMetadata.iid}`;
|
|
271
|
-
const suffix = Math.random().toString(36).slice(2, 8);
|
|
272
|
-
const prNumber = prMetadata.platform === "github" ? prMetadata.number : prMetadata.iid;
|
|
273
|
-
sessionDir = join(realpathSync(tmpdir()), `plannotator-pr-${identifier}-${suffix}`);
|
|
274
|
-
localPath = join(sessionDir, "pool", `pr-${prNumber}`);
|
|
275
|
-
const fetchRefStr = prMetadata.platform === "github"
|
|
276
|
-
? `refs/pull/${prMetadata.number}/head`
|
|
277
|
-
: `refs/merge-requests/${prMetadata.iid}/head`;
|
|
278
|
-
|
|
279
|
-
// Validate inputs from platform API to prevent git flag/path injection
|
|
280
|
-
if (prMetadata.baseBranch.includes('..') || prMetadata.baseBranch.startsWith('-')) throw new Error(`Invalid base branch: ${prMetadata.baseBranch}`);
|
|
281
|
-
if (!/^[0-9a-f]{40,64}$/i.test(prMetadata.baseSha)) throw new Error(`Invalid base SHA: ${prMetadata.baseSha}`);
|
|
282
|
-
|
|
283
|
-
// Detect same-repo vs cross-repo (must match both owner/repo AND host)
|
|
284
|
-
let isSameRepo = false;
|
|
267
|
+
if (shouldUseLocalPrCheckout(options)) {
|
|
268
|
+
// Create local worktree for agent file access (--local is the default for PR reviews)
|
|
269
|
+
let localPath: string | undefined;
|
|
270
|
+
let sessionDir: string | undefined;
|
|
285
271
|
try {
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
272
|
+
const repoDir = options.cwd ?? ctx.cwd;
|
|
273
|
+
const identifier = prMetadata.platform === "github"
|
|
274
|
+
? `${prMetadata.owner}-${prMetadata.repo}-${prMetadata.number}`
|
|
275
|
+
: `${prMetadata.projectPath.replace(/\//g, "-")}-${prMetadata.iid}`;
|
|
276
|
+
const suffix = Math.random().toString(36).slice(2, 8);
|
|
277
|
+
const prNumber = prMetadata.platform === "github" ? prMetadata.number : prMetadata.iid;
|
|
278
|
+
sessionDir = join(realpathSync(tmpdir()), `plannotator-pr-${identifier}-${suffix}`);
|
|
279
|
+
localPath = join(sessionDir, "pool", `pr-${prNumber}`);
|
|
280
|
+
const fetchRefStr = prMetadata.platform === "github"
|
|
281
|
+
? `refs/pull/${prMetadata.number}/head`
|
|
282
|
+
: `refs/merge-requests/${prMetadata.iid}/head`;
|
|
283
|
+
|
|
284
|
+
// Validate inputs from platform API to prevent git flag/path injection
|
|
285
|
+
if (prMetadata.baseBranch.includes('..') || prMetadata.baseBranch.startsWith('-')) throw new Error(`Invalid base branch: ${prMetadata.baseBranch}`);
|
|
286
|
+
if (!/^[0-9a-f]{40,64}$/i.test(prMetadata.baseSha)) throw new Error(`Invalid base SHA: ${prMetadata.baseSha}`);
|
|
287
|
+
|
|
288
|
+
// Detect same-repo vs cross-repo (must match both owner/repo AND host)
|
|
289
|
+
let isSameRepo = false;
|
|
290
|
+
try {
|
|
291
|
+
const remoteResult = await reviewRuntime.runGit(["remote", "get-url", "origin"], { cwd: repoDir });
|
|
292
|
+
if (remoteResult.exitCode === 0) {
|
|
293
|
+
const remoteUrl = remoteResult.stdout.trim();
|
|
294
|
+
const currentRepo = parseRemoteUrl(remoteUrl);
|
|
295
|
+
const prRepo = prMetadata.platform === "github"
|
|
296
|
+
? `${prMetadata.owner}/${prMetadata.repo}`
|
|
297
|
+
: prMetadata.projectPath;
|
|
298
|
+
const repoMatches = !!currentRepo && currentRepo.toLowerCase() === prRepo.toLowerCase();
|
|
299
|
+
const sshHost = remoteUrl.match(/^[^@]+@([^:]+):/)?.[1];
|
|
300
|
+
const httpsHost = (() => { try { return new URL(remoteUrl).hostname; } catch { return null; } })();
|
|
301
|
+
const remoteHost = (sshHost || httpsHost || "").toLowerCase();
|
|
302
|
+
const prHost = prMetadata.host.toLowerCase();
|
|
303
|
+
isSameRepo = repoMatches && remoteHost === prHost;
|
|
304
|
+
}
|
|
305
|
+
} catch { /* not in a git repo — cross-repo path */ }
|
|
306
|
+
|
|
307
|
+
if (isSameRepo) {
|
|
308
|
+
// ── Same-repo: fast worktree path ──
|
|
309
|
+
console.error("Fetching PR branch and creating local worktree...");
|
|
310
|
+
await fetchRef(reviewRuntime, prMetadata.baseBranch, { cwd: repoDir });
|
|
311
|
+
await ensureObjectAvailable(reviewRuntime, prMetadata.baseSha, { cwd: repoDir });
|
|
312
|
+
await fetchRef(reviewRuntime, fetchRefStr, { cwd: repoDir });
|
|
313
|
+
|
|
314
|
+
await createWorktree(reviewRuntime, {
|
|
315
|
+
ref: "FETCH_HEAD",
|
|
316
|
+
path: localPath,
|
|
317
|
+
detach: true,
|
|
318
|
+
cwd: repoDir,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const wtRepoDir = repoDir;
|
|
322
|
+
exitHandler = () => {
|
|
323
|
+
try {
|
|
324
|
+
for (const entry of worktreePool?.entries() ?? []) {
|
|
325
|
+
spawnSync("git", ["worktree", "remove", "--force", entry.path], { cwd: wtRepoDir });
|
|
326
|
+
}
|
|
327
|
+
} catch {}
|
|
328
|
+
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
329
|
+
};
|
|
330
|
+
worktreeCleanup = async () => {
|
|
331
|
+
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
|
332
|
+
if (worktreePool) await worktreePool.cleanup(reviewRuntime);
|
|
333
|
+
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
334
|
+
};
|
|
335
|
+
process.once("exit", exitHandler);
|
|
336
|
+
} else {
|
|
337
|
+
// ── Cross-repo: shallow clone + fetch PR head ──
|
|
290
338
|
const prRepo = prMetadata.platform === "github"
|
|
291
339
|
? `${prMetadata.owner}/${prMetadata.repo}`
|
|
292
340
|
: prMetadata.projectPath;
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
cwd:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// ── Cross-repo: shallow clone + fetch PR head ──
|
|
333
|
-
const prRepo = prMetadata.platform === "github"
|
|
334
|
-
? `${prMetadata.owner}/${prMetadata.repo}`
|
|
335
|
-
: prMetadata.projectPath;
|
|
336
|
-
if (/^-/.test(prRepo)) throw new Error(`Invalid repository identifier: ${prRepo}`);
|
|
337
|
-
const cli = prMetadata.platform === "github" ? "gh" : "glab";
|
|
338
|
-
const host = prMetadata.host;
|
|
339
|
-
// gh/glab repo clone doesn't accept --hostname; set GH_HOST/GITLAB_HOST env instead
|
|
340
|
-
const isDefaultHost = host === "github.com" || host === "gitlab.com";
|
|
341
|
-
const cloneEnv = isDefaultHost ? undefined : {
|
|
342
|
-
...process.env,
|
|
343
|
-
...(prMetadata.platform === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
console.error(`Cloning ${prRepo} (shallow)...`);
|
|
347
|
-
const cloneResult = spawnSync(cli, ["repo", "clone", prRepo, localPath, "--", "--depth=1", "--no-checkout"], { encoding: "utf-8", env: cloneEnv });
|
|
348
|
-
if ((cloneResult.status ?? 1) !== 0) {
|
|
349
|
-
throw new Error(`${cli} repo clone failed: ${(cloneResult.stderr ?? "").trim()}`);
|
|
341
|
+
if (/^-/.test(prRepo)) throw new Error(`Invalid repository identifier: ${prRepo}`);
|
|
342
|
+
const cli = prMetadata.platform === "github" ? "gh" : "glab";
|
|
343
|
+
const host = prMetadata.host;
|
|
344
|
+
// gh/glab repo clone doesn't accept --hostname; set GH_HOST/GITLAB_HOST env instead
|
|
345
|
+
const isDefaultHost = host === "github.com" || host === "gitlab.com";
|
|
346
|
+
const cloneEnv = isDefaultHost ? undefined : {
|
|
347
|
+
...process.env,
|
|
348
|
+
...(prMetadata.platform === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
console.error(`Cloning ${prRepo} (shallow)...`);
|
|
352
|
+
const cloneResult = spawnSync(cli, ["repo", "clone", prRepo, localPath, "--", "--depth=1", "--no-checkout"], { encoding: "utf-8", env: cloneEnv });
|
|
353
|
+
if ((cloneResult.status ?? 1) !== 0) {
|
|
354
|
+
throw new Error(`${cli} repo clone failed: ${(cloneResult.stderr ?? "").trim()}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.error("Fetching PR branch...");
|
|
358
|
+
const fetchResult = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", fetchRefStr], { cwd: localPath });
|
|
359
|
+
if (fetchResult.exitCode !== 0) throw new Error(`Failed to fetch PR head ref: ${fetchResult.stderr.trim()}`);
|
|
360
|
+
|
|
361
|
+
const checkoutResult = await reviewRuntime.runGit(["checkout", "FETCH_HEAD"], { cwd: localPath });
|
|
362
|
+
if (checkoutResult.exitCode !== 0) {
|
|
363
|
+
throw new Error(`git checkout FETCH_HEAD failed: ${checkoutResult.stderr.trim()}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Best-effort: create base refs so agent diffs work
|
|
367
|
+
const baseFetch = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", prMetadata.baseSha], { cwd: localPath });
|
|
368
|
+
if (baseFetch.exitCode !== 0) console.error("Warning: failed to fetch baseSha, agent diffs may be inaccurate");
|
|
369
|
+
await reviewRuntime.runGit(["branch", "--", prMetadata.baseBranch, prMetadata.baseSha], { cwd: localPath });
|
|
370
|
+
await reviewRuntime.runGit(["update-ref", `refs/remotes/origin/${prMetadata.baseBranch}`, prMetadata.baseSha], { cwd: localPath });
|
|
371
|
+
|
|
372
|
+
exitHandler = () => {
|
|
373
|
+
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
374
|
+
};
|
|
375
|
+
worktreeCleanup = () => {
|
|
376
|
+
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
|
377
|
+
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
378
|
+
};
|
|
379
|
+
process.once("exit", exitHandler);
|
|
350
380
|
}
|
|
351
381
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
exitHandler = () => {
|
|
368
|
-
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
369
|
-
};
|
|
370
|
-
worktreeCleanup = () => {
|
|
371
|
-
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
|
372
|
-
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
373
|
-
};
|
|
374
|
-
process.once("exit", exitHandler);
|
|
382
|
+
agentCwd = localPath;
|
|
383
|
+
worktreePool = createWorktreePool(
|
|
384
|
+
{ sessionDir: sessionDir!, repoDir, isSameRepo },
|
|
385
|
+
{ path: localPath, prUrl: prMetadata.url, number: prNumber, ready: true },
|
|
386
|
+
);
|
|
387
|
+
console.error(`Local checkout ready at ${localPath}`);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error("Warning: local worktree creation failed, falling back to remote diff");
|
|
390
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
391
|
+
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
|
392
|
+
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
393
|
+
agentCwd = undefined;
|
|
394
|
+
worktreePool = undefined;
|
|
395
|
+
worktreeCleanup = undefined;
|
|
375
396
|
}
|
|
376
|
-
|
|
377
|
-
agentCwd = localPath;
|
|
378
|
-
worktreePool = createWorktreePool(
|
|
379
|
-
{ sessionDir: sessionDir!, repoDir, isSameRepo },
|
|
380
|
-
{ path: localPath, prUrl: prMetadata.url, number: prNumber, ready: true },
|
|
381
|
-
);
|
|
382
|
-
console.error(`Local checkout ready at ${localPath}`);
|
|
383
|
-
} catch (err) {
|
|
384
|
-
console.error("Warning: local worktree creation failed, falling back to remote diff");
|
|
385
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
386
|
-
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
|
387
|
-
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
|
388
|
-
agentCwd = undefined;
|
|
389
|
-
worktreePool = undefined;
|
|
390
|
-
worktreeCleanup = undefined;
|
|
391
397
|
}
|
|
392
398
|
} else {
|
|
393
399
|
// --- Local Review Mode ---
|
|
394
400
|
const cwd = options.cwd ?? ctx.cwd;
|
|
395
|
-
gitCtx = await getGitContext(cwd);
|
|
396
|
-
const defaultBranch = options.defaultBranch ?? gitCtx.defaultBranch;
|
|
397
401
|
const config = loadConfig();
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
const result = await prepareLocalReviewDiff({
|
|
403
|
+
cwd,
|
|
404
|
+
vcsType: options.vcsType,
|
|
405
|
+
requestedDiffType: options.diffType,
|
|
406
|
+
requestedBase: options.defaultBranch,
|
|
407
|
+
configuredDiffType: resolveDefaultDiffType(config),
|
|
400
408
|
hideWhitespace: config.diffOptions?.hideWhitespace ?? false,
|
|
401
409
|
});
|
|
402
|
-
|
|
403
|
-
|
|
410
|
+
gitCtx = result.gitContext;
|
|
411
|
+
diffType = result.diffType;
|
|
412
|
+
rawPatch = result.rawPatch;
|
|
413
|
+
gitRef = result.gitRef;
|
|
404
414
|
diffError = result.error;
|
|
405
415
|
// Remember which base the initial diff was computed against so it can
|
|
406
416
|
// be forwarded to the server below. Only matters when the caller
|
|
407
417
|
// overrode the detected default; otherwise it matches gitCtx already.
|
|
408
|
-
initialBase =
|
|
418
|
+
initialBase = result.base;
|
|
409
419
|
}
|
|
410
420
|
|
|
411
421
|
const server = await startReviewServer({
|
package/plannotator-events.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import type { DiffType } from "./server.js";
|
|
5
|
+
import type { DiffType, VcsSelection } from "./server.js";
|
|
6
6
|
import {
|
|
7
7
|
getLastAssistantMessageText,
|
|
8
8
|
getStartupErrorMessage,
|
|
@@ -87,6 +87,8 @@ export type PlannotatorReviewStatusResult =
|
|
|
87
87
|
export interface PlannotatorCodeReviewPayload {
|
|
88
88
|
diffType?: DiffType;
|
|
89
89
|
defaultBranch?: string;
|
|
90
|
+
vcsType?: VcsSelection;
|
|
91
|
+
useLocal?: boolean;
|
|
90
92
|
cwd?: string;
|
|
91
93
|
prUrl?: string;
|
|
92
94
|
}
|
|
@@ -264,6 +266,8 @@ export function registerPlannotatorEventListeners(pi: ExtensionAPI): void {
|
|
|
264
266
|
cwd: request.payload?.cwd,
|
|
265
267
|
defaultBranch: request.payload?.defaultBranch,
|
|
266
268
|
diffType: request.payload?.diffType,
|
|
269
|
+
vcsType: request.payload?.vcsType,
|
|
270
|
+
useLocal: request.payload?.useLocal,
|
|
267
271
|
prUrl: request.payload?.prUrl,
|
|
268
272
|
});
|
|
269
273
|
request.respond({ status: "handled", result });
|