Merge origin/main into deferred OpenCode restart branch

This commit is contained in:
Bohdan Triapitsyn
2026-08-07 10:08:50 +03:00
218 changed files with 12131 additions and 1293 deletions
@@ -0,0 +1,28 @@
import { describe, expect, test } from 'bun:test';
import {
FilesystemError,
isFilesystemError,
parseFilesystemErrorReason,
} from './files-errors';
describe('FilesystemError', () => {
test('retains a stable reason and HTTP status', () => {
const error = new FilesystemError('Access denied', {
reason: 'os-permission',
status: 403,
});
expect(isFilesystemError(error)).toBe(true);
expect(error.name).toBe('FilesystemError');
expect(error.message).toBe('Access denied');
expect(error.reason).toBe('os-permission');
expect(error.status).toBe(403);
});
test('normalizes unsupported response reasons to unknown', () => {
expect(parseFilesystemErrorReason('os-permission')).toBe('os-permission');
expect(parseFilesystemErrorReason('made-up')).toBe('unknown');
expect(parseFilesystemErrorReason(undefined)).toBe('unknown');
});
});
+40
View File
@@ -0,0 +1,40 @@
export type FilesystemErrorReason =
| 'os-permission'
| 'not-found'
| 'not-directory'
| 'invalid-response'
| 'unknown';
export class FilesystemError extends Error {
readonly reason: FilesystemErrorReason;
readonly status?: number;
constructor(message: string, options: { reason?: FilesystemErrorReason; status?: number } = {}) {
super(message);
this.name = 'FilesystemError';
this.reason = options.reason ?? 'unknown';
this.status = options.status;
}
}
export const isFilesystemError = (error: unknown): error is FilesystemError => (
error instanceof FilesystemError
|| Boolean(
error
&& typeof error === 'object'
&& 'reason' in error
&& typeof (error as { reason?: unknown }).reason === 'string'
)
);
export const parseFilesystemErrorReason = (value: unknown): FilesystemErrorReason => {
switch (value) {
case 'os-permission':
case 'not-found':
case 'not-directory':
case 'invalid-response':
return value;
default:
return 'unknown';
}
};
+1
View File
@@ -183,6 +183,7 @@ export interface GitBranch {
all: string[];
current: string;
branches: Record<string, GitBranchDetails>;
defaultBranches?: Record<string, string>;
}
interface GitCommitSummary {
@@ -4,6 +4,7 @@ import {
buildPairingConnectionPayload,
encodePairingConnectionPayload,
parsePairingConnectionPayload,
parsePairingConnectionPayloadString,
} from './connectionPayload';
const hostEncPubJwk = { kty: 'EC', crv: 'P-256', x: 'eHhY', y: 'eVlZ' } as const;
@@ -103,3 +104,45 @@ describe('connection payload helpers', () => {
expect(parsePairingConnectionPayload(`openchamber://connect?v=2&p=${expired}`)).toBeNull();
});
});
describe('parsePairingConnectionPayloadString (Android WebView fallback)', () => {
const payload = buildPairingConnectionPayload({
pairingId: 'pair_123',
secret: 'one-time-secret',
label: 'Desktop',
candidates: [
{ type: 'lan', url: 'http://192.168.1.20:4096', priority: 20 },
{ type: 'relay', relayUrl: 'wss://relay.example/ws', serverId: 'srv_abc', hostEncPubJwk, priority: 30 },
],
});
const encoded = encodePairingConnectionPayload(payload);
test('parses the canonical link identically to the URL-based parser', () => {
// Old Android WebViews resolve the same string with hostname "" / pathname "//connect";
// the string parser must not depend on the URL API to succeed.
expect(parsePairingConnectionPayloadString(encoded)).toEqual(parsePairingConnectionPayload(encoded));
});
test('recovers a link whose scheme/host case the URL parser would reject', () => {
const mixedCase = encoded.replace('openchamber://connect', 'OpenChamber://CONNECT');
expect(parsePairingConnectionPayload(mixedCase)).toBeNull();
expect(parsePairingConnectionPayloadString(mixedCase)).toEqual(parsePairingConnectionPayload(encoded));
});
test('tolerates a trailing slash and reordered query params', () => {
const trailingSlash = encoded.replace('openchamber://connect?', 'openchamber://connect/?');
expect(parsePairingConnectionPayloadString(trailingSlash)).toEqual(parsePairingConnectionPayload(encoded));
const p = encoded.slice(encoded.indexOf('p=') + 2);
expect(parsePairingConnectionPayloadString(`openchamber://connect?p=${p}&v=2`)).toEqual(parsePairingConnectionPayload(encoded));
});
test('still rejects non-pairing and malformed payloads', () => {
expect(parsePairingConnectionPayloadString('')).toBeNull();
expect(parsePairingConnectionPayloadString('hello world')).toBeNull();
expect(parsePairingConnectionPayloadString('openchamber://connect')).toBeNull();
expect(parsePairingConnectionPayloadString('openchamber:///connect?v=2&p=x')).toBeNull();
expect(parsePairingConnectionPayloadString('openchamber://connect?v=1&server=http%3A%2F%2F192.168.1.10%3A2606&token=t')).toBeNull();
expect(parsePairingConnectionPayloadString('openchamber://connect?v=2&p=not-json')).toBeNull();
});
});
+32
View File
@@ -212,3 +212,35 @@ export const parsePairingConnectionPayload = (value: string): PairingConnectionP
return null;
}
};
// URL-string-only sibling of parsePairingConnectionPayload. Old Android WebViews
// (e.g. WebView 114) mis-parse non-special schemes: `new URL('openchamber://connect?...')`
// yields hostname "" and pathname "//connect", so the URL-based parser above rejects a
// perfectly valid pairing link. This parser never touches the URL/URLSearchParams APIs —
// it matches the head with a regex and reads `v`/`p` straight off the query string.
// Used by the Android QR-scan path after the standard parse fails; keeps every existing
// validation (version, payload length, base64url, candidate normalization).
export const parsePairingConnectionPayloadString = (value: string): PairingConnectionPayload | null => {
const trimmed = value.trim();
if (!trimmed || trimmed.length > MAX_PAIRING_PAYLOAD_LENGTH) return null;
const question = trimmed.indexOf('?');
if (question === -1 || !/^openchamber:\/\/connect\/?$/i.test(trimmed.slice(0, question))) return null;
let version: string | null = null;
let encoded: string | null = null;
for (const part of trimmed.slice(question + 1).split('&')) {
const eq = part.indexOf('=');
if (eq === -1) continue;
const key = part.slice(0, eq);
const value_ = part.slice(eq + 1);
if (key === 'v') version = value_;
else if (key === 'p') encoded = value_;
}
if (version !== '2' || !encoded || encoded.length > MAX_PAIRING_PAYLOAD_LENGTH) return null;
const decoded = base64UrlDecode(encoded);
if (!decoded || decoded.length > MAX_PAIRING_PAYLOAD_LENGTH) return null;
try {
return normalizePairingPayload(JSON.parse(decoded) as unknown);
} catch {
return null;
}
};
+7 -2
View File
@@ -531,6 +531,10 @@ export const isDesktopShell = (): boolean => {
return isElectronShell();
};
export const canRequestNativeDirectoryAccess = (): boolean => (
isDesktopShell() && hasDesktopInvoke() && isDesktopLocalOriginActive()
);
export const startDesktopWindowDrag = async (): Promise<boolean> => {
if (!isDesktopShell()) {
return false;
@@ -586,12 +590,13 @@ export const requestDirectoryAccess = async (
directoryPath: string
): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => {
// Desktop shell on local instance: use native folder picker.
if (hasDesktopInvoke() && isDesktopLocalOriginActive()) {
if (canRequestNativeDirectoryAccess()) {
try {
const selected = await getDesktopBridge()?.openDialog?.({
directory: true,
multiple: false,
title: 'Select Working Directory',
...(directoryPath ? { defaultPath: directoryPath } : {}),
});
if (!selected || typeof selected !== 'string') {
return { success: false, error: 'Directory selection cancelled' };
@@ -603,7 +608,7 @@ export const requestDirectoryAccess = async (
}
}
return { success: true, path: directoryPath };
return { success: false, error: 'Native directory picker not available' };
};
const isDesktopFileGrantResult = (
@@ -457,6 +457,9 @@ export const settingsDict = {
'settings.openchamber.about.title': 'Über OpenChamber',
'settings.openchamber.about.field.version': 'Version',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode-Version',
'settings.openchamber.about.field.instanceUrls': 'Instanz-URLs',
'settings.openchamber.about.field.applicationUrl': 'Anwendung',
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
'settings.openchamber.about.state.checking': 'Wird geprüft...',
'settings.openchamber.about.state.upToDate': 'Aktuell',
'settings.openchamber.about.state.unknown': 'unbekannt',
@@ -1055,6 +1058,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Rechte Seitenleiste umschalten',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git-Tab der rechten Seitenleiste öffnen',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Datei-Tab der rechten Seitenleiste öffnen',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Kontextpanel-Oberfläche wechseln',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Neue Sitzung',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Neuer Worktree-Entwurf',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Neues Mini-Chat-Fenster',
@@ -1333,6 +1338,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth-Methode {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'Autorisierungscode einfügen',
'settings.providers.page.auth.oauth.starting': 'Autorisierung wird gestartet …',
'settings.providers.page.auth.oauth.waiting': 'Warten auf Autorisierung …',
'settings.providers.page.auth.oauth.waitingHint': 'Schließen Sie die Anmeldung im Browser ab. Lassen Sie diese Seite geöffnet die Verbindung wird von selbst hergestellt.',
'settings.providers.page.auth.oauth.codeHint': 'Kopieren Sie den Autorisierungscode aus dem Browser und fügen Sie ihn hier ein.',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'Gerätecode',
'settings.providers.page.auth.oauth.linkLabel': 'Autorisierungslink',
'settings.providers.page.auth.oauth.promptRequired': 'Füllen Sie „{field}“ aus, um fortzufahren',
'settings.providers.page.auth.oauth.error.sessionExpired': 'Die Autorisierungsanfrage ist abgelaufen. Verbinden Sie erneut, um sie neu zu starten.',
'settings.providers.page.auth.oauth.error.codeRequired': 'Dieser Anbieter benötigt den Autorisierungscode aus Ihrem Browser.',
'settings.providers.page.auth.oauth.error.declined': 'Die Autorisierung wurde abgelehnt oder nicht abgeschlossen.',
'settings.providers.page.auth.oauth.error.invalidInput': 'Die eingegebenen Angaben wurden abgelehnt.',
'settings.providers.page.auth.connected': 'Verbunden',
'settings.providers.page.auth.incomplete': 'Anmeldedaten fehlen',
'settings.providers.page.auth.incompleteHint': '· Fügen Sie einen API-Schlüssel oder {env:VAR} hinzu, bevor Sie diesen Anbieter im Chat verwenden',
@@ -1363,6 +1379,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': 'Öffnen',
'settings.providers.page.actions.copy': 'Kopieren',
'settings.providers.page.actions.complete': 'Vervollständigen',
'settings.providers.page.actions.continue': 'Weiter',
'settings.providers.page.actions.cancel': 'Abbrechen',
'settings.providers.page.actions.tryAgain': 'Wiederholen',
'settings.providers.page.actions.hide': 'Ausblenden',
'settings.providers.page.actions.reconnect': 'Erneut verbinden',
'settings.providers.page.actions.edit': 'Bearbeiten',
@@ -1377,7 +1396,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API-Schlüssel gespeichert',
'settings.providers.page.toast.oauthStartFailed': 'Fehler beim Starten des OAuth-Flows',
'settings.providers.page.toast.oauthDetailsMissing': 'Keine OAuth-Details zurückgegeben',
'settings.providers.page.toast.completeOAuthInBrowser': 'Schließen Sie den OAuth-Flow in Ihrem Browser ab',
'settings.providers.page.toast.oauthCompleteFailed': 'Fehler beim Abschließen des OAuth-Flows',
'settings.providers.page.toast.oauthCompleted': 'OAuth-Verbindung abgeschlossen',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth-Link kopiert',
+29 -4
View File
@@ -248,6 +248,9 @@ export const dict = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': '{taskName} pausieren',
'sessions.scheduledTasks.dialog.taskToggle.enabled': 'Aktiviert',
'sessions.scheduledTasks.dialog.taskToggle.paused': 'Pausiert',
'sessions.scheduledTasks.dialog.loopFile.note': 'Von Loop-Datei verwaltet {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'Aktiviert wird durch die Loop-Datei gesteuert; setze enabled im Markdown-Frontmatter',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Loop-Aufgaben werden in ihrer .agents/loops-Markdown-Datei konfiguriert',
'sessions.scheduledTasks.editor.title.edit': 'Geplante Aufgabe bearbeiten',
'sessions.scheduledTasks.editor.title.new': 'Neue geplante Aufgabe',
'sessions.scheduledTasks.editor.description': 'Konfigurieren Sie eine serverseitige Aufgabe, die eine neue Sitzung erstellt und eine Eingabeaufforderung sendet.',
@@ -473,6 +476,10 @@ export const dict = {
'sessions.sidebar.session.status.unread': 'Ungelesene Updates',
'sessions.sidebar.session.status.pinned': 'Angeheftete Sitzung',
'sessions.sidebar.session.status.permissionRequired': 'Berechtigung erforderlich',
'sessions.sidebar.session.status.questionPendingSingle': '1 ausstehende Frage',
'sessions.sidebar.session.status.questionPendingMany': '{count} ausstehende Fragen',
'sessions.sidebar.session.status.activeFor': 'Seit {duration} aktiv',
'sessions.sidebar.session.status.lastTurnDuration': 'Letzter Durchlauf dauerte {duration}',
'sessions.sidebar.session.subsessions.collapse': 'Untersitzungen einklappen',
'sessions.sidebar.session.subsessions.expand': 'Untersitzungen ausklappen',
'sessions.sidebar.dialogs.deleteSession.title': 'Sitzung löschen?',
@@ -1492,6 +1499,10 @@ export const dict = {
'directoryExplorerDialog.browse.directories': 'Verzeichnisse',
'directoryExplorerDialog.browse.loading': 'Lade Verzeichnisse...',
'directoryExplorerDialog.browse.empty': 'Keine passenden Verzeichnisse.',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber benötigt Zugriff auf diesen Ordner.',
'directoryExplorerDialog.browse.loadFailed': 'Dieser Ordner konnte nicht geladen werden.',
'directoryExplorerDialog.browse.grantAccess': 'Zugriff gewähren',
'directoryExplorerDialog.browse.retry': 'Erneut versuchen',
'directoryExplorerDialog.browse.parentDirectory': 'Übergeordnetes Verzeichnis',
'directoryExplorerDialog.browse.addedBadge': 'Hinzugefügt',
'directoryExplorerDialog.browse.quickAdd': 'Hinzufügen',
@@ -1558,7 +1569,7 @@ export const dict = {
'helpDialog.item.toggleTerminalExpanded': 'Terminal erweitert umschalten',
'helpDialog.item.togglePlanContextPanel': 'Plan-Kontext-Panel umschalten',
'helpDialog.item.cycleTheme': 'Thema wechseln (Hell → Dunkel → System)',
'helpDialog.item.switchProject': 'Projekt wechseln',
'helpDialog.item.switchContextSurface': 'Kontextpanel-Oberfläche wechseln (Zahlentaste)',
'helpDialog.item.toggleServicesMenu': 'Dienstemenü umschalten',
'helpDialog.item.cycleServicesTab': 'Dienste-Registerkarte durchgehen',
'helpDialog.item.openSettings': 'Einstellungen öffnen',
@@ -2728,6 +2739,9 @@ export const dict = {
'common.relative.daysAgoCompact': '{count}d her',
'common.relative.weeksAgoCompact': '{count}w her',
'common.relative.yearsAgoCompact': '{count}y her',
'common.duration.secondsCompact': '{seconds}s',
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
'contextFileOpen.failure.tooLarge': 'Datei ist zu groß zum Öffnen (>{count} Zeilen)',
'contextFileOpen.failure.missing': 'Datei nicht gefunden',
'contextFileOpen.failure.unreadable': 'Fehler beim Öffnen der Datei',
@@ -2823,6 +2837,10 @@ export const dict = {
'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.',
'contextRail.surface.editor.description': 'Bearbeitungskontext',
'contextRail.surface.git.description': 'Git-Kontext',
'contextRail.surface.git.changesCountAriaSingle': '{label}, {count} geänderte Datei',
'contextRail.surface.git.changesCountAriaPlural': '{label}, {count} geänderte Dateien',
'contextRail.surface.git.changesCountTooltipSingle': '{count} geänderte Datei',
'contextRail.surface.git.changesCountTooltipPlural': '{count} geänderte Dateien',
'contextRail.surface.terminal.description': 'Terminal-Kontext',
'contextRail.surface.diff.description': 'Diff-Kontext',
'contextPanel.mode.walkthrough': 'Walkthrough',
@@ -2853,17 +2871,20 @@ export const dict = {
'walkthrough.empty.title': 'Noch nichts vorhanden',
'walkthrough.empty.description': 'Wählen Sie Inhalte aus, um einen Walkthrough zu erstellen.',
'walkthrough.stale.banner': 'Der Code hat sich nach diesem Review geändert. Veraltete Schritte: {count}',
'walkthrough.stop.staleAll': 'Alle veralteten Inhalte stoppen',
'walkthrough.stop.staleAll': 'Der gesamte Code, den dieser Schritt beschrieben hat, hat sich geändert.',
'walkthrough.stop.stalePartial': 'Ein Teil des vom Schritt beschriebenen Codes hat sich geändert. Fehlende Teile: {count}',
'walkthrough.stop.staleShort': 'Veraltete stoppen',
'walkthrough.stop.staleShort': 'Veraltet',
'walkthrough.stop.noCode': 'Kein Code vorhanden',
'walkthrough.uncovered.title': 'Vom Review ausgelassene Änderungen: {count}',
'walkthrough.uncovered.description': 'Diese Bereiche wurden noch nicht in den Walkthrough aufgenommen.',
'walkthrough.toc.moreFiles': 'Weitere Dateien: {count}',
'walkthrough.toc.uncovered': 'Nicht abgedeckt: {count}',
'walkthrough.toc.resize': 'Größe ändern',
'walkthrough.importance.critical': 'Kritisch',
'walkthrough.importance.critical': 'Kernänderung',
'walkthrough.importance.criticalHint': 'Dieser Schritt trägt die eigentliche Änderung, lesen Sie ihn genau. Es ist kein in Ihrem Code gefundenes Problem.',
'walkthrough.importance.context': 'Kontext',
'walkthrough.importance.contextHint': 'Eine unterstützende Änderung, damit der Rest verständlich bleibt.',
'walkthrough.help.guide': 'So funktionieren Walkthroughs',
'walkthrough.blocked.noModel.title': 'Kein Modell ausgewählt',
'walkthrough.blocked.noModel.description': 'Wählen Sie zuerst ein Modell aus.',
'walkthrough.blocked.emptyDiff.title': 'Kein Diff vorhanden',
@@ -2878,6 +2899,8 @@ export const dict = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': 'Das kleine Modell hat sein gesamtes Ausgabelimit fürs Nachdenken verbraucht und nichts zurückgegeben. Denkende Modelle tun das bei großen Diffs oft — ein Modell, das weniger denkt, oder ein schmalerer Review-Bereich reicht eher aus.',
'walkthrough.blocked.onlyGenerated.title': 'Nur generierter Inhalt',
'walkthrough.blocked.onlyGenerated.description': 'Es ist nur generierter Inhalt vorhanden.',
'walkthrough.blocked.serverUnsupported.title': 'Dieser Server unterstützt keine Walkthroughs',
'walkthrough.blocked.serverUnsupported.description': 'Der OpenChamber-Server, mit dem diese App verbunden ist, hat die Walkthrough-API nicht beantwortet — er ist also älter als die App. Aktualisieren Sie den Server auf 1.18 oder neuer und aktualisieren Sie dann die Ansicht.',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': 'Das kleine Modell passt in etwa {available}K Zeichen, und dieser Diff braucht etwa {required}K. Nichts wird abgeschnitten — wähle stattdessen ein Modell mit größerem Kontext.',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Das kleine Modell unterstützt die strukturierten Antworten nicht, die ein Walkthrough benötigt.',
'contextRail.surface.plan.description': 'Plankontext',
@@ -2905,6 +2928,8 @@ export const dict = {
'sessions.sidebar.group.empty.loadingSessions': 'Sitzungen werden geladen...',
'sessions.sidebar.group.empty.loadFailed': 'Sitzungen konnten nicht geladen werden',
'sessions.sidebar.group.empty.retry': 'Erneut versuchen',
'sessions.sidebar.group.empty.permissionDenied': 'Ordnerzugriff ist erforderlich.',
'sessions.sidebar.group.empty.grantAccess': 'Zugriff gewähren',
'chat.messageBody.actions.pinContext': 'Kontext anheften',
'chat.messageBody.actions.unpinContext': 'Kontext lösen',
'chat.messageBody.actions.contextPinFailed': 'Kontext konnte nicht angeheftet werden',
@@ -476,6 +476,9 @@ export const settingsDict = {
'settings.openchamber.about.title': 'About OpenChamber',
'settings.openchamber.about.field.version': 'Version',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode version',
'settings.openchamber.about.field.instanceUrls': 'Instance URLs',
'settings.openchamber.about.field.applicationUrl': 'Application',
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
'settings.openchamber.about.state.checking': 'Checking...',
'settings.openchamber.about.state.upToDate': 'Up to date',
'settings.openchamber.about.state.unknown': 'unknown',
@@ -1120,6 +1123,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Toggle context panel',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Open Git surface',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Open Files surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Switch context panel surface',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
@@ -1398,6 +1403,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth method {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'Paste authorization code',
'settings.providers.page.auth.oauth.starting': 'Starting authorization…',
'settings.providers.page.auth.oauth.waiting': 'Waiting for authorization…',
'settings.providers.page.auth.oauth.waitingHint': 'Finish signing in in your browser. Keep this page open — the connection completes on its own.',
'settings.providers.page.auth.oauth.codeHint': 'Copy the authorization code from your browser and paste it here.',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'Device code',
'settings.providers.page.auth.oauth.linkLabel': 'Authorization link',
'settings.providers.page.auth.oauth.promptRequired': 'Fill in “{field}” to continue',
'settings.providers.page.auth.oauth.error.sessionExpired': 'The authorization request expired. Connect again to restart it.',
'settings.providers.page.auth.oauth.error.codeRequired': 'This provider needs the authorization code from your browser.',
'settings.providers.page.auth.oauth.error.declined': 'Authorization was declined or did not complete.',
'settings.providers.page.auth.oauth.error.invalidInput': 'The details you entered were rejected.',
'settings.providers.page.auth.connected': 'Connected',
'settings.providers.page.auth.incomplete': 'Credentials missing',
'settings.providers.page.auth.incompleteHint': '· Add an API key or {env:VAR} before using this provider in chat',
@@ -1428,6 +1444,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': 'Open',
'settings.providers.page.actions.copy': 'Copy',
'settings.providers.page.actions.complete': 'Complete',
'settings.providers.page.actions.continue': 'Continue',
'settings.providers.page.actions.cancel': 'Cancel',
'settings.providers.page.actions.tryAgain': 'Try again',
'settings.providers.page.actions.hide': 'Hide',
'settings.providers.page.actions.reconnect': 'Reconnect',
'settings.providers.page.actions.edit': 'Edit',
@@ -1442,7 +1461,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API key saved',
'settings.providers.page.toast.oauthStartFailed': 'Failed to start OAuth flow',
'settings.providers.page.toast.oauthDetailsMissing': 'No OAuth details returned',
'settings.providers.page.toast.completeOAuthInBrowser': 'Complete the OAuth flow in your browser',
'settings.providers.page.toast.oauthCompleteFailed': 'Failed to complete OAuth flow',
'settings.providers.page.toast.oauthCompleted': 'OAuth connection completed',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth link copied',
+27 -2
View File
@@ -268,6 +268,9 @@ export const dict = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': 'Pause {taskName}',
'sessions.scheduledTasks.dialog.taskToggle.enabled': 'Enabled',
'sessions.scheduledTasks.dialog.taskToggle.paused': 'Paused',
'sessions.scheduledTasks.dialog.loopFile.note': 'Managed by loop file {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'Enabled is controlled by the loop file; set enabled in the markdown frontmatter',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Loop tasks are configured in their .agents/loops markdown file',
'sessions.scheduledTasks.editor.title.edit': 'Edit scheduled task',
'sessions.scheduledTasks.editor.title.new': 'New scheduled task',
'sessions.scheduledTasks.editor.description': 'Configure a server-side task that creates a new session and sends a prompt.',
@@ -530,6 +533,10 @@ export const dict = {
'sessions.sidebar.session.status.pinned': 'Pinned session',
'sessions.sidebar.session.status.movingToWorktree': 'Moving session to a new worktree',
'sessions.sidebar.session.status.permissionRequired': 'Permission required',
'sessions.sidebar.session.status.questionPendingSingle': '1 pending question',
'sessions.sidebar.session.status.questionPendingMany': '{count} pending questions',
'sessions.sidebar.session.status.activeFor': 'Active for {duration}',
'sessions.sidebar.session.status.lastTurnDuration': 'Last turn took {duration}',
'sessions.sidebar.session.subsessions.collapse': 'Collapse subsessions',
'sessions.sidebar.session.subsessions.expand': 'Expand subsessions',
'sessions.sidebar.dialogs.deleteSession.title': 'Delete session?',
@@ -1104,6 +1111,10 @@ export const dict = {
'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.',
'contextRail.surface.editor.description': 'Edit project files',
'contextRail.surface.git.description': 'Commits, branches, and pull requests',
'contextRail.surface.git.changesCountAriaSingle': '{label}, {count} changed file',
'contextRail.surface.git.changesCountAriaPlural': '{label}, {count} changed files',
'contextRail.surface.git.changesCountTooltipSingle': '{count} changed file',
'contextRail.surface.git.changesCountTooltipPlural': '{count} changed files',
'contextRail.surface.terminal.description': 'Built-in terminal',
'contextRail.surface.diff.description': 'Review working changes',
'contextPanel.mode.walkthrough': 'Walkthrough',
@@ -1143,8 +1154,11 @@ export const dict = {
'walkthrough.toc.moreFiles': 'More files: {count}',
'walkthrough.toc.uncovered': 'Not covered: {count}',
'walkthrough.toc.resize': 'Resize the contents column',
'walkthrough.importance.critical': 'Critical',
'walkthrough.importance.critical': 'Key change',
'walkthrough.importance.criticalHint': 'This step drives the rest of the change, so read it closely. It is not a problem found in your code.',
'walkthrough.importance.context': 'Context',
'walkthrough.importance.contextHint': 'A supporting change, included so the rest makes sense.',
'walkthrough.help.guide': 'How walkthroughs work',
'walkthrough.blocked.noModel.title': 'No small model available',
'walkthrough.blocked.noModel.description': 'Sign in to a model provider to generate a review.',
'walkthrough.blocked.emptyDiff.title': 'Nothing to review',
@@ -1159,6 +1173,8 @@ export const dict = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': 'The small model spent its whole output allowance on reasoning and returned nothing. Reasoning models often do this on large diffs — a model that thinks less, or reviewing a narrower scope, will get through.',
'walkthrough.blocked.onlyGenerated.title': 'Only generated files changed',
'walkthrough.blocked.onlyGenerated.description': 'Every change here is a lockfile or other tool-produced output, which the review deliberately skips.',
'walkthrough.blocked.serverUnsupported.title': 'This server has no walkthrough support',
'walkthrough.blocked.serverUnsupported.description': 'The OpenChamber server this app is connected to did not answer the walkthrough API, which means it is older than the app. Update the server to 1.18 or newer, then refresh.',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': 'The small model fits about {available}K characters and this diff needs about {required}K. Nothing gets truncated — pick a model with a larger context instead.',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'The small model does not support the structured responses a walkthrough needs.',
'contextRail.surface.plan.description': 'View the current plan',
@@ -1640,6 +1656,10 @@ export const dict = {
'directoryExplorerDialog.browse.directories': 'Directories',
'directoryExplorerDialog.browse.loading': 'Loading directories...',
'directoryExplorerDialog.browse.empty': 'No matching directories.',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber needs access to this folder.',
'directoryExplorerDialog.browse.loadFailed': 'Could not load this folder.',
'directoryExplorerDialog.browse.grantAccess': 'Grant access',
'directoryExplorerDialog.browse.retry': 'Try again',
'directoryExplorerDialog.browse.parentDirectory': 'Parent directory',
'directoryExplorerDialog.browse.addedBadge': 'Added',
'directoryExplorerDialog.browse.quickAdd': 'Add',
@@ -1705,8 +1725,8 @@ export const dict = {
'helpDialog.item.toggleTerminalDock': 'Toggle Terminal Dock',
'helpDialog.item.toggleTerminalExpanded': 'Toggle Terminal Expanded',
'helpDialog.item.togglePlanContextPanel': 'Toggle Plan Context Panel',
'helpDialog.item.switchContextSurface': 'Switch Context Panel Surface (number key)',
'helpDialog.item.cycleTheme': 'Cycle Theme (Light → Dark → System)',
'helpDialog.item.switchProject': 'Switch Project',
'helpDialog.item.toggleServicesMenu': 'Toggle Services Menu',
'helpDialog.item.cycleServicesTab': 'Cycle Services Tab',
'helpDialog.item.openSettings': 'Open Settings',
@@ -1991,6 +2011,8 @@ export const dict = {
'sessions.sidebar.group.empty.loadingSessions': 'Loading sessions…',
'sessions.sidebar.group.empty.loadFailed': 'Could not refresh sessions.',
'sessions.sidebar.group.empty.retry': 'Try again',
'sessions.sidebar.group.empty.permissionDenied': 'Folder access is required.',
'sessions.sidebar.group.empty.grantAccess': 'Grant access',
'chat.unifiedControls.title': 'Controls',
'chat.unifiedControls.model.title': 'Model',
'chat.unifiedControls.model.noRecent': 'No recent models',
@@ -2895,6 +2917,9 @@ export const dict = {
'common.relative.daysAgoCompact': '{count}d ago',
'common.relative.weeksAgoCompact': '{count}w ago',
'common.relative.yearsAgoCompact': '{count}y ago',
'common.duration.secondsCompact': '{seconds}s',
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
'contextFileOpen.failure.missing': 'File not found',
'contextFileOpen.failure.unreadable': 'Failed to open file',
@@ -444,6 +444,9 @@ export const settingsDict = {
"settings.openchamber.about.title": "Acerca de OpenChamber",
"settings.openchamber.about.field.version": "Versión",
"settings.openchamber.about.field.openCodeVersion": "Versión de OpenCode",
"settings.openchamber.about.field.instanceUrls": "URLs de la instancia",
"settings.openchamber.about.field.applicationUrl": "Aplicación",
"settings.openchamber.about.field.tunnelUrl": "Túnel",
"settings.openchamber.about.state.checking": "Comprobando...",
"settings.openchamber.about.state.upToDate": "Actualizado",
"settings.openchamber.about.state.unknown": "desconocido",
@@ -1088,6 +1091,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar panel de contexto',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superficie de Git',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superficie de archivos',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Cambiar superficie del panel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
@@ -1372,6 +1377,17 @@ export const settingsDict = {
"settings.providers.page.auth.apiKeyPlaceholder": "sk-...",
"settings.providers.page.auth.oauthMethodFallback": "Método OAuth {index}",
"settings.providers.page.auth.pasteAuthorizationCodePlaceholder": "Pegar código de autorización",
"settings.providers.page.auth.oauth.starting": "Iniciando la autorización…",
"settings.providers.page.auth.oauth.waiting": "Esperando la autorización…",
"settings.providers.page.auth.oauth.waitingHint": "Termina de iniciar sesión en el navegador. Mantén esta página abierta: la conexión se completará sola.",
"settings.providers.page.auth.oauth.codeHint": "Copia el código de autorización del navegador y pégalo aquí.",
"settings.providers.page.auth.oauth.deviceCodeLabel": "Código del dispositivo",
"settings.providers.page.auth.oauth.linkLabel": "Enlace de autorización",
"settings.providers.page.auth.oauth.promptRequired": "Completa «{field}» para continuar",
"settings.providers.page.auth.oauth.error.sessionExpired": "La solicitud de autorización caducó. Vuelve a conectar para reiniciarla.",
"settings.providers.page.auth.oauth.error.codeRequired": "Este proveedor necesita el código de autorización de tu navegador.",
"settings.providers.page.auth.oauth.error.declined": "La autorización se rechazó o no se completó.",
"settings.providers.page.auth.oauth.error.invalidInput": "Se rechazaron los datos introducidos.",
"settings.providers.page.auth.connected": "Conectado",
"settings.providers.page.auth.incomplete": "Faltan credenciales",
"settings.providers.page.auth.incompleteHint": "· Añade una clave API o {env:VAR} antes de usar este proveedor en el chat",
@@ -1404,6 +1420,9 @@ export const settingsDict = {
"settings.providers.page.actions.open": "Abrir",
"settings.providers.page.actions.copy": "Copiar",
"settings.providers.page.actions.complete": "Completar",
"settings.providers.page.actions.continue": "Continuar",
"settings.providers.page.actions.cancel": "Cancelar",
"settings.providers.page.actions.tryAgain": "Reintentar",
"settings.providers.page.actions.hide": "Ocultar",
"settings.providers.page.actions.reconnect": "Reconectar",
"settings.providers.page.actions.edit": "Editar",
@@ -1419,7 +1438,6 @@ export const settingsDict = {
"settings.providers.page.toast.apiKeySaved": "Clave API guardada",
"settings.providers.page.toast.oauthStartFailed": "No se pudo iniciar el flujo OAuth",
"settings.providers.page.toast.oauthDetailsMissing": "No se devolvieron detalles de OAuth",
"settings.providers.page.toast.completeOAuthInBrowser": "Completa el flujo OAuth en tu navegador",
"settings.providers.page.toast.oauthCompleteFailed": "No se pudo completar el flujo OAuth",
"settings.providers.page.toast.oauthCompleted": "Conexión OAuth completada",
"settings.providers.page.toast.oauthLinkCopied": "Enlace de OAuth copiado",
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
"sessions.scheduledTasks.dialog.taskToggle.pauseAria": "Pausar {taskName}",
"sessions.scheduledTasks.dialog.taskToggle.enabled": "Habilitado",
"sessions.scheduledTasks.dialog.taskToggle.paused": "Pausado",
'sessions.scheduledTasks.dialog.loopFile.note': 'Gestionada por el archivo de bucle {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'La activación la controla el archivo de bucle; establece enabled en el frontmatter de Markdown',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Las tareas de bucle se configuran en su archivo Markdown .agents/loops',
"sessions.scheduledTasks.editor.title.edit": "Editar tarea programada",
"sessions.scheduledTasks.editor.title.new": "Nueva tarea programada",
"sessions.scheduledTasks.editor.description": "Configura una tarea del lado del servidor que crea una nueva sesión y envía un prompt.",
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.status.pinned": "Sesión anclada",
"sessions.sidebar.session.status.movingToWorktree": "Moviendo la sesión a un worktree nuevo",
"sessions.sidebar.session.status.permissionRequired": "Permiso requerido",
"sessions.sidebar.session.status.questionPendingSingle": "1 pregunta pendiente",
"sessions.sidebar.session.status.questionPendingMany": "{count} preguntas pendientes",
"sessions.sidebar.session.status.activeFor": "Activa desde hace {duration}",
"sessions.sidebar.session.status.lastTurnDuration": "El último turno duró {duration}",
"sessions.sidebar.session.subsessions.collapse": "Colapsar subsesiones",
"sessions.sidebar.session.subsessions.expand": "Expandir subsesiones",
"sessions.sidebar.dialogs.deleteSession.title": "¿Eliminar sesión?",
@@ -1105,6 +1112,10 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.",
"contextRail.surface.editor.description": "Editar archivos del proyecto",
"contextRail.surface.git.description": "Commits, ramas y pull requests",
"contextRail.surface.git.changesCountAriaSingle": "{label}, {count} archivo modificado",
"contextRail.surface.git.changesCountAriaPlural": "{label}, {count} archivos modificados",
"contextRail.surface.git.changesCountTooltipSingle": "{count} archivo modificado",
"contextRail.surface.git.changesCountTooltipPlural": "{count} archivos modificados",
"contextRail.surface.terminal.description": "Terminal integrada",
"contextRail.surface.diff.description": "Revisar cambios en curso",
"contextPanel.mode.walkthrough": "Recorrido",
@@ -1144,8 +1155,11 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.toc.moreFiles": "Más archivos: {count}",
"walkthrough.toc.uncovered": "Sin cubrir: {count}",
"walkthrough.toc.resize": "Cambiar el ancho de la columna de contenidos",
"walkthrough.importance.critical": "Crítico",
"walkthrough.importance.critical": "Cambio clave",
"walkthrough.importance.criticalHint": "Este paso impulsa el resto del cambio, así que léelo con atención. No es un problema detectado en tu código.",
"walkthrough.importance.context": "Contexto",
"walkthrough.importance.contextHint": "Un cambio de apoyo, incluido para que el resto tenga sentido.",
"walkthrough.help.guide": "Cómo funcionan los walkthroughs",
"walkthrough.blocked.noModel.title": "No hay ningún modelo pequeño disponible",
"walkthrough.blocked.noModel.description": "Inicia sesión en un proveedor de modelos para generar una revisión.",
"walkthrough.blocked.emptyDiff.title": "Nada que revisar",
@@ -1160,6 +1174,8 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.outputExhausted.descriptionUnknownModel": "El modelo pequeño gastó todo su margen de salida razonando y no devolvió nada. Los modelos de razonamiento suelen hacerlo con diffs grandes: prueba con un modelo que razone menos o revisa un ámbito más reducido.",
"walkthrough.blocked.onlyGenerated.title": "Solo cambiaron archivos generados",
"walkthrough.blocked.onlyGenerated.description": "Todos los cambios son archivos de bloqueo u otra salida generada por herramientas, que la revisión omite a propósito.",
"walkthrough.blocked.serverUnsupported.title": "Este servidor no admite walkthroughs",
"walkthrough.blocked.serverUnsupported.description": "El servidor de OpenChamber al que está conectada esta app no respondió a la API de walkthrough, así que es más antiguo que la app. Actualiza el servidor a 1.18 o posterior y vuelve a intentarlo.",
"walkthrough.blocked.contextTooSmall.descriptionUnknownModel": "El modelo pequeño admite unos {available} mil caracteres y este diff necesita unos {required} mil. No se recorta nada: elige un modelo con más contexto.",
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "El modelo pequeño no admite las respuestas estructuradas que necesita un recorrido.",
"contextRail.surface.plan.description": "Ver el plan actual",
@@ -1618,6 +1634,10 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.browse.directories": "Directorios",
"directoryExplorerDialog.browse.loading": "Cargando directorios...",
"directoryExplorerDialog.browse.empty": "No hay directorios coincidentes.",
"directoryExplorerDialog.browse.permissionDenied": "OpenChamber necesita acceso a esta carpeta.",
"directoryExplorerDialog.browse.loadFailed": "No se pudo cargar esta carpeta.",
"directoryExplorerDialog.browse.grantAccess": "Permitir acceso",
"directoryExplorerDialog.browse.retry": "Reintentar",
"directoryExplorerDialog.browse.parentDirectory": "Directorio padre",
"directoryExplorerDialog.browse.addedBadge": "Añadido",
"directoryExplorerDialog.browse.quickAdd": "Añadir",
@@ -1684,7 +1704,7 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.toggleTerminalExpanded": "Expandir o contraer terminal",
"helpDialog.item.togglePlanContextPanel": "Alternar panel de contexto del plan",
"helpDialog.item.cycleTheme": "Cambiar tema (Claro → Oscuro → Sistema)",
"helpDialog.item.switchProject": "Cambiar proyecto",
"helpDialog.item.switchContextSurface": "Cambiar superficie del panel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar u ocultar menú de servicios",
"helpDialog.item.cycleServicesTab": "Cambiar pestaña de servicios",
"helpDialog.item.openSettings": "Abrir configuración",
@@ -1969,6 +1989,8 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.group.empty.loadingSessions": "Cargando sesiones…",
"sessions.sidebar.group.empty.loadFailed": "No se pudieron actualizar las sesiones.",
"sessions.sidebar.group.empty.retry": "Reintentar",
"sessions.sidebar.group.empty.permissionDenied": "Se requiere acceso a la carpeta.",
"sessions.sidebar.group.empty.grantAccess": "Permitir acceso",
"chat.unifiedControls.title": "Controles",
"chat.unifiedControls.model.title": "Modelo",
"chat.unifiedControls.model.noRecent": "No hay modelos recientes",
@@ -2896,6 +2918,9 @@ export const dict: Record<I18nKey, string> = {
"common.relative.daysAgoCompact": "{count}d ago",
"common.relative.weeksAgoCompact": "{count}w ago",
"common.relative.yearsAgoCompact": "{count}y ago",
"common.duration.secondsCompact": "{seconds}s",
"common.duration.minutesSecondsCompact": "{minutes}m {seconds}s",
"common.duration.hoursMinutesCompact": "{hours}h {minutes}m",
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
"contextFileOpen.failure.missing": "File not found",
"contextFileOpen.failure.unreadable": "Failed to open file",
@@ -1009,6 +1009,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Afficher/masquer le panneau de contexte',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Ouvrir la surface Git',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Ouvrir la surface Fichiers',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Basculer la surface du panneau contextuel',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nouvelle session',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nouvelle ébauche d\'worktree',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nouvelle fenêtre de mini-chat',
@@ -1293,6 +1295,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'Méthode OAuth {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'Coller le code d\'autorisation',
'settings.providers.page.auth.oauth.starting': 'Démarrage de lautorisation…',
'settings.providers.page.auth.oauth.waiting': 'En attente de lautorisation…',
'settings.providers.page.auth.oauth.waitingHint': 'Terminez la connexion dans votre navigateur. Laissez cette page ouverte : la connexion se finalisera delle-même.',
'settings.providers.page.auth.oauth.codeHint': 'Copiez le code dautorisation depuis votre navigateur et collez-le ici.',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'Code de lappareil',
'settings.providers.page.auth.oauth.linkLabel': 'Lien dautorisation',
'settings.providers.page.auth.oauth.promptRequired': 'Renseignez « {field} » pour continuer',
'settings.providers.page.auth.oauth.error.sessionExpired': 'La demande dautorisation a expiré. Reconnectez-vous pour la relancer.',
'settings.providers.page.auth.oauth.error.codeRequired': 'Ce fournisseur a besoin du code dautorisation de votre navigateur.',
'settings.providers.page.auth.oauth.error.declined': 'Lautorisation a été refusée ou na pas abouti.',
'settings.providers.page.auth.oauth.error.invalidInput': 'Les informations saisies ont été refusées.',
'settings.providers.page.auth.connected': 'Connecté',
'settings.providers.page.auth.incomplete': 'Identifiants manquants',
'settings.providers.page.auth.incompleteHint': '· Ajoutez une clé API ou {env:VAR} avant dutiliser ce fournisseur dans le chat',
@@ -1325,6 +1338,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': 'Ouvrir',
'settings.providers.page.actions.copy': 'Copie',
'settings.providers.page.actions.complete': 'Complet',
'settings.providers.page.actions.continue': 'Continuer',
'settings.providers.page.actions.cancel': 'Annuler',
'settings.providers.page.actions.tryAgain': 'Réessayer',
'settings.providers.page.actions.hide': 'Cacher',
'settings.providers.page.actions.reconnect': 'Reconnecter',
'settings.providers.page.actions.edit': 'Modifier',
@@ -1340,7 +1356,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'Clé API enregistrée',
'settings.providers.page.toast.oauthStartFailed': 'Échec du démarrage du flux OAuth',
'settings.providers.page.toast.oauthDetailsMissing': 'Aucun détail OAuth renvoyé',
'settings.providers.page.toast.completeOAuthInBrowser': 'Complétez le flux OAuth dans votre navigateur',
'settings.providers.page.toast.oauthCompleteFailed': 'Échec de la réalisation du flux OAuth',
'settings.providers.page.toast.oauthCompleted': 'Connexion OAuth terminée',
'settings.providers.page.toast.oauthLinkCopied': 'Lien OAuth copié',
@@ -2057,6 +2072,9 @@ export const settingsDict = {
'settings.remoteInstances.relay.toast.offerFailed': 'Échec de la création du lien dassociation',
'settings.remoteInstances.relay.toast.linkCopied': 'Lien dassociation copié',
'settings.openchamber.about.field.openCodeVersion': 'Version dOpenCode',
'settings.openchamber.about.field.instanceUrls': 'URLs de linstance',
'settings.openchamber.about.field.applicationUrl': 'Application',
'settings.openchamber.about.field.tunnelUrl': 'Tunnel',
'settings.openchamber.about.state.unknown': 'inconnue',
'settings.voice.page.field.ttsInputMode': 'Mode dentrée TTS',
'settings.voice.page.field.ttsInputModeSanitized': 'Nettoyé',
+27 -2
View File
@@ -105,6 +105,9 @@ export const dict = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': 'Pause {taskName}',
'sessions.scheduledTasks.dialog.taskToggle.enabled': 'Activé',
'sessions.scheduledTasks.dialog.taskToggle.paused': 'En pause',
'sessions.scheduledTasks.dialog.loopFile.note': 'Gérée par le fichier de boucle {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': "L'activation est contrôlée par le fichier de boucle ; définissez enabled dans le frontmatter Markdown",
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Les tâches de boucle sont configurées dans leur fichier Markdown .agents/loops',
'sessions.scheduledTasks.editor.title.edit': 'Modifier une tâche planifiée',
'sessions.scheduledTasks.editor.title.new': 'Nouvelle tâche planifiée',
'sessions.scheduledTasks.editor.description': 'Configurez une tâche côté serveur qui crée une nouvelle session et envoie un prompt.',
@@ -366,6 +369,10 @@ export const dict = {
'sessions.sidebar.session.status.pinned': 'Session épinglée',
'sessions.sidebar.session.status.movingToWorktree': 'Déplacement de la session vers un nouveau worktree',
'sessions.sidebar.session.status.permissionRequired': 'Autorisation requise',
'sessions.sidebar.session.status.questionPendingSingle': '1 question en attente',
'sessions.sidebar.session.status.questionPendingMany': '{count} questions en attente',
'sessions.sidebar.session.status.activeFor': 'Active depuis {duration}',
'sessions.sidebar.session.status.lastTurnDuration': 'Le dernier tour a duré {duration}',
'sessions.sidebar.session.subsessions.collapse': 'Réduire les sous-sessions',
'sessions.sidebar.session.subsessions.expand': 'Développer les sous-sessions',
'sessions.sidebar.dialogs.deleteSession.title': 'Supprimer la session ?',
@@ -929,6 +936,10 @@ export const dict = {
'contextPanel.editorEmpty.description': 'Choisissez un fichier dans larborescence pour commencer.',
'contextRail.surface.editor.description': 'Modifier les fichiers du projet',
'contextRail.surface.git.description': 'Commits, branches et pull requests',
'contextRail.surface.git.changesCountAriaSingle': '{label}, {count} fichier modifié',
'contextRail.surface.git.changesCountAriaPlural': '{label}, {count} fichiers modifiés',
'contextRail.surface.git.changesCountTooltipSingle': '{count} fichier modifié',
'contextRail.surface.git.changesCountTooltipPlural': '{count} fichiers modifiés',
'contextRail.surface.terminal.description': 'Terminal intégré',
'contextRail.surface.diff.description': 'Passer en revue les modifications',
'contextPanel.mode.walkthrough': 'Parcours',
@@ -968,8 +979,11 @@ export const dict = {
'walkthrough.toc.moreFiles': 'Autres fichiers : {count}',
'walkthrough.toc.uncovered': 'Non traité : {count}',
'walkthrough.toc.resize': 'Redimensionner la colonne du sommaire',
'walkthrough.importance.critical': 'Critique',
'walkthrough.importance.critical': 'Changement clé',
'walkthrough.importance.criticalHint': "Cette étape porte l'essentiel du changement, lisez-la attentivement. Ce n'est pas un problème détecté dans votre code.",
'walkthrough.importance.context': 'Contexte',
'walkthrough.importance.contextHint': 'Un changement de soutien, présent pour que le reste ait du sens.',
'walkthrough.help.guide': 'Comment fonctionnent les walkthroughs',
'walkthrough.blocked.noModel.title': 'Aucun petit modèle disponible',
'walkthrough.blocked.noModel.description': 'Connectez-vous à un fournisseur de modèles pour générer une revue.',
'walkthrough.blocked.emptyDiff.title': 'Rien à examiner',
@@ -984,6 +998,8 @@ export const dict = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': 'Le petit modèle a dépensé toute sa marge de sortie en raisonnement et na rien renvoyé. Les modèles de raisonnement le font souvent sur de gros diffs : essayez un modèle qui réfléchit moins, ou une portée plus étroite.',
'walkthrough.blocked.onlyGenerated.title': 'Seuls des fichiers générés ont changé',
'walkthrough.blocked.onlyGenerated.description': 'Toutes les modifications concernent des fichiers de verrouillage ou dautres sorties générées, que la revue ignore délibérément.',
'walkthrough.blocked.serverUnsupported.title': 'Ce serveur ne prend pas en charge les walkthroughs',
'walkthrough.blocked.serverUnsupported.description': "Le serveur OpenChamber auquel cette application est connectée n'a pas répondu à l'API walkthrough : il est donc plus ancien que l'application. Mettez le serveur à jour en 1.18 ou plus récent, puis actualisez.",
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': 'Le petit modèle accepte environ {available} k caractères et ce diff en demande environ {required} k. Rien nest tronqué : choisissez un modèle au contexte plus large.',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Le petit modèle ne prend pas en charge les réponses structurées nécessaires à un parcours.',
'contextRail.surface.plan.description': 'Voir le plan actuel',
@@ -1453,6 +1469,10 @@ export const dict = {
'directoryExplorerDialog.browse.directories': 'Annuaires',
'directoryExplorerDialog.browse.loading': 'Chargement des répertoires...',
'directoryExplorerDialog.browse.empty': 'Aucun répertoire correspondant.',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber doit accéder à ce dossier.',
'directoryExplorerDialog.browse.loadFailed': 'Impossible de charger ce dossier.',
'directoryExplorerDialog.browse.grantAccess': 'Autoriser laccès',
'directoryExplorerDialog.browse.retry': 'Réessayer',
'directoryExplorerDialog.browse.parentDirectory': 'Annuaire parent',
'directoryExplorerDialog.browse.addedBadge': 'Ajouté',
'directoryExplorerDialog.browse.quickAdd': 'Ajouter',
@@ -1519,7 +1539,7 @@ export const dict = {
'helpDialog.item.toggleTerminalExpanded': 'Terminal à bascule étendu',
'helpDialog.item.togglePlanContextPanel': 'Toggle Panneau contextuel du plan',
'helpDialog.item.cycleTheme': 'Basculer le thème (clair → sombre → système)',
'helpDialog.item.switchProject': 'Changer de projet',
'helpDialog.item.switchContextSurface': 'Basculer la surface du panneau contextuel (touche numérique)',
'helpDialog.item.toggleServicesMenu': 'Basculer le menu des services',
'helpDialog.item.cycleServicesTab': 'Onglet Services de vélo',
'helpDialog.item.openSettings': 'Ouvrir les paramètres',
@@ -1778,6 +1798,8 @@ export const dict = {
'sessions.sidebar.group.empty.loadingSessions': 'Chargement des sessions…',
'sessions.sidebar.group.empty.loadFailed': 'Impossible dactualiser les sessions.',
'sessions.sidebar.group.empty.retry': 'Réessayer',
'sessions.sidebar.group.empty.permissionDenied': 'Laccès au dossier est requis.',
'sessions.sidebar.group.empty.grantAccess': 'Autoriser laccès',
'chat.unifiedControls.title': 'Contrôles',
'chat.unifiedControls.model.title': 'Modèle',
'chat.unifiedControls.model.noRecent': 'Aucun modèle récent',
@@ -2643,6 +2665,9 @@ export const dict = {
'common.relative.daysAgoCompact': '{count} j',
'common.relative.weeksAgoCompact': '{count} sem',
'common.relative.yearsAgoCompact': '{count} a',
'common.duration.secondsCompact': '{seconds}s',
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
'contextFileOpen.failure.tooLarge': 'Le fichier est trop volumineux pour être ouvert (> {count} lignes)',
'contextFileOpen.failure.missing': 'Fichier introuvable',
'contextFileOpen.failure.unreadable': 'Impossible douvrir le fichier',
@@ -477,6 +477,9 @@ export const settingsDict = {
'settings.openchamber.about.title': 'OpenChamber について',
'settings.openchamber.about.field.version': 'バージョン',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode バージョン',
'settings.openchamber.about.field.instanceUrls': 'インスタンスのURL',
'settings.openchamber.about.field.applicationUrl': 'アプリケーション',
'settings.openchamber.about.field.tunnelUrl': 'トンネル',
'settings.openchamber.about.state.checking': '確認中...',
'settings.openchamber.about.state.upToDate': '最新です',
'settings.openchamber.about.state.unknown': '不明',
@@ -1121,6 +1124,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'コンテキストパネルの表示切替',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git サーフェスを開く',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'ファイルサーフェスを開く',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'コンテキストパネルのサーフェスを切り替え',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新しい Session',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新しい Worktree 下書き',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新しいミニチャットウィンドウ',
@@ -1405,6 +1410,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth 方法 {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': '認証コードを貼り付け',
'settings.providers.page.auth.oauth.starting': '認証を開始しています…',
'settings.providers.page.auth.oauth.waiting': '認証を待っています…',
'settings.providers.page.auth.oauth.waitingHint': 'ブラウザーでサインインを完了してください。このページは開いたままにしてください。接続は自動的に完了します。',
'settings.providers.page.auth.oauth.codeHint': 'ブラウザーから認証コードをコピーして、ここに貼り付けてください。',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'デバイスコード',
'settings.providers.page.auth.oauth.linkLabel': '認証リンク',
'settings.providers.page.auth.oauth.promptRequired': '続行するには「{field}」を入力してください',
'settings.providers.page.auth.oauth.error.sessionExpired': '認証リクエストの有効期限が切れました。もう一度接続してやり直してください。',
'settings.providers.page.auth.oauth.error.codeRequired': 'このプロバイダーにはブラウザーの認証コードが必要です。',
'settings.providers.page.auth.oauth.error.declined': '認証が拒否されたか、完了しませんでした。',
'settings.providers.page.auth.oauth.error.invalidInput': '入力された内容は拒否されました。',
'settings.providers.page.auth.connected': '接続済み',
'settings.providers.page.auth.incomplete': '認証情報が不足しています',
'settings.providers.page.auth.incompleteHint': '· チャットでこのプロバイダーを使う前に API キーまたは {env:VAR} を追加してください',
@@ -1437,6 +1453,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': '開く',
'settings.providers.page.actions.copy': 'コピー',
'settings.providers.page.actions.complete': '完了',
'settings.providers.page.actions.continue': '続行',
'settings.providers.page.actions.cancel': 'キャンセル',
'settings.providers.page.actions.tryAgain': '再試行',
'settings.providers.page.actions.hide': '非表示',
'settings.providers.page.actions.reconnect': '再接続',
'settings.providers.page.actions.edit': '編集',
@@ -1452,7 +1471,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API キーを保存しました',
'settings.providers.page.toast.oauthStartFailed': 'OAuth フローの開始に失敗しました',
'settings.providers.page.toast.oauthDetailsMissing': 'OAuth の詳細が返されませんでした',
'settings.providers.page.toast.completeOAuthInBrowser': 'ブラウザで OAuth フローを完了してください',
'settings.providers.page.toast.oauthCompleteFailed': 'OAuth フローの完了に失敗しました',
'settings.providers.page.toast.oauthCompleted': 'OAuth 接続が完了しました',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth リンクをコピーしました',
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': '{taskName}を一時停止',
'sessions.scheduledTasks.dialog.taskToggle.enabled': '有効',
'sessions.scheduledTasks.dialog.taskToggle.paused': '一時停止中',
'sessions.scheduledTasks.dialog.loopFile.note': 'ループファイル {file} によって管理',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': '有効状態はループファイルが制御します。Markdown フロントマターで enabled を設定してください',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'ループタスクは .agents/loops の Markdown ファイルで設定します',
'sessions.scheduledTasks.editor.title.edit': 'スケジュールタスクを編集',
'sessions.scheduledTasks.editor.title.new': '新しいスケジュールタスク',
'sessions.scheduledTasks.editor.description': '新しいセッションを作成しプロンプトを送信するサーバーサイドタスクを設定します。',
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.status.pinned': 'ピン留めされたセッション',
'sessions.sidebar.session.status.movingToWorktree': 'セッションを新しいworktreeへ移動中',
'sessions.sidebar.session.status.permissionRequired': '権限が必要です',
'sessions.sidebar.session.status.questionPendingSingle': '保留中の質問が1件あります',
'sessions.sidebar.session.status.questionPendingMany': '保留中の質問が{count}件あります',
'sessions.sidebar.session.status.activeFor': 'アクティブ時間 {duration}',
'sessions.sidebar.session.status.lastTurnDuration': '前回のターンの所要時間 {duration}',
'sessions.sidebar.session.subsessions.collapse': 'サブセッションを折りたたむ',
'sessions.sidebar.session.subsessions.expand': 'サブセッションを展開',
'sessions.sidebar.dialogs.deleteSession.title': 'セッションを削除しますか?',
@@ -1101,6 +1108,10 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。',
'contextRail.surface.editor.description': 'プロジェクトのファイルを編集',
'contextRail.surface.git.description': 'コミット・ブランチ・プルリクエスト',
'contextRail.surface.git.changesCountAriaSingle': '{label}、変更ファイル{count}件',
'contextRail.surface.git.changesCountAriaPlural': '{label}、変更ファイル{count}件',
'contextRail.surface.git.changesCountTooltipSingle': '変更ファイル{count}件',
'contextRail.surface.git.changesCountTooltipPlural': '変更ファイル{count}件',
'contextRail.surface.terminal.description': '内蔵ターミナル',
'contextRail.surface.diff.description': '作業中の変更をレビュー',
'contextPanel.mode.walkthrough': 'ウォークスルー',
@@ -1140,8 +1151,11 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.toc.moreFiles': 'その他のファイル: {count}',
'walkthrough.toc.uncovered': '未対応: {count}',
'walkthrough.toc.resize': '目次の列幅を変更',
'walkthrough.importance.critical': '重要',
'walkthrough.importance.critical': '主要な変更',
'walkthrough.importance.criticalHint': 'このステップが変更全体を動かしているため、じっくり読んでください。コードで見つかった問題ではありません。',
'walkthrough.importance.context': '補足',
'walkthrough.importance.contextHint': '全体を理解するために添えられた補助的な変更です。',
'walkthrough.help.guide': 'ウォークスルーの仕組み',
'walkthrough.blocked.noModel.title': '利用できるスモールモデルがありません',
'walkthrough.blocked.noModel.description': 'レビューを生成するにはモデルプロバイダーにサインインしてください。',
'walkthrough.blocked.emptyDiff.title': 'レビュー対象がありません',
@@ -1156,6 +1170,8 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': 'スモールモデルは出力枠をすべて推論に使い、回答を返しませんでした。推論モデルは大きな差分でよくこうなります。推論の少ないモデルを選ぶか、対象範囲を絞ってください。',
'walkthrough.blocked.onlyGenerated.title': '生成ファイルのみが変更されています',
'walkthrough.blocked.onlyGenerated.description': 'ここでの変更はロックファイルなどツールが生成した出力だけで、レビューは意図的にこれらを対象外にしています。',
'walkthrough.blocked.serverUnsupported.title': 'このサーバーはウォークスルーに対応していません',
'walkthrough.blocked.serverUnsupported.description': 'このアプリが接続している OpenChamber サーバーはウォークスルー API に応答しませんでした。つまりアプリより古いバージョンです。サーバーを 1.18 以降に更新してから再読み込みしてください。',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': 'スモールモデルが扱えるのは約 {available} 千文字ですが、この差分には約 {required} 千文字が必要です。切り詰めは行いません。コンテキストの大きいモデルを選んでください。',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'スモールモデルはウォークスルーに必要な構造化応答をサポートしていません。',
'contextRail.surface.plan.description': '現在のプランを表示',
@@ -1636,6 +1652,10 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.browse.directories': 'ディレクトリ',
'directoryExplorerDialog.browse.loading': 'ディレクトリを読み込み中...',
'directoryExplorerDialog.browse.empty': '一致するディレクトリがありません。',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber がこのフォルダにアクセスする必要があります。',
'directoryExplorerDialog.browse.loadFailed': 'このフォルダを読み込めませんでした。',
'directoryExplorerDialog.browse.grantAccess': 'アクセスを許可',
'directoryExplorerDialog.browse.retry': '再試行',
'directoryExplorerDialog.browse.parentDirectory': '親ディレクトリ',
'directoryExplorerDialog.browse.addedBadge': '追加済み',
'directoryExplorerDialog.browse.quickAdd': '追加',
@@ -1702,7 +1722,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.toggleTerminalExpanded': 'ターミナル展開の切り替え',
'helpDialog.item.togglePlanContextPanel': '計画コンテキストパネルの切り替え',
'helpDialog.item.cycleTheme': 'テーマ切り替え(ライト→ダーク→システム)',
'helpDialog.item.switchProject': 'プロジェクトを切り替え',
'helpDialog.item.switchContextSurface': 'コンテキストパネルのサーフェスを切り替え(数字キー)',
'helpDialog.item.toggleServicesMenu': 'サービスの切り替え',
'helpDialog.item.cycleServicesTab': 'サービス変数の切り替え',
'helpDialog.item.openSettings': '設定を開く',
@@ -1987,6 +2007,8 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.group.empty.loadingSessions': 'セッションを読み込んでいます…',
'sessions.sidebar.group.empty.loadFailed': 'セッションを更新できませんでした。',
'sessions.sidebar.group.empty.retry': '再試行',
'sessions.sidebar.group.empty.permissionDenied': 'フォルダへのアクセスが必要です。',
'sessions.sidebar.group.empty.grantAccess': 'アクセスを許可',
'chat.unifiedControls.title': 'コントロール',
'chat.unifiedControls.model.title': 'モデル',
'chat.unifiedControls.model.noRecent': '最近のモデルはありません',
@@ -2891,6 +2913,9 @@ export const dict: Record<I18nKey, string> = {
'common.relative.daysAgoCompact': '{count}日前',
'common.relative.weeksAgoCompact': '{count}週前',
'common.relative.yearsAgoCompact': '{count}年前',
'common.duration.secondsCompact': '{seconds}秒',
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
'common.duration.hoursMinutesCompact': '{hours}時間{minutes}分',
'contextFileOpen.failure.tooLarge': 'ファイルが大きすぎて開けません(>{count}行)',
'contextFileOpen.failure.missing': 'ファイルが見つかりません',
'contextFileOpen.failure.unreadable': 'ファイルを開けませんでした',
@@ -444,6 +444,9 @@ export const settingsDict = {
'settings.openchamber.about.title': 'OpenChamber 정보',
'settings.openchamber.about.field.version': '버전',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 버전',
'settings.openchamber.about.field.instanceUrls': '인스턴스 URL',
'settings.openchamber.about.field.applicationUrl': '애플리케이션',
'settings.openchamber.about.field.tunnelUrl': '터널',
'settings.openchamber.about.state.checking': '확인 중...',
'settings.openchamber.about.state.upToDate': '최신 상태',
'settings.openchamber.about.state.unknown': '알 수 없음',
@@ -1088,6 +1091,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '컨텍스트 패널 표시 전환',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git 서피스 열기',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '파일 서피스 열기',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '컨텍스트 패널 서피스 전환',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
@@ -1372,6 +1377,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth 방식 {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'authorization code 붙여넣기',
'settings.providers.page.auth.oauth.starting': '인증을 시작하는 중…',
'settings.providers.page.auth.oauth.waiting': '인증을 기다리는 중…',
'settings.providers.page.auth.oauth.waitingHint': '브라우저에서 로그인을 완료하세요. 이 페이지를 열어 두면 연결이 자동으로 완료됩니다.',
'settings.providers.page.auth.oauth.codeHint': '브라우저에서 인증 코드를 복사해 여기에 붙여넣으세요.',
'settings.providers.page.auth.oauth.deviceCodeLabel': '기기 코드',
'settings.providers.page.auth.oauth.linkLabel': '인증 링크',
'settings.providers.page.auth.oauth.promptRequired': '계속하려면 “{field}”을(를) 입력하세요',
'settings.providers.page.auth.oauth.error.sessionExpired': '인증 요청이 만료되었습니다. 다시 연결해 처음부터 시작하세요.',
'settings.providers.page.auth.oauth.error.codeRequired': '이 제공자에는 브라우저의 인증 코드가 필요합니다.',
'settings.providers.page.auth.oauth.error.declined': '인증이 거부되었거나 완료되지 않았습니다.',
'settings.providers.page.auth.oauth.error.invalidInput': '입력한 정보가 거부되었습니다.',
'settings.providers.page.auth.connected': '연결됨',
'settings.providers.page.auth.incomplete': '자격 증명 없음',
'settings.providers.page.auth.incompleteHint': '· 채팅에서 이 공급자를 사용하기 전에 API 키 또는 {env:VAR}을(를) 추가하세요',
@@ -1404,6 +1420,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': '열기',
'settings.providers.page.actions.copy': '복사',
'settings.providers.page.actions.complete': '완료',
'settings.providers.page.actions.continue': '계속',
'settings.providers.page.actions.cancel': '취소',
'settings.providers.page.actions.tryAgain': '다시 시도',
'settings.providers.page.actions.hide': '숨기기',
'settings.providers.page.actions.reconnect': '재연결',
'settings.providers.page.actions.edit': '편집',
@@ -1419,7 +1438,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API key가 저장되었습니다',
'settings.providers.page.toast.oauthStartFailed': 'OAuth flow를 시작하지 못했습니다',
'settings.providers.page.toast.oauthDetailsMissing': '반환된 OAuth 세부 정보가 없습니다',
'settings.providers.page.toast.completeOAuthInBrowser': '브라우저에서 OAuth flow를 완료하세요',
'settings.providers.page.toast.oauthCompleteFailed': 'OAuth flow를 완료하지 못했습니다',
'settings.providers.page.toast.oauthCompleted': 'OAuth 연결이 완료되었습니다',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth 링크가 복사되었습니다',
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': '{taskName} 일시 중지',
'sessions.scheduledTasks.dialog.taskToggle.enabled': '활성화됨',
'sessions.scheduledTasks.dialog.taskToggle.paused': '일시 중지됨',
'sessions.scheduledTasks.dialog.loopFile.note': '루프 파일에서 관리됨: {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': '활성화 여부는 루프 파일이 제어합니다. Markdown frontmatter에서 enabled를 설정하세요',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': '루프 작업은 .agents/loops Markdown 파일에서 구성합니다',
'sessions.scheduledTasks.editor.title.edit': '예약 작업 편집',
'sessions.scheduledTasks.editor.title.new': '새 예약 작업',
'sessions.scheduledTasks.editor.description': '새 세션을 만들고 프롬프트를 보내는 서버 작업을 설정합니다.',
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.status.pinned': '고정된 세션',
'sessions.sidebar.session.status.movingToWorktree': '세션을 새 worktree로 이동하는 중',
'sessions.sidebar.session.status.permissionRequired': '권한 필요',
'sessions.sidebar.session.status.questionPendingSingle': '대기 중인 질문 1개',
'sessions.sidebar.session.status.questionPendingMany': '대기 중인 질문 {count}개',
'sessions.sidebar.session.status.activeFor': '{duration} 동안 활성 상태',
'sessions.sidebar.session.status.lastTurnDuration': '마지막 턴 소요 시간 {duration}',
'sessions.sidebar.session.subsessions.collapse': '하위 세션 접기',
'sessions.sidebar.session.subsessions.expand': '하위 세션 펼치기',
'sessions.sidebar.dialogs.deleteSession.title': '세션 삭제?',
@@ -1105,6 +1112,10 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.',
'contextRail.surface.editor.description': '프로젝트 파일 편집',
'contextRail.surface.git.description': '커밋, 브랜치, 풀 리퀘스트',
'contextRail.surface.git.changesCountAriaSingle': '{label}, 변경된 파일 {count}개',
'contextRail.surface.git.changesCountAriaPlural': '{label}, 변경된 파일 {count}개',
'contextRail.surface.git.changesCountTooltipSingle': '변경된 파일 {count}개',
'contextRail.surface.git.changesCountTooltipPlural': '변경된 파일 {count}개',
'contextRail.surface.terminal.description': '내장 터미널',
'contextRail.surface.diff.description': '작업 중인 변경 사항 검토',
'contextPanel.mode.walkthrough': '워크스루',
@@ -1144,8 +1155,11 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.toc.moreFiles': '다른 파일: {count}',
'walkthrough.toc.uncovered': '미포함: {count}',
'walkthrough.toc.resize': '목차 열 너비 조절',
'walkthrough.importance.critical': '중요',
'walkthrough.importance.critical': '핵심 변경',
'walkthrough.importance.criticalHint': '이 단계가 변경 전체를 이끌고 있으니 꼼꼼히 읽어 보세요. 코드에서 발견된 문제가 아닙니다.',
'walkthrough.importance.context': '참고',
'walkthrough.importance.contextHint': '나머지를 이해하는 데 도움이 되도록 함께 실은 보조 변경입니다.',
'walkthrough.help.guide': '워크스루 작동 방식',
'walkthrough.blocked.noModel.title': '사용할 수 있는 스몰 모델이 없습니다',
'walkthrough.blocked.noModel.description': '리뷰를 생성하려면 모델 제공자에 로그인하세요.',
'walkthrough.blocked.emptyDiff.title': '리뷰할 내용이 없습니다',
@@ -1160,6 +1174,8 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': '스몰 모델이 출력 예산을 모두 추론에 쓰고 아무것도 반환하지 않았습니다. 추론 모델은 큰 diff에서 흔히 이렇게 됩니다. 덜 추론하는 모델을 고르거나 범위를 좁혀 보세요.',
'walkthrough.blocked.onlyGenerated.title': '생성된 파일만 변경되었습니다',
'walkthrough.blocked.onlyGenerated.description': '여기의 변경은 모두 잠금 파일이거나 도구가 만든 산출물이며, 리뷰는 이런 파일을 의도적으로 건너뜁니다.',
'walkthrough.blocked.serverUnsupported.title': '이 서버는 워크스루를 지원하지 않습니다',
'walkthrough.blocked.serverUnsupported.description': '이 앱이 연결된 OpenChamber 서버가 워크스루 API에 응답하지 않았습니다. 즉 앱보다 오래된 버전입니다. 서버를 1.18 이상으로 업데이트한 뒤 새로 고치세요.',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': '스몰 모델은 약 {available}천 자를 담을 수 있는데 이 diff에는 약 {required}천 자가 필요합니다. 잘라내지 않으니 컨텍스트가 더 큰 모델을 선택하세요.',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '스몰 모델은 워크스루에 필요한 구조화된 응답을 지원하지 않습니다.',
'contextRail.surface.plan.description': '현재 계획 보기',
@@ -1642,6 +1658,10 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.browse.directories': '디렉터리',
'directoryExplorerDialog.browse.loading': '디렉터리 로드 중...',
'directoryExplorerDialog.browse.empty': '일치하는 디렉터리가 없습니다.',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber에서 이 폴더에 접근해야 합니다.',
'directoryExplorerDialog.browse.loadFailed': '이 폴더를 불러올 수 없습니다.',
'directoryExplorerDialog.browse.grantAccess': '접근 허용',
'directoryExplorerDialog.browse.retry': '다시 시도',
'directoryExplorerDialog.browse.parentDirectory': '상위 디렉터리',
'directoryExplorerDialog.browse.addedBadge': '추가됨',
'directoryExplorerDialog.browse.quickAdd': '추가',
@@ -1708,7 +1728,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.toggleTerminalExpanded': '터미널 펼치기/접기',
'helpDialog.item.togglePlanContextPanel': '플랜 컨텍스트 패널 전환',
'helpDialog.item.cycleTheme': '테마 순환(라이트 → 다크 → 시스템)',
'helpDialog.item.switchProject': '프로젝트 전환',
'helpDialog.item.switchContextSurface': '컨텍스트 패널 서피스 전환(숫자 키)',
'helpDialog.item.toggleServicesMenu': '서비스 메뉴 전환',
'helpDialog.item.cycleServicesTab': '서비스 탭 순환',
'helpDialog.item.openSettings': '설정 열기',
@@ -1993,6 +2013,8 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.group.empty.loadingSessions': '세션을 불러오는 중…',
'sessions.sidebar.group.empty.loadFailed': '세션을 새로 고칠 수 없습니다.',
'sessions.sidebar.group.empty.retry': '다시 시도',
'sessions.sidebar.group.empty.permissionDenied': '폴더 접근이 필요합니다.',
'sessions.sidebar.group.empty.grantAccess': '접근 허용',
'chat.unifiedControls.title': '컨트롤',
'chat.unifiedControls.model.title': '모델',
'chat.unifiedControls.model.noRecent': '최근 모델 없음',
@@ -2895,6 +2917,9 @@ export const dict: Record<I18nKey, string> = {
'common.relative.daysAgoCompact': '{count}d ago',
'common.relative.weeksAgoCompact': '{count}w ago',
'common.relative.yearsAgoCompact': '{count}y ago',
'common.duration.secondsCompact': '{seconds}초',
'common.duration.minutesSecondsCompact': '{minutes}분 {seconds}초',
'common.duration.hoursMinutesCompact': '{hours}시간 {minutes}분',
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
'contextFileOpen.failure.missing': 'File not found',
'contextFileOpen.failure.unreadable': 'Failed to open file',
@@ -722,6 +722,9 @@ export const settingsDict = {
'settings.openchamber.about.actions.updateToVersion': 'Aktualizuj do wersji {version}',
'settings.openchamber.about.field.version': 'Wersja',
'settings.openchamber.about.field.openCodeVersion': 'Wersja OpenCode',
'settings.openchamber.about.field.instanceUrls': 'Adresy URL instancji',
'settings.openchamber.about.field.applicationUrl': 'Aplikacja',
'settings.openchamber.about.field.tunnelUrl': 'Tunel',
'settings.openchamber.about.state.checking': 'Sprawdzanie...',
'settings.openchamber.about.state.upToDate': 'Aktualna wersja',
'settings.openchamber.about.state.unknown': 'nieznane',
@@ -820,6 +823,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Przejdź do linii (edytor plików)',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Otwórz skróty klawiszowe',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': 'Otwórz powierzchnię plików',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': 'Przełącz powierzchnię panelu kontekstu',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Otwórz powierzchnię Git',
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Otwórz ustawienia',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Przełącz panel kontekstu planu',
@@ -1360,6 +1365,9 @@ export const settingsDict = {
'settings.projects.sidebar.actions.addProject': 'Dodaj projekt',
'settings.projects.sidebar.total': 'Suma: {count}',
'settings.providers.page.actions.complete': 'Zakończ',
'settings.providers.page.actions.continue': 'Kontynuuj',
'settings.providers.page.actions.cancel': 'Anuluj',
'settings.providers.page.actions.tryAgain': 'Spróbuj ponownie',
'settings.providers.page.actions.connect': 'Połącz',
'settings.providers.page.actions.copy': 'Kopiuj',
'settings.providers.page.actions.copyCode': 'Kopiuj kod',
@@ -1385,6 +1393,17 @@ export const settingsDict = {
'settings.providers.page.auth.loadingMethods': 'Ładowanie metod uwierzytelniania...',
'settings.providers.page.auth.oauthMethodFallback': 'Metoda OAuth {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': 'Wklej kod autoryzacyjny',
'settings.providers.page.auth.oauth.starting': 'Rozpoczynanie autoryzacji…',
'settings.providers.page.auth.oauth.waiting': 'Oczekiwanie na autoryzację…',
'settings.providers.page.auth.oauth.waitingHint': 'Dokończ logowanie w przeglądarce. Zostaw tę stronę otwartą — połączenie zakończy się samo.',
'settings.providers.page.auth.oauth.codeHint': 'Skopiuj kod autoryzacji z przeglądarki i wklej go tutaj.',
'settings.providers.page.auth.oauth.deviceCodeLabel': 'Kod urządzenia',
'settings.providers.page.auth.oauth.linkLabel': 'Link autoryzacyjny',
'settings.providers.page.auth.oauth.promptRequired': 'Wypełnij pole „{field}”, aby kontynuować',
'settings.providers.page.auth.oauth.error.sessionExpired': 'Żądanie autoryzacji wygasło. Połącz ponownie, aby zacząć od nowa.',
'settings.providers.page.auth.oauth.error.codeRequired': 'Ten dostawca wymaga kodu autoryzacji z przeglądarki.',
'settings.providers.page.auth.oauth.error.declined': 'Autoryzacja została odrzucona lub nie została ukończona.',
'settings.providers.page.auth.oauth.error.invalidInput': 'Wprowadzone dane zostały odrzucone.',
'settings.providers.page.auth.title': 'Uwierzytelnianie',
'settings.providers.page.auth.useReconnectHint': '· Użyj Połącz ponownie, aby zaktualizować dane logowania',
'settings.providers.page.custom.optionLabel': 'Inny / Niestandardowy',
@@ -1474,7 +1493,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaveFailed': 'Nie udało się zapisać klucza API',
'settings.providers.page.toast.apiKeySaved': 'Klucz API został zapisany',
'settings.providers.page.toast.authMethodsLoadFailed': 'Nie udało się załadować metod uwierzytelniania dostawcy',
'settings.providers.page.toast.completeOAuthInBrowser': 'Dokończ proces OAuth w przeglądarce',
'settings.providers.page.toast.deviceCodeCopied': 'Kod urządzenia został skopiowany',
'settings.providers.page.toast.deviceCodeCopyFailed': 'Nie udało się skopiować kodu urządzenia',
'settings.providers.page.toast.oauthCompleteFailed': 'Nie udało się dokończyć procesu OAuth',
+27 -2
View File
@@ -396,6 +396,9 @@ export const dict: Record<I18nKey, string> = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': 'Wstrzymaj {taskName}',
'sessions.scheduledTasks.dialog.taskToggle.enabled': 'Włączone',
'sessions.scheduledTasks.dialog.taskToggle.paused': 'Wstrzymane',
'sessions.scheduledTasks.dialog.loopFile.note': 'Zarządzane przez plik pętli {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'Włączenie jest kontrolowane przez plik pętli; ustaw enabled w frontmatterze Markdown',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Zadania pętli są konfigurowane w pliku Markdown .agents/loops',
'sessions.scheduledTasks.editor.title.edit': 'Edytuj zaplanowane zadanie',
'sessions.scheduledTasks.editor.title.new': 'Nowe zaplanowane zadanie',
'sessions.scheduledTasks.editor.description': 'Skonfiguruj zadanie po stronie serwera, które tworzy nową sesję i wysyła prompt.',
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.status.pinned': 'Przypięta sesja',
'sessions.sidebar.session.status.movingToWorktree': 'Przenoszenie sesji do nowego worktree',
'sessions.sidebar.session.status.permissionRequired': 'Wymagane uprawnienie',
'sessions.sidebar.session.status.questionPendingSingle': '1 oczekujące pytanie',
'sessions.sidebar.session.status.questionPendingMany': 'Liczba oczekujących pytań: {count}',
'sessions.sidebar.session.status.activeFor': 'Aktywna od {duration}',
'sessions.sidebar.session.status.lastTurnDuration': 'Ostatnia tura trwała {duration}',
'sessions.sidebar.session.subsessions.collapse': 'Zwiń pod-sesje',
'sessions.sidebar.session.subsessions.expand': 'Rozwiń pod-sesje',
'sessions.sidebar.dialogs.deleteSession.title': 'Usunąć sesję?',
@@ -804,6 +811,8 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.group.empty.loadingSessions': 'Wczytywanie sesji…',
'sessions.sidebar.group.empty.loadFailed': 'Nie udało się odświeżyć sesji.',
'sessions.sidebar.group.empty.retry': 'Spróbuj ponownie',
'sessions.sidebar.group.empty.permissionDenied': 'Wymagany jest dostęp do folderu.',
'sessions.sidebar.group.empty.grantAccess': 'Przyznaj dostęp',
'chat.unifiedControls.title': 'Kontrolki',
'chat.unifiedControls.model.title': 'Model',
'chat.unifiedControls.model.noRecent': 'Brak ostatnich modeli',
@@ -1417,6 +1426,10 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.',
'contextRail.surface.editor.description': 'Edytuj pliki projektu',
'contextRail.surface.git.description': 'Commity, gałęzie i pull requesty',
'contextRail.surface.git.changesCountAriaSingle': '{label}, {count} zmieniony plik',
'contextRail.surface.git.changesCountAriaPlural': '{label}, {count} zmienionych plików',
'contextRail.surface.git.changesCountTooltipSingle': '{count} zmieniony plik',
'contextRail.surface.git.changesCountTooltipPlural': '{count} zmienionych plików',
'contextRail.surface.terminal.description': 'Wbudowany terminal',
'contextRail.surface.diff.description': 'Przeglądaj bieżące zmiany',
'contextPanel.mode.walkthrough': 'Przewodnik',
@@ -1456,8 +1469,11 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.toc.moreFiles': 'Więcej plików: {count}',
'walkthrough.toc.uncovered': 'Nieuwzględnione: {count}',
'walkthrough.toc.resize': 'Zmień szerokość kolumny spisu treści',
'walkthrough.importance.critical': 'Krytyczne',
'walkthrough.importance.critical': 'Kluczowa zmiana',
'walkthrough.importance.criticalHint': 'Ten krok napędza resztę zmiany, więc przeczytaj go uważnie. To nie jest problem znaleziony w Twoim kodzie.',
'walkthrough.importance.context': 'Kontekst',
'walkthrough.importance.contextHint': 'Zmiana pomocnicza, dołączona po to, by reszta miała sens.',
'walkthrough.help.guide': 'Jak działają walkthroughy',
'walkthrough.blocked.noModel.title': 'Brak dostępnego małego modelu',
'walkthrough.blocked.noModel.description': 'Zaloguj się u dostawcy modeli, aby wygenerować przegląd.',
'walkthrough.blocked.emptyDiff.title': 'Nie ma czego przeglądać',
@@ -1472,6 +1488,8 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': 'Mały model zużył cały limit wyjścia na rozumowanie i nic nie zwrócił. Modele rozumujące często tak robią przy dużych różnicach — pomoże model mniej „myślący” albo węższy zakres przeglądu.',
'walkthrough.blocked.onlyGenerated.title': 'Zmieniły się tylko pliki generowane',
'walkthrough.blocked.onlyGenerated.description': 'Wszystkie zmiany to pliki blokad lub inne wyniki pracy narzędzi, które przegląd celowo pomija.',
'walkthrough.blocked.serverUnsupported.title': 'Ten serwer nie obsługuje walkthroughów',
'walkthrough.blocked.serverUnsupported.description': 'Serwer OpenChamber, z którym połączona jest ta aplikacja, nie odpowiedział na API walkthroughu — jest więc starszy niż aplikacja. Zaktualizuj serwer do wersji 1.18 lub nowszej i odśwież.',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': 'Mały model mieści około {available} tys. znaków, a te różnice potrzebują około {required} tys. Nic nie jest obcinane — wybierz model z większym kontekstem.',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': 'Mały model nie obsługuje ustrukturyzowanych odpowiedzi wymaganych przez przewodnik.',
'contextRail.surface.plan.description': 'Zobacz bieżący plan',
@@ -1722,6 +1740,10 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.browse.quickAdd': 'Dodaj',
'directoryExplorerDialog.browse.directories': 'Katalogi',
'directoryExplorerDialog.browse.empty': 'Brak pasujących katalogów.',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber potrzebuje dostępu do tego folderu.',
'directoryExplorerDialog.browse.loadFailed': 'Nie udało się wczytać tego folderu.',
'directoryExplorerDialog.browse.grantAccess': 'Przyznaj dostęp',
'directoryExplorerDialog.browse.retry': 'Spróbuj ponownie',
'directoryExplorerDialog.browse.loading': 'Ładowanie katalogów...',
'directoryExplorerDialog.browse.parentDirectory': 'Katalog nadrzędny',
'directoryExplorerDialog.description': 'Wybierz folder, który chcesz dodać jako projekt.',
@@ -2328,7 +2350,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.openRightSidebarGitTab': 'Otwórz powierzchnię Git',
'helpDialog.item.openSettings': 'Otwórz ustawienia',
'helpDialog.item.showKeyboardShortcuts': 'Pokaż skróty klawiaturowe (to okno)',
'helpDialog.item.switchProject': 'Przełącz projekt',
'helpDialog.item.switchContextSurface': 'Przełącz powierzchnię panelu kontekstu (klawisz liczbowy)',
'helpDialog.item.togglePlanContextPanel': 'Przełącz panel kontekstu planu',
'helpDialog.item.toggleRightSidebar': 'Przełącz panel kontekstu',
'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług',
@@ -2912,6 +2934,9 @@ export const dict: Record<I18nKey, string> = {
'common.relative.daysAgoCompact': '{count}d ago',
'common.relative.weeksAgoCompact': '{count}w ago',
'common.relative.yearsAgoCompact': '{count}y ago',
'common.duration.secondsCompact': '{seconds}s',
'common.duration.minutesSecondsCompact': '{minutes}m {seconds}s',
'common.duration.hoursMinutesCompact': '{hours}h {minutes}m',
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
'contextFileOpen.failure.missing': 'File not found',
'contextFileOpen.failure.unreadable': 'Failed to open file',
@@ -444,6 +444,9 @@ export const settingsDict = {
"settings.openchamber.about.title": "Sobre o OpenChamber",
"settings.openchamber.about.field.version": "Versão",
"settings.openchamber.about.field.openCodeVersion": "Versão do OpenCode",
"settings.openchamber.about.field.instanceUrls": "URLs da instância",
"settings.openchamber.about.field.applicationUrl": "Aplicativo",
"settings.openchamber.about.field.tunnelUrl": "Túnel",
"settings.openchamber.about.state.checking": "Verificando...",
"settings.openchamber.about.state.upToDate": "Atualizado",
"settings.openchamber.about.state.unknown": "desconhecido",
@@ -1088,6 +1091,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Alternar painel de contexto',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Abrir superfície do Git',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Abrir superfície de arquivos',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Alternar superfície do painel de contexto",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
@@ -1372,6 +1377,17 @@ export const settingsDict = {
"settings.providers.page.auth.apiKeyPlaceholder": "sk-...",
"settings.providers.page.auth.oauthMethodFallback": "Método OAuth {index}",
"settings.providers.page.auth.pasteAuthorizationCodePlaceholder": "Colar código de autorização",
"settings.providers.page.auth.oauth.starting": "Iniciando a autorização…",
"settings.providers.page.auth.oauth.waiting": "Aguardando a autorização…",
"settings.providers.page.auth.oauth.waitingHint": "Conclua o login no navegador. Mantenha esta página aberta — a conexão será concluída sozinha.",
"settings.providers.page.auth.oauth.codeHint": "Copie o código de autorização do navegador e cole aqui.",
"settings.providers.page.auth.oauth.deviceCodeLabel": "Código do dispositivo",
"settings.providers.page.auth.oauth.linkLabel": "Link de autorização",
"settings.providers.page.auth.oauth.promptRequired": "Preencha “{field}” para continuar",
"settings.providers.page.auth.oauth.error.sessionExpired": "A solicitação de autorização expirou. Conecte novamente para reiniciá-la.",
"settings.providers.page.auth.oauth.error.codeRequired": "Este provedor precisa do código de autorização do seu navegador.",
"settings.providers.page.auth.oauth.error.declined": "A autorização foi recusada ou não foi concluída.",
"settings.providers.page.auth.oauth.error.invalidInput": "Os dados informados foram recusados.",
"settings.providers.page.auth.connected": "Conectado",
"settings.providers.page.auth.incomplete": "Credenciais ausentes",
"settings.providers.page.auth.incompleteHint": "· Adicione uma chave de API ou {env:VAR} antes de usar este provedor no chat",
@@ -1404,6 +1420,9 @@ export const settingsDict = {
"settings.providers.page.actions.open": "Abrir",
"settings.providers.page.actions.copy": "Copiar",
"settings.providers.page.actions.complete": "Completar",
"settings.providers.page.actions.continue": "Continuar",
"settings.providers.page.actions.cancel": "Cancelar",
"settings.providers.page.actions.tryAgain": "Tentar novamente",
"settings.providers.page.actions.hide": "Ocultar",
"settings.providers.page.actions.reconnect": "Reconectar",
"settings.providers.page.actions.edit": "Editar",
@@ -1419,7 +1438,6 @@ export const settingsDict = {
"settings.providers.page.toast.apiKeySaved": "Chave API salva",
"settings.providers.page.toast.oauthStartFailed": "Não foi possível iniciar o fluxo OAuth",
"settings.providers.page.toast.oauthDetailsMissing": "Não se devolvieron detalhes de OAuth",
"settings.providers.page.toast.completeOAuthInBrowser": "Complete o fluxo OAuth no navegador",
"settings.providers.page.toast.oauthCompleteFailed": "Não foi possível concluir o fluxo OAuth",
"settings.providers.page.toast.oauthCompleted": "Conexão OAuth concluída",
"settings.providers.page.toast.oauthLinkCopied": "Link de OAuth copiado",
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
"sessions.scheduledTasks.dialog.taskToggle.pauseAria": "Pausar {taskName}",
"sessions.scheduledTasks.dialog.taskToggle.enabled": "Ativado",
"sessions.scheduledTasks.dialog.taskToggle.paused": "Pausado",
'sessions.scheduledTasks.dialog.loopFile.note': 'Gerenciada pelo arquivo de loop {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'A ativação é controlada pelo arquivo de loop; defina enabled no frontmatter Markdown',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Tarefas de loop são configuradas no arquivo Markdown .agents/loops',
"sessions.scheduledTasks.editor.title.edit": "Editar tarefa agendada",
"sessions.scheduledTasks.editor.title.new": "Nova tarefa agendada",
"sessions.scheduledTasks.editor.description": "Configure uma tarefa do lado do servidor que cria uma nova sessão e envia um prompt.",
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.status.pinned": "Sessão fixada",
"sessions.sidebar.session.status.movingToWorktree": "Movendo a sessão para um novo worktree",
"sessions.sidebar.session.status.permissionRequired": "Permissão obrigatória",
"sessions.sidebar.session.status.questionPendingSingle": "1 pergunta pendente",
"sessions.sidebar.session.status.questionPendingMany": "{count} perguntas pendentes",
"sessions.sidebar.session.status.activeFor": "Ativa há {duration}",
"sessions.sidebar.session.status.lastTurnDuration": "O último turno levou {duration}",
"sessions.sidebar.session.subsessions.collapse": "Recolher subsessões",
"sessions.sidebar.session.subsessions.expand": "Expandir subsessões",
"sessions.sidebar.dialogs.deleteSession.title": "Excluir sessão?",
@@ -1105,6 +1112,10 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.",
"contextRail.surface.editor.description": "Editar arquivos do projeto",
"contextRail.surface.git.description": "Commits, branches e pull requests",
"contextRail.surface.git.changesCountAriaSingle": "{label}, {count} arquivo modificado",
"contextRail.surface.git.changesCountAriaPlural": "{label}, {count} arquivos modificados",
"contextRail.surface.git.changesCountTooltipSingle": "{count} arquivo modificado",
"contextRail.surface.git.changesCountTooltipPlural": "{count} arquivos modificados",
"contextRail.surface.terminal.description": "Terminal integrado",
"contextRail.surface.diff.description": "Revisar alterações em andamento",
"contextPanel.mode.walkthrough": "Percurso",
@@ -1144,8 +1155,11 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.toc.moreFiles": "Mais arquivos: {count}",
"walkthrough.toc.uncovered": "Sem cobertura: {count}",
"walkthrough.toc.resize": "Redimensionar a coluna de conteúdo",
"walkthrough.importance.critical": "Crítico",
"walkthrough.importance.critical": "Mudança principal",
"walkthrough.importance.criticalHint": "Este passo conduz o restante da mudança, então leia com atenção. Não é um problema encontrado no seu código.",
"walkthrough.importance.context": "Contexto",
"walkthrough.importance.contextHint": "Uma mudança de apoio, incluída para que o restante faça sentido.",
"walkthrough.help.guide": "Como funcionam os walkthroughs",
"walkthrough.blocked.noModel.title": "Nenhum modelo pequeno disponível",
"walkthrough.blocked.noModel.description": "Entre em um provedor de modelos para gerar uma revisão.",
"walkthrough.blocked.emptyDiff.title": "Nada para revisar",
@@ -1160,6 +1174,8 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.outputExhausted.descriptionUnknownModel": "O modelo pequeno gastou toda a margem de saída raciocinando e não devolveu nada. Modelos de raciocínio costumam fazer isso em diffs grandes — escolha um modelo que raciocine menos ou revise um escopo menor.",
"walkthrough.blocked.onlyGenerated.title": "Só mudaram arquivos gerados",
"walkthrough.blocked.onlyGenerated.description": "Todas as mudanças são arquivos de lock ou outra saída gerada por ferramentas, que a revisão ignora de propósito.",
"walkthrough.blocked.serverUnsupported.title": "Este servidor não oferece walkthroughs",
"walkthrough.blocked.serverUnsupported.description": "O servidor OpenChamber ao qual este app está conectado não respondeu à API de walkthrough, ou seja, é mais antigo que o app. Atualize o servidor para 1.18 ou mais recente e atualize a visualização.",
"walkthrough.blocked.contextTooSmall.descriptionUnknownModel": "O modelo pequeno comporta cerca de {available} mil caracteres e este diff precisa de cerca de {required} mil. Nada é cortado — escolha um modelo com contexto maior.",
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "O modelo pequeno não suporta as respostas estruturadas que um percurso exige.",
"contextRail.surface.plan.description": "Ver o plano atual",
@@ -1618,6 +1634,10 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.browse.directories": "Diretórios",
"directoryExplorerDialog.browse.loading": "Carregando diretórios...",
"directoryExplorerDialog.browse.empty": "Nenhum diretório correspondente.",
"directoryExplorerDialog.browse.permissionDenied": "OpenChamber precisa acessar esta pasta.",
"directoryExplorerDialog.browse.loadFailed": "Não foi possível carregar esta pasta.",
"directoryExplorerDialog.browse.grantAccess": "Conceder acesso",
"directoryExplorerDialog.browse.retry": "Tentar novamente",
"directoryExplorerDialog.browse.parentDirectory": "Diretório pai",
"directoryExplorerDialog.browse.addedBadge": "Adicionado",
"directoryExplorerDialog.browse.quickAdd": "Adicionar",
@@ -1684,7 +1704,7 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.toggleTerminalExpanded": "Expandir ou recolher o terminal",
"helpDialog.item.togglePlanContextPanel": "Alternar painel de contexto do plano",
"helpDialog.item.cycleTheme": "Alternar tema (Claro → Escuro → Sistema)",
"helpDialog.item.switchProject": "Alternar projeto",
"helpDialog.item.switchContextSurface": "Alternar superfície do painel de contexto (tecla numérica)",
"helpDialog.item.toggleServicesMenu": "Mostrar ou ocultar menu de serviços",
"helpDialog.item.cycleServicesTab": "Alternar aba de serviços",
"helpDialog.item.openSettings": "Abrir configurações",
@@ -1969,6 +1989,8 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.group.empty.loadingSessions": "Carregando sessões…",
"sessions.sidebar.group.empty.loadFailed": "Não foi possível atualizar as sessões.",
"sessions.sidebar.group.empty.retry": "Tentar novamente",
"sessions.sidebar.group.empty.permissionDenied": "É necessário acesso à pasta.",
"sessions.sidebar.group.empty.grantAccess": "Conceder acesso",
"chat.unifiedControls.title": "Controles",
"chat.unifiedControls.model.title": "Modelo",
"chat.unifiedControls.model.noRecent": "Não há modelos recentes",
@@ -2896,6 +2918,9 @@ export const dict: Record<I18nKey, string> = {
"common.relative.daysAgoCompact": "{count}d ago",
"common.relative.weeksAgoCompact": "{count}w ago",
"common.relative.yearsAgoCompact": "{count}y ago",
"common.duration.secondsCompact": "{seconds}s",
"common.duration.minutesSecondsCompact": "{minutes}m {seconds}s",
"common.duration.hoursMinutesCompact": "{hours}h {minutes}m",
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
"contextFileOpen.failure.missing": "File not found",
"contextFileOpen.failure.unreadable": "Failed to open file",
@@ -444,6 +444,9 @@ export const settingsDict = {
"settings.openchamber.about.title": "Про OpenChamber",
"settings.openchamber.about.field.version": "Версія",
"settings.openchamber.about.field.openCodeVersion": "Версія OpenCode",
"settings.openchamber.about.field.instanceUrls": "URL-адреси екземпляра",
"settings.openchamber.about.field.applicationUrl": "Застосунок",
"settings.openchamber.about.field.tunnelUrl": "Тунель",
"settings.openchamber.about.state.checking": "Перевірка...",
"settings.openchamber.about.state.upToDate": "В актуальному стані",
"settings.openchamber.about.state.unknown": "невідомо",
@@ -1088,6 +1091,8 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Перемкнути контекстну панель',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Відкрити поверхню Git',
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label": 'Відкрити поверхню файлів',
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.label": "Перемкнути поверхню панелі контексту",
"settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix": " + 1…0",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
"settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
@@ -1372,6 +1377,17 @@ export const settingsDict = {
"settings.providers.page.auth.apiKeyPlaceholder": "sk-...",
"settings.providers.page.auth.oauthMethodFallback": "OAuth метод {index}",
"settings.providers.page.auth.pasteAuthorizationCodePlaceholder": "Вставити код авторизації",
"settings.providers.page.auth.oauth.starting": "Запускаємо авторизацію…",
"settings.providers.page.auth.oauth.waiting": "Очікуємо на авторизацію…",
"settings.providers.page.auth.oauth.waitingHint": "Завершіть вхід у браузері. Не закривайте цю сторінку — підключення завершиться саме.",
"settings.providers.page.auth.oauth.codeHint": "Скопіюйте код авторизації з браузера і вставте його сюди.",
"settings.providers.page.auth.oauth.deviceCodeLabel": "Код пристрою",
"settings.providers.page.auth.oauth.linkLabel": "Посилання для авторизації",
"settings.providers.page.auth.oauth.promptRequired": "Заповніть «{field}», щоб продовжити",
"settings.providers.page.auth.oauth.error.sessionExpired": "Термін дії запиту на авторизацію минув. Підключіться ще раз, щоб почати заново.",
"settings.providers.page.auth.oauth.error.codeRequired": "Цьому провайдеру потрібен код авторизації з браузера.",
"settings.providers.page.auth.oauth.error.declined": "Авторизацію відхилено або не завершено.",
"settings.providers.page.auth.oauth.error.invalidInput": "Введені дані відхилено.",
"settings.providers.page.auth.connected": "Підключено",
"settings.providers.page.auth.incomplete": "Облікові дані відсутні",
"settings.providers.page.auth.incompleteHint": "· Додайте API-ключ або {env:VAR} перед використанням цього провайдера в чаті",
@@ -1404,6 +1420,9 @@ export const settingsDict = {
"settings.providers.page.actions.open": "Відкрити",
"settings.providers.page.actions.copy": "Копіювати",
"settings.providers.page.actions.complete": "Завершити",
"settings.providers.page.actions.continue": "Продовжити",
"settings.providers.page.actions.cancel": "Скасувати",
"settings.providers.page.actions.tryAgain": "Повторити спробу",
"settings.providers.page.actions.hide": "Сховати",
"settings.providers.page.actions.reconnect": "Перепідключити",
"settings.providers.page.actions.edit": "Редагувати",
@@ -1419,7 +1438,6 @@ export const settingsDict = {
"settings.providers.page.toast.apiKeySaved": "Ключ API збережено",
"settings.providers.page.toast.oauthStartFailed": "Не вдалося запустити потік OAuth",
"settings.providers.page.toast.oauthDetailsMissing": "Деталі OAuth не повернуто",
"settings.providers.page.toast.completeOAuthInBrowser": "Завершіть процес OAuth у вашому браузері",
"settings.providers.page.toast.oauthCompleteFailed": "Не вдалося завершити потік OAuth",
"settings.providers.page.toast.oauthCompleted": "Підключення OAuth завершено",
"settings.providers.page.toast.oauthLinkCopied": "Посилання OAuth скопійовано",
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
"sessions.scheduledTasks.dialog.taskToggle.pauseAria": "Призупинити {taskName}",
"sessions.scheduledTasks.dialog.taskToggle.enabled": "Увімкнено",
"sessions.scheduledTasks.dialog.taskToggle.paused": "Призупинено",
'sessions.scheduledTasks.dialog.loopFile.note': 'Керується файлом циклу {file}',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': 'Активність контролюється файлом циклу; встановіть enabled у frontmatter Markdown',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': 'Завдання циклів налаштовуються у файлі Markdown .agents/loops',
"sessions.scheduledTasks.editor.title.edit": "Редагувати заплановане завдання",
"sessions.scheduledTasks.editor.title.new": "Нове заплановане завдання",
"sessions.scheduledTasks.editor.description": "Налаштувати завдання на стороні сервера, яке створює нову сесію і надсилає запит.",
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.session.status.pinned": "Закріплений сесія",
"sessions.sidebar.session.status.movingToWorktree": "Перенесення сесії в новий worktree",
"sessions.sidebar.session.status.permissionRequired": "Потрібен дозвіл",
"sessions.sidebar.session.status.questionPendingSingle": "1 запитання очікує відповіді",
"sessions.sidebar.session.status.questionPendingMany": "Кількість запитань, що очікують відповіді: {count}",
"sessions.sidebar.session.status.activeFor": "Активна вже {duration}",
"sessions.sidebar.session.status.lastTurnDuration": "Останній хід тривав {duration}",
"sessions.sidebar.session.subsessions.collapse": "Згорнути підсесії",
"sessions.sidebar.session.subsessions.expand": "Розгорнути підсесії",
"sessions.sidebar.dialogs.deleteSession.title": "Видалити сесія?",
@@ -1105,6 +1112,10 @@ export const dict: Record<I18nKey, string> = {
"contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.",
"contextRail.surface.editor.description": "Редагування файлів проєкту",
"contextRail.surface.git.description": "Коміти, гілки та pull request-и",
"contextRail.surface.git.changesCountAriaSingle": "{label}, {count} змінений файл",
"contextRail.surface.git.changesCountAriaPlural": "{label}, {count} змінених файлів",
"contextRail.surface.git.changesCountTooltipSingle": "{count} змінений файл",
"contextRail.surface.git.changesCountTooltipPlural": "{count} змінених файлів",
"contextRail.surface.terminal.description": "Вбудований термінал",
"contextRail.surface.diff.description": "Перегляд поточних змін",
"contextPanel.mode.walkthrough": "Розбір",
@@ -1144,8 +1155,11 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.toc.moreFiles": "Ще файлів: {count}",
"walkthrough.toc.uncovered": "Не описано: {count}",
"walkthrough.toc.resize": "Змінити ширину колонки змісту",
"walkthrough.importance.critical": "Критично",
"walkthrough.importance.critical": "Ключова зміна",
"walkthrough.importance.criticalHint": "Цей крок веде за собою решту зміни, тож прочитайте його уважно. Це не знайдена у вашому коді проблема.",
"walkthrough.importance.context": "Контекст",
"walkthrough.importance.contextHint": "Допоміжна зміна, додана, щоб решта мала сенс.",
"walkthrough.help.guide": "Як працюють walkthrough",
"walkthrough.blocked.noModel.title": "Немає доступної small model",
"walkthrough.blocked.noModel.description": "Увійдіть до провайдера моделей, щоб створити розбір.",
"walkthrough.blocked.emptyDiff.title": "Немає що оглядати",
@@ -1160,6 +1174,8 @@ export const dict: Record<I18nKey, string> = {
"walkthrough.blocked.outputExhausted.descriptionUnknownModel": "Small model витратила весь бюджет виводу на роздуми й нічого не повернула. Reasoning-моделі часто так поводяться на великих diff — допоможе модель, яка менше «думає», або вужча область огляду.",
"walkthrough.blocked.onlyGenerated.title": "Змінились лише згенеровані файли",
"walkthrough.blocked.onlyGenerated.description": "Усі зміни тут — це lock-файли чи інший результат роботи інструментів, які розбір свідомо пропускає.",
"walkthrough.blocked.serverUnsupported.title": "Цей сервер не підтримує walkthrough",
"walkthrough.blocked.serverUnsupported.description": "Сервер OpenChamber, до якого підключено застосунок, не відповів на walkthrough API — отже, він старіший за застосунок. Оновіть сервер до 1.18 або новішої версії та оновіть панель.",
"walkthrough.blocked.contextTooSmall.descriptionUnknownModel": "Small model вміщає близько {available} тис. символів, а цьому diff потрібно близько {required} тис. Нічого не обрізається — оберіть модель із більшим контекстом.",
"walkthrough.blocked.structuredOutput.descriptionUnknownModel": "Small model не підтримує структуровані відповіді, потрібні для розбору.",
"contextRail.surface.plan.description": "Перегляд поточного плану",
@@ -1618,6 +1634,10 @@ export const dict: Record<I18nKey, string> = {
"directoryExplorerDialog.browse.directories": "Каталоги",
"directoryExplorerDialog.browse.loading": "Завантаження каталогів...",
"directoryExplorerDialog.browse.empty": "Немає відповідних каталогів.",
"directoryExplorerDialog.browse.permissionDenied": "OpenChamber потрібен доступ до цієї папки.",
"directoryExplorerDialog.browse.loadFailed": "Не вдалося завантажити цю папку.",
"directoryExplorerDialog.browse.grantAccess": "Надати доступ",
"directoryExplorerDialog.browse.retry": "Спробувати знову",
"directoryExplorerDialog.browse.parentDirectory": "Батьківський каталог",
"directoryExplorerDialog.browse.addedBadge": "Додано",
"directoryExplorerDialog.browse.quickAdd": "Додати",
@@ -1684,7 +1704,7 @@ export const dict: Record<I18nKey, string> = {
"helpDialog.item.toggleTerminalExpanded": "Розгорнути або згорнути термінал",
"helpDialog.item.togglePlanContextPanel": "Перемкнути панель контексту плану",
"helpDialog.item.cycleTheme": "Перемкнути тему (Світла → Темна → Системна)",
"helpDialog.item.switchProject": "Перемкнути проєкт",
"helpDialog.item.switchContextSurface": "Перемкнути поверхню панелі контексту (цифрова клавіша)",
"helpDialog.item.toggleServicesMenu": "Перемкнути меню сервісів",
"helpDialog.item.cycleServicesTab": "Перемкнути вкладку сервісів",
"helpDialog.item.openSettings": "Відкрити налаштування",
@@ -1969,6 +1989,8 @@ export const dict: Record<I18nKey, string> = {
"sessions.sidebar.group.empty.loadingSessions": "Завантаження сесій…",
"sessions.sidebar.group.empty.loadFailed": "Не вдалося оновити сесії.",
"sessions.sidebar.group.empty.retry": "Спробувати знову",
"sessions.sidebar.group.empty.permissionDenied": "Потрібен доступ до папки.",
"sessions.sidebar.group.empty.grantAccess": "Надати доступ",
"chat.unifiedControls.title": "Елементи керування",
"chat.unifiedControls.model.title": "Модель",
"chat.unifiedControls.model.noRecent": "Немає останніх моделей",
@@ -2896,6 +2918,9 @@ export const dict: Record<I18nKey, string> = {
"common.relative.daysAgoCompact": "{count}d ago",
"common.relative.weeksAgoCompact": "{count}w ago",
"common.relative.yearsAgoCompact": "{count}y ago",
"common.duration.secondsCompact": "{seconds}с",
"common.duration.minutesSecondsCompact": "{minutes}хв {seconds}с",
"common.duration.hoursMinutesCompact": "{hours}год {minutes}хв",
"contextFileOpen.failure.tooLarge": "File is too large to open (>{count} lines)",
"contextFileOpen.failure.missing": "File not found",
"contextFileOpen.failure.unreadable": "Failed to open file",
@@ -444,6 +444,9 @@ export const settingsDict = {
'settings.openchamber.about.title': '关于 OpenChamber',
'settings.openchamber.about.field.version': '版本',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
'settings.openchamber.about.field.instanceUrls': '实例 URL',
'settings.openchamber.about.field.applicationUrl': '应用',
'settings.openchamber.about.field.tunnelUrl': '隧道',
'settings.openchamber.about.state.checking': '检查中...',
'settings.openchamber.about.state.upToDate': '已是最新',
'settings.openchamber.about.state.unknown': '未知',
@@ -1088,6 +1091,8 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切换上下文面板',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '打开 Git 界面',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '打开文件界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切换上下文面板界面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
@@ -1372,6 +1377,17 @@ export const settingsDict = {
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth 方式 {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': '粘贴授权码',
'settings.providers.page.auth.oauth.starting': '正在启动授权…',
'settings.providers.page.auth.oauth.waiting': '正在等待授权…',
'settings.providers.page.auth.oauth.waitingHint': '请在浏览器中完成登录。保持此页面打开,连接会自动完成。',
'settings.providers.page.auth.oauth.codeHint': '从浏览器复制授权码并粘贴到此处。',
'settings.providers.page.auth.oauth.deviceCodeLabel': '设备码',
'settings.providers.page.auth.oauth.linkLabel': '授权链接',
'settings.providers.page.auth.oauth.promptRequired': '请填写“{field}”后继续',
'settings.providers.page.auth.oauth.error.sessionExpired': '授权请求已过期。请重新连接以重新开始。',
'settings.providers.page.auth.oauth.error.codeRequired': '此提供方需要浏览器中的授权码。',
'settings.providers.page.auth.oauth.error.declined': '授权被拒绝或未完成。',
'settings.providers.page.auth.oauth.error.invalidInput': '输入的信息被拒绝。',
'settings.providers.page.auth.connected': '已连接',
'settings.providers.page.auth.incomplete': '缺少凭据',
'settings.providers.page.auth.incompleteHint': '· 在聊天中使用此提供商之前,请添加 API 密钥或 {env:VAR}',
@@ -1404,6 +1420,9 @@ export const settingsDict = {
'settings.providers.page.actions.open': '打开',
'settings.providers.page.actions.copy': '复制',
'settings.providers.page.actions.complete': '完成',
'settings.providers.page.actions.continue': '继续',
'settings.providers.page.actions.cancel': '取消',
'settings.providers.page.actions.tryAgain': '重试',
'settings.providers.page.actions.hide': '隐藏',
'settings.providers.page.actions.reconnect': '重新连接',
'settings.providers.page.actions.edit': '编辑',
@@ -1419,7 +1438,6 @@ export const settingsDict = {
'settings.providers.page.toast.apiKeySaved': 'API Key 已保存',
'settings.providers.page.toast.oauthStartFailed': '启动 OAuth 流程失败',
'settings.providers.page.toast.oauthDetailsMissing': '未返回 OAuth 详情',
'settings.providers.page.toast.completeOAuthInBrowser': '请在浏览器中完成 OAuth 流程',
'settings.providers.page.toast.oauthCompleteFailed': '完成 OAuth 流程失败',
'settings.providers.page.toast.oauthCompleted': 'OAuth 连接已完成',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth 链接已复制',
+27 -2
View File
@@ -269,6 +269,9 @@ export const dict: Record<I18nKey, string> = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': '暂停 {taskName}',
'sessions.scheduledTasks.dialog.taskToggle.enabled': '已启用',
'sessions.scheduledTasks.dialog.taskToggle.paused': '已暂停',
'sessions.scheduledTasks.dialog.loopFile.note': '由循环文件 {file} 管理',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': '启用状态由循环文件控制;请在 Markdown frontmatter 中设置 enabled',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': '循环任务在其 .agents/loops Markdown 文件中配置',
'sessions.scheduledTasks.editor.title.edit': '编辑计划任务',
'sessions.scheduledTasks.editor.title.new': '新建计划任务',
'sessions.scheduledTasks.editor.description': '配置一个服务端任务,用于创建新会话并发送提示词。',
@@ -531,6 +534,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.status.pinned': '已置顶会话',
'sessions.sidebar.session.status.movingToWorktree': '正在将会话移至新工作树',
'sessions.sidebar.session.status.permissionRequired': '需要权限',
'sessions.sidebar.session.status.questionPendingSingle': '1 个待回答问题',
'sessions.sidebar.session.status.questionPendingMany': '{count} 个待回答问题',
'sessions.sidebar.session.status.activeFor': '已活动 {duration}',
'sessions.sidebar.session.status.lastTurnDuration': '上一轮耗时 {duration}',
'sessions.sidebar.session.subsessions.collapse': '折叠子会话',
'sessions.sidebar.session.subsessions.expand': '展开子会话',
'sessions.sidebar.dialogs.deleteSession.title': '删除会话?',
@@ -1105,6 +1112,10 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。',
'contextRail.surface.editor.description': '编辑项目文件',
'contextRail.surface.git.description': '提交、分支和拉取请求',
'contextRail.surface.git.changesCountAriaSingle': '{label}{count} 个更改的文件',
'contextRail.surface.git.changesCountAriaPlural': '{label}{count} 个更改的文件',
'contextRail.surface.git.changesCountTooltipSingle': '{count} 个更改的文件',
'contextRail.surface.git.changesCountTooltipPlural': '{count} 个更改的文件',
'contextRail.surface.terminal.description': '内置终端',
'contextRail.surface.diff.description': '查看工作区更改',
'contextPanel.mode.walkthrough': '导读',
@@ -1144,8 +1155,11 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.toc.moreFiles': '其他文件:{count}',
'walkthrough.toc.uncovered': '未涵盖:{count}',
'walkthrough.toc.resize': '调整目录栏宽度',
'walkthrough.importance.critical': '关键',
'walkthrough.importance.critical': '关键改动',
'walkthrough.importance.criticalHint': '这一步带动了其余改动,值得仔细阅读。它不是在你的代码中发现的问题。',
'walkthrough.importance.context': '背景',
'walkthrough.importance.contextHint': '辅助性的改动,列在这里是为了让其余部分说得通。',
'walkthrough.help.guide': 'Walkthrough 的工作方式',
'walkthrough.blocked.noModel.title': '没有可用的小模型',
'walkthrough.blocked.noModel.description': '请登录模型提供方后再生成评审。',
'walkthrough.blocked.emptyDiff.title': '没有可评审的内容',
@@ -1160,6 +1174,8 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': '小模型把全部输出额度用在了推理上,没有返回结果。推理模型在大差异上经常如此——可以换一个少推理的模型,或缩小评审范围。',
'walkthrough.blocked.onlyGenerated.title': '只有生成文件发生了改动',
'walkthrough.blocked.onlyGenerated.description': '这里的改动全部是锁文件或其他工具生成的产物,评审会有意跳过它们。',
'walkthrough.blocked.serverUnsupported.title': '该服务器不支持 walkthrough',
'walkthrough.blocked.serverUnsupported.description': '此应用连接的 OpenChamber 服务器没有响应 walkthrough API,说明它比应用更旧。请将服务器升级到 1.18 或更高版本后刷新。',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': '小模型大约可容纳 {available} 千字符,而这份差异约需 {required} 千字符。我们不会截断内容,请改选上下文更大的模型。',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '小模型不支持导读所需的结构化响应。',
'contextRail.surface.plan.description': '查看当前计划',
@@ -1606,6 +1622,10 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.browse.directories': '目录',
'directoryExplorerDialog.browse.loading': '正在加载目录...',
'directoryExplorerDialog.browse.empty': '没有匹配的目录。',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber 需要访问此文件夹。',
'directoryExplorerDialog.browse.loadFailed': '无法加载此文件夹。',
'directoryExplorerDialog.browse.grantAccess': '授予访问权限',
'directoryExplorerDialog.browse.retry': '重试',
'directoryExplorerDialog.browse.parentDirectory': '上级目录',
'directoryExplorerDialog.browse.addedBadge': '已添加',
'directoryExplorerDialog.browse.quickAdd': '添加',
@@ -1672,7 +1692,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.toggleTerminalExpanded': '切换终端展开状态',
'helpDialog.item.togglePlanContextPanel': '切换计划上下文面板',
'helpDialog.item.cycleTheme': '循环切换主题(浅色 → 深色 → 跟随系统)',
'helpDialog.item.switchProject': '切换项目',
'helpDialog.item.switchContextSurface': '切换上下文面板界面(数字键)',
'helpDialog.item.toggleServicesMenu': '切换服务菜单',
'helpDialog.item.cycleServicesTab': '循环服务标签',
'helpDialog.item.openSettings': '打开设置',
@@ -1957,6 +1977,8 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.group.empty.loadingSessions': '正在加载会话…',
'sessions.sidebar.group.empty.loadFailed': '无法刷新会话。',
'sessions.sidebar.group.empty.retry': '重试',
'sessions.sidebar.group.empty.permissionDenied': '需要文件夹访问权限。',
'sessions.sidebar.group.empty.grantAccess': '授予访问权限',
'chat.unifiedControls.title': '控制',
'chat.unifiedControls.model.title': '模型',
'chat.unifiedControls.model.noRecent': '没有最近使用的模型',
@@ -2896,6 +2918,9 @@ export const dict: Record<I18nKey, string> = {
'common.relative.daysAgoCompact': '{count}d ago',
'common.relative.weeksAgoCompact': '{count}w ago',
'common.relative.yearsAgoCompact': '{count}y ago',
'common.duration.secondsCompact': '{seconds}秒',
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
'common.duration.hoursMinutesCompact': '{hours}小时{minutes}分',
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
'contextFileOpen.failure.missing': 'File not found',
'contextFileOpen.failure.unreadable': 'Failed to open file',
@@ -441,6 +441,9 @@
'settings.openchamber.about.title': '關於 OpenChamber',
'settings.openchamber.about.field.version': '版本',
'settings.openchamber.about.field.openCodeVersion': 'OpenCode 版本',
'settings.openchamber.about.field.instanceUrls': '執行個體 URL',
'settings.openchamber.about.field.applicationUrl': '應用程式',
'settings.openchamber.about.field.tunnelUrl': '隧道',
'settings.openchamber.about.state.checking': '檢查中...',
'settings.openchamber.about.state.upToDate': '已是最新',
'settings.openchamber.about.state.unknown': '未知',
@@ -995,6 +998,8 @@
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切換上下文面板',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '開啟 Git 介面',
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_files.label': '開啟檔案介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.label': '切換上下文面板介面',
'settings.openchamber.keyboardShortcuts.action.switch_context_surface.suffix': ' + 1…0',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建工作階段',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新增 worktree 草稿',
'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 視窗',
@@ -1278,6 +1283,17 @@
'settings.providers.page.auth.apiKeyPlaceholder': 'sk-...',
'settings.providers.page.auth.oauthMethodFallback': 'OAuth 方式 {index}',
'settings.providers.page.auth.pasteAuthorizationCodePlaceholder': '貼上授權碼',
'settings.providers.page.auth.oauth.starting': '正在啟動授權…',
'settings.providers.page.auth.oauth.waiting': '正在等待授權…',
'settings.providers.page.auth.oauth.waitingHint': '請在瀏覽器中完成登入。保持此頁面開啟,連線會自動完成。',
'settings.providers.page.auth.oauth.codeHint': '從瀏覽器複製授權碼並貼上到這裡。',
'settings.providers.page.auth.oauth.deviceCodeLabel': '裝置碼',
'settings.providers.page.auth.oauth.linkLabel': '授權連結',
'settings.providers.page.auth.oauth.promptRequired': '請填寫「{field}」後繼續',
'settings.providers.page.auth.oauth.error.sessionExpired': '授權請求已過期。請重新連線以重新開始。',
'settings.providers.page.auth.oauth.error.codeRequired': '此提供者需要瀏覽器中的授權碼。',
'settings.providers.page.auth.oauth.error.declined': '授權遭拒或未完成。',
'settings.providers.page.auth.oauth.error.invalidInput': '輸入的資訊遭拒。',
'settings.providers.page.auth.connected': '已連線',
'settings.providers.page.auth.incomplete': '缺少憑證',
'settings.providers.page.auth.incompleteHint': '· 在聊天中使用此提供者之前,請新增 API 金鑰或 {env:VAR}',
@@ -1310,6 +1326,9 @@
'settings.providers.page.actions.open': '開啟',
'settings.providers.page.actions.copy': '複製',
'settings.providers.page.actions.complete': '完成',
'settings.providers.page.actions.continue': '繼續',
'settings.providers.page.actions.cancel': '取消',
'settings.providers.page.actions.tryAgain': '重試',
'settings.providers.page.actions.hide': '隱藏',
'settings.providers.page.actions.reconnect': '重新連線',
'settings.providers.page.actions.edit': '編輯',
@@ -1325,7 +1344,6 @@
'settings.providers.page.toast.apiKeySaved': 'API Key 已儲存',
'settings.providers.page.toast.oauthStartFailed': '啟動 OAuth 流程失敗',
'settings.providers.page.toast.oauthDetailsMissing': '未回傳 OAuth 詳情',
'settings.providers.page.toast.completeOAuthInBrowser': '請在瀏覽器中完成 OAuth 流程',
'settings.providers.page.toast.oauthCompleteFailed': '完成 OAuth 流程失敗',
'settings.providers.page.toast.oauthCompleted': 'OAuth 連線已完成',
'settings.providers.page.toast.oauthLinkCopied': 'OAuth 連結已複製',
+27 -2
View File
@@ -282,6 +282,9 @@ export const dict: Record<I18nKey, string> = {
'sessions.scheduledTasks.dialog.taskToggle.pauseAria': '暫停 {taskName}',
'sessions.scheduledTasks.dialog.taskToggle.enabled': '已啟用',
'sessions.scheduledTasks.dialog.taskToggle.paused': '已暫停',
'sessions.scheduledTasks.dialog.loopFile.note': '由迴圈檔案 {file} 管理',
'sessions.scheduledTasks.dialog.loopFile.toggleDisabled': '啟用狀態由迴圈檔案控制;請在 Markdown frontmatter 中設定 enabled',
'sessions.scheduledTasks.dialog.loopFile.actionsDisabled': '迴圈任務在其 .agents/loops Markdown 檔案中設定',
'sessions.scheduledTasks.editor.title.edit': '編輯排程任務',
'sessions.scheduledTasks.editor.title.new': '新增排程任務',
'sessions.scheduledTasks.editor.description': '設定一個伺服器端任務,用於建立新會話並傳送提示詞。',
@@ -544,6 +547,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.session.status.pinned': '已釘選會話',
'sessions.sidebar.session.status.movingToWorktree': '正在將會話移至新工作樹',
'sessions.sidebar.session.status.permissionRequired': '需要權限',
'sessions.sidebar.session.status.questionPendingSingle': '1 個待回答問題',
'sessions.sidebar.session.status.questionPendingMany': '{count} 個待回答問題',
'sessions.sidebar.session.status.activeFor': '已活動 {duration}',
'sessions.sidebar.session.status.lastTurnDuration': '上一輪耗時 {duration}',
'sessions.sidebar.session.subsessions.collapse': '摺疊子會話',
'sessions.sidebar.session.subsessions.expand': '展開子會話',
'sessions.sidebar.dialogs.deleteSession.title': '刪除會話?',
@@ -1117,6 +1124,10 @@ export const dict: Record<I18nKey, string> = {
'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。',
'contextRail.surface.editor.description': '編輯專案檔案',
'contextRail.surface.git.description': '提交、分支與拉取請求',
'contextRail.surface.git.changesCountAriaSingle': '{label}{count} 個變更的檔案',
'contextRail.surface.git.changesCountAriaPlural': '{label}{count} 個變更的檔案',
'contextRail.surface.git.changesCountTooltipSingle': '{count} 個變更的檔案',
'contextRail.surface.git.changesCountTooltipPlural': '{count} 個變更的檔案',
'contextRail.surface.terminal.description': '內建終端機',
'contextRail.surface.diff.description': '檢視工作區變更',
'contextPanel.mode.walkthrough': '導讀',
@@ -1156,8 +1167,11 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.toc.moreFiles': '其他檔案:{count}',
'walkthrough.toc.uncovered': '未涵蓋:{count}',
'walkthrough.toc.resize': '調整目錄欄寬度',
'walkthrough.importance.critical': '關鍵',
'walkthrough.importance.critical': '關鍵變更',
'walkthrough.importance.criticalHint': '這一步帶動了其餘變更,值得仔細閱讀。它不是在你的程式碼中發現的問題。',
'walkthrough.importance.context': '背景',
'walkthrough.importance.contextHint': '輔助性的變更,列在這裡是為了讓其餘部分說得通。',
'walkthrough.help.guide': 'Walkthrough 的運作方式',
'walkthrough.blocked.noModel.title': '沒有可用的小模型',
'walkthrough.blocked.noModel.description': '請先登入模型供應商再產生審閱。',
'walkthrough.blocked.emptyDiff.title': '沒有可審閱的內容',
@@ -1172,6 +1186,8 @@ export const dict: Record<I18nKey, string> = {
'walkthrough.blocked.outputExhausted.descriptionUnknownModel': '小模型把全部輸出額度用在推理上,沒有回傳結果。推理模型在大型差異上經常如此——可以改用較少推理的模型,或縮小審閱範圍。',
'walkthrough.blocked.onlyGenerated.title': '只有產生的檔案有變動',
'walkthrough.blocked.onlyGenerated.description': '這裡的變更全部是鎖定檔或其他工具產生的輸出,審閱會刻意略過它們。',
'walkthrough.blocked.serverUnsupported.title': '該伺服器不支援 walkthrough',
'walkthrough.blocked.serverUnsupported.description': '此應用程式連線的 OpenChamber 伺服器沒有回應 walkthrough API,代表它比應用程式更舊。請將伺服器升級到 1.18 或更新版本後重新整理。',
'walkthrough.blocked.contextTooSmall.descriptionUnknownModel': '小模型大約可容納 {available} 千字元,而這份差異約需 {required} 千字元。我們不會截斷內容,請改選上下文更大的模型。',
'walkthrough.blocked.structuredOutput.descriptionUnknownModel': '小模型不支援導讀所需的結構化回應。',
'contextRail.surface.plan.description': '檢視目前計畫',
@@ -1610,6 +1626,10 @@ export const dict: Record<I18nKey, string> = {
'directoryExplorerDialog.browse.directories': '目錄',
'directoryExplorerDialog.browse.loading': '正在載入目錄...',
'directoryExplorerDialog.browse.empty': '沒有符合的目錄。',
'directoryExplorerDialog.browse.permissionDenied': 'OpenChamber 需要存取此資料夾。',
'directoryExplorerDialog.browse.loadFailed': '無法載入此資料夾。',
'directoryExplorerDialog.browse.grantAccess': '授予存取權限',
'directoryExplorerDialog.browse.retry': '再試一次',
'directoryExplorerDialog.browse.parentDirectory': '上層目錄',
'directoryExplorerDialog.browse.addedBadge': '已新增',
'directoryExplorerDialog.browse.quickAdd': '添加',
@@ -1676,7 +1696,7 @@ export const dict: Record<I18nKey, string> = {
'helpDialog.item.toggleTerminalExpanded': '切換終端機展開狀態',
'helpDialog.item.togglePlanContextPanel': '切換計畫上下文面板',
'helpDialog.item.cycleTheme': '循環切換主題(淺色 → 深色 → 跟隨系統)',
'helpDialog.item.switchProject': '切換專案',
'helpDialog.item.switchContextSurface': '切換上下文面板介面(數字鍵)',
'helpDialog.item.toggleServicesMenu': '切換服務選單',
'helpDialog.item.cycleServicesTab': '循環服務標籤',
'helpDialog.item.openSettings': '開啟設定',
@@ -1961,6 +1981,8 @@ export const dict: Record<I18nKey, string> = {
'sessions.sidebar.group.empty.loadingSessions': '正在載入工作階段…',
'sessions.sidebar.group.empty.loadFailed': '無法重新整理工作階段。',
'sessions.sidebar.group.empty.retry': '再試一次',
'sessions.sidebar.group.empty.permissionDenied': '需要資料夾存取權限。',
'sessions.sidebar.group.empty.grantAccess': '授予存取權限',
'chat.unifiedControls.title': '控制',
'chat.unifiedControls.model.title': '模型',
'chat.unifiedControls.model.noRecent': '沒有最近使用的模型',
@@ -2895,6 +2917,9 @@ export const dict: Record<I18nKey, string> = {
'common.relative.daysAgoCompact': '{count}d ago',
'common.relative.weeksAgoCompact': '{count}w ago',
'common.relative.yearsAgoCompact': '{count}y ago',
'common.duration.secondsCompact': '{seconds}秒',
'common.duration.minutesSecondsCompact': '{minutes}分{seconds}秒',
'common.duration.hoursMinutesCompact': '{hours}小時{minutes}分',
'contextFileOpen.failure.tooLarge': 'File is too large to open (>{count} lines)',
'contextFileOpen.failure.missing': 'File not found',
'contextFileOpen.failure.unreadable': 'Failed to open file',
@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, test } from 'bun:test';
import { MessageFreshnessDetector } from './messageFreshness';
import type { Message } from '@opencode-ai/sdk/v2';
const makeAssistantMessage = (id: string, created: number): Message =>
({
id,
role: 'assistant',
sessionID: 'session-a',
time: { created },
}) as unknown as Message;
describe('MessageFreshnessDetector.shouldAnimateMessage', () => {
let detector: MessageFreshnessDetector;
beforeEach(() => {
detector = MessageFreshnessDetector.getInstance();
detector.clearAll();
});
test('fresh message animates once and is recorded as seen', () => {
detector.recordSessionStart('session-a');
const message = makeAssistantMessage('msg-fresh', Date.now());
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(true);
expect(detector.hasBeenAnimated('msg-fresh')).toBe(true);
});
test('regression #2124: fresh message does not re-animate when returning to the session', () => {
detector.recordSessionStart('session-a');
const message = makeAssistantMessage('msg-fresh', Date.now());
// First visit: the message is fresh and animates.
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(true);
// User switches away and back; ChatViewport remounts and re-evaluates
// before recordSessionStart runs again, so the old session start time
// is still in effect. The message must not animate a second time.
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
});
test('stale history message never animates and is recorded as seen', () => {
detector.recordSessionStart('session-a');
const message = makeAssistantMessage('msg-old', Date.now() - 60_000);
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
expect(detector.hasBeenAnimated('msg-old')).toBe(true);
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
});
test('message evaluated without a recorded session start does not animate and is recorded', () => {
const message = makeAssistantMessage('msg-no-session', Date.now());
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
expect(detector.hasBeenAnimated('msg-no-session')).toBe(true);
// Recording the session start afterwards must not resurrect the animation.
detector.recordSessionStart('session-a');
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
});
test('non-assistant messages never animate', () => {
detector.recordSessionStart('session-a');
const message = {
id: 'msg-user',
role: 'user',
sessionID: 'session-a',
time: { created: Date.now() },
} as unknown as Message;
expect(detector.shouldAnimateMessage(message, 'session-a')).toBe(false);
});
test('a new fresh message still animates after older fresh messages were seen', () => {
detector.recordSessionStart('session-a');
const first = makeAssistantMessage('msg-first', Date.now());
const second = makeAssistantMessage('msg-second', Date.now());
expect(detector.shouldAnimateMessage(first, 'session-a')).toBe(true);
expect(detector.shouldAnimateMessage(second, 'session-a')).toBe(true);
expect(detector.shouldAnimateMessage(second, 'session-a')).toBe(false);
});
});
+7 -4
View File
@@ -44,10 +44,13 @@ export class MessageFreshnessDetector {
const isFresh = message.time.created > (sessionStartTime - 5000);
if (!isFresh) {
this.seenMessageIds.add(message.id);
this.messageCreationTimes.set(message.id, message.time.created);
}
// Record fresh messages too so they animate at most once per detector
// lifetime. The detector is a module singleton that outlives ChatViewport
// remounts; without this, switching away and back re-evaluates the same
// message against the stale session start time (recordSessionStart runs
// in an effect after the first render) and replays the entry animation.
this.seenMessageIds.add(message.id);
this.messageCreationTimes.set(message.id, message.time.created);
return isFresh;
}
@@ -0,0 +1,145 @@
import { describe, expect, test } from 'bun:test'
import type { Message, Part } from '@opencode-ai/sdk/v2'
import {
extractUserModelChoice,
findLatestUserModelChoice,
shouldPreserveManualModelOverride,
} from './userModelChoice'
const userMessage = (
id: string,
model: { providerID: string; modelID: string },
agent = 'custom-agent',
): Message => ({
id,
sessionID: 'ses_1',
role: 'user',
time: { created: 1 },
agent,
model,
} as Message)
const assistantMessage = (id: string): Message => ({
id,
sessionID: 'ses_1',
role: 'assistant',
time: { created: 2 },
parentID: 'u1',
modelID: 'model-a',
providerID: 'provider',
} as Message)
const textPart = (id: string, text: string, synthetic = false): Part => ({
id,
sessionID: 'ses_1',
messageID: 'u1',
type: 'text',
text,
...(synthetic ? { synthetic: true } : {}),
} as Part)
describe('findLatestUserModelChoice', () => {
test('returns the latest real user prompt model', () => {
const messages = [
userMessage('u1', { providerID: 'provider', modelID: 'model-a' }),
assistantMessage('a1'),
userMessage('u2', { providerID: 'provider', modelID: 'model-b' }),
]
const partsById: Record<string, Part[]> = {
u1: [textPart('p1', 'first')],
u2: [textPart('p2', 'second')],
}
const choice = findLatestUserModelChoice(messages, (id) => partsById[id])
expect(choice?.id).toBe('u2')
expect(choice?.modelID).toBe('model-b')
expect(choice?.providerID).toBe('provider')
expect(choice?.agent).toBe('custom-agent')
})
test('[issue-2404] skips synthetic subagent-completion nudges so manual override is not clobbered', () => {
// Real prompt sent with the manual override (model-b).
const realPrompt = userMessage('u-real', { providerID: 'provider', modelID: 'model-b' })
// After a delegated child session goes idle, OpenCode injects a synthetic
// user nudge that often carries the agent default model (model-a).
const syntheticNudge = userMessage('u-nudge', { providerID: 'provider', modelID: 'model-a' })
const messages = [realPrompt, assistantMessage('a1'), syntheticNudge]
const partsById: Record<string, Part[]> = {
'u-real': [textPart('p-real', 'please investigate', false)],
'u-nudge': [textPart('p-nudge', 'Subagent finished.', true)],
}
const choice = findLatestUserModelChoice(messages, (id) => partsById[id])
expect(choice?.id).toBe('u-real')
expect(choice?.modelID).toBe('model-b')
})
test('skips user messages whose parts have not loaded yet', () => {
const messages = [
userMessage('u1', { providerID: 'provider', modelID: 'model-a' }),
userMessage('u2', { providerID: 'provider', modelID: 'model-b' }),
]
const partsById: Record<string, Part[]> = {
u1: [textPart('p1', 'first')],
// u2 parts missing
}
const choice = findLatestUserModelChoice(messages, (id) => partsById[id])
expect(choice?.id).toBe('u1')
expect(choice?.modelID).toBe('model-a')
})
test('returns null when only synthetic user messages exist', () => {
const messages = [userMessage('u-nudge', { providerID: 'provider', modelID: 'model-a' })]
const partsById: Record<string, Part[]> = {
'u-nudge': [textPart('p-nudge', 'Subagent finished.', true)],
}
expect(findLatestUserModelChoice(messages, (id) => partsById[id])).toBeNull()
})
})
describe('shouldPreserveManualModelOverride', () => {
test('preserves manual override when it differs from the candidate message model', () => {
expect(shouldPreserveManualModelOverride({
selectionSource: 'manual',
savedSessionModel: { providerId: 'provider', modelId: 'model-b' },
candidate: { providerID: 'provider', modelID: 'model-a' },
})).toBe(true)
})
test('does not preserve when selection matches the candidate', () => {
expect(shouldPreserveManualModelOverride({
selectionSource: 'manual',
savedSessionModel: { providerId: 'provider', modelId: 'model-b' },
candidate: { providerID: 'provider', modelID: 'model-b' },
})).toBe(false)
})
test('does not preserve auto selections', () => {
expect(shouldPreserveManualModelOverride({
selectionSource: 'auto',
savedSessionModel: { providerId: 'provider', modelId: 'model-b' },
candidate: { providerID: 'provider', modelID: 'model-a' },
})).toBe(false)
})
test('preserves manual override when candidate has no model', () => {
expect(shouldPreserveManualModelOverride({
selectionSource: 'manual',
savedSessionModel: { providerId: 'provider', modelId: 'model-b' },
candidate: { providerID: undefined, modelID: undefined },
})).toBe(true)
})
})
describe('extractUserModelChoice', () => {
test('reads variant from model.variant', () => {
const message = {
...userMessage('u1', { providerID: 'provider', modelID: 'model-b' }),
model: { providerID: 'provider', modelID: 'model-b', variant: 'high' },
} as Message
expect(extractUserModelChoice(message as never)?.variant).toBe('high')
})
})
@@ -0,0 +1,103 @@
import type { Message, Part } from '@opencode-ai/sdk/v2'
import { isFullySyntheticMessage } from './synthetic'
type UserModelChoice = {
id: string
agent?: string
providerID?: string
modelID?: string
variant?: string
}
type MessageLike = Message & {
model?: { providerID?: string; modelID?: string; variant?: string }
variant?: string
mode?: string
}
/**
* Extract agent/model selection metadata from a user message, if present.
*/
export const extractUserModelChoice = (message: MessageLike): UserModelChoice | null => {
if (message.role !== 'user') {
return null
}
const providerID = typeof message.model?.providerID === 'string' && message.model.providerID.trim().length > 0
? message.model.providerID
: undefined
const modelID = typeof message.model?.modelID === 'string' && message.model.modelID.trim().length > 0
? message.model.modelID
: undefined
const agent = typeof message.agent === 'string' && message.agent.trim().length > 0
? message.agent
: (typeof message.mode === 'string' && message.mode.trim().length > 0 ? message.mode : undefined)
// OpenCode 1.4.0 moved variant from top-level to model.variant.
const variantCandidate = message.model?.variant ?? message.variant
const variant = typeof variantCandidate === 'string' && variantCandidate.trim().length > 0
? variantCandidate
: undefined
return { id: message.id, agent, providerID, modelID, variant }
}
/**
* Find the latest *real* user prompt's model/agent choice.
*
* Synthetic user messages (e.g. subagent-completion nudges injected when a
* delegated child session goes idle) must not drive the composer model
* selector restoring from them clobber a manual session override and reset
* to the agent default.
*
* Messages whose parts have not been loaded yet are skipped so an incomplete
* snapshot cannot be treated as authoritative.
*/
export const findLatestUserModelChoice = (
messages: readonly MessageLike[],
getParts: (messageId: string) => Part[] | undefined,
): UserModelChoice | null => {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i]
if (message.role !== 'user') {
continue
}
const parts = getParts(message.id)
if (!Array.isArray(parts) || parts.length === 0) {
continue
}
if (isFullySyntheticMessage(parts)) {
continue
}
return extractUserModelChoice(message)
}
return null
}
/**
* When the user has a manual session model override, historical (or synthetic)
* user-message metadata must not overwrite it. After a real send the selection
* store is updated to match the message, so a conflict means the picker was
* changed after the last prompt keep the override.
*/
export const shouldPreserveManualModelOverride = ({
selectionSource,
savedSessionModel,
candidate,
}: {
selectionSource: 'auto' | 'manual' | undefined
savedSessionModel: { providerId: string; modelId: string } | null | undefined
candidate: Pick<UserModelChoice, 'providerID' | 'modelID'> | null | undefined
}): boolean => {
if (selectionSource !== 'manual' || !savedSessionModel?.providerId || !savedSessionModel.modelId) {
return false
}
if (!candidate?.providerID || !candidate.modelID) {
return true
}
return savedSessionModel.providerId !== candidate.providerID
|| savedSessionModel.modelId !== candidate.modelID
}
+33 -1
View File
@@ -6,6 +6,7 @@ type ConfigResponse = { data: Record<string, unknown> };
const configResolvers: Array<(response: ConfigResponse) => void> = [];
let configCalls = 0;
let runtimeKey = 'test-runtime';
const promptAsyncCalls: unknown[][] = [];
const promptAsyncResults: Array<unknown> = [];
@@ -44,7 +45,7 @@ mock.module('@/lib/runtime-url', () => ({
mock.module('@/lib/runtime-switch', () => ({
getRuntimeApiBaseUrl: mock(() => ''),
getRuntimeKey: mock(() => 'test-runtime'),
getRuntimeKey: mock(() => runtimeKey),
}));
mock.module('@/lib/runtime-fetch', () => ({
@@ -60,6 +61,7 @@ mock.module('@/lib/startupTrace', () => ({
const { opencodeClient } = await import(`./client?cache-test=${Date.now()}`);
beforeEach(() => {
runtimeKey = 'test-runtime';
promptAsyncCalls.length = 0;
promptAsyncResults.length = 0;
});
@@ -160,4 +162,34 @@ describe('opencodeClient prompt retry behavior', () => {
expect(promptAsyncCalls.length).toBe(1);
expect(error instanceof Error ? error.message : String(error)).toContain('Failed to send message (503)');
});
test('does not dispatch after the runtime changes while preparing attachments', async () => {
runtimeKey = 'runtime-a';
const pending = opencodeClient.sendMessage({
id: 'ses_runtime_race',
providerID: 'runtime-race-provider',
modelID: 'model-a',
text: 'hello',
runtimeKey: 'runtime-a',
files: [{
type: 'file',
mime: 'text/markdown',
filename: 'notes.md',
url: 'data:text/markdown,hello',
}],
});
runtimeKey = 'runtime-b';
let error: unknown = null;
try {
await pending;
} catch (caught) {
error = caught;
}
expect(error).toBeInstanceOf(Error);
expect(error instanceof Error ? error.message : String(error)).toContain('runtime changed');
expect(promptAsyncCalls).toHaveLength(0);
});
});
+64 -45
View File
@@ -13,6 +13,7 @@ import type {
FilePartInput,
} from "@opencode-ai/sdk/v2";
import { isAmbiguousTransportFailure, markAmbiguousTransportFailure } from "@/lib/relay/transport-error";
import { FilesystemError, parseFilesystemErrorReason } from "@/lib/api/files-errors";
import type { PermissionRequest } from "@/types/permission";
import type { QuestionRequest } from "@/types/question";
@@ -269,6 +270,12 @@ class OpencodeService {
this.client = createRuntimeOpencodeClient({ baseUrl: this.baseUrl });
}
private assertRuntimeUnchanged(runtimeKey?: string): void {
if (runtimeKey && runtimeKey !== getRuntimeKey()) {
throw new Error('Message was not sent because the runtime changed.');
}
}
getBaseUrl(): string {
return this.baseUrl;
}
@@ -744,6 +751,7 @@ class OpencodeService {
}
async sendMessage(params: {
runtimeKey?: string;
id: string;
providerID: string;
modelID: string;
@@ -769,6 +777,8 @@ class OpencodeService {
};
directory?: string | null;
}): Promise<string> {
this.assertRuntimeUnchanged(params.runtimeKey);
// Use the optimistic/client-generated ID as the real user message ID so SSE
// can reconcile the echoed server message in-place.
const messageId = params.messageId ?? ascendingId("msg");
@@ -852,6 +862,7 @@ class OpencodeService {
}
assertProviderCircuitClosed(params.providerID);
this.assertRuntimeUnchanged(params.runtimeKey);
let response: Response;
@@ -918,6 +929,7 @@ class OpencodeService {
}
async sendCommand(params: {
runtimeKey?: string;
id: string;
providerID: string;
modelID: string;
@@ -929,6 +941,8 @@ class OpencodeService {
messageId?: string;
directory?: string | null;
}): Promise<string> {
this.assertRuntimeUnchanged(params.runtimeKey);
const tempMessageId = params.messageId ?? ascendingId("msg");
const parts: FilePartInput[] = [];
@@ -939,6 +953,7 @@ class OpencodeService {
}
const requestDirectory = this.normalizeCandidatePath(params.directory ?? null) ?? this.currentDirectory;
this.assertRuntimeUnchanged(params.runtimeKey);
const response = await this.client.session.command({
sessionID: params.id,
@@ -968,6 +983,7 @@ class OpencodeService {
}
async shellSession(params: {
runtimeKey?: string;
sessionId: string;
command: string;
agent: string;
@@ -975,6 +991,7 @@ class OpencodeService {
messageId?: string;
directory?: string | null;
}): Promise<{ info: Message; parts: Part[] }> {
this.assertRuntimeUnchanged(params.runtimeKey);
const requestDirectory = this.normalizeCandidatePath(params.directory ?? null) ?? this.currentDirectory;
const response = await this.client.session.shell({
sessionID: params.sessionId,
@@ -1751,20 +1768,55 @@ class OpencodeService {
}
const task = (async () => {
const desktopFiles = getDesktopFilesApi();
if (desktopFiles) {
const desktopFiles = getDesktopFilesApi();
try {
const result = await desktopFiles.listDirectory(directoryPath || '', options);
if (!result || !Array.isArray(result.entries)) {
return [];
if (desktopFiles) {
const result = await desktopFiles.listDirectory(directoryPath || '', options);
if (!result || !Array.isArray(result.entries)) {
throw new FilesystemError('Directory listing returned an invalid response', {
reason: 'invalid-response',
});
}
const entries = result.entries.map<FilesystemEntry>((entry) => ({
name: entry.name,
path: normalizeFsPath(entry.path),
isDirectory: !!entry.isDirectory,
isFile: !entry.isDirectory,
isSymbolicLink: false,
}));
this.listDirectoryCache.set(cacheKey, {
entries,
expiresAt: Date.now() + FS_LIST_CACHE_TTL_MS,
});
return entries;
}
const entries = result.entries.map<FilesystemEntry>((entry) => ({
name: entry.name,
path: normalizeFsPath(entry.path),
isDirectory: !!entry.isDirectory,
isFile: !entry.isDirectory,
isSymbolicLink: false,
}));
const params = new URLSearchParams();
if (directoryPath && directoryPath.trim().length > 0) {
params.set('path', directoryPath);
}
if (options?.respectGitignore) {
params.set('respectGitignore', 'true');
}
const query = params.toString();
const response = await runtimeFetch(`${this.baseUrl}/fs/list${query ? `?${query}` : ''}`);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const message = typeof error.error === 'string' ? error.error : 'Failed to list directory';
throw new FilesystemError(message, {
reason: parseFilesystemErrorReason((error as { reason?: unknown }).reason),
status: response.status,
});
}
const result = await response.json();
if (!result || !Array.isArray(result.entries)) {
throw new FilesystemError('Directory listing returned an invalid response', {
reason: 'invalid-response',
});
}
const entries = result.entries as FilesystemEntry[];
this.listDirectoryCache.set(cacheKey, {
entries,
expiresAt: Date.now() + FS_LIST_CACHE_TTL_MS,
@@ -1774,39 +1826,6 @@ class OpencodeService {
console.error('Failed to list directory contents:', error);
throw error;
}
}
try {
const params = new URLSearchParams();
if (directoryPath && directoryPath.trim().length > 0) {
params.set('path', directoryPath);
}
if (options?.respectGitignore) {
params.set('respectGitignore', 'true');
}
const query = params.toString();
const response = await runtimeFetch(`${this.baseUrl}/fs/list${query ? `?${query}` : ''}`);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
const message = typeof error.error === 'string' ? error.error : 'Failed to list directory';
throw new Error(message);
}
const result = await response.json();
if (!result || !Array.isArray(result.entries)) {
return [];
}
const entries = result.entries as FilesystemEntry[];
this.listDirectoryCache.set(cacheKey, {
entries,
expiresAt: Date.now() + FS_LIST_CACHE_TTL_MS,
});
return entries;
} catch (error) {
console.error('Failed to list directory contents:', error);
throw error;
}
})();
const trackedTask = task.finally(() => {
+3
View File
@@ -6,6 +6,9 @@ export type ScheduledTask = {
id: string;
name: string;
enabled: boolean;
/** Absolute path of the `.agents/loops/*.md` file driving this task, when
* any. Present only for loop-sourced tasks; unknown to older clients. */
loopFile?: string;
schedule: {
kind: 'daily' | 'weekly' | 'once' | 'cron';
times?: string[];
+63
View File
@@ -1,4 +1,5 @@
import type { SidebarSection } from '@/constants/sidebar';
import type { IconName } from '@/components/icon/icons';
export type SettingsPageSlug =
| 'home'
@@ -237,3 +238,65 @@ export function resolveSettingsSlug(value: string | null | undefined): SettingsP
return 'home';
}
// Lives here (not in SettingsView) so light consumers such as the command
// palette can render settings entries without statically importing the whole
// settings surface into the eager startup graph.
export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null {
switch (slug) {
case 'general':
return 'settings-3';
case 'projects':
return 'folders';
case 'remote-instances':
return 'computer';
case 'appearance':
return 'palette';
case 'chat':
return 'chat-ai-3';
case 'magic-prompts':
return 'ai-generate-2';
case 'snippets':
return 'chat-thread';
case 'notifications':
return 'notification-3';
case 'shortcuts':
return 'command';
case 'sessions':
return 'chat-history';
case 'providers':
return 'cloud';
case 'agents':
return 'ai-agent';
case 'behavior':
return 'brain';
case 'commands':
return 'slash-commands-2';
case 'mcp':
return null;
case 'plugins':
return 'plug-2';
case 'skills.installed':
return 'book-open';
case 'skills.catalog':
return 'book';
case 'git':
return 'git-branch';
case 'usage':
return 'bar-chart-2';
case 'voice':
return 'mic';
case 'tunnel':
return 'home-office';
case 'about':
return 'information';
case 'home':
return null;
default:
return 'robot-2';
}
}
+80
View File
@@ -0,0 +1,80 @@
import { describe, expect, test } from 'bun:test';
import {
eventMatchesShortcutPrefix,
getEffectiveShortcutPrefix,
isShortcutPrefixHeld,
UNASSIGNED_SHORTCUT,
} from './shortcuts';
describe('getEffectiveShortcutPrefix', () => {
test('falls back to the action default (bare mod) when unset', () => {
expect(getEffectiveShortcutPrefix('switch_context_surface', {})).toBe('mod');
});
test('honors modifier + key overrides', () => {
expect(getEffectiveShortcutPrefix('switch_context_surface', { switch_context_surface: 'mod+p' })).toBe('mod+p');
});
test('honors modifier-only overrides', () => {
expect(getEffectiveShortcutPrefix('switch_context_surface', { switch_context_surface: 'shift' })).toBe('shift');
});
test('returns UNASSIGNED for an explicit unassignment', () => {
expect(
getEffectiveShortcutPrefix('switch_context_surface', { switch_context_surface: UNASSIGNED_SHORTCUT }),
).toBe(UNASSIGNED_SHORTCUT);
});
test('returns empty string for an unknown action', () => {
expect(getEffectiveShortcutPrefix('does_not_exist', {})).toBe('');
});
});
describe('isShortcutPrefixHeld', () => {
test('false for an unassigned prefix', () => {
expect(isShortcutPrefixHeld(UNASSIGNED_SHORTCUT, new Set(['control']))).toBe(false);
});
test('requires the prefix primary key to be held', () => {
expect(isShortcutPrefixHeld('mod+p', new Set(['control']))).toBe(false);
expect(isShortcutPrefixHeld('mod+p', new Set(['control', 'p']))).toBe(true);
});
test('requires every prefix modifier to be held', () => {
expect(isShortcutPrefixHeld('mod+shift', new Set(['control']))).toBe(false);
expect(isShortcutPrefixHeld('mod+shift', new Set(['control', 'shift']))).toBe(true);
});
});
const keydown = (key: string, mods: { meta?: boolean; ctrl?: boolean; shift?: boolean; alt?: boolean }): KeyboardEvent =>
({
key,
metaKey: mods.meta ?? false,
ctrlKey: mods.ctrl ?? false,
shiftKey: mods.shift ?? false,
altKey: mods.alt ?? false,
}) as KeyboardEvent;
describe('eventMatchesShortcutPrefix', () => {
test('matches a bare mod prefix when the primary modifier is held', () => {
expect(eventMatchesShortcutPrefix(keydown('1', { ctrl: true }), 'mod')).toBe(true);
});
test('rejects a bare mod prefix without the primary modifier', () => {
expect(eventMatchesShortcutPrefix(keydown('1', {}), 'mod')).toBe(false);
});
test('rejects when the event carries modifiers the prefix does not expect', () => {
expect(eventMatchesShortcutPrefix(keydown('1', { ctrl: true, shift: true }), 'mod')).toBe(false);
});
test('requires the prefix primary key to be held at match time', () => {
expect(eventMatchesShortcutPrefix(keydown('1', { ctrl: true }), 'mod+p', new Set(['control']))).toBe(false);
expect(eventMatchesShortcutPrefix(keydown('1', { ctrl: true }), 'mod+p', new Set(['control', 'p']))).toBe(true);
});
test('false for an unassigned prefix', () => {
expect(eventMatchesShortcutPrefix(keydown('1', { ctrl: true }), UNASSIGNED_SHORTCUT)).toBe(false);
});
});
+141 -72
View File
@@ -39,6 +39,17 @@ const DISPLAY_LABEL_MAP: Record<ShortcutModifier, string> = {
'ctrl': '⌃',
};
// Physical `event.key` values (lowercased) that satisfy each modifier while a
// chord is being held. `mod` maps to the platform primary key; on web macOS it
// accepts either Meta or Ctrl, matching eventMatchesShortcut.
const MODIFIER_KEY_ALIASES: Record<ShortcutModifier, readonly string[]> = {
'mod': isMacOS() && isDesktopShell() ? ['meta'] : isMacOS() ? ['meta', 'control'] : ['control'],
'shift': ['shift'],
'alt': ['alt'],
'option': ['alt'],
'ctrl': ['control'],
};
const KEY_LABEL_MAP: Record<string, string> = {
'comma': ',',
'period': '.',
@@ -207,6 +218,13 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
description: 'Open right sidebar and select Files',
customizable: true,
},
{
id: 'switch_context_surface',
defaultCombo: 'mod',
label: 'Switch context panel surface',
description: 'Hold the modifier and press a number to open or close the matching rail icon',
customizable: true,
},
{
id: 'new_chat',
defaultCombo: 'mod+n',
@@ -240,24 +258,6 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
label: 'Clear input',
description: 'Clear the input field',
},
{
id: 'open_diff_panel',
defaultCombo: 'mod+2',
label: 'Open diff panel',
description: 'Switch to the diff panel',
},
{
id: 'open_terminal_panel',
defaultCombo: 'mod+3',
label: 'Open terminal panel',
description: 'Switch to the terminal panel',
},
{
id: 'open_git_panel',
defaultCombo: 'mod+4',
label: 'Open git panel',
description: 'Switch to the git panel',
},
{
id: 'open_help',
defaultCombo: 'mod+.',
@@ -347,60 +347,6 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
label: 'Abort active run',
description: 'Abort the currently running task (double press)',
},
{
id: 'switch_tab_1',
defaultCombo: 'mod+1',
label: 'Switch to tab 1',
description: 'Switch to the first tab or project',
},
{
id: 'switch_tab_2',
defaultCombo: 'mod+2',
label: 'Switch to tab 2',
description: 'Switch to the second tab or project',
},
{
id: 'switch_tab_3',
defaultCombo: 'mod+3',
label: 'Switch to tab 3',
description: 'Switch to the third tab or project',
},
{
id: 'switch_tab_4',
defaultCombo: 'mod+4',
label: 'Switch to tab 4',
description: 'Switch to the fourth tab or project',
},
{
id: 'switch_tab_5',
defaultCombo: 'mod+5',
label: 'Switch to tab 5',
description: 'Switch to the fifth tab or project',
},
{
id: 'switch_tab_6',
defaultCombo: 'mod+6',
label: 'Switch to tab 6',
description: 'Switch to the sixth tab or project',
},
{
id: 'switch_tab_7',
defaultCombo: 'mod+7',
label: 'Switch to tab 7',
description: 'Switch to the seventh tab or project',
},
{
id: 'switch_tab_8',
defaultCombo: 'mod+8',
label: 'Switch to tab 8',
description: 'Switch to the eighth tab or project',
},
{
id: 'switch_tab_9',
defaultCombo: 'mod+9',
label: 'Switch to tab 9',
description: 'Switch to the ninth tab or project',
},
] as const;
export function normalizeCombo(combo: ShortcutCombo): ShortcutCombo {
@@ -610,3 +556,126 @@ export function eventMatchesShortcut(
export function getModifierLabel(): string {
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
}
/**
* Resolves the configurable prefix for chord-style shortcuts such as
* "switch context panel surface", where a trailing digit key completes the
* combo. Unlike getEffectiveShortcutCombo, modifier-only overrides (e.g. the
* bare `mod` primary key) are honored so the prefix can omit a primary key.
* Returns UNASSIGNED_SHORTCUT when the user explicitly unassigned the prefix.
*/
export function getEffectiveShortcutPrefix(
actionId: string,
overrides?: Record<string, ShortcutCombo>,
): ShortcutCombo {
const action = getShortcutAction(actionId);
if (!action) {
return '';
}
const override = overrides?.[actionId];
if (typeof override === 'string' && override.trim() !== '') {
const normalized = normalizeCombo(override);
if (normalized === UNASSIGNED_SHORTCUT) {
return UNASSIGNED_SHORTCUT;
}
if (normalized) {
const parsed = parseShortcut(normalized);
if (parsed.modifiers.size > 0 || parsed.key) {
return normalized;
}
}
}
return action.defaultCombo;
}
/**
* True when the physical keys required to "arm" a prefix combo are currently
* held. For modifiers with multiple aliases (e.g. `mod` on web macOS), at
* least one alias must be held.
*/
export function isShortcutPrefixHeld(prefixCombo: ShortcutCombo, heldKeys: ReadonlySet<string>): boolean {
if (isUnassignedShortcut(prefixCombo)) {
return false;
}
const parsed = parseShortcut(prefixCombo);
for (const modifier of parsed.modifiers) {
const aliases = MODIFIER_KEY_ALIASES[modifier];
if (!aliases.some((alias) => heldKeys.has(alias))) {
return false;
}
}
if (parsed.key && !heldKeys.has(parsed.key.toLowerCase())) {
return false;
}
return true;
}
/**
* Matches an activating keydown (the caller checks the event's own key, e.g. a
* digit) against a chord prefix: the event's modifier state must match the
* prefix's modifiers, and when the prefix has a primary key that key must
* currently be held.
*/
export function eventMatchesShortcutPrefix(
event: KeyboardEvent | React.KeyboardEvent,
prefixCombo: ShortcutCombo,
heldKeys?: ReadonlySet<string>,
): boolean {
if (isUnassignedShortcut(prefixCombo)) {
return false;
}
const parsed = parseShortcut(prefixCombo);
const expectedMod = parsed.modifiers.has('mod');
const expectedShift = parsed.modifiers.has('shift');
const expectedAlt = parsed.modifiers.has('alt');
const expectedCtrl = parsed.modifiers.has('ctrl');
const isDesktopMac = isMacOS() && isDesktopShell();
const isMac = isMacOS();
const modMatches = isDesktopMac
? event.metaKey
: isMac
? (event.metaKey || event.ctrlKey)
: event.ctrlKey;
if (expectedMod && !modMatches) {
return false;
}
if (!expectedMod && event.metaKey) {
return false;
}
if (expectedShift !== event.shiftKey) {
return false;
}
if (expectedAlt !== event.altKey) {
return false;
}
if (expectedCtrl) {
if (!event.ctrlKey) {
return false;
}
} else {
const ctrlUsedAsMod = expectedMod && !isDesktopMac && event.ctrlKey;
if (event.ctrlKey && !ctrlUsedAsMod) {
return false;
}
}
if (parsed.key && (!heldKeys || !heldKeys.has(parsed.key.toLowerCase()))) {
return false;
}
return true;
}
@@ -20,6 +20,13 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
- Rail order is user-reorderable and persisted globally in
`useUIStore.contextRailOrder`; `sortContextSurfaces` applies it on top of the
registry's default order and appends any missing surfaces.
- `getVisibleContextRailSurfaces` is the single visibility filter shared by the
rail and the global surface-switch shortcut (`switch_context_surface` in
`lib/shortcuts.ts`): it drops the plan surface unless plan mode is enabled,
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides
`has-content` surfaces until a tab of their mode exists. Both consumers use
it so the digit shown on a rail badge always maps to the same surface the
shortcut opens.
## Adding a surface
@@ -0,0 +1,53 @@
import { describe, expect, test } from 'bun:test';
import {
CONTEXT_SURFACES,
getVisibleContextRailSurfaces,
WALKTHROUGH_MIN_WIDTH,
} from './registry';
const baseOptions = {
railOrder: [],
planModeEnabled: true,
isVSCode: false,
screenWidth: 1200,
tabs: [],
} as const;
describe('getVisibleContextRailSurfaces', () => {
test('hides the plan surface while plan mode is disabled', () => {
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, planModeEnabled: false });
expect(surfaces.some((surface) => surface.id === 'plan')).toBe(false);
expect(surfaces.some((surface) => surface.id === 'context')).toBe(true);
});
test('shows the plan surface while plan mode is enabled', () => {
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, planModeEnabled: true });
expect(surfaces.some((surface) => surface.id === 'plan')).toBe(true);
});
test('hides the walkthrough on VS Code and below the min width', () => {
expect(getVisibleContextRailSurfaces({ ...baseOptions, isVSCode: true }).some((s) => s.id === 'walkthrough')).toBe(false);
expect(
getVisibleContextRailSurfaces({ ...baseOptions, screenWidth: WALKTHROUGH_MIN_WIDTH - 1 }).some((s) => s.id === 'walkthrough'),
).toBe(false);
expect(
getVisibleContextRailSurfaces({ ...baseOptions, screenWidth: WALKTHROUGH_MIN_WIDTH }).some((s) => s.id === 'walkthrough'),
).toBe(true);
});
test('hides content-driven surfaces until a matching tab exists', () => {
const preview = CONTEXT_SURFACES.find((surface) => surface.id === 'preview');
if (!preview) {
throw new Error('preview surface missing from registry');
}
expect(preview.availability).toBe('has-content');
expect(getVisibleContextRailSurfaces(baseOptions).some((s) => s.id === 'preview')).toBe(false);
expect(getVisibleContextRailSurfaces({ ...baseOptions, tabs: [{ mode: preview.mode }] }).some((s) => s.id === 'preview')).toBe(true);
});
test('respects the persisted user rail order', () => {
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, railOrder: ['git', 'context'] });
expect(surfaces.slice(0, 2).map((surface) => surface.id)).toEqual(['git', 'context']);
});
});
+37
View File
@@ -152,6 +152,10 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
const SURFACE_BY_ID = new Map(CONTEXT_SURFACES.map((surface) => [surface.id, surface]));
const FRACTION_BY_MODE = new Map(CONTEXT_SURFACES.map((surface) => [surface.mode, surface.defaultWidthFraction]));
// Tablet width and up: below this the walkthrough cannot show a stop and its
// code side by side, which is the whole point of the surface.
export const WALKTHROUGH_MIN_WIDTH = 768;
export const getContextSurfaceWidthFraction = (mode: ContextPanelMode): number => {
return FRACTION_BY_MODE.get(mode) ?? 1 / 2;
};
@@ -187,3 +191,36 @@ export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfac
return ordered;
};
type VisibleRailSurfacesOptions = {
railOrder: readonly string[];
planModeEnabled: boolean;
isVSCode: boolean;
screenWidth: number;
tabs: readonly { mode: ContextPanelMode }[];
};
/**
* The context panel rail's visible, user-ordered surfaces. Shared by the rail
* (for rendering and number badges) and the global surface-switch shortcut so
* both agree on which surface each digit maps to.
*
* Content-driven surfaces are hidden (not disabled) until content exists; an
* existing tab keeps them visible even if the content source went away.
*/
export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => {
return sortContextSurfaces(options.railOrder).filter((surface) => {
if (surface.id === 'plan' && !options.planModeEnabled) {
return false;
}
// The walkthrough needs room for a stop list beside real code, and its
// diffs come from OpenChamber's Git routes, which VS Code does not serve.
if (surface.id === 'walkthrough' && (options.isVSCode || options.screenWidth < WALKTHROUGH_MIN_WIDTH)) {
return false;
}
if (surface.availability === 'has-content') {
return options.tabs.some((tab) => tab.mode === surface.mode);
}
return true;
});
};
@@ -0,0 +1,88 @@
import { beforeEach, describe, expect, mock, test } from 'bun:test';
// A server older than this client does not answer 404-with-JSON: unmatched
// `/api/*` reaches the OpenCode proxy, and OpenCode serves its embedded web UI
// for any unknown path — HTML, status 200. These tests pin that the panel gets
// an actionable code instead of a JSON parser error.
let nextResponse: Response = new Response('{}', { headers: { 'Content-Type': 'application/json' } });
mock.module('@/lib/runtime-fetch', () => ({
runtimeFetch: mock(async () => nextResponse),
}));
const { fetchWalkthrough, generateWalkthrough } = await import('./api');
const { WalkthroughError } = await import('./types');
import type { WalkthroughSource } from './types';
const SOURCE: WalkthroughSource = { kind: 'working-tree', scope: 'all' };
const html = (status: number) =>
new Response('<!doctype html><html><body>OpenCode</body></html>', {
status,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
describe('walkthrough api', () => {
beforeEach(() => {
nextResponse = new Response('{}', { headers: { 'Content-Type': 'application/json' } });
});
test('reads a JSON answer', async () => {
nextResponse = new Response(JSON.stringify({ hunkCount: 3 }), {
headers: { 'Content-Type': 'application/json' },
});
const result = await fetchWalkthrough('/repo', SOURCE);
expect(result.hunkCount).toBe(3);
});
test('reports HTML served with 200 as a server without the routes', async () => {
nextResponse = html(200);
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(WalkthroughError);
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('server-unsupported');
expect((error as Error).message).not.toContain('JSON');
});
test('reports a non-JSON 404 the same way', async () => {
nextResponse = html(404);
const error = await generateWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('server-unsupported');
});
test('keeps a server-side failure rather than blaming the server version', async () => {
nextResponse = new Response(JSON.stringify({ error: 'model exploded', code: 'output-exhausted' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
const error = await generateWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
expect((error as InstanceType<typeof WalkthroughError>).code).toBe('output-exhausted');
expect((error as Error).message).toBe('model exploded');
});
test('a 5xx that is not JSON is a broken server, not a missing route', async () => {
nextResponse = html(502);
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
expect((error as InstanceType<typeof WalkthroughError>).code).toBe(undefined);
expect((error as Error).message).toBe('Failed to load walkthrough');
});
test('JSON that does not parse is reported without the parser wording', async () => {
nextResponse = new Response('{"walkthrough":', { headers: { 'Content-Type': 'application/json' } });
const error = await fetchWalkthrough('/repo', SOURCE).catch((caught: unknown) => caught);
expect(error).toBeInstanceOf(WalkthroughError);
expect((error as Error).message).toBe('The server returned a malformed walkthrough response');
});
});
+34 -2
View File
@@ -16,9 +16,30 @@ interface ErrorPayload {
availableChars?: unknown;
}
const isJsonResponse = (response: Response): boolean =>
/^application\/(?:[\w.+-]+\+)?json\b/i.test(response.headers.get('content-type') ?? '');
/**
* A server without these routes does not answer 404 with JSON. Unmatched
* `/api/*` falls through to the OpenCode proxy, and OpenCode serves its embedded
* web UI for any path it does not know HTML, status 200. Parsing that as JSON
* surfaced `Unexpected token '<', "<!doctype "...` in the panel, which names
* neither the cause nor the remedy.
*
* Only a missing route is reported this way: 2xx and 404 are the shapes it
* produces. A 5xx that is not JSON came from a server that did answer, so it
* keeps its own failure rather than becoming advice to upgrade.
*/
const serverUnsupported = () =>
new WalkthroughError('This OpenChamber server has no walkthrough API', { code: 'server-unsupported' });
const looksUnsupported = (response: Response): boolean =>
!isJsonResponse(response) && (response.ok || response.status === 404);
// An authoritative read that fails must never look like "there is nothing
// here" — the caller would clear a perfectly good walkthrough off the screen.
const throwFromResponse = async (response: Response, fallback: string): Promise<never> => {
if (looksUnsupported(response)) throw serverUnsupported();
const payload = (await response.json().catch(() => null)) as ErrorPayload | null;
throw new WalkthroughError(typeof payload?.error === 'string' ? payload.error : fallback, {
code: typeof payload?.code === 'string' ? (payload.code as WalkthroughError['code']) : undefined,
@@ -28,6 +49,17 @@ const throwFromResponse = async (response: Response, fallback: string): Promise<
});
};
const readJson = async <T>(response: Response): Promise<T> => {
if (!isJsonResponse(response)) throw serverUnsupported();
try {
return (await response.json()) as T;
} catch {
// Declared JSON, arrived truncated or empty: still not an answer, and the
// parser's own message says nothing a reader can act on.
throw new WalkthroughError('The server returned a malformed walkthrough response');
}
};
export async function fetchWalkthrough(
directory: string,
source: WalkthroughSource,
@@ -45,7 +77,7 @@ export async function fetchWalkthrough(
if (!response.ok) {
return throwFromResponse(response, 'Failed to load walkthrough');
}
return response.json();
return readJson<WalkthroughResult>(response);
}
export async function generateWalkthrough(
@@ -68,7 +100,7 @@ export async function generateWalkthrough(
if (!response.ok) {
return throwFromResponse(response, 'Failed to generate walkthrough');
}
return response.json();
return readJson<WalkthroughResult>(response);
}
/**
+12 -1
View File
@@ -89,14 +89,23 @@ export interface WalkthroughResult {
*/
export type WalkthroughStage = 'collecting' | 'asking' | 'retrying' | 'assembling';
/** Reasons the server reports for refusing to generate. */
export type WalkthroughBlockedReason =
| 'no-model'
| 'no-provider-login'
| 'empty-diff'
| 'only-generated'
| 'context-too-small'
| 'structured-output-unsupported'
| 'output-exhausted';
/**
* Everything the panel can render as a blocking screen. `server-unsupported` is
* never sent by a server it is what the client concludes when the answer is
* not JSON at all, which is how a server too old to have these routes replies.
*/
export type WalkthroughBlockedState = WalkthroughBlockedReason | 'server-unsupported';
export interface WalkthroughReadiness {
ready: boolean;
reason?: WalkthroughBlockedReason;
@@ -104,6 +113,8 @@ export interface WalkthroughReadiness {
inputCharBudget?: number;
contextTokens?: number;
structuredOutput?: boolean | null;
/** False when the resolved provider has no usable OpenCode login. */
hasLogin?: boolean;
};
requiredChars?: number;
availableChars?: number;
@@ -113,7 +124,7 @@ export interface WalkthroughReadiness {
}
export class WalkthroughError extends Error {
readonly code?: WalkthroughBlockedReason | 'invalid-walkthrough' | 'github-not-connected' | 'no-github-remote';
readonly code?: WalkthroughBlockedState | 'invalid-walkthrough' | 'github-not-connected' | 'no-github-remote';
readonly model?: WalkthroughModel;
readonly requiredChars?: number;
readonly availableChars?: number;