Merge upstream main into feat/subagent-cost-rollup

This commit is contained in:
igorvelho
2026-08-25 23:09:44 +01:00
156 changed files with 5084 additions and 4690 deletions
@@ -2,7 +2,7 @@ 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 activeSurfaceCalls: string[] = [];
const sessionSwitcherCalls: boolean[] = [];
const codeMirrorDispatches: Array<{ selection: { anchor: number } }> = [];
@@ -41,8 +41,8 @@ mock.module('@/sync/input-store', () => ({
mock.module('@/stores/useUIStore', () => ({
useUIStore: {
getState: () => ({
setActiveMainTab: (tab: string) => {
activeMainTabCalls.push(tab);
setActiveSurface: (tab: string) => {
activeSurfaceCalls.push(tab);
},
setSessionSwitcherOpen: (open: boolean) => {
sessionSwitcherCalls.push(open);
@@ -86,7 +86,7 @@ const installSelectionEnvironment = (options: {
const clearCalls = () => {
focusChatInputCalls.length = 0;
pendingInputCalls.length = 0;
activeMainTabCalls.length = 0;
activeSurfaceCalls.length = 0;
sessionSwitcherCalls.length = 0;
codeMirrorDispatches.length = 0;
codeMirrorView = null;
@@ -262,7 +262,7 @@ describe('addSelectionToChat', () => {
installSelectionEnvironment({ activeElement: textarea });
expect(addSelectionToChat()).toBe(true);
expect(activeMainTabCalls).toEqual(['chat']);
expect(activeSurfaceCalls).toEqual(['chat']);
expect(sessionSwitcherCalls).toEqual([false]);
expect(pendingInputCalls).toEqual([{ text: '```md\nselected\n```', mode: 'append' }]);
@@ -290,7 +290,7 @@ describe('addSelectionToChat', () => {
expect(addSelectionToChat()).toBe(false);
expect(pendingInputCalls).toEqual([]);
expect(activeMainTabCalls).toEqual(['chat']);
expect(activeSurfaceCalls).toEqual(['chat']);
await Promise.resolve();
expect(focusChatInputCalls.length).toBe(1);
@@ -151,7 +151,6 @@ export const captureSelectionMarkdownForChat = (): string | null => {
export const addSelectionToChat = (): boolean => {
const markdown = captureSelectionMarkdownForChat();
useUIStore.getState().setActiveMainTab('chat');
useUIStore.getState().setSessionSwitcherOpen(false);
if (markdown) {
+11
View File
@@ -80,8 +80,19 @@ export interface ForceKillOptions {
cwd?: string;
}
export interface TerminalServerSession {
sessionId: string;
cwd: string;
status: 'running' | 'exited';
createdAt: number | null;
}
export interface TerminalAPI {
listShells?(): Promise<TerminalShellOption[]>;
/** Server-side sessions for a working directory; absent on runtimes without a server terminal list. */
listSessions?(cwd: string): Promise<TerminalServerSession[]>;
/** Marks the sessions as active so the server's idle sweep does not reap terminals an open client still shows. */
touchSessions?(sessionIds: string[]): Promise<void>;
createSession(options: CreateTerminalOptions): Promise<TerminalSession>;
connect(sessionId: string, handlers: TerminalHandlers): Subscription;
sendInput(sessionId: string, input: string): Promise<void>;
@@ -7,6 +7,7 @@ import type { TerminalShell } from '@/lib/api/types';
type AppearanceSlice = {
showReasoningTraces: boolean;
streamingAutoFollowEnabled: boolean;
workStatusPanelEnabled: boolean;
workStatusHiddenSections: string[];
sessionRecapEnabled: boolean;
@@ -62,6 +63,7 @@ export const startAppearanceAutoSave = (): void => {
let previous: AppearanceSlice = {
showReasoningTraces: useUIStore.getState().showReasoningTraces,
streamingAutoFollowEnabled: useUIStore.getState().streamingAutoFollowEnabled,
workStatusPanelEnabled: useUIStore.getState().workStatusPanelEnabled,
workStatusHiddenSections: useUIStore.getState().workStatusHiddenSections,
sessionRecapEnabled: useUIStore.getState().sessionRecapEnabled,
@@ -104,6 +106,7 @@ export const startAppearanceAutoSave = (): void => {
useUIStore.subscribe((state) => {
const current: AppearanceSlice = {
showReasoningTraces: state.showReasoningTraces,
streamingAutoFollowEnabled: state.streamingAutoFollowEnabled,
workStatusPanelEnabled: state.workStatusPanelEnabled,
workStatusHiddenSections: state.workStatusHiddenSections,
sessionRecapEnabled: state.sessionRecapEnabled,
@@ -156,6 +159,9 @@ export const startAppearanceAutoSave = (): void => {
if (current.showReasoningTraces !== previous.showReasoningTraces) {
diff.showReasoningTraces = current.showReasoningTraces;
}
if (current.streamingAutoFollowEnabled !== previous.streamingAutoFollowEnabled) {
diff.streamingAutoFollowEnabled = current.streamingAutoFollowEnabled;
}
if (current.sessionRecapEnabled !== previous.sessionRecapEnabled) {
diff.sessionRecapEnabled = current.sessionRecapEnabled;
}
+1 -1
View File
@@ -131,6 +131,7 @@ export type DesktopSettings = {
defaultVariant?: string;
defaultAgent?: string;
smallModelUseDefault?: boolean;
streamingAutoFollowEnabled?: boolean;
sessionRecapEnabled?: boolean;
sessionSuggestionEnabled?: boolean;
sessionGoalEnabled?: boolean;
@@ -179,7 +180,6 @@ export type DesktopSettings = {
collapsibleUserMessages?: boolean;
stickyUserHeader?: boolean;
promptNavigatorEnabled?: boolean;
expandedEditorToolbar?: boolean;
wideChatLayoutEnabled?: boolean;
showSplitAssistantMessageActions?: boolean;
fontSize?: number;
@@ -1845,9 +1845,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeRaw': 'Rohes Markdown',
'settings.voice.page.field.ttsInputModeSummarized': 'zusammengefasst',
'settings.openchamber.visual.section.colorMode': 'Farbmodus',
'settings.openchamber.visual.section.mobileLayout': 'Mobiles Layout',
'settings.openchamber.visual.option.mobileLayout.default': 'Alt',
'settings.openchamber.visual.option.mobileLayout.new': 'Neu',
'settings.openchamber.visual.section.localization': 'Lokalisierung',
'settings.openchamber.visual.section.spacingAndLayout': 'Abstand & Layout',
'settings.openchamber.visual.section.navigation': 'Navigation',
@@ -1859,6 +1856,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': 'Werkzeuge standardmäßig geöffnet anzeigen:',
'settings.openchamber.visual.section.sessionAssistance': 'Sitzungshilfe',
'settings.openchamber.visual.section.reasoning': 'Reasoning',
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Neuen Inhalten beim Streaming folgen',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Neuen Inhalten automatisch folgen, während eine Antwort gestreamt wird',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Während eine Antwort eintrifft, folgt die Ansicht laufend dem neuesten Inhalt. Deaktivieren, um die Ansicht ruhig zu halten und manuell zu scrollen.',
'settings.openchamber.visual.section.messageAppearance': 'Nachrichten-Erscheinungsbild',
'settings.openchamber.visual.section.toolsAndFiles': 'Werkzeuge & Dateien',
'settings.openchamber.visual.section.composer': 'Komponist',
@@ -1920,6 +1921,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Versatz der Eingabeleiste zurücksetzen',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Schnelltasten des Terminals',
'settings.openchamber.visual.field.terminalQuickKeys': 'Schnelltasten des Terminals',
'settings.openchamber.visual.field.sessionTabsGroup': 'Sitzungs-Tabs',
'settings.openchamber.visual.field.sessionTabs': 'Sitzungen als Tabs in der Kopfzeile anzeigen',
'settings.openchamber.visual.field.sessionTabsAria': 'Sitzungs-Tabs in der Kopfzeile umschalten',
'settings.openchamber.visual.field.sessionTabsInfo': 'Geöffnete Sitzungen erscheinen als Tabs in der Kopfzeile. Ausgeschaltet zeigt die Kopfzeile wieder nur den Sitzungstitel.',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Esc, Strg, Pfeiltasten in der Terminalansicht anzeigen',
'settings.openchamber.visual.field.fileEditorKeymap': 'Tastaturlayout für Datei-Editor',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Standard',
@@ -1952,8 +1957,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': 'Fixierter Benutzerkopf',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': 'Prompt-Navigator',
'settings.openchamber.visual.field.promptNavigatorEnabled': 'Prompt-Navigator',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Editor-Werkzeugleiste immer anzeigen',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Editor-Werkzeugleiste immer anzeigen (unter den Datei-Reitern angeheftet)',
'settings.openchamber.visual.field.wideChatLayoutAria': 'Breites Chat-Layout',
'settings.openchamber.visual.field.wideChatLayout': 'Breites Chat-Layout',
'settings.openchamber.visual.field.codeBlockLineWrapAria': 'Codeblock-Zeilen umbrechen',
+4
View File
@@ -419,6 +419,10 @@ export const dict = {
'sessions.sidebar.activity.chatsEmpty': 'Noch keine Chats.',
'chat.chatInput.chooseProject': 'Projekt auswählen',
'sessions.switcher.openAria': 'Sitzungswechsler öffnen',
'header.sessionTabs.stripAria': 'Offene Sitzungen',
'header.sessionTabs.tabMenuAria': 'Aktionen für den Sitzungs-Tab',
'header.sessionTabs.closeTab': 'Tab schließen',
'header.sessionTabs.closeOtherTabs': 'Andere Tabs schließen',
'sessions.switcher.empty': 'Keine kürzlichen Sitzungen',
'sessions.switcher.draftTitle': 'Neue Sitzung',
'sessions.sidebar.updateCheck.errorTitle': 'Fehler beim Prüfen auf Aktualisierungen',
@@ -1913,9 +1913,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeSummarized': 'summarized',
'settings.openchamber.visual.section.colorMode': 'Color Mode',
'settings.openchamber.visual.section.colorModeAndTheme': 'Color mode & Theme',
'settings.openchamber.visual.section.mobileLayout': 'Mobile Layout',
'settings.openchamber.visual.option.mobileLayout.default': 'Old',
'settings.openchamber.visual.option.mobileLayout.new': 'New',
'settings.openchamber.visual.section.localization': 'Localization',
'settings.openchamber.visual.section.spacingAndLayout': 'Spacing & Layout',
'settings.openchamber.visual.section.densityAndType': 'Density & type',
@@ -1932,6 +1929,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': 'Show tools opened by default',
'settings.openchamber.visual.section.sessionAssistance': 'Session Assistance',
'settings.openchamber.visual.section.reasoning': 'Reasoning',
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Follow new content while streaming',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Automatically follow new content while a response streams',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'While a reply streams in, the view keeps gliding to the newest content. Turn this off to keep the view still and scroll manually.',
'settings.openchamber.visual.section.messageAppearance': 'Message Appearance',
'settings.openchamber.visual.section.toolsAndFiles': 'Tools & Files',
'settings.openchamber.visual.section.composer': 'Composer',
@@ -1998,6 +1999,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Reset input bar offset',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Terminal quick keys',
'settings.openchamber.visual.field.terminalQuickKeys': 'Terminal Quick Keys',
'settings.openchamber.visual.field.sessionTabsGroup': 'Session tabs',
'settings.openchamber.visual.field.sessionTabs': 'Show sessions as tabs in the header',
'settings.openchamber.visual.field.sessionTabsAria': 'Toggle session tabs in the header',
'settings.openchamber.visual.field.sessionTabsInfo': 'Sessions you open line up as tabs in the header. Turning this off restores the plain session title.',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Show Esc, Ctrl, Arrows in terminal view',
'settings.openchamber.visual.field.fileEditorKeymap': 'File editor keymap',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'Default',
@@ -2030,8 +2035,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': 'Sticky User Header',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': 'Prompt navigator',
'settings.openchamber.visual.field.promptNavigatorEnabled': 'Prompt Navigator',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Always show editor toolbar',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Always show editor toolbar (docked under the file tabs)',
'settings.openchamber.visual.field.autoSaveEnabledAria': 'Auto-save files',
'settings.openchamber.visual.field.autoSaveEnabled': 'Auto-save files',
'settings.openchamber.visual.field.autoSaveEnabledInfo': 'Automatically save file edits after you stop typing. Disable to require manual save.',
+4
View File
@@ -481,6 +481,10 @@ export const dict = {
'sessions.archivePage.deleteSessionAria': 'Delete {title}',
'sessions.archivePage.restoreSessionAria': 'Restore {title}',
'sessions.switcher.openAria': 'Open session switcher',
'header.sessionTabs.stripAria': 'Open sessions',
'header.sessionTabs.tabMenuAria': 'Session tab actions',
'header.sessionTabs.closeTab': 'Close tab',
'header.sessionTabs.closeOtherTabs': 'Close other tabs',
'sessions.switcher.empty': 'No recent sessions',
'sessions.switcher.draftTitle': 'New session',
'sessions.sidebar.updateCheck.errorTitle': 'Failed to check for updates',
@@ -1890,9 +1890,6 @@ export const settingsDict = {
"settings.voice.page.field.ttsInputModeSummarized": "resumido",
"settings.openchamber.visual.section.colorMode": "Modo de color",
"settings.openchamber.visual.section.colorModeAndTheme": "Modo de color y tema",
"settings.openchamber.visual.section.mobileLayout": "Diseño móvil",
"settings.openchamber.visual.option.mobileLayout.default": "Anterior",
"settings.openchamber.visual.option.mobileLayout.new": "Nuevo",
"settings.openchamber.visual.section.localization": "Localización",
"settings.openchamber.visual.section.spacingAndLayout": "Espaciado y diseño",
"settings.openchamber.visual.section.densityAndType": "Densidad y tipografía",
@@ -1909,6 +1906,10 @@ export const settingsDict = {
"settings.openchamber.visual.section.showToolsOpenedByDefault": "Mostrar herramientas abiertas por defecto",
"settings.openchamber.visual.section.sessionAssistance": "Asistencia de sesión",
"settings.openchamber.visual.section.reasoning": "Razonamiento",
"settings.openchamber.visual.section.streaming": "Streaming",
"settings.openchamber.visual.field.streamingAutoFollow": "Seguir el contenido nuevo durante el streaming",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Seguir automáticamente el contenido nuevo mientras se transmite una respuesta",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Mientras llega una respuesta, la vista se desplaza hacia el contenido más reciente. Desactívalo para mantener la vista quieta y desplazarte manualmente.",
"settings.openchamber.visual.section.messageAppearance": "Apariencia de los mensajes",
"settings.openchamber.visual.section.toolsAndFiles": "Herramientas y archivos",
"settings.openchamber.visual.section.composer": "Compositor",
@@ -1975,6 +1976,10 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Restablecer desplazamiento de la barra de entrada",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas del terminal",
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas del terminal",
"settings.openchamber.visual.field.sessionTabsGroup": "Pestañas de sesión",
"settings.openchamber.visual.field.sessionTabs": "Mostrar las sesiones como pestañas en el encabezado",
"settings.openchamber.visual.field.sessionTabsAria": "Alternar las pestañas de sesión en el encabezado",
"settings.openchamber.visual.field.sessionTabsInfo": "Las sesiones que abres se alinean como pestañas en el encabezado. Al desactivarlo, el encabezado vuelve a mostrar solo el título de la sesión.",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl y flechas en la vista del terminal",
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas del editor de archivos",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Predeterminado",
@@ -2007,8 +2012,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.stickyUserHeader": "Encabezado de usuario fijo",
"settings.openchamber.visual.field.promptNavigatorEnabledAria": "Navegador de prompts",
"settings.openchamber.visual.field.promptNavigatorEnabled": "Navegador de prompts",
"settings.openchamber.visual.field.expandedEditorToolbarAria": "Mostrar siempre la barra de herramientas del editor",
"settings.openchamber.visual.field.expandedEditorToolbar": "Mostrar siempre la barra de herramientas del editor (anclada bajo las pestañas)",
"settings.openchamber.visual.field.autoSaveEnabledAria": "Guardado automático de archivos",
"settings.openchamber.visual.field.autoSaveEnabled": "Guardado automático de archivos",
"settings.openchamber.visual.field.autoSaveEnabledInfo": "Guarda automáticamente las ediciones del archivo después de dejar de escribir. Desactívalo para exigir un guardado manual.",
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Eliminar {title}",
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
"sessions.switcher.openAria": "Abrir selector de sesiones",
"header.sessionTabs.stripAria": "Sesiones abiertas",
"header.sessionTabs.tabMenuAria": "Acciones de la pestaña de sesión",
"header.sessionTabs.closeTab": "Cerrar pestaña",
"header.sessionTabs.closeOtherTabs": "Cerrar las demás pestañas",
"sessions.switcher.empty": "No hay sesiones recientes",
"sessions.switcher.draftTitle": "Nueva sesión",
"sessions.sidebar.updateCheck.errorTitle": "No se pudo comprobar actualizaciones",
@@ -1820,6 +1820,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': 'Afficher les outils ouverts par défaut',
'settings.openchamber.visual.section.sessionAssistance': 'Assistance de session',
'settings.openchamber.visual.section.reasoning': 'Raisonnement',
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Suivre le nouveau contenu pendant le streaming',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Suivre automatiquement le nouveau contenu pendant la diffusion dune réponse',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Pendant quune réponse arrive, la vue glisse vers le contenu le plus récent. Désactivez pour garder la vue immobile et défiler manuellement.',
'settings.openchamber.visual.section.messageAppearance': 'Apparence des messages',
'settings.openchamber.visual.section.toolsAndFiles': 'Outils et fichiers',
'settings.openchamber.visual.section.composer': 'Zone de saisie',
@@ -1882,6 +1886,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': 'Réinitialiser le décalage de la barre d\'entrée',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Touches rapides du terminal',
'settings.openchamber.visual.field.terminalQuickKeys': 'Touches rapides du terminal',
'settings.openchamber.visual.field.sessionTabsGroup': 'Onglets de session',
'settings.openchamber.visual.field.sessionTabs': 'Afficher les sessions sous forme d\'onglets dans l\'en-tête',
'settings.openchamber.visual.field.sessionTabsAria': 'Basculer les onglets de session dans l\'en-tête',
'settings.openchamber.visual.field.sessionTabsInfo': 'Les sessions ouvertes s\'alignent en onglets dans l\'en-tête. Désactivé, l\'en-tête n\'affiche que le titre de la session.',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Afficher Esc, Ctrl, Flèches dans la vue du terminal',
'settings.openchamber.visual.field.activityDefaultModeAria': 'Mode d\'activité par défaut : {option}',
'settings.openchamber.visual.field.showExpandedBashToolsAria': 'Afficher les outils bash étendus',
@@ -1911,8 +1919,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': 'En-tête utilisateur collant',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': 'Navigateur de prompts',
'settings.openchamber.visual.field.promptNavigatorEnabled': 'Navigateur de prompts',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Toujours afficher la barre doutils de l’éditeur',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Toujours afficher la barre doutils de l’éditeur (ancrée sous les onglets de fichiers)',
'settings.openchamber.visual.field.autoSaveEnabledAria': 'Enregistrement automatique des fichiers',
'settings.openchamber.visual.field.autoSaveEnabled': 'Enregistrement automatique des fichiers',
'settings.openchamber.visual.field.autoSaveEnabledInfo': 'Enregistre automatiquement les modifications après larrêt de la saisie. Désactivez pour exiger un enregistrement manuel.',
@@ -2147,9 +2153,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeSanitized': 'Nettoyé',
'settings.voice.page.field.ttsInputModeRaw': 'Markdown brut',
'settings.voice.page.field.ttsInputModeSummarized': 'résumé',
'settings.openchamber.visual.section.mobileLayout': 'Mise en page mobile',
'settings.openchamber.visual.option.mobileLayout.default': 'Ancienne',
'settings.openchamber.visual.option.mobileLayout.new': 'Nouvelle',
'settings.openchamber.visual.field.dockBadge': 'Badge du Dock',
'settings.openchamber.visual.field.dockBadgeHint': 'Afficher sur licône du Dock de macOS le nombre de discussions avec une activité non vue.',
'settings.openchamber.visual.actions.saveAndRestart': 'Enregistrer et redémarrer',
+4
View File
@@ -312,6 +312,10 @@ export const dict = {
'sessions.archivePage.deleteSessionAria': 'Supprimer {title}',
'sessions.archivePage.restoreSessionAria': 'Restaurer {title}',
'sessions.switcher.openAria': 'Sélecteur de session ouvert',
'header.sessionTabs.stripAria': 'Sessions ouvertes',
'header.sessionTabs.tabMenuAria': 'Actions de l\'onglet de session',
'header.sessionTabs.closeTab': 'Fermer l\'onglet',
'header.sessionTabs.closeOtherTabs': 'Fermer les autres onglets',
'sessions.switcher.empty': 'Aucune session récente',
'sessions.switcher.draftTitle': 'Nouvelle session',
'sessions.sidebar.updateCheck.errorTitle': 'Échec de la vérification des mises à jour',
@@ -1923,9 +1923,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeSummarized': '要約',
'settings.openchamber.visual.section.colorMode': 'カラーモード',
'settings.openchamber.visual.section.colorModeAndTheme': 'カラーモードとテーマ',
'settings.openchamber.visual.section.mobileLayout': 'モバイルレイアウト',
'settings.openchamber.visual.option.mobileLayout.default': '旧',
'settings.openchamber.visual.option.mobileLayout.new': '新',
'settings.openchamber.visual.section.localization': 'ローカライゼーション',
'settings.openchamber.visual.section.spacingAndLayout': '間隔とレイアウト',
'settings.openchamber.visual.section.densityAndType': '密度と書体',
@@ -1942,6 +1939,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': 'デフォルトで開くツールを表示',
'settings.openchamber.visual.section.sessionAssistance': 'セッション支援',
'settings.openchamber.visual.section.reasoning': '推論',
'settings.openchamber.visual.section.streaming': 'ストリーミング',
'settings.openchamber.visual.field.streamingAutoFollow': '応答のストリーミング中に新しい内容を追従',
'settings.openchamber.visual.field.streamingAutoFollowAria': '応答のストリーミング中に新しい内容へ自動スクロールする',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '応答の受信中、ビューは常に最新の内容へスクロールします。オフにするとビューは動かず、手動でスクロールできます。',
'settings.openchamber.visual.section.messageAppearance': 'メッセージの外観',
'settings.openchamber.visual.section.toolsAndFiles': 'ツールとファイル',
'settings.openchamber.visual.section.composer': '入力欄',
@@ -2008,6 +2009,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '入力バーオフセットをリセット',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'ターミナルクイックキー',
'settings.openchamber.visual.field.terminalQuickKeys': 'ターミナルクイックキー',
'settings.openchamber.visual.field.sessionTabsGroup': 'セッションタブ',
'settings.openchamber.visual.field.sessionTabs': 'ヘッダーにセッションをタブとして表示',
'settings.openchamber.visual.field.sessionTabsAria': 'ヘッダーのセッションタブを切り替え',
'settings.openchamber.visual.field.sessionTabsInfo': '開いたセッションがヘッダーにタブとして並びます。オフにするとヘッダーはセッションタイトルのみ表示します。',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'ターミナルビューに Esc、Ctrl、矢印を表示',
'settings.openchamber.visual.field.fileEditorKeymap': 'ファイルエディターキーマップ',
'settings.openchamber.visual.option.fileEditorKeymap.default': 'デフォルト',
@@ -2040,8 +2045,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': 'ユーザーヘッダー固定',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': 'プロンプトナビゲーター',
'settings.openchamber.visual.field.promptNavigatorEnabled': 'プロンプトナビゲーター',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'エディターツールバーを常に表示',
'settings.openchamber.visual.field.expandedEditorToolbar': 'エディターツールバーを常に表示(ファイルタブの下にドッキング)',
'settings.openchamber.visual.field.autoSaveEnabledAria': 'ファイルの自動保存',
'settings.openchamber.visual.field.autoSaveEnabled': 'ファイルの自動保存',
'settings.openchamber.visual.field.autoSaveEnabledInfo': '入力を止めた後にファイルの編集内容を自動保存します。無効にすると手動保存が必要になります。',
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '{title} を削除',
'sessions.archivePage.restoreSessionAria': '{title} を復元',
'sessions.switcher.openAria': 'セッションスイッチャーを開く',
'header.sessionTabs.stripAria': '開いているセッション',
'header.sessionTabs.tabMenuAria': 'セッションタブの操作',
'header.sessionTabs.closeTab': 'タブを閉じる',
'header.sessionTabs.closeOtherTabs': '他のタブを閉じる',
'sessions.switcher.empty': '最近のセッションはありません',
'sessions.switcher.draftTitle': '新しいセッション',
'sessions.sidebar.updateCheck.errorTitle': '更新の確認に失敗しました',
@@ -1890,9 +1890,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeSummarized': '요약',
'settings.openchamber.visual.section.colorMode': '색상 모드',
'settings.openchamber.visual.section.colorModeAndTheme': '색상 모드 및 테마',
'settings.openchamber.visual.section.mobileLayout': '모바일 레이아웃',
'settings.openchamber.visual.option.mobileLayout.default': '이전',
'settings.openchamber.visual.option.mobileLayout.new': '새로움',
'settings.openchamber.visual.section.localization': '지역화',
'settings.openchamber.visual.section.spacingAndLayout': '간격 및 레이아웃',
'settings.openchamber.visual.section.densityAndType': '밀도 및 서체',
@@ -1909,6 +1906,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': '도구를 기본으로 펼쳐 표시',
'settings.openchamber.visual.section.sessionAssistance': '세션 지원',
'settings.openchamber.visual.section.reasoning': '추론',
'settings.openchamber.visual.section.streaming': '스트리밍',
'settings.openchamber.visual.field.streamingAutoFollow': '스트리밍 중 새 내용 따라가기',
'settings.openchamber.visual.field.streamingAutoFollowAria': '응답 스트리밍 중 새 내용으로 자동 스크롤',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '응답이 스트리밍되는 동안 화면이 최신 내용으로 계속 이동합니다. 끄면 화면이 고정되어 직접 스크롤할 수 있습니다.',
'settings.openchamber.visual.section.messageAppearance': '메시지 모양',
'settings.openchamber.visual.section.toolsAndFiles': '도구 및 파일',
'settings.openchamber.visual.section.composer': '입력창',
@@ -1975,6 +1976,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '입력 바 오프셋 초기화',
'settings.openchamber.visual.field.terminalQuickKeysAria': '터미널 빠른 키',
'settings.openchamber.visual.field.terminalQuickKeys': '터미널 빠른 키',
'settings.openchamber.visual.field.sessionTabsGroup': '세션 탭',
'settings.openchamber.visual.field.sessionTabs': '헤더에 세션을 탭으로 표시',
'settings.openchamber.visual.field.sessionTabsAria': '헤더 세션 탭 전환',
'settings.openchamber.visual.field.sessionTabsInfo': '연 세션이 헤더에 탭으로 나열됩니다. 끄면 헤더에 세션 제목만 표시됩니다.',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '터미널 보기에서 Esc, Ctrl, 화살표를 표시합니다',
'settings.openchamber.visual.field.fileEditorKeymap': '파일 편집기 키맵',
'settings.openchamber.visual.option.fileEditorKeymap.default': '기본값',
@@ -2007,8 +2012,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': '고정 사용자 헤더',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': '프롬프트 탐색기',
'settings.openchamber.visual.field.promptNavigatorEnabled': '프롬프트 탐색기',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Always show editor toolbar',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Always show editor toolbar (docked under the file tabs)',
'settings.openchamber.visual.field.autoSaveEnabledAria': '파일 자동 저장',
'settings.openchamber.visual.field.autoSaveEnabled': '파일 자동 저장',
'settings.openchamber.visual.field.autoSaveEnabledInfo': '입력을 멈춘 후 파일 편집 내용을 자동으로 저장합니다. 끄면 수동으로 저장해야 합니다.',
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '{title} 삭제',
'sessions.archivePage.restoreSessionAria': '{title} 복원',
'sessions.switcher.openAria': '세션 전환기 열기',
'header.sessionTabs.stripAria': '열린 세션',
'header.sessionTabs.tabMenuAria': '세션 탭 작업',
'header.sessionTabs.closeTab': '탭 닫기',
'header.sessionTabs.closeOtherTabs': '다른 탭 닫기',
'sessions.switcher.empty': '최근 세션 없음',
'sessions.switcher.draftTitle': '새 세션',
'sessions.sidebar.updateCheck.errorTitle': '업데이트 확인 실패',
@@ -1111,8 +1111,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeaderAria': 'Przyklejony nagłówek użytkownika',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': 'Nawigator promptów',
'settings.openchamber.visual.field.promptNavigatorEnabled': 'Nawigator promptów',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Always show editor toolbar',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Always show editor toolbar (docked under the file tabs)',
'settings.openchamber.visual.field.autoSaveEnabledAria': 'Autozapis plików',
'settings.openchamber.visual.field.autoSaveEnabled': 'Autozapis plików',
'settings.openchamber.visual.field.autoSaveEnabledInfo': 'Automatycznie zapisuje edycje pliku po zatrzymaniu pisania. Wyłącz, aby wymagać ręcznego zapisu.',
@@ -1124,6 +1122,10 @@ export const settingsDict = {
'settings.openchamber.visual.option.terminalShell.auto': 'Automatycznie',
'settings.openchamber.visual.field.editorFontSize': 'Rozmiar czcionki edytora',
'settings.openchamber.visual.field.terminalQuickKeys': 'Szybkie klawisze terminala',
'settings.openchamber.visual.field.sessionTabsGroup': 'Karty sesji',
'settings.openchamber.visual.field.sessionTabs': 'Pokazuj sesje jako karty w nagłówku',
'settings.openchamber.visual.field.sessionTabsAria': 'Przełącz karty sesji w nagłówku',
'settings.openchamber.visual.field.sessionTabsInfo': 'Otwierane sesje układają się jako karty w nagłówku. Po wyłączeniu nagłówek pokazuje tylko tytuł sesji.',
'settings.openchamber.visual.field.terminalQuickKeysAria': 'Szybkie klawisze terminala',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': 'Pokaż Esc, Ctrl i strzałki w widoku terminala',
'settings.openchamber.visual.field.fileEditorKeymap': 'Mapa klawiszy edytora plików',
@@ -1196,9 +1198,6 @@ export const settingsDict = {
'settings.openchamber.visual.section.chatFeatures': 'Funkcje',
'settings.openchamber.visual.section.colorMode': 'Tryb kolorów',
'settings.openchamber.visual.section.colorModeAndTheme': 'Tryb kolorów i motyw',
'settings.openchamber.visual.section.mobileLayout': 'Układ mobilny',
'settings.openchamber.visual.option.mobileLayout.default': 'Poprzedni',
'settings.openchamber.visual.option.mobileLayout.new': 'Nowy',
'settings.openchamber.visual.section.diffLayout': 'Układ diffa',
'settings.openchamber.visual.section.diffLayoutAria': 'Układ diffa',
'settings.openchamber.visual.section.localization': 'Lokalizacja',
@@ -1210,6 +1209,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': 'Pokaż narzędzia domyślnie otwarte',
'settings.openchamber.visual.section.sessionAssistance': 'Wsparcie sesji',
'settings.openchamber.visual.section.reasoning': 'Rozumowanie',
'settings.openchamber.visual.section.streaming': 'Streaming',
'settings.openchamber.visual.field.streamingAutoFollow': 'Podążaj za nową treścią podczas streamingu',
'settings.openchamber.visual.field.streamingAutoFollowAria': 'Automatycznie podążaj za nową treścią podczas streamowania odpowiedzi',
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Podczas napływania odpowiedzi widok płynnie podąża za najnowszą treścią. Wyłącz, aby widok pozostał nieruchomy i przewijać ręcznie.',
'settings.openchamber.visual.section.messageAppearance': 'Wygląd wiadomości',
'settings.openchamber.visual.section.toolsAndFiles': 'Narzędzia i pliki',
'settings.openchamber.visual.section.composer': 'Pole wiadomości',
+4
View File
@@ -293,6 +293,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': 'Usuń {title}',
'sessions.archivePage.restoreSessionAria': 'Przywróć {title}',
'sessions.switcher.openAria': 'Otwórz przełącznik sesji',
'header.sessionTabs.stripAria': 'Otwarte sesje',
'header.sessionTabs.tabMenuAria': 'Akcje karty sesji',
'header.sessionTabs.closeTab': 'Zamknij kartę',
'header.sessionTabs.closeOtherTabs': 'Zamknij pozostałe karty',
'sessions.switcher.empty': 'Brak ostatnich sesji',
'sessions.switcher.draftTitle': 'Nowa sesja',
'sessions.sidebar.updateCheck.errorTitle': 'Nie udało się sprawdzić aktualizacji',
@@ -1890,9 +1890,6 @@ export const settingsDict = {
"settings.voice.page.field.ttsInputModeSummarized": "resumido",
"settings.openchamber.visual.section.colorMode": "Modo de cor",
"settings.openchamber.visual.section.colorModeAndTheme": "Modo de cor e tema",
"settings.openchamber.visual.section.mobileLayout": "Layout móvel",
"settings.openchamber.visual.option.mobileLayout.default": "Anterior",
"settings.openchamber.visual.option.mobileLayout.new": "Novo",
"settings.openchamber.visual.section.localization": "Localização",
"settings.openchamber.visual.section.spacingAndLayout": "Espaçamento e layout",
"settings.openchamber.visual.section.densityAndType": "Densidade e tipografia",
@@ -1909,6 +1906,10 @@ export const settingsDict = {
"settings.openchamber.visual.section.showToolsOpenedByDefault": "Mostrar ferramentas abertas por padrão",
"settings.openchamber.visual.section.sessionAssistance": "Assistência da sessão",
"settings.openchamber.visual.section.reasoning": "Raciocínio",
"settings.openchamber.visual.section.streaming": "Streaming",
"settings.openchamber.visual.field.streamingAutoFollow": "Seguir o novo conteúdo durante o streaming",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Seguir automaticamente o novo conteúdo enquanto uma resposta é transmitida",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Enquanto uma resposta chega, a visualização acompanha o conteúdo mais recente. Desative para manter a visualização parada e rolar manualmente.",
"settings.openchamber.visual.section.messageAppearance": "Aparência das mensagens",
"settings.openchamber.visual.section.toolsAndFiles": "Ferramentas e arquivos",
"settings.openchamber.visual.section.composer": "Campo de mensagem",
@@ -1975,6 +1976,10 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Redefinir deslocamento da barra de entrada",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Teclas rápidas do terminal",
"settings.openchamber.visual.field.terminalQuickKeys": "Teclas rápidas do terminal",
"settings.openchamber.visual.field.sessionTabsGroup": "Abas de sessão",
"settings.openchamber.visual.field.sessionTabs": "Mostrar sessões como abas no cabeçalho",
"settings.openchamber.visual.field.sessionTabsAria": "Alternar abas de sessão no cabeçalho",
"settings.openchamber.visual.field.sessionTabsInfo": "As sessões abertas se alinham como abas no cabeçalho. Desativado, o cabeçalho volta a mostrar apenas o título da sessão.",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Mostrar Esc, Ctrl e flechas na vista do terminal",
"settings.openchamber.visual.field.fileEditorKeymap": "Mapa de teclas do editor de arquivos",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Padrão",
@@ -2007,8 +2012,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.stickyUserHeader": "Cabeçalho do usuário fixo",
"settings.openchamber.visual.field.promptNavigatorEnabledAria": "Navegador de prompts",
"settings.openchamber.visual.field.promptNavigatorEnabled": "Navegador de prompts",
"settings.openchamber.visual.field.expandedEditorToolbarAria": "Mostrar sempre a barra de ferramentas do editor",
"settings.openchamber.visual.field.expandedEditorToolbar": "Mostrar sempre a barra de ferramentas do editor (ancorada sob as abas)",
"settings.openchamber.visual.field.autoSaveEnabledAria": "Salvamento automático de arquivos",
"settings.openchamber.visual.field.autoSaveEnabled": "Salvamento automático de arquivos",
"settings.openchamber.visual.field.autoSaveEnabledInfo": "Salva automaticamente as edições do arquivo depois que você parar de digitar. Desative para exigir salvamento manual.",
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Excluir {title}",
"sessions.archivePage.restoreSessionAria": "Restaurar {title}",
"sessions.switcher.openAria": "Abrir seletor de sessões",
"header.sessionTabs.stripAria": "Sessões abertas",
"header.sessionTabs.tabMenuAria": "Ações da aba de sessão",
"header.sessionTabs.closeTab": "Fechar aba",
"header.sessionTabs.closeOtherTabs": "Fechar outras abas",
"sessions.switcher.empty": "Nenhuma sessão recente",
"sessions.switcher.draftTitle": "Nova sessão",
"sessions.sidebar.updateCheck.errorTitle": "Não foi possível verificar atualizações",
@@ -1890,9 +1890,6 @@ export const settingsDict = {
"settings.voice.page.field.ttsInputModeSummarized": "скорочений",
"settings.openchamber.visual.section.colorMode": "Режим теми",
"settings.openchamber.visual.section.colorModeAndTheme": "Режим кольору та тема",
"settings.openchamber.visual.section.mobileLayout": "Мобільний макет",
"settings.openchamber.visual.option.mobileLayout.default": "Попередній",
"settings.openchamber.visual.option.mobileLayout.new": "Новий",
"settings.openchamber.visual.section.localization": "Локалізація",
"settings.openchamber.visual.section.spacingAndLayout": "Відступи й компонування",
"settings.openchamber.visual.section.densityAndType": "Щільність і шрифти",
@@ -1909,6 +1906,10 @@ export const settingsDict = {
"settings.openchamber.visual.section.showToolsOpenedByDefault": "Показувати інструменти відкритими за замовчуванням",
"settings.openchamber.visual.section.sessionAssistance": "Допомога із сесією",
"settings.openchamber.visual.section.reasoning": "Міркування",
"settings.openchamber.visual.section.streaming": "Стримінг",
"settings.openchamber.visual.field.streamingAutoFollow": "Слідкувати за новим вмістом під час стримінгу",
"settings.openchamber.visual.field.streamingAutoFollowAria": "Автоматично слідкувати за новим вмістом під час стримінгу відповіді",
"settings.openchamber.visual.field.streamingAutoFollowInfo": "Поки відповідь надходить, вигляд плавно рухається до найновішого вмісту. Вимкніть, щоб вигляд залишався нерухомим і гортати вручну.",
"settings.openchamber.visual.section.messageAppearance": "Вигляд повідомлень",
"settings.openchamber.visual.section.toolsAndFiles": "Інструменти та файли",
"settings.openchamber.visual.section.composer": "Поле вводу",
@@ -1975,6 +1976,10 @@ export const settingsDict = {
"settings.openchamber.visual.actions.resetInputBarOffsetAria": "Скинути зміщення панелі вводу",
"settings.openchamber.visual.field.terminalQuickKeysAria": "Швидкі клавіші терміналу",
"settings.openchamber.visual.field.terminalQuickKeys": "Швидкі клавіші терміналу",
"settings.openchamber.visual.field.sessionTabsGroup": "Вкладки сесій",
"settings.openchamber.visual.field.sessionTabs": "Показувати сесії як вкладки в хедері",
"settings.openchamber.visual.field.sessionTabsAria": "Перемкнути вкладки сесій у хедері",
"settings.openchamber.visual.field.sessionTabsInfo": "Відкриті сесії шикуються вкладками в хедері. Якщо вимкнено, хедер знову показує лише назву сесії.",
"settings.openchamber.visual.field.terminalQuickKeysTooltip": "Показати Esc, Ctrl, стрілки в поданні терміналу",
"settings.openchamber.visual.field.fileEditorKeymap": "Розкладка клавіш редактора файлів",
"settings.openchamber.visual.option.fileEditorKeymap.default": "Типова",
@@ -2007,8 +2012,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.stickyUserHeader": "Закріплений заголовок користувача",
"settings.openchamber.visual.field.promptNavigatorEnabledAria": "Навігатор промптів",
"settings.openchamber.visual.field.promptNavigatorEnabled": "Навігатор промптів",
"settings.openchamber.visual.field.expandedEditorToolbarAria": "Завжди показувати панель інструментів редактора",
"settings.openchamber.visual.field.expandedEditorToolbar": "Завжди показувати панель інструментів редактора (закріплена під вкладками)",
"settings.openchamber.visual.field.autoSaveEnabledAria": "Автозбереження файлів",
"settings.openchamber.visual.field.autoSaveEnabled": "Автозбереження файлів",
"settings.openchamber.visual.field.autoSaveEnabledInfo": "Автоматично зберігати зміни у файлі після того, як ви припините друкувати. Вимкніть, щоб зберігати лише вручну.",
+4
View File
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
"sessions.archivePage.deleteSessionAria": "Видалити {title}",
"sessions.archivePage.restoreSessionAria": "Відновити {title}",
"sessions.switcher.openAria": "Відкрити перемикач сесій",
"header.sessionTabs.stripAria": "Відкриті сесії",
"header.sessionTabs.tabMenuAria": "Дії вкладки сесії",
"header.sessionTabs.closeTab": "Закрити вкладку",
"header.sessionTabs.closeOtherTabs": "Закрити інші вкладки",
"sessions.switcher.empty": "Немає недавніх сесій",
"sessions.switcher.draftTitle": "Нова сесія",
"sessions.sidebar.updateCheck.errorTitle": "Не вдалося перейти на наявність оновлень",
@@ -1890,9 +1890,6 @@ export const settingsDict = {
'settings.voice.page.field.ttsInputModeSummarized': '摘要',
'settings.openchamber.visual.section.colorMode': '颜色模式',
'settings.openchamber.visual.section.colorModeAndTheme': '颜色模式与主题',
'settings.openchamber.visual.section.mobileLayout': '移动端布局',
'settings.openchamber.visual.option.mobileLayout.default': '旧版',
'settings.openchamber.visual.option.mobileLayout.new': '新版',
'settings.openchamber.visual.section.localization': '本地化',
'settings.openchamber.visual.section.spacingAndLayout': '间距与布局',
'settings.openchamber.visual.section.densityAndType': '密度与字体',
@@ -1909,6 +1906,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': '默认展开以下工具',
'settings.openchamber.visual.section.sessionAssistance': '会话辅助',
'settings.openchamber.visual.section.reasoning': '推理',
'settings.openchamber.visual.section.streaming': '流式输出',
'settings.openchamber.visual.field.streamingAutoFollow': '流式输出时跟随新内容',
'settings.openchamber.visual.field.streamingAutoFollowAria': '在回复流式输出时自动跟随新内容',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回复流式输出时,视图会持续滚动到最新内容。关闭后视图保持不动,可手动滚动。',
'settings.openchamber.visual.section.messageAppearance': '消息外观',
'settings.openchamber.visual.section.toolsAndFiles': '工具和文件',
'settings.openchamber.visual.section.composer': '输入框',
@@ -1975,6 +1976,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '重置输入栏偏移',
'settings.openchamber.visual.field.terminalQuickKeysAria': '终端快捷键',
'settings.openchamber.visual.field.terminalQuickKeys': '终端快捷键',
'settings.openchamber.visual.field.sessionTabsGroup': '会话标签页',
'settings.openchamber.visual.field.sessionTabs': '在页眉中以标签页显示会话',
'settings.openchamber.visual.field.sessionTabsAria': '切换页眉会话标签页',
'settings.openchamber.visual.field.sessionTabsInfo': '打开的会话会以标签页形式排列在页眉中。关闭后页眉仅显示会话标题。',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在终端视图显示 Esc、Ctrl、方向键',
'settings.openchamber.visual.field.fileEditorKeymap': '文件编辑器键位映射',
'settings.openchamber.visual.option.fileEditorKeymap.default': '默认',
@@ -2007,8 +2012,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': '固定用户消息头',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': '提示词导航',
'settings.openchamber.visual.field.promptNavigatorEnabled': '提示词导航',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Always show editor toolbar',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Always show editor toolbar (docked under the file tabs)',
'settings.openchamber.visual.field.autoSaveEnabledAria': '自动保存文件',
'settings.openchamber.visual.field.autoSaveEnabled': '自动保存文件',
'settings.openchamber.visual.field.autoSaveEnabledInfo': '停止输入后自动保存文件编辑内容。关闭后需手动保存。',
@@ -482,6 +482,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '删除 {title}',
'sessions.archivePage.restoreSessionAria': '还原 {title}',
'sessions.switcher.openAria': '打开会话切换器',
'header.sessionTabs.stripAria': '打开的会话',
'header.sessionTabs.tabMenuAria': '会话标签页操作',
'header.sessionTabs.closeTab': '关闭标签页',
'header.sessionTabs.closeOtherTabs': '关闭其他标签页',
'sessions.switcher.empty': '没有最近会话',
'sessions.switcher.draftTitle': '新会话',
'sessions.sidebar.updateCheck.errorTitle': '检查更新失败',
@@ -1801,7 +1801,6 @@ export const settingsDict = {
'settings.openchamber.visual.section.spacingAndLayout': '間距與佈局',
'settings.openchamber.visual.section.densityAndType': '密度與字型',
'settings.openchamber.visual.section.appInstall': '應用程式安裝',
'settings.openchamber.visual.section.mobileLayout': '行動版版面',
'settings.openchamber.visual.section.navigation': '導覽',
'settings.openchamber.visual.section.chatRenderMode': '聊天渲染模式',
'settings.openchamber.visual.section.chatRenderModeAria': '聊天渲染模式',
@@ -1814,6 +1813,10 @@ export const settingsDict = {
'settings.openchamber.visual.section.showToolsOpenedByDefault': '預設展開以下工具',
'settings.openchamber.visual.section.sessionAssistance': '工作階段輔助',
'settings.openchamber.visual.section.reasoning': '推理',
'settings.openchamber.visual.section.streaming': '串流',
'settings.openchamber.visual.field.streamingAutoFollow': '串流時跟隨新內容',
'settings.openchamber.visual.field.streamingAutoFollowAria': '回覆串流時自動跟隨新內容',
'settings.openchamber.visual.field.streamingAutoFollowInfo': '回覆串流時,畫面會持續捲動到最新內容。關閉後畫面保持不動,可手動捲動。',
'settings.openchamber.visual.section.messageAppearance': '訊息外觀',
'settings.openchamber.visual.section.toolsAndFiles': '工具與檔案',
'settings.openchamber.visual.section.composer': '輸入框',
@@ -1855,8 +1858,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.mobileKeyboardModeAria': '行動裝置鍵盤行為',
'settings.openchamber.visual.field.selectMobileKeyboardModePlaceholder': '選擇鍵盤行為',
'settings.openchamber.visual.actions.resetMobileKeyboardModeAria': '重設行動裝置鍵盤行為',
'settings.openchamber.visual.option.mobileLayout.default': '舊版',
'settings.openchamber.visual.option.mobileLayout.new': '新版',
'settings.openchamber.visual.field.interfaceFontSize': '介面字體大小',
'settings.openchamber.visual.field.interfaceFont': '介面字體',
'settings.openchamber.visual.field.selectInterfaceFontAria': '選擇介面字體',
@@ -1882,6 +1883,10 @@ export const settingsDict = {
'settings.openchamber.visual.actions.resetInputBarOffsetAria': '重設輸入列偏移',
'settings.openchamber.visual.field.terminalQuickKeysAria': '終端機快速鍵',
'settings.openchamber.visual.field.terminalQuickKeys': '終端機快速鍵',
'settings.openchamber.visual.field.sessionTabsGroup': '會話分頁',
'settings.openchamber.visual.field.sessionTabs': '在頁首以分頁顯示會話',
'settings.openchamber.visual.field.sessionTabsAria': '切換頁首會話分頁',
'settings.openchamber.visual.field.sessionTabsInfo': '開啟的會話會以分頁排列在頁首。關閉後頁首僅顯示會話標題。',
'settings.openchamber.visual.field.terminalQuickKeysTooltip': '在終端機檢視顯示 Esc、Ctrl、方向鍵',
'settings.openchamber.visual.field.fileEditorKeymap': '檔案編輯器鍵位映射',
'settings.openchamber.visual.option.fileEditorKeymap.default': '預設',
@@ -1914,8 +1919,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.stickyUserHeader': '固定使用者訊息標頭',
'settings.openchamber.visual.field.promptNavigatorEnabledAria': '提示詞導覽',
'settings.openchamber.visual.field.promptNavigatorEnabled': '提示詞導覽',
'settings.openchamber.visual.field.expandedEditorToolbarAria': 'Always show editor toolbar',
'settings.openchamber.visual.field.expandedEditorToolbar': 'Always show editor toolbar (docked under the file tabs)',
'settings.openchamber.visual.field.autoSaveEnabledAria': '自動儲存檔案',
'settings.openchamber.visual.field.autoSaveEnabled': '自動儲存檔案',
'settings.openchamber.visual.field.autoSaveEnabledInfo': '停止輸入後自動儲存檔案編輯內容。關閉後需手動儲存。',
@@ -495,6 +495,10 @@ export const dict: Record<I18nKey, string> = {
'sessions.archivePage.deleteSessionAria': '刪除 {title}',
'sessions.archivePage.restoreSessionAria': '還原 {title}',
'sessions.switcher.openAria': '開啟會話切換器',
'header.sessionTabs.stripAria': '開啟的會話',
'header.sessionTabs.tabMenuAria': '工作階段分頁動作',
'header.sessionTabs.closeTab': '關閉分頁',
'header.sessionTabs.closeOtherTabs': '關閉其他分頁',
'sessions.switcher.empty': '沒有最近會話',
'sessions.switcher.draftTitle': '新會話',
'sessions.sidebar.updateCheck.errorTitle': '檢查更新失敗',
@@ -1,34 +0,0 @@
export type MobileLayoutPreference = 'default' | 'new';
const MOBILE_LAYOUT_PREFERENCE_KEY = 'openchamber-mobile-layout';
const normalizeMobileLayoutPreference = (value: unknown): MobileLayoutPreference => {
// 'new' is the default; only an explicit 'default' (the legacy/"Old" layout)
// opts out of it.
return value === 'default' ? 'default' : 'new';
};
export const getStoredMobileLayoutPreference = (): MobileLayoutPreference => {
if (typeof window === 'undefined') {
return 'new';
}
try {
return normalizeMobileLayoutPreference(window.localStorage.getItem(MOBILE_LAYOUT_PREFERENCE_KEY));
} catch {
return 'new';
}
};
export const setStoredMobileLayoutPreference = (value: MobileLayoutPreference): boolean => {
if (typeof window === 'undefined') {
return false;
}
try {
window.localStorage.setItem(MOBILE_LAYOUT_PREFERENCE_KEY, value);
return true;
} catch {
return false;
}
};
+7 -7
View File
@@ -531,6 +531,7 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
darkThemeId: DEFAULT_DARK_THEME_ID,
openInAppId: DEFAULT_OPEN_IN_APP_ID,
showReasoningTraces: defaults.showReasoningTraces,
streamingAutoFollowEnabled: defaults.streamingAutoFollowEnabled,
workStatusPanelEnabled: defaults.workStatusPanelEnabled,
workStatusHiddenSections: defaults.workStatusHiddenSections,
sessionRecapEnabled: defaults.sessionRecapEnabled,
@@ -578,7 +579,6 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
messageStreamTransport: 'auto',
stickyUserHeader: defaults.stickyUserHeader,
promptNavigatorEnabled: defaults.promptNavigatorEnabled,
expandedEditorToolbar: defaults.expandedEditorToolbar,
wideChatLayoutEnabled: defaults.wideChatLayoutEnabled,
showSplitAssistantMessageActions: defaults.showSplitAssistantMessageActions,
draftStartersVisible: defaults.draftStartersVisible,
@@ -638,6 +638,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) {
store.setShowReasoningTraces(settings.showReasoningTraces);
}
if (typeof settings.streamingAutoFollowEnabled === 'boolean' && settings.streamingAutoFollowEnabled !== store.streamingAutoFollowEnabled) {
store.setStreamingAutoFollowEnabled(settings.streamingAutoFollowEnabled);
}
if (typeof settings.sessionRecapEnabled === 'boolean' && settings.sessionRecapEnabled !== store.sessionRecapEnabled) {
store.setSessionRecapEnabled(settings.sessionRecapEnabled);
}
@@ -842,9 +845,6 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
if (typeof settings.promptNavigatorEnabled === 'boolean' && settings.promptNavigatorEnabled !== store.promptNavigatorEnabled) {
store.setPromptNavigatorEnabled(settings.promptNavigatorEnabled);
}
if (typeof settings.expandedEditorToolbar === 'boolean' && settings.expandedEditorToolbar !== store.expandedEditorToolbar) {
store.setExpandedEditorToolbar(settings.expandedEditorToolbar);
}
if (typeof settings.wideChatLayoutEnabled === 'boolean' && settings.wideChatLayoutEnabled !== store.wideChatLayoutEnabled) {
store.setWideChatLayoutEnabled(settings.wideChatLayoutEnabled);
}
@@ -1162,6 +1162,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.showReasoningTraces === 'boolean') {
result.showReasoningTraces = candidate.showReasoningTraces;
}
if (typeof candidate.streamingAutoFollowEnabled === 'boolean') {
result.streamingAutoFollowEnabled = candidate.streamingAutoFollowEnabled;
}
if (typeof candidate.sessionRecapEnabled === 'boolean') {
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
}
@@ -1511,9 +1514,6 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.promptNavigatorEnabled === 'boolean') {
result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
}
if (typeof candidate.expandedEditorToolbar === 'boolean') {
result.expandedEditorToolbar = candidate.expandedEditorToolbar;
}
if (typeof candidate.wideChatLayoutEnabled === 'boolean') {
result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled;
}
+3 -3
View File
@@ -1,6 +1,6 @@
import type { WorkspaceSurface } from '@/stores/useUIStore';
import {
type RouteState,
type RouteTab,
VALID_TABS,
VALID_SETTINGS_SECTIONS,
ROUTE_PARAMS,
@@ -52,13 +52,13 @@ function parseSessionId(params: URLSearchParams): string | null {
* Parse main tab from URL parameters.
* Returns null if missing or invalid.
*/
function parseTab(params: URLSearchParams): WorkspaceSurface | null {
function parseTab(params: URLSearchParams): RouteTab | null {
const value = params.get(ROUTE_PARAMS.TAB);
if (!value) {
return null;
}
const normalized = value.toLowerCase().trim() as WorkspaceSurface;
const normalized = value.toLowerCase().trim() as RouteTab;
if (VALID_TABS.includes(normalized)) {
return normalized;
}
@@ -65,10 +65,8 @@ afterAll(() => {
const sessionState = (sessionId: string): AppRouteState => ({
sessionId,
tab: 'chat',
isSettingsOpen: false,
settingsPath: '',
diffFile: null,
});
describe('updateBrowserURL embedded-session-chat guard', () => {
@@ -1,4 +1,3 @@
import type { WorkspaceSurface } from '@/stores/useUIStore';
import { isEmbeddedSessionChat } from '@/components/layout/contextPanelEmbeddedChat';
import { ROUTE_PARAMS } from './types';
@@ -7,17 +6,10 @@ import { ROUTE_PARAMS } from './types';
*/
export interface AppRouteState {
sessionId: string | null;
tab: WorkspaceSurface;
isSettingsOpen: boolean;
settingsPath: string;
diffFile: string | null;
}
/**
* Default tab when none is specified.
*/
const DEFAULT_TAB: WorkspaceSurface = 'chat';
/**
* Serialize application state to URL search parameters.
* Only includes parameters that differ from defaults to keep URLs clean.
@@ -38,15 +30,6 @@ function serializeRoute(state: AppRouteState): URLSearchParams {
return params;
}
// Tab - only include if not the default
if (state.tab !== DEFAULT_TAB) {
params.set(ROUTE_PARAMS.TAB, state.tab);
}
// Diff file - only include when on diff tab
if (state.tab === 'diff' && state.diffFile && state.diffFile.trim().length > 0) {
params.set(ROUTE_PARAMS.FILE, state.diffFile);
}
return params;
}
+5 -4
View File
@@ -1,5 +1,4 @@
import type { SidebarSection } from '@/constants/sidebar';
import type { WorkspaceSurface } from '@/stores/useUIStore';
/**
* Represents the current route state derived from URL parameters.
@@ -9,7 +8,7 @@ export interface RouteState {
/** Session ID to navigate to */
sessionId: string | null;
/** View selected through the legacy `tab` URL parameter. */
tab: WorkspaceSurface | null;
tab: RouteTab | null;
/** Settings section - when non-null, settings dialog should be open */
settingsPath: string | null;
/** File path for diff view */
@@ -17,9 +16,11 @@ export interface RouteState {
}
/**
* Valid values for the legacy `tab` URL parameter.
* Valid values for the legacy `tab` URL parameter. Non-chat tabs open the
* matching context-panel surface; the chat always owns the main area.
*/
export const VALID_TABS: readonly WorkspaceSurface[] = ['chat', 'git', 'diff', 'terminal', 'files', 'diagram'] as const;
export type RouteTab = 'chat' | 'git' | 'diff' | 'terminal' | 'files';
export const VALID_TABS: readonly RouteTab[] = ['chat', 'git', 'diff', 'terminal', 'files'] as const;
/**
* Valid settings section values for URL routing.
+57 -11
View File
@@ -1,6 +1,5 @@
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { isCapacitorApp } from '@/lib/platform';
import { getStoredMobileLayoutPreference } from '@/lib/mobileLayoutPreference';
export type HostedSurface = 'desktop' | 'mobile';
@@ -11,6 +10,7 @@ declare global {
}
const MOBILE_SURFACE_MAX_WIDTH = 768;
const SURFACE_SWITCH_DEBOUNCE_MS = 800;
const isTouchOrCoarsePointer = (): boolean => {
if (typeof window === 'undefined') return false;
@@ -22,12 +22,29 @@ const isTouchOrCoarsePointer = (): boolean => {
return coarsePointer || touchPoints > 0;
};
const hasSurfaceUrlOverride = (): boolean => {
if (typeof window === 'undefined') return false;
const override = new URLSearchParams(window.location.search).get('surface');
return override === 'mobile' || override === 'desktop';
};
/** Viewport half of the surface decision; re-evaluated on resize by the watcher. */
const isPhoneViewport = (): boolean => {
if (typeof window === 'undefined') return false;
const width = Math.min(
window.innerWidth || Number.POSITIVE_INFINITY,
window.screen?.width || Number.POSITIVE_INFINITY,
);
return Number.isFinite(width)
&& width <= MOBILE_SURFACE_MAX_WIDTH
&& isTouchOrCoarsePointer();
};
/**
* Single authority for the mobile-vs-desktop surface decision.
*
* Priority: explicit stamp (set once at boot) URL override Capacitor
* shell (always the mobile surface) desktop shells phone heuristic
* gated by the stored mobile layout preference.
* shell (always the mobile surface) desktop shells phone heuristic.
*/
const detectHostedSurface = (): HostedSurface => {
if (typeof window === 'undefined') return 'desktop';
@@ -45,14 +62,7 @@ const detectHostedSurface = (): HostedSurface => {
if (isCapacitorApp()) return 'mobile';
if (isDesktopShell() || isVSCodeRuntime()) return 'desktop';
const width = Math.min(
window.innerWidth || Number.POSITIVE_INFINITY,
window.screen?.width || Number.POSITIVE_INFINITY,
);
const likelyPhone = Number.isFinite(width)
&& width <= MOBILE_SURFACE_MAX_WIDTH
&& isTouchOrCoarsePointer();
return likelyPhone && getStoredMobileLayoutPreference() === 'new' ? 'mobile' : 'desktop';
return isPhoneViewport() ? 'mobile' : 'desktop';
};
/**
@@ -69,3 +79,39 @@ export const resolveHostedSurface = (): HostedSurface => {
};
export const isMobileSurfaceRuntime = (): boolean => detectHostedSurface() === 'mobile';
/**
* The surface is stamped once at boot, so a browser window that crosses the
* phone threshold after load would otherwise keep the wrong app shell (the
* app trees, stores, and sync bootstrap differ, so an in-place switch is not
* safe). Watch for the viewport heuristic disagreeing with the stamp and
* reload the same mechanism a surface change has always used once the
* resize settles. Fixed shells never switch: Capacitor is always mobile,
* desktop/VS Code shells are always desktop, and an explicit ?surface=
* override wins over the heuristic.
*/
export const watchHostedSurfaceViewport = (): (() => void) => {
if (typeof window === 'undefined') return () => {};
if (isCapacitorApp() || isDesktopShell() || isVSCodeRuntime()) return () => {};
if (hasSurfaceUrlOverride()) return () => {};
let timeout: ReturnType<typeof setTimeout> | null = null;
const handleResize = () => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
const stamped = window.__OPENCHAMBER_SURFACE__;
const desired: HostedSurface = isPhoneViewport() ? 'mobile' : 'desktop';
if (stamped && stamped !== desired) {
window.__OPENCHAMBER_SURFACE__ = undefined;
window.location.reload();
}
}, SURFACE_SWITCH_DEBOUNCE_MS);
};
window.addEventListener('resize', handleResize);
return () => {
if (timeout) clearTimeout(timeout);
window.removeEventListener('resize', handleResize);
};
};
@@ -0,0 +1,68 @@
import { describe, expect, test } from 'bun:test';
import { matchesRankQuery, rankByQuery } from './fuzzySearch';
const rank = (items: string[], query: string) => rankByQuery(items, query, (item) => [item]);
describe('rankByQuery', () => {
test('orders word-boundary matches above mid-word matches, earlier positions first', () => {
const items = ['prefixed-thing', 'workspace-fix', 'feat/fix-scroll'];
expect(rank(items, 'fix')).toEqual(['feat/fix-scroll', 'workspace-fix', 'prefixed-thing']);
});
test('exact prefix comes first, ties keep original order', () => {
const items = ['main', 'feat/main-menu', 'maintenance', 'release/main'];
const ranked = rank(items, 'main');
expect(ranked[0]).toBe('main');
expect(ranked[1]).toBe('maintenance');
expect(ranked.slice(2)).toEqual(['feat/main-menu', 'release/main']);
});
test('multi-token queries match in any order and all tokens are required', () => {
const items = ['feat/scroll-anchored-chat', 'fix/chat-header', 'feat/scroll-perf'];
expect(rank(items, 'chat scroll')).toEqual(['feat/scroll-anchored-chat']);
});
test('punctuation-insensitive compact matching finds joined words', () => {
const items = ['gpt-4o-mini', 'claude-sonnet-5'];
expect(rank(items, 'gpt4o')).toEqual(['gpt-4o-mini']);
expect(rank(items, 'sonnet5')).toEqual(['claude-sonnet-5']);
});
test('single-token queries tolerate typos via fuzzy fallback', () => {
const items = ['workspace-rail-layout', 'unrelated'];
expect(rank(items, 'worskpace')).toEqual(['workspace-rail-layout']);
});
test('fuzzy fallback can be disabled', () => {
const items = ['workspace-rail-layout'];
expect(rankByQuery(items, 'worskpace', (item) => [item], { fuzzy: false })).toEqual([]);
});
test('earlier fields outrank later fields', () => {
const items = [
{ name: 'docs', path: '/repo/build-agent' },
{ name: 'build-agent', path: '/repo/build-agent' },
];
const ranked = rankByQuery(items, 'build', (item) => [item.name, item.path]);
expect(ranked[0].name).toBe('build-agent');
expect(ranked).toHaveLength(2);
});
test('empty query returns items unchanged within the limit', () => {
expect(rank(['b', 'a'], ' ')).toEqual(['b', 'a']);
expect(rankByQuery(['a', 'b', 'c'], '', (item) => [item], { limit: 2 })).toEqual(['a', 'b']);
});
});
describe('matchesRankQuery', () => {
test('requires every token across the fields', () => {
expect(matchesRankQuery(['GLM-5.3', 'Zhipu'], 'zhipu glm')).toBe(true);
expect(matchesRankQuery(['GLM-5.3', 'Zhipu'], 'zhipu gpt')).toBe(false);
});
test('is punctuation-insensitive and skips empty fields', () => {
expect(matchesRankQuery([null, 'claude-sonnet-5', undefined], 'sonnet5')).toBe(true);
expect(matchesRankQuery([''], 'a')).toBe(false);
});
});
+125 -63
View File
@@ -41,54 +41,6 @@ export function matchesFuzzyQuery(
return fuse.search(query).length > 0;
}
function getFuzzyMatchMask<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: FuzzySearchOptions
): boolean[] {
if (!query) {
return items.map(() => true);
}
const mergedOptions = { ...DEFAULT_FUZZY_OPTIONS, ...options };
const queryLower = query.toLowerCase();
const matches = new Array(items.length).fill(false);
const fuzzyCandidateTexts: string[] = [];
const fuzzyCandidateIndices: number[] = [];
for (let i = 0; i < items.length; i++) {
const target = getText(items[i]);
if (!target) {
continue;
}
if (mergedOptions.preferSubstring && target.toLowerCase().includes(queryLower)) {
matches[i] = true;
continue;
}
fuzzyCandidateTexts.push(target);
fuzzyCandidateIndices.push(i);
}
if (fuzzyCandidateTexts.length === 0) {
return matches;
}
const fuse = new Fuse(fuzzyCandidateTexts, {
threshold: mergedOptions.threshold,
distance: mergedOptions.distance,
ignoreLocation: mergedOptions.ignoreLocation,
});
for (const result of fuse.search(query)) {
matches[fuzzyCandidateIndices[result.refIndex]] = true;
}
return matches;
}
/**
* Score-sorted fuzzy ranking. Strict (low threshold), prioritizes substring
* matches (especially prefix matches), and returns the top N.
@@ -141,23 +93,133 @@ export function scoreByFuzzyQuery<T>(
return scored.slice(0, limit);
}
export function partitionByFuzzyQuery<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: FuzzySearchOptions
): { matching: T[]; other: T[] } {
const matches = getFuzzyMatchMask(items, query, getText, options);
const matching: T[] = [];
const other: T[] = [];
const RANK_TOKEN_MISS = Number.POSITIVE_INFINITY;
for (let i = 0; i < items.length; i++) {
if (matches[i]) {
matching.push(items[i]);
continue;
const tokenizeRankQuery = (query: string): string[] =>
query.trim().toLowerCase().split(/\s+/).filter(Boolean);
const compactText = (value: string): string => value.replace(/[^a-z0-9]+/g, '');
type RankFields = { fields: string[]; compact: string[] };
const buildRankFields = (texts: ReadonlyArray<string | null | undefined>): RankFields => {
const fields: string[] = [];
const compact: string[] = [];
for (const text of texts) {
if (!text) continue;
const lower = text.toLowerCase();
fields.push(lower);
compact.push(compactText(lower));
}
return { fields, compact };
};
/**
* Score one query token against an item's fields. Lower is better:
* field prefix < word-boundary substring < mid-word substring <
* punctuation-insensitive ("compact") substring. Earlier fields win ties, so
* callers should order `getTexts` by importance (name before path/description).
*/
const scoreRankToken = (token: string, { fields, compact }: RankFields): number => {
let best = RANK_TOKEN_MISS;
for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
const field = fields[fieldIndex];
const fieldPenalty = fieldIndex * 0.01;
const idx = field.indexOf(token);
let score = RANK_TOKEN_MISS;
if (idx === 0) {
score = fieldPenalty;
} else if (idx > 0) {
const boundary = !/[a-z0-9]/.test(field[idx - 1]);
score = (boundary ? 0.1 : 0.2) + idx / 1000 + fieldPenalty;
} else {
const compactIdx = compact[fieldIndex].indexOf(compactText(token));
if (compactIdx >= 0 && token.length > 1) {
score = 0.4 + compactIdx / 1000 + fieldPenalty;
}
}
if (score < best) best = score;
}
return best;
};
export interface RankByQueryOptions {
limit?: number;
/** Typo-tolerant Fuse fallback for single-token queries (default true). */
fuzzy?: boolean;
}
/**
* The canonical dropdown matcher: every whitespace-separated query token must
* match somewhere in the item's fields (any order, punctuation-insensitive),
* and results come back ordered by relevance exact/prefix matches first,
* then word-boundary and substring matches, original order breaking ties.
* Single-token queries additionally fall back to typo-tolerant fuzzy matching.
*
* Use this for every searchable dropdown (projects, agents, branches, models)
* instead of ad hoc `toLowerCase().includes` filters, so matching quality and
* ordering stay consistent across pickers.
*/
export function rankByQuery<T>(
items: readonly T[],
query: string,
getTexts: (item: T) => ReadonlyArray<string | null | undefined>,
options?: RankByQueryOptions,
): T[] {
const tokens = tokenizeRankQuery(query);
const limit = options?.limit ?? items.length;
if (tokens.length === 0) return items.slice(0, limit);
const scored: { item: T; score: number; order: number }[] = [];
const missed: { item: T; joined: string; order: number }[] = [];
for (let order = 0; order < items.length; order++) {
const item = items[order];
const rankFields = buildRankFields(getTexts(item));
let total = 0;
for (const token of tokens) {
const tokenScore = scoreRankToken(token, rankFields);
if (tokenScore === RANK_TOKEN_MISS) {
total = RANK_TOKEN_MISS;
break;
}
total += tokenScore;
}
if (total === RANK_TOKEN_MISS) {
missed.push({ item, joined: rankFields.fields.join(' '), order });
} else {
scored.push({ item, score: total, order });
}
other.push(items[i]);
}
return { matching, other };
const fuzzyEnabled = options?.fuzzy ?? true;
if (fuzzyEnabled && tokens.length === 1 && tokens[0].length >= 3 && missed.length > 0) {
const fuse = new Fuse(
missed.map((entry) => entry.joined),
{ threshold: 0.35, ignoreLocation: true, distance: 100, includeScore: true, minMatchCharLength: 2 },
);
for (const result of fuse.search(tokens[0])) {
const entry = missed[result.refIndex];
scored.push({ item: entry.item, score: 1 + (result.score ?? 1), order: entry.order });
}
}
scored.sort((a, b) => (a.score - b.score) || (a.order - b.order));
return scored.slice(0, limit).map((entry) => entry.item);
}
/**
* Boolean companion to `rankByQuery` for lists that keep their own grouping or
* order: every token must match one of the fields, punctuation-insensitive,
* without the fuzzy fallback.
*/
export function matchesRankQuery(
texts: ReadonlyArray<string | null | undefined>,
query: string,
): boolean {
const tokens = tokenizeRankQuery(query);
if (tokens.length === 0) return true;
const rankFields = buildRankFields(texts);
return tokens.every((token) => scoreRankToken(token, rankFields) !== RANK_TOKEN_MISS);
}
+33
View File
@@ -0,0 +1,33 @@
import { useSessionTabsStore } from '@/stores/useSessionTabsStore';
import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
/**
* Close one header session tab. Closing the active tab activates its right
* neighbour (falling back left), or opens a new-session draft when it was the
* last tab. Only tabs whose session is present in the loaded session list
* count as neighbours the same rule the strip uses for rendering. The
* session itself is never touched.
*/
export const closeSessionTabAndActivateNeighbour = (sessionId: string): void => {
const { tabIds, closeTab } = useSessionTabsStore.getState();
if (!tabIds.includes(sessionId)) return;
const { currentSessionId, setCurrentSession, openNewSessionDraft } = useSessionUIStore.getState();
if (sessionId === currentSessionId) {
const sessionsById = new Map(
useGlobalSessionsStore.getState().activeSessions.map((session) => [session.id, session] as const),
);
const renderable = tabIds.filter((id) => sessionsById.has(id));
const index = renderable.indexOf(sessionId);
const neighbourId = renderable[index + 1] ?? renderable[index - 1] ?? null;
const neighbour = neighbourId ? sessionsById.get(neighbourId) : null;
if (neighbour) {
setCurrentSession(neighbour.id, resolveGlobalSessionDirectory(neighbour));
} else {
openNewSessionDraft();
}
}
closeTab(sessionId);
};
+21 -7
View File
@@ -148,19 +148,20 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
descriptionKey: 'settings.openchamber.visual.field.autoSaveEnabledInfo',
keywords: ['editor', 'autosave', 'auto-save', 'files', 'save'],
},
{
id: 'appearance.expanded-editor-toolbar',
page: 'general',
titleKey: 'settings.openchamber.visual.field.expandedEditorToolbar',
keywords: ['editor', 'toolbar', 'tabs', 'docked', 'files'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'appearance.file-editor-keymap',
page: 'general',
titleKey: 'settings.openchamber.visual.field.fileEditorKeymap',
keywords: ['editor', 'vim', 'keymap'],
},
{
id: 'appearance.session-tabs',
page: 'general',
titleKey: 'settings.openchamber.visual.field.sessionTabsGroup',
descriptionKey: 'settings.openchamber.visual.field.sessionTabsInfo',
keywords: ['session', 'tabs', 'header', 'working set'],
isAvailable: (ctx) => !ctx.isMobile && !ctx.isVSCode,
},
{
id: 'appearance.terminal-quick-keys',
page: 'general',
@@ -239,6 +240,19 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
titleKey: 'settings.openchamber.visual.section.reasoning',
keywords: ['thinking', 'traces'],
},
{
id: 'chat.streaming',
page: 'chat',
titleKey: 'settings.openchamber.visual.section.streaming',
keywords: ['stream', 'scroll'],
},
{
id: 'chat.streaming-auto-follow',
page: 'chat',
titleKey: 'settings.openchamber.visual.field.streamingAutoFollow',
descriptionKey: 'settings.openchamber.visual.field.streamingAutoFollowInfo',
keywords: ['autoscroll', 'auto-scroll', 'follow', 'stick to bottom', 'streaming'],
},
{
id: 'chat.sticky-user-header',
page: 'chat',
+7
View File
@@ -239,6 +239,13 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
description: 'Create a new worktree and open a draft in it',
customizable: true,
},
{
id: 'close_session_tab',
defaultCombo: 'alt+w',
label: 'Close session tab',
description: 'Close the active session tab in the header (the session itself stays)',
customizable: true,
},
{
id: 'new_mini_chat',
defaultCombo: 'mod+alt+n',
+27 -1
View File
@@ -1,4 +1,4 @@
import type { CreateTerminalOptions, TerminalError, TerminalHandlers, TerminalSession, TerminalShellOption, TerminalStreamEvent } from './api/types';
import type { CreateTerminalOptions, TerminalError, TerminalHandlers, TerminalServerSession, TerminalSession, TerminalShellOption, TerminalStreamEvent } from './api/types';
import { openRuntimeWebSocket } from './relay/runtime-socket';
import type { RelayTunnelWebSocket } from './relay/tunnel-client';
import { runtimeFetch } from './runtime-fetch';
@@ -356,6 +356,32 @@ export async function createTerminalSession(options: CreateTerminalOptions): Pro
if (!response.ok) throw await responseError(response, 'Failed to create terminal session');
return response.json() as Promise<TerminalSession>;
}
export async function listTerminalSessions(cwd: string): Promise<TerminalServerSession[]> {
const response = await runtimeFetch(`/api/terminal/sessions?cwd=${encodeURIComponent(cwd)}`);
if (!response.ok) throw await responseError(response, 'Failed to list terminal sessions');
const payload: unknown = await response.json().catch(() => null);
const rawSessions = payload && typeof payload === 'object' && 'sessions' in payload ? payload.sessions : null;
if (!Array.isArray(rawSessions)) throw new Error('Failed to list terminal sessions');
const parsed: TerminalServerSession[] = [];
for (const entry of rawSessions as unknown[]) {
if (typeof entry !== 'object' || entry === null) continue;
// SAFETY: every field is verified below before the value is used.
const candidate = entry as Partial<Record<keyof TerminalServerSession, unknown>>;
if (typeof candidate.sessionId !== 'string' || typeof candidate.cwd !== 'string') continue;
if (candidate.status !== 'running' && candidate.status !== 'exited') continue;
parsed.push({
sessionId: candidate.sessionId,
cwd: candidate.cwd,
status: candidate.status,
createdAt: typeof candidate.createdAt === 'number' ? candidate.createdAt : null,
});
}
return parsed;
}
export async function touchTerminalSessions(sessionIds: string[]): Promise<void> {
if (sessionIds.length === 0) return;
await command('/api/terminal/touch', 'POST', { sessionIds });
}
export async function listTerminalShells(): Promise<TerminalShellOption[]> {
const response = await runtimeFetch('/api/terminal/shells');
if (!response.ok) throw await responseError(response, 'Failed to list terminal shells');
@@ -37,7 +37,7 @@
"border": "#242323",
"borderHover": "#504e4c",
"borderFocus": "#da7c47",
"selection": "#b9a5992b",
"selection": "#c8c6c52b",
"selectionForeground": "#c9c5ba",
"focus": "#da7c47",
"focusRing": "#da7c4755",
@@ -37,7 +37,7 @@
"border": "#e5e1de",
"borderHover": "#cbc7c2",
"borderFocus": "#b35017",
"selection": "#b350172b",
"selection": "#a9998f2b",
"selectionForeground": "#393a34",
"focus": "#b35017",
"focusRing": "#b3501755",
+9 -16
View File
@@ -66,29 +66,22 @@ export const truncatePathMiddle = (
return source;
}
const prefixBudget = Math.max(0, maxLength - (fileName.length + 2));
if (prefixBudget <= 0) {
return `…/${fileName}`;
}
let prefix = '';
for (const segment of segments) {
// Keep the segments closest to the file name: in trees full of index.md the
// parent directory is the distinguishing part, so drop leading segments.
let suffix = fileName;
for (let i = segments.length - 1; i >= 0; i--) {
const segment = segments[i];
if (!segment) {
continue;
}
const candidate = prefix ? `${prefix}/${segment}` : segment;
if (candidate.length > prefixBudget) {
const candidate = `${segment}/${suffix}`;
if (candidate.length + 2 > maxLength) {
break;
}
prefix = candidate;
suffix = candidate;
}
if (!prefix) {
const first = segments[0] ?? '';
prefix = first ? first.slice(0, prefixBudget) : '';
}
return prefix ? `${prefix}…/${fileName}` : `…/${fileName}`;
return `…/${suffix}`;
};
const normalizePath = (value: string) => {
@@ -0,0 +1,33 @@
import { describe, expect, test } from 'bun:test';
import { rankBranchesForQuery } from './branchSearch';
describe('rankBranchesForQuery', () => {
test('empty query keeps everything in the other groups', () => {
const result = rankBranchesForQuery({ localBranches: ['main'], remoteBranches: ['origin/dev'], query: ' ' });
expect(result.matching).toEqual([]);
expect(result.otherLocal).toEqual(['main']);
expect(result.otherRemote).toEqual(['origin/dev']);
});
test('orders matches by relevance, not alphabetically', () => {
const result = rankBranchesForQuery({
localBranches: ['aaa-fix-scroll', 'fix/scroll', 'main'],
remoteBranches: ['origin/fix/scroll-old'],
query: 'fix',
});
expect(result.matching[0]).toEqual({ label: 'fix/scroll', value: 'fix/scroll', source: 'local' });
expect(result.matching.map((entry) => entry.label)).toEqual([
'fix/scroll',
'aaa-fix-scroll',
'origin/fix/scroll-old',
]);
expect(result.otherLocal).toEqual(['main']);
expect(result.otherRemote).toEqual([]);
});
test('remote matches carry the remotes/ checkout value', () => {
const result = rankBranchesForQuery({ localBranches: [], remoteBranches: ['origin/feat/x'], query: 'feat' });
expect(result.matching[0].value).toBe('remotes/origin/feat/x');
});
});
+12 -35
View File
@@ -1,4 +1,4 @@
import { partitionByFuzzyQuery } from "@/lib/search/fuzzySearch";
import { rankByQuery } from "@/lib/search/fuzzySearch";
export interface RankedBranchGroups {
matching: Array<{
@@ -26,42 +26,19 @@ export function rankBranchesForQuery(args: {
};
}
const localPartition = partitionByFuzzyQuery(localBranches, normalizedQuery, (branch) => branch);
const remotePartition = partitionByFuzzyQuery(remoteBranches, normalizedQuery, (branch) => branch);
const matching: RankedBranchGroups['matching'] = [];
const otherLocal = localPartition.other;
const otherRemote = remotePartition.other;
for (const branch of localPartition.matching) {
matching.push({
label: branch,
value: branch,
source: 'local',
});
}
for (const branch of remotePartition.matching) {
matching.push({
label: branch,
value: `remotes/${branch}`,
source: 'remote',
});
}
matching.sort((a, b) => {
const byLabel = a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
if (byLabel !== 0) {
return byLabel;
}
if (a.source !== b.source) {
return a.source.localeCompare(b.source);
}
return a.value.localeCompare(b.value);
});
// Rank local and remote branches together so the order reflects match
// quality (an exact or prefix match lands first), not the source group or
// the alphabet.
const candidates: RankedBranchGroups['matching'] = [
...localBranches.map((branch) => ({ label: branch, value: branch, source: 'local' as const })),
...remoteBranches.map((branch) => ({ label: branch, value: `remotes/${branch}`, source: 'remote' as const })),
];
const matching = rankByQuery(candidates, normalizedQuery, (branch) => [branch.label]);
const matched = new Set(matching);
return {
matching,
otherLocal,
otherRemote,
otherLocal: candidates.filter((entry) => entry.source === 'local' && !matched.has(entry)).map((entry) => entry.label),
otherRemote: candidates.filter((entry) => entry.source === 'remote' && !matched.has(entry)).map((entry) => entry.label),
};
}