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/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/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 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; 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/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 - +
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/SystemMessageReview/index.tsx b/webviews/codex-webviews/src/SystemMessageReview/index.tsx new file mode 100644 index 000000000..f5880be77 --- /dev/null +++ b/webviews/codex-webviews/src/SystemMessageReview/index.tsx @@ -0,0 +1,130 @@ +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(); diff --git a/webviews/codex-webviews/src/StartupFlow/components/SystemMessageStep.tsx b/webviews/codex-webviews/src/components/SystemMessageStep.tsx similarity index 71% rename from webviews/codex-webviews/src/StartupFlow/components/SystemMessageStep.tsx rename to webviews/codex-webviews/src/components/SystemMessageStep.tsx index 3dafad14e..664b34415 100644 --- a/webviews/codex-webviews/src/StartupFlow/components/SystemMessageStep.tsx +++ b/webviews/codex-webviews/src/components/SystemMessageStep.tsx @@ -8,6 +8,24 @@ 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; + /** 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; + /** + * 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; } /** @@ -20,22 +38,32 @@ export const SystemMessageStep: React.FC = ({ onContinue, onSkip, isWaitingForMessage = false, + headerBanner, + dismissLabel, + onDismiss, + 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); - - // Icon positioning constants - const ICON_LEFT_MARGIN = "3px"; - const TEXT_LEFT_PADDING = "4px"; - const ICON_SIZE = 16; // pixels + 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]); @@ -46,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); @@ -63,13 +92,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({ @@ -126,6 +155,9 @@ export const SystemMessageStep: React.FC = ({ gap: "12px", }} > + {headerBanner && ( +
{headerBanner}
+ )}
= ({ }} >
- Generating a new message will overwrite your current text. Do you want to proceed? + Generating a new message will overwrite your current text.
= ({ setSystemMessage(e.target.value)} + onInput={(e: any) => { + setSystemMessage(e.target.value); + setHasInteracted(true); + }} placeholder={ isGenerating || isWaitingForMessage ? "Generating translation instructions..." @@ -242,46 +277,33 @@ export const SystemMessageStep: React.FC = ({ }} > - {(isGenerating || isWaitingForMessage) ? ( - - - Generating... - - ) : ( - - - Generate - - )} + justifyContent: "center", + gap: "6px", + whiteSpace: "nowrap", + }} + > + + {isGenerating || isWaitingForMessage + ? "Generating..." + : resolvedGenerateLabel} +
@@ -305,41 +327,47 @@ export const SystemMessageStep: React.FC = ({ Skip for Now )} */} + {dismissLabel && onDismiss && ( + + {dismissLabel} + + )} - {isSaving ? ( - - - Saving... - - ) : systemMessage.trim() ? ( - <> - Save and Start Translating - - - ) : ( - <> - Start Translating - - - )} + justifyContent: "center", + gap: "6px", + whiteSpace: "nowrap", + }} + > + {isSaving ? ( + <> + + Saving... + + ) : ( + <> + {saveLabel ?? + (systemMessage.trim() + ? "Save and Start Translating" + : "Start Translating")} + + + )} +