From bdbf70d01f2420fc3adc9a114fd1d64870c80ea2 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:00:47 -0500 Subject: [PATCH 1/9] refactor(webviews): move SystemMessageStep to shared components The component is consumed outside StartupFlow already (NewSourceUploader), and an upcoming SystemMessageReview webview will reuse it too. Move it to the shared components directory so its location reflects its scope. No behavior change. --- .../codex-webviews/src/NewSourceUploader/NewSourceUploader.tsx | 2 +- .../src/{StartupFlow => }/components/SystemMessageStep.tsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename webviews/codex-webviews/src/{StartupFlow => }/components/SystemMessageStep.tsx (100%) diff --git a/webviews/codex-webviews/src/NewSourceUploader/NewSourceUploader.tsx b/webviews/codex-webviews/src/NewSourceUploader/NewSourceUploader.tsx index a4759e3ef..ff7abf9ad 100644 --- a/webviews/codex-webviews/src/NewSourceUploader/NewSourceUploader.tsx +++ b/webviews/codex-webviews/src/NewSourceUploader/NewSourceUploader.tsx @@ -31,7 +31,7 @@ import { SourceFileSelection } from "./components/SourceFileSelection"; import { EmptySourceState } from "./components/EmptySourceState"; import { PluginSelection } from "./components/PluginSelection"; import { ImportProgressView } from "./components/ImportProgressView"; -import { SystemMessageStep } from "../StartupFlow/components/SystemMessageStep"; +import { SystemMessageStep } from "../components/SystemMessageStep"; import { deriveTargetPathFromSource } from "../../../../sharedUtils"; import { createDownloadHelper } from "./utils/downloadHelper"; import { notifyImportEnded } from "./utils/importProgress"; diff --git a/webviews/codex-webviews/src/StartupFlow/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx similarity index 100% rename from webviews/codex-webviews/src/StartupFlow/components/SystemMessageStep.tsx rename to webviews/codex-webviews/src/components/SystemMessageStep.tsx From 50de272988671658d4dff666e9dd5ebba737c319 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:02:04 -0500 Subject: [PATCH 2/9] feat(webviews): add optional banner/dismiss/save-label props to SystemMessageStep These let consumers reuse the component in non-walkthrough contexts (e.g. the upcoming system-message review prompt after a language change), where a contextual banner and an explicit "I don't need to change this" dismiss button are needed. Defaults preserve current behavior in NewSourceUploader. --- .../src/components/SystemMessageStep.tsx | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/webviews/codex-webviews/src/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx index 3dafad14e..91b63b9bb 100644 --- a/webviews/codex-webviews/src/components/SystemMessageStep.tsx +++ b/webviews/codex-webviews/src/components/SystemMessageStep.tsx @@ -8,6 +8,13 @@ export interface SystemMessageStepProps { onContinue: () => void; onSkip?: () => void; isWaitingForMessage?: boolean; + /** Optional banner shown above the heading (e.g. to explain why this view appeared). */ + headerBanner?: React.ReactNode; + /** When provided, renders an additional button (e.g. "I don't need to change this") that calls onDismiss. */ + dismissLabel?: string; + onDismiss?: () => void; + /** Override label shown on the primary save button. Defaults to "Save and Start Translating". */ + saveLabel?: string; } /** @@ -20,6 +27,10 @@ export const SystemMessageStep: React.FC = ({ onContinue, onSkip, isWaitingForMessage = false, + headerBanner, + dismissLabel, + onDismiss, + saveLabel, }) => { const [systemMessage, setSystemMessage] = useState(initialMessage); const [isGenerating, setIsGenerating] = useState(false); @@ -126,6 +137,9 @@ export const SystemMessageStep: React.FC = ({ gap: "12px", }} > + {headerBanner && ( +
{headerBanner}
+ )}
= ({ Skip for Now )} */} + {dismissLabel && onDismiss && ( + + {dismissLabel} + + )} = ({ ) : systemMessage.trim() ? ( <> - Save and Start Translating + {saveLabel ?? "Save and Start Translating"} ) : ( <> - Start Translating + {saveLabel ?? "Start Translating"} )} From 239b316a5888af2043ec62203d2889bc14f3c160 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:03:33 -0500 Subject: [PATCH 3/9] feat(webviews): add SystemMessageReview webview app A standalone React app that hosts the shared SystemMessageStep with a language-change warning banner and an "I don't need to change this" dismiss button. Will be opened after a project source/target language change to prompt the user to review their AI translation instructions. Wired into the build pipeline alongside the other webview apps. Not yet opened from anywhere; provider wiring follows in subsequent commits. --- webviews/codex-webviews/package.json | 4 +- .../src/SystemMessageReview/index.tsx | 127 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 webviews/codex-webviews/src/SystemMessageReview/index.tsx diff --git a/webviews/codex-webviews/package.json b/webviews/codex-webviews/package.json index cc6795eca..702c7a08c 100644 --- a/webviews/codex-webviews/package.json +++ b/webviews/codex-webviews/package.json @@ -22,7 +22,8 @@ "build:CopilotSettings": "cross-env APP_NAME=CopilotSettings vite build", "build:InterfaceSettings": "cross-env APP_NAME=InterfaceSettings vite build", "build:MissingToolsWarning": "cross-env APP_NAME=MissingToolsWarning vite build", - "build:all": "pnpm run build:StartupFlow && pnpm run build:ParallelView && pnpm run build:PublishProject && pnpm run build:CodexCellEditor && pnpm run build:CommentsView && pnpm run build:NavigationView && pnpm run build:MainMenu && pnpm run build:SplashScreen && pnpm run build:CellLabelImporterView && pnpm run build:CodexMigrationToolView && pnpm run build:NewSourceUploader && pnpm run build:CopilotSettings && pnpm run build:InterfaceSettings && pnpm run build:MissingToolsWarning", + "build:SystemMessageReview": "cross-env APP_NAME=SystemMessageReview vite build", + "build:all": "pnpm run build:StartupFlow && pnpm run build:ParallelView && pnpm run build:PublishProject && pnpm run build:CodexCellEditor && pnpm run build:CommentsView && pnpm run build:NavigationView && pnpm run build:MainMenu && pnpm run build:SplashScreen && pnpm run build:CellLabelImporterView && pnpm run build:CodexMigrationToolView && pnpm run build:NewSourceUploader && pnpm run build:CopilotSettings && pnpm run build:InterfaceSettings && pnpm run build:MissingToolsWarning && pnpm run build:SystemMessageReview", "type-check": "tsc --noEmit", "watch:all": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:all", "watch:CodexCellEditor": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:CodexCellEditor", @@ -38,6 +39,7 @@ "watch:NewSourceUploader": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:NewSourceUploader", "watch:CopilotSettings": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:CopilotSettings", "watch:InterfaceSettings": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:InterfaceSettings", + "watch:SystemMessageReview": "nodemon --watch src --ext ts,tsx,css --exec pnpm run build:SystemMessageReview", "smart-watch": "node smart-watch.cjs", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/webviews/codex-webviews/src/SystemMessageReview/index.tsx b/webviews/codex-webviews/src/SystemMessageReview/index.tsx new file mode 100644 index 000000000..3a2ef3743 --- /dev/null +++ b/webviews/codex-webviews/src/SystemMessageReview/index.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from "react"; +import ReactDOM from "react-dom/client"; +import { SystemMessageStep } from "../components/SystemMessageStep"; + +function getVSCodeAPI() { + const w = window as any; + if (w.__vscodeApi) return w.__vscodeApi as any; + const api = (window as any).acquireVsCodeApi(); + w.__vscodeApi = api; + return api; +} + +interface ProjectLanguage { + tag?: string; + refName?: string; + projectStatus?: string; +} + +interface InitData { + systemMessage: string; + sourceLanguage?: ProjectLanguage | null; + targetLanguage?: ProjectLanguage | null; + reason: "sourceLanguageChanged" | "targetLanguageChanged" | "both"; +} + +const SystemMessageReviewApp: React.FC = () => { + const vscode = getVSCodeAPI(); + const [initData, setInitData] = useState(null); + + useEffect(() => { + const handler = (event: MessageEvent) => { + const message = event.data; + if (message.command === "init") { + setInitData(message.data as InitData); + } + }; + window.addEventListener("message", handler); + vscode.postMessage({ command: "webviewReady" }); + return () => window.removeEventListener("message", handler); + }, [vscode]); + + const handleContinue = () => { + // The backend closes the panel once the save succeeds, so this is a no-op. + }; + + const handleDismiss = () => { + vscode.postMessage({ command: "systemMessage.dismiss" }); + }; + + if (!initData) { + return ( +
+ Loading... +
+ ); + } + + const sourceName = initData.sourceLanguage?.refName ?? "your source language"; + const targetName = initData.targetLanguage?.refName ?? "your target language"; + + let bannerText: string; + if (initData.reason === "sourceLanguageChanged") { + bannerText = `You changed your project's source language to ${sourceName}. Please review your AI translation instructions so they match the new ${sourceName} → ${targetName} direction.`; + } else if (initData.reason === "targetLanguageChanged") { + bannerText = `You changed your project's target language to ${targetName}. Please review your AI translation instructions so they match the new ${sourceName} → ${targetName} direction.`; + } else { + bannerText = `You changed your project's languages. Please review your AI translation instructions so they match the new ${sourceName} → ${targetName} direction.`; + } + + const banner = ( +
+ + {bannerText} +
+ ); + + return ( +
+ +
+ ); +}; + +const root = ReactDOM.createRoot(document.getElementById("root")!); +root.render(); From 00f9af4e1bb12525a9eb959cbd2a9367c938f42d Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:07:42 -0500 Subject: [PATCH 4/9] feat(extension): add system-message review panel + command Adds openSystemMessageReview(reason) and registers codex-editor-extension.promptSystemMessageReview as a thin wrapper. The panel hosts the SystemMessageReview webview and handles the generate / save / dismiss flow against MetadataManager. If the panel is closed without the user addressing the prompt (save, regenerate, or "I don't need to change this"), the AI translation instructions are regenerated automatically so they don't stay stale relative to the new language. Concurrent triggers reuse a single panel so back-to-back source+target changes don't spawn duplicate windows. Not yet triggered from anywhere; wiring follows in the next commit. --- src/copilotSettings/systemMessageReview.ts | 323 +++++++++++++++++++++ src/extension.ts | 11 +- types/index.d.ts | 3 +- 3 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 src/copilotSettings/systemMessageReview.ts diff --git a/src/copilotSettings/systemMessageReview.ts b/src/copilotSettings/systemMessageReview.ts new file mode 100644 index 000000000..2984a5def --- /dev/null +++ b/src/copilotSettings/systemMessageReview.ts @@ -0,0 +1,323 @@ +import * as vscode from "vscode"; +import { MetadataManager } from "../utils/metadataManager"; +import { generateChatSystemMessage } from "./copilotSettings"; + +type ReviewReason = "sourceLanguageChanged" | "targetLanguageChanged" | "both"; + +interface ProjectLanguage { + tag?: string; + refName?: string; + projectStatus?: string; +} + +// Track a single open panel so repeated language changes (e.g. user picks +// source then target back-to-back) don't spawn duplicate review windows. +let currentPanel: vscode.WebviewPanel | undefined; +let currentReason: ReviewReason | undefined; + +function escapeRefName(name: string | undefined): string { + return name ?? "your project"; +} + +async function readLanguagesFromMetadata( + workspaceFolder: vscode.Uri +): Promise<{ source?: ProjectLanguage; target?: ProjectLanguage; }> { + try { + const metadataUri = vscode.Uri.joinPath(workspaceFolder, "metadata.json"); + const content = await vscode.workspace.fs.readFile(metadataUri); + const metadata = JSON.parse(content.toString()); + const source = metadata.languages?.find( + (l: any) => l?.projectStatus === "source" + ) as ProjectLanguage | undefined; + const target = metadata.languages?.find( + (l: any) => l?.projectStatus === "target" + ) as ProjectLanguage | undefined; + return { source, target }; + } catch { + return {}; + } +} + +async function autoRegenerateAfterClose( + workspaceFolder: vscode.Uri, + reason: ReviewReason +): Promise { + const { source, target } = await readLanguagesFromMetadata(workspaceFolder); + if (!source?.refName || !target?.refName) { + vscode.window.showWarningMessage( + "You changed your project's language. Please review your AI translation instructions to make sure they match." + ); + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Regenerating AI translation instructions", + cancellable: false, + }, + async () => { + const generated = await generateChatSystemMessage( + { refName: source.refName! }, + { refName: target.refName! }, + workspaceFolder + ); + if (!generated) { + vscode.window.showWarningMessage( + `You changed your project's language to ${escapeRefName( + reason === "sourceLanguageChanged" ? source.refName : target.refName + )}, but the AI translation instructions could not be regenerated automatically. Please update them in Copilot Settings.` + ); + return; + } + const saveResult = await MetadataManager.setChatSystemMessage( + generated, + workspaceFolder + ); + if (saveResult.success) { + vscode.window.showInformationMessage( + "Your AI translation instructions were regenerated to match the new language." + ); + } else { + vscode.window.showWarningMessage( + `Generated new AI translation instructions but could not save them: ${saveResult.error}` + ); + } + } + ); +} + +function buildWebviewHtml(panel: vscode.WebviewPanel, extensionUri: vscode.Uri): string { + const scriptUri = panel.webview.asWebviewUri( + vscode.Uri.joinPath( + extensionUri, + "webviews", + "codex-webviews", + "dist", + "SystemMessageReview", + "index.js" + ) + ); + const codiconsUri = panel.webview.asWebviewUri( + vscode.Uri.joinPath( + extensionUri, + "out", + "node_modules", + "@vscode", + "codicons", + "dist", + "codicon.css" + ) + ); + const nonce = Math.random().toString(36).slice(2); + return ` + + + + + + + +
+ + + `; +} + +/** + * Opens the system-message review panel after a project source/target language change. + * + * The user must either save (after editing or regenerating), explicitly dismiss + * with "I don't need to change this", or close the panel. If the panel is closed + * without the user addressing the prompt, the AI translation instructions are + * regenerated automatically so they don't get left in a stale state. + */ +export async function openSystemMessageReview(reason: ReviewReason): Promise { + const extension = vscode.extensions.getExtension( + "project-accelerate.codex-editor-extension" + ); + if (!extension) { + console.warn("[SystemMessageReview] Codex extension not found; cannot open panel."); + return; + } + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return; + } + const workspaceFolder = workspaceFolders[0].uri; + + // If a review panel is already open, just reveal it and update the reason + // to "both" if it's a different category than before. + if (currentPanel) { + if (currentReason && currentReason !== reason) { + currentReason = "both"; + currentPanel.webview.postMessage({ + command: "init", + data: await buildInitData(workspaceFolder, currentReason), + }); + } + currentPanel.reveal(vscode.ViewColumn.One); + return; + } + + const panel = vscode.window.createWebviewPanel( + "systemMessageReview", + "Review AI Translation Instructions", + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [extension.extensionUri], + } + ); + currentPanel = panel; + currentReason = reason; + + panel.webview.html = buildWebviewHtml(panel, extension.extensionUri); + + let addressed = false; + + const sendInit = async () => { + panel.webview.postMessage({ + command: "init", + data: await buildInitData(workspaceFolder, currentReason ?? reason), + }); + }; + + // Send optimistically; also resend on webviewReady. + sendInit(); + + const messageSub = panel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case "webviewReady": { + await sendInit(); + break; + } + case "systemMessage.generate": { + try { + const { source, target } = await readLanguagesFromMetadata(workspaceFolder); + if (!source?.refName || !target?.refName) { + panel.webview.postMessage({ + command: "systemMessage.generateError", + error: "Source and target languages must be set before generating system message", + }); + break; + } + const generated = await generateChatSystemMessage( + { refName: source.refName }, + { refName: target.refName }, + workspaceFolder + ); + if (!generated) { + panel.webview.postMessage({ + command: "systemMessage.generateError", + error: "Failed to generate system message. Please check your API configuration.", + }); + break; + } + const saveResult = await MetadataManager.setChatSystemMessage( + generated, + workspaceFolder + ); + if (!saveResult.success) { + console.warn( + "[SystemMessageReview] Generated system message but failed to save:", + saveResult.error + ); + } + addressed = true; + panel.webview.postMessage({ + command: "systemMessage.generated", + message: generated, + }); + } catch (error) { + console.error("[SystemMessageReview] Error generating system message:", error); + panel.webview.postMessage({ + command: "systemMessage.generateError", + error: + error instanceof Error + ? error.message + : "Failed to generate system message", + }); + } + break; + } + case "systemMessage.save": { + try { + if (typeof message.message !== "string") { + panel.webview.postMessage({ + command: "systemMessage.saveError", + error: "Invalid message format", + }); + break; + } + const saveResult = await MetadataManager.setChatSystemMessage( + message.message, + workspaceFolder + ); + if (saveResult.success) { + addressed = true; + panel.webview.postMessage({ command: "systemMessage.saved" }); + panel.dispose(); + } else { + panel.webview.postMessage({ + command: "systemMessage.saveError", + error: saveResult.error || "Failed to save system message", + }); + } + } catch (error) { + console.error("[SystemMessageReview] Error saving system message:", error); + panel.webview.postMessage({ + command: "systemMessage.saveError", + error: + error instanceof Error + ? error.message + : "Failed to save system message", + }); + } + break; + } + case "systemMessage.dismiss": { + addressed = true; + vscode.window.showWarningMessage( + "You have changed your project's language. Please make sure that the translation instructions match!" + ); + panel.dispose(); + break; + } + default: + break; + } + }); + + panel.onDidDispose(async () => { + messageSub.dispose(); + const wasAddressed = addressed; + const reasonAtClose = currentReason ?? reason; + currentPanel = undefined; + currentReason = undefined; + if (!wasAddressed) { + // User closed the panel without addressing the prompt — auto-regenerate. + await autoRegenerateAfterClose(workspaceFolder, reasonAtClose); + } + }); +} + +async function buildInitData( + workspaceFolder: vscode.Uri, + reason: ReviewReason +): Promise<{ + systemMessage: string; + sourceLanguage: ProjectLanguage | undefined; + targetLanguage: ProjectLanguage | undefined; + reason: ReviewReason; +}> { + const systemMessage = await MetadataManager.getChatSystemMessage(workspaceFolder); + const { source, target } = await readLanguagesFromMetadata(workspaceFolder); + return { + systemMessage, + sourceLanguage: source, + targetLanguage: target, + reason, + }; +} diff --git a/src/extension.ts b/src/extension.ts index e8c8a4060..87dba4fad 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1295,7 +1295,16 @@ export async function activate(context: vscode.ExtensionContext) { // once the webview initializes and sends getCurrentCellId commentsProvider.setPendingReloadData(options); } - }) + }), + vscode.commands.registerCommand( + "codex-editor-extension.promptSystemMessageReview", + async (reason?: "sourceLanguageChanged" | "targetLanguageChanged" | "both") => { + const { openSystemMessageReview } = await import( + "./copilotSettings/systemMessageReview" + ); + await openSystemMessageReview(reason ?? "both"); + } + ) ); diff --git a/types/index.d.ts b/types/index.d.ts index 4a62f3622..e6ba87798 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -201,7 +201,8 @@ export type MessagesToStartupFlowProvider = | { command: "project.fixAndOpen"; projectPath: string; } | { command: "project.performSwap"; projectPath: string; } | { command: "systemMessage.generate"; } - | { command: "systemMessage.save"; message: string; }; + | { command: "systemMessage.save"; message: string; } + | { command: "systemMessage.dismiss"; }; export type GitLabProject = { id: number; From 22503f211374eabf6268b27b8ca350ec045533bd Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:10:52 -0500 Subject: [PATCH 5/9] feat: prompt system-message review when project language changes Wires the codex-editor-extension.promptSystemMessageReview command into both paths that can change a project's source or target language after initial setup: - codex-project-manager.changeSourceLanguage / changeTargetLanguage (MainMenu and project settings entry points) - StartupFlowProvider.handleProjectChange (pre-open project edit screen) The prompt is only shown when the selected language tag differs from the existing one, so re-confirming the same language is a no-op. The initial project-creation flow remains untouched. Closes #873. --- src/projectManager/index.ts | 37 +++++++++++++++++++ .../StartupFlow/StartupFlowProvider.ts | 13 +++++++ 2 files changed, 50 insertions(+) diff --git a/src/projectManager/index.ts b/src/projectManager/index.ts index 52f80a7b5..a29fde9b9 100644 --- a/src/projectManager/index.ts +++ b/src/projectManager/index.ts @@ -354,6 +354,27 @@ export async function registerProjectManager(context: vscode.ExtensionContext) { await setTargetFont ); + const findLanguageInMetadata = ( + metadata: any, + status: LanguageProjectStatus + ): LanguageMetadata | undefined => { + if (!metadata?.languages || !Array.isArray(metadata.languages)) return undefined; + return metadata.languages.find((l: any) => l?.projectStatus === status); + }; + + const triggerSystemMessageReview = async ( + reason: "sourceLanguageChanged" | "targetLanguageChanged" + ) => { + try { + await vscode.commands.executeCommand( + "codex-editor-extension.promptSystemMessageReview", + reason + ); + } catch (error) { + console.warn("Failed to open system message review:", error); + } + }; + const changeTargetLanguageCommand = vscode.commands.registerCommand( "codex-project-manager.changeTargetLanguage", executeWithRedirecting(async () => { @@ -362,6 +383,10 @@ export async function registerProjectManager(context: vscode.ExtensionContext) { vscode.commands.executeCommand("codex-project-manager.showProjectOverview"); return; } + const previousTarget = findLanguageInMetadata( + metadata, + LanguageProjectStatus.TARGET + ); const projectDetails = await promptForTargetLanguage(); const targetLanguage = projectDetails?.targetLanguage; if (targetLanguage) { @@ -377,6 +402,10 @@ export async function registerProjectManager(context: vscode.ExtensionContext) { vscode.window.showInformationMessage( `Target language set to ${targetLanguage.refName}.` ); + const tagChanged = previousTarget?.tag !== targetLanguage.tag; + if (tagChanged) { + await triggerSystemMessageReview("targetLanguageChanged"); + } } }) ); @@ -390,6 +419,10 @@ export async function registerProjectManager(context: vscode.ExtensionContext) { return; } try { + const previousSource = findLanguageInMetadata( + metadata, + LanguageProjectStatus.SOURCE + ); const projectDetails = await promptForSourceLanguage(); const sourceLanguage = projectDetails?.sourceLanguage; console.log("sourceLanguage", sourceLanguage); @@ -406,6 +439,10 @@ export async function registerProjectManager(context: vscode.ExtensionContext) { vscode.window.showInformationMessage( `Source language set to ${sourceLanguage.refName}.` ); + const tagChanged = previousSource?.tag !== sourceLanguage.tag; + if (tagChanged) { + await triggerSystemMessageReview("sourceLanguageChanged"); + } } } catch (error) { vscode.window.showErrorMessage(`Failed to set source language: ${error}`); diff --git a/src/providers/StartupFlow/StartupFlowProvider.ts b/src/providers/StartupFlow/StartupFlowProvider.ts index b68a352ef..c5f33fc3b 100644 --- a/src/providers/StartupFlow/StartupFlowProvider.ts +++ b/src/providers/StartupFlow/StartupFlowProvider.ts @@ -2173,11 +2173,24 @@ export class StartupFlowProvider implements vscode.CustomTextEditorProvider { const config = vscode.workspace.getConfiguration("codex-project-manager"); const configKey = command === "changeSourceLanguage" ? "sourceLanguage" : "targetLanguage"; + const previousLanguage = config.get(configKey) as { tag?: string; } | undefined; await config.update(configKey, data.language, vscode.ConfigurationTarget.Workspace); await vscode.commands.executeCommand("codex-project-manager.updateMetadataFile"); vscode.window.showInformationMessage( `${command === "changeSourceLanguage" ? "Source" : "Target"} language updated to ${data.language.refName}.` ); + if (previousLanguage?.tag !== data?.language?.tag) { + try { + await vscode.commands.executeCommand( + "codex-editor-extension.promptSystemMessageReview", + command === "changeSourceLanguage" + ? "sourceLanguageChanged" + : "targetLanguageChanged" + ); + } catch (error) { + console.warn("Failed to open system message review:", error); + } + } } else { await vscode.commands.executeCommand(`codex-project-manager.${command}`); // After any project change command, show the Project Manager view From 5052e56db9f862a72db2008adb0056a89e3d6503 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 16:32:33 -0500 Subject: [PATCH 6/9] fix(webviews): clean up generate/save button alignment and tailor warning per context - Simplify the Generate/Save button internals: drop the hand-rolled margin/padding offsets that produced uneven icon spacing in favor of a single inline-flex wrapper with a uniform gap. Fixes the wonky- looking sparkle icon on the Generate button. - Add generateLabel + skipOverwriteWarning props to SystemMessageStep. - The new SystemMessageReview panel now shows "Regenerate" and skips the overwrite-confirmation dialog (regenerating is the expected next step of that flow). - Trim the overwrite warning copy to "Generating a new message will overwrite your current text." since the Cancel / Overwrite and Generate buttons make the rest self-explanatory. --- .../src/SystemMessageReview/index.tsx | 2 + .../src/components/SystemMessageStep.tsx | 121 ++++++++---------- 2 files changed, 55 insertions(+), 68 deletions(-) diff --git a/webviews/codex-webviews/src/SystemMessageReview/index.tsx b/webviews/codex-webviews/src/SystemMessageReview/index.tsx index 3a2ef3743..3ae160dde 100644 --- a/webviews/codex-webviews/src/SystemMessageReview/index.tsx +++ b/webviews/codex-webviews/src/SystemMessageReview/index.tsx @@ -118,6 +118,8 @@ const SystemMessageReviewApp: React.FC = () => { dismissLabel="I don't need to change this" onDismiss={handleDismiss} saveLabel="Save Translation Instructions" + generateLabel="Regenerate" + skipOverwriteWarning />
); diff --git a/webviews/codex-webviews/src/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx index 91b63b9bb..a67476994 100644 --- a/webviews/codex-webviews/src/components/SystemMessageStep.tsx +++ b/webviews/codex-webviews/src/components/SystemMessageStep.tsx @@ -15,6 +15,10 @@ export interface SystemMessageStepProps { onDismiss?: () => void; /** Override label shown on the primary save button. Defaults to "Save and Start Translating". */ saveLabel?: string; + /** Override label shown on the generate button. Defaults to "Generate". */ + generateLabel?: string; + /** When true, clicking Generate skips the overwrite-confirmation warning and generates immediately. */ + skipOverwriteWarning?: boolean; } /** @@ -31,17 +35,16 @@ export const SystemMessageStep: React.FC = ({ dismissLabel, onDismiss, saveLabel, + generateLabel, + skipOverwriteWarning = false, }) => { const [systemMessage, setSystemMessage] = useState(initialMessage); const [isGenerating, setIsGenerating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [showOverwriteWarning, setShowOverwriteWarning] = useState(false); - - // Icon positioning constants - const ICON_LEFT_MARGIN = "3px"; - const TEXT_LEFT_PADDING = "4px"; - const ICON_SIZE = 16; // pixels + + const resolvedGenerateLabel = generateLabel ?? "Generate"; // Update local state when initialMessage changes useEffect(() => { @@ -74,13 +77,13 @@ export const SystemMessageStep: React.FC = ({ }, [onContinue]); const handleGenerate = () => { - // Show warning if there's existing text - if (systemMessage.trim()) { + // Show warning if there's existing text (unless the consumer opted out, e.g. the + // language-change review flow, where regenerating is the expected next step). + if (!skipOverwriteWarning && systemMessage.trim()) { setShowOverwriteWarning(true); return; } - - // Proceed with generation + setIsGenerating(true); setError(null); vscode.postMessage({ @@ -189,7 +192,7 @@ export const SystemMessageStep: React.FC = ({ }} >
- Generating a new message will overwrite your current text. Do you want to proceed? + Generating a new message will overwrite your current text.
= ({ minWidth: "180px", }} > - {(isGenerating || isWaitingForMessage) ? ( - - - Generating... - - ) : ( - - - Generate - - )} + justifyContent: "center", + gap: "6px", + }} + > + + {isGenerating || isWaitingForMessage + ? "Generating..." + : resolvedGenerateLabel} +
@@ -336,33 +325,29 @@ export const SystemMessageStep: React.FC = ({ minWidth: "200px", }} > - {isSaving ? ( - - - Saving... - - ) : systemMessage.trim() ? ( - <> - {saveLabel ?? "Save and Start Translating"} - - - ) : ( - <> - {saveLabel ?? "Start Translating"} - - - )} + justifyContent: "center", + gap: "6px", + }} + > + {isSaving ? ( + <> + + Saving... + + ) : ( + <> + {saveLabel ?? + (systemMessage.trim() + ? "Save and Start Translating" + : "Start Translating")} + + + )} + From a36d866204235591191bde7346c359d86e5c73a8 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 17:21:58 -0500 Subject: [PATCH 7/9] fix(webviews): keep system-message buttons on one line The review panel's "Save Translation Instructions" label was long enough to wrap inside the fixed-min-width button, producing a visually broken multi-line label. Shorten the review label to "Save" and add white-space: nowrap to the button content wrappers so future labels can't wrap either. --- webviews/codex-webviews/src/SystemMessageReview/index.tsx | 2 +- webviews/codex-webviews/src/components/SystemMessageStep.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/webviews/codex-webviews/src/SystemMessageReview/index.tsx b/webviews/codex-webviews/src/SystemMessageReview/index.tsx index 3ae160dde..73eb7cb4d 100644 --- a/webviews/codex-webviews/src/SystemMessageReview/index.tsx +++ b/webviews/codex-webviews/src/SystemMessageReview/index.tsx @@ -117,7 +117,7 @@ const SystemMessageReviewApp: React.FC = () => { headerBanner={banner} dismissLabel="I don't need to change this" onDismiss={handleDismiss} - saveLabel="Save Translation Instructions" + saveLabel="Save" generateLabel="Regenerate" skipOverwriteWarning /> diff --git a/webviews/codex-webviews/src/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx index a67476994..2b2760fb7 100644 --- a/webviews/codex-webviews/src/components/SystemMessageStep.tsx +++ b/webviews/codex-webviews/src/components/SystemMessageStep.tsx @@ -272,6 +272,7 @@ export const SystemMessageStep: React.FC = ({ alignItems: "center", justifyContent: "center", gap: "6px", + whiteSpace: "nowrap", }} > = ({ alignItems: "center", justifyContent: "center", gap: "6px", + whiteSpace: "nowrap", }} > {isSaving ? ( From 77b4798ba937d36068fd52588978b2dc61829383 Mon Sep 17 00:00:00 2001 From: Luke-Bilhorn Date: Mon, 1 Jun 2026 17:28:05 -0500 Subject: [PATCH 8/9] feat(webviews): swap Regenerate/Save emphasis until user interacts In the language-change review flow the expectation is that the user *changes* their translation instructions, so Regenerate should be the primary call-to-action and Save should be demoted. Once the user has either regenerated or edited the text manually, swap them so Save is the highlighted next step. Behavior is gated behind a new emphasizeGenerateUntilEdited prop so the existing project-creation flow (where Save is always primary) is unaffected. --- .../src/SystemMessageReview/index.tsx | 1 + .../src/components/SystemMessageStep.tsx | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/webviews/codex-webviews/src/SystemMessageReview/index.tsx b/webviews/codex-webviews/src/SystemMessageReview/index.tsx index 73eb7cb4d..f5880be77 100644 --- a/webviews/codex-webviews/src/SystemMessageReview/index.tsx +++ b/webviews/codex-webviews/src/SystemMessageReview/index.tsx @@ -120,6 +120,7 @@ const SystemMessageReviewApp: React.FC = () => { saveLabel="Save" generateLabel="Regenerate" skipOverwriteWarning + emphasizeGenerateUntilEdited /> ); diff --git a/webviews/codex-webviews/src/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx index 2b2760fb7..664b34415 100644 --- a/webviews/codex-webviews/src/components/SystemMessageStep.tsx +++ b/webviews/codex-webviews/src/components/SystemMessageStep.tsx @@ -19,6 +19,13 @@ export interface SystemMessageStepProps { generateLabel?: string; /** When true, clicking Generate skips the overwrite-confirmation warning and generates immediately. */ skipOverwriteWarning?: boolean; + /** + * When true, the Generate button is the primary call-to-action until the user has + * either edited the text or regenerated. After that, Save becomes primary. + * Useful for the review flow where the expectation is that the user changes + * something before saving. + */ + emphasizeGenerateUntilEdited?: boolean; } /** @@ -37,19 +44,26 @@ export const SystemMessageStep: React.FC = ({ saveLabel, generateLabel, skipOverwriteWarning = false, + emphasizeGenerateUntilEdited = false, }) => { const [systemMessage, setSystemMessage] = useState(initialMessage); const [isGenerating, setIsGenerating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [showOverwriteWarning, setShowOverwriteWarning] = useState(false); + const [hasInteracted, setHasInteracted] = useState(false); const resolvedGenerateLabel = generateLabel ?? "Generate"; + const generateAppearance: "primary" | "secondary" = + emphasizeGenerateUntilEdited && !hasInteracted ? "primary" : "secondary"; + const saveAppearance: "primary" | "secondary" = + emphasizeGenerateUntilEdited && !hasInteracted ? "secondary" : "primary"; // Update local state when initialMessage changes useEffect(() => { if (initialMessage) { setSystemMessage(initialMessage); + setHasInteracted(false); } }, [initialMessage]); @@ -60,6 +74,7 @@ export const SystemMessageStep: React.FC = ({ setSystemMessage(event.data.message || ""); setIsGenerating(false); setError(null); + setHasInteracted(true); } else if (event.data.command === "systemMessage.generateError") { setError(event.data.error || "Failed to generate system message"); setIsGenerating(false); @@ -230,7 +245,10 @@ export const SystemMessageStep: React.FC = ({ setSystemMessage(e.target.value)} + onInput={(e: any) => { + setSystemMessage(e.target.value); + setHasInteracted(true); + }} placeholder={ isGenerating || isWaitingForMessage ? "Generating translation instructions..." @@ -259,7 +277,7 @@ export const SystemMessageStep: React.FC = ({ }} > = ({ )} Date: Mon, 1 Jun 2026 17:34:25 -0500 Subject: [PATCH 9/9] feat(copilot-settings): show progress + lock controls while regenerating In the Copilot Settings system-message editor: - The Regenerate button keeps its layout but swaps the sparkle icon for a spinning loader while a generation is in flight, and disables itself to prevent re-entry. - The textarea, the empty-state Generate button, and the "Save All Settings" button are also disabled during generation so the user can't edit or save half-state. Cancel stays available as an escape. - The provider now posts generateError when generation throws, so the webview can recover its UI state instead of staying stuck on the spinner. --- src/copilotSettings/copilotSettings.ts | 1 + .../src/CopilotSettings/index.tsx | 42 +++++++++++++++---- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/copilotSettings/copilotSettings.ts b/src/copilotSettings/copilotSettings.ts index fbe927f47..aa943b0c7 100644 --- a/src/copilotSettings/copilotSettings.ts +++ b/src/copilotSettings/copilotSettings.ts @@ -300,6 +300,7 @@ export async function openSystemMessageEditor() { vscode.window.showErrorMessage( "Failed to generate instructions. Please check your API configuration." ); + panel.webview.postMessage({ command: "generateError" }); } break; } diff --git a/webviews/codex-webviews/src/CopilotSettings/index.tsx b/webviews/codex-webviews/src/CopilotSettings/index.tsx index 7c0fa73f2..371cc26fb 100644 --- a/webviews/codex-webviews/src/CopilotSettings/index.tsx +++ b/webviews/codex-webviews/src/CopilotSettings/index.tsx @@ -27,6 +27,7 @@ function CopilotSettingsApp() { const [systemMessage, setSystemMessage] = useState(""); const [sourceLanguage, setSourceLanguage] = useState(null); const [targetLanguage, setTargetLanguage] = useState(null); + const [isGenerating, setIsGenerating] = useState(false); // ASR (Speech to Text) settings const [asrSettings, setAsrSettings] = useState<{ @@ -48,6 +49,9 @@ function CopilotSettingsApp() { // Optional toast could be shown } else if (message.command === "updateInput") { setSystemMessage(message.text || ""); + setIsGenerating(false); + } else if (message.command === "generateError") { + setIsGenerating(false); } }; window.addEventListener("message", handler); @@ -57,6 +61,11 @@ function CopilotSettingsApp() { return () => window.removeEventListener("message", handler); }, [vscode]); + const handleGenerate = () => { + setIsGenerating(true); + vscode.postMessage({ command: "generate" }); + }; + const saveAll = () => { vscode.postMessage({ command: "saveSettings", @@ -124,11 +133,16 @@ function CopilotSettingsApp() {
System Message
{systemMessage && (
{sourceLanguage?.refName && targetLanguage?.refName @@ -169,7 +195,9 @@ function CopilotSettingsApp() { > Cancel - +