From 81f29dfe6ecedd94ce170399ebb07afb74360500 Mon Sep 17 00:00:00 2001 From: Alexandre Silvestri Date: Wed, 25 Mar 2026 15:06:29 -0300 Subject: [PATCH] unix update command for wsl users on UpdateBanner (#395) --- packages/editor/App.tsx | 8 ++++++-- packages/review-editor/App.tsx | 5 ++++- packages/server/annotate.ts | 3 +++ packages/server/browser.ts | 2 +- packages/server/index.ts | 5 ++++- packages/server/review.ts | 3 +++ packages/ui/components/UpdateBanner.tsx | 9 +++++---- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 28cceb6c..0a9424d9 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -85,6 +85,7 @@ const App: React.FC = () => { const [uiPrefs, setUiPrefs] = useState(() => getUIPreferences()); const [isApiMode, setIsApiMode] = useState(false); const [origin, setOrigin] = useState<'claude-code' | 'opencode' | 'pi' | 'codex' | null>(null); + const [isWSL, setIsWSL] = useState(false); const [globalAttachments, setGlobalAttachments] = useState([]); const [annotateMode, setAnnotateMode] = useState(false); const [annotateSource, setAnnotateSource] = useState<'file' | 'message' | 'folder' | null>(null); @@ -385,7 +386,7 @@ const App: React.FC = () => { if (!res.ok) throw new Error('Not in API mode'); return res.json(); }) - .then((data: { plan: string; origin?: 'claude-code' | 'opencode' | 'pi' | 'codex'; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive'; filePath?: string; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string }) => { + .then((data: { plan: string; origin?: 'claude-code' | 'opencode' | 'pi' | 'codex'; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive'; filePath?: string; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string; isWSL?: boolean }) => { if (data.mode === 'archive') { // Archive mode: show first archived plan or clear demo content setMarkdown(data.plan || ''); @@ -443,6 +444,9 @@ const App: React.FC = () => { // Load saved permission mode preference setPermissionMode(getPermissionModeSettings().mode); } + if (data.isWSL) { + setIsWSL(true); + } }) .catch(() => { // Not in API mode - use default content @@ -1591,7 +1595,7 @@ const App: React.FC = () => { /> {/* Update notification */} - + {/* Image Annotator for pasted images */} { const [viewedFiles, setViewedFiles] = useState>(new Set()); const [hideViewedFiles, setHideViewedFiles] = useState(false); const [origin, setOrigin] = useState<'opencode' | 'claude-code' | 'pi' | null>(null); + const [isWSL, setIsWSL] = useState(false); const [diffType, setDiffType] = useState('uncommitted'); const [gitContext, setGitContext] = useState(null); const [isLoadingDiff, setIsLoadingDiff] = useState(false); @@ -387,6 +388,7 @@ const ReviewApp: React.FC = () => { prMetadata?: PRMetadata; platformUser?: string; error?: string; + isWSL?: boolean; }) => { const apiFiles = parseDiffToFiles(data.rawPatch); setDiffData({ @@ -407,6 +409,7 @@ const ReviewApp: React.FC = () => { if (data.prMetadata) setPrMetadata(data.prMetadata); if (data.platformUser) setPlatformUser(data.platformUser); if (data.error) setDiffError(data.error); + if (data.isWSL) setIsWSL(true); }) .catch(() => { // Not in API mode - use demo content @@ -1543,7 +1546,7 @@ const ReviewApp: React.FC = () => { /> {/* Update notification */} - + {/* GitHub general comment dialog */} {platformCommentDialog && ( diff --git a/packages/server/annotate.ts b/packages/server/annotate.ts index 12426028..d2319501 100644 --- a/packages/server/annotate.ts +++ b/packages/server/annotate.ts @@ -17,6 +17,7 @@ import { handleImage, handleUpload, handleServerReady, handleDraftSave, handleDr import { handleDoc, handleFileBrowserFiles } from "./reference-handlers"; import { contentHash, deleteDraft } from "./draft"; import { dirname } from "path"; +import { isWSL } from "./browser"; // Re-export utilities export { isRemoteSession, getServerPort } from "./remote"; @@ -95,6 +96,7 @@ export async function startAnnotateServer( const isRemote = isRemoteSession(); const configuredPort = getServerPort(); + const wslFlag = await isWSL(); const draftKey = contentHash(markdown); // Detect repo info (cached for this session) @@ -135,6 +137,7 @@ export async function startAnnotateServer( pasteApiUrl, repoInfo, projectRoot: folderPath || process.cwd(), + isWSL: wslFlag, }); } diff --git a/packages/server/browser.ts b/packages/server/browser.ts index 4e9a0c8a..65b8685c 100644 --- a/packages/server/browser.ts +++ b/packages/server/browser.ts @@ -41,7 +41,7 @@ async function tryVscodeIpc(url: string): Promise { /** * Check if running in WSL (Windows Subsystem for Linux) */ -async function isWSL(): Promise { +export async function isWSL(): Promise { if (process.platform !== "linux") { return false; } diff --git a/packages/server/index.ts b/packages/server/index.ts index 909576d0..fe424131 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -41,6 +41,7 @@ import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraft import { contentHash, deleteDraft } from "./draft"; import { handleDoc, handleObsidianVaults, handleObsidianFiles, handleObsidianDoc, handleFileBrowserFiles } from "./reference-handlers"; import { createEditorAnnotationHandler } from "./editor-annotations"; +import { isWSL } from "./browser"; // Re-export utilities export { isRemoteSession, getServerPort } from "./remote"; @@ -119,6 +120,7 @@ export async function startPlannotatorServer( const isRemote = isRemoteSession(); const configuredPort = getServerPort(); + const wslFlag = await isWSL(); // --- Archive mode setup --- let archivePlans: ArchivedPlan[] = []; @@ -262,9 +264,10 @@ export async function startPlannotatorServer( archivePlans, sharingEnabled, shareBaseUrl, + isWSL: wslFlag, }); } - return Response.json({ plan, origin, permissionMode, sharingEnabled, shareBaseUrl, pasteApiUrl, repoInfo, previousPlan, versionInfo, projectRoot: process.cwd() }); + return Response.json({ plan, origin, permissionMode, sharingEnabled, shareBaseUrl, pasteApiUrl, repoInfo, previousPlan, versionInfo, projectRoot: process.cwd(), isWSL: wslFlag }); } // API: Serve a linked markdown document diff --git a/packages/server/review.ts b/packages/server/review.ts index 1a1c40c9..7ba6e6a6 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -17,6 +17,7 @@ import { contentHash, deleteDraft } from "./draft"; import { createEditorAnnotationHandler } from "./editor-annotations"; import { type PRMetadata, type PRReviewFileComment, fetchPRFileContent, fetchPRContext, submitPRReview, getPRUser, prRefFromMetadata, getDisplayRepo, getMRLabel, getMRNumberLabel } from "./pr"; import { createAIEndpoints, ProviderRegistry, SessionManager, createProvider, type AIEndpoints, type PiSDKConfig } from "@plannotator/ai"; +import { isWSL } from "./browser"; // Re-export utilities export { isRemoteSession, getServerPort } from "./remote"; @@ -189,6 +190,7 @@ export async function startReviewServer( const isRemote = isRemoteSession(); const configuredPort = getServerPort(); + const wslFlag = await isWSL(); // Detect repo info (cached for this session) // In PR mode, derive from metadata instead of local git @@ -238,6 +240,7 @@ export async function startReviewServer( sharingEnabled, shareBaseUrl, repoInfo, + isWSL: wslFlag, ...(isPRMode && { prMetadata, platformUser }), ...(currentError && { error: currentError }), }); diff --git a/packages/ui/components/UpdateBanner.tsx b/packages/ui/components/UpdateBanner.tsx index e100d438..a46878a8 100644 --- a/packages/ui/components/UpdateBanner.tsx +++ b/packages/ui/components/UpdateBanner.tsx @@ -4,17 +4,18 @@ import { isWindows } from '../utils/platform'; const PI_INSTALL_COMMAND = 'pi install npm:@plannotator/pi-extension'; -function getInstallCommand(): string { - return isWindows +function getInstallCommand(isWSL: boolean): string { + return isWindows && !isWSL ? 'powershell -c "irm https://plannotator.ai/install.ps1 | iex"' : 'curl -fsSL https://plannotator.ai/install.sh | bash'; } interface UpdateBannerProps { origin?: 'claude-code' | 'opencode' | 'pi' | 'codex' | null; + isWSL?: boolean; } -export const UpdateBanner: React.FC = ({ origin }) => { +export const UpdateBanner: React.FC = ({ origin, isWSL = false }) => { const updateInfo = useUpdateCheck(); const [copied, setCopied] = useState(false); const [dismissed, setDismissed] = useState(false); @@ -25,7 +26,7 @@ export const UpdateBanner: React.FC = ({ origin }) => { const effectiveOrigin = previewOrigin || origin; const isPi = effectiveOrigin === 'pi'; const isOpenCode = effectiveOrigin === 'opencode'; - const installCommand = isPi ? PI_INSTALL_COMMAND : getInstallCommand(); + const installCommand = isPi ? PI_INSTALL_COMMAND : getInstallCommand(isWSL); if (!updateInfo?.updateAvailable || dismissed) return null;