Merge upstream main into feat/subagent-cost-rollup
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 d’une réponse',
|
||||
'settings.openchamber.visual.field.streamingAutoFollowInfo': 'Pendant qu’une 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 d’outils de l’éditeur',
|
||||
'settings.openchamber.visual.field.expandedEditorToolbar': 'Toujours afficher la barre d’outils 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 l’arrê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 l’icône du Dock de macOS le nombre de discussions avec une activité non vue.',
|
||||
'settings.openchamber.visual.actions.saveAndRestart': 'Enregistrer et redémarrer',
|
||||
|
||||
@@ -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': '入力を止めた後にファイルの編集内容を自動保存します。無効にすると手動保存が必要になります。',
|
||||
|
||||
@@ -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': '업데이트 확인 실패',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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": "Автоматично зберігати зміни у файлі після того, як ви припините друкувати. Вимкніть, щоб зберігати лише вручну.",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user