Add plannotator extension v0.19.10
This commit is contained in:
549
extensions/plannotator/plannotator-browser.ts
Normal file
549
extensions/plannotator/plannotator-browser.ts
Normal file
@@ -0,0 +1,549 @@
|
||||
import { existsSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createWorktreePool, type WorktreePool } from "./generated/worktree-pool.js";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
getGitContext,
|
||||
reviewRuntime,
|
||||
runGitDiff,
|
||||
startAnnotateServer,
|
||||
startPlanReviewServer,
|
||||
startReviewServer,
|
||||
type DiffType,
|
||||
} from "./server.js";
|
||||
import { openBrowser, isRemoteSession } from "./server/network.js";
|
||||
import { parsePRUrl, checkPRAuth, fetchPR } from "./server/pr.js";
|
||||
import {
|
||||
getMRLabel,
|
||||
getMRNumberLabel,
|
||||
getDisplayRepo,
|
||||
getCliName,
|
||||
getCliInstallUrl,
|
||||
} from "./generated/pr-provider.js";
|
||||
import { parseRemoteUrl } from "./generated/repo.js";
|
||||
import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "./generated/worktree.js";
|
||||
import { loadConfig, resolveDefaultDiffType } from "./generated/config.js";
|
||||
export { getLastAssistantMessageText } from "./assistant-message.js";
|
||||
|
||||
export type AnnotateMode = "annotate" | "annotate-folder" | "annotate-last";
|
||||
export interface PlanReviewDecision {
|
||||
approved: boolean;
|
||||
feedback?: string;
|
||||
savedPath?: string;
|
||||
agentSwitch?: string;
|
||||
permissionMode?: string;
|
||||
}
|
||||
|
||||
export interface BrowserDecisionSession<T> {
|
||||
url: string;
|
||||
waitForDecision: () => Promise<T>;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
export interface PlanReviewBrowserSession extends BrowserDecisionSession<PlanReviewDecision> {
|
||||
reviewId: string;
|
||||
onDecision: (listener: (result: PlanReviewDecision) => void | Promise<void>) => () => void;
|
||||
}
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
let planHtmlContent = "";
|
||||
let reviewHtmlContent = "";
|
||||
|
||||
try {
|
||||
planHtmlContent = readFileSync(resolve(__dirname, "plannotator.html"), "utf-8");
|
||||
} catch {
|
||||
// built assets unavailable
|
||||
}
|
||||
|
||||
try {
|
||||
reviewHtmlContent = readFileSync(resolve(__dirname, "review-editor.html"), "utf-8");
|
||||
} catch {
|
||||
// built assets unavailable
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
||||
}
|
||||
|
||||
export function hasPlanBrowserHtml(): boolean {
|
||||
return Boolean(planHtmlContent);
|
||||
}
|
||||
|
||||
export function hasReviewBrowserHtml(): boolean {
|
||||
return Boolean(reviewHtmlContent);
|
||||
}
|
||||
|
||||
export function getStartupErrorMessage(err: unknown): string {
|
||||
return err instanceof Error ? err.message : "Unknown error";
|
||||
}
|
||||
|
||||
function openBrowserForServer(serverUrl: string, ctx: ExtensionContext): void {
|
||||
const browserResult = openBrowser(serverUrl);
|
||||
if (isRemoteSession()) {
|
||||
ctx.ui.notify(`[Plannotator] ${serverUrl}`, "info");
|
||||
} else if (!browserResult.opened) {
|
||||
ctx.ui.notify(`Open this URL to review: ${serverUrl}`, "info");
|
||||
}
|
||||
}
|
||||
|
||||
async function openBrowserAndWait<T>(
|
||||
server: { url: string; stop: () => void },
|
||||
ctx: ExtensionContext,
|
||||
waitForResult: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
openBrowserForServer(server.url, ctx);
|
||||
return waitForDecisionWithCleanup(server, waitForResult);
|
||||
}
|
||||
|
||||
async function waitForDecisionWithCleanup<T>(
|
||||
server: { url: string; stop: () => void },
|
||||
waitForResult: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const result = await waitForResult();
|
||||
await delay(1500);
|
||||
return result;
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function startBrowserDecisionSession<T>(
|
||||
server: { url: string; stop: () => void },
|
||||
ctx: ExtensionContext,
|
||||
waitForResult: () => Promise<T>,
|
||||
): BrowserDecisionSession<T> {
|
||||
openBrowserForServer(server.url, ctx);
|
||||
let stopped = false;
|
||||
let stopReject: ((err: Error) => void) | undefined;
|
||||
let decisionPromise: Promise<T> | undefined;
|
||||
const createStoppedError = () => new Error("Plannotator browser session was stopped.");
|
||||
const stop = () => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
server.stop();
|
||||
stopReject?.(createStoppedError());
|
||||
stopReject = undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
url: server.url,
|
||||
waitForDecision: () => {
|
||||
if (decisionPromise) return decisionPromise;
|
||||
if (stopped) return Promise.reject(createStoppedError());
|
||||
decisionPromise = (async () => {
|
||||
const stoppedPromise = new Promise<never>((_, reject) => {
|
||||
stopReject = reject;
|
||||
});
|
||||
try {
|
||||
const result = await Promise.race([waitForResult(), stoppedPromise]);
|
||||
stopReject = undefined;
|
||||
await delay(1500);
|
||||
return result;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
})();
|
||||
return decisionPromise;
|
||||
},
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
export async function startPlanReviewBrowserSession(
|
||||
ctx: ExtensionContext,
|
||||
planContent: string,
|
||||
): Promise<PlanReviewBrowserSession> {
|
||||
if (!ctx.hasUI || !planHtmlContent) {
|
||||
throw new Error("Plannotator browser review is unavailable in this session.");
|
||||
}
|
||||
|
||||
const server = await startPlanReviewServer({
|
||||
plan: planContent,
|
||||
htmlContent: planHtmlContent,
|
||||
origin: "pi",
|
||||
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
|
||||
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
|
||||
pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
|
||||
});
|
||||
|
||||
const session = startBrowserDecisionSession(server, ctx, server.waitForDecision);
|
||||
server.onDecision(() => {
|
||||
setTimeout(() => session.stop(), 1500);
|
||||
});
|
||||
|
||||
return {
|
||||
...session,
|
||||
reviewId: server.reviewId,
|
||||
onDecision: server.onDecision,
|
||||
};
|
||||
}
|
||||
|
||||
export async function openPlanReviewBrowser(
|
||||
ctx: ExtensionContext,
|
||||
planContent: string,
|
||||
): Promise<PlanReviewDecision> {
|
||||
const session = await startPlanReviewBrowserSession(ctx, planContent);
|
||||
return session.waitForDecision();
|
||||
}
|
||||
|
||||
export async function openCodeReview(
|
||||
ctx: ExtensionContext,
|
||||
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
|
||||
): Promise<{ approved: boolean; feedback?: string; annotations?: unknown[]; agentSwitch?: string; exit?: boolean }> {
|
||||
const session = await startCodeReviewBrowserSession(ctx, options);
|
||||
return session.waitForDecision();
|
||||
}
|
||||
|
||||
export async function startCodeReviewBrowserSession(
|
||||
ctx: ExtensionContext,
|
||||
options: { cwd?: string; defaultBranch?: string; diffType?: DiffType; prUrl?: string } = {},
|
||||
): Promise<
|
||||
BrowserDecisionSession<{
|
||||
approved: boolean;
|
||||
feedback?: string;
|
||||
annotations?: unknown[];
|
||||
agentSwitch?: string;
|
||||
exit?: boolean;
|
||||
}>
|
||||
> {
|
||||
if (!ctx.hasUI || !reviewHtmlContent) {
|
||||
throw new Error("Plannotator code review browser is unavailable in this session.");
|
||||
}
|
||||
|
||||
const urlArg = options.prUrl;
|
||||
const isPRMode = urlArg?.startsWith("http://") || urlArg?.startsWith("https://");
|
||||
|
||||
let rawPatch: string;
|
||||
let gitRef: string;
|
||||
let diffError: string | undefined;
|
||||
let gitCtx: Awaited<ReturnType<typeof getGitContext>> | undefined;
|
||||
let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;
|
||||
let diffType: DiffType | undefined;
|
||||
let agentCwd: string | undefined;
|
||||
let initialBase: string | undefined;
|
||||
let worktreeCleanup: (() => void | Promise<void>) | undefined;
|
||||
let worktreePool: WorktreePool | undefined;
|
||||
let exitHandler: (() => void) | undefined;
|
||||
|
||||
if (isPRMode && urlArg) {
|
||||
// --- PR Review Mode ---
|
||||
const prRef = parsePRUrl(urlArg);
|
||||
if (!prRef) {
|
||||
throw new Error(
|
||||
`Invalid PR/MR URL: ${urlArg}\n` +
|
||||
"Supported formats:\n" +
|
||||
" GitHub: https://github.com/owner/repo/pull/123\n" +
|
||||
" GitLab: https://gitlab.com/group/project/-/merge_requests/42",
|
||||
);
|
||||
}
|
||||
|
||||
const cliName = getCliName(prRef);
|
||||
const cliUrl = getCliInstallUrl(prRef);
|
||||
|
||||
try {
|
||||
await checkPRAuth(prRef);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("not found") || msg.includes("ENOENT")) {
|
||||
throw new Error(`${cliName === "gh" ? "GitHub" : "GitLab"} CLI (${cliName}) is not installed. Install it from ${cliUrl}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.error(`Fetching ${getMRLabel(prRef)} ${getMRNumberLabel(prRef)} from ${getDisplayRepo(prRef)}...`);
|
||||
const pr = await fetchPR(prRef);
|
||||
rawPatch = pr.rawPatch;
|
||||
gitRef = `${getMRLabel(prRef)} ${getMRNumberLabel(prRef)}`;
|
||||
prMetadata = pr.metadata;
|
||||
|
||||
// Create local worktree for agent file access (--local is the default for PR reviews)
|
||||
let localPath: string | undefined;
|
||||
let sessionDir: string | undefined;
|
||||
try {
|
||||
const repoDir = options.cwd ?? ctx.cwd;
|
||||
const identifier = prMetadata.platform === "github"
|
||||
? `${prMetadata.owner}-${prMetadata.repo}-${prMetadata.number}`
|
||||
: `${prMetadata.projectPath.replace(/\//g, "-")}-${prMetadata.iid}`;
|
||||
const suffix = Math.random().toString(36).slice(2, 8);
|
||||
const prNumber = prMetadata.platform === "github" ? prMetadata.number : prMetadata.iid;
|
||||
sessionDir = join(realpathSync(tmpdir()), `plannotator-pr-${identifier}-${suffix}`);
|
||||
localPath = join(sessionDir, "pool", `pr-${prNumber}`);
|
||||
const fetchRefStr = prMetadata.platform === "github"
|
||||
? `refs/pull/${prMetadata.number}/head`
|
||||
: `refs/merge-requests/${prMetadata.iid}/head`;
|
||||
|
||||
// Validate inputs from platform API to prevent git flag/path injection
|
||||
if (prMetadata.baseBranch.includes('..') || prMetadata.baseBranch.startsWith('-')) throw new Error(`Invalid base branch: ${prMetadata.baseBranch}`);
|
||||
if (!/^[0-9a-f]{40,64}$/i.test(prMetadata.baseSha)) throw new Error(`Invalid base SHA: ${prMetadata.baseSha}`);
|
||||
|
||||
// Detect same-repo vs cross-repo (must match both owner/repo AND host)
|
||||
let isSameRepo = false;
|
||||
try {
|
||||
const remoteResult = await reviewRuntime.runGit(["remote", "get-url", "origin"], { cwd: repoDir });
|
||||
if (remoteResult.exitCode === 0) {
|
||||
const remoteUrl = remoteResult.stdout.trim();
|
||||
const currentRepo = parseRemoteUrl(remoteUrl);
|
||||
const prRepo = prMetadata.platform === "github"
|
||||
? `${prMetadata.owner}/${prMetadata.repo}`
|
||||
: prMetadata.projectPath;
|
||||
const repoMatches = !!currentRepo && currentRepo.toLowerCase() === prRepo.toLowerCase();
|
||||
const sshHost = remoteUrl.match(/^[^@]+@([^:]+):/)?.[1];
|
||||
const httpsHost = (() => { try { return new URL(remoteUrl).hostname; } catch { return null; } })();
|
||||
const remoteHost = (sshHost || httpsHost || "").toLowerCase();
|
||||
const prHost = prMetadata.host.toLowerCase();
|
||||
isSameRepo = repoMatches && remoteHost === prHost;
|
||||
}
|
||||
} catch { /* not in a git repo — cross-repo path */ }
|
||||
|
||||
if (isSameRepo) {
|
||||
// ── Same-repo: fast worktree path ──
|
||||
console.error("Fetching PR branch and creating local worktree...");
|
||||
await fetchRef(reviewRuntime, prMetadata.baseBranch, { cwd: repoDir });
|
||||
await ensureObjectAvailable(reviewRuntime, prMetadata.baseSha, { cwd: repoDir });
|
||||
await fetchRef(reviewRuntime, fetchRefStr, { cwd: repoDir });
|
||||
|
||||
await createWorktree(reviewRuntime, {
|
||||
ref: "FETCH_HEAD",
|
||||
path: localPath,
|
||||
detach: true,
|
||||
cwd: repoDir,
|
||||
});
|
||||
|
||||
const wtRepoDir = repoDir;
|
||||
exitHandler = () => {
|
||||
try {
|
||||
for (const entry of worktreePool?.entries() ?? []) {
|
||||
spawnSync("git", ["worktree", "remove", "--force", entry.path], { cwd: wtRepoDir });
|
||||
}
|
||||
} catch {}
|
||||
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
||||
};
|
||||
worktreeCleanup = async () => {
|
||||
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
||||
if (worktreePool) await worktreePool.cleanup(reviewRuntime);
|
||||
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
||||
};
|
||||
process.once("exit", exitHandler);
|
||||
} else {
|
||||
// ── Cross-repo: shallow clone + fetch PR head ──
|
||||
const prRepo = prMetadata.platform === "github"
|
||||
? `${prMetadata.owner}/${prMetadata.repo}`
|
||||
: prMetadata.projectPath;
|
||||
if (/^-/.test(prRepo)) throw new Error(`Invalid repository identifier: ${prRepo}`);
|
||||
const cli = prMetadata.platform === "github" ? "gh" : "glab";
|
||||
const host = prMetadata.host;
|
||||
// gh/glab repo clone doesn't accept --hostname; set GH_HOST/GITLAB_HOST env instead
|
||||
const isDefaultHost = host === "github.com" || host === "gitlab.com";
|
||||
const cloneEnv = isDefaultHost ? undefined : {
|
||||
...process.env,
|
||||
...(prMetadata.platform === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
|
||||
};
|
||||
|
||||
console.error(`Cloning ${prRepo} (shallow)...`);
|
||||
const cloneResult = spawnSync(cli, ["repo", "clone", prRepo, localPath, "--", "--depth=1", "--no-checkout"], { encoding: "utf-8", env: cloneEnv });
|
||||
if ((cloneResult.status ?? 1) !== 0) {
|
||||
throw new Error(`${cli} repo clone failed: ${(cloneResult.stderr ?? "").trim()}`);
|
||||
}
|
||||
|
||||
console.error("Fetching PR branch...");
|
||||
const fetchResult = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", fetchRefStr], { cwd: localPath });
|
||||
if (fetchResult.exitCode !== 0) throw new Error(`Failed to fetch PR head ref: ${fetchResult.stderr.trim()}`);
|
||||
|
||||
const checkoutResult = await reviewRuntime.runGit(["checkout", "FETCH_HEAD"], { cwd: localPath });
|
||||
if (checkoutResult.exitCode !== 0) {
|
||||
throw new Error(`git checkout FETCH_HEAD failed: ${checkoutResult.stderr.trim()}`);
|
||||
}
|
||||
|
||||
// Best-effort: create base refs so agent diffs work
|
||||
const baseFetch = await reviewRuntime.runGit(["fetch", "--depth=200", "origin", prMetadata.baseSha], { cwd: localPath });
|
||||
if (baseFetch.exitCode !== 0) console.error("Warning: failed to fetch baseSha, agent diffs may be inaccurate");
|
||||
await reviewRuntime.runGit(["branch", "--", prMetadata.baseBranch, prMetadata.baseSha], { cwd: localPath });
|
||||
await reviewRuntime.runGit(["update-ref", `refs/remotes/origin/${prMetadata.baseBranch}`, prMetadata.baseSha], { cwd: localPath });
|
||||
|
||||
exitHandler = () => {
|
||||
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
||||
};
|
||||
worktreeCleanup = () => {
|
||||
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
||||
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
||||
};
|
||||
process.once("exit", exitHandler);
|
||||
}
|
||||
|
||||
agentCwd = localPath;
|
||||
worktreePool = createWorktreePool(
|
||||
{ sessionDir: sessionDir!, repoDir, isSameRepo },
|
||||
{ path: localPath, prUrl: prMetadata.url, number: prNumber, ready: true },
|
||||
);
|
||||
console.error(`Local checkout ready at ${localPath}`);
|
||||
} catch (err) {
|
||||
console.error("Warning: local worktree creation failed, falling back to remote diff");
|
||||
console.error(err instanceof Error ? err.message : String(err));
|
||||
if (exitHandler) { process.removeListener("exit", exitHandler); exitHandler = undefined; }
|
||||
if (sessionDir) try { rmSync(sessionDir, { recursive: true, force: true }); } catch {}
|
||||
agentCwd = undefined;
|
||||
worktreePool = undefined;
|
||||
worktreeCleanup = undefined;
|
||||
}
|
||||
} else {
|
||||
// --- Local Review Mode ---
|
||||
const cwd = options.cwd ?? ctx.cwd;
|
||||
gitCtx = await getGitContext(cwd);
|
||||
const defaultBranch = options.defaultBranch ?? gitCtx.defaultBranch;
|
||||
const config = loadConfig();
|
||||
diffType = options.diffType ?? resolveDefaultDiffType(config);
|
||||
const result = await runGitDiff(diffType, defaultBranch, cwd, {
|
||||
hideWhitespace: config.diffOptions?.hideWhitespace ?? false,
|
||||
});
|
||||
rawPatch = result.patch;
|
||||
gitRef = result.label;
|
||||
diffError = result.error;
|
||||
// Remember which base the initial diff was computed against so it can
|
||||
// be forwarded to the server below. Only matters when the caller
|
||||
// overrode the detected default; otherwise it matches gitCtx already.
|
||||
initialBase = defaultBranch;
|
||||
}
|
||||
|
||||
const server = await startReviewServer({
|
||||
rawPatch,
|
||||
gitRef,
|
||||
error: diffError,
|
||||
origin: "pi",
|
||||
diffType,
|
||||
gitContext: gitCtx,
|
||||
initialBase,
|
||||
prMetadata,
|
||||
agentCwd,
|
||||
worktreePool,
|
||||
htmlContent: reviewHtmlContent,
|
||||
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
|
||||
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
|
||||
pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
|
||||
onCleanup: worktreeCleanup,
|
||||
});
|
||||
|
||||
return startBrowserDecisionSession(server, ctx, server.waitForDecision);
|
||||
}
|
||||
|
||||
export async function openMarkdownAnnotation(
|
||||
ctx: ExtensionContext,
|
||||
filePath: string,
|
||||
markdown: string,
|
||||
mode: AnnotateMode,
|
||||
folderPath?: string,
|
||||
sourceInfo?: string,
|
||||
sourceConverted?: boolean,
|
||||
gate?: boolean,
|
||||
): Promise<{ feedback: string; exit?: boolean; approved?: boolean }> {
|
||||
const session = await startMarkdownAnnotationSession(
|
||||
ctx,
|
||||
filePath,
|
||||
markdown,
|
||||
mode,
|
||||
folderPath,
|
||||
sourceInfo,
|
||||
sourceConverted,
|
||||
gate,
|
||||
);
|
||||
return session.waitForDecision();
|
||||
}
|
||||
|
||||
export async function startMarkdownAnnotationSession(
|
||||
ctx: ExtensionContext,
|
||||
filePath: string,
|
||||
markdown: string,
|
||||
mode: AnnotateMode,
|
||||
folderPath?: string,
|
||||
sourceInfo?: string,
|
||||
sourceConverted?: boolean,
|
||||
gate?: boolean,
|
||||
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean }>> {
|
||||
if (!ctx.hasUI || !planHtmlContent) {
|
||||
throw new Error("Plannotator annotation browser is unavailable in this session.");
|
||||
}
|
||||
|
||||
let resolvedMarkdown = markdown;
|
||||
if (!resolvedMarkdown.trim() && existsSync(filePath)) {
|
||||
try {
|
||||
const fileStat = statSync(filePath);
|
||||
if (!fileStat.isDirectory()) {
|
||||
resolvedMarkdown = readFileSync(filePath, "utf-8");
|
||||
}
|
||||
} catch {
|
||||
// fall back to provided markdown
|
||||
}
|
||||
}
|
||||
|
||||
const server = await startAnnotateServer({
|
||||
markdown: resolvedMarkdown,
|
||||
filePath,
|
||||
origin: "pi",
|
||||
mode,
|
||||
folderPath,
|
||||
sourceInfo,
|
||||
sourceConverted,
|
||||
gate,
|
||||
htmlContent: planHtmlContent,
|
||||
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
|
||||
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
|
||||
pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
|
||||
});
|
||||
|
||||
return startBrowserDecisionSession(server, ctx, server.waitForDecision);
|
||||
}
|
||||
|
||||
export async function openLastMessageAnnotation(
|
||||
ctx: ExtensionContext,
|
||||
lastText: string,
|
||||
gate?: boolean,
|
||||
): Promise<{ feedback: string; exit?: boolean; approved?: boolean }> {
|
||||
return openMarkdownAnnotation(ctx, "last-message", lastText, "annotate-last", undefined, undefined, undefined, gate);
|
||||
}
|
||||
|
||||
export async function startLastMessageAnnotationSession(
|
||||
ctx: ExtensionContext,
|
||||
lastText: string,
|
||||
gate?: boolean,
|
||||
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean }>> {
|
||||
return startMarkdownAnnotationSession(
|
||||
ctx,
|
||||
"last-message",
|
||||
lastText,
|
||||
"annotate-last",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
gate,
|
||||
);
|
||||
}
|
||||
|
||||
export async function openArchiveBrowserAction(
|
||||
ctx: ExtensionContext,
|
||||
customPlanPath?: string,
|
||||
): Promise<{ opened: boolean }> {
|
||||
if (!ctx.hasUI || !planHtmlContent) {
|
||||
throw new Error("Plannotator archive browser is unavailable in this session.");
|
||||
}
|
||||
|
||||
const server = await startPlanReviewServer({
|
||||
plan: "",
|
||||
htmlContent: planHtmlContent,
|
||||
origin: "pi",
|
||||
mode: "archive",
|
||||
customPlanPath,
|
||||
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
|
||||
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
|
||||
pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
|
||||
});
|
||||
|
||||
return openBrowserAndWait(server, ctx, async () => {
|
||||
if (server.waitForDone) {
|
||||
await server.waitForDone();
|
||||
}
|
||||
return { opened: true };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user