(null);
@@ -417,7 +419,8 @@ const UserMessageBody = React.memo(({ messageId, parts, isMobile, alwaysShowActi
[hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint]
);
- const actionsBlock = ((canCopyMessage && hasCopyableText) || onRevert || onFork) && showUserActions ? (
+ const effectiveOnFork = chatSurfaceMode === 'mini-chat' ? undefined : onFork;
+ const actionsBlock = ((canCopyMessage && hasCopyableText) || onRevert || effectiveOnFork) && showUserActions ? (
{t('chat.messageBody.actions.revert')}
)}
- {onFork && (
+ {effectiveOnFork && (
)}
-
+ {chatSurfaceMode !== 'mini-chat' ?
{isSharing ? t('chat.messageBody.actions.savingImage') : t('chat.messageBody.actions.saveAsImage')}
-
- {showMessageTTSButtons && hasCopyableText && (
+ : null}
+ {chatSurfaceMode !== 'mini-chat' && showMessageTTSButtons && hasCopyableText && (
) : null}
- {!isVSCode ? (
+ {!isMiniChatSurface && !isVSCode ? (
) : null}
-
+ {!isMiniChatSurface ?
{t('chat.messageBody.actions.startNewSession')}
-
- {!isVSCode ? (
+ : null}
+ {!isMiniChatSurface && !isVSCode ? (
) : null}
- {isLastAssistantInTurn && hasStopFinish ? (
+ {!isMiniChatSurface && isLastAssistantInTurn && hasStopFinish ? (
) : null}
diff --git a/packages/ui/src/components/chat/useChatSurfaceMode.ts b/packages/ui/src/components/chat/useChatSurfaceMode.ts
new file mode 100644
index 00000000..3615e051
--- /dev/null
+++ b/packages/ui/src/components/chat/useChatSurfaceMode.ts
@@ -0,0 +1,4 @@
+import React from 'react';
+import { ChatSurfaceContext, type ChatSurfaceMode } from './chatSurfaceContextValue';
+
+export const useChatSurfaceMode = (): ChatSurfaceMode => React.useContext(ChatSurfaceContext);
diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx
index c15badc2..7506c80b 100644
--- a/packages/ui/src/components/layout/Header.tsx
+++ b/packages/ui/src/components/layout/Header.tsx
@@ -16,7 +16,7 @@ import {
} from '@/components/ui/dropdown-menu';
import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip';
-import { RiArrowLeftSLine, RiChat4Line, RiChatNewLine, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiLayoutRightLine, RiPlayListAddLine, RiRefreshLine, RiServerLine, RiStackLine, RiTerminalBoxLine, RiTimerLine, RiAlertLine, type RemixiconComponentType } from '@remixicon/react';
+import { RiArrowLeftSLine, RiChat4Line, RiChatNewLine, RiCheckLine, RiCloseLine, RiCommandLine, RiFileTextLine, RiFolder6Line, RiGitBranchLine, RiGithubFill, RiLayoutLeftLine, RiLayoutRightLine, RiPictureInPicture2Line, RiPlayListAddLine, RiRefreshLine, RiServerLine, RiStackLine, RiTerminalBoxLine, RiTimerLine, RiAlertLine, RiWindowLine, type RemixiconComponentType } from '@remixicon/react';
import { DiffIcon } from '@/components/icons/DiffIcon';
import { useUIStore, type MainTab } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore';
@@ -65,7 +65,7 @@ import { OpenInAppButton } from '@/components/desktop/OpenInAppButton';
import { forceKillTerminal } from '@/lib/terminalApi';
import { useTerminalStore } from '@/stores/useTerminalStore';
import { ProjectActionsButton } from '@/components/layout/ProjectActionsButton';
-import { isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
+import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, startDesktopWindowDrag } from '@/lib/desktop';
import { desktopHostsGet, locationMatchesHost, redactSensitiveUrl } from '@/lib/desktopHosts';
import { resolveSessionDiffStats } from '@/components/session/sidebar/utils';
import { useI18n } from '@/lib/i18n';
@@ -722,6 +722,7 @@ export const Header: React.FC = ({
}
return isDesktopShell();
});
+ const hasElectronDesktopIPC = React.useMemo(() => canUseElectronDesktopIPC(), []);
const isTabletStandalonePwa = useTabletStandalonePwaRuntime();
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
@@ -1279,6 +1280,27 @@ export const Header: React.FC = ({
openNewSessionDraft();
}, [openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]);
+ const handleOpenDraftMiniChat = React.useCallback(() => {
+ void invokeDesktop('desktop_open_draft_mini_chat_window', {
+ directory: normalize(openDirectory || activeProject?.path || ''),
+ projectId: activeProject?.id ?? null,
+ }).catch((error) => {
+ console.warn('[header] failed to open draft mini chat window', error);
+ });
+ }, [activeProject?.id, activeProject?.path, openDirectory]);
+
+ const handleOpenCurrentSessionMiniChat = React.useCallback(() => {
+ if (!currentSessionId) {
+ return;
+ }
+ void invokeDesktop('desktop_open_session_mini_chat_window', {
+ sessionId: currentSessionId,
+ directory: normalize(openDirectory || activeProject?.path || ''),
+ }).catch((error) => {
+ console.warn('[header] failed to open session mini chat window', error);
+ });
+ }, [activeProject?.path, currentSessionId, openDirectory]);
+
const handleOpenContextPanel = React.useCallback(() => {
const directory = normalize(openDirectory || '');
if (!directory) {
@@ -1841,6 +1863,23 @@ export const Header: React.FC = ({
) : null}
+ {hasElectronDesktopIPC && !isLeftSidebarOpen ? (
+
+
+
+
+
+ {t('header.actions.newMiniChat')}
+
+
+ ) : null}
{projectActionsContext && (
= ({
+
{showDesktopHeaderContextUsage && stableDesktopContextUsage ? (
{
+ const path = typeof value === 'string' ? value.trim() : '';
+ if (!path) return '';
+ const home = typeof window !== 'undefined' ? window.__OPENCHAMBER_HOME__ : '';
+ if (home && path === home) return '~';
+ if (home && path.startsWith(`${home}/`)) return `~/${path.slice(home.length + 1)}`;
+ const segments = path.split('/').filter(Boolean);
+ if (segments.length <= 3) return path;
+ return `.../${segments.slice(-3).join('/')}`;
+};
+
+const normalizePath = (value: string | null | undefined): string => {
+ const raw = typeof value === 'string' ? value.trim() : '';
+ if (!raw) return '';
+ const normalized = raw.replace(/\\/g, '/');
+ return normalized === '/' ? '/' : normalized.replace(/\/+$/, '');
+};
+
+const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
+ const { t } = useI18n();
+ const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
+ const draftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
+ const draftProjectId = useSessionUIStore((state) => state.newSessionDraft?.selectedProjectId ?? null);
+ const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
+ const projects = useProjectsStore((state) => state.projects);
+ const activeProject = useProjectsStore((state) => state.getActiveProject());
+ const sessions = useSessions();
+ const runtimeApis = useRuntimeAPIs();
+ const ensureGitStatus = useGitStore((state) => state.ensureStatus);
+ const worktreePath = useSessionUIStore((state) => currentSessionId ? state.worktreeMetadata.get(currentSessionId)?.path ?? '' : '');
+ const worktreeMetadataBranch = useSessionUIStore((state) => currentSessionId ? state.worktreeMetadata.get(currentSessionId)?.branch?.trim() ?? null : null);
+ const worktreeAttachment = useSessionWorktreeStore((state) => currentSessionId ? state.getAttachment(currentSessionId) : undefined);
+ const draftDirectory = useSessionUIStore((state) => {
+ if (!state.newSessionDraft?.open) return '';
+ return normalizePath(state.newSessionDraft.bootstrapPendingDirectory ?? state.newSessionDraft.directoryOverride ?? '');
+ });
+ const [pinned, setPinned] = React.useState(false);
+ const macosMajor = typeof window !== 'undefined' ? window.__OPENCHAMBER_MACOS_MAJOR__ ?? 0 : 0;
+ const hasMacTrafficLights = Number.isFinite(macosMajor) && macosMajor > 0;
+ const macosHeaderSizeClass = hasMacTrafficLights
+ ? macosMajor >= 26
+ ? 'h-12'
+ : macosMajor <= 15
+ ? 'h-14'
+ : ''
+ : '';
+
+ const session = React.useMemo(
+ () => currentSessionId ? sessions.find((entry) => entry.id === currentSessionId) ?? null : null,
+ [currentSessionId, sessions],
+ );
+ const sessionWorktreeMetadata = (session as { worktreeMetadata?: { path?: string | null; branch?: string | null; projectDirectory?: string | null } } | null)?.worktreeMetadata ?? null;
+
+ React.useEffect(() => {
+ if (!isElectronShell()) return;
+ void invokeDesktop<{ pinned?: boolean }>('desktop_get_window_pinned').then((result) => {
+ if (typeof result?.pinned === 'boolean') setPinned(result.pinned);
+ });
+ }, []);
+
+ const title = session?.title?.trim()
+ || (draftOpen || mode === 'draft' ? t('miniChat.header.newSession') : t('miniChat.header.session'));
+ const sessionDirectory = normalizePath((session as { directory?: string | null } | null)?.directory ?? null);
+ const worktreeDirectory = normalizePath(worktreePath || sessionWorktreeMetadata?.path || worktreeAttachment?.cwd || worktreeAttachment?.worktreeRoot || '');
+ const currentDirectoryNormalized = normalizePath(currentDirectory);
+ const openDirectory = worktreeDirectory || sessionDirectory || draftDirectory || currentDirectoryNormalized;
+ const directoryLabel = compactPath(openDirectory);
+ const catalogWorktreeBranch = useSessionUIStore((state) => {
+ const candidateDirectory = normalizePath(worktreeDirectory || sessionDirectory || '');
+ if (!candidateDirectory) return null;
+ for (const worktrees of state.availableWorktreesByProject.values()) {
+ const match = worktrees.find((worktree) => normalizePath(worktree.path) === candidateDirectory);
+ const branch = match?.branch?.trim();
+ if (branch) return branch;
+ }
+ return null;
+ });
+ React.useEffect(() => {
+ if (!openDirectory) return;
+ void ensureGitStatus(openDirectory, runtimeApis.git).catch(() => {});
+ }, [ensureGitStatus, openDirectory, runtimeApis.git]);
+
+ const pathMatchedProject = React.useMemo(() => {
+ const projectDirectory = normalizePath(sessionWorktreeMetadata?.projectDirectory ?? worktreeAttachment?.worktreeRoot ?? null);
+ const candidateDirectory = projectDirectory || openDirectory;
+ if (!candidateDirectory) return null;
+ return projects
+ .map((entry) => ({ ...entry, normalizedPath: normalizePath(entry.path) }))
+ .filter((entry) => entry.normalizedPath && (entry.normalizedPath === candidateDirectory || candidateDirectory.startsWith(`${entry.normalizedPath}/`)))
+ .sort((left, right) => right.path.length - left.path.length)[0] ?? null;
+ }, [openDirectory, projects, sessionWorktreeMetadata?.projectDirectory, worktreeAttachment?.worktreeRoot]);
+ const projectLabel = React.useMemo(() => {
+ const project = pathMatchedProject ?? activeProject;
+ if (!project) return directoryLabel || 'OpenChamber';
+ const label = project.label?.trim();
+ if (label) return label;
+ const segments = project.path.split(/[\\/]/).filter(Boolean);
+ return segments.at(-1) ?? project.path;
+ }, [activeProject, directoryLabel, pathMatchedProject]);
+ const gitBranchForDirectory = useGitBranchLabel(openDirectory || null);
+ const branchLabel = gitBranchForDirectory || worktreeMetadataBranch || sessionWorktreeMetadata?.branch?.trim() || worktreeAttachment?.branch?.trim() || catalogWorktreeBranch;
+ const diffStats = React.useMemo(() => {
+ return resolveSessionDiffStats(session?.summary as Parameters[0]);
+ }, [session?.summary]);
+ const changes = diffStats ?? { additions: 0, deletions: 0 };
+ const hasChanges = changes.additions > 0 || changes.deletions > 0;
+ const dragRegionStyle = { WebkitAppRegion: 'drag' } as React.CSSProperties;
+ const noDragRegionStyle = { WebkitAppRegion: 'no-drag' } as React.CSSProperties;
+
+ const handleTogglePinned = React.useCallback(() => {
+ const nextPinned = !pinned;
+ setPinned(nextPinned);
+ void invokeDesktop('desktop_set_window_pinned', { pinned: nextPinned }).catch(() => {
+ setPinned(!nextPinned);
+ });
+ }, [pinned]);
+
+ const handleOpenMainApp = React.useCallback(() => {
+ const payload = currentSessionId
+ ? { sessionId: currentSessionId, directory: (session as { directory?: string | null } | null)?.directory ?? currentDirectory ?? '' }
+ : { mode: 'draft', directory: openDirectory || currentDirectory || '', projectId: draftProjectId };
+ void invokeDesktop<{ focused?: boolean }>('desktop_focus_main_window', payload)
+ .then((result) => {
+ if (result?.focused === true) {
+ return invokeDesktop('desktop_close_current_window');
+ }
+ return null;
+ });
+ }, [currentDirectory, currentSessionId, draftProjectId, openDirectory, session]);
+
+ return (
+
+ );
+};
+
+export const MiniChatLayout: React.FC = ({ mode, autoOpenDraft = false, unavailable = false }) => {
+ const { t } = useI18n();
+
+ return (
+
+
+
+ {unavailable ? (
+
+
+
{t('miniChat.unavailable.title')}
+
{t('miniChat.unavailable.description')}
+
+
+ ) : (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx
index bca7c47f..305d5ad8 100644
--- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx
+++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx
@@ -32,9 +32,10 @@ import {
RiShieldLine,
RiUnpinLine,
RiGitBranchLine,
+ RiWindowLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
-import { isVSCodeRuntime } from '@/lib/desktop';
+import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop';
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
@@ -267,6 +268,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const displayMode = useSessionDisplayStore((state) => state.displayMode);
const isMinimalMode = displayMode === 'minimal';
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
+ const isElectron = React.useMemo(() => canUseElectronDesktopIPC(), []);
const revealOnHoverClass = isVSCode
? 'group-hover:opacity-100 group-hover:pointer-events-auto'
: 'group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto';
@@ -429,6 +431,16 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
await doExportSession(false);
}, [doExportSession, node.children.length]);
+ const handleOpenMiniChatWindow = React.useCallback(() => {
+ if (!sessionDirectory) return;
+ void invokeDesktop('desktop_open_session_mini_chat_window', {
+ sessionId: session.id,
+ directory: sessionDirectory,
+ }).catch((error) => {
+ console.warn('[session-sidebar] failed to open mini chat window', error);
+ });
+ }, [session.id, sessionDirectory]);
+
if (editingId === session.id) {
return (
) : null}
+ {isElectron ? (
+
+
+ {t('sessions.sidebar.session.menu.openMiniChatWindow')}
+
+ ) : null}
+
handleDeleteSession(session, { archivedBucket })}>
diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx
index 072a1916..47beb26d 100644
--- a/packages/ui/src/components/ui/CommandPalette.tsx
+++ b/packages/ui/src/components/ui/CommandPalette.tsx
@@ -36,19 +36,21 @@ import {
RiLayoutLeftLine,
RiLayoutRightLine,
RiPieChartLine,
+ RiWindowLine,
RiSettings3Line,
RiTerminalBoxLine,
} from '@remixicon/react';
import type { Session } from '@opencode-ai/sdk/v2';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
-import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
+import { canUseElectronDesktopIPC, invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata';
import { getSettingsNavIcon } from '@/components/views/SettingsView';
import { scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
import { truncatePathMiddle } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { sessionEvents } from '@/lib/sessionEvents';
+import { useProjectsStore } from '@/stores/useProjectsStore';
type CommandEntry = {
id: string;
@@ -94,6 +96,7 @@ export const CommandPalette: React.FC = () => {
const activeSessions = useGlobalSessionsStore((s) => s.activeSessions);
const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
+ const activeProject = useProjectsStore((s) => s.getActiveProject());
const effectiveDirectory = useEffectiveDirectory();
const searchFiles = useFileSearchStore((s) => s.searchFiles);
const { files: filesApi, git: gitApi } = useRuntimeAPIs();
@@ -232,6 +235,23 @@ export const CommandPalette: React.FC = () => {
onSelect: run(() => setSettingsDialogOpen(true)),
},
];
+ if (canUseElectronDesktopIPC()) {
+ list.splice(1, 0, {
+ id: 'new-mini-chat',
+ title: t('commandPalette.item.newMiniChat'),
+ icon: ,
+ shortcutId: 'new_mini_chat',
+ searchText: t('commandPalette.item.newMiniChat'),
+ onSelect: run(() => {
+ void invokeDesktop('desktop_open_draft_mini_chat_window', {
+ directory: normalizePath(currentDirectory || activeProject?.path || ''),
+ projectId: activeProject?.id ?? null,
+ }).catch((error) => {
+ console.warn('[command-palette] failed to open draft mini chat window', error);
+ });
+ }),
+ });
+ }
return list;
}, [
t,
@@ -246,6 +266,8 @@ export const CommandPalette: React.FC = () => {
currentDirectory,
openContextOverview,
setSettingsDialogOpen,
+ activeProject?.id,
+ activeProject?.path,
]);
// ---------------------------------------------------------------------------
diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts
index e1ee756d..29277a5b 100644
--- a/packages/ui/src/hooks/useKeyboardShortcuts.ts
+++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts
@@ -7,9 +7,11 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { useConfigStore } from '@/stores/useConfigStore';
-import { isVSCodeRuntime } from '@/lib/desktop';
+import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/desktop';
import { showOpenCodeStatus } from '@/lib/openCodeStatus';
import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { useProjectsStore } from '@/stores/useProjectsStore';
export const useKeyboardShortcuts = () => {
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
@@ -33,6 +35,8 @@ export const useKeyboardShortcuts = () => {
const setTimelineDialogOpen = useUIStore((s) => s.setTimelineDialogOpen);
const toggleExpandedInput = useUIStore((s) => s.toggleExpandedInput);
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
+ const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
+ const activeProject = useProjectsStore((s) => s.getActiveProject());
const { themeMode, setThemeMode } = useThemeSystem();
const { working } = useAssistantStatus();
const abortPrimedUntilRef = React.useRef(null);
@@ -121,6 +125,17 @@ export const useKeyboardShortcuts = () => {
return;
}
+ if (canUseElectronDesktopIPC() && eventMatchesShortcut(e, combo('new_mini_chat'))) {
+ e.preventDefault();
+ void invokeDesktop('desktop_open_draft_mini_chat_window', {
+ directory: currentDirectory || activeProject?.path || '',
+ projectId: activeProject?.id ?? null,
+ }).catch((error) => {
+ console.warn('[keyboard-shortcuts] failed to open draft mini chat window', error);
+ });
+ return;
+ }
+
const matchedNewSessionShortcut = eventMatchesShortcut(e, combo('new_chat'));
const matchedWorktreeShortcut = eventMatchesShortcut(e, combo('new_chat_worktree'));
@@ -487,6 +502,9 @@ export const useKeyboardShortcuts = () => {
armAbortPrompt,
resetAbortPriming,
currentSessionId,
+ currentDirectory,
+ activeProject?.id,
+ activeProject?.path,
shortcutOverrides,
]);
diff --git a/packages/ui/src/hooks/useMiniChatKeyboardShortcuts.ts b/packages/ui/src/hooks/useMiniChatKeyboardShortcuts.ts
new file mode 100644
index 00000000..fdb8a815
--- /dev/null
+++ b/packages/ui/src/hooks/useMiniChatKeyboardShortcuts.ts
@@ -0,0 +1,94 @@
+import React from 'react';
+import { canUseElectronDesktopIPC, invokeDesktop } from '@/lib/desktop';
+import { eventMatchesShortcut, getEffectiveShortcutCombo } from '@/lib/shortcuts';
+import { useConfigStore } from '@/stores/useConfigStore';
+import { useDirectoryStore } from '@/stores/useDirectoryStore';
+import { useProjectsStore } from '@/stores/useProjectsStore';
+import { useUIStore } from '@/stores/useUIStore';
+import { useSelectionStore } from '@/sync/selection-store';
+import { useSessionUIStore } from '@/sync/session-ui-store';
+
+const focusChatInput = () => {
+ const textarea = document.querySelector('textarea[data-chat-input="true"]');
+ textarea?.focus();
+};
+
+export const useMiniChatKeyboardShortcuts = () => {
+ const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
+ const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
+ const activeProject = useProjectsStore((state) => state.getActiveProject());
+
+ React.useEffect(() => {
+ const combo = (actionId: string) => getEffectiveShortcutCombo(actionId, shortcutOverrides);
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (eventMatchesShortcut(event, combo('focus_input'))) {
+ event.preventDefault();
+ focusChatInput();
+ return;
+ }
+
+ if (canUseElectronDesktopIPC() && eventMatchesShortcut(event, combo('new_mini_chat'))) {
+ event.preventDefault();
+ void invokeDesktop('desktop_open_draft_mini_chat_window', {
+ directory: currentDirectory || activeProject?.path || '',
+ projectId: activeProject?.id ?? null,
+ })?.catch((error) => {
+ console.warn('[mini-chat-shortcuts] failed to open draft mini chat window', error);
+ });
+ return;
+ }
+
+ if (eventMatchesShortcut(event, combo('open_model_selector'))) {
+ event.preventDefault();
+ const { isModelSelectorOpen, setModelSelectorOpen } = useUIStore.getState();
+ setModelSelectorOpen(!isModelSelectorOpen);
+ return;
+ }
+
+ if (eventMatchesShortcut(event, combo('cycle_thinking_variant'))) {
+ const configState = useConfigStore.getState();
+ const variants = configState.getCurrentModelVariants();
+ if (variants.length === 0) {
+ return;
+ }
+
+ event.preventDefault();
+ configState.cycleCurrentVariant();
+
+ const nextVariant = useConfigStore.getState().currentVariant;
+ const sessionId = useSessionUIStore.getState().currentSessionId;
+ const agentName = useConfigStore.getState().currentAgentName;
+ const providerId = useConfigStore.getState().currentProviderId;
+ const modelId = useConfigStore.getState().currentModelId;
+
+ if (sessionId && agentName && providerId && modelId) {
+ useSelectionStore.getState().saveAgentModelVariantForSession(sessionId, agentName, providerId, modelId, nextVariant);
+ }
+ return;
+ }
+
+ const cyclesForward = eventMatchesShortcut(event, combo('cycle_favorite_model_forward'));
+ const cyclesBackward = eventMatchesShortcut(event, combo('cycle_favorite_model_backward'));
+ if (cyclesForward || cyclesBackward) {
+ const { favoriteModels, addRecentModel } = useUIStore.getState();
+ if (favoriteModels.length === 0) {
+ return;
+ }
+
+ event.preventDefault();
+ const { currentProviderId, currentModelId, setProvider, setModel } = useConfigStore.getState();
+ const currentIndex = favoriteModels.findIndex((favorite) => favorite.providerID === currentProviderId && favorite.modelID === currentModelId);
+ const delta = cyclesForward ? 1 : -1;
+ const next = favoriteModels[(currentIndex + delta + favoriteModels.length) % favoriteModels.length];
+
+ setProvider(next.providerID);
+ setModel(next.modelID);
+ addRecentModel(next.providerID, next.modelID);
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [activeProject?.id, activeProject?.path, currentDirectory, shortcutOverrides]);
+};
diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts
index 7a1450fd..e80571d9 100644
--- a/packages/ui/src/lib/desktop.ts
+++ b/packages/ui/src/lib/desktop.ts
@@ -200,6 +200,21 @@ export const isTauriShell = (): boolean => {
export const isElectronShell = (): boolean => getElectronRuntime()?.runtime === 'electron';
+export const hasDesktopInvoke = (): boolean => {
+ if (typeof window === 'undefined') return false;
+ const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
+ return typeof tauri?.core?.invoke === 'function';
+};
+
+export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke();
+
+export const invokeDesktop = async (command: string, args?: Record): Promise => {
+ if (typeof window === 'undefined') return null;
+ const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
+ if (typeof tauri?.core?.invoke !== 'function') return null;
+ return tauri.core.invoke(command, args ?? {}) as Promise;
+};
+
const normalizeOrigin = (raw: string): string | null => {
const trimmed = raw.trim();
if (!trimmed) return null;
diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts
index 30db1dd6..5c80ca14 100644
--- a/packages/ui/src/lib/i18n/messages/en.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/en.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label': 'Cycle right sidebar tab',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'New session',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'New worktree draft',
+ 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'New Mini Chat window',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Open keyboard shortcuts',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': 'Toggle plan context panel',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': 'Toggle services menu',
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index cef075dd..c353d00c 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -1651,6 +1651,7 @@ export const dict = {
'commandPalette.empty.noResults': 'No results found.',
'commandPalette.empty.searchingFiles': 'Searching files...',
'commandPalette.item.newSession': 'New Session',
+ 'commandPalette.item.newMiniChat': 'New Mini Chat Window',
'commandPalette.item.newWorktreeDraft': 'New Worktree Draft',
'commandPalette.item.addProject': 'Add Project',
'commandPalette.item.showSessionSwitcher': 'Show Session Switcher',
@@ -1956,6 +1957,26 @@ export const dict = {
'desktopHostSwitcher.toast.sshConnected': 'SSH instance "{host}" connected',
'desktopHostSwitcher.toast.sshFailedToConnect': 'SSH instance "{host}" failed to connect',
'desktopHostSwitcher.toast.instanceUnreachable': 'Instance "{host}" is unreachable',
+ 'miniChat.header.newSession': 'New session',
+ 'miniChat.header.session': 'Session',
+ 'miniChat.header.defaultAgent': 'Default agent',
+ 'miniChat.header.noModel': 'No model',
+ 'miniChat.status.busy': 'Running',
+ 'miniChat.status.retry': 'Retrying',
+ 'miniChat.status.idle': 'Idle',
+ 'miniChat.actions.pin': 'Pin above other windows',
+ 'miniChat.actions.unpin': 'Unpin window',
+ 'miniChat.actions.pinAria': 'Pin Mini Chat window',
+ 'miniChat.actions.unpinAria': 'Unpin Mini Chat window',
+ 'miniChat.actions.openMain': 'Open in main window',
+ 'miniChat.actions.openMainAria': 'Open session in main window',
+ 'miniChat.unavailable.title': 'Session unavailable',
+ 'miniChat.unavailable.description': 'This session could not be loaded. It may have been deleted, archived, or opened from a different project context.',
+ 'sessions.sidebar.session.menu.openMiniChatWindow': 'Open in Mini Chat Window',
+ 'header.actions.newMiniChat': 'New Mini Chat Window',
+ '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',
'errorBoundary.title': 'Something went wrong',
'errorBoundary.description': 'The application encountered an unexpected error. This has been logged for debugging.',
'errorBoundary.state.unknownError': 'Unknown error',
diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts
index 305676b0..c2fd1527 100644
--- a/packages/ui/src/lib/i18n/messages/es.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/es.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label": "Cambiar pestaña de la barra lateral derecha",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nueva sesión",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Nuevo borrador de worktree",
+ "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nueva ventana Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atajos de teclado",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar panel de plan de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar u ocultar menú de servicios",
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index 0c795709..4ae14555 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -1617,6 +1617,7 @@ export const dict: Record = {
"commandPalette.empty.noResults": "No se encontraron resultados.",
"commandPalette.empty.searchingFiles": "Buscando archivos...",
"commandPalette.item.newSession": "Nueva sesión",
+ "commandPalette.item.newMiniChat": "Nueva ventana Mini Chat",
"commandPalette.item.newWorktreeDraft": "Nuevo borrador de worktree",
"commandPalette.item.addProject": "Añadir proyecto",
"commandPalette.item.showSessionSwitcher": "Mostrar cambiador de sesiones",
@@ -1922,6 +1923,26 @@ export const dict: Record = {
"desktopHostSwitcher.toast.sshConnected": "Instancia SSH \"{host}\" conectada",
"desktopHostSwitcher.toast.sshFailedToConnect": "No se pudo conectar con la instancia SSH \"{host}\"",
"desktopHostSwitcher.toast.instanceUnreachable": "Instancia \"{host}\" no está disponible",
+ "miniChat.header.newSession": "Nueva sesión",
+ "miniChat.header.session": "Sesión",
+ "miniChat.header.defaultAgent": "Agente predeterminado",
+ "miniChat.header.noModel": "Sin modelo",
+ "miniChat.status.busy": "En ejecución",
+ "miniChat.status.retry": "Reintentando",
+ "miniChat.status.idle": "Inactivo",
+ "miniChat.actions.pin": "Fijar sobre otras ventanas",
+ "miniChat.actions.unpin": "Desfijar ventana",
+ "miniChat.actions.pinAria": "Fijar ventana Mini Chat",
+ "miniChat.actions.unpinAria": "Desfijar ventana Mini Chat",
+ "miniChat.actions.openMain": "Abrir en la ventana principal",
+ "miniChat.actions.openMainAria": "Abrir sesión en la ventana principal",
+ "miniChat.unavailable.title": "Sesión no disponible",
+ "miniChat.unavailable.description": "No se pudo cargar esta sesión. Puede haber sido eliminada, archivada o abierta desde otro contexto de proyecto.",
+ "sessions.sidebar.session.menu.openMiniChatWindow": "Abrir en ventana Mini Chat",
+ "header.actions.newMiniChat": "Nueva ventana Mini Chat",
+ "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",
"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",
diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts
index 96f1b462..ddcede71 100644
--- a/packages/ui/src/lib/i18n/messages/ko.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label': '오른쪽 사이드바 탭 순환',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '새 세션',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '새 worktree 초안',
+ 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '새 Mini Chat 창',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '키보드 단축키 열기',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '계획 컨텍스트 패널 토글',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '서비스 메뉴 토글',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 0f61234d..dee46a18 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -1651,6 +1651,7 @@ export const dict: Record = {
'commandPalette.empty.noResults': '결과 없음',
'commandPalette.empty.searchingFiles': '파일 검색 중…',
'commandPalette.item.newSession': '새 세션',
+ 'commandPalette.item.newMiniChat': '새 Mini Chat 창',
'commandPalette.item.newWorktreeDraft': '새 워크트리 드래프트',
'commandPalette.item.addProject': '프로젝트 추가',
'commandPalette.item.showSessionSwitcher': '세션 전환기 표시',
@@ -1956,6 +1957,26 @@ export const dict: Record = {
'desktopHostSwitcher.toast.sshConnected': 'SSH 인스턴스 "{host}"에 연결했습니다',
'desktopHostSwitcher.toast.sshFailedToConnect': 'SSH 인스턴스 "{host}" 연결에 실패했습니다',
'desktopHostSwitcher.toast.instanceUnreachable': '인스턴스 "{host}"에 연결할 수 없음',
+ 'miniChat.header.newSession': '새 세션',
+ 'miniChat.header.session': '세션',
+ 'miniChat.header.defaultAgent': '기본 에이전트',
+ 'miniChat.header.noModel': '모델 없음',
+ 'miniChat.status.busy': '실행 중',
+ 'miniChat.status.retry': '재시도 중',
+ 'miniChat.status.idle': '유휴',
+ 'miniChat.actions.pin': '다른 창 위에 고정',
+ 'miniChat.actions.unpin': '창 고정 해제',
+ 'miniChat.actions.pinAria': 'Mini Chat 창 고정',
+ 'miniChat.actions.unpinAria': 'Mini Chat 창 고정 해제',
+ 'miniChat.actions.openMain': '메인 창에서 열기',
+ 'miniChat.actions.openMainAria': '메인 창에서 세션 열기',
+ 'miniChat.unavailable.title': '세션을 사용할 수 없음',
+ 'miniChat.unavailable.description': '이 세션을 불러올 수 없습니다. 삭제, 보관되었거나 다른 프로젝트 컨텍스트에서 열렸을 수 있습니다.',
+ 'sessions.sidebar.session.menu.openMiniChatWindow': 'Mini Chat 창에서 열기',
+ 'header.actions.newMiniChat': '새 Mini Chat 창',
+ 'header.actions.newMiniChatAria': '새 Mini Chat 창 열기',
+ 'header.actions.openSessionMiniChat': 'Mini Chat에서 세션 열기',
+ 'header.actions.openSessionMiniChatAria': '현재 세션을 Mini Chat에서 열기',
'errorBoundary.title': '문제가 발생했습니다',
'errorBoundary.description': '애플리케이션에서 예상치 못한 오류가 발생했습니다. 디버깅을 위해 기록되었습니다.',
'errorBoundary.state.unknownError': '알 수 없음 오류',
diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts
index cef8d1c9..50d82870 100644
--- a/packages/ui/src/lib/i18n/messages/pl.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts
@@ -602,6 +602,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Skup pole wprowadzania',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': 'Nowa sesja',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': 'Nowy szkic obszaru roboczego',
+ 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': 'Nowe okno Mini Chat',
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Otwórz paletę poleceń',
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Przejdź do linii (edytor plików)',
'settings.openchamber.keyboardShortcuts.action.open_help.label': 'Otwórz skróty klawiszowe',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index 848834b5..9ac6c423 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -521,6 +521,26 @@ export const dict: Record = {
'chat.messageBody.toast.planSaved': 'Plan zapisany',
'chat.messageBody.toast.imageSaved': 'Obraz zapisany',
'chat.messageBody.toast.generateImageFailed': 'Nie udało się wygenerować obrazu',
+ 'miniChat.header.newSession': 'Nowa sesja',
+ 'miniChat.header.session': 'Sesja',
+ 'miniChat.header.defaultAgent': 'Domyślny agent',
+ 'miniChat.header.noModel': 'Brak modelu',
+ 'miniChat.status.busy': 'Działa',
+ 'miniChat.status.retry': 'Ponawianie',
+ 'miniChat.status.idle': 'Bezczynny',
+ 'miniChat.actions.pin': 'Przypnij nad innymi oknami',
+ 'miniChat.actions.unpin': 'Odepnij okno',
+ 'miniChat.actions.pinAria': 'Przypnij okno Mini Chat',
+ 'miniChat.actions.unpinAria': 'Odepnij okno Mini Chat',
+ 'miniChat.actions.openMain': 'Otwórz w głównym oknie',
+ 'miniChat.actions.openMainAria': 'Otwórz sesję w głównym oknie',
+ 'miniChat.unavailable.title': 'Sesja niedostępna',
+ 'miniChat.unavailable.description': 'Nie udało się załadować tej sesji. Mogła zostać usunięta, zarchiwizowana albo otwarta z innego kontekstu projektu.',
+ 'sessions.sidebar.session.menu.openMiniChatWindow': 'Otwórz w oknie Mini Chat',
+ 'header.actions.newMiniChat': 'Nowe okno Mini Chat',
+ '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',
'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',
@@ -961,6 +981,7 @@ export const dict: Record = {
'commandPalette.empty.searchingFiles': 'Wyszukiwanie plików...',
'commandPalette.input.placeholder': 'Szukaj plików, sesji i poleceń...',
'commandPalette.item.newSession': 'Nowa sesja',
+ 'commandPalette.item.newMiniChat': 'Nowe okno Mini Chat',
'commandPalette.item.newWorktreeDraft': 'Nowy szkic drzewa pracy',
'commandPalette.item.addProject': 'Dodaj projekt',
'commandPalette.item.openSettings': 'Otwórz ustawienia...',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
index 6d2b912d..c5f37d7f 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label": "Alternar aba da barra lateral direita",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Nova sessão",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Novo rascunho de worktree",
+ "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Nova janela Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Abrir atalhos de teclado",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Alternar painel de plano de contexto",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Mostrar ou ocultar menu de serviços",
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index 91477e6d..366a1262 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -1617,6 +1617,7 @@ export const dict: Record = {
"commandPalette.empty.noResults": "Nenhum resultado encontrado.",
"commandPalette.empty.searchingFiles": "Buscando arquivos...",
"commandPalette.item.newSession": "Nova sessão",
+ "commandPalette.item.newMiniChat": "Nova janela Mini Chat",
"commandPalette.item.newWorktreeDraft": "Novo rascunho de worktree",
"commandPalette.item.addProject": "Adicionar projeto",
"commandPalette.item.showSessionSwitcher": "Mostrar seletor de sessões",
@@ -1922,6 +1923,26 @@ export const dict: Record = {
"desktopHostSwitcher.toast.sshConnected": "Instância SSH \"{host}\" conectada",
"desktopHostSwitcher.toast.sshFailedToConnect": "Não foi possível conectar com a instância SSH \"{host}\"",
"desktopHostSwitcher.toast.instanceUnreachable": "Instância \"{host}\" não está disponível",
+ "miniChat.header.newSession": "Nova sessão",
+ "miniChat.header.session": "Sessão",
+ "miniChat.header.defaultAgent": "Agente padrão",
+ "miniChat.header.noModel": "Sem modelo",
+ "miniChat.status.busy": "Executando",
+ "miniChat.status.retry": "Tentando novamente",
+ "miniChat.status.idle": "Ocioso",
+ "miniChat.actions.pin": "Fixar acima de outras janelas",
+ "miniChat.actions.unpin": "Desafixar janela",
+ "miniChat.actions.pinAria": "Fixar janela Mini Chat",
+ "miniChat.actions.unpinAria": "Desafixar janela Mini Chat",
+ "miniChat.actions.openMain": "Abrir na janela principal",
+ "miniChat.actions.openMainAria": "Abrir sessão na janela principal",
+ "miniChat.unavailable.title": "Sessão indisponível",
+ "miniChat.unavailable.description": "Não foi possível carregar esta sessão. Ela pode ter sido excluída, arquivada ou aberta a partir de outro contexto de projeto.",
+ "sessions.sidebar.session.menu.openMiniChatWindow": "Abrir na janela Mini Chat",
+ "header.actions.newMiniChat": "Nova janela Mini Chat",
+ "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",
"errorBoundary.title": "Algo deu errado",
"errorBoundary.description": "O aplicativo encontrou um erro inesperado. Isso foi registrado para depuração.",
"errorBoundary.state.unknownError": "Erro desconhecido",
diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts
index 8c8c24e2..5655c138 100644
--- a/packages/ui/src/lib/i18n/messages/uk.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
"settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label": "Перемкнути вкладку правої бічної панелі",
"settings.openchamber.keyboardShortcuts.action.new_chat.label": "Нова сесія",
"settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label": "Нова чернетка worktree",
+ "settings.openchamber.keyboardShortcuts.action.new_mini_chat.label": "Нове вікно Mini Chat",
"settings.openchamber.keyboardShortcuts.action.open_help.label": "Відкрити комбінації клавіш",
"settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label": "Перемкнути контекстну панель плану",
"settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label": "Перемкнути меню сервісів",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index 5d465875..23bd7f04 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -1617,6 +1617,7 @@ export const dict: Record = {
"commandPalette.empty.noResults": "Результатів не знайдено.",
"commandPalette.empty.searchingFiles": "Пошук файлів...",
"commandPalette.item.newSession": "Нова сесія",
+ "commandPalette.item.newMiniChat": "Нове вікно Mini Chat",
"commandPalette.item.newWorktreeDraft": "Чернетка нового worktree",
"commandPalette.item.addProject": "Додати проєкт",
"commandPalette.item.showSessionSwitcher": "Показати перемикач сесій",
@@ -1922,6 +1923,26 @@ export const dict: Record = {
"desktopHostSwitcher.toast.sshConnected": "Інстанс SSH \"{host}\" підключено",
"desktopHostSwitcher.toast.sshFailedToConnect": "Не вдалося підключитися до інстанса SSH \"{host}\".",
"desktopHostSwitcher.toast.instanceUnreachable": "Інстанс \"{host}\" недоступний",
+ "miniChat.header.newSession": "Нова сесія",
+ "miniChat.header.session": "Сесія",
+ "miniChat.header.defaultAgent": "Агент за замовчуванням",
+ "miniChat.header.noModel": "Модель не вибрано",
+ "miniChat.status.busy": "Виконується",
+ "miniChat.status.retry": "Повторна спроба",
+ "miniChat.status.idle": "Очікує",
+ "miniChat.actions.pin": "Закріпити над іншими вікнами",
+ "miniChat.actions.unpin": "Відкріпити вікно",
+ "miniChat.actions.pinAria": "Закріпити вікно Mini Chat",
+ "miniChat.actions.unpinAria": "Відкріпити вікно Mini Chat",
+ "miniChat.actions.openMain": "Відкрити в головному вікні",
+ "miniChat.actions.openMainAria": "Відкрити сесію в головному вікні",
+ "miniChat.unavailable.title": "Сесія недоступна",
+ "miniChat.unavailable.description": "Не вдалося завантажити цю сесію. Її могли видалити, заархівувати або відкрити з іншого контексту проєкту.",
+ "sessions.sidebar.session.menu.openMiniChatWindow": "Відкрити у вікні Mini Chat",
+ "header.actions.newMiniChat": "Нове вікно Mini Chat",
+ "header.actions.newMiniChatAria": "Відкрити нове вікно Mini Chat",
+ "header.actions.openSessionMiniChat": "Відкрити сесію в Mini Chat",
+ "header.actions.openSessionMiniChatAria": "Відкрити поточну сесію в Mini Chat",
"errorBoundary.title": "Щось пішло не так",
"errorBoundary.description": "У програмі сталася неочікувана помилка. Це було зареєстровано для налагодження.",
"errorBoundary.state.unknownError": "Невідома помилка",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
index fbdba8e2..34bdc06f 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts
@@ -753,6 +753,7 @@ export const settingsDict = {
'settings.openchamber.keyboardShortcuts.action.cycle_right_sidebar_tab.label': '轮换右侧边栏标签',
'settings.openchamber.keyboardShortcuts.action.new_chat.label': '新建会话',
'settings.openchamber.keyboardShortcuts.action.new_chat_worktree.label': '新建工作树草稿',
+ 'settings.openchamber.keyboardShortcuts.action.new_mini_chat.label': '新建 Mini Chat 窗口',
'settings.openchamber.keyboardShortcuts.action.open_help.label': '打开键盘快捷键',
'settings.openchamber.keyboardShortcuts.action.toggle_context_plan.label': '切换上下文面板中的计划',
'settings.openchamber.keyboardShortcuts.action.toggle_services_menu.label': '切换服务菜单',
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index 245b30cd..e121369b 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -1617,6 +1617,7 @@ export const dict: Record = {
'commandPalette.empty.noResults': '未找到结果。',
'commandPalette.empty.searchingFiles': '正在搜索文件...',
'commandPalette.item.newSession': '新建会话',
+ 'commandPalette.item.newMiniChat': '新建 Mini Chat 窗口',
'commandPalette.item.newWorktreeDraft': '新建工作树草稿',
'commandPalette.item.addProject': '添加项目',
'commandPalette.item.showSessionSwitcher': '显示会话切换器',
@@ -1922,6 +1923,26 @@ export const dict: Record = {
'desktopHostSwitcher.toast.sshConnected': 'SSH 实例“{host}”已连接',
'desktopHostSwitcher.toast.sshFailedToConnect': 'SSH 实例“{host}”连接失败',
'desktopHostSwitcher.toast.instanceUnreachable': '实例“{host}”不可达',
+ 'miniChat.header.newSession': '新会话',
+ 'miniChat.header.session': '会话',
+ 'miniChat.header.defaultAgent': '默认智能体',
+ 'miniChat.header.noModel': '未选择模型',
+ 'miniChat.status.busy': '运行中',
+ 'miniChat.status.retry': '重试中',
+ 'miniChat.status.idle': '空闲',
+ 'miniChat.actions.pin': '固定在其他窗口上方',
+ 'miniChat.actions.unpin': '取消固定窗口',
+ 'miniChat.actions.pinAria': '固定 Mini Chat 窗口',
+ 'miniChat.actions.unpinAria': '取消固定 Mini Chat 窗口',
+ 'miniChat.actions.openMain': '在主窗口中打开',
+ 'miniChat.actions.openMainAria': '在主窗口中打开会话',
+ 'miniChat.unavailable.title': '会话不可用',
+ 'miniChat.unavailable.description': '无法加载此会话。它可能已被删除、归档,或从其他项目上下文打开。',
+ 'sessions.sidebar.session.menu.openMiniChatWindow': '在 Mini Chat 窗口中打开',
+ 'header.actions.newMiniChat': '新建 Mini Chat 窗口',
+ 'header.actions.newMiniChatAria': '打开新的 Mini Chat 窗口',
+ 'header.actions.openSessionMiniChat': '在 Mini Chat 中打开会话',
+ 'header.actions.openSessionMiniChatAria': '在 Mini Chat 中打开当前会话',
'errorBoundary.title': '发生错误',
'errorBoundary.description': '应用遇到意外错误,已记录用于调试。',
'errorBoundary.state.unknownError': '未知错误',
diff --git a/packages/ui/src/lib/shortcuts.ts b/packages/ui/src/lib/shortcuts.ts
index 1cfd764b..813cae46 100644
--- a/packages/ui/src/lib/shortcuts.ts
+++ b/packages/ui/src/lib/shortcuts.ts
@@ -214,6 +214,12 @@ const SHORTCUT_ACTIONS: ReadonlyArray = [
description: 'Create a new worktree and open a draft in it',
customizable: true,
},
+ {
+ id: 'new_mini_chat',
+ defaultCombo: 'mod+alt+n',
+ label: 'New Mini Chat window',
+ description: 'Open a new Mini Chat draft window',
+ },
{
id: 'submit_message',
defaultCombo: 'mod+enter',
diff --git a/packages/ui/src/sync/sync-context.tsx b/packages/ui/src/sync/sync-context.tsx
index 428b6e3b..0d63ac70 100644
--- a/packages/ui/src/sync/sync-context.tsx
+++ b/packages/ui/src/sync/sync-context.tsx
@@ -213,6 +213,18 @@ async function materializeSessionFromServer(
// Used to determine if user is currently viewing the session when a notification arrives.
let _activeDirectory = ""
let _activeSession = ""
+const externallyViewedSessions = new Map()
+const EXTERNAL_VIEW_TTL_MS = 15_000
+
+const viewedSessionKey = (directory: string, sessionId: string) => `${directory}\n${sessionId}`
+
+function pruneExternallyViewedSessions(now = Date.now()) {
+ for (const [key, expiresAt] of externallyViewedSessions.entries()) {
+ if (expiresAt <= now) {
+ externallyViewedSessions.delete(key)
+ }
+ }
+}
const pendingQuestionToastIds = new Set()
const pendingPermissionToastIds = new Set()
@@ -239,10 +251,21 @@ export function setActiveSession(directory: string, sessionId: string) {
_activeSession = sessionId
}
+export function setExternallyViewedSession(directory: string, sessionId: string, viewed: boolean) {
+ if (!directory || !sessionId) return
+ const key = viewedSessionKey(directory, sessionId)
+ if (!viewed) {
+ externallyViewedSessions.delete(key)
+ return
+ }
+ externallyViewedSessions.set(key, Date.now() + EXTERNAL_VIEW_TTL_MS)
+}
+
function isViewedInCurrentSession(directory: string, sessionId?: string): boolean {
- if (!_activeDirectory || !_activeSession || !sessionId) return false
- if (directory !== _activeDirectory) return false
- return sessionId === _activeSession
+ if (!sessionId) return false
+ if (_activeDirectory && _activeSession && directory === _activeDirectory && sessionId === _activeSession) return true
+ pruneExternallyViewedSessions()
+ return externallyViewedSessions.has(viewedSessionKey(directory, sessionId))
}
function isRecentBoot() {
diff --git a/packages/web/mini-chat.html b/packages/web/mini-chat.html
new file mode 100644
index 00000000..ee84d145
--- /dev/null
+++ b/packages/web/mini-chat.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ OpenChamber Mini Chat
+
+
+
+
+
+
diff --git a/packages/web/src/mini-chat-main.tsx b/packages/web/src/mini-chat-main.tsx
new file mode 100644
index 00000000..590f7b72
--- /dev/null
+++ b/packages/web/src/mini-chat-main.tsx
@@ -0,0 +1,17 @@
+import { createWebAPIs } from './api';
+import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
+import '@openchamber/ui/index.css';
+import '@openchamber/ui/styles/fonts';
+
+declare global {
+ interface Window {
+ __OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
+ }
+}
+
+window.__OPENCHAMBER_RUNTIME_APIS__ = createWebAPIs();
+
+void import('@openchamber/ui/apps/renderElectronMiniChatApp')
+ .then(({ renderElectronMiniChatApp }) => {
+ renderElectronMiniChatApp(window.__OPENCHAMBER_RUNTIME_APIS__ ?? createWebAPIs());
+ });
diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts
index 27560c95..5bfb3cee 100644
--- a/packages/web/vite.config.ts
+++ b/packages/web/vite.config.ts
@@ -101,6 +101,10 @@ export default defineConfig({
emptyOutDir: true,
chunkSizeWarningLimit: 500,
rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'index.html'),
+ miniChat: path.resolve(__dirname, 'mini-chat.html'),
+ },
external: ['node:child_process', 'node:fs', 'node:path', 'node:url'],
output: {
manualChunks(id) {