Add Windows Electron desktop support (#1093)
* fix: make upstream sync actions target the selected remote Ensure fetch and pull actually honor upstream selection so fork maintenance works from the Git sidebar, and surface upstream branch status alongside the primary origin-tracking indicators. * feat: add Windows Electron desktop foundation * fix(electron): stabilize Windows desktop packaging * fix(electron): stabilize Windows desktop chrome Use native Windows titlebar behavior with an Alt-accessible hidden menu, and harden Windows dev command launching so the desktop app follows platform conventions. * fix(electron): stabilize Windows dev startup * fix(electron): clarify desktop artifact names * fix(electron): harden Windows desktop release and launch * fix(electron): address Windows release review * fix(electron): point updater and release links to org repo * Fix Windows settings persistence fallback * Fix Windows Electron dev startup * Add Windows Electron window controls * Fix Windows Electron install and opencode launch * fix: resolve git status for repositories without upstream Fixes repository detection stuck on Checking repository Handles git status when no upstream is configured Adds regression coverage for git status loading * Add Windows app menu button * fix: preserve file editor line endings * ci: add desktop release smoke workflow --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
cc7969ac00
commit
becd240168
@@ -55,29 +55,39 @@ const submitPassword = async (password: string, trustDevice: boolean): Promise<R
|
||||
return response;
|
||||
};
|
||||
|
||||
const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<div
|
||||
className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background text-foreground"
|
||||
style={{ fontFamily: '"Inter", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif' }}
|
||||
>
|
||||
const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const titlebarDragStyle = React.useMemo<React.CSSProperties>(() => {
|
||||
return {
|
||||
height: 'var(--oc-wco-titlebar-height, 0px)',
|
||||
right: 'var(--oc-wco-right-inset, 0px)',
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-55"
|
||||
style={{
|
||||
background: 'radial-gradient(120% 140% at 50% -20%, var(--surface-overlay) 0%, transparent 68%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-subtle)',
|
||||
opacity: 0.22,
|
||||
}}
|
||||
/>
|
||||
<div className="relative z-10 flex w-full justify-center px-4 py-12 sm:px-6">
|
||||
{children}
|
||||
className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background text-foreground"
|
||||
style={{ fontFamily: '"Inter", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif' }}
|
||||
>
|
||||
<div className="app-region-drag fixed left-0 top-0 z-20" style={titlebarDragStyle} aria-hidden />
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-55"
|
||||
style={{
|
||||
background: 'radial-gradient(120% 140% at 50% -20%, var(--surface-overlay) 0%, transparent 68%)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
backgroundColor: 'var(--surface-subtle)',
|
||||
opacity: 0.22,
|
||||
}}
|
||||
/>
|
||||
<div className="app-region-no-drag relative z-10 flex w-full justify-center px-4 py-12 sm:px-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingScreen: React.FC = () => (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background text-foreground">
|
||||
|
||||
@@ -316,7 +316,7 @@ FileChip.displayName = 'FileChip';
|
||||
const VSCodeFileChip = memo(({ file, onRemove }: FileChipProps) => {
|
||||
const { t } = useI18n();
|
||||
const { displayName, extension } = useFileDetails(file);
|
||||
|
||||
|
||||
// Detect selection-style attachments: ends with ":N" or ":N-M"
|
||||
const isSelectionAttachment = /:\d+(?:-\d+)?$/.test(displayName);
|
||||
|
||||
@@ -359,7 +359,7 @@ interface AttachedFilesListProps {
|
||||
onShowPopup?: (content: ToolPopupContent) => void;
|
||||
}
|
||||
|
||||
export const AttachedVSCodeFileChips = memo(({ onShowPopup }: AttachedFilesListProps) => {
|
||||
export const AttachedVSCodeFileChips = memo(({ onShowPopup }: AttachedFilesListProps) => {
|
||||
const attachedFiles = useInputStore((state) => state.attachedFiles);
|
||||
const removeAttachedFile = useInputStore((state) => state.removeAttachedFile);
|
||||
|
||||
|
||||
@@ -491,7 +491,6 @@ function SessionItem({
|
||||
title={`Sub-session: ${getSessionTitle(child)}`}
|
||||
>
|
||||
<Icon name="loader-4" className="h-2.5 w-2.5 animate-spin"
|
||||
|
||||
style={{ color: `var(${childColor.var})` }}/>
|
||||
</div>
|
||||
);
|
||||
@@ -692,7 +691,6 @@ function SessionStatusHeader({
|
||||
title={`Sub-session: ${child.session.title || 'Untitled'}`}
|
||||
>
|
||||
<Icon name="loader-4" className="h-2.5 w-2.5 animate-spin"
|
||||
|
||||
style={{ color: `var(${childColor.var})` }}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,6 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { useOpenCodeReadiness } from '@/hooks/useOpenCodeReadiness';
|
||||
import { eventMatchesShortcut, getEffectiveShortcutCombo, normalizeCombo } from '@/lib/shortcuts';
|
||||
|
||||
|
||||
type IconComponent = IconName;
|
||||
|
||||
type ProviderModel = Record<string, unknown> & { id?: string; name?: string };
|
||||
|
||||
@@ -39,7 +39,6 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
|
||||
/>
|
||||
) : (
|
||||
<Icon name="brain-ai-3" className="h-4 w-4"
|
||||
|
||||
style={{ color: `var(${getAgentColor(agentName).var})` }}/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -729,7 +729,6 @@ const ToolScrollableSection: React.FC<ToolScrollableSectionProps> = ({
|
||||
disableHorizontal ? 'overflow-y-auto overflow-x-hidden' : 'overflow-auto',
|
||||
className,
|
||||
)}
|
||||
|
||||
>
|
||||
<div className="w-full min-w-0">
|
||||
{children}
|
||||
|
||||
@@ -153,6 +153,7 @@ export const iconSpriteData = {
|
||||
"lock-unlock": `<path d="M7 10H20C20.5523 10 21 10.4477 21 11V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V11C3 10.4477 3.44772 10 4 10H5V9C5 5.13401 8.13401 2 12 2C14.7405 2 17.1131 3.5748 18.2624 5.86882L16.4731 6.76344C15.6522 5.12486 13.9575 4 12 4C9.23858 4 7 6.23858 7 9V10ZM5 12V20H19V12H5ZM10 15H14V17H10V15Z" fill="currentColor"/>`,
|
||||
"loop-right-ai": `<path d="M22 12C22 17.5228 17.5228 22 12 22C8.72774 22 5.82382 20.4286 4 18.001V20.5H2V14.5H8V16.5H5.38477C6.82543 18.6137 9.25151 20 12 20C16.4183 20 20 16.4183 20 12H22ZM11.5293 8.31934C11.7059 7.8935 12.2943 7.89349 12.4707 8.31934L12.7236 8.93066C13.1556 9.97346 13.9615 10.8062 14.9746 11.2568L15.6924 11.5762C16.1026 11.759 16.1026 12.3562 15.6924 12.5391L14.9326 12.877C13.9449 13.3162 13.1534 14.1194 12.7139 15.1279L12.4668 15.6934C12.2864 16.1075 11.7137 16.1075 11.5332 15.6934L11.2871 15.1279C10.8476 14.1193 10.0552 13.3163 9.06738 12.877L8.30762 12.5391C7.89744 12.3562 7.89741 11.759 8.30762 11.5762L9.02539 11.2568C10.0385 10.8062 10.8445 9.97348 11.2764 8.93066L11.5293 8.31934ZM12 2C15.2723 2 18.1762 3.57144 20 5.99902V3.5H22V9.5H16V7.5H18.6152C17.1746 5.38634 14.7485 4 12 4C7.58172 4 4 7.58172 4 12H2C2 6.47715 6.47715 2 12 2Z" fill="currentColor"/>`,
|
||||
"macbook": `<path d="M4 5V16H20V5H4ZM2 4.00748C2 3.45107 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44892 22 4.00748V18H2V4.00748ZM1 19H23V21H1V19Z" fill="currentColor"/>`,
|
||||
"menu-2": `<path d="M3 4H21V6H3V4ZM3 11H15V13H3V11ZM3 18H21V20H3V18Z" fill="currentColor"/>`,
|
||||
"menu-fold-2": `<path d="M4.40347 3.90332L2.98926 5.31753L6.17124 8.49951L2.98926 11.6815L4.40347 13.0957L8.99967 8.49951L4.40347 3.90332ZM20.9997 19.9995V17.9995H2.99967V19.9995H20.9997ZM20.9997 12.9995V10.9995H11.9997V12.9995H20.9997ZM20.9997 5.99951V3.99951H11.9997V5.99951H20.9997Z" fill="currentColor"/>`,
|
||||
"menu-search": `<path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z" fill="currentColor"/>`,
|
||||
"message-2": `<path d="M6.45455 19L2 22.5V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.45455ZM5.76282 17H20V5H4V18.3851L5.76282 17ZM11 10H13V12H11V10ZM7 10H9V12H7V10ZM15 10H17V12H15V10Z" fill="currentColor"/>`,
|
||||
|
||||
@@ -77,7 +77,7 @@ type HeaderIconActionButtonProps = {
|
||||
visible?: boolean;
|
||||
title: string;
|
||||
ariaLabel: string;
|
||||
onClick: () => void;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
className?: string;
|
||||
Icon: IconName;
|
||||
iconClassName?: string;
|
||||
@@ -115,6 +115,83 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
||||
);
|
||||
});
|
||||
|
||||
type WindowsWindowControlsProps = {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
const WindowsWindowControls = React.memo(function WindowsWindowControls({ visible }: WindowsWindowControlsProps) {
|
||||
const { t } = useI18n();
|
||||
const [isMaximized, setIsMaximized] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state')
|
||||
.then((state) => {
|
||||
if (!disposed) {
|
||||
setIsMaximized(Boolean(state?.maximized));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const handleMaximizedChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ maximized?: boolean }>).detail;
|
||||
setIsMaximized(Boolean(detail?.maximized));
|
||||
};
|
||||
|
||||
window.addEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
|
||||
return () => {
|
||||
disposed = true;
|
||||
window.removeEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonClassName = 'app-region-no-drag inline-flex h-12 w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary';
|
||||
|
||||
return (
|
||||
<div className="app-region-no-drag -mr-3 ml-2 flex h-12 shrink-0 items-center" aria-label={t('header.windowControls.groupAria')}>
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClassName}
|
||||
onClick={() => { void invokeDesktop('desktop_minimize_current_window'); }}
|
||||
title={t('header.windowControls.minimize')}
|
||||
aria-label={t('header.windowControls.minimize')}
|
||||
>
|
||||
<Icon name="subtract" className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={buttonClassName}
|
||||
onClick={() => {
|
||||
void invokeDesktop<{ maximized?: boolean }>('desktop_toggle_current_window_maximized')
|
||||
.then((state) => setIsMaximized(Boolean(state?.maximized)))
|
||||
.catch(() => {});
|
||||
}}
|
||||
title={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
|
||||
aria-label={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
|
||||
>
|
||||
<Icon name={isMaximized ? 'fullscreen-exit' : 'checkbox-blank'} className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonClassName, 'hover:bg-status-error hover:text-status-error-foreground')}
|
||||
onClick={() => { void invokeDesktop('desktop_close_current_window'); }}
|
||||
title={t('header.windowControls.close')}
|
||||
aria-label={t('header.windowControls.close')}
|
||||
>
|
||||
<Icon name="close" className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
type DesktopGitHubControlProps = {
|
||||
isMobile: boolean;
|
||||
githubAuthStatus: GitHubAuthStatus | null;
|
||||
@@ -731,6 +808,13 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||
}, []);
|
||||
|
||||
const isWindowsElectronDesktop = React.useMemo(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return Boolean(window.__OPENCHAMBER_ELECTRON__) && window.__OPENCHAMBER_PLATFORM__ === 'win32';
|
||||
}, []);
|
||||
|
||||
const macosMajorVersion = React.useMemo(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
@@ -1262,6 +1346,16 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
toggleSidebar();
|
||||
}, [blurActiveElement, isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]);
|
||||
|
||||
const handleOpenWindowsAppMenu = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
void invokeDesktop('desktop_show_app_menu', {
|
||||
x: rect.left,
|
||||
y: rect.bottom,
|
||||
}).catch((error) => {
|
||||
console.warn('[header] failed to open app menu', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOpenDraftMiniChat = React.useCallback(() => {
|
||||
void invokeDesktop('desktop_open_draft_mini_chat_window', {
|
||||
directory: normalize(openDirectory || activeProject?.path || ''),
|
||||
@@ -1454,7 +1548,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
}, [isDesktopApp, isMacPlatform, macosMajorVersion]);
|
||||
|
||||
const webWindowControlsOverlayStyle = React.useMemo<React.CSSProperties | undefined>(() => {
|
||||
if (isDesktopApp || isVSCode) {
|
||||
if ((isDesktopApp && !isWindowsElectronDesktop) || isVSCode) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1466,7 +1560,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
|
||||
};
|
||||
}, [isDesktopApp, isTabletStandalonePwa, isVSCode]);
|
||||
}, [isDesktopApp, isTabletStandalonePwa, isVSCode, isWindowsElectronDesktop]);
|
||||
|
||||
const updateHeaderHeight = React.useCallback(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
@@ -1884,6 +1978,15 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
role="tablist"
|
||||
aria-label={t('header.navigation.mainAria')}
|
||||
>
|
||||
{isWindowsElectronDesktop ? (
|
||||
<HeaderIconActionButton
|
||||
title={t('header.actions.openAppMenu')}
|
||||
ariaLabel={t('header.actions.openAppMenuAria')}
|
||||
onClick={handleOpenWindowsAppMenu}
|
||||
className={`${desktopHeaderIconButtonClass} shrink-0`}
|
||||
Icon={'menu-2'}
|
||||
/>
|
||||
) : null}
|
||||
<HeaderIconActionButton
|
||||
title={t('header.actions.openSessionsWithShortcut', { shortcut: shortcutLabel('toggle_sidebar') })}
|
||||
ariaLabel={t('header.actions.openSessionsAria')}
|
||||
@@ -1974,6 +2077,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
Icon={'picture-in-picture-2'}
|
||||
/>
|
||||
{desktopSidebarActions}
|
||||
<WindowsWindowControls visible={isWindowsElectronDesktop} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -319,7 +319,7 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
|
||||
export const SidebarFilesTree: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const { files, runtime } = useRuntimeAPIs();
|
||||
const { files } = useRuntimeAPIs();
|
||||
const currentDirectory = useEffectiveDirectory() ?? '';
|
||||
const root = normalizePath(currentDirectory.trim());
|
||||
const showHidden = useDirectoryShowHidden();
|
||||
@@ -335,6 +335,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
const [searching, setSearching] = React.useState(false);
|
||||
|
||||
const [childrenByDir, setChildrenByDir] = React.useState<Record<string, FileNode[]>>({});
|
||||
const [loadErrorsByDir, setLoadErrorsByDir] = React.useState<Record<string, string>>({});
|
||||
const loadedDirsRef = React.useRef<Set<string>>(new Set());
|
||||
const inFlightDirsRef = React.useRef<Set<string>>(new Set());
|
||||
|
||||
@@ -419,7 +420,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
inFlightDirsRef.current.add(normalizedDir);
|
||||
|
||||
const respectGitignore = !showGitignored;
|
||||
const listPromise = runtime.isDesktop
|
||||
const listPromise = files.listDirectory
|
||||
? files.listDirectory(normalizedDir, { respectGitignore }).then((result) => result.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
@@ -437,25 +438,34 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
|
||||
loadedDirsRef.current = new Set(loadedDirsRef.current);
|
||||
loadedDirsRef.current.add(normalizedDir);
|
||||
setLoadErrorsByDir((prev) => {
|
||||
if (!prev[normalizedDir]) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[normalizedDir];
|
||||
return next;
|
||||
});
|
||||
setChildrenByDir((prev) => ({ ...prev, [normalizedDir]: mapped }));
|
||||
})
|
||||
.catch(() => {
|
||||
setChildrenByDir((prev) => ({
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error ?? '');
|
||||
console.error('Failed to load sidebar directory:', error);
|
||||
setLoadErrorsByDir((prev) => ({
|
||||
...prev,
|
||||
[normalizedDir]: prev[normalizedDir] ?? [],
|
||||
[normalizedDir]: message,
|
||||
}));
|
||||
})
|
||||
.finally(() => {
|
||||
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||
inFlightDirsRef.current.delete(normalizedDir);
|
||||
});
|
||||
}, [files, mapDirectoryEntries, runtime.isDesktop, showGitignored]);
|
||||
}, [files, mapDirectoryEntries, showGitignored]);
|
||||
|
||||
const refreshRoot = React.useCallback(async () => {
|
||||
if (!root) return;
|
||||
|
||||
loadedDirsRef.current = new Set();
|
||||
inFlightDirsRef.current = new Set();
|
||||
setLoadErrorsByDir({});
|
||||
setChildrenByDir((prev) => (Object.keys(prev).length === 0 ? prev : {}));
|
||||
|
||||
await loadDirectory(root);
|
||||
@@ -484,6 +494,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
|
||||
loadedDirsRef.current = new Set();
|
||||
inFlightDirsRef.current = new Set();
|
||||
setLoadErrorsByDir({});
|
||||
setChildrenByDir((prev) => (Object.keys(prev).length === 0 ? prev : {}));
|
||||
void loadDirectory(root);
|
||||
}, [loadDirectory, root, showHidden, showGitignored]);
|
||||
@@ -807,6 +818,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
}
|
||||
|
||||
const hasTree = Boolean(root && childrenByDir[root]);
|
||||
const rootLoadError = root ? loadErrorsByDir[root] : null;
|
||||
|
||||
return (
|
||||
<section className="flex h-full min-h-0 flex-col overflow-hidden bg-sidebar">
|
||||
@@ -923,6 +935,14 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : rootLoadError ? (
|
||||
<li className="flex flex-col gap-2 px-2 py-1 typography-meta text-muted-foreground">
|
||||
<span>{rootLoadError}</span>
|
||||
<Button variant="outline" size="xs" className="w-fit gap-1.5" onClick={() => void refreshRoot()}>
|
||||
<Icon name="refresh" className="h-3.5 w-3.5" />
|
||||
{t('sidebarFilesTree.actions.refreshTitle')}
|
||||
</Button>
|
||||
</li>
|
||||
) : hasTree && root ? (
|
||||
renderTree(root, 0)
|
||||
) : (
|
||||
|
||||
@@ -36,7 +36,7 @@ const guessLabelFromSource = (value: string) => {
|
||||
: trimmed.startsWith("git@")
|
||||
? "ssh"
|
||||
: "shorthand";
|
||||
|
||||
|
||||
if (urlFormat === 'ssh') {
|
||||
return `${trimmed.split(":")[1].replace(/\.git$/i, '')}`;
|
||||
}
|
||||
|
||||
@@ -777,7 +777,6 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||
<Icon name="git-branch" className="h-3.5 w-3.5 text-muted-foreground"
|
||||
|
||||
style={branchIconColor ? { color: branchIconColor } : undefined}/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -810,7 +809,6 @@ export function SessionGroupSection(props: Props): React.ReactNode {
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Icon name="git-branch" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground"
|
||||
|
||||
style={branchIconColor ? { color: branchIconColor } : undefined}/>
|
||||
)
|
||||
) : null}
|
||||
|
||||
@@ -290,6 +290,32 @@ const isFileMissingError = (error: unknown): boolean => {
|
||||
|
||||
const MAX_VIEW_CHARS = 200_000;
|
||||
const FILE_EDITOR_AUTO_SAVE_KEY = 'openchamber:files:auto-save-enabled';
|
||||
type FileLineEnding = '\n' | '\r\n';
|
||||
|
||||
const detectFileLineEnding = (content: string): FileLineEnding => {
|
||||
let crlf = 0;
|
||||
let lf = 0;
|
||||
|
||||
for (let index = 0; index < content.length; index += 1) {
|
||||
if (content.charCodeAt(index) !== 10) {
|
||||
continue;
|
||||
}
|
||||
if (index > 0 && content.charCodeAt(index - 1) === 13) {
|
||||
crlf += 1;
|
||||
} else {
|
||||
lf += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return crlf > lf ? '\r\n' : '\n';
|
||||
};
|
||||
|
||||
const normalizeEditorLineEndings = (content: string): string => content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
const serializeEditorContent = (content: string, lineEnding: FileLineEnding): string => {
|
||||
const normalized = normalizeEditorLineEndings(content);
|
||||
return lineEnding === '\r\n' ? normalized.replace(/\n/g, '\r\n') : normalized;
|
||||
};
|
||||
|
||||
const getInitialAutoSaveEnabled = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -763,6 +789,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
|
||||
const [draftContent, setDraftContent] = React.useState('');
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
const [loadedFileLineEnding, setLoadedFileLineEnding] = React.useState<FileLineEnding>('\n');
|
||||
const dialogInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const autoSaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const lastLoadedFileStatRef = React.useRef<FileStatSnapshot | null>(null);
|
||||
@@ -1007,7 +1034,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
const isCurrentRequest = () => activeDirectoryLoadIdsRef.current.get(normalizedDir) === requestId;
|
||||
|
||||
const respectGitignore = !showGitignored;
|
||||
const listPromise = runtime.isDesktop
|
||||
const listPromise = files.listDirectory
|
||||
? files.listDirectory(normalizedDir, { respectGitignore }).then((result) => result.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
@@ -1051,7 +1078,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||
inFlightDirsRef.current.delete(normalizedDir);
|
||||
});
|
||||
}, [files, mapDirectoryEntries, runtime.isDesktop, showGitignored]);
|
||||
}, [files, mapDirectoryEntries, showGitignored]);
|
||||
|
||||
const refreshRoot = React.useCallback(async () => {
|
||||
if (!root) {
|
||||
@@ -1446,7 +1473,8 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
setIsSaving(true);
|
||||
|
||||
try {
|
||||
const result = await files.writeFile(selectedFile.path, draftContent);
|
||||
const contentToWrite = serializeEditorContent(draftContent, loadedFileLineEnding);
|
||||
const result = await files.writeFile(selectedFile.path, contentToWrite);
|
||||
if (!result?.success) {
|
||||
toast.error(t('filesView.toast.writeFileFailed'));
|
||||
return false;
|
||||
@@ -1467,7 +1495,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [draftContent, files, isDirty, readFileStat, selectedFile, t]);
|
||||
}, [draftContent, files, isDirty, loadedFileLineEnding, readFileStat, selectedFile, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isDirty) {
|
||||
@@ -1622,10 +1650,12 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
if (!isCurrentLoad()) {
|
||||
return;
|
||||
}
|
||||
setFileContent(content);
|
||||
setDraftContent(content.length > MAX_VIEW_CHARS
|
||||
? `${content.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
||||
: content);
|
||||
const editorContent = normalizeEditorLineEndings(content);
|
||||
setLoadedFileLineEnding(detectFileLineEnding(content));
|
||||
setFileContent(editorContent);
|
||||
setDraftContent(editorContent.length > MAX_VIEW_CHARS
|
||||
? `${editorContent.slice(0, MAX_VIEW_CHARS)}\n\n… truncated …`
|
||||
: editorContent);
|
||||
setLoadedFilePath(node.path);
|
||||
void readFileStat(node.path, readOptions)
|
||||
.then((stat) => {
|
||||
|
||||
@@ -1001,6 +1001,14 @@ export const GitView: React.FC = () => {
|
||||
};
|
||||
}, [changeEntries, currentDirectory, git, prefetchDiffs, stagedChangeEntries, visibleChangePaths]);
|
||||
|
||||
const getPushedRemoteName = (result?: Awaited<ReturnType<typeof git.gitPush>>) => {
|
||||
return result?.pushed[0]?.remote
|
||||
|| status?.tracking?.split('/')[0]
|
||||
|| effectiveRemotes.find((remote) => remote.name === 'origin')?.name
|
||||
|| effectiveRemotes[0]?.name
|
||||
|| 'origin';
|
||||
};
|
||||
|
||||
const handleSyncAction = async (action: Exclude<SyncAction, null>, remote?: GitRemote) => {
|
||||
if (!currentDirectory) return;
|
||||
setSyncAction(action);
|
||||
@@ -1035,8 +1043,8 @@ export const GitView: React.FC = () => {
|
||||
: t('gitView.toast.pulledFilesPlural', { count: result.files.length, name: remote.name })
|
||||
);
|
||||
} else if (action === 'push') {
|
||||
await git.gitPush(currentDirectory);
|
||||
toast.success(t('gitView.toast.pushedToUpstream'));
|
||||
const result = await git.gitPush(currentDirectory);
|
||||
toast.success(t('gitView.toast.pushedToUpstream', { name: getPushedRemoteName(result) }));
|
||||
} else if (action === 'sync') {
|
||||
if (!remote) {
|
||||
throw new Error('No remote available for sync');
|
||||
@@ -1073,7 +1081,7 @@ export const GitView: React.FC = () => {
|
||||
: t('gitView.toast.pulledFilesPlural', { count: pulledFileCount, name: remote.name })
|
||||
);
|
||||
} else if (pushedChanges) {
|
||||
toast.success(t('gitView.toast.pushedToUpstream'));
|
||||
toast.success(t('gitView.toast.pushedToUpstream', { name: remote.name }));
|
||||
} else {
|
||||
toast.success(t('gitView.toast.alreadyUpToDate'));
|
||||
}
|
||||
@@ -1150,56 +1158,8 @@ export const GitView: React.FC = () => {
|
||||
await refreshStatusAndBranches();
|
||||
|
||||
if (options.pushAfter) {
|
||||
setSyncAction('sync');
|
||||
const trackingRemoteName = status?.tracking?.split('/')[0];
|
||||
const syncRemote = effectiveRemotes.find((remote) => remote.name === trackingRemoteName) ?? effectiveRemotes[0];
|
||||
if (!syncRemote) {
|
||||
throw new Error('No remote available for sync');
|
||||
}
|
||||
|
||||
const trackingPrefix = `${syncRemote.name}/`;
|
||||
const trackedBranch = status?.tracking?.startsWith(trackingPrefix)
|
||||
? status.tracking.slice(trackingPrefix.length)
|
||||
: undefined;
|
||||
let pulledFileCount = 0;
|
||||
let pushedChanges = false;
|
||||
|
||||
await git.gitFetch(currentDirectory, { remote: syncRemote.name });
|
||||
const afterFetch = await git.getGitStatus(currentDirectory);
|
||||
|
||||
if ((afterFetch.behind ?? 0) > 0) {
|
||||
const pullResult = await git.gitPull(currentDirectory, {
|
||||
remote: syncRemote.name,
|
||||
branch: trackedBranch,
|
||||
rebase: true,
|
||||
});
|
||||
pulledFileCount = pullResult.files.length;
|
||||
}
|
||||
|
||||
const afterPull = await git.getGitStatus(currentDirectory);
|
||||
if ((afterPull.ahead ?? 0) > 0) {
|
||||
await git.gitPush(currentDirectory);
|
||||
pushedChanges = true;
|
||||
}
|
||||
|
||||
if (pulledFileCount > 0 && pushedChanges) {
|
||||
toast.success(
|
||||
pulledFileCount === 1
|
||||
? t('gitView.toast.syncedPulledSingleAndPushed', { count: pulledFileCount, name: syncRemote.name })
|
||||
: t('gitView.toast.syncedPulledPluralAndPushed', { count: pulledFileCount, name: syncRemote.name })
|
||||
);
|
||||
} else if (pulledFileCount > 0) {
|
||||
toast.success(
|
||||
pulledFileCount === 1
|
||||
? t('gitView.toast.pulledFilesSingle', { count: pulledFileCount, name: syncRemote.name })
|
||||
: t('gitView.toast.pulledFilesPlural', { count: pulledFileCount, name: syncRemote.name })
|
||||
);
|
||||
} else if (pushedChanges) {
|
||||
toast.success(t('gitView.toast.pushedToUpstream'));
|
||||
} else {
|
||||
toast.success(t('gitView.toast.alreadyUpToDate'));
|
||||
}
|
||||
|
||||
const result = await git.gitPush(currentDirectory);
|
||||
toast.success(t('gitView.toast.pushedToUpstream', { name: getPushedRemoteName(result) }));
|
||||
triggerFireworks();
|
||||
await refreshStatusAndBranches(false);
|
||||
} else {
|
||||
@@ -2257,7 +2217,7 @@ export const GitView: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading && isGitRepo === null) {
|
||||
if (isGitRepo === null || (isGitRepo === true && !status)) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
|
||||
@@ -13,7 +13,12 @@ import type { IconName } from "@/components/icon/icons";
|
||||
import { BranchSelector } from './BranchSelector';
|
||||
import { WorktreeBranchDisplay } from './WorktreeBranchDisplay';
|
||||
import { SyncActions } from './SyncActions';
|
||||
import type { GitStatus, GitIdentityProfile, GitRemote } from '@/lib/api/types';
|
||||
import type {
|
||||
GitStatus,
|
||||
GitIdentityProfile,
|
||||
GitRemote,
|
||||
GitRemoteComparison,
|
||||
} from '@/lib/api/types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type SyncAction = 'fetch' | 'pull' | 'push' | 'sync' | null;
|
||||
@@ -178,6 +183,49 @@ export const IdentityDropdown: React.FC<IdentityDropdownProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface UpstreamStatusPillProps {
|
||||
comparison: GitRemoteComparison;
|
||||
trackingBranch: string | null;
|
||||
tooltipDelayMs?: number;
|
||||
}
|
||||
|
||||
const UpstreamStatusPill: React.FC<UpstreamStatusPillProps> = ({
|
||||
comparison,
|
||||
trackingBranch,
|
||||
tooltipDelayMs = 1000,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const target = `${comparison.remote}/${comparison.branch}`;
|
||||
const isSynced = comparison.ahead === 0 && comparison.behind === 0;
|
||||
const tooltipText = trackingBranch
|
||||
? t('gitView.header.upstreamTooltipTracking', { target, tracking: trackingBranch })
|
||||
: t('gitView.header.upstreamTooltip', { target });
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={tooltipDelayMs}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex h-8 max-w-full items-center gap-1.5 rounded-md border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2 typography-micro text-muted-foreground">
|
||||
<Icon name="git-branch" className="size-3.5 shrink-0" />
|
||||
<span className="min-w-0 truncate text-foreground/80">{target}</span>
|
||||
{isSynced ? (
|
||||
<span className="tabular-nums text-muted-foreground">{t('gitView.header.upstreamSynced')}</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 tabular-nums">
|
||||
{comparison.ahead > 0 ? (
|
||||
<span className="text-[var(--status-info)]">↑{comparison.ahead}</span>
|
||||
) : null}
|
||||
{comparison.behind > 0 ? (
|
||||
<span className="text-[var(--status-warning)]">↓{comparison.behind}</span>
|
||||
) : null}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={8}>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
status,
|
||||
localBranches,
|
||||
@@ -264,13 +312,20 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const upstreamStatusPill = status.upstreamComparison ? (
|
||||
<UpstreamStatusPill
|
||||
comparison={status.upstreamComparison}
|
||||
trackingBranch={status.tracking}
|
||||
tooltipDelayMs={1000}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const identityControl = (
|
||||
<IdentityDropdown
|
||||
activeProfile={activeIdentityProfile}
|
||||
identities={availableIdentities}
|
||||
onSelect={onSelectIdentity}
|
||||
isApplying={isApplyingIdentity}
|
||||
|
||||
iconOnly={true}
|
||||
/>
|
||||
);
|
||||
@@ -293,7 +348,6 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
onCheckout={onCheckoutBranch}
|
||||
onCreate={onCreateBranch}
|
||||
remotes={remotes}
|
||||
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -317,6 +371,9 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
{upstreamStatusPill ? (
|
||||
<div className="min-w-0 shrink">{upstreamStatusPill}</div>
|
||||
) : null}
|
||||
<div className="shrink-0">{syncButtons}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -343,7 +343,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
|
||||
normalizedError.includes('network') ||
|
||||
normalizedError.includes('connection') ||
|
||||
normalizedError.includes('check connection');
|
||||
|
||||
|
||||
if (isNetworkError) {
|
||||
console.error('[useBrowserVoice] Network error — staying in error state:', errorMsg);
|
||||
setError(errorMsg);
|
||||
@@ -355,7 +355,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.error('[useBrowserVoice] Recognition error:', errorMsg);
|
||||
setError(errorMsg);
|
||||
setStatus('error');
|
||||
@@ -374,7 +374,7 @@ export function useBrowserVoice(): UseBrowserVoiceReturn {
|
||||
if (nextRetry <= MAX_RECOVERY_RETRIES) {
|
||||
const delay = Math.min(1000 * Math.pow(2, nextRetry - 1), 8000);
|
||||
console.log(`[useBrowserVoice] Scheduling recovery retry ${nextRetry}/${MAX_RECOVERY_RETRIES} in ${delay}ms`);
|
||||
|
||||
|
||||
if (recoveryTimerRef.current !== null) {
|
||||
clearTimeout(recoveryTimerRef.current);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react';
|
||||
import { toast } from '@/components/ui';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { getSyncSessions } from '@/sync/sync-refs';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
@@ -83,6 +86,12 @@ type MenuAction =
|
||||
| 'theme-system'
|
||||
| 'toggle-sidebar'
|
||||
| 'toggle-memory-debug'
|
||||
| 'go-back'
|
||||
| 'go-forward'
|
||||
| 'previous-session'
|
||||
| 'next-session'
|
||||
| 'previous-project'
|
||||
| 'next-project'
|
||||
| 'help-dialog'
|
||||
| 'download-logs';
|
||||
|
||||
@@ -136,6 +145,39 @@ export const useMenuActions = (
|
||||
sessionEvents.requestDirectoryDialog();
|
||||
}, []);
|
||||
|
||||
const navigateSession = React.useCallback((direction: -1 | 1) => {
|
||||
const sessions = getSyncSessions();
|
||||
if (sessions.length === 0) return;
|
||||
|
||||
const currentSessionId = useSessionUIStore.getState().currentSessionId;
|
||||
const currentIndex = sessions.findIndex((session) => session.id === currentSessionId);
|
||||
let nextIndex = direction > 0 ? 0 : sessions.length - 1;
|
||||
if (currentIndex >= 0) {
|
||||
nextIndex = (currentIndex + direction + sessions.length) % sessions.length;
|
||||
}
|
||||
const nextSession = sessions[nextIndex];
|
||||
if (!nextSession) return;
|
||||
|
||||
setActiveMainTab('chat');
|
||||
setSessionSwitcherOpen(false);
|
||||
useSessionUIStore.getState().setCurrentSession(nextSession.id);
|
||||
}, [setActiveMainTab, setSessionSwitcherOpen]);
|
||||
|
||||
const navigateProject = React.useCallback((direction: -1 | 1) => {
|
||||
const { activeProjectId, projects, setActiveProject } = useProjectsStore.getState();
|
||||
if (projects.length === 0) return;
|
||||
|
||||
const currentIndex = projects.findIndex((project) => project.id === activeProjectId);
|
||||
let nextIndex = direction > 0 ? 0 : projects.length - 1;
|
||||
if (currentIndex >= 0) {
|
||||
nextIndex = (currentIndex + direction + projects.length) % projects.length;
|
||||
}
|
||||
const nextProject = projects[nextIndex];
|
||||
if (!nextProject) return;
|
||||
|
||||
setActiveProject(nextProject.id);
|
||||
}, []);
|
||||
|
||||
const handleAction = React.useCallback(
|
||||
(action: MenuAction) => {
|
||||
switch (action) {
|
||||
@@ -222,6 +264,30 @@ export const useMenuActions = (
|
||||
onToggleMemoryDebug?.();
|
||||
break;
|
||||
|
||||
case 'go-back':
|
||||
useDirectoryStore.getState().goBack();
|
||||
break;
|
||||
|
||||
case 'go-forward':
|
||||
useDirectoryStore.getState().goForward();
|
||||
break;
|
||||
|
||||
case 'previous-session':
|
||||
navigateSession(-1);
|
||||
break;
|
||||
|
||||
case 'next-session':
|
||||
navigateSession(1);
|
||||
break;
|
||||
|
||||
case 'previous-project':
|
||||
navigateProject(-1);
|
||||
break;
|
||||
|
||||
case 'next-project':
|
||||
navigateProject(1);
|
||||
break;
|
||||
|
||||
case 'help-dialog':
|
||||
toggleHelpDialog();
|
||||
break;
|
||||
@@ -236,6 +302,8 @@ export const useMenuActions = (
|
||||
},
|
||||
[
|
||||
handleChangeWorkspace,
|
||||
navigateProject,
|
||||
navigateSession,
|
||||
onToggleMemoryDebug,
|
||||
openNewSessionDraft,
|
||||
setAboutDialogOpen,
|
||||
|
||||
@@ -100,6 +100,8 @@ export const useWindowControlsOverlayLayout = () => {
|
||||
if (overlay && typeof overlay.removeEventListener === 'function') {
|
||||
overlay.removeEventListener('geometrychange', updateGeometry);
|
||||
}
|
||||
|
||||
applyOverlayInsets(root, 0, 0, 0);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -118,11 +118,19 @@ export interface GitRebaseInProgress {
|
||||
onto: string;
|
||||
}
|
||||
|
||||
export interface GitRemoteComparison {
|
||||
remote: string;
|
||||
branch: string;
|
||||
ahead: number;
|
||||
behind: number;
|
||||
}
|
||||
|
||||
export interface GitStatus {
|
||||
current: string;
|
||||
tracking: string | null;
|
||||
ahead: number;
|
||||
behind: number;
|
||||
upstreamComparison?: GitRemoteComparison | null;
|
||||
files: GitStatusFile[];
|
||||
isClean: boolean;
|
||||
diffStats?: Record<string, { insertions: number; deletions: number }>;
|
||||
|
||||
@@ -505,6 +505,9 @@ export const dict = {
|
||||
'gitView.header.noProfiles': 'No profiles available to apply.',
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.header.upstreamSynced': 'synced',
|
||||
'gitView.header.upstreamTooltip': 'Compared with {target}.',
|
||||
'gitView.header.upstreamTooltipTracking': 'Compared with {target}. Primary sync badges still reflect {tracking}.',
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
@@ -762,7 +765,7 @@ export const dict = {
|
||||
'gitView.toast.mergedIntoBranch': 'Merged {branch} into {currentBranch}',
|
||||
'gitView.toast.pulledFilesPlural': 'Pulled {count} files from {name}',
|
||||
'gitView.toast.pulledFilesSingle': 'Pulled {count} file from {name}',
|
||||
'gitView.toast.pushedToUpstream': 'Pushed to upstream',
|
||||
'gitView.toast.pushedToUpstream': 'Pushed to {name}',
|
||||
'gitView.toast.commitOrStashBeforeSync': 'Commit or stash your changes before syncing',
|
||||
'gitView.toast.alreadyUpToDate': 'Already up to date',
|
||||
'gitView.toast.syncedPulledPluralAndPushed': 'Pulled {count} files from {name} and pushed to upstream',
|
||||
@@ -1259,6 +1262,8 @@ export const dict = {
|
||||
'helpDialog.proTips.themeCycling': 'Theme cycling remembers your preference across sessions',
|
||||
'header.actions.rightSidebarWithShortcut': 'Right sidebar ({shortcut})',
|
||||
'header.actions.toggleRightSidebarAria': 'Toggle right sidebar',
|
||||
'header.actions.openAppMenu': 'OpenChamber menu',
|
||||
'header.actions.openAppMenuAria': 'Open OpenChamber menu',
|
||||
'header.actions.openSessionsWithShortcut': 'Open sessions ({shortcut})',
|
||||
'header.actions.openSessionsAria': 'Open sessions',
|
||||
'header.actions.closeSessionsAria': 'Close sessions',
|
||||
@@ -2085,6 +2090,11 @@ export const dict = {
|
||||
'header.actions.newMiniChatAria': 'Open a new Mini Chat window',
|
||||
'header.actions.openSessionMiniChat': 'Open Session in Mini Chat',
|
||||
'header.actions.openSessionMiniChatAria': 'Open current session in Mini Chat',
|
||||
'header.windowControls.groupAria': 'Window controls',
|
||||
'header.windowControls.minimize': 'Minimize window',
|
||||
'header.windowControls.maximize': 'Maximize window',
|
||||
'header.windowControls.restore': 'Restore window',
|
||||
'header.windowControls.close': 'Close window',
|
||||
'errorBoundary.title': 'Something went wrong',
|
||||
'errorBoundary.description': 'The application encountered an unexpected error. This has been logged for debugging.',
|
||||
'errorBoundary.state.unknownError': 'Unknown error',
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.noProfiles": "No hay perfiles disponibles para aplicar.",
|
||||
"gitView.header.removeRemoteAria": "Eliminar remoto",
|
||||
"gitView.header.removeRemoteTitle": "Eliminar remoto",
|
||||
"gitView.header.upstreamSynced": "sincronizado",
|
||||
"gitView.header.upstreamTooltip": "Comparado con {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Comparado con {target}. Los indicadores principales de sincronización aún reflejan {tracking}.",
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Archivo binario — no hay diff disponible",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
@@ -763,7 +766,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.toast.mergedIntoBranch": "Merge de {branch} en {currentBranch}",
|
||||
"gitView.toast.pulledFilesPlural": "Se trajeron {count} archivos de {name}",
|
||||
"gitView.toast.pulledFilesSingle": "Se trajo {count} archivo de {name}",
|
||||
"gitView.toast.pushedToUpstream": "Enviado al upstream",
|
||||
"gitView.toast.pushedToUpstream": "Enviado a {name}",
|
||||
"gitView.toast.commitOrStashBeforeSync": "Haz commit o stash de tus cambios antes de sincronizar",
|
||||
"gitView.toast.alreadyUpToDate": "Ya está actualizado",
|
||||
"gitView.toast.syncedPulledPluralAndPushed": "Se trajeron {count} archivos de {name} y se envió al upstream",
|
||||
@@ -1225,6 +1228,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.proTips.themeCycling": "El ciclo de tema recuerda tu preferencia entre sesiones",
|
||||
"header.actions.rightSidebarWithShortcut": "Barra lateral derecha ({shortcut})",
|
||||
"header.actions.toggleRightSidebarAria": "Mostrar u ocultar barra lateral derecha",
|
||||
"header.actions.openAppMenu": "Menú de OpenChamber",
|
||||
"header.actions.openAppMenuAria": "Abrir menú de OpenChamber",
|
||||
"header.actions.openSessionsWithShortcut": "Abrir sesiones ({shortcut})",
|
||||
"header.actions.openSessionsAria": "Abrir sesiones",
|
||||
"header.actions.closeSessionsAria": "Cerrar sesiones",
|
||||
@@ -2051,6 +2056,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.newMiniChatAria": "Abrir una nueva ventana Mini Chat",
|
||||
"header.actions.openSessionMiniChat": "Abrir sesión en Mini Chat",
|
||||
"header.actions.openSessionMiniChatAria": "Abrir la sesión actual en Mini Chat",
|
||||
"header.windowControls.groupAria": "Controles de ventana",
|
||||
"header.windowControls.minimize": "Minimizar ventana",
|
||||
"header.windowControls.maximize": "Maximizar ventana",
|
||||
"header.windowControls.restore": "Restaurar ventana",
|
||||
"header.windowControls.close": "Cerrar ventana",
|
||||
"errorBoundary.title": "Algo salió mal",
|
||||
"errorBoundary.description": "La aplicación encontró un error inesperado. Esto se ha registrado para depuración.",
|
||||
"errorBoundary.state.unknownError": "Error desconocido",
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.noProfiles': '적용할 프로필 없음',
|
||||
'gitView.header.removeRemoteAria': '리모트 제거',
|
||||
'gitView.header.removeRemoteTitle': '리모트 제거',
|
||||
'gitView.header.upstreamSynced': '동기화됨',
|
||||
'gitView.header.upstreamTooltip': '{target}와 비교됨.',
|
||||
'gitView.header.upstreamTooltipTracking': '{target}와 비교됨. 기본 동기화 배지는 계속 {tracking}을 반영합니다.',
|
||||
'gitView.history.binary': '바이너리',
|
||||
'gitView.history.binaryNoDiff': '바이너리 파일 — diff 없음',
|
||||
'gitView.history.commitsPlaceholder': '커밋 검색',
|
||||
@@ -763,7 +766,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.toast.mergedIntoBranch': '{branch}을(를) {currentBranch}에 병합했습니다',
|
||||
'gitView.toast.pulledFilesPlural': '{name}에서 파일 {count}개를 풀했습니다',
|
||||
'gitView.toast.pulledFilesSingle': '{name}에서 파일 {count}개를 풀했습니다',
|
||||
'gitView.toast.pushedToUpstream': '업스트림에 푸시했습니다',
|
||||
'gitView.toast.pushedToUpstream': '{name}에 푸시했습니다',
|
||||
'gitView.toast.commitOrStashBeforeSync': '동기화하기 전에 변경 사항을 커밋하거나 stash하세요',
|
||||
'gitView.toast.alreadyUpToDate': '이미 최신 상태입니다',
|
||||
'gitView.toast.syncedPulledPluralAndPushed': '{name}에서 파일 {count}개를 풀하고 업스트림에 푸시했습니다',
|
||||
@@ -1261,6 +1264,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.proTips.themeCycling': '테마 순환은 세션 간에도 선호 설정을 기억합니다',
|
||||
'header.actions.rightSidebarWithShortcut': '오른쪽 사이드바 ({shortcut})',
|
||||
'header.actions.toggleRightSidebarAria': '오른쪽 사이드바 토글',
|
||||
'header.actions.openAppMenu': 'OpenChamber 메뉴',
|
||||
'header.actions.openAppMenuAria': 'OpenChamber 메뉴 열기',
|
||||
'header.actions.openSessionsWithShortcut': '세션 ({shortcut}) 열기',
|
||||
'header.actions.openSessionsAria': '세션 열기',
|
||||
'header.actions.closeSessionsAria': '세션 닫기',
|
||||
@@ -2085,6 +2090,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.newMiniChatAria': '새 Mini Chat 창 열기',
|
||||
'header.actions.openSessionMiniChat': 'Mini Chat에서 세션 열기',
|
||||
'header.actions.openSessionMiniChatAria': '현재 세션을 Mini Chat에서 열기',
|
||||
'header.windowControls.groupAria': '창 컨트롤',
|
||||
'header.windowControls.minimize': '창 최소화',
|
||||
'header.windowControls.maximize': '창 최대화',
|
||||
'header.windowControls.restore': '창 복원',
|
||||
'header.windowControls.close': '창 닫기',
|
||||
'errorBoundary.title': '문제가 발생했습니다',
|
||||
'errorBoundary.description': '애플리케이션에서 예상치 못한 오류가 발생했습니다. 디버깅을 위해 기록되었습니다.',
|
||||
'errorBoundary.state.unknownError': '알 수 없음 오류',
|
||||
|
||||
@@ -601,6 +601,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.newMiniChatAria': 'Otwórz nowe okno Mini Chat',
|
||||
'header.actions.openSessionMiniChat': 'Otwórz sesję w Mini Chat',
|
||||
'header.actions.openSessionMiniChatAria': 'Otwórz bieżącą sesję w Mini Chat',
|
||||
'header.windowControls.groupAria': 'Elementy sterujące oknem',
|
||||
'header.windowControls.minimize': 'Minimalizuj okno',
|
||||
'header.windowControls.maximize': 'Maksymalizuj okno',
|
||||
'header.windowControls.restore': 'Przywróć okno',
|
||||
'header.windowControls.close': 'Zamknij okno',
|
||||
'errorBoundary.title': 'Coś poszło nie tak',
|
||||
'errorBoundary.description': 'Aplikacja napotkała nieoczekiwany błąd. Zostało to zalogowane do celów debugowania.',
|
||||
'errorBoundary.state.unknownError': 'Nieznany błąd',
|
||||
@@ -1488,6 +1493,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.noProfiles': 'No profiles available to apply.',
|
||||
'gitView.header.removeRemoteAria': 'Remove Remote aria label',
|
||||
'gitView.header.removeRemoteTitle': 'Remove Remote Title',
|
||||
'gitView.header.upstreamSynced': 'zsynchronizowano',
|
||||
'gitView.header.upstreamTooltip': 'Porównano z {target}.',
|
||||
'gitView.header.upstreamTooltipTracking': 'Porównano z {target}. Główne wskaźniki synchronizacji nadal odzwierciedlają {tracking}.',
|
||||
'gitView.history.binary': 'Binary',
|
||||
'gitView.history.binaryNoDiff': 'Binary file — no diff available',
|
||||
'gitView.history.commitsPlaceholder': 'Commits Placeholder',
|
||||
@@ -1729,6 +1737,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.newSessionWithShortcut': 'Nowa sesja ({shortcut})',
|
||||
'header.actions.openPlanAria': 'Otwórz plan',
|
||||
'header.actions.openSessionsAria': 'Otwórz sesje',
|
||||
'header.actions.openAppMenu': 'Menu OpenChamber',
|
||||
'header.actions.openAppMenuAria': 'Otwórz menu OpenChamber',
|
||||
'header.actions.openSessionsWithShortcut': 'Otwórz sesje ({shortcut})',
|
||||
'header.actions.planWithShortcut': 'Plan ({shortcut})',
|
||||
'header.actions.rightSidebarWithShortcut': 'Prawy panel boczny ({shortcut})',
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.noProfiles": "Não há perfiles disponíveis para aplicar.",
|
||||
"gitView.header.removeRemoteAria": "Excluir remoto",
|
||||
"gitView.header.removeRemoteTitle": "Excluir remoto",
|
||||
"gitView.header.upstreamSynced": "sincronizado",
|
||||
"gitView.header.upstreamTooltip": "Comparado com {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Comparado com {target}. Os indicadores principais de sincronização ainda refletem {tracking}.",
|
||||
"gitView.history.binary": "Binario",
|
||||
"gitView.history.binaryNoDiff": "Arquivo binário — diff não disponível",
|
||||
"gitView.history.commitsPlaceholder": "Buscar commits...",
|
||||
@@ -763,7 +766,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.toast.mergedIntoBranch": "Merge de {branch} em {currentBranch}",
|
||||
"gitView.toast.pulledFilesPlural": "Se trajeron {count} arquivos de {name}",
|
||||
"gitView.toast.pulledFilesSingle": "Se trajo {count} arquivo de {name}",
|
||||
"gitView.toast.pushedToUpstream": "Enviado ao upstream",
|
||||
"gitView.toast.pushedToUpstream": "Enviado para {name}",
|
||||
"gitView.toast.commitOrStashBeforeSync": "Faça commit ou stash das alterações antes de sincronizar",
|
||||
"gitView.toast.alreadyUpToDate": "Já está atualizado",
|
||||
"gitView.toast.syncedPulledPluralAndPushed": "Foram trazidos {count} arquivos de {name} e enviados ao upstream",
|
||||
@@ -1225,6 +1228,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.proTips.themeCycling": "A alternância de tema lembra sua preferência entre sessões",
|
||||
"header.actions.rightSidebarWithShortcut": "Barra lateral direita ({shortcut})",
|
||||
"header.actions.toggleRightSidebarAria": "Mostrar ou ocultar barra lateral direita",
|
||||
"header.actions.openAppMenu": "Menu do OpenChamber",
|
||||
"header.actions.openAppMenuAria": "Abrir menu do OpenChamber",
|
||||
"header.actions.openSessionsWithShortcut": "Abrir sessões ({shortcut})",
|
||||
"header.actions.openSessionsAria": "Abrir sessões",
|
||||
"header.actions.closeSessionsAria": "Fechar sessões",
|
||||
@@ -2051,6 +2056,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.newMiniChatAria": "Abrir uma nova janela Mini Chat",
|
||||
"header.actions.openSessionMiniChat": "Abrir sessão no Mini Chat",
|
||||
"header.actions.openSessionMiniChatAria": "Abrir a sessão atual no Mini Chat",
|
||||
"header.windowControls.groupAria": "Controles da janela",
|
||||
"header.windowControls.minimize": "Minimizar janela",
|
||||
"header.windowControls.maximize": "Maximizar janela",
|
||||
"header.windowControls.restore": "Restaurar janela",
|
||||
"header.windowControls.close": "Fechar janela",
|
||||
"errorBoundary.title": "Algo deu errado",
|
||||
"errorBoundary.description": "O aplicativo encontrou um erro inesperado. Isso foi registrado para depuração.",
|
||||
"errorBoundary.state.unknownError": "Erro desconhecido",
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.header.noProfiles": "Немає доступних профілів для застосування.",
|
||||
"gitView.header.removeRemoteAria": "Видалити remote",
|
||||
"gitView.header.removeRemoteTitle": "Видалити remote",
|
||||
"gitView.header.upstreamSynced": "синхронізовано",
|
||||
"gitView.header.upstreamTooltip": "Порівняно з {target}.",
|
||||
"gitView.header.upstreamTooltipTracking": "Порівняно з {target}. Основні індикатори синхронізації все ще відображають {tracking}.",
|
||||
"gitView.history.binary": "Бінарний",
|
||||
"gitView.history.binaryNoDiff": "Бінарний файл — diff недоступний",
|
||||
"gitView.history.commitsPlaceholder": "Пошук комітів",
|
||||
@@ -763,7 +766,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"gitView.toast.mergedIntoBranch": "Злито {branch} в {currentBranch}",
|
||||
"gitView.toast.pulledFilesPlural": "Отримано файлів: {count} з {name}",
|
||||
"gitView.toast.pulledFilesSingle": "Отримано файл: {count} з {name}",
|
||||
"gitView.toast.pushedToUpstream": "Надіслано в upstream",
|
||||
"gitView.toast.pushedToUpstream": "Надіслано в {name}",
|
||||
"gitView.toast.commitOrStashBeforeSync": "Закомітьте або сховайте зміни перед синхронізацією",
|
||||
"gitView.toast.alreadyUpToDate": "Вже актуально",
|
||||
"gitView.toast.syncedPulledPluralAndPushed": "Отримано файлів: {count} з {name} і надіслано в upstream",
|
||||
@@ -1225,6 +1228,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.proTips.themeCycling": "Перемикання теми запам’ятовує ваші переваги протягом сесій",
|
||||
"header.actions.rightSidebarWithShortcut": "Права бічна панель ({shortcut})",
|
||||
"header.actions.toggleRightSidebarAria": "Перемкнути праву бічну панель",
|
||||
"header.actions.openAppMenu": "Меню OpenChamber",
|
||||
"header.actions.openAppMenuAria": "Відкрити меню OpenChamber",
|
||||
"header.actions.openSessionsWithShortcut": "Відкрити сесії ({shortcut})",
|
||||
"header.actions.openSessionsAria": "Відкрити сесії",
|
||||
"header.actions.closeSessionsAria": "Закрити сесії",
|
||||
@@ -2051,6 +2056,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
"header.actions.newMiniChatAria": "Відкрити нове вікно Mini Chat",
|
||||
"header.actions.openSessionMiniChat": "Відкрити сесію в Mini Chat",
|
||||
"header.actions.openSessionMiniChatAria": "Відкрити поточну сесію в Mini Chat",
|
||||
"header.windowControls.groupAria": "Елементи керування вікном",
|
||||
"header.windowControls.minimize": "Згорнути вікно",
|
||||
"header.windowControls.maximize": "Розгорнути вікно",
|
||||
"header.windowControls.restore": "Відновити вікно",
|
||||
"header.windowControls.close": "Закрити вікно",
|
||||
"errorBoundary.title": "Щось пішло не так",
|
||||
"errorBoundary.description": "У програмі сталася неочікувана помилка. Це було зареєстровано для налагодження.",
|
||||
"errorBoundary.state.unknownError": "Невідома помилка",
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.noProfiles': '没有可应用的配置。',
|
||||
'gitView.header.removeRemoteAria': '移除远程 {name}',
|
||||
'gitView.header.removeRemoteTitle': '移除 {name}',
|
||||
'gitView.header.upstreamSynced': '已同步',
|
||||
'gitView.header.upstreamTooltip': '与 {target} 对比。',
|
||||
'gitView.header.upstreamTooltipTracking': '与 {target} 对比。主要同步徽标仍然反映 {tracking}。',
|
||||
'gitView.history.binary': '二进制',
|
||||
'gitView.history.binaryNoDiff': '二进制文件,无法显示差异',
|
||||
'gitView.history.commitsPlaceholder': '提交数',
|
||||
@@ -763,7 +766,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.toast.mergedIntoBranch': '已将 {branch} 合并到 {currentBranch}',
|
||||
'gitView.toast.pulledFilesPlural': '已从 {name} 拉取 {count} 个文件',
|
||||
'gitView.toast.pulledFilesSingle': '已从 {name} 拉取 {count} 个文件',
|
||||
'gitView.toast.pushedToUpstream': '已推送到上游',
|
||||
'gitView.toast.pushedToUpstream': '已推送到 {name}',
|
||||
'gitView.toast.commitOrStashBeforeSync': '同步前请先提交或储藏你的更改',
|
||||
'gitView.toast.alreadyUpToDate': '已是最新状态',
|
||||
'gitView.toast.syncedPulledPluralAndPushed': '已从 {name} 拉取 {count} 个文件并推送到上游',
|
||||
@@ -1225,6 +1228,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.proTips.themeCycling': '主题循环会记住你在各会话中的偏好',
|
||||
'header.actions.rightSidebarWithShortcut': '右侧边栏({shortcut})',
|
||||
'header.actions.toggleRightSidebarAria': '切换右侧边栏',
|
||||
'header.actions.openAppMenu': 'OpenChamber 菜单',
|
||||
'header.actions.openAppMenuAria': '打开 OpenChamber 菜单',
|
||||
'header.actions.openSessionsWithShortcut': '打开会话({shortcut})',
|
||||
'header.actions.openSessionsAria': '打开会话',
|
||||
'header.actions.closeSessionsAria': '关闭会话',
|
||||
@@ -2051,6 +2056,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.newMiniChatAria': '打开新的 Mini Chat 窗口',
|
||||
'header.actions.openSessionMiniChat': '在 Mini Chat 中打开会话',
|
||||
'header.actions.openSessionMiniChatAria': '在 Mini Chat 中打开当前会话',
|
||||
'header.windowControls.groupAria': '窗口控件',
|
||||
'header.windowControls.minimize': '最小化窗口',
|
||||
'header.windowControls.maximize': '最大化窗口',
|
||||
'header.windowControls.restore': '还原窗口',
|
||||
'header.windowControls.close': '关闭窗口',
|
||||
'errorBoundary.title': '发生错误',
|
||||
'errorBoundary.description': '应用遇到意外错误,已记录用于调试。',
|
||||
'errorBoundary.state.unknownError': '未知错误',
|
||||
|
||||
@@ -506,6 +506,9 @@ export const dict: Record<I18nKey, string> = {
|
||||
'gitView.header.noProfiles': '沒有可套用的設定。',
|
||||
'gitView.header.removeRemoteAria': '移除遠端 {name}',
|
||||
'gitView.header.removeRemoteTitle': '移除 {name}',
|
||||
'gitView.header.upstreamSynced': '已同步',
|
||||
'gitView.header.upstreamTooltip': '與 {target} 比較。',
|
||||
'gitView.header.upstreamTooltipTracking': '與 {target} 比較。主要同步徽章仍反映 {tracking}。',
|
||||
'gitView.history.binary': '二進位',
|
||||
'gitView.history.binaryNoDiff': '二進位檔案 — 無可用 diff',
|
||||
'gitView.history.commitsPlaceholder': '提交數',
|
||||
@@ -1223,6 +1226,8 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.proTips.themeCycling': '主題循環會記住你在各會話中的偏好',
|
||||
'header.actions.rightSidebarWithShortcut': '右側邊欄({shortcut})',
|
||||
'header.actions.toggleRightSidebarAria': '切換右側邊欄',
|
||||
'header.actions.openAppMenu': 'OpenChamber 選單',
|
||||
'header.actions.openAppMenuAria': '開啟 OpenChamber 選單',
|
||||
'header.actions.openSessionsWithShortcut': '開啟會話({shortcut})',
|
||||
'header.actions.openSessionsAria': '開啟會話',
|
||||
'header.actions.closeSessionsAria': '關閉會話',
|
||||
@@ -2049,6 +2054,11 @@ export const dict: Record<I18nKey, string> = {
|
||||
'header.actions.newMiniChatAria': '開啟新的 Mini Chat 視窗',
|
||||
'header.actions.openSessionMiniChat': '在 Mini Chat 中開啟會話',
|
||||
'header.actions.openSessionMiniChatAria': '在 Mini Chat 中開啟目前會話',
|
||||
'header.windowControls.groupAria': '視窗控制項',
|
||||
'header.windowControls.minimize': '最小化視窗',
|
||||
'header.windowControls.maximize': '最大化視窗',
|
||||
'header.windowControls.restore': '還原視窗',
|
||||
'header.windowControls.close': '關閉視窗',
|
||||
'errorBoundary.title': '發生錯誤',
|
||||
'errorBoundary.description': '應用程式遇到意外錯誤,已記錄用於偵錯。',
|
||||
'errorBoundary.state.unknownError': '未知錯誤',
|
||||
|
||||
@@ -34,13 +34,23 @@ export const DEFAULT_OPEN_IN_APP_ID = 'finder';
|
||||
export const OPEN_IN_ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']);
|
||||
export const OPEN_DIRECTORY_APP_IDS = new Set(['finder', 'terminal', 'iterm2', 'ghostty']);
|
||||
|
||||
export const getPlatformOpenInApp = (app: OpenInApp): OpenInApp => {
|
||||
if (typeof window !== 'undefined' && window.__OPENCHAMBER_PLATFORM__ === 'win32') {
|
||||
if (app.id === 'finder') {
|
||||
return { ...app, label: 'Explorer', appName: 'File Explorer' };
|
||||
}
|
||||
}
|
||||
return app;
|
||||
};
|
||||
|
||||
export const getOpenInAppById = (id: string | null | undefined): OpenInApp | null => {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
return OPEN_IN_APPS.find((app) => app.id === id) ?? null;
|
||||
const app = OPEN_IN_APPS.find((candidate) => candidate.id === id) ?? null;
|
||||
return app ? getPlatformOpenInApp(app) : null;
|
||||
};
|
||||
|
||||
export const getDefaultOpenInApp = (): OpenInApp => {
|
||||
return getOpenInAppById(DEFAULT_OPEN_IN_APP_ID) ?? OPEN_IN_APPS[0];
|
||||
return getOpenInAppById(DEFAULT_OPEN_IN_APP_ID) ?? getPlatformOpenInApp(OPEN_IN_APPS[0]);
|
||||
};
|
||||
|
||||
@@ -204,6 +204,21 @@ const haveDiffStatsChanged = (
|
||||
return false;
|
||||
};
|
||||
|
||||
const haveRemoteComparisonChanged = (
|
||||
previous?: GitStatus['upstreamComparison'],
|
||||
next?: GitStatus['upstreamComparison']
|
||||
): boolean => {
|
||||
if (!previous && !next) return false;
|
||||
if (!previous || !next) return true;
|
||||
|
||||
return (
|
||||
previous.remote !== next.remote
|
||||
|| previous.branch !== next.branch
|
||||
|| previous.ahead !== next.ahead
|
||||
|| previous.behind !== next.behind
|
||||
);
|
||||
};
|
||||
|
||||
const hasStatusChanged = (oldStatus: GitStatus | null, newStatus: GitStatus | null): boolean => {
|
||||
if (!oldStatus && !newStatus) return false;
|
||||
if (!oldStatus || !newStatus) return true;
|
||||
@@ -217,6 +232,12 @@ const hasStatusChanged = (oldStatus: GitStatus | null, newStatus: GitStatus | nu
|
||||
if (oldStatus.current !== newStatus.current) return true;
|
||||
if (oldStatus.tracking !== newStatus.tracking) return true;
|
||||
if (oldStatus.isClean !== newStatus.isClean) return true;
|
||||
if (
|
||||
newStatus.upstreamComparison !== undefined
|
||||
&& haveRemoteComparisonChanged(oldStatus.upstreamComparison, newStatus.upstreamComparison)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldPaths = new Set(oldFiles.map(f => `${f.path}:${f.index}:${f.working_dir}`));
|
||||
for (const file of newFiles) {
|
||||
@@ -465,9 +486,17 @@ export const useGitStore = create<GitStore>()(
|
||||
}
|
||||
|
||||
// Preserve diffStats from previous status when light mode returns none
|
||||
const mergedStatus = newStatus.diffStats === undefined && currentDirState.status?.diffStats
|
||||
? { ...newStatus, diffStats: currentDirState.status.diffStats }
|
||||
: newStatus;
|
||||
const mergedStatus = {
|
||||
...newStatus,
|
||||
diffStats:
|
||||
newStatus.diffStats === undefined && currentDirState.status?.diffStats !== undefined
|
||||
? currentDirState.status.diffStats
|
||||
: newStatus.diffStats,
|
||||
upstreamComparison:
|
||||
newStatus.upstreamComparison === undefined
|
||||
? currentDirState.status?.upstreamComparison
|
||||
: newStatus.upstreamComparison,
|
||||
};
|
||||
|
||||
newDirectories.set(directory, {
|
||||
...currentDirState,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { fetchDesktopInstalledApps, isDesktopLocalOriginActive, isTauriShell, type DesktopSettings, type InstalledDesktopAppInfo } from '@/lib/desktop';
|
||||
import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, type OpenInApp } from '@/lib/openInApps';
|
||||
import { OPEN_IN_APPS, DEFAULT_OPEN_IN_APP_ID, OPEN_IN_ALWAYS_AVAILABLE_APP_IDS, getOpenInAppById, getPlatformOpenInApp, type OpenInApp } from '@/lib/openInApps';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
|
||||
export type OpenInAppOption = OpenInApp & {
|
||||
@@ -22,7 +22,7 @@ type OpenInAppsState = {
|
||||
const getAlwaysAvailableApps = (): OpenInAppOption[] => {
|
||||
return OPEN_IN_APPS
|
||||
.filter((app) => OPEN_IN_ALWAYS_AVAILABLE_APP_IDS.has(app.id))
|
||||
.map((app) => ({ ...app }));
|
||||
.map((app) => ({ ...getPlatformOpenInApp(app) }));
|
||||
};
|
||||
|
||||
const getStoredAppId = (): string => {
|
||||
@@ -78,7 +78,7 @@ export const useOpenInAppsStore = create<OpenInAppsState>()((set, get) => ({
|
||||
);
|
||||
|
||||
const withIcons = filtered.map((app) => ({
|
||||
...app,
|
||||
...getPlatformOpenInApp(app),
|
||||
iconDataUrl: iconMap.get(app.appName),
|
||||
}));
|
||||
|
||||
|
||||
Vendored
+1
@@ -6,6 +6,7 @@ declare global {
|
||||
__OPENCHAMBER_MACOS_MAJOR__?: number;
|
||||
__OPENCHAMBER_LOCAL_ORIGIN__?: string;
|
||||
__OPENCHAMBER_ELECTRON__?: { runtime?: string };
|
||||
__OPENCHAMBER_PLATFORM__?: string;
|
||||
__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__?: DesktopBootOutcome;
|
||||
}
|
||||
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
declare module '@xenova/transformers' {
|
||||
export const env: {
|
||||
allowLocalModels: boolean;
|
||||
backends: {
|
||||
onnx: {
|
||||
wasm: {
|
||||
numThreads: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export function pipeline(
|
||||
task: 'automatic-speech-recognition',
|
||||
model: string,
|
||||
options?: {
|
||||
progress_callback?: (info: { status?: string; file?: string; loaded?: number; total?: number }) => void;
|
||||
},
|
||||
): Promise<(input: Float32Array, options?: Record<string, unknown>) => Promise<{ text: string }>>;
|
||||
}
|
||||
Reference in New Issue
Block a user