Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/opencode-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Restart OpenCode. The `submit_plan` tool is now available.
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |
| `PLANNOTATOR_PASTE_URL` | Custom paste service URL for self-hosting. Default: `https://plannotator-paste.plannotator.workers.dev`. |
| `PLANNOTATOR_PLAN_TIMEOUT_SECONDS` | Timeout for `submit_plan` review wait. Default: `345600` (96h). Set `0` to disable timeout. |

## Devcontainer / Docker
Expand Down
10 changes: 7 additions & 3 deletions apps/opencode-plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface CommandDeps {
reviewHtmlContent: string;
getSharingEnabled: () => Promise<boolean>;
getShareBaseUrl: () => string | undefined;
getPasteApiUrl: () => string | undefined;
directory?: string;
}

Expand Down Expand Up @@ -147,7 +148,7 @@ export async function handleAnnotateCommand(
event: any,
deps: CommandDeps
) {
const { client, htmlContent, getSharingEnabled, getShareBaseUrl } = deps;
const { client, htmlContent, getSharingEnabled, getShareBaseUrl, getPasteApiUrl } = deps;

// @ts-ignore - Event properties contain arguments
const filePath = event.properties?.arguments || event.arguments || "";
Expand Down Expand Up @@ -228,6 +229,7 @@ export async function handleAnnotateCommand(
sourceInfo,
sharingEnabled: await getSharingEnabled(),
shareBaseUrl: getShareBaseUrl(),
pasteApiUrl: getPasteApiUrl(),
htmlContent,
onReady: handleAnnotateServerReady,
});
Expand Down Expand Up @@ -271,7 +273,7 @@ export async function handleAnnotateLastCommand(
event: any,
deps: CommandDeps
): Promise<string | null> {
const { client, htmlContent, getSharingEnabled, getShareBaseUrl } = deps;
const { client, htmlContent, getSharingEnabled, getShareBaseUrl, getPasteApiUrl } = deps;

// @ts-ignore - Event properties contain sessionID
const sessionId = event.properties?.sessionID;
Expand Down Expand Up @@ -317,6 +319,7 @@ export async function handleAnnotateLastCommand(
mode: "annotate-last",
sharingEnabled: await getSharingEnabled(),
shareBaseUrl: getShareBaseUrl(),
pasteApiUrl: getPasteApiUrl(),
htmlContent,
onReady: handleAnnotateServerReady,
});
Expand All @@ -336,7 +339,7 @@ export async function handleArchiveCommand(
event: any,
deps: CommandDeps
) {
const { client, htmlContent, getSharingEnabled, getShareBaseUrl } = deps;
const { client, htmlContent, getSharingEnabled, getShareBaseUrl, getPasteApiUrl } = deps;

client.app.log({ level: "info", message: "Opening plan archive..." });

Expand All @@ -346,6 +349,7 @@ export async function handleArchiveCommand(
mode: "archive",
sharingEnabled: await getSharingEnabled(),
shareBaseUrl: getShareBaseUrl(),
pasteApiUrl: getPasteApiUrl(),
htmlContent,
onReady: handleServerReady,
});
Expand Down
7 changes: 7 additions & 0 deletions apps/opencode-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ export const PlannotatorPlugin: Plugin = async (ctx) => {
return process.env.PLANNOTATOR_SHARE_URL || undefined;
}

function getPasteApiUrl(): string | undefined {
return process.env.PLANNOTATOR_PASTE_URL || undefined;
}

function getPlanTimeoutSeconds(): number | null {
const raw = process.env.PLANNOTATOR_PLAN_TIMEOUT_SECONDS?.trim();
if (!raw) return DEFAULT_PLAN_TIMEOUT_SECONDS;
Expand Down Expand Up @@ -363,6 +367,7 @@ Do NOT proceed with implementation until your plan is approved.`);
reviewHtmlContent: getReviewHtml(),
getSharingEnabled,
getShareBaseUrl,
getPasteApiUrl,
directory: ctx.directory,
};

Expand Down Expand Up @@ -405,6 +410,7 @@ Do NOT proceed with implementation until your plan is approved.`);
reviewHtmlContent: getReviewHtml(),
getSharingEnabled,
getShareBaseUrl,
getPasteApiUrl,
directory: ctx.directory,
};

Expand Down Expand Up @@ -448,6 +454,7 @@ Do NOT proceed with implementation until your plan is approved.`);
origin: "opencode",
sharingEnabled,
shareBaseUrl: getShareBaseUrl(),
pasteApiUrl: getPasteApiUrl(),
htmlContent: getPlanHtml(),
opencodeClient: ctx.client,
onReady: async (url, isRemote, port) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/paste-service/core/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const BASE_CORS_HEADERS = {
"Access-Control-Max-Age": "86400",
};

// Defaults target the hosted plannotator.ai deployment.
// Self-hosters should set PASTE_ALLOWED_ORIGINS (Bun) or ALLOWED_ORIGINS (Cloudflare)
// to their own portal origin so requests from the hosted share.plannotator.ai
// portal are not granted CORS access against their service.
export function getAllowedOrigins(envValue?: string): string[] {
if (envValue) {
return envValue.split(",").map((o) => o.trim());
Comment on lines +7 to 13
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (blocking): This needs documentation

Expand Down
3 changes: 3 additions & 0 deletions apps/paste-service/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ id = "9bc2647f6f5244499c26c90d87a743a0"
preview_id = "6efae5ac33c4443ba8f0a0b83a2eb111"

[vars]
# Default values target the hosted plannotator.ai deployment.
# Self-hosters must override ALLOWED_ORIGINS to point at their own portal,
# either by editing this file or setting it via `wrangler secret put`.
ALLOWED_ORIGINS = "https://share.plannotator.ai,http://localhost:3001"
Comment on lines +12 to 15
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (blocking): This needs documentation

1 change: 1 addition & 0 deletions apps/pi-extension/plannotator-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ export async function openCodeReview(
htmlContent: reviewHtmlContent,
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,
pasteApiUrl: process.env.PLANNOTATOR_PASTE_URL || undefined,
onCleanup: worktreeCleanup,
});

Expand Down
4 changes: 4 additions & 0 deletions apps/pi-extension/server/serverReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export async function startReviewServer(options: {
error?: string;
sharingEnabled?: boolean;
shareBaseUrl?: string;
pasteApiUrl?: string;
prMetadata?: PRMetadata;
/** Working directory for agent processes (e.g., --local worktree). Independent of diff pipeline. */
agentCwd?: string;
Expand Down Expand Up @@ -281,6 +282,8 @@ export async function startReviewServer(options: {
options.sharingEnabled ?? process.env.PLANNOTATOR_SHARE !== "disabled";
const shareBaseUrl =
(options.shareBaseUrl ?? process.env.PLANNOTATOR_SHARE_URL) || undefined;
const pasteApiUrl =
(options.pasteApiUrl ?? process.env.PLANNOTATOR_PASTE_URL) || undefined;
let resolveDecision!: (result: {
approved: boolean;
feedback: string;
Expand Down Expand Up @@ -421,6 +424,7 @@ export async function startReviewServer(options: {
gitContext: hasLocalAccess ? options.gitContext : undefined,
sharingEnabled,
shareBaseUrl,
pasteApiUrl,
repoInfo,
isWSL: wslFlag,
...(options.agentCwd && { agentCwd: options.agentCwd }),
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/components/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { ModeToggle } from "./ModeToggle";

interface LandingProps {
onEnter?: () => void;
shareBaseUrl?: string;
}

export const Landing: React.FC<LandingProps> = ({ onEnter }) => {
export const Landing: React.FC<LandingProps> = ({ onEnter, shareBaseUrl }) => {
const demoUrl = shareBaseUrl || "https://share.plannotator.ai";
return (
<div className="min-h-screen bg-background text-foreground">
{/* Nav */}
Expand Down Expand Up @@ -112,7 +114,7 @@ export const Landing: React.FC<LandingProps> = ({ onEnter }) => {
</button>
) : (
<a
href="https://share.plannotator.ai"
href={demoUrl}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:opacity-90 transition-opacity"
>
Open Demo
Expand Down
20 changes: 14 additions & 6 deletions packages/ui/hooks/useSharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,15 @@ export function useSharing(
if (pathMatch) {
const pasteId = pathMatch[1];

// Extract encryption key from URL fragment: #key=<base64url>
// Extract key and optional paste origin from fragment: #key=<k>&paste=<base64url>
const fragment = window.location.hash.slice(1);
const encryptionKey = fragment.startsWith('key=') ? fragment.slice(4) : undefined;
const params = new URLSearchParams(fragment);
const encryptionKey = params.get('key') ?? undefined;
const pasteFromFragment = params.get('paste')
? atob(params.get('paste')!.replace(/-/g, '+').replace(/_/g, '/'))
: undefined;

const payload = await loadFromPasteId(pasteId, pasteApiUrl, encryptionKey);
const payload = await loadFromPasteId(pasteId, pasteFromFragment ?? pasteApiUrl, encryptionKey);
if (payload) {
setMarkdown(payload.p);

Expand Down Expand Up @@ -279,11 +283,15 @@ export function useSharing(
let payload: SharePayload | undefined;

// Check for short URL pattern: /p/<id> with optional #key=<key> fragment
const shortMatch = url.match(/\/p\/([A-Za-z0-9]{6,16})(?:#key=([A-Za-z0-9_-]+))?(?:\?|#|$)/);
const shortMatch = url.match(/\/p\/([A-Za-z0-9]{6,16})(?:#(.*))?(?:\?|$)/);
if (shortMatch) {
const pasteId = shortMatch[1];
const encryptionKey = shortMatch[2]; // undefined if no key fragment
const loaded = await loadFromPasteId(pasteId, pasteApiUrl, encryptionKey);
const fragParams = new URLSearchParams(shortMatch[2] ?? '');
const encryptionKey = fragParams.get('key') ?? undefined;
const pasteFromFragment = fragParams.get('paste')
? atob(fragParams.get('paste')!.replace(/-/g, '+').replace(/_/g, '/'))
: undefined;
const loaded = await loadFromPasteId(pasteId, pasteFromFragment ?? pasteApiUrl, encryptionKey);
if (!loaded) {
return { success: false, count: 0, planTitle: '', error: 'Failed to load from short URL — paste may have expired' };
}
Expand Down
8 changes: 6 additions & 2 deletions packages/ui/utils/sharing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,12 @@ export async function createShortShareUrl(
}

const result = (await response.json()) as { id: string };
// Key in fragment — never sent to server per HTTP spec
const shortUrl = `${shareBase}/p/${result.id}#key=${key}`;
// Embed paste origin in fragment when non-default so the share portal can
// fetch from the right service without a server.
const pasteParam = pasteApi !== DEFAULT_PASTE_API
? `&paste=${btoa(pasteApi).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')}`
: '';
const shortUrl = `${shareBase}/p/${result.id}#key=${key}${pasteParam}`;

return { shortUrl, id: result.id };
} catch (e) {
Expand Down