Merge origin/main into deferred OpenCode restart branch.
Adopt main's providerAuth helpers (OAuth index preservation, OAuth-only API key hiding, always-load auth methods) while keeping deferred Apply & Restart for provider mutations. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
const focusChatInputCalls: number[] = [];
|
||||
const pendingInputCalls: Array<{ text: string | null; mode?: string }> = [];
|
||||
const activeMainTabCalls: string[] = [];
|
||||
const sessionSwitcherCalls: boolean[] = [];
|
||||
const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = [];
|
||||
|
||||
type MockCodeMirrorView = {
|
||||
state: {
|
||||
selection: { main: { from: number; to: number } };
|
||||
sliceDoc: (from: number, to: number) => string;
|
||||
};
|
||||
dispatch: (transaction: { selection: { anchor: number } }) => void;
|
||||
};
|
||||
|
||||
let codeMirrorView: MockCodeMirrorView | null = null;
|
||||
|
||||
mock.module('@codemirror/view', () => ({
|
||||
EditorView: {
|
||||
findFromDOM: () => codeMirrorView,
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/components/chat/composer/editor/dom', () => ({
|
||||
focusChatInput: () => {
|
||||
focusChatInputCalls.push(1);
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/sync/input-store', () => ({
|
||||
useInputStore: {
|
||||
getState: () => ({
|
||||
setPendingInputText: (text: string | null, mode?: string) => {
|
||||
pendingInputCalls.push({ text, mode });
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
mock.module('@/stores/useUIStore', () => ({
|
||||
useUIStore: {
|
||||
getState: () => ({
|
||||
setActiveMainTab: (tab: string) => {
|
||||
activeMainTabCalls.push(tab);
|
||||
},
|
||||
setSessionSwitcherOpen: (open: boolean) => {
|
||||
sessionSwitcherCalls.push(open);
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const { addSelectionToChat, captureSelectionMarkdownForChat } = await import('./addSelectionToChat');
|
||||
|
||||
const originalDocument = globalThis.document;
|
||||
const originalWindow = globalThis.window;
|
||||
|
||||
const installSelectionEnvironment = (options: {
|
||||
activeElement?: Element | null;
|
||||
focusedCodeMirror?: Element | null;
|
||||
selection?: Selection | null;
|
||||
} = {}) => {
|
||||
const {
|
||||
activeElement = null,
|
||||
focusedCodeMirror = null,
|
||||
selection = null,
|
||||
} = options;
|
||||
|
||||
const documentLike = {
|
||||
activeElement,
|
||||
querySelector: (selector: string) => {
|
||||
if (selector === '.cm-editor.cm-focused') {
|
||||
return focusedCodeMirror;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const windowLike = {
|
||||
getSelection: () => selection,
|
||||
};
|
||||
Object.defineProperty(globalThis, 'document', { value: documentLike, configurable: true });
|
||||
Object.defineProperty(globalThis, 'window', { value: windowLike, configurable: true });
|
||||
};
|
||||
|
||||
const clearCalls = () => {
|
||||
focusChatInputCalls.length = 0;
|
||||
pendingInputCalls.length = 0;
|
||||
activeMainTabCalls.length = 0;
|
||||
sessionSwitcherCalls.length = 0;
|
||||
codeMirrorDispatches.length = 0;
|
||||
codeMirrorView = null;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(globalThis, 'document', { value: originalDocument, configurable: true });
|
||||
Object.defineProperty(globalThis, 'window', { value: originalWindow, configurable: true });
|
||||
});
|
||||
|
||||
describe('captureSelectionMarkdownForChat', () => {
|
||||
beforeEach(() => {
|
||||
clearCalls();
|
||||
});
|
||||
|
||||
test('returns null when nothing is selected', () => {
|
||||
installSelectionEnvironment();
|
||||
expect(captureSelectionMarkdownForChat()).toBeNull();
|
||||
});
|
||||
|
||||
test('captures a textarea selection outside the composer and collapses it', () => {
|
||||
const textarea = {
|
||||
tagName: 'TEXTAREA',
|
||||
value: 'alpha beta gamma',
|
||||
selectionStart: 6,
|
||||
selectionEnd: 10,
|
||||
closest: () => null,
|
||||
} as unknown as HTMLTextAreaElement;
|
||||
|
||||
installSelectionEnvironment({ activeElement: textarea });
|
||||
expect(captureSelectionMarkdownForChat()).toBe('```md\nbeta\n```');
|
||||
expect(textarea.selectionStart).toBe(10);
|
||||
expect(textarea.selectionEnd).toBe(10);
|
||||
});
|
||||
|
||||
test('ignores selections inside the chat composer', () => {
|
||||
const textarea = {
|
||||
tagName: 'TEXTAREA',
|
||||
value: 'draft text',
|
||||
selectionStart: 0,
|
||||
selectionEnd: 5,
|
||||
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? textarea : null),
|
||||
} as unknown as HTMLTextAreaElement;
|
||||
|
||||
installSelectionEnvironment({ activeElement: textarea });
|
||||
expect(captureSelectionMarkdownForChat()).toBeNull();
|
||||
});
|
||||
|
||||
test('captures a focused CodeMirror selection outside the composer and collapses it', () => {
|
||||
const focusedEditor = {
|
||||
closest: () => null,
|
||||
} as unknown as HTMLElement;
|
||||
|
||||
codeMirrorView = {
|
||||
state: {
|
||||
selection: { main: { from: 4, to: 11 } },
|
||||
sliceDoc: (from: number, to: number) => 'const x'.slice(0, to - from),
|
||||
},
|
||||
dispatch: (transaction) => {
|
||||
codeMirrorDispatches.push(transaction);
|
||||
codeMirrorView!.state.selection.main = {
|
||||
from: transaction.selection.anchor,
|
||||
to: transaction.selection.anchor,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// sliceDoc should return the selected slice; use explicit text instead of slice math.
|
||||
codeMirrorView.state.sliceDoc = () => 'const x';
|
||||
|
||||
installSelectionEnvironment({ focusedCodeMirror: focusedEditor });
|
||||
expect(captureSelectionMarkdownForChat()).toBe('```\nconst x\n```');
|
||||
expect(codeMirrorDispatches).toEqual([{ selection: { anchor: 11 } }]);
|
||||
expect(codeMirrorView.state.selection.main).toEqual({ from: 11, to: 11 });
|
||||
});
|
||||
|
||||
test('ignores a focused CodeMirror editor inside the chat composer', () => {
|
||||
const focusedEditor = {
|
||||
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? focusedEditor : null),
|
||||
} as unknown as HTMLElement;
|
||||
|
||||
codeMirrorView = {
|
||||
state: {
|
||||
selection: { main: { from: 0, to: 5 } },
|
||||
sliceDoc: () => 'draft',
|
||||
},
|
||||
dispatch: (transaction) => {
|
||||
codeMirrorDispatches.push(transaction);
|
||||
},
|
||||
};
|
||||
|
||||
installSelectionEnvironment({ focusedCodeMirror: focusedEditor });
|
||||
expect(captureSelectionMarkdownForChat()).toBeNull();
|
||||
expect(codeMirrorDispatches).toEqual([]);
|
||||
});
|
||||
|
||||
test('captures a DOM selection from chat-message content and clears it', () => {
|
||||
const parent = {
|
||||
closest: (selector: string) => (selector === 'pre code' ? null : null),
|
||||
};
|
||||
const textNode = {
|
||||
nodeType: 3,
|
||||
parentElement: parent,
|
||||
};
|
||||
let rangeCount = 1;
|
||||
let collapsed = false;
|
||||
const selection = {
|
||||
get rangeCount() {
|
||||
return rangeCount;
|
||||
},
|
||||
get isCollapsed() {
|
||||
return collapsed;
|
||||
},
|
||||
toString: () => 'Hello world',
|
||||
getRangeAt: () => ({
|
||||
commonAncestorContainer: textNode,
|
||||
startContainer: textNode,
|
||||
endContainer: textNode,
|
||||
cloneContents: () => ({ childNodes: [] }),
|
||||
}),
|
||||
removeAllRanges: () => {
|
||||
rangeCount = 0;
|
||||
collapsed = true;
|
||||
},
|
||||
} as unknown as Selection;
|
||||
|
||||
installSelectionEnvironment({ selection });
|
||||
expect(captureSelectionMarkdownForChat()).toBe('```md\nHello world\n```');
|
||||
expect(selection.rangeCount).toBe(0);
|
||||
expect(selection.isCollapsed).toBe(true);
|
||||
});
|
||||
|
||||
test('ignores a DOM selection inside the chat composer', () => {
|
||||
const composerHost = {};
|
||||
const parent = {
|
||||
closest: (selector: string) => (selector === '[data-chat-input="true"]' ? composerHost : null),
|
||||
};
|
||||
const textNode = {
|
||||
nodeType: 3,
|
||||
parentElement: parent,
|
||||
};
|
||||
const selection = {
|
||||
rangeCount: 1,
|
||||
isCollapsed: false,
|
||||
toString: () => 'draft',
|
||||
getRangeAt: () => ({
|
||||
commonAncestorContainer: textNode,
|
||||
startContainer: textNode,
|
||||
endContainer: textNode,
|
||||
cloneContents: () => ({ childNodes: [] }),
|
||||
}),
|
||||
removeAllRanges: () => undefined,
|
||||
} as unknown as Selection;
|
||||
|
||||
installSelectionEnvironment({ selection });
|
||||
expect(captureSelectionMarkdownForChat()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSelectionToChat', () => {
|
||||
beforeEach(() => {
|
||||
clearCalls();
|
||||
});
|
||||
|
||||
test('appends captured selection and focuses chat input', async () => {
|
||||
const textarea = {
|
||||
tagName: 'TEXTAREA',
|
||||
value: 'selected',
|
||||
selectionStart: 0,
|
||||
selectionEnd: 8,
|
||||
closest: () => null,
|
||||
} as unknown as HTMLTextAreaElement;
|
||||
installSelectionEnvironment({ activeElement: textarea });
|
||||
|
||||
expect(addSelectionToChat()).toBe(true);
|
||||
expect(activeMainTabCalls).toEqual(['chat']);
|
||||
expect(sessionSwitcherCalls).toEqual([false]);
|
||||
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(focusChatInputCalls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('second capture after textarea collapse does not append again', () => {
|
||||
const textarea = {
|
||||
tagName: 'TEXTAREA',
|
||||
value: 'selected',
|
||||
selectionStart: 0,
|
||||
selectionEnd: 8,
|
||||
closest: () => null,
|
||||
} as unknown as HTMLTextAreaElement;
|
||||
installSelectionEnvironment({ activeElement: textarea });
|
||||
|
||||
expect(addSelectionToChat()).toBe(true);
|
||||
expect(addSelectionToChat()).toBe(false);
|
||||
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
|
||||
});
|
||||
|
||||
test('focuses chat input when nothing is selected', async () => {
|
||||
installSelectionEnvironment();
|
||||
|
||||
expect(addSelectionToChat()).toBe(false);
|
||||
expect(pendingInputCalls).toEqual([]);
|
||||
expect(activeMainTabCalls).toEqual(['chat']);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(focusChatInputCalls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { focusChatInput } from '@/components/chat/composer/editor/dom';
|
||||
import {
|
||||
formatCodeSelectionMarkdown,
|
||||
rangeToMarkdown,
|
||||
trimSelectionValue,
|
||||
wrapMarkdownSelectionForChat,
|
||||
} from '@/components/chat/message/selectionMarkdown';
|
||||
import { useInputStore } from '@/sync/input-store';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
const CHAT_INPUT_HOST_SELECTOR = '[data-chat-input="true"]';
|
||||
|
||||
const isInsideChatComposer = (node: Node | null): boolean => {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const asElement = node as Element;
|
||||
const element = typeof asElement.closest === 'function'
|
||||
? asElement
|
||||
: (node as Node).parentElement;
|
||||
return Boolean(element?.closest(CHAT_INPUT_HOST_SELECTOR));
|
||||
};
|
||||
|
||||
const readTextControlSelection = (element: Element): string | null => {
|
||||
if (isInsideChatComposer(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tag = element.tagName?.toLowerCase();
|
||||
if (tag === 'textarea') {
|
||||
const control = element as HTMLTextAreaElement;
|
||||
const start = control.selectionStart ?? 0;
|
||||
const end = control.selectionEnd ?? 0;
|
||||
const text = trimSelectionValue(control.value.slice(start, end));
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
// Collapse so a duplicate menu delivery cannot append the same range twice.
|
||||
control.selectionStart = end;
|
||||
control.selectionEnd = end;
|
||||
return text;
|
||||
}
|
||||
|
||||
if (tag === 'input') {
|
||||
const control = element as HTMLInputElement;
|
||||
const type = control.type?.toLowerCase() ?? 'text';
|
||||
if (!['text', 'search', 'url', 'tel', 'password'].includes(type)) {
|
||||
return null;
|
||||
}
|
||||
const start = control.selectionStart ?? 0;
|
||||
const end = control.selectionEnd ?? 0;
|
||||
const text = trimSelectionValue(control.value.slice(start, end));
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
control.selectionStart = end;
|
||||
control.selectionEnd = end;
|
||||
return text;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const captureActiveElementSelection = (): string | null => {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeElement = document.activeElement;
|
||||
if (!activeElement || typeof (activeElement as Element).tagName !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = readTextControlSelection(activeElement as Element);
|
||||
return text ? wrapMarkdownSelectionForChat(text) : null;
|
||||
};
|
||||
|
||||
const captureCodeMirrorSelection = (): string | null => {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const focusedEditor = document.querySelector<HTMLElement>('.cm-editor.cm-focused');
|
||||
if (!focusedEditor || isInsideChatComposer(focusedEditor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const view = EditorView.findFromDOM(focusedEditor);
|
||||
if (!view) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { from, to } = view.state.selection.main;
|
||||
if (from === to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = trimSelectionValue(view.state.sliceDoc(from, to));
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
selection: { anchor: to },
|
||||
});
|
||||
|
||||
return formatCodeSelectionMarkdown(text);
|
||||
};
|
||||
|
||||
const captureDomSelection = (): string | null => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
if (isInsideChatComposer(range.commonAncestorContainer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const plainText = trimSelectionValue(selection.toString());
|
||||
if (!plainText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markdown = rangeToMarkdown(range, plainText);
|
||||
selection.removeAllRanges();
|
||||
return wrapMarkdownSelectionForChat(markdown);
|
||||
};
|
||||
|
||||
/**
|
||||
* Capture the current non-composer selection as chat-ready markdown.
|
||||
* Returns null when nothing usable is selected.
|
||||
*/
|
||||
export const captureSelectionMarkdownForChat = (): string | null => {
|
||||
return captureCodeMirrorSelection()
|
||||
?? captureActiveElementSelection()
|
||||
?? captureDomSelection();
|
||||
};
|
||||
|
||||
/**
|
||||
* Append the current selection to the chat composer.
|
||||
* When nothing is selected, focuses the chat input (Cursor-style Ctrl/Cmd+L).
|
||||
* Returns true when selected text was appended.
|
||||
*/
|
||||
export const addSelectionToChat = (): boolean => {
|
||||
const markdown = captureSelectionMarkdownForChat();
|
||||
|
||||
useUIStore.getState().setActiveMainTab('chat');
|
||||
useUIStore.getState().setSessionSwitcherOpen(false);
|
||||
|
||||
if (markdown) {
|
||||
useInputStore.getState().setPendingInputText(markdown, 'append');
|
||||
}
|
||||
|
||||
queueMicrotask(() => {
|
||||
focusChatInput();
|
||||
});
|
||||
|
||||
return markdown !== null;
|
||||
};
|
||||
@@ -441,7 +441,11 @@ export const debugUtils = {
|
||||
const sources = {
|
||||
attachment,
|
||||
worktreeMetadata,
|
||||
authoritative: owningStoreDirectory ?? recordDirectory,
|
||||
// Record first, matching the resolver: holding a session proves
|
||||
// containment, not ownership, so the parent repository holds its
|
||||
// worktrees' sessions too. Reporting membership first made this
|
||||
// diagnostic contradict the routing it exists to explain.
|
||||
authoritative: recordDirectory ?? owningStoreDirectory,
|
||||
selected,
|
||||
remembered: remembered.runtime,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { isBrowserClientRuntime } from './desktop';
|
||||
|
||||
describe('browser client runtime', () => {
|
||||
test('uses browser file behavior only outside the Electron shell', () => {
|
||||
expect(isBrowserClientRuntime('web', false)).toBe(true);
|
||||
expect(isBrowserClientRuntime('web', true)).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps desktop and VS Code runtime behavior out of browser-only flows', () => {
|
||||
expect(isBrowserClientRuntime('desktop', false)).toBe(false);
|
||||
expect(isBrowserClientRuntime('vscode', false)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ProjectEntry, TerminalShell } from '@/lib/api/types';
|
||||
import type { ProjectEntry, RuntimeAPIs, TerminalShell } from '@/lib/api/types';
|
||||
import { getInjectedBootOutcome } from '@/lib/desktopBoot';
|
||||
import type { DraftStarterRef } from '@/lib/draftStarters';
|
||||
import type { MobileKeyboardMode } from '@/lib/mobileKeyboardMode';
|
||||
@@ -562,6 +562,15 @@ export const isWebRuntime = (): boolean => {
|
||||
return !isVSCodeRuntime();
|
||||
};
|
||||
|
||||
/**
|
||||
* Electron reuses the web RuntimeAPIs implementation, so distinguish a browser
|
||||
* client from an Electron renderer with both the runtime descriptor and shell.
|
||||
*/
|
||||
export const isBrowserClientRuntime = (
|
||||
platform: RuntimeAPIs['runtime']['platform'],
|
||||
desktopShell = isDesktopShell(),
|
||||
): boolean => platform === 'web' && !desktopShell;
|
||||
|
||||
export const getDesktopHomeDirectory = async (): Promise<string | null> => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const embedded = window.__OPENCHAMBER_HOME__;
|
||||
|
||||
@@ -1050,6 +1050,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Einstellungen öffnen',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Terminal-Dock umschalten',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal erweitert umschalten',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Auswahl zum Chat hinzufügen',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Seitenleiste umschalten',
|
||||
'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',
|
||||
|
||||
@@ -426,6 +426,11 @@ export const dict = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': '{count} Sitzungen archiviert',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': 'Fehler beim Archivieren von {count} Sitzung',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': 'Fehler beim Archivieren von {count} Sitzungen',
|
||||
'sessions.sidebar.bulkActions.restore': 'Wiederherstellen',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': '{count} Sitzung wiederhergestellt',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': '{count} Sitzungen wiederhergestellt',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': 'Fehler beim Wiederherstellen von {count} Sitzung',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': 'Fehler beim Wiederherstellen von {count} Sitzungen',
|
||||
'sessions.sidebar.folders.none': 'Noch keine Ordner',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': 'Neuer Ordner...',
|
||||
'sessions.sidebar.folders.removeFromFolder': 'Aus Ordner entfernen',
|
||||
@@ -517,6 +522,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.delete.error': 'Fehler beim Löschen der Sitzung',
|
||||
'sessions.sidebar.session.archive.success': 'Sitzung archiviert',
|
||||
'sessions.sidebar.session.archive.error': 'Fehler beim Archivieren der Sitzung',
|
||||
'sessions.sidebar.session.restore.success': 'Sitzung wiederhergestellt',
|
||||
'sessions.sidebar.session.restore.error': 'Fehler beim Wiederherstellen der Sitzung',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total} Checks bestanden',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count} fehlgeschlagen',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} ausstehend',
|
||||
@@ -1085,6 +1092,7 @@ export const dict = {
|
||||
'sidebarFilesTree.menu.rename': 'Umbenennen',
|
||||
'sidebarFilesTree.menu.copyPath': 'Pfad kopieren',
|
||||
'sidebarFilesTree.menu.save': 'Speichern',
|
||||
'sidebarFilesTree.menu.download': 'Herunterladen',
|
||||
'sidebarFilesTree.menu.newFile': 'Neue Datei',
|
||||
'sidebarFilesTree.menu.newFolder': 'Neuer Ordner',
|
||||
'sidebarFilesTree.menu.delete': 'Löschen',
|
||||
@@ -1531,6 +1539,7 @@ export const dict = {
|
||||
'helpDialog.item.openCommandPalette': 'Befehlspalette öffnen',
|
||||
'helpDialog.item.showKeyboardShortcuts': 'Tastaturkürzel anzeigen (dieses Dialogfeld)',
|
||||
'helpDialog.item.toggleSessionSidebar': 'Sitzungs-Seitenleiste umschalten',
|
||||
'helpDialog.item.addSelectionToChat': 'Auswahl zum Chat hinzufügen',
|
||||
'helpDialog.item.cycleAgent': 'Agent wechseln (Chat-Eingabe)',
|
||||
'helpDialog.item.openModelSelector': 'Modell-Auswahldialog öffnen',
|
||||
'helpDialog.item.navigateModels': 'Modelle navigieren (in Auswahl)',
|
||||
@@ -2785,6 +2794,7 @@ export const dict = {
|
||||
'sessions.archivePage.deleteProject': 'Alle archivierten Sitzungen in diesem Projekt löschen',
|
||||
'sessions.archivePage.deleteProjectAria': 'Alle archivierten Sitzungen in {label} löschen',
|
||||
'sessions.archivePage.deleteSessionAria': '{title} löschen',
|
||||
'sessions.archivePage.restoreSessionAria': '{title} wiederherstellen',
|
||||
'header.sessionActions.openAria': 'Sitzungsaktionen öffnen',
|
||||
'sessions.sidebar.session.menu.copyId': 'Sitzungs-ID kopieren',
|
||||
'sessions.sidebar.session.copyId.success': 'Sitzungs-ID kopiert',
|
||||
|
||||
@@ -1115,6 +1115,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Open settings',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Toggle terminal dock',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Toggle terminal expanded',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Add selection to chat',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Toggle sidebar',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Toggle context panel',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Open Git surface',
|
||||
|
||||
@@ -449,6 +449,7 @@ export const dict = {
|
||||
'sessions.archivePage.deleteProject': 'Delete all archived sessions in this project',
|
||||
'sessions.archivePage.deleteProjectAria': 'Delete all archived sessions in {label}',
|
||||
'sessions.archivePage.deleteSessionAria': 'Delete {title}',
|
||||
'sessions.archivePage.restoreSessionAria': 'Restore {title}',
|
||||
'sessions.switcher.openAria': 'Open session switcher',
|
||||
'sessions.switcher.empty': 'No recent sessions',
|
||||
'sessions.switcher.draftTitle': 'New session',
|
||||
@@ -470,6 +471,11 @@ export const dict = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': 'Archived {count} sessions',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': 'Failed to archive {count} session',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': 'Failed to archive {count} sessions',
|
||||
'sessions.sidebar.bulkActions.restore': 'Restore',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': 'Restored {count} session',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': 'Restored {count} sessions',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': 'Failed to restore {count} session',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': 'Failed to restore {count} sessions',
|
||||
'sessions.sidebar.folders.none': 'No folders yet',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': 'New folder...',
|
||||
'sessions.sidebar.folders.removeFromFolder': 'Remove from folder',
|
||||
@@ -573,6 +579,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.delete.error': 'Failed to delete session',
|
||||
'sessions.sidebar.session.archive.success': 'Session archived',
|
||||
'sessions.sidebar.session.archive.error': 'Failed to archive session',
|
||||
'sessions.sidebar.session.restore.success': 'Session restored',
|
||||
'sessions.sidebar.session.restore.error': 'Failed to restore session',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total} checks passed',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count} failing',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} pending',
|
||||
@@ -1225,6 +1233,7 @@ export const dict = {
|
||||
'sidebarFilesTree.menu.rename': 'Rename',
|
||||
'sidebarFilesTree.menu.copyPath': 'Copy Path',
|
||||
'sidebarFilesTree.menu.save': 'Save',
|
||||
'sidebarFilesTree.menu.download': 'Download',
|
||||
'sidebarFilesTree.menu.newFile': 'New File',
|
||||
'sidebarFilesTree.menu.newFolder': 'New Folder',
|
||||
'sidebarFilesTree.menu.delete': 'Delete',
|
||||
@@ -1678,6 +1687,7 @@ export const dict = {
|
||||
'helpDialog.item.openCommandPalette': 'Open Command Palette',
|
||||
'helpDialog.item.showKeyboardShortcuts': 'Show Keyboard Shortcuts (this dialog)',
|
||||
'helpDialog.item.toggleSessionSidebar': 'Toggle Session Sidebar',
|
||||
'helpDialog.item.addSelectionToChat': 'Add Selection to Chat',
|
||||
'helpDialog.item.cycleAgent': 'Cycle Agent (chat input)',
|
||||
'helpDialog.item.openModelSelector': 'Open Model Selector',
|
||||
'helpDialog.item.navigateModels': 'Navigate Models (in picker)',
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.action.open_settings.label": "Abrir configuración",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Mostrar u ocultar panel de terminal",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir o contraer terminal",
|
||||
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Agregar selección al chat",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar u ocultar barra lateral",
|
||||
"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',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.archivePage.deleteProject": "Eliminar todas las sesiones archivadas de este proyecto",
|
||||
"sessions.archivePage.deleteProjectAria": "Eliminar todas las sesiones archivadas de {label}",
|
||||
"sessions.archivePage.deleteSessionAria": "Eliminar {title}",
|
||||
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
|
||||
"sessions.switcher.openAria": "Abrir selector de sesiones",
|
||||
"sessions.switcher.empty": "No hay sesiones recientes",
|
||||
"sessions.switcher.draftTitle": "Nueva sesión",
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.bulkActions.archivedPlural": "Se archivaron {count} sesiones",
|
||||
"sessions.sidebar.bulkActions.failedArchiveSingle": "No se pudo archivar {count} sesión",
|
||||
"sessions.sidebar.bulkActions.failedArchivePlural": "No se pudo archivar {count} sesiones",
|
||||
"sessions.sidebar.bulkActions.restore": "Restaurar",
|
||||
"sessions.sidebar.bulkActions.restoredSingle": "Se restauró {count} sesión",
|
||||
"sessions.sidebar.bulkActions.restoredPlural": "Se restauraron {count} sesiones",
|
||||
"sessions.sidebar.bulkActions.failedRestoreSingle": "No se pudo restaurar {count} sesión",
|
||||
"sessions.sidebar.bulkActions.failedRestorePlural": "No se pudo restaurar {count} sesiones",
|
||||
"sessions.sidebar.folders.none": "No hay carpetas aún",
|
||||
"sessions.sidebar.folders.newFolderEllipsis": "Nueva carpeta...",
|
||||
"sessions.sidebar.folders.removeFromFolder": "Quitar de carpeta",
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.delete.error": "No se pudo eliminar la sesión",
|
||||
"sessions.sidebar.session.archive.success": "Sesión archivada",
|
||||
"sessions.sidebar.session.archive.error": "No se pudo archivar la sesión",
|
||||
"sessions.sidebar.session.restore.success": "Sesión restaurada",
|
||||
"sessions.sidebar.session.restore.error": "No se pudo restaurar la sesión",
|
||||
"sessions.sidebar.group.pr.checksPassed": "{success}/{total} comprobaciones aprobadas",
|
||||
"sessions.sidebar.group.pr.failingCount": "{count} con fallos",
|
||||
"sessions.sidebar.group.pr.pendingCount": "{count} pendientes",
|
||||
@@ -1191,6 +1199,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sidebarFilesTree.menu.rename": "Cambiar nombre",
|
||||
"sidebarFilesTree.menu.copyPath": "Copiar ruta",
|
||||
"sidebarFilesTree.menu.save": "Guardar",
|
||||
"sidebarFilesTree.menu.download": "Descargar",
|
||||
"sidebarFilesTree.menu.newFile": "Nuevo archivo",
|
||||
"sidebarFilesTree.menu.newFolder": "Nueva carpeta",
|
||||
"sidebarFilesTree.menu.delete": "Eliminar",
|
||||
@@ -1656,6 +1665,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.item.openCommandPalette": "Abrir paleta de comandos",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Mostrar atajos de teclado (este diálogo)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Mostrar u ocultar barra lateral de sesión",
|
||||
"helpDialog.item.addSelectionToChat": "Agregar selección al chat",
|
||||
"helpDialog.item.cycleAgent": "Cambiar agente (entrada de chat)",
|
||||
"helpDialog.item.openModelSelector": "Abrir selector de modelos",
|
||||
"helpDialog.item.navigateModels": "Navegar modelos (en selector)",
|
||||
|
||||
@@ -1004,6 +1004,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': 'Ouvrir les paramètres',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Basculer la station d\'accueil du terminal',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Terminal à bascule étendu',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Ajouter la sélection au chat',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Basculer la barre latérale',
|
||||
'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',
|
||||
|
||||
@@ -285,6 +285,7 @@ export const dict = {
|
||||
'sessions.archivePage.deleteProject': 'Supprimer toutes les sessions archivées de ce projet',
|
||||
'sessions.archivePage.deleteProjectAria': 'Supprimer toutes les sessions archivées de {label}',
|
||||
'sessions.archivePage.deleteSessionAria': 'Supprimer {title}',
|
||||
'sessions.archivePage.restoreSessionAria': 'Restaurer {title}',
|
||||
'sessions.switcher.openAria': 'Sélecteur de session ouvert',
|
||||
'sessions.switcher.empty': 'Aucune session récente',
|
||||
'sessions.switcher.draftTitle': 'Nouvelle session',
|
||||
@@ -306,6 +307,11 @@ export const dict = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': 'Sessions {count} archivées',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': 'Échec de l\'archivage de la session {count}',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': 'Échec de l\'archivage des sessions {count}',
|
||||
'sessions.sidebar.bulkActions.restore': 'Restaurer',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': 'Session {count} restaurée',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': 'Sessions {count} restaurées',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': 'Échec de la restauration de la session {count}',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': 'Échec de la restauration des sessions {count}',
|
||||
'sessions.sidebar.folders.none': 'Aucun dossier pour l\'instant',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': 'Nouveau dossier...',
|
||||
'sessions.sidebar.folders.removeFromFolder': 'Supprimer du dossier',
|
||||
@@ -409,6 +415,8 @@ export const dict = {
|
||||
'sessions.sidebar.session.delete.error': 'Échec de la suppression de la session',
|
||||
'sessions.sidebar.session.archive.success': 'Session archivée',
|
||||
'sessions.sidebar.session.archive.error': 'Échec de l\'archivage de la session',
|
||||
'sessions.sidebar.session.restore.success': 'Session restaurée',
|
||||
'sessions.sidebar.session.restore.error': 'Échec de la restauration de la session',
|
||||
'sessions.sidebar.group.pr.checksPassed': 'Contrôles {success}/{total} réussis',
|
||||
'sessions.sidebar.group.pr.failingCount': 'Échec de {count}',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} en attente',
|
||||
@@ -1047,6 +1055,7 @@ export const dict = {
|
||||
'sidebarFilesTree.menu.rename': 'Rebaptiser',
|
||||
'sidebarFilesTree.menu.copyPath': 'Copier le chemin',
|
||||
'sidebarFilesTree.menu.save': 'Sauvegarder',
|
||||
'sidebarFilesTree.menu.download': 'Télécharger',
|
||||
'sidebarFilesTree.menu.newFile': 'Nouveau fichier',
|
||||
'sidebarFilesTree.menu.newFolder': 'Nouveau dossier',
|
||||
'sidebarFilesTree.menu.delete': 'Supprimer',
|
||||
@@ -1491,6 +1500,7 @@ export const dict = {
|
||||
'helpDialog.item.openCommandPalette': 'Ouvrir la palette de commandes',
|
||||
'helpDialog.item.showKeyboardShortcuts': 'Afficher les raccourcis clavier (cette boîte de dialogue)',
|
||||
'helpDialog.item.toggleSessionSidebar': 'Toggle la barre latérale de la session',
|
||||
'helpDialog.item.addSelectionToChat': 'Ajouter la sélection au chat',
|
||||
'helpDialog.item.cycleAgent': 'Agent de cycle (entrée de chat)',
|
||||
'helpDialog.item.openModelSelector': 'Ouvrir le sélecteur de modèle',
|
||||
'helpDialog.item.navigateModels': 'Naviguer dans les modèles (dans le sélecteur)',
|
||||
|
||||
@@ -1116,6 +1116,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': '設定を開く',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'ターミナルドックの切替',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'ターミナル拡大の切替',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '選択範囲をチャットに追加',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'サイドバーの切替',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'コンテキストパネルの表示切替',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git サーフェスを開く',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.archivePage.deleteProject': 'このプロジェクトのアーカイブ済みセッションをすべて削除',
|
||||
'sessions.archivePage.deleteProjectAria': '{label} のアーカイブ済みセッションをすべて削除',
|
||||
'sessions.archivePage.deleteSessionAria': '{title} を削除',
|
||||
'sessions.archivePage.restoreSessionAria': '{title} を復元',
|
||||
'sessions.switcher.openAria': 'セッションスイッチャーを開く',
|
||||
'sessions.switcher.empty': '最近のセッションはありません',
|
||||
'sessions.switcher.draftTitle': '新しいセッション',
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': '{count}セッションをアーカイブしました',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': '{count}セッションのアーカイブに失敗しました',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': '{count}セッションのアーカイブに失敗しました',
|
||||
'sessions.sidebar.bulkActions.restore': '復元',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': '{count}セッションを復元しました',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': '{count}セッションを復元しました',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': '{count}セッションの復元に失敗しました',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': '{count}セッションの復元に失敗しました',
|
||||
'sessions.sidebar.folders.none': 'まだフォルダがありません',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': '新しいフォルダ...',
|
||||
'sessions.sidebar.folders.removeFromFolder': 'フォルダから削除',
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.delete.error': 'セッションの削除に失敗しました',
|
||||
'sessions.sidebar.session.archive.success': 'セッションをアーカイブしました',
|
||||
'sessions.sidebar.session.archive.error': 'セッションのアーカイブに失敗しました',
|
||||
'sessions.sidebar.session.restore.success': 'セッションを復元しました',
|
||||
'sessions.sidebar.session.restore.error': 'セッションの復元に失敗しました',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total}のチェックに合格',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count}件失敗',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count}件保留中',
|
||||
@@ -1221,6 +1229,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sidebarFilesTree.menu.rename': '名前の変更',
|
||||
'sidebarFilesTree.menu.copyPath': 'パスをコピー',
|
||||
'sidebarFilesTree.menu.save': '保存',
|
||||
'sidebarFilesTree.menu.download': 'ダウンロード',
|
||||
'sidebarFilesTree.menu.newFile': '新しいファイル',
|
||||
'sidebarFilesTree.menu.newFolder': '新しいフォルダ',
|
||||
'sidebarFilesTree.menu.delete': '削除',
|
||||
@@ -1674,6 +1683,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.item.openCommandPalette': 'コマンドパレットを開く',
|
||||
'helpDialog.item.showKeyboardShortcuts': 'キーボードショートカットを表示(このダイアログ)',
|
||||
'helpDialog.item.toggleSessionSidebar': 'セッションサイドバーの切り替え',
|
||||
'helpDialog.item.addSelectionToChat': '選択範囲をチャットに追加',
|
||||
'helpDialog.item.cycleAgent': 'エージェント切り替え(チャット入力)',
|
||||
'helpDialog.item.openModelSelector': 'モデルセレクターを開く',
|
||||
'helpDialog.item.navigateModels': 'モデルを移動(ピッカー内)',
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': '설정 열기',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '터미널 dock 토글',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '터미널 확장 토글',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '선택 내용을 채팅에 추가',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '사이드바 토글',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '컨텍스트 패널 표시 전환',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': 'Git 서피스 열기',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.archivePage.deleteProject': '이 프로젝트의 보관된 세션 모두 삭제',
|
||||
'sessions.archivePage.deleteProjectAria': '{label}의 보관된 세션 모두 삭제',
|
||||
'sessions.archivePage.deleteSessionAria': '{title} 삭제',
|
||||
'sessions.archivePage.restoreSessionAria': '{title} 복원',
|
||||
'sessions.switcher.openAria': '세션 전환기 열기',
|
||||
'sessions.switcher.empty': '최근 세션 없음',
|
||||
'sessions.switcher.draftTitle': '새 세션',
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': '세션 {count}개 보관됨',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': '세션 {count}개 보관 실패',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': '세션 {count}개 보관 실패',
|
||||
'sessions.sidebar.bulkActions.restore': '복원',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': '세션 {count}개 복원됨',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': '세션 {count}개 복원됨',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': '세션 {count}개 복원 실패',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': '세션 {count}개 복원 실패',
|
||||
'sessions.sidebar.folders.none': '아직 폴더 없음',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': '새 폴더…',
|
||||
'sessions.sidebar.folders.removeFromFolder': '폴더에서 제거',
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.delete.error': '세션 삭제 실패',
|
||||
'sessions.sidebar.session.archive.success': '세션 보관됨',
|
||||
'sessions.sidebar.session.archive.error': '세션 보관 실패',
|
||||
'sessions.sidebar.session.restore.success': '세션 복원됨',
|
||||
'sessions.sidebar.session.restore.error': '세션 복원 실패',
|
||||
'sessions.sidebar.group.pr.checksPassed': '검사 통과: {success}/{total}',
|
||||
'sessions.sidebar.group.pr.failingCount': '실패 {count}개',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} 대기 중',
|
||||
@@ -1228,6 +1236,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sidebarFilesTree.menu.rename': '이름 변경',
|
||||
'sidebarFilesTree.menu.copyPath': '경로 복사',
|
||||
'sidebarFilesTree.menu.save': '저장',
|
||||
'sidebarFilesTree.menu.download': '다운로드',
|
||||
'sidebarFilesTree.menu.newFile': '새 파일',
|
||||
'sidebarFilesTree.menu.newFolder': '새 폴더',
|
||||
'sidebarFilesTree.menu.delete': '삭제',
|
||||
@@ -1680,6 +1689,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.item.openCommandPalette': '명령 팔레트 열기',
|
||||
'helpDialog.item.showKeyboardShortcuts': '키보드 단축키 보기(이 대화상자)',
|
||||
'helpDialog.item.toggleSessionSidebar': '토글 세션 사이드바',
|
||||
'helpDialog.item.addSelectionToChat': '선택 내용을 채팅에 추가',
|
||||
'helpDialog.item.cycleAgent': '에이전트 순환(채팅 입력)',
|
||||
'helpDialog.item.openModelSelector': '모델 선택기 열기',
|
||||
'helpDialog.item.navigateModels': '모델 이동(선택기)',
|
||||
|
||||
@@ -825,6 +825,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Przełącz panel kontekstu planu',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': 'Przełącz panel kontekstu',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Przełącz menu usług',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': 'Dodaj zaznaczenie do czatu',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': 'Przełącz pasek boczny',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': 'Przełącz dokowanie terminala',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': 'Przełącz rozszerzony terminal',
|
||||
|
||||
@@ -266,6 +266,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.archivePage.deleteProject': 'Usuń wszystkie zarchiwizowane sesje tego projektu',
|
||||
'sessions.archivePage.deleteProjectAria': 'Usuń wszystkie zarchiwizowane sesje w {label}',
|
||||
'sessions.archivePage.deleteSessionAria': 'Usuń {title}',
|
||||
'sessions.archivePage.restoreSessionAria': 'Przywróć {title}',
|
||||
'sessions.switcher.openAria': 'Otwórz przełącznik sesji',
|
||||
'sessions.switcher.empty': 'Brak ostatnich sesji',
|
||||
'sessions.switcher.draftTitle': 'Nowa sesja',
|
||||
@@ -333,6 +334,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': 'Zarchiwizowano {count} sesji',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': 'Nie udało się zarchiwizować {count} sesji',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': 'Nie udało się zarchiwizować {count} sesji',
|
||||
'sessions.sidebar.bulkActions.restore': 'Przywróć',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': 'Przywrócono {count} sesję',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': 'Przywrócono {count} sesji',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': 'Nie udało się przywrócić {count} sesji',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': 'Nie udało się przywrócić {count} sesji',
|
||||
'sessions.scheduledTasks.dialog.title': 'Zaplanowane zadania',
|
||||
'sessions.scheduledTasks.dialog.description': 'Zadania po stronie serwera, które tworzą nową sesję i wysyłają skonfigurowany prompt.',
|
||||
'sessions.scheduledTasks.dialog.project.label': 'Projekt',
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.delete.error': 'Nie udało się usunąć sesji',
|
||||
'sessions.sidebar.session.archive.success': 'Sesja zarchiwizowana',
|
||||
'sessions.sidebar.session.archive.error': 'Nie udało się zarchiwizować sesji',
|
||||
'sessions.sidebar.session.restore.success': 'Sesja przywrócona',
|
||||
'sessions.sidebar.session.restore.error': 'Nie udało się przywrócić sesji',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total} testów przeszło',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count} niepowodzeń',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} oczekujących',
|
||||
@@ -2325,6 +2333,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.item.toggleRightSidebar': 'Przełącz panel kontekstu',
|
||||
'helpDialog.item.toggleServicesMenu': 'Przełącz menu usług',
|
||||
'helpDialog.item.toggleSessionSidebar': 'Przełącz panel sesji',
|
||||
'helpDialog.item.addSelectionToChat': 'Dodaj zaznaczenie do czatu',
|
||||
'helpDialog.item.toggleTerminalDock': 'Przełącz dolny terminal',
|
||||
'helpDialog.item.toggleTerminalExpanded': 'Przełącz rozszerzenie terminala',
|
||||
'helpDialog.keyCombiner.or': 'lub',
|
||||
@@ -2708,6 +2717,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sidebarFilesTree.menu.newFolder': 'Nowy folder',
|
||||
'sidebarFilesTree.menu.rename': 'Zmień nazwę',
|
||||
'sidebarFilesTree.menu.save': 'Zapisz',
|
||||
'sidebarFilesTree.menu.download': 'Pobierz',
|
||||
'sidebarFilesTree.search.clearAria': 'Wyczyść wyszukiwanie',
|
||||
'sidebarFilesTree.search.placeholder': 'Szukaj plików...',
|
||||
'sidebarFilesTree.state.loading': 'Ładowanie...',
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.action.open_settings.label": "Abrir configurações",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Mostrar ou ocultar painel de terminal",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Expandir ou recolher terminal",
|
||||
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Adicionar seleção ao chat",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Mostrar ou ocultar barra lateral",
|
||||
"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',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.archivePage.deleteProject": "Excluir todas as sessões arquivadas deste projeto",
|
||||
"sessions.archivePage.deleteProjectAria": "Excluir todas as sessões arquivadas de {label}",
|
||||
"sessions.archivePage.deleteSessionAria": "Excluir {title}",
|
||||
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
|
||||
"sessions.switcher.openAria": "Abrir seletor de sessões",
|
||||
"sessions.switcher.empty": "Nenhuma sessão recente",
|
||||
"sessions.switcher.draftTitle": "Nova sessão",
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.bulkActions.archivedPlural": "{count} sessões arquivadas",
|
||||
"sessions.sidebar.bulkActions.failedArchiveSingle": "Não foi possível arquivar {count} sessão",
|
||||
"sessions.sidebar.bulkActions.failedArchivePlural": "Não foi possível arquivar {count} sessões",
|
||||
"sessions.sidebar.bulkActions.restore": "Restaurar",
|
||||
"sessions.sidebar.bulkActions.restoredSingle": "{count} sessão restaurada",
|
||||
"sessions.sidebar.bulkActions.restoredPlural": "{count} sessões restauradas",
|
||||
"sessions.sidebar.bulkActions.failedRestoreSingle": "Não foi possível restaurar {count} sessão",
|
||||
"sessions.sidebar.bulkActions.failedRestorePlural": "Não foi possível restaurar {count} sessões",
|
||||
"sessions.sidebar.folders.none": "Não há pastas ainda",
|
||||
"sessions.sidebar.folders.newFolderEllipsis": "Nova pasta...",
|
||||
"sessions.sidebar.folders.removeFromFolder": "Remover da pasta",
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.delete.error": "Não foi possível excluir a sessão",
|
||||
"sessions.sidebar.session.archive.success": "Sessão archivada",
|
||||
"sessions.sidebar.session.archive.error": "Não foi possível arquivar a sessão",
|
||||
"sessions.sidebar.session.restore.success": "Sessão restaurada",
|
||||
"sessions.sidebar.session.restore.error": "Não foi possível restaurar a sessão",
|
||||
"sessions.sidebar.group.pr.checksPassed": "{success}/{total} checks pasadas",
|
||||
"sessions.sidebar.group.pr.failingCount": "{count} com fallos",
|
||||
"sessions.sidebar.group.pr.pendingCount": "{count} pendentes",
|
||||
@@ -1191,6 +1199,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sidebarFilesTree.menu.rename": "Renomear",
|
||||
"sidebarFilesTree.menu.copyPath": "Copiar caminho",
|
||||
"sidebarFilesTree.menu.save": "Salvar",
|
||||
"sidebarFilesTree.menu.download": "Baixar",
|
||||
"sidebarFilesTree.menu.newFile": "Novo arquivo",
|
||||
"sidebarFilesTree.menu.newFolder": "Nova pasta",
|
||||
"sidebarFilesTree.menu.delete": "Excluir",
|
||||
@@ -1656,6 +1665,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.item.openCommandPalette": "Abrir paleta de comandos",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Mostrar atalhos de teclado (este diálogo)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Mostrar ou ocultar barra lateral de sessão",
|
||||
"helpDialog.item.addSelectionToChat": "Adicionar seleção ao chat",
|
||||
"helpDialog.item.cycleAgent": "Alternar agente (entrada do chat)",
|
||||
"helpDialog.item.openModelSelector": "Abrir seletor de modelos",
|
||||
"helpDialog.item.navigateModels": "Navegar por modelos (no seletor)",
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.action.open_settings.label": "Відкрити налаштування",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal.label": "Перемкнути панель терміналу",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label": "Розгорнути або згорнути термінал",
|
||||
"settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label": "Додати виділення в чат",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label": "Перемкнути бічну панель",
|
||||
"settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label": 'Перемкнути контекстну панель',
|
||||
"settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label": 'Відкрити поверхню Git',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.archivePage.deleteProject": "Видалити всі архівні сесії цього проєкту",
|
||||
"sessions.archivePage.deleteProjectAria": "Видалити всі архівні сесії у {label}",
|
||||
"sessions.archivePage.deleteSessionAria": "Видалити {title}",
|
||||
"sessions.archivePage.restoreSessionAria": "Відновити {title}",
|
||||
"sessions.switcher.openAria": "Відкрити перемикач сесій",
|
||||
"sessions.switcher.empty": "Немає недавніх сесій",
|
||||
"sessions.switcher.draftTitle": "Нова сесія",
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.bulkActions.archivedPlural": "Заархівовано сесій: {count}",
|
||||
"sessions.sidebar.bulkActions.failedArchiveSingle": "Не вдалося архівувати сесія {count}",
|
||||
"sessions.sidebar.bulkActions.failedArchivePlural": "Не вдалося архівувати сесії {count}",
|
||||
"sessions.sidebar.bulkActions.restore": "Відновити",
|
||||
"sessions.sidebar.bulkActions.restoredSingle": "Відновлено сесію: {count}",
|
||||
"sessions.sidebar.bulkActions.restoredPlural": "Відновлено сесій: {count}",
|
||||
"sessions.sidebar.bulkActions.failedRestoreSingle": "Не вдалося відновити сесія {count}",
|
||||
"sessions.sidebar.bulkActions.failedRestorePlural": "Не вдалося відновити сесії {count}",
|
||||
"sessions.sidebar.folders.none": "Папок ще немає",
|
||||
"sessions.sidebar.folders.newFolderEllipsis": "Нова папка...",
|
||||
"sessions.sidebar.folders.removeFromFolder": "Видалити з папки",
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sessions.sidebar.session.delete.error": "Не вдалося видалити сесію",
|
||||
"sessions.sidebar.session.archive.success": "Сесію заархівовано",
|
||||
"sessions.sidebar.session.archive.error": "Не вдалося заархівувати сесію",
|
||||
"sessions.sidebar.session.restore.success": "Сесію відновлено",
|
||||
"sessions.sidebar.session.restore.error": "Не вдалося відновити сесію",
|
||||
"sessions.sidebar.group.pr.checksPassed": "Перевірки {success}/{total} пройдено",
|
||||
"sessions.sidebar.group.pr.failingCount": "{count} з помилкою",
|
||||
"sessions.sidebar.group.pr.pendingCount": "{count} очікує",
|
||||
@@ -1191,6 +1199,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"sidebarFilesTree.menu.rename": "Перейменувати",
|
||||
"sidebarFilesTree.menu.copyPath": "Копіювати шлях",
|
||||
"sidebarFilesTree.menu.save": "Зберегти",
|
||||
"sidebarFilesTree.menu.download": "Завантажити",
|
||||
"sidebarFilesTree.menu.newFile": "Новий файл",
|
||||
"sidebarFilesTree.menu.newFolder": "Нова папка",
|
||||
"sidebarFilesTree.menu.delete": "Видалити",
|
||||
@@ -1656,6 +1665,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.item.openCommandPalette": "Відкрити палітру команд",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Показати комбінації клавіш (це діалогове вікно)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Перемкнути бічну панель сесій",
|
||||
"helpDialog.item.addSelectionToChat": "Додати виділення в чат",
|
||||
"helpDialog.item.cycleAgent": "Перемкнути агента (введення в чат)",
|
||||
"helpDialog.item.openModelSelector": "Відкрити засіб вибору моделі",
|
||||
"helpDialog.item.navigateModels": "Навігація моделями (у засобі вибору)",
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': '打开设置',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '切换终端停靠区',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切换终端展开',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '将选中内容添加到聊天',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切换侧边栏',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切换上下文面板',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '打开 Git 界面',
|
||||
|
||||
@@ -450,6 +450,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.archivePage.deleteProject': '删除此项目的所有已归档会话',
|
||||
'sessions.archivePage.deleteProjectAria': '删除 {label} 的所有已归档会话',
|
||||
'sessions.archivePage.deleteSessionAria': '删除 {title}',
|
||||
'sessions.archivePage.restoreSessionAria': '还原 {title}',
|
||||
'sessions.switcher.openAria': '打开会话切换器',
|
||||
'sessions.switcher.empty': '没有最近会话',
|
||||
'sessions.switcher.draftTitle': '新会话',
|
||||
@@ -471,6 +472,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': '已归档 {count} 个会话',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': '归档 {count} 个会话失败',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': '归档 {count} 个会话失败',
|
||||
'sessions.sidebar.bulkActions.restore': '还原',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': '已还原 {count} 个会话',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': '已还原 {count} 个会话',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': '还原 {count} 个会话失败',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': '还原 {count} 个会话失败',
|
||||
'sessions.sidebar.folders.none': '暂无文件夹',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': '新建文件夹...',
|
||||
'sessions.sidebar.folders.removeFromFolder': '从文件夹中移除',
|
||||
@@ -574,6 +580,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.delete.error': '删除会话失败',
|
||||
'sessions.sidebar.session.archive.success': '会话已归档',
|
||||
'sessions.sidebar.session.archive.error': '归档会话失败',
|
||||
'sessions.sidebar.session.restore.success': '会话已还原',
|
||||
'sessions.sidebar.session.restore.error': '还原会话失败',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total} 项检查已通过',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count} 项失败',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} 项等待中',
|
||||
@@ -1191,6 +1199,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sidebarFilesTree.menu.rename': '重命名',
|
||||
'sidebarFilesTree.menu.copyPath': '复制路径',
|
||||
'sidebarFilesTree.menu.save': '保存',
|
||||
'sidebarFilesTree.menu.download': '下载',
|
||||
'sidebarFilesTree.menu.newFile': '新建文件',
|
||||
'sidebarFilesTree.menu.newFolder': '新建文件夹',
|
||||
'sidebarFilesTree.menu.delete': '删除',
|
||||
@@ -1644,6 +1653,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.item.openCommandPalette': '打开命令面板',
|
||||
'helpDialog.item.showKeyboardShortcuts': '显示键盘快捷键(此对话框)',
|
||||
'helpDialog.item.toggleSessionSidebar': '切换会话侧边栏',
|
||||
'helpDialog.item.addSelectionToChat': '将选中内容添加到聊天',
|
||||
'helpDialog.item.cycleAgent': '循环切换智能体(聊天输入)',
|
||||
'helpDialog.item.openModelSelector': '打开模型选择器',
|
||||
'helpDialog.item.navigateModels': '导航模型(选择器中)',
|
||||
|
||||
@@ -990,6 +990,7 @@
|
||||
'settings.openchamber.keyboardShortcuts.action.open_settings.label': '開啟設定',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal.label': '切換終端機停靠區',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_terminal_expanded.label': '切換終端機展開',
|
||||
'settings.openchamber.keyboardShortcuts.action.add_selection_to_chat.label': '將選取內容加入聊天',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_sidebar.label': '切換側邊欄',
|
||||
'settings.openchamber.keyboardShortcuts.action.toggle_right_sidebar.label': '切換上下文面板',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_right_sidebar_git.label': '開啟 Git 介面',
|
||||
|
||||
@@ -463,6 +463,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.archivePage.deleteProject': '刪除此專案的所有已封存工作階段',
|
||||
'sessions.archivePage.deleteProjectAria': '刪除 {label} 的所有已封存工作階段',
|
||||
'sessions.archivePage.deleteSessionAria': '刪除 {title}',
|
||||
'sessions.archivePage.restoreSessionAria': '還原 {title}',
|
||||
'sessions.switcher.openAria': '開啟會話切換器',
|
||||
'sessions.switcher.empty': '沒有最近會話',
|
||||
'sessions.switcher.draftTitle': '新會話',
|
||||
@@ -484,6 +485,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.bulkActions.archivedPlural': '已封存 {count} 個會話',
|
||||
'sessions.sidebar.bulkActions.failedArchiveSingle': '封存 {count} 個會話失敗',
|
||||
'sessions.sidebar.bulkActions.failedArchivePlural': '封存 {count} 個會話失敗',
|
||||
'sessions.sidebar.bulkActions.restore': '還原',
|
||||
'sessions.sidebar.bulkActions.restoredSingle': '已還原 {count} 個會話',
|
||||
'sessions.sidebar.bulkActions.restoredPlural': '已還原 {count} 個會話',
|
||||
'sessions.sidebar.bulkActions.failedRestoreSingle': '還原 {count} 個會話失敗',
|
||||
'sessions.sidebar.bulkActions.failedRestorePlural': '還原 {count} 個會話失敗',
|
||||
'sessions.sidebar.folders.none': '暫無資料夾',
|
||||
'sessions.sidebar.folders.newFolderEllipsis': '新增資料夾...',
|
||||
'sessions.sidebar.folders.removeFromFolder': '從資料夾中移除',
|
||||
@@ -587,6 +593,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sessions.sidebar.session.delete.error': '刪除會話失敗',
|
||||
'sessions.sidebar.session.archive.success': '會話已封存',
|
||||
'sessions.sidebar.session.archive.error': '封存會話失敗',
|
||||
'sessions.sidebar.session.restore.success': '會話已還原',
|
||||
'sessions.sidebar.session.restore.error': '還原會話失敗',
|
||||
'sessions.sidebar.group.pr.checksPassed': '{success}/{total} 項檢查已通過',
|
||||
'sessions.sidebar.group.pr.failingCount': '{count} 項失敗',
|
||||
'sessions.sidebar.group.pr.pendingCount': '{count} 項等待中',
|
||||
@@ -1203,6 +1211,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'sidebarFilesTree.menu.rename': '重新命名',
|
||||
'sidebarFilesTree.menu.copyPath': '複製路徑',
|
||||
'sidebarFilesTree.menu.save': '儲存',
|
||||
'sidebarFilesTree.menu.download': '下載',
|
||||
'sidebarFilesTree.menu.newFile': '新增檔案',
|
||||
'sidebarFilesTree.menu.newFolder': '新增資料夾',
|
||||
'sidebarFilesTree.menu.delete': '刪除',
|
||||
@@ -1648,6 +1657,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.item.openCommandPalette': '開啟命令面板',
|
||||
'helpDialog.item.showKeyboardShortcuts': '顯示鍵盤快速鍵(此對話方塊)',
|
||||
'helpDialog.item.toggleSessionSidebar': '切換會話側邊欄',
|
||||
'helpDialog.item.addSelectionToChat': '將選取內容加入聊天',
|
||||
'helpDialog.item.cycleAgent': '循環切換 Agent(聊天輸入)',
|
||||
'helpDialog.item.openModelSelector': '開啟模型選擇器',
|
||||
'helpDialog.item.navigateModels': '導覽模型(選擇器中)',
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
TextPartInput,
|
||||
FilePartInput,
|
||||
} from "@opencode-ai/sdk/v2";
|
||||
import { isAmbiguousTransportFailure, markAmbiguousTransportFailure } from "@/lib/relay/transport-error";
|
||||
import type { PermissionRequest } from "@/types/permission";
|
||||
import type { QuestionRequest } from "@/types/question";
|
||||
|
||||
@@ -878,7 +879,13 @@ class OpencodeService {
|
||||
// failure) — there is no HTTP response to report. Never fabricate a
|
||||
// status: surface it as a transport error so callers treat it like
|
||||
// any other network failure instead of a server 500.
|
||||
throw new Error(`Message send transport failure: ${formatSdkError(result.error)}`);
|
||||
// Preserve the transport's "dispatched, outcome unknown" tag through
|
||||
// the wrap: without it the caller cannot tell a lost response from a
|
||||
// send that never reached the server, and re-sends a running prompt.
|
||||
const transportError = new Error(`Message send transport failure: ${formatSdkError(result.error)}`);
|
||||
throw isAmbiguousTransportFailure(result.error)
|
||||
? markAmbiguousTransportFailure(transportError)
|
||||
: transportError;
|
||||
}
|
||||
response = new Response(JSON.stringify(result.error), { status });
|
||||
} else {
|
||||
|
||||
@@ -22,5 +22,6 @@ export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
|
||||
{ id: 'wafer', name: 'Wafer.ai' },
|
||||
{ id: 'opencode-go', name: 'OpenCode Go' },
|
||||
{ id: 'crof', name: 'CrofAI' },
|
||||
{ id: 'deepseek', name: 'DeepSeek' },
|
||||
{ id: 'neuralwatt', name: 'NeuralWatt' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Ambiguous transport failures.
|
||||
*
|
||||
* When a request dies after it was already handed to the transport, the client
|
||||
* knows the response was lost — it does NOT know whether the server processed
|
||||
* the request. Over the relay tunnel this is the common case: a reconnect, a
|
||||
* host-side stream abort, or a dead channel all fail an in-flight POST that may
|
||||
* already be running server-side.
|
||||
*
|
||||
* Callers must be able to tell that state apart from a definite failure, and
|
||||
* string-matching the message text is not a contract — a renamed abort reason
|
||||
* silently reclassifies a send. Transports therefore tag these errors, and
|
||||
* callers read the tag (see `isAmbiguousTransportFailure`).
|
||||
*
|
||||
* `prompt_async` is the motivating case: treating an ambiguous failure as a
|
||||
* definite one rolls back the user message and lets the queue re-send a prompt
|
||||
* the engine is already answering, producing two independent AI responses.
|
||||
*/
|
||||
|
||||
const AMBIGUOUS_TRANSPORT_FLAG = '__openchamberAmbiguousTransport';
|
||||
|
||||
/**
|
||||
* Mark an error as "dispatched, outcome unknown". Returns the same error so it
|
||||
* can be thrown inline.
|
||||
*/
|
||||
export const markAmbiguousTransportFailure = <T extends Error>(error: T): T => {
|
||||
Object.defineProperty(error, AMBIGUOUS_TRANSPORT_FLAG, {
|
||||
value: true,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return error;
|
||||
};
|
||||
|
||||
/**
|
||||
* True when a transport tagged this error as dispatched-but-unconfirmed.
|
||||
* Deliberately tag-only: text heuristics belong to the caller that owns them.
|
||||
*/
|
||||
export const isAmbiguousTransportFailure = (error: unknown): boolean => {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
return (error as Record<string, unknown>)[AMBIGUOUS_TRANSPORT_FLAG] === true;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from './crypto';
|
||||
import { createHostHandshake } from './handshake';
|
||||
import { TunnelFrameType } from './protocol';
|
||||
import { isAmbiguousTransportFailure } from './transport-error';
|
||||
import {
|
||||
createFragmentAssembler,
|
||||
decodeFrameBatch,
|
||||
@@ -339,6 +340,24 @@ describe('createRelayTunnelClient', () => {
|
||||
await expect(reader.read()).rejects.toThrow();
|
||||
});
|
||||
|
||||
// A POST that dies after dispatch may already have been processed by the
|
||||
// server. Callers must be able to tell that apart from a definite failure —
|
||||
// a prompt re-sent on this error produces a second AI response (#2425).
|
||||
test('tags an in-flight request killed by reconnect as an ambiguous failure', async () => {
|
||||
const { client, killWire } = await setupClient({ silent: true });
|
||||
track(client);
|
||||
const pending = client.fetch('/api/session/s1/prompt_async', { method: 'POST', body: '{}' });
|
||||
let caught: unknown = null;
|
||||
const settled = pending.catch((error: unknown) => {
|
||||
caught = error;
|
||||
});
|
||||
await wait(20);
|
||||
killWire();
|
||||
await settled;
|
||||
expect(caught).toBeInstanceOf(Error);
|
||||
expect(isAmbiguousTransportFailure(caught)).toBe(true);
|
||||
});
|
||||
|
||||
test('opens, echoes, and closes a tunneled WebSocket', async () => {
|
||||
const { client } = await setupClient();
|
||||
track(client);
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
isWsClosePayload,
|
||||
normalizeTunnelRequest,
|
||||
} from './tunnel-payloads';
|
||||
import { markAmbiguousTransportFailure } from './transport-error';
|
||||
|
||||
const EMPTY_PAYLOAD = new Uint8Array(0);
|
||||
const textEncoder = new TextEncoder();
|
||||
@@ -721,6 +722,13 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
}
|
||||
};
|
||||
|
||||
// The request head is written to the channel below before any of these
|
||||
// failures can fire, so losing the stream never proves the server did
|
||||
// not process the request — only that the response was lost. Callers
|
||||
// that would otherwise retry (prompt sends) must see that distinction.
|
||||
const dispatchedFailure = (message: string): Error =>
|
||||
markAmbiguousTransportFailure(new Error(message));
|
||||
|
||||
onAbort = () => {
|
||||
sendAbort('aborted');
|
||||
finishError(abortError());
|
||||
@@ -735,7 +743,7 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
head = decodeJsonPayload(payload, isHttpResponsePayload);
|
||||
} catch (error) {
|
||||
sendAbort('malformed response head');
|
||||
finishError(toError(error));
|
||||
finishError(dispatchedFailure(toError(error).message));
|
||||
return;
|
||||
}
|
||||
const nullBody = head.status === 204 || head.status === 205 || head.status === 304;
|
||||
@@ -773,7 +781,7 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
if (frameType === TunnelFrameType.StreamEnd) {
|
||||
if (finished) return;
|
||||
if (!responseDelivered) {
|
||||
finishError(new Error('tunnel stream ended before response head'));
|
||||
finishError(dispatchedFailure('tunnel stream ended before response head'));
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
@@ -792,11 +800,15 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
} catch {
|
||||
// Keep the generic reason.
|
||||
}
|
||||
finishError(new Error(reason));
|
||||
finishError(dispatchedFailure(reason));
|
||||
}
|
||||
},
|
||||
fail(error) {
|
||||
finishError(error);
|
||||
// Channel death (reconnect, keepalive timeout) with this stream still
|
||||
// open — same rule as above: dispatched, outcome unknown. A fresh
|
||||
// error is tagged instead of the shared one so the tag cannot leak to
|
||||
// waiters whose request never reached the wire.
|
||||
finishError(dispatchedFailure(error.message));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -824,7 +836,7 @@ export const createRelayTunnelClient = (options: RelayTunnelClientOptions): Rela
|
||||
}
|
||||
} catch (error) {
|
||||
sendAbort('request body failed');
|
||||
finishError(toError(error));
|
||||
finishError(dispatchedFailure(toError(error).message));
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||
|
||||
import { getRuntimeKey } from './runtime-switch';
|
||||
|
||||
/**
|
||||
* `getRuntimeKey` runs on store, event, and render paths, so its cost is
|
||||
* multiplied by everything the UI does. These tests pin both directions of the
|
||||
* derived-key cache: repeated calls with unchanged inputs must do no work, and
|
||||
* any change to the inputs it derives from must still be observed.
|
||||
*
|
||||
* This lives in its own file because the cache is only reachable while the
|
||||
* runtime endpoint has not been explicitly initialised, and module state is
|
||||
* shared across tests within a file.
|
||||
*/
|
||||
|
||||
type RuntimeWindow = typeof globalThis & {
|
||||
__OPENCHAMBER_API_BASE_URL__?: string;
|
||||
__OPENCHAMBER_LOCAL_ORIGIN__?: string;
|
||||
};
|
||||
|
||||
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||
const NativeURL = globalThis.URL;
|
||||
let urlConstructions = 0;
|
||||
|
||||
const setRuntimeWindow = (apiBaseUrl: string | undefined, localOrigin: string | undefined): void => {
|
||||
const runtimeWindow = {} as RuntimeWindow;
|
||||
if (apiBaseUrl !== undefined) runtimeWindow.__OPENCHAMBER_API_BASE_URL__ = apiBaseUrl;
|
||||
if (localOrigin !== undefined) runtimeWindow.__OPENCHAMBER_LOCAL_ORIGIN__ = localOrigin;
|
||||
Object.defineProperty(globalThis, 'window', { value: runtimeWindow, configurable: true, writable: true });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
urlConstructions = 0;
|
||||
class CountingURL extends NativeURL {
|
||||
constructor(url: string | URL, base?: string | URL) {
|
||||
urlConstructions += 1;
|
||||
super(url, base);
|
||||
}
|
||||
}
|
||||
globalThis.URL = CountingURL as unknown as typeof URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.URL = NativeURL;
|
||||
if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow);
|
||||
else Reflect.deleteProperty(globalThis, 'window');
|
||||
});
|
||||
|
||||
describe('getRuntimeKey caching', () => {
|
||||
test('resolves a same-origin endpoint to the local runtime key', () => {
|
||||
setRuntimeWindow('https://app.example.com/api', 'https://app.example.com');
|
||||
expect(getRuntimeKey()).toBe('local');
|
||||
});
|
||||
|
||||
test('performs no URL work on repeated calls with unchanged inputs', () => {
|
||||
setRuntimeWindow('https://remote.example.com', 'https://app.example.com');
|
||||
const first = getRuntimeKey();
|
||||
expect(first).toBe('url:https://remote.example.com');
|
||||
|
||||
urlConstructions = 0;
|
||||
for (let index = 0; index < 50; index += 1) expect(getRuntimeKey()).toBe(first);
|
||||
expect(urlConstructions).toBe(0);
|
||||
});
|
||||
|
||||
test('recomputes when the injected API base URL changes at runtime', () => {
|
||||
setRuntimeWindow('https://first.example.com', 'https://app.example.com');
|
||||
expect(getRuntimeKey()).toBe('url:https://first.example.com');
|
||||
|
||||
(globalThis as RuntimeWindow & { window: RuntimeWindow }).window.__OPENCHAMBER_API_BASE_URL__ = 'https://second.example.com';
|
||||
expect(getRuntimeKey()).toBe('url:https://second.example.com');
|
||||
});
|
||||
|
||||
test('recomputes when the injected local origin changes at runtime', () => {
|
||||
setRuntimeWindow('https://app.example.com', 'https://other.example.com');
|
||||
expect(getRuntimeKey()).toBe('url:https://app.example.com');
|
||||
|
||||
(globalThis as RuntimeWindow & { window: RuntimeWindow }).window.__OPENCHAMBER_LOCAL_ORIGIN__ = 'https://app.example.com';
|
||||
expect(getRuntimeKey()).toBe('local');
|
||||
});
|
||||
});
|
||||
@@ -76,11 +76,52 @@ const sameOrigin = (left: string, right: string): boolean => {
|
||||
};
|
||||
|
||||
export const getRuntimeApiBaseUrl = (): string => activeApiBaseUrl || readInjectedApiBaseUrl();
|
||||
|
||||
// `getRuntimeKey` keys caches, stores, and persisted state across the whole UI,
|
||||
// so it runs on store reads, event handling, and render paths. Before the
|
||||
// runtime endpoint is explicitly initialised, every call re-derived the key by
|
||||
// trimming two injected globals and constructing three `URL` objects, which
|
||||
// made this one of the most expensive functions during streaming.
|
||||
//
|
||||
// The result depends only on `activeApiBaseUrl` and the two injected globals,
|
||||
// and `switchRuntimeEndpoint` writes the injected API base URL at runtime, so
|
||||
// the cache is validated against the raw, untrimmed values. That comparison
|
||||
// allocates nothing and still recomputes the moment any input changes.
|
||||
let cachedRuntimeKey = '';
|
||||
let cachedActiveApiBaseUrl: string | null = null;
|
||||
let cachedRawApiBaseUrl: string | undefined;
|
||||
let cachedRawLocalOrigin: string | undefined;
|
||||
|
||||
const readRawRuntimeGlobal = (key: '__OPENCHAMBER_API_BASE_URL__' | '__OPENCHAMBER_LOCAL_ORIGIN__'): string | undefined => {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
const value = (window as typeof window & {
|
||||
__OPENCHAMBER_API_BASE_URL__?: string;
|
||||
__OPENCHAMBER_LOCAL_ORIGIN__?: string;
|
||||
})[key];
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
};
|
||||
|
||||
export const getRuntimeKey = (): string => {
|
||||
if (activeRuntimeKey) return activeRuntimeKey;
|
||||
|
||||
const rawApiBaseUrl = readRawRuntimeGlobal('__OPENCHAMBER_API_BASE_URL__');
|
||||
const rawLocalOrigin = readRawRuntimeGlobal('__OPENCHAMBER_LOCAL_ORIGIN__');
|
||||
if (
|
||||
cachedActiveApiBaseUrl === activeApiBaseUrl
|
||||
&& cachedRawApiBaseUrl === rawApiBaseUrl
|
||||
&& cachedRawLocalOrigin === rawLocalOrigin
|
||||
) {
|
||||
return cachedRuntimeKey;
|
||||
}
|
||||
|
||||
const apiBaseUrl = getRuntimeApiBaseUrl();
|
||||
if (sameOrigin(apiBaseUrl, readInjectedLocalOrigin())) return 'local';
|
||||
return normalizeRuntimeUrlKey(apiBaseUrl);
|
||||
cachedRuntimeKey = sameOrigin(apiBaseUrl, readInjectedLocalOrigin())
|
||||
? 'local'
|
||||
: normalizeRuntimeUrlKey(apiBaseUrl);
|
||||
cachedActiveApiBaseUrl = activeApiBaseUrl;
|
||||
cachedRawApiBaseUrl = rawApiBaseUrl;
|
||||
cachedRawLocalOrigin = rawLocalOrigin;
|
||||
return cachedRuntimeKey;
|
||||
};
|
||||
|
||||
export const initializeRuntimeEndpoint = (options: { apiBaseUrl?: string | null; runtimeKey?: string | null } = {}): void => {
|
||||
|
||||
@@ -159,8 +159,15 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
|
||||
description: 'Toggle the files panel',
|
||||
},
|
||||
{
|
||||
id: 'toggle_sidebar',
|
||||
id: 'add_selection_to_chat',
|
||||
defaultCombo: 'mod+l',
|
||||
label: 'Add selection to chat',
|
||||
description: 'Add the selected text to the chat input',
|
||||
customizable: true,
|
||||
},
|
||||
{
|
||||
id: 'toggle_sidebar',
|
||||
defaultCombo: 'mod+alt+l',
|
||||
label: 'Toggle sidebar',
|
||||
description: 'Toggle the session sidebar',
|
||||
customizable: true,
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
"emphasis": "#fd9b66"
|
||||
},
|
||||
"surface": {
|
||||
"background": "#0c0b0a",
|
||||
"foreground": "#dbd7ca",
|
||||
"muted": "#131211",
|
||||
"background": "#120f0e",
|
||||
"foreground": "#c9c5ba",
|
||||
"muted": "#171615",
|
||||
"mutedForeground": "#8f8b81",
|
||||
"elevated": "#181715",
|
||||
"elevatedForeground": "#dbd7ca",
|
||||
"elevatedForeground": "#c9c5ba",
|
||||
"overlay": "#00000099",
|
||||
"subtle": "#171616"
|
||||
},
|
||||
@@ -37,11 +37,11 @@
|
||||
"border": "#242323",
|
||||
"borderHover": "#504e4c",
|
||||
"borderFocus": "#da7c47",
|
||||
"selection": "#da7c472b",
|
||||
"selectionForeground": "#dbd7ca",
|
||||
"selection": "#b9a5992b",
|
||||
"selectionForeground": "#c9c5ba",
|
||||
"focus": "#da7c47",
|
||||
"focusRing": "#da7c4755",
|
||||
"cursor": "#dbd7ca",
|
||||
"cursor": "#c9c5ba",
|
||||
"hover": "#ffffff12",
|
||||
"active": "#ffffff1f"
|
||||
},
|
||||
@@ -72,8 +72,8 @@
|
||||
},
|
||||
"syntax": {
|
||||
"base": {
|
||||
"background": "#131211",
|
||||
"foreground": "#dbd7ca",
|
||||
"background": "#120f0e",
|
||||
"foreground": "#c9c5ba",
|
||||
"comment": "#728772",
|
||||
"keyword": "#34983a",
|
||||
"string": "#d58373",
|
||||
@@ -127,14 +127,14 @@
|
||||
"diffModified": "#5d99a9",
|
||||
"diffModifiedBackground": "#5d99a920",
|
||||
"lineNumber": "#3c3a37",
|
||||
"lineNumberActive": "#dbd7ca"
|
||||
"lineNumberActive": "#c9c5ba"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"heading1": "#dbd7ca",
|
||||
"heading2": "#dbd7ca",
|
||||
"heading3": "#dbd7ca",
|
||||
"heading4": "#dbd7ca",
|
||||
"heading1": "#c9c5ba",
|
||||
"heading2": "#c9c5ba",
|
||||
"heading3": "#c9c5ba",
|
||||
"heading4": "#c9c5ba",
|
||||
"link": "#5d99a9",
|
||||
"linkHover": "#6ba7b8",
|
||||
"inlineCode": "#76ad4f",
|
||||
@@ -144,19 +144,19 @@
|
||||
"listMarker": "#4d934e99"
|
||||
},
|
||||
"chat": {
|
||||
"userMessage": "#dbd7ca",
|
||||
"userMessage": "#c9c5ba",
|
||||
"userMessageBackground": "#25170e",
|
||||
"assistantMessage": "#dbd7ca",
|
||||
"assistantMessageBackground": "#0c0b0a",
|
||||
"assistantMessage": "#c9c5ba",
|
||||
"assistantMessageBackground": "#120f0e",
|
||||
"timestamp": "#8f8b81",
|
||||
"divider": "#302e2b"
|
||||
},
|
||||
"tools": {
|
||||
"background": "#13121150",
|
||||
"background": "#120f0e50",
|
||||
"border": "#302e2b99",
|
||||
"headerHover": "#ffffff0d",
|
||||
"icon": "#ada9a0",
|
||||
"title": "#dbd7ca",
|
||||
"title": "#c9c5ba",
|
||||
"description": "#aba9a3",
|
||||
"edit": {
|
||||
"added": "#4d934e",
|
||||
|
||||
Reference in New Issue
Block a user