feat(preview): embedded dev-server preview pane + dev shutdown controls (#1062)

* feat: embedded preview proxy for local dev servers

Add a same-origin server proxy under /api/preview/proxy/:id and
matching UI surfaces so local dev servers (Vite, Next, etc.) can be
embedded inside OpenChamber.

Server (packages/web/server):
- New lib/preview/proxy-runtime.js: cookie-gated HTTP+WebSocket proxy
  to loopback hosts only, with TTL'd targets and SSRF allowlist.
- index.js wires the runtime alongside terminal/event-stream.

UI (packages/ui):
- ContextPanel preview tab with iframe, reload, and open-in-browser.
- Inline html code-block preview in MarkdownRenderer.
- Terminal auto-detects loopback URLs and offers to open them.
- i18n keys across en, es, pt-BR, uk, zh-CN.

* perf(preview): cache proxy targets across PreviewPane remounts

Module-scoped Map keyed by upstream URL so tab switches and component
remounts within the same page session reuse the existing proxy
registration instead of POSTing a fresh target each time.

In-memory only by design: the server holds the target map in memory
and the auth cookie is HttpOnly + scoped to the proxy id, so a stale
persisted entry would 404 after a server restart. Entries are evicted
on registration error and on a 30s safety margin before TTL expiry.

* feat(preview): surface dev-server-down state with retry overlay

Iframes don't expose HTTP status to the parent, so when the proxy
returns a 502 (upstream dev server is offline) the iframe just renders
the raw JSON error body. Probe the proxy URL out-of-band with HEAD
(falling back to GET on 404/405) and replace the iframe with a
friendly 'Dev server is not responding' overlay + retry button when
the upstream is unreachable.

Re-probes on reload, on URL change, and on proxy re-registration.

* feat(preview): strip frame-busting response headers

Many dev servers (Next.js, others) send X-Frame-Options: SAMEORIGIN
and/or a CSP with frame-ancestors that block embedding inside the
OpenChamber iframe. The proxy is same-origin and already
authenticated per-target, so embedding is otherwise safe.

- Drop X-Frame-Options outright on proxied responses.
- Surgically remove only the frame-ancestors directive from
  Content-Security-Policy and Content-Security-Policy-Report-Only,
  preserving every other directive. Drops the header entirely if no
  directives remain.
- Verified end-to-end: upstream sending both headers comes through
  with X-Frame-Options removed, CSP retaining default-src/script-src
  but no frame-ancestors, and unrelated headers untouched.

* docs(preview): design for remote-host relay agent

Design-only doc for the next phase of the embedded preview feature:
when OpenChamber runs remotely (cloud/shared/tunnel) and the user's
dev server runs on their local machine. Covers architecture (local
agent + outbound control WebSocket + server dispatch), pairing flow,
wire protocol, security model, failure modes, open questions, and
implementation milestones. No code changes.

* feat(preview): auto-open preview pane for loopback URLs in chat

Detect http(s) loopback URLs in incoming assistant messages and open the
preview pane automatically, deduped per (session, url) pair so re-renders
or repeated mentions do not steal focus. Add an inline Preview button
next to loopback links in chat markdown as a manual fallback when the
auto-open was dismissed or the URL appeared in an older message.

- url.ts: isLoopbackHttpUrl / extractLoopbackUrls helpers
- ChatContainer: module-level dedupe Set + effect on active session tail
- MarkdownRendererImpl: optional onPreviewLoopback in main renderer only
  (SimpleMarkdownRenderer for tool diffs is intentionally untouched)
- Reuses existing terminalView.preview.open i18n keys

* feat: preview enhancements, dev shutdown, and reliability fixes

Add preview start/stop UI in ContextPanel/Header, improve URL detection (Python HTTP server logs, trailing punctuation, IPv6 loopback), fix proxy path filtering to avoid disrupting non-preview WebSockets. Add dev-only /api/system/dev-shutdown endpoint and Header button to terminate local dev processes and orphaned preview servers. Improve terminal cleanup with process group killing, event pipeline reconnect backoff. Update file read APIs with optional flag and cache control. Add /api/system/free-port endpoint, detectDevServer.ts utility, and preview/shutdown i18n strings for 5 languages.

* fix: harden preview support

* fix: keep terminal toolbar interactive

* fix: keep expanded terminal below header

* fix: keep preview iframe under proxy path

* fix: respect project action preview urls

* fix: rewrite preview asset urls

* feat: capture preview console logs

* feat: annotate preview elements

* feat: attach preview annotation screenshots

* fix: improve proxied preview hmr

* feat: refine preview action UX

* fix: address preview review feedback

* fix: show auto-discover preview wait state

---------

Co-authored-by: William Biggers <will@Williams-MacBook-Pro.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
wpbiggs
2026-04-30 00:03:38 +03:00
committed by GitHub
co-authored by William Biggers Bohdan Triapitsyn
parent 67d05a23fc
commit bd9a91335c
37 changed files with 4238 additions and 399 deletions
+1
View File
@@ -511,6 +511,7 @@ export interface ListDirectoryOptions {
export interface FileReadOptions {
allowOutsideWorkspace?: boolean;
optional?: boolean;
}
export interface FilesAPI {
+6 -3
View File
@@ -26,12 +26,15 @@ const classifyReadError = (error: unknown): ContextFileOpenFailureReason => {
const readFileContent = async (files: FilesAPI, path: string): Promise<string> => {
if (files.readFile) {
const result = await files.readFile(path, { allowOutsideWorkspace: true });
const result = await files.readFile(path, { allowOutsideWorkspace: true, optional: true });
return result.content ?? '';
}
const params = new URLSearchParams({ path, allowOutsideWorkspace: 'true' });
const response = await fetch(`/api/fs/read?${params.toString()}`);
const params = new URLSearchParams({ path, allowOutsideWorkspace: 'true', optional: 'true' });
const response = await fetch(`/api/fs/read?${params.toString()}`, {
// Avoid conditional requests (304 + empty body).
cache: 'no-store',
});
if (!response.ok) {
const errorPayload = await response.json().catch(() => ({ error: response.statusText }));
throw new Error((errorPayload as { error?: string }).error || 'Failed to read file');
+209
View File
@@ -0,0 +1,209 @@
import type { OpenChamberProjectAction } from './openchamberConfig';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
type DevServerInfo = {
command: string;
label: string;
actionId?: string;
previewUrlHint?: string;
};
type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
const DEV_COMMAND_PATTERNS = [
{ pattern: /^dev(:.*)?$/i },
{ pattern: /^start(:.*)?$/i },
{ pattern: /^preview(:.*)?$/i },
{ pattern: /^serve(:.*)?$/i },
{ pattern: /^develop(:.*)?$/i },
];
const COMMON_DEV_COMMANDS = [
'dev',
'start',
'preview',
'serve',
];
/**
* Detect the dev server command from project actions or package.json scripts
*/
export async function detectDevServerCommand(
directory: string,
projectActions: OpenChamberProjectAction[],
packageJsonScripts: Record<string, string> | null,
): Promise<DevServerInfo | null> {
if (!directory) return null;
// First, check if there's a project action that looks like a dev server
const devAction = findDevServerAction(projectActions);
if (devAction) {
return {
command: devAction.command,
label: devAction.name || 'Start Preview',
actionId: devAction.id,
};
}
// Then, check package.json scripts
if (packageJsonScripts) {
const devScript = findDevScript(packageJsonScripts);
if (devScript) {
// Determine the package manager command
const pm = await detectPackageManager(directory);
const pmCommand = pm === 'npm' ? 'npm run' : pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : pm === 'bun' ? 'bun' : 'npm run';
return {
command: `${pmCommand} ${devScript}`,
label: `Start (${devScript})`,
};
}
}
// Fallback: static sites (no package.json) can be previewed via a simple file server.
// This keeps Start Preview usable for non-Node projects.
if (await hasStaticIndexHtml(directory)) {
const port = await allocatePreviewPort();
const resolvedPort = typeof port === 'number' && Number.isFinite(port) && port > 0 ? port : 8000;
return {
command: `python3 -m http.server ${resolvedPort}`,
label: 'Static preview',
previewUrlHint: `http://127.0.0.1:${resolvedPort}/`,
};
}
return null;
}
async function hasStaticIndexHtml(directory: string): Promise<boolean> {
const target = `${directory}/index.html`;
const content = await readOptionalTextFile(target);
return typeof content === 'string' && content.trim().length > 0;
}
async function allocatePreviewPort(): Promise<number | null> {
try {
const response = await fetch('/api/system/free-port', { cache: 'no-store' });
if (!response.ok) return null;
const body = await response.json().catch(() => null) as { port?: unknown } | null;
const port = typeof body?.port === 'number' ? body.port : null;
return port && Number.isFinite(port) ? port : null;
} catch {
return null;
}
}
/**
* Find a project action that looks like a dev server
*/
function findDevServerAction(actions: OpenChamberProjectAction[]): OpenChamberProjectAction | null {
// Look for actions with "dev", "preview", "start" in the name or command
for (const action of actions) {
const nameAndCommand = `${action.name} ${action.command}`.toLowerCase();
// Check if it's likely a dev server action
const isDevAction = COMMON_DEV_COMMANDS.some(cmd =>
nameAndCommand.includes(cmd)
);
if (isDevAction) {
return action;
}
}
// Fallback: return the first action if there's only one
if (actions.length === 1) {
return actions[0];
}
return null;
}
/**
* Find a dev script in package.json scripts
*/
function findDevScript(scripts: Record<string, string>): string | null {
for (const { pattern } of DEV_COMMAND_PATTERNS) {
for (const scriptName of Object.keys(scripts)) {
if (pattern.test(scriptName)) {
return scriptName;
}
}
}
return null;
}
/**
* Simple package manager detection based on lock files
* Note: This is intentionally a simple client-side check.
* For server-side operations, the server's package-manager.js is used.
*/
async function detectPackageManager(directory: string): Promise<PackageManager> {
const packageJsonContent = await readOptionalTextFile(`${directory}/package.json`);
if (packageJsonContent) {
try {
const pkg = JSON.parse(packageJsonContent) as { packageManager?: unknown };
const packageManager = typeof pkg.packageManager === 'string' ? pkg.packageManager.toLowerCase() : '';
if (packageManager.startsWith('bun@')) return 'bun';
if (packageManager.startsWith('pnpm@')) return 'pnpm';
if (packageManager.startsWith('yarn@')) return 'yarn';
if (packageManager.startsWith('npm@')) return 'npm';
} catch {
// Ignore malformed package.json here; readPackageJsonScripts handles it separately.
}
}
const lockfiles: Array<[string, PackageManager]> = [
['bun.lock', 'bun'],
['bun.lockb', 'bun'],
['pnpm-lock.yaml', 'pnpm'],
['yarn.lock', 'yarn'],
['package-lock.json', 'npm'],
];
for (const [fileName, packageManager] of lockfiles) {
const content = await readOptionalTextFile(`${directory}/${fileName}`);
if (typeof content === 'string' && content.trim().length > 0) {
return packageManager;
}
}
return 'npm';
}
async function readOptionalTextFile(path: string): Promise<string | null> {
const runtimeFiles = getRegisteredRuntimeAPIs()?.files;
if (runtimeFiles?.readFile) {
try {
const result = await runtimeFiles.readFile(path, { optional: true });
return typeof result?.content === 'string' ? result.content : null;
} catch {
return null;
}
}
try {
const response = await fetch(`/api/fs/read?path=${encodeURIComponent(path)}&optional=true`, {
cache: 'no-store',
});
if (!response.ok) return null;
return response.text();
} catch {
return null;
}
}
/**
* Read package.json scripts from a directory
*/
export async function readPackageJsonScripts(directory: string): Promise<Record<string, string> | null> {
try {
const content = await readOptionalTextFile(`${directory}/package.json`);
if (content == null) return null;
const pkg = JSON.parse(content);
return pkg.scripts || null;
} catch {
return null;
}
}
+59
View File
@@ -695,12 +695,61 @@ export const dict = {
'contextPanel.mode.diff': 'Diff',
'contextPanel.mode.plan': 'Plan',
'contextPanel.mode.context': 'Context',
'contextPanel.mode.preview': 'Preview',
'contextPanel.tab.closeTabAria': 'Close {label} tab',
'contextPanel.actions.collapsePanel': 'Collapse panel',
'contextPanel.actions.expandPanel': 'Expand panel',
'contextPanel.actions.closePanel': 'Close panel',
'contextPanel.actions.resizePanelAria': 'Resize context panel',
'contextPanel.iframe.sessionChatTitle': 'Session chat {sessionID}',
'contextPanel.preview.actions.reload': 'Reload preview',
'contextPanel.preview.actions.openExternal': 'Open in browser',
'contextPanel.preview.actions.retry': 'Retry',
'contextPanel.preview.iframeTitle': 'Preview',
'contextPanel.preview.invalidUrl': 'Preview needs a valid http(s) URL.',
'contextPanel.preview.empty': 'No preview URL',
'contextPanel.preview.loading': 'Connecting preview proxy...',
'contextPanel.preview.proxyError': 'Could not start preview proxy.',
'contextPanel.preview.upstreamUnreachable': 'Dev server is not responding.',
'contextPanel.preview.upstreamUnreachableHint': 'Make sure your dev server is still running, then retry.',
'contextPanel.preview.startingServer': 'Starting dev server...',
'contextPanel.preview.startingServerHint': 'Waiting for the server to accept connections.',
'contextPanel.preview.title': 'Preview',
'contextPanel.preview.description': 'Use Project Actions or a terminal Preview button to open a preview.',
'contextPanel.preview.startPreview': 'Start Preview',
'contextPanel.preview.starting': 'Starting...',
'contextPanel.preview.noDevServer': 'No dev server command found. Configure a project action or add a "dev" script to package.json.',
'contextPanel.preview.startFailed': 'Failed to start preview server.',
'contextPanel.preview.serverExited': 'Dev server exited unexpectedly.',
'contextPanel.preview.noUrlDetected': 'Dev server started, but no URL was detected in its output. Open the Preview terminal tab to check logs.',
'contextPanel.preview.serverExitedWithLog': 'Dev server exited unexpectedly. Last output:\n\n{log}',
'contextPanel.preview.noUrlDetectedWithLog': 'Dev server did not become reachable. Last output:\n\n{log}',
'contextPanel.preview.console.open': 'Open preview console',
'contextPanel.preview.console.waiting': 'Waiting for preview console',
'contextPanel.preview.console.title': 'Preview console',
'contextPanel.preview.console.attach': 'Attach',
'contextPanel.preview.console.copy': 'Copy',
'contextPanel.preview.console.clear': 'Clear',
'contextPanel.preview.console.empty': 'No preview console events yet.',
'contextPanel.preview.console.noFilteredEvents': 'No events match this filter.',
'contextPanel.preview.console.runtimeError': 'Runtime error',
'contextPanel.preview.console.copied': 'Preview console copied',
'contextPanel.preview.console.copyFailed': 'Failed to copy preview console',
'contextPanel.preview.console.attached': 'Preview console attached to chat',
'contextPanel.preview.console.attachNoSession': 'Open a chat session before attaching preview logs',
'contextPanel.preview.console.attachAnnotation': 'These are browser console logs from the dev server running for this project.',
'contextPanel.preview.inspect.toggle': 'Inspect preview element',
'contextPanel.preview.inspect.attached': 'Preview annotation attached to chat',
'contextPanel.preview.inspect.attachNoSession': 'Open a chat session before attaching preview annotations',
'contextPanel.preview.inspect.attachAnnotation': 'This is a selected DOM element from the in-app preview.',
'contextPanel.preview.inspect.attachAnnotationWithScreenshot': 'This is a selected DOM element from the in-app preview. A screenshot of the visible preview area with the selected element highlighted is attached.',
'contextPanel.preview.console.filter.all': 'All',
'contextPanel.preview.console.filter.errors': 'Errors',
'contextPanel.preview.console.filter.warnings': 'Warnings',
'contextPanel.preview.console.filter.logs': 'Logs',
'terminalView.preview.open': 'Preview',
'terminalView.preview.openTitle': 'Open preview pane',
'sidebarFilesTree.menu.rename': 'Rename',
'sidebarFilesTree.menu.copyPath': 'Copy Path',
'sidebarFilesTree.menu.save': 'Save',
@@ -958,6 +1007,7 @@ export const dict = {
'header.services.used': 'Used',
'header.services.remaining': 'Remaining',
'header.services.modelFamily.other': 'Other',
'header.services.shutdownDev': 'Stop OpenChamber',
'header.actions.openPlanAria': 'Open plan',
'header.actions.planWithShortcut': 'Plan ({shortcut})',
'header.actions.terminalPanelWithShortcut': 'Terminal panel ({shortcut})',
@@ -1329,6 +1379,8 @@ export const dict = {
'chat.messageBody.actions.fork': 'Fork from here',
'chat.messageBody.actions.copyMessageAria': 'Copy message text',
'chat.messageBody.actions.copyMessage': 'Copy message',
'chat.messageBody.actions.openPreviewAria': 'Open preview',
'chat.messageBody.actions.openPreview': 'Open preview',
'chat.messageBody.actions.copyAnswer': 'Copy answer',
'chat.messageBody.actions.savingImage': 'Saving image...',
'chat.messageBody.actions.saveAsImage': 'Save as image',
@@ -1382,6 +1434,11 @@ export const dict = {
'chat.chatInput.toast.openSessionFirst': 'Open a session first',
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': 'Failed to toggle permission auto-accept',
'chat.chatInput.reviewComments': 'Review comments:',
'chat.chatInput.devServerLogs': 'Dev Server logs:',
'chat.chatInput.devServerLogsRemove': 'Remove Dev Server logs',
'chat.chatInput.previewAnnotations': 'Preview annotations:',
'chat.chatInput.previewContext': 'Preview context:',
'chat.chatInput.previewContextRemove': 'Remove preview context',
'chat.chatInput.projectRoot': 'Project root',
'chat.chatInput.branch': 'Branch',
'chat.chatInput.worktrees': 'Worktrees',
@@ -1762,7 +1819,9 @@ export const dict = {
'projectActions.actions.addActionAria': 'Add action',
'projectActions.actions.addAction': 'Add action',
'projectActions.actions.addNewAction': 'Add new action',
'projectActions.actions.autoDiscover': 'Auto-discover',
'projectActions.actions.chooseActionAria': 'Choose project action',
'projectActions.actions.openPreview': 'Open Preview',
'projectActions.actions.runNamedAria': 'Run {name}',
'projectActions.actions.stopNamedAria': 'Stop {name}',
'projectActions.label.fallbackAction': 'Action',
+59
View File
@@ -696,12 +696,26 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.diff": "Diff",
"contextPanel.mode.plan": "Plan",
"contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Vista previa",
"contextPanel.tab.closeTabAria": "Cerrar pestaña {label}",
"contextPanel.actions.collapsePanel": "Colapsar panel",
"contextPanel.actions.expandPanel": "Expandir panel",
"contextPanel.actions.closePanel": "Cerrar panel",
"contextPanel.actions.resizePanelAria": "Ajustar tamaño del panel de contexto",
"contextPanel.iframe.sessionChatTitle": "Chat de sesión {sessionID}",
"contextPanel.preview.actions.reload": "Recargar vista previa",
"contextPanel.preview.actions.openExternal": "Abrir en el navegador",
"contextPanel.preview.actions.retry": "Reintentar",
"contextPanel.preview.iframeTitle": "Vista previa",
"contextPanel.preview.invalidUrl": "La vista previa necesita una URL http(s) válida.",
"contextPanel.preview.empty": "Sin URL de vista previa",
"contextPanel.preview.loading": "Conectando proxy de vista previa...",
"contextPanel.preview.proxyError": "No se pudo iniciar el proxy de vista previa.",
"contextPanel.preview.upstreamUnreachable": "El servidor de desarrollo no responde.",
"contextPanel.preview.upstreamUnreachableHint": "Verifica que tu servidor de desarrollo siga en ejecución y vuelve a intentarlo.",
"terminalView.preview.open": "Vista previa",
"terminalView.preview.openTitle": "Abrir panel de vista previa",
"sidebarFilesTree.menu.rename": "Cambiar nombre",
"sidebarFilesTree.menu.copyPath": "Copiar ruta",
"sidebarFilesTree.menu.save": "Guardar",
@@ -959,6 +973,7 @@ export const dict: Record<I18nKey, string> = {
"header.services.used": "Usado",
"header.services.remaining": "Restante",
"header.services.modelFamily.other": "Otro",
"header.services.shutdownDev": "Detener OpenChamber",
"header.actions.openPlanAria": "Abrir plan",
"header.actions.planWithShortcut": "Plan ({shortcut})",
"header.actions.terminalPanelWithShortcut": "Panel de terminal ({shortcut})",
@@ -1330,6 +1345,8 @@ export const dict: Record<I18nKey, string> = {
"chat.messageBody.actions.fork": "Bifurcar desde aquí",
"chat.messageBody.actions.copyMessageAria": "Copiar texto del mensaje",
"chat.messageBody.actions.copyMessage": "Copiar mensaje",
"chat.messageBody.actions.openPreviewAria": "Abrir vista previa",
"chat.messageBody.actions.openPreview": "Abrir vista previa",
"chat.messageBody.actions.copyAnswer": "Copiar respuesta",
"chat.messageBody.actions.savingImage": "Guardando imagen...",
"chat.messageBody.actions.saveAsImage": "Guardar como imagen",
@@ -1383,6 +1400,11 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.toast.openSessionFirst": "Abre una sesión primero",
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "No se pudo cambiar la aceptación automática de permisos",
"chat.chatInput.reviewComments": "Comentarios de revisión:",
"chat.chatInput.devServerLogs": "Logs del Dev Server:",
"chat.chatInput.devServerLogsRemove": "Quitar logs del Dev Server",
"chat.chatInput.previewAnnotations": "Anotaciones de vista previa:",
"chat.chatInput.previewContext": "Contexto de vista previa:",
"chat.chatInput.previewContextRemove": "Quitar contexto de vista previa",
"chat.chatInput.projectRoot": "Raíz del proyecto",
"chat.chatInput.branch": "Rama",
"chat.chatInput.worktrees": "Worktrees",
@@ -1763,7 +1785,9 @@ export const dict: Record<I18nKey, string> = {
"projectActions.actions.addActionAria": "Añadir acción",
"projectActions.actions.addAction": "Añadir acción",
"projectActions.actions.addNewAction": "Añadir nueva acción",
"projectActions.actions.autoDiscover": "Autodetectar",
"projectActions.actions.chooseActionAria": "Elegir acción del proyecto",
"projectActions.actions.openPreview": "Abrir Preview",
"projectActions.actions.runNamedAria": "Ejecutar {name}",
"projectActions.actions.stopNamedAria": "Detener {name}",
"projectActions.label.fallbackAction": "Acción",
@@ -2092,4 +2116,39 @@ export const dict: Record<I18nKey, string> = {
"markdownRenderer.mermaid.actions.copySourceTitle": "Copiar fuente",
"markdownRenderer.mermaid.actions.downloadSvgTitle": "Descargar SVG",
"markdownRenderer.mermaid.toast.downloadFailed": "No se pudo descargar el diagrama",
"contextPanel.preview.title": "Vista previa",
"contextPanel.preview.description": "Usa Acciones del proyecto o el botón Preview del terminal para abrir una vista previa.",
"contextPanel.preview.startPreview": "Iniciar vista previa",
"contextPanel.preview.starting": "Iniciando...",
"contextPanel.preview.noDevServer": "No se encontro un comando de servidor de desarrollo. Configura una accion del proyecto o anade un script \"dev\" a package.json.",
"contextPanel.preview.startFailed": "Fallo al iniciar el servidor de vista previa.",
"contextPanel.preview.serverExited": "El servidor de desarrollo se cerro inesperadamente.",
"contextPanel.preview.noUrlDetected": "El servidor de desarrollo se inicio, pero no se detecto ningun URL en su salida. Abre la pestaña de terminal de vista previa para ver los registros.",
"contextPanel.preview.serverExitedWithLog": "El servidor de desarrollo terminó inesperadamente. Última salida:\n\n{log}",
"contextPanel.preview.noUrlDetectedWithLog": "El servidor de desarrollo no respondió. Última salida:\n\n{log}",
"contextPanel.preview.startingServer": "Iniciando servidor de desarrollo...",
"contextPanel.preview.startingServerHint": "Esperando a que el servidor acepte conexiones.",
"contextPanel.preview.console.open": "Abrir consola de vista previa",
"contextPanel.preview.console.waiting": "Esperando la consola de vista previa",
"contextPanel.preview.console.title": "Consola de vista previa",
"contextPanel.preview.console.attach": "Adjuntar",
"contextPanel.preview.console.copy": "Copiar",
"contextPanel.preview.console.clear": "Limpiar",
"contextPanel.preview.console.empty": "Aún no hay eventos de consola de vista previa.",
"contextPanel.preview.console.noFilteredEvents": "Ningún evento coincide con este filtro.",
"contextPanel.preview.console.runtimeError": "Error de ejecución",
"contextPanel.preview.console.copied": "Consola de vista previa copiada",
"contextPanel.preview.console.copyFailed": "No se pudo copiar la consola de vista previa",
"contextPanel.preview.console.attached": "Consola de vista previa adjuntada al chat",
"contextPanel.preview.console.attachNoSession": "Abre una sesión de chat antes de adjuntar logs de vista previa",
"contextPanel.preview.console.attachAnnotation": "Estos son logs de la consola del navegador del servidor de desarrollo que se ejecuta para este proyecto.",
"contextPanel.preview.inspect.toggle": "Inspeccionar elemento de vista previa",
"contextPanel.preview.inspect.attached": "Anotación de vista previa adjuntada al chat",
"contextPanel.preview.inspect.attachNoSession": "Abre una sesión de chat antes de adjuntar anotaciones de vista previa",
"contextPanel.preview.inspect.attachAnnotation": "Este es un elemento DOM seleccionado de la vista previa integrada.",
"contextPanel.preview.inspect.attachAnnotationWithScreenshot": "Este es un elemento DOM seleccionado de la vista previa integrada. Se adjunta una captura del área visible de la vista previa con el elemento resaltado.",
"contextPanel.preview.console.filter.all": "Todo",
"contextPanel.preview.console.filter.errors": "Errores",
"contextPanel.preview.console.filter.warnings": "Advertencias",
"contextPanel.preview.console.filter.logs": "Registros",
};
+58
View File
@@ -696,6 +696,57 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.diff': 'diff',
'contextPanel.mode.plan': '플랜',
'contextPanel.mode.context': '컨텍스트',
'contextPanel.mode.preview': '미리보기',
'contextPanel.preview.actions.reload': '미리보기 새로고침',
'contextPanel.preview.actions.openExternal': '브라우저에서 열기',
'contextPanel.preview.actions.retry': '다시 시도',
'contextPanel.preview.iframeTitle': '미리보기',
'contextPanel.preview.invalidUrl': '미리보기에는 유효한 http(s) URL이 필요합니다.',
'contextPanel.preview.empty': '미리보기 URL이 없습니다',
'contextPanel.preview.loading': '미리보기 프록시에 연결 중...',
'contextPanel.preview.proxyError': '미리보기 프록시를 시작할 수 없습니다.',
'contextPanel.preview.upstreamUnreachable': '개발 서버가 응답하지 않습니다.',
'contextPanel.preview.upstreamUnreachableHint': '개발 서버가 여전히 실행 중인지 확인한 후 다시 시도하세요.',
'contextPanel.preview.startingServer': '개발 서버를 시작하는 중...',
'contextPanel.preview.startingServerHint': '서버가 연결을 수락할 때까지 기다리는 중입니다.',
'contextPanel.preview.title': '미리보기',
'contextPanel.preview.description': '프로젝트 작업 또는 터미널 Preview 버튼으로 미리보기를 여세요.',
'contextPanel.preview.startPreview': '미리보기 시작',
'contextPanel.preview.starting': '시작 중...',
'contextPanel.preview.noDevServer': '개발 서버 명령을 찾을 수 없습니다. 프로젝트 동작을 구성하거나 package.json에 "dev" 스크립트를 추가하세요.',
'contextPanel.preview.startFailed': '미리보기 서버를 시작하지 못했습니다.',
'contextPanel.preview.serverExited': '개발 서버가 예기치 않게 종료되었습니다.',
'contextPanel.preview.noUrlDetected': '개발 서버가 시작되었지만 출력에서 URL이 감지되지 않았습니다. 미리보기 터미널 탭에서 로그를 확인하세요.',
'contextPanel.preview.serverExitedWithLog': '개발 서버가 예기치 않게 종료되었습니다. 마지막 출력:\n\n{log}',
'contextPanel.preview.noUrlDetectedWithLog': '개발 서버가 응답할 수 있는 상태가 되지 못했습니다. 마지막 출력:\n\n{log}',
'contextPanel.preview.console.open': '미리보기 콘솔 열기',
'contextPanel.preview.console.waiting': '미리보기 콘솔 대기 중',
'contextPanel.preview.console.title': '미리보기 콘솔',
'contextPanel.preview.console.attach': '첨부',
'contextPanel.preview.console.copy': '복사',
'contextPanel.preview.console.clear': '지우기',
'contextPanel.preview.console.empty': '아직 미리보기 콘솔 이벤트가 없습니다.',
'contextPanel.preview.console.noFilteredEvents': '이 필터와 일치하는 이벤트가 없습니다.',
'contextPanel.preview.console.runtimeError': '런타임 오류',
'contextPanel.preview.console.copied': '미리보기 콘솔 복사됨',
'contextPanel.preview.console.copyFailed': '미리보기 콘솔 복사 실패',
'contextPanel.preview.console.attached': '미리보기 콘솔이 채팅에 첨부되었습니다',
'contextPanel.preview.console.attachNoSession': '미리보기 로그를 첨부하기 전에 채팅 세션을 여세요',
'contextPanel.preview.console.attachAnnotation': '이것은 이 프로젝트에서 실행 중인 개발 서버의 브라우저 콘솔 로그입니다.',
'contextPanel.preview.inspect.toggle': '미리보기 요소 검사',
'contextPanel.preview.inspect.attached': '미리보기 주석이 채팅에 첨부되었습니다',
'contextPanel.preview.inspect.attachNoSession': '미리보기 주석을 첨부하기 전에 채팅 세션을 여세요',
'contextPanel.preview.inspect.attachAnnotation': '인앱 미리보기에서 선택한 DOM 요소입니다.',
'contextPanel.preview.inspect.attachAnnotationWithScreenshot': '인앱 미리보기에서 선택한 DOM 요소입니다. 선택한 요소가 강조된 보이는 미리보기 영역 스크린샷이 첨부되었습니다.',
'contextPanel.preview.console.filter.all': '전체',
'contextPanel.preview.console.filter.errors': '오류',
'contextPanel.preview.console.filter.warnings': '경고',
'contextPanel.preview.console.filter.logs': '로그',
'terminalView.preview.open': '미리보기',
'terminalView.preview.openTitle': '미리보기 패널 열기',
'header.services.shutdownDev': 'OpenChamber 종료',
'chat.messageBody.actions.openPreviewAria': '미리보기 열기',
'chat.messageBody.actions.openPreview': '미리보기 열기',
'contextPanel.tab.closeTabAria': '{label} tab 닫기',
'contextPanel.actions.collapsePanel': '접기 패널',
'contextPanel.actions.expandPanel': '펼치기 패널',
@@ -1383,6 +1434,11 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.toast.openSessionFirst': '먼저 세션을 여세요',
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': 'toggle permission auto-accept 실패',
'chat.chatInput.reviewComments': 'Review 댓글:',
'chat.chatInput.devServerLogs': 'Dev Server 로그:',
'chat.chatInput.devServerLogsRemove': 'Dev Server 로그 제거',
'chat.chatInput.previewAnnotations': '미리보기 주석:',
'chat.chatInput.previewContext': '미리보기 컨텍스트:',
'chat.chatInput.previewContextRemove': '미리보기 컨텍스트 제거',
'chat.chatInput.projectRoot': '프로젝트 root',
'chat.chatInput.branch': '브랜치',
'chat.chatInput.worktrees': '워크트리',
@@ -1763,7 +1819,9 @@ export const dict: Record<I18nKey, string> = {
'projectActions.actions.addActionAria': 'action 추가',
'projectActions.actions.addAction': 'action 추가',
'projectActions.actions.addNewAction': 'new action 추가',
'projectActions.actions.autoDiscover': '자동 검색',
'projectActions.actions.chooseActionAria': '선택 프로젝트 작업',
'projectActions.actions.openPreview': 'Preview 열기',
'projectActions.actions.runNamedAria': '실행 {name}',
'projectActions.actions.stopNamedAria': '중지 {name}',
'projectActions.label.fallbackAction': '작업',
@@ -696,12 +696,26 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.diff": "Diff",
"contextPanel.mode.plan": "Plano",
"contextPanel.mode.context": "Contexto",
"contextPanel.mode.preview": "Prévia",
"contextPanel.tab.closeTabAria": "Fechar aba {label}",
"contextPanel.actions.collapsePanel": "Recolher painel",
"contextPanel.actions.expandPanel": "Expandir painel",
"contextPanel.actions.closePanel": "Fechar painel",
"contextPanel.actions.resizePanelAria": "Ajustar tamanho do painel de contexto",
"contextPanel.iframe.sessionChatTitle": "Chat de sessão {sessionID}",
"contextPanel.preview.actions.reload": "Recarregar prévia",
"contextPanel.preview.actions.openExternal": "Abrir no navegador",
"contextPanel.preview.actions.retry": "Tentar novamente",
"contextPanel.preview.iframeTitle": "Prévia",
"contextPanel.preview.invalidUrl": "A prévia precisa de uma URL http(s) válida.",
"contextPanel.preview.empty": "Sem URL de prévia",
"contextPanel.preview.loading": "Conectando proxy de prévia...",
"contextPanel.preview.proxyError": "Não foi possível iniciar o proxy de prévia.",
"contextPanel.preview.upstreamUnreachable": "O servidor de desenvolvimento não está respondendo.",
"contextPanel.preview.upstreamUnreachableHint": "Verifique se o servidor de desenvolvimento ainda está em execução e tente novamente.",
"terminalView.preview.open": "Prévia",
"terminalView.preview.openTitle": "Abrir painel de prévia",
"sidebarFilesTree.menu.rename": "Renomear",
"sidebarFilesTree.menu.copyPath": "Copiar caminho",
"sidebarFilesTree.menu.save": "Salvar",
@@ -959,6 +973,7 @@ export const dict: Record<I18nKey, string> = {
"header.services.used": "Usado",
"header.services.remaining": "Restante",
"header.services.modelFamily.other": "Outro",
"header.services.shutdownDev": "Parar OpenChamber",
"header.actions.openPlanAria": "Abrir plano",
"header.actions.planWithShortcut": "Plano ({shortcut})",
"header.actions.terminalPanelWithShortcut": "Painel de terminal ({shortcut})",
@@ -1330,6 +1345,8 @@ export const dict: Record<I18nKey, string> = {
"chat.messageBody.actions.fork": "Bifurcar daqui",
"chat.messageBody.actions.copyMessageAria": "Copiar texto da mensagem",
"chat.messageBody.actions.copyMessage": "Copiar mensagem",
"chat.messageBody.actions.openPreviewAria": "Abrir visualização",
"chat.messageBody.actions.openPreview": "Abrir visualização",
"chat.messageBody.actions.copyAnswer": "Copiar resposta",
"chat.messageBody.actions.savingImage": "Salvando imagem...",
"chat.messageBody.actions.saveAsImage": "Salvar como imagem",
@@ -1383,6 +1400,11 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.toast.openSessionFirst": "Abra uma sessão primeiro",
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "Não foi possível alterar a aceitação automática de permissões",
"chat.chatInput.reviewComments": "Comentários de revisão:",
"chat.chatInput.devServerLogs": "Logs do Dev Server:",
"chat.chatInput.devServerLogsRemove": "Remover logs do Dev Server",
"chat.chatInput.previewAnnotations": "Anotações da visualização:",
"chat.chatInput.previewContext": "Contexto da visualização:",
"chat.chatInput.previewContextRemove": "Remover contexto da visualização",
"chat.chatInput.projectRoot": "Raiz do projeto",
"chat.chatInput.branch": "Branch",
"chat.chatInput.worktrees": "Worktrees",
@@ -1763,7 +1785,9 @@ export const dict: Record<I18nKey, string> = {
"projectActions.actions.addActionAria": "Adicionar ação",
"projectActions.actions.addAction": "Adicionar ação",
"projectActions.actions.addNewAction": "Adicionar nova ação",
"projectActions.actions.autoDiscover": "Detectar automaticamente",
"projectActions.actions.chooseActionAria": "Escolher ação do projeto",
"projectActions.actions.openPreview": "Abrir Preview",
"projectActions.actions.runNamedAria": "Executar {name}",
"projectActions.actions.stopNamedAria": "Parar {name}",
"projectActions.label.fallbackAction": "Ação",
@@ -2092,4 +2116,39 @@ export const dict: Record<I18nKey, string> = {
"markdownRenderer.mermaid.actions.copySourceTitle": "Copiar origem",
"markdownRenderer.mermaid.actions.downloadSvgTitle": "Baixar SVG",
"markdownRenderer.mermaid.toast.downloadFailed": "Não foi possível baixar o diagrama",
"contextPanel.preview.title": "Visualização",
"contextPanel.preview.description": "Use Ações do projeto ou o botão Preview do terminal para abrir uma visualização.",
"contextPanel.preview.startPreview": "Iniciar visualização",
"contextPanel.preview.starting": "Iniciando...",
"contextPanel.preview.noDevServer": "Nenhum comando de servidor de desenvolvimento encontrado. Configure uma ação do projeto ou adicione um script \"dev\" ao package.json.",
"contextPanel.preview.startFailed": "Falha ao iniciar o servidor de visualização.",
"contextPanel.preview.serverExited": "Servidor de desenvolvimento encerrou inesperadamente.",
"contextPanel.preview.noUrlDetected": "O servidor de desenvolvimento foi iniciado, mas nenhum URL foi detectado na saída. Abra a aba de terminal da visualização para ver os logs.",
"contextPanel.preview.serverExitedWithLog": "O servidor de desenvolvimento encerrou inesperadamente. Última saída:\n\n{log}",
"contextPanel.preview.noUrlDetectedWithLog": "O servidor de desenvolvimento não respondeu. Última saída:\n\n{log}",
"contextPanel.preview.startingServer": "Iniciando servidor de desenvolvimento...",
"contextPanel.preview.startingServerHint": "Aguardando o servidor aceitar conexões.",
"contextPanel.preview.console.open": "Abrir console da visualização",
"contextPanel.preview.console.waiting": "Aguardando console da visualização",
"contextPanel.preview.console.title": "Console da visualização",
"contextPanel.preview.console.attach": "Anexar",
"contextPanel.preview.console.copy": "Copiar",
"contextPanel.preview.console.clear": "Limpar",
"contextPanel.preview.console.empty": "Ainda não há eventos do console da visualização.",
"contextPanel.preview.console.noFilteredEvents": "Nenhum evento corresponde a este filtro.",
"contextPanel.preview.console.runtimeError": "Erro de execução",
"contextPanel.preview.console.copied": "Console da visualização copiado",
"contextPanel.preview.console.copyFailed": "Não foi possível copiar o console da visualização",
"contextPanel.preview.console.attached": "Console da visualização anexado ao chat",
"contextPanel.preview.console.attachNoSession": "Abra uma sessão de chat antes de anexar logs da visualização",
"contextPanel.preview.console.attachAnnotation": "Estes são logs do console do navegador do servidor de desenvolvimento em execução para este projeto.",
"contextPanel.preview.inspect.toggle": "Inspecionar elemento da visualização",
"contextPanel.preview.inspect.attached": "Anotação da visualização anexada ao chat",
"contextPanel.preview.inspect.attachNoSession": "Abra uma sessão de chat antes de anexar anotações da visualização",
"contextPanel.preview.inspect.attachAnnotation": "Este é um elemento DOM selecionado da visualização integrada.",
"contextPanel.preview.inspect.attachAnnotationWithScreenshot": "Este é um elemento DOM selecionado da visualização integrada. Uma captura da área visível da visualização com o elemento destacado foi anexada.",
"contextPanel.preview.console.filter.all": "Tudo",
"contextPanel.preview.console.filter.errors": "Erros",
"contextPanel.preview.console.filter.warnings": "Avisos",
"contextPanel.preview.console.filter.logs": "Logs",
};
+59
View File
@@ -696,12 +696,26 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.mode.diff": "Diff",
"contextPanel.mode.plan": "План",
"contextPanel.mode.context": "Контекст",
"contextPanel.mode.preview": "Перегляд",
"contextPanel.tab.closeTabAria": "Закрити вкладку {label}",
"contextPanel.actions.collapsePanel": "Згорнути панель",
"contextPanel.actions.expandPanel": "Розгорнути панель",
"contextPanel.actions.closePanel": "Закрити панель",
"contextPanel.actions.resizePanelAria": "Змінити розмір контекстної панелі",
"contextPanel.iframe.sessionChatTitle": "Сесійний чат {sessionID}",
"contextPanel.preview.actions.reload": "Перезавантажити перегляд",
"contextPanel.preview.actions.openExternal": "Відкрити в браузері",
"contextPanel.preview.actions.retry": "Повторити",
"contextPanel.preview.iframeTitle": "Перегляд",
"contextPanel.preview.invalidUrl": "Для перегляду потрібна коректна URL http(s).",
"contextPanel.preview.empty": "Немає URL для перегляду",
"contextPanel.preview.loading": "Підключення проксі перегляду...",
"contextPanel.preview.proxyError": "Не вдалося запустити проксі перегляду.",
"contextPanel.preview.upstreamUnreachable": "Сервер розробки не відповідає.",
"contextPanel.preview.upstreamUnreachableHint": "Переконайтеся, що сервер розробки все ще працює, і повторіть спробу.",
"terminalView.preview.open": "Перегляд",
"terminalView.preview.openTitle": "Відкрити панель перегляду",
"sidebarFilesTree.menu.rename": "Перейменувати",
"sidebarFilesTree.menu.copyPath": "Копіювати шлях",
"sidebarFilesTree.menu.save": "Зберегти",
@@ -958,6 +972,7 @@ export const dict: Record<I18nKey, string> = {
"header.services.noRateLimitsReported": "Ліміти запитів не надходять.",
"header.services.used": "Використано",
"header.services.remaining": "Залишилося",
"header.services.shutdownDev": "Зупинити OpenChamber",
"header.services.modelFamily.other": "інше",
"header.actions.openPlanAria": "Відкрити план",
"header.actions.planWithShortcut": "План ({shortcut})",
@@ -1330,6 +1345,8 @@ export const dict: Record<I18nKey, string> = {
"chat.messageBody.actions.fork": "Відгалузити звідси",
"chat.messageBody.actions.copyMessageAria": "Копіювати текст повідомлення",
"chat.messageBody.actions.copyMessage": "Копіювати повідомлення",
"chat.messageBody.actions.openPreviewAria": "Відкрити попередній перегляд",
"chat.messageBody.actions.openPreview": "Відкрити попередній перегляд",
"chat.messageBody.actions.copyAnswer": "Скопіювати відповідь",
"chat.messageBody.actions.savingImage": "Збереження зображення...",
"chat.messageBody.actions.saveAsImage": "Зберегти як зображення",
@@ -1383,6 +1400,11 @@ export const dict: Record<I18nKey, string> = {
"chat.chatInput.toast.openSessionFirst": "Спочатку відкрийте сесію",
"chat.chatInput.toast.togglePermissionAutoAcceptFailed": "Не вдалося ввімкнути автоматичне прийняття дозволів",
"chat.chatInput.reviewComments": "Коментарі рев’ю:",
"chat.chatInput.devServerLogs": "Логи Dev Server:",
"chat.chatInput.devServerLogsRemove": "Прибрати логи Dev Server",
"chat.chatInput.previewAnnotations": "Анотації перегляду:",
"chat.chatInput.previewContext": "Контекст перегляду:",
"chat.chatInput.previewContextRemove": "Прибрати контекст перегляду",
"chat.chatInput.projectRoot": "Корінь проєкту",
"chat.chatInput.branch": "гілка",
"chat.chatInput.worktrees": "Worktree",
@@ -1763,7 +1785,9 @@ export const dict: Record<I18nKey, string> = {
"projectActions.actions.addActionAria": "Додати дію",
"projectActions.actions.addAction": "Додати дію",
"projectActions.actions.addNewAction": "Додати нову дію",
"projectActions.actions.autoDiscover": "Автовиявлення",
"projectActions.actions.chooseActionAria": "Вибрати дію проєкту",
"projectActions.actions.openPreview": "Відкрити Preview",
"projectActions.actions.runNamedAria": "Запустити {name}",
"projectActions.actions.stopNamedAria": "Зупинити {name}",
"projectActions.label.fallbackAction": "Дія",
@@ -2092,4 +2116,39 @@ export const dict: Record<I18nKey, string> = {
"markdownRenderer.mermaid.actions.copySourceTitle": "Копіювати джерело",
"markdownRenderer.mermaid.actions.downloadSvgTitle": "Завантажити SVG",
"markdownRenderer.mermaid.toast.downloadFailed": "Не вдалося завантажити діаграму",
"contextPanel.preview.title": "Попередній перегляд",
"contextPanel.preview.description": "Використайте дії проєкту або кнопку Preview у терміналі, щоб відкрити перегляд.",
"contextPanel.preview.startPreview": "Почати перегляд",
"contextPanel.preview.starting": "Запуск...",
"contextPanel.preview.noDevServer": "Команду сервера розробки не знайдено. Налаштуйте дію проекту або додайте скрипт \"dev\" у package.json.",
"contextPanel.preview.startFailed": "Не вдалося запустити сервер перегляду.",
"contextPanel.preview.serverExited": "Сервер розробки несподівано завершив роботу.",
"contextPanel.preview.noUrlDetected": "Сервер розробки запущено, але URL не виявлено у його виводі. Відкрийте вкладку термінала попереднього перегляду, щоб переглянути журнали.",
"contextPanel.preview.serverExitedWithLog": "Сервер розробки несподівано завершив роботу. Останній вивід:\n\n{log}",
"contextPanel.preview.noUrlDetectedWithLog": "Сервер розробки не став доступним. Останній вивід:\n\n{log}",
"contextPanel.preview.startingServer": "Запуск сервера розробки...",
"contextPanel.preview.startingServerHint": "Очікуємо, поки сервер почне приймати з'єднання.",
"contextPanel.preview.console.open": "Відкрити консоль перегляду",
"contextPanel.preview.console.waiting": "Очікування консолі перегляду",
"contextPanel.preview.console.title": "Консоль перегляду",
"contextPanel.preview.console.attach": "Додати",
"contextPanel.preview.console.copy": "Копіювати",
"contextPanel.preview.console.clear": "Очистити",
"contextPanel.preview.console.empty": "Подій консолі перегляду ще немає.",
"contextPanel.preview.console.noFilteredEvents": "Немає подій для цього фільтра.",
"contextPanel.preview.console.runtimeError": "Помилка виконання",
"contextPanel.preview.console.copied": "Консоль перегляду скопійовано",
"contextPanel.preview.console.copyFailed": "Не вдалося скопіювати консоль перегляду",
"contextPanel.preview.console.attached": "Консоль перегляду додано до чату",
"contextPanel.preview.console.attachNoSession": "Відкрийте чат-сесію перед додаванням логів перегляду",
"contextPanel.preview.console.attachAnnotation": "Це браузерні console logs із dev server, що запущений для цього проєкту.",
"contextPanel.preview.inspect.toggle": "Інспектувати елемент перегляду",
"contextPanel.preview.inspect.attached": "Анотацію перегляду додано до чату",
"contextPanel.preview.inspect.attachNoSession": "Відкрийте чат-сесію перед додаванням анотацій перегляду",
"contextPanel.preview.inspect.attachAnnotation": "Це вибраний DOM-елемент із вбудованого перегляду.",
"contextPanel.preview.inspect.attachAnnotationWithScreenshot": "Це вибраний DOM-елемент із вбудованого перегляду. Скріншот видимої області перегляду з підсвіченим елементом додано як вкладення.",
"contextPanel.preview.console.filter.all": "Усі",
"contextPanel.preview.console.filter.errors": "Помилки",
"contextPanel.preview.console.filter.warnings": "Попередження",
"contextPanel.preview.console.filter.logs": "Логи",
};
@@ -696,12 +696,26 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.mode.diff': '对比',
'contextPanel.mode.plan': '计划',
'contextPanel.mode.context': '上下文',
'contextPanel.mode.preview': '预览',
'contextPanel.tab.closeTabAria': '关闭 {label} 标签',
'contextPanel.actions.collapsePanel': '折叠面板',
'contextPanel.actions.expandPanel': '展开面板',
'contextPanel.actions.closePanel': '关闭面板',
'contextPanel.actions.resizePanelAria': '调整上下文面板大小',
'contextPanel.iframe.sessionChatTitle': '会话聊天 {sessionID}',
'contextPanel.preview.actions.reload': '刷新预览',
'contextPanel.preview.actions.openExternal': '在浏览器中打开',
'contextPanel.preview.actions.retry': '重试',
'contextPanel.preview.iframeTitle': '预览',
'contextPanel.preview.invalidUrl': '预览需要有效的 http(s) URL。',
'contextPanel.preview.empty': '没有预览 URL',
'contextPanel.preview.loading': '正在连接预览代理...',
'contextPanel.preview.proxyError': '无法启动预览代理。',
'contextPanel.preview.upstreamUnreachable': '开发服务器无响应。',
'contextPanel.preview.upstreamUnreachableHint': '请确认开发服务器仍在运行,然后重试。',
'terminalView.preview.open': '预览',
'terminalView.preview.openTitle': '打开预览面板',
'sidebarFilesTree.menu.rename': '重命名',
'sidebarFilesTree.menu.copyPath': '复制路径',
'sidebarFilesTree.menu.save': '保存',
@@ -958,6 +972,7 @@ export const dict: Record<I18nKey, string> = {
'header.services.noRateLimitsReported': '未上报速率限制。',
'header.services.used': '已用',
'header.services.remaining': '剩余',
'header.services.shutdownDev': '停止 OpenChamber',
'header.services.modelFamily.other': '其他',
'header.actions.openPlanAria': '打开计划',
'header.actions.planWithShortcut': '计划({shortcut}',
@@ -1330,6 +1345,8 @@ export const dict: Record<I18nKey, string> = {
'chat.messageBody.actions.fork': '从此处分叉',
'chat.messageBody.actions.copyMessageAria': '复制消息文本',
'chat.messageBody.actions.copyMessage': '复制消息',
'chat.messageBody.actions.openPreviewAria': '打开预览',
'chat.messageBody.actions.openPreview': '打开预览',
'chat.messageBody.actions.copyAnswer': '复制回答',
'chat.messageBody.actions.savingImage': '正在保存图片...',
'chat.messageBody.actions.saveAsImage': '保存为图片',
@@ -1383,6 +1400,11 @@ export const dict: Record<I18nKey, string> = {
'chat.chatInput.toast.openSessionFirst': '请先打开一个会话',
'chat.chatInput.toast.togglePermissionAutoAcceptFailed': '切换权限自动接受失败',
'chat.chatInput.reviewComments': '审查评论:',
'chat.chatInput.devServerLogs': 'Dev Server 日志:',
'chat.chatInput.devServerLogsRemove': '移除 Dev Server 日志',
'chat.chatInput.previewAnnotations': '预览注释:',
'chat.chatInput.previewContext': '预览上下文:',
'chat.chatInput.previewContextRemove': '移除预览上下文',
'chat.chatInput.projectRoot': '项目根目录',
'chat.chatInput.branch': '分支',
'chat.chatInput.worktrees': '工作树',
@@ -1763,7 +1785,9 @@ export const dict: Record<I18nKey, string> = {
'projectActions.actions.addActionAria': '添加操作',
'projectActions.actions.addAction': '添加操作',
'projectActions.actions.addNewAction': '添加新操作',
'projectActions.actions.autoDiscover': '自动发现',
'projectActions.actions.chooseActionAria': '选择项目操作',
'projectActions.actions.openPreview': '打开 Preview',
'projectActions.actions.runNamedAria': '运行 {name}',
'projectActions.actions.stopNamedAria': '停止 {name}',
'projectActions.label.fallbackAction': '操作',
@@ -2092,4 +2116,39 @@ export const dict: Record<I18nKey, string> = {
'markdownRenderer.mermaid.actions.copySourceTitle': '复制源码',
'markdownRenderer.mermaid.actions.downloadSvgTitle': '下载 SVG',
'markdownRenderer.mermaid.toast.downloadFailed': '下载图表失败',
'contextPanel.preview.title': '预览',
'contextPanel.preview.description': '使用项目操作或终端 Preview 按钮打开预览。',
'contextPanel.preview.startPreview': '启动预览',
'contextPanel.preview.starting': '正在启动...',
'contextPanel.preview.noDevServer': '未找到开发服务器命令。请配置项目操作或添加 "dev" 脚本到 package.json。',
'contextPanel.preview.startFailed': '启动预览服务器失败。',
'contextPanel.preview.serverExited': '开发服务器意外退出。',
'contextPanel.preview.noUrlDetected': '开发服务器已启动,但未在输出中检测到 URL。请打开预览终端标签页查看日志。',
'contextPanel.preview.serverExitedWithLog': '开发服务器意外退出。最后输出:\n\n{log}',
'contextPanel.preview.noUrlDetectedWithLog': '开发服务器未能响应。最后输出:\n\n{log}',
'contextPanel.preview.startingServer': '正在启动开发服务器...',
'contextPanel.preview.startingServerHint': '正在等待服务器开始接受连接。',
'contextPanel.preview.console.open': '打开预览控制台',
'contextPanel.preview.console.waiting': '正在等待预览控制台',
'contextPanel.preview.console.title': '预览控制台',
'contextPanel.preview.console.attach': '附加',
'contextPanel.preview.console.copy': '复制',
'contextPanel.preview.console.clear': '清除',
'contextPanel.preview.console.empty': '还没有预览控制台事件。',
'contextPanel.preview.console.noFilteredEvents': '没有匹配此筛选器的事件。',
'contextPanel.preview.console.runtimeError': '运行时错误',
'contextPanel.preview.console.copied': '预览控制台已复制',
'contextPanel.preview.console.copyFailed': '无法复制预览控制台',
'contextPanel.preview.console.attached': '预览控制台已附加到聊天',
'contextPanel.preview.console.attachNoSession': '请先打开聊天会话,再附加预览日志',
'contextPanel.preview.console.attachAnnotation': '这些是此项目开发服务器的浏览器控制台日志。',
'contextPanel.preview.inspect.toggle': '检查预览元素',
'contextPanel.preview.inspect.attached': '预览注释已附加到聊天',
'contextPanel.preview.inspect.attachNoSession': '请先打开聊天会话,再附加预览注释',
'contextPanel.preview.inspect.attachAnnotation': '这是内置预览中选中的 DOM 元素。',
'contextPanel.preview.inspect.attachAnnotationWithScreenshot': '这是内置预览中选中的 DOM 元素。已附加带有高亮选中元素的可见预览区域截图。',
'contextPanel.preview.console.filter.all': '全部',
'contextPanel.preview.console.filter.errors': '错误',
'contextPanel.preview.console.filter.warnings': '警告',
'contextPanel.preview.console.filter.logs': '日志',
};
+13 -1
View File
@@ -11,6 +11,14 @@ export function formatInlineCommentDraft(draft: InlineCommentDraft): string {
if (draft.source === 'diff' && side) {
return `Comment on \`${fileLabel}\` lines ${startLine}-${endLine} (${side}):\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
}
if (draft.source === 'preview-console') {
return `Attached preview context from \`${fileLabel}\`:\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
}
if (draft.source === 'preview-annotation') {
return text ? `${code}\n\n${text}` : code;
}
// Plan and file format (no side)
return `Comment on \`${fileLabel}\` lines ${startLine}-${endLine}:\n\`\`\`${language}\n${code}\n\`\`\`\n\n${text}`;
@@ -22,7 +30,11 @@ export function formatInlineCommentDraft(draft: InlineCommentDraft): string {
*/
export function formatInlineCommentDrafts(drafts: InlineCommentDraft[]): string {
if (drafts.length === 0) return '';
if (drafts.every((draft) => draft.source === 'preview-annotation')) {
return drafts.map(formatInlineCommentDraft).join('\n\n---\n\n');
}
return drafts.map(formatInlineCommentDraft).join('\n\n');
}
+10 -2
View File
@@ -169,7 +169,12 @@ const readTextFile = async (path: string): Promise<string | null> => {
}
try {
const response = await fetch(`${getBaseUrl()}/fs/read?path=${encodeURIComponent(path)}`);
const response = await fetch(`${getBaseUrl()}/fs/read?path=${encodeURIComponent(path)}`,
{
// Avoid conditional requests (304 + empty body).
cache: 'no-store',
}
);
if (!response.ok) {
return null;
}
@@ -201,7 +206,10 @@ const resolveHomeDirectory = async (): Promise<string | null> => {
// In some runtimes, window.__OPENCHAMBER_HOME__ can be workspace/project-root
// scoped, which would incorrectly route writes into the project directory.
try {
const response = await fetch(`${getBaseUrl()}/fs/home`);
const response = await fetch(`${getBaseUrl()}/fs/home`, {
// Avoid conditional requests (304 + empty body).
cache: 'no-store',
});
if (!response.ok) {
throw new Error('Failed to resolve home directory from API');
}
+51
View File
@@ -28,6 +28,57 @@ export const isExternalHttpUrl = (url: string): boolean => {
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
};
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1']);
/**
* Returns true when the URL is an http(s) URL pointing at a loopback host
* (localhost, 127.0.0.1, 0.0.0.0, ::1). Used to decide whether to offer an in-app
* preview pane instead of opening the system browser.
*/
export const isLoopbackHttpUrl = (url: string): boolean => {
const parsed = parseUrlSafely(url.trim());
if (!parsed) {
return false;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
return LOOPBACK_HOSTNAMES.has(parsed.hostname.toLowerCase());
};
const LOOPBACK_URL_PATTERN
// eslint-disable-next-line no-control-regex
= /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d{2,5})?(?:\/[^\s<>"'`\u0000-\u001f]*)?/gi;
/**
* Extracts loopback http(s) URLs from a free-text string. Returns unique URLs
* in order of first appearance. Trailing punctuation that is unlikely to be
* part of a real URL is stripped.
*/
export const extractLoopbackUrls = (text: string): string[] => {
if (!text) {
return [];
}
const matches = text.match(LOOPBACK_URL_PATTERN);
if (!matches || matches.length === 0) {
return [];
}
const seen = new Set<string>();
const out: string[] = [];
for (const raw of matches) {
const cleaned = raw.replace(/[),.;:!?'"`]+$/g, '');
if (!cleaned || !isLoopbackHttpUrl(cleaned)) {
continue;
}
if (seen.has(cleaned)) {
continue;
}
seen.add(cleaned);
out.push(cleaned);
}
return out;
};
/**
* Opens an external URL in the system browser.
* In Tauri desktop runtime, uses tauri.shell.open() for proper handling.