feat(chat): delay session creation until first message
The app now opens to a new chat by default on startup New chats no longer create a session until you send your first message Fixed mobile and VSCode sessions handling for better consistency
This commit is contained in:
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- New chats no longer create a session until you send your first message.
|
||||||
|
- The app opens to a new chat by default.
|
||||||
|
- Fixed mobile and VSCode sessions handling
|
||||||
|
|
||||||
|
|
||||||
## [1.3.0] - 2025-12-21
|
## [1.3.0] - 2025-12-21
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ type AppProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App({ apis }: AppProps) {
|
function App({ apis }: AppProps) {
|
||||||
const { initializeApp, loadProviders, isInitialized } = useConfigStore();
|
const { initializeApp, isInitialized } = useConfigStore();
|
||||||
const { error, clearError, loadSessions } = useSessionStore();
|
const { error, clearError, loadSessions } = useSessionStore();
|
||||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||||
const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
|
const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory);
|
||||||
@@ -120,11 +120,10 @@ function App({ apis }: AppProps) {
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await initializeApp();
|
await initializeApp();
|
||||||
await loadProviders();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
}, [initializeApp, loadProviders]);
|
}, [initializeApp]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isSwitchingDirectory) {
|
if (isSwitchingDirectory) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const ChatContainer: React.FC = () => {
|
|||||||
messageStreamStates,
|
messageStreamStates,
|
||||||
trimToViewportWindow,
|
trimToViewportWindow,
|
||||||
sessionActivityPhase,
|
sessionActivityPhase,
|
||||||
|
newSessionDraft,
|
||||||
} = useSessionStore();
|
} = useSessionStore();
|
||||||
|
|
||||||
const streamingMessageId = React.useMemo(() => {
|
const streamingMessageId = React.useMemo(() => {
|
||||||
@@ -35,6 +36,7 @@ export const ChatContainer: React.FC = () => {
|
|||||||
}, [currentSessionId, streamingMessageIds]);
|
}, [currentSessionId, streamingMessageIds]);
|
||||||
|
|
||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
|
const draftOpen = Boolean(newSessionDraft?.open);
|
||||||
|
|
||||||
const sessionMessages = React.useMemo(() => {
|
const sessionMessages = React.useMemo(() => {
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ export const ChatContainer: React.FC = () => {
|
|||||||
void load();
|
void load();
|
||||||
}, [currentSessionId, loadMessages, messages, scrollToBottom]);
|
}, [currentSessionId, loadMessages, messages, scrollToBottom]);
|
||||||
|
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId && !draftOpen) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col h-full bg-background"
|
className="flex flex-col h-full bg-background"
|
||||||
@@ -156,6 +158,26 @@ export const ChatContainer: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentSessionId && draftOpen) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col h-full bg-background transform-gpu"
|
||||||
|
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
|
||||||
|
>
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<OpenChamberLogo width={140} height={140} className="opacity-20" isAnimated />
|
||||||
|
</div>
|
||||||
|
<div className="relative bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 z-10">
|
||||||
|
<ChatInput scrollToBottom={scrollToBottom} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentSessionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading && sessionMessages.length === 0 && !streamingMessageId) {
|
if (isLoading && sessionMessages.length === 0 && !streamingMessageId) {
|
||||||
const hasMessagesEntry = messages.has(currentSessionId);
|
const hasMessagesEntry = messages.has(currentSessionId);
|
||||||
if (!hasMessagesEntry) {
|
if (!hasMessagesEntry) {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
|
|
||||||
const sendMessage = useSessionStore((state) => state.sendMessage);
|
const sendMessage = useSessionStore((state) => state.sendMessage);
|
||||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
|
const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
|
||||||
const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation);
|
const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation);
|
||||||
const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort);
|
const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort);
|
||||||
const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId);
|
const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId);
|
||||||
@@ -229,7 +230,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
const handleSubmit = async (e?: React.FormEvent) => {
|
const handleSubmit = async (e?: React.FormEvent) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
|
||||||
if (!hasContent || !currentSessionId) return;
|
if (!hasContent || (!currentSessionId && !newSessionDraftOpen)) return;
|
||||||
|
|
||||||
const messageToSend = message.replace(/^\n+|\n+$/g, '');
|
const messageToSend = message.replace(/^\n+|\n+$/g, '');
|
||||||
|
|
||||||
@@ -542,7 +543,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentSessionId) {
|
if (!currentSessionId && !newSessionDraftOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +573,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
if (attachedCount > 0) {
|
if (attachedCount > 0) {
|
||||||
toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`);
|
toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`);
|
||||||
}
|
}
|
||||||
}, [addAttachedFile, currentSessionId, insertTextAtSelection]);
|
}, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]);
|
||||||
|
|
||||||
const handleFileSelect = (file: { name: string; path: string }) => {
|
const handleFileSelect = (file: { name: string; path: string }) => {
|
||||||
|
|
||||||
@@ -660,7 +661,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (currentSessionId && !isDragging) {
|
if ((currentSessionId || newSessionDraftOpen) && !isDragging) {
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -678,7 +679,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
if (!currentSessionId) return;
|
if (!currentSessionId && !newSessionDraftOpen) return;
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
const files = Array.from(e.dataTransfer.files);
|
||||||
let attachedCount = 0;
|
let attachedCount = 0;
|
||||||
@@ -838,13 +839,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type={isMobile ? 'button' : 'submit'}
|
type={isMobile ? 'button' : 'submit'}
|
||||||
disabled={!hasContent || !currentSessionId}
|
disabled={!hasContent || (!currentSessionId && !newSessionDraftOpen)}
|
||||||
onPointerDownCapture={(event) => {
|
onPointerDownCapture={(event) => {
|
||||||
if (!isMobile || event.pointerType !== 'touch') {
|
if (!isMobile || event.pointerType !== 'touch') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContent || !currentSessionId) {
|
if (!hasContent || (!currentSessionId && !newSessionDraftOpen)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,7 +869,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
iconButtonBaseClass,
|
iconButtonBaseClass,
|
||||||
hasContent && currentSessionId
|
hasContent && (currentSessionId || newSessionDraftOpen)
|
||||||
? 'text-primary hover:text-primary'
|
? 'text-primary hover:text-primary'
|
||||||
: 'opacity-30'
|
: 'opacity-30'
|
||||||
)}
|
)}
|
||||||
@@ -1080,8 +1081,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onPointerDownCapture={handleTextareaPointerDownCapture}
|
onPointerDownCapture={handleTextareaPointerDownCapture}
|
||||||
placeholder={currentSessionId ? "# for agents; @ for files; / for commands" : "Select or create a session to start chatting"}
|
placeholder={currentSessionId
|
||||||
disabled={!currentSessionId}
|
? "# for agents; @ for files; / for commands"
|
||||||
|
: newSessionDraftOpen
|
||||||
|
? "Type your first message..."
|
||||||
|
: "Select or create a session to start chatting"}
|
||||||
|
disabled={!currentSessionId && !newSessionDraftOpen}
|
||||||
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-t-xl rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
'min-h-[52px] resize-none border-0 px-3 shadow-none rounded-t-xl rounded-b-none appearance-none focus:shadow-none focus-visible:shadow-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:ring-transparent hover:border-transparent bg-transparent',
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import { SettingsPage } from '@/components/sections/settings/SettingsPage';
|
|||||||
type VSCodeView = 'sessions' | 'chat' | 'settings';
|
type VSCodeView = 'sessions' | 'chat' | 'settings';
|
||||||
|
|
||||||
export const VSCodeLayout: React.FC = () => {
|
export const VSCodeLayout: React.FC = () => {
|
||||||
const [currentView, setCurrentView] = React.useState<VSCodeView>('sessions');
|
const [currentView, setCurrentView] = React.useState<VSCodeView>('chat');
|
||||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
const sessions = useSessionStore((state) => state.sessions);
|
const sessions = useSessionStore((state) => state.sessions);
|
||||||
const createSession = useSessionStore((state) => state.createSession);
|
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
||||||
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
||||||
const [connectionStatus, setConnectionStatus] = React.useState<'connecting' | 'connected' | 'error' | 'disconnected'>(
|
const [connectionStatus, setConnectionStatus] = React.useState<'connecting' | 'connected' | 'error' | 'disconnected'>(
|
||||||
() => (typeof window !== 'undefined'
|
() => (typeof window !== 'undefined'
|
||||||
? (window as { __OPENCHAMBER_CONNECTION__?: { status?: string } }).__OPENCHAMBER_CONNECTION__?.status as
|
? (window as { __OPENCHAMBER_CONNECTION__?: { status?: string } }).__OPENCHAMBER_CONNECTION__?.status as
|
||||||
@@ -38,8 +38,6 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
const messages = useSessionStore((state) => state.messages);
|
const messages = useSessionStore((state) => state.messages);
|
||||||
const [hasInitializedOnce, setHasInitializedOnce] = React.useState<boolean>(() => configInitialized);
|
const [hasInitializedOnce, setHasInitializedOnce] = React.useState<boolean>(() => configInitialized);
|
||||||
const [isInitializing, setIsInitializing] = React.useState<boolean>(false);
|
const [isInitializing, setIsInitializing] = React.useState<boolean>(false);
|
||||||
const autoSelectedRef = React.useRef<boolean>(false);
|
|
||||||
const startedFreshSessionRef = React.useRef<boolean>(false);
|
|
||||||
|
|
||||||
// Navigate to chat when a session is selected
|
// Navigate to chat when a session is selected
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -50,21 +48,19 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
|
|
||||||
// If the active session disappears (e.g., deleted), stay on the sessions list
|
// If the active session disappears (e.g., deleted), stay on the sessions list
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentSessionId && currentView === 'chat') {
|
if (!currentSessionId && !newSessionDraftOpen && currentView === 'chat') {
|
||||||
setCurrentView('sessions');
|
setCurrentView('sessions');
|
||||||
}
|
}
|
||||||
}, [currentSessionId, currentView]);
|
}, [currentSessionId, currentView, newSessionDraftOpen]);
|
||||||
|
|
||||||
const handleBackToSessions = React.useCallback(() => {
|
const handleBackToSessions = React.useCallback(() => {
|
||||||
setCurrentView('sessions');
|
setCurrentView('sessions');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleNewSession = React.useCallback(async () => {
|
const handleNewSession = React.useCallback(() => {
|
||||||
const result = await createSession();
|
openNewSessionDraft();
|
||||||
if (result?.id) {
|
setCurrentView('chat');
|
||||||
setCurrentView('chat');
|
}, [openNewSessionDraft]);
|
||||||
}
|
|
||||||
}, [createSession]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handler = (event: Event) => {
|
const handler = (event: Event) => {
|
||||||
@@ -137,65 +133,26 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const hydrateMessages = async () => {
|
const hydrateMessages = async () => {
|
||||||
if (!hasInitializedOnce || connectionStatus !== 'connected' || currentView !== 'chat') {
|
if (!hasInitializedOnce || connectionStatus !== 'connected' || currentView !== 'chat' || newSessionDraftOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targetSessionId = currentSessionId || sessions[0]?.id;
|
|
||||||
if (!targetSessionId) return;
|
|
||||||
|
|
||||||
const hasMessages = messages.has(targetSessionId) && (messages.get(targetSessionId)?.length || 0) > 0;
|
if (!currentSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMessages = messages.has(currentSessionId) && (messages.get(currentSessionId)?.length || 0) > 0;
|
||||||
if (!hasMessages) {
|
if (!hasMessages) {
|
||||||
if (!currentSessionId) {
|
|
||||||
setCurrentSession(targetSessionId);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await loadMessages(targetSessionId);
|
await loadMessages(currentSessionId);
|
||||||
} catch { /* ignored */ }
|
} catch { /* ignored */ }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void hydrateMessages();
|
void hydrateMessages();
|
||||||
}, [connectionStatus, currentSessionId, currentView, hasInitializedOnce, loadMessages, messages, sessions, setCurrentSession]);
|
}, [connectionStatus, currentSessionId, currentView, hasInitializedOnce, loadMessages, messages, newSessionDraftOpen]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!hasInitializedOnce || autoSelectedRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currentSessionId && sessions.length > 0) {
|
|
||||||
setCurrentSession(sessions[0].id);
|
|
||||||
autoSelectedRef.current = true;
|
|
||||||
}
|
|
||||||
}, [currentSessionId, hasInitializedOnce, sessions, setCurrentSession]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const ensureFreshSession = async () => {
|
|
||||||
if (connectionStatus !== 'connected' || !hasInitializedOnce || startedFreshSessionRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = sessions.find((s) => s.id === currentSessionId);
|
|
||||||
const isCurrentPlaceholder = current?.title?.toLowerCase()?.startsWith('new session');
|
|
||||||
|
|
||||||
if (current && isCurrentPlaceholder) {
|
|
||||||
setCurrentSession(current.id);
|
|
||||||
} else {
|
|
||||||
// Look for an existing empty session to reuse before creating a new one
|
|
||||||
const reusableSession = sessions.find((s) => s.title?.toLowerCase()?.startsWith('new session'));
|
|
||||||
|
|
||||||
if (reusableSession) {
|
|
||||||
setCurrentSession(reusableSession.id);
|
|
||||||
} else {
|
|
||||||
const newSession = await createSession();
|
|
||||||
if (newSession?.id) {
|
|
||||||
setCurrentSession(newSession.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startedFreshSessionRef.current = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
void ensureFreshSession();
|
|
||||||
}, [connectionStatus, createSession, currentSessionId, hasInitializedOnce, sessions, setCurrentSession]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-background text-foreground flex flex-col">
|
<div className="h-full w-full bg-background text-foreground flex flex-col">
|
||||||
@@ -229,7 +186,9 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<VSCodeHeader
|
<VSCodeHeader
|
||||||
title={sessions.find(s => s.id === currentSessionId)?.title || 'Chat'}
|
title={newSessionDraftOpen && !currentSessionId
|
||||||
|
? 'New session'
|
||||||
|
: sessions.find(s => s.id === currentSessionId)?.title || 'Chat'}
|
||||||
showBack
|
showBack
|
||||||
onBack={handleBackToSessions}
|
onBack={handleBackToSessions}
|
||||||
showContextUsage
|
showContextUsage
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { sessionEvents } from '@/lib/sessionEvents';
|
|||||||
import { formatDirectoryName, formatPathForDisplay, cn } from '@/lib/utils';
|
import { formatDirectoryName, formatPathForDisplay, cn } from '@/lib/utils';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import type { WorktreeMetadata } from '@/types/worktree';
|
import type { WorktreeMetadata } from '@/types/worktree';
|
||||||
import { opencodeClient } from '@/lib/opencode/client';
|
import { opencodeClient } from '@/lib/opencode/client';
|
||||||
import { checkIsGitRepository } from '@/lib/gitApi';
|
import { checkIsGitRepository } from '@/lib/gitApi';
|
||||||
@@ -126,7 +126,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
);
|
);
|
||||||
const checkingDirectories = React.useRef<Set<string>>(new Set());
|
const checkingDirectories = React.useRef<Set<string>>(new Set());
|
||||||
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
const safeStorage = React.useMemo(() => getSafeStorage(), []);
|
||||||
const [isCreatingSession, setIsCreatingSession] = React.useState(false);
|
|
||||||
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
|
const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
|
||||||
const [isGitRepo, setIsGitRepo] = React.useState<boolean | null>(null);
|
const [isGitRepo, setIsGitRepo] = React.useState<boolean | null>(null);
|
||||||
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
const [expandedSessionGroups, setExpandedSessionGroups] = React.useState<Set<string>>(new Set());
|
||||||
@@ -136,7 +135,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||||
const setDirectory = useDirectoryStore((state) => state.setDirectory);
|
const setDirectory = useDirectoryStore((state) => state.setDirectory);
|
||||||
|
|
||||||
const agents = useConfigStore((state) => state.agents);
|
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||||
|
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
|
||||||
|
|
||||||
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
const getSessionsByDirectory = useSessionStore((state) => state.getSessionsByDirectory);
|
||||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
@@ -148,9 +148,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
const sessionActivityPhase = useSessionStore((state) => state.sessionActivityPhase);
|
const sessionActivityPhase = useSessionStore((state) => state.sessionActivityPhase);
|
||||||
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
|
const worktreeMetadata = useSessionStore((state) => state.worktreeMetadata);
|
||||||
const availableWorktrees = useSessionStore((state) => state.availableWorktrees);
|
const availableWorktrees = useSessionStore((state) => state.availableWorktrees);
|
||||||
const createSession = useSessionStore((state) => state.createSession);
|
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
||||||
const initializeNewOpenChamberSession = useSessionStore((state) => state.initializeNewOpenChamberSession);
|
|
||||||
const setSessionDirectory = useSessionStore((state) => state.setSessionDirectory);
|
|
||||||
|
|
||||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -342,6 +340,12 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mobileVariant) {
|
||||||
|
setActiveMainTab('chat');
|
||||||
|
setSessionSwitcherOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!allowReselect && sessionId === currentSessionId) {
|
if (!allowReselect && sessionId === currentSessionId) {
|
||||||
onSessionSelected?.(sessionId);
|
onSessionSelected?.(sessionId);
|
||||||
return;
|
return;
|
||||||
@@ -349,7 +353,15 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
setCurrentSession(sessionId);
|
setCurrentSession(sessionId);
|
||||||
onSessionSelected?.(sessionId);
|
onSessionSelected?.(sessionId);
|
||||||
},
|
},
|
||||||
[allowReselect, currentSessionId, onSessionSelected, setCurrentSession],
|
[
|
||||||
|
allowReselect,
|
||||||
|
currentSessionId,
|
||||||
|
mobileVariant,
|
||||||
|
onSessionSelected,
|
||||||
|
setActiveMainTab,
|
||||||
|
setCurrentSession,
|
||||||
|
setSessionSwitcherOpen,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveEdit = React.useCallback(async () => {
|
const handleSaveEdit = React.useCallback(async () => {
|
||||||
@@ -455,31 +467,14 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateSessionInGroup = React.useCallback(
|
const handleCreateSessionInGroup = React.useCallback(
|
||||||
async (directory: string | null) => {
|
(directory: string | null) => {
|
||||||
if (!directory) {
|
setActiveMainTab('chat');
|
||||||
toast.error('No directory available for session creation');
|
if (mobileVariant) {
|
||||||
return;
|
setSessionSwitcherOpen(false);
|
||||||
}
|
|
||||||
if (isCreatingSession) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
setIsCreatingSession(true);
|
|
||||||
const session = await createSession(undefined, directory);
|
|
||||||
if (!session) {
|
|
||||||
toast.error('Failed to create session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initializeNewOpenChamberSession(session.id, agents);
|
|
||||||
setSessionDirectory(session.id, directory);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Failed to create session';
|
|
||||||
toast.error(message);
|
|
||||||
} finally {
|
|
||||||
setIsCreatingSession(false);
|
|
||||||
}
|
}
|
||||||
|
openNewSessionDraft({ directoryOverride: directory ?? null });
|
||||||
},
|
},
|
||||||
[createSession, initializeNewOpenChamberSession, setSessionDirectory, agents, isCreatingSession],
|
[openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen, mobileVariant],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenWorktreeManager = React.useCallback(() => {
|
const handleOpenWorktreeManager = React.useCallback(() => {
|
||||||
@@ -1016,13 +1011,46 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
emptyState
|
emptyState
|
||||||
) : hideDirectoryControls && groupedSessions.length === 1 && groupedSessions[0].isMain ? (
|
) : hideDirectoryControls && groupedSessions.length === 1 && groupedSessions[0].isMain ? (
|
||||||
<div className="space-y-[0.6rem] py-1">
|
<div className="space-y-[0.6rem] py-1">
|
||||||
{groupedSessions[0].sessions.length === 0 ? (
|
{(() => {
|
||||||
<div className="py-1 text-left typography-micro text-muted-foreground">
|
const group = groupedSessions[0];
|
||||||
No sessions yet.
|
const maxVisible = hideDirectoryControls ? 10 : 7;
|
||||||
</div>
|
const totalSessions = group.sessions.length;
|
||||||
) : (
|
const isExpanded = expandedSessionGroups.has(group.id);
|
||||||
groupedSessions[0].sessions.map((node) => renderSessionNode(node, 0, groupedSessions[0].directory))
|
const visibleSessions = isExpanded ? group.sessions : group.sessions.slice(0, maxVisible);
|
||||||
)}
|
const remainingCount = totalSessions - visibleSessions.length;
|
||||||
|
|
||||||
|
if (totalSessions === 0) {
|
||||||
|
return (
|
||||||
|
<div className="py-1 text-left typography-micro text-muted-foreground">
|
||||||
|
No sessions yet.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{visibleSessions.map((node) => renderSessionNode(node, 0, group.directory))}
|
||||||
|
{remainingCount > 0 && !isExpanded ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleGroupSessionLimit(group.id)}
|
||||||
|
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
Show {remainingCount} more {remainingCount === 1 ? 'session' : 'sessions'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{isExpanded && totalSessions > maxVisible ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleGroupSessionLimit(group.id)}
|
||||||
|
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
|
||||||
|
>
|
||||||
|
Show fewer sessions
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
groupedSessions.map((group) => (
|
groupedSessions.map((group) => (
|
||||||
@@ -1054,19 +1082,15 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
<span
|
<span
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-disabled={isCreatingSession}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
|
'inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
|
||||||
isCreatingSession && 'opacity-40 cursor-default',
|
|
||||||
)}
|
)}
|
||||||
aria-label="Create session in this group"
|
aria-label="Create session in this group"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (isCreatingSession) return;
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCreateSessionInGroup(group.directory);
|
handleCreateSessionInGroup(group.directory);
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (isCreatingSession) return;
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCreateSessionInGroup(group.directory);
|
handleCreateSessionInGroup(group.directory);
|
||||||
@@ -1084,7 +1108,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
<div className="space-y-[0.6rem] py-1">
|
<div className="space-y-[0.6rem] py-1">
|
||||||
{(() => {
|
{(() => {
|
||||||
const isExpanded = expandedSessionGroups.has(group.id);
|
const isExpanded = expandedSessionGroups.has(group.id);
|
||||||
const maxVisible = 7;
|
const maxVisible = hideDirectoryControls ? 10 : 7;
|
||||||
const totalSessions = group.sessions.length;
|
const totalSessions = group.sessions.length;
|
||||||
const visibleSessions = isExpanded ? group.sessions : group.sessions.slice(0, maxVisible);
|
const visibleSessions = isExpanded ? group.sessions : group.sessions.slice(0, maxVisible);
|
||||||
const remainingCount = totalSessions - visibleSessions.length;
|
const remainingCount = totalSessions - visibleSessions.length;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
||||||
@@ -31,14 +30,12 @@ export const CommandPalette: React.FC = () => {
|
|||||||
} = useUIStore();
|
} = useUIStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createSession,
|
openNewSessionDraft,
|
||||||
setCurrentSession,
|
setCurrentSession,
|
||||||
getSessionsByDirectory,
|
getSessionsByDirectory,
|
||||||
initializeNewOpenChamberSession,
|
|
||||||
} = useSessionStore();
|
} = useSessionStore();
|
||||||
|
|
||||||
const { currentDirectory } = useDirectoryStore();
|
const { currentDirectory } = useDirectoryStore();
|
||||||
const { agents } = useConfigStore();
|
|
||||||
const { themeMode, setThemeMode } = useThemeSystem();
|
const { themeMode, setThemeMode } = useThemeSystem();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -46,10 +43,9 @@ export const CommandPalette: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateSession = async () => {
|
const handleCreateSession = async () => {
|
||||||
const session = await createSession();
|
setActiveMainTab('chat');
|
||||||
if (session) {
|
setSessionSwitcherOpen(false);
|
||||||
initializeNewOpenChamberSession(session.id, agents);
|
openNewSessionDraft();
|
||||||
}
|
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
|
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
|
||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
|
|
||||||
export const useKeyboardShortcuts = () => {
|
export const useKeyboardShortcuts = () => {
|
||||||
const { createSession, abortCurrentOperation, initializeNewOpenChamberSession, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore();
|
const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore();
|
||||||
const {
|
const {
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
toggleHelpDialog,
|
toggleHelpDialog,
|
||||||
@@ -17,7 +16,6 @@ export const useKeyboardShortcuts = () => {
|
|||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
setSettingsDialogOpen,
|
setSettingsDialogOpen,
|
||||||
} = useUIStore();
|
} = useUIStore();
|
||||||
const { agents } = useConfigStore();
|
|
||||||
const { themeMode, setThemeMode } = useThemeSystem();
|
const { themeMode, setThemeMode } = useThemeSystem();
|
||||||
const { working } = useAssistantStatus();
|
const { working } = useAssistantStatus();
|
||||||
const abortPrimedUntilRef = React.useRef<number | null>(null);
|
const abortPrimedUntilRef = React.useRef<number | null>(null);
|
||||||
@@ -86,11 +84,9 @@ export const useKeyboardShortcuts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSession().then(session => {
|
setActiveMainTab('chat');
|
||||||
if (session) {
|
setSessionSwitcherOpen(false);
|
||||||
initializeNewOpenChamberSession(session.id, agents);
|
openNewSessionDraft();
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
if ((e.metaKey || e.ctrlKey) && e.key === '/') {
|
||||||
@@ -189,7 +185,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
createSession,
|
openNewSessionDraft,
|
||||||
abortCurrentOperation,
|
abortCurrentOperation,
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
toggleHelpDialog,
|
toggleHelpDialog,
|
||||||
@@ -200,8 +196,6 @@ export const useKeyboardShortcuts = () => {
|
|||||||
setSettingsDialogOpen,
|
setSettingsDialogOpen,
|
||||||
setThemeMode,
|
setThemeMode,
|
||||||
themeMode,
|
themeMode,
|
||||||
initializeNewOpenChamberSession,
|
|
||||||
agents,
|
|
||||||
working,
|
working,
|
||||||
armAbortPrompt,
|
armAbortPrompt,
|
||||||
resetAbortPriming,
|
resetAbortPriming,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
|
||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||||
@@ -32,17 +31,17 @@ type MenuAction =
|
|||||||
export const useMenuActions = (
|
export const useMenuActions = (
|
||||||
onToggleMemoryDebug?: () => void
|
onToggleMemoryDebug?: () => void
|
||||||
) => {
|
) => {
|
||||||
const { createSession, initializeNewOpenChamberSession } = useSessionStore();
|
const { openNewSessionDraft } = useSessionStore();
|
||||||
const {
|
const {
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
toggleHelpDialog,
|
toggleHelpDialog,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
|
setSessionSwitcherOpen,
|
||||||
setSessionCreateDialogOpen,
|
setSessionCreateDialogOpen,
|
||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
setSettingsDialogOpen,
|
setSettingsDialogOpen,
|
||||||
setAboutDialogOpen,
|
setAboutDialogOpen,
|
||||||
} = useUIStore();
|
} = useUIStore();
|
||||||
const { agents } = useConfigStore();
|
|
||||||
const { setDirectory } = useDirectoryStore();
|
const { setDirectory } = useDirectoryStore();
|
||||||
const { setThemeMode } = useThemeSystem();
|
const { setThemeMode } = useThemeSystem();
|
||||||
const isDownloadingLogsRef = React.useRef(false);
|
const isDownloadingLogsRef = React.useRef(false);
|
||||||
@@ -87,11 +86,9 @@ export const useMenuActions = (
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'new-session':
|
case 'new-session':
|
||||||
createSession().then(session => {
|
setActiveMainTab('chat');
|
||||||
if (session) {
|
setSessionSwitcherOpen(false);
|
||||||
initializeNewOpenChamberSession(session.id, agents);
|
openNewSessionDraft();
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'worktree-creator':
|
case 'worktree-creator':
|
||||||
@@ -183,17 +180,16 @@ export const useMenuActions = (
|
|||||||
window.addEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
window.addEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
||||||
return () => window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
return () => window.removeEventListener(MENU_ACTION_EVENT, handleMenuAction);
|
||||||
}, [
|
}, [
|
||||||
createSession,
|
openNewSessionDraft,
|
||||||
initializeNewOpenChamberSession,
|
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
toggleHelpDialog,
|
toggleHelpDialog,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
|
setSessionSwitcherOpen,
|
||||||
setSessionCreateDialogOpen,
|
setSessionCreateDialogOpen,
|
||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
setSettingsDialogOpen,
|
setSettingsDialogOpen,
|
||||||
setAboutDialogOpen,
|
setAboutDialogOpen,
|
||||||
setThemeMode,
|
setThemeMode,
|
||||||
agents,
|
|
||||||
onToggleMemoryDebug,
|
onToggleMemoryDebug,
|
||||||
handleChangeWorkspace,
|
handleChangeWorkspace,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
set((state) => {
|
set((state) => {
|
||||||
const filteredSessions = state.sessions.filter((session) => !deletedSet.has(session.id));
|
const filteredSessions = state.sessions.filter((session) => !deletedSet.has(session.id));
|
||||||
if (state.currentSessionId && deletedSet.has(state.currentSessionId)) {
|
if (state.currentSessionId && deletedSet.has(state.currentSessionId)) {
|
||||||
nextCurrentId = filteredSessions.length > 0 ? filteredSessions[0].id : null;
|
nextCurrentId = null;
|
||||||
} else {
|
} else {
|
||||||
nextCurrentId = state.currentSessionId;
|
nextCurrentId = state.currentSessionId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export const MEMORY_LIMITS = {
|
|||||||
|
|
||||||
export const ACTIVE_SESSION_WINDOW = 180;
|
export const ACTIVE_SESSION_WINDOW = 180;
|
||||||
|
|
||||||
|
export type NewSessionDraftState = {
|
||||||
|
open: boolean;
|
||||||
|
directoryOverride: string | null;
|
||||||
|
parentID: string | null;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface SessionStore {
|
export interface SessionStore {
|
||||||
|
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
@@ -98,10 +105,16 @@ export interface SessionStore {
|
|||||||
|
|
||||||
pendingInputText: string | null;
|
pendingInputText: string | null;
|
||||||
|
|
||||||
|
newSessionDraft: NewSessionDraftState;
|
||||||
|
|
||||||
getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => EditPermissionMode;
|
getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => EditPermissionMode;
|
||||||
toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => void;
|
toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => void;
|
||||||
setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => void;
|
setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => void;
|
||||||
loadSessions: () => Promise<void>;
|
loadSessions: () => Promise<void>;
|
||||||
|
|
||||||
|
openNewSessionDraft: (options?: { directoryOverride?: string | null; parentID?: string | null; title?: string }) => void;
|
||||||
|
closeNewSessionDraft: () => void;
|
||||||
|
|
||||||
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise<Session | null>;
|
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise<Session | null>;
|
||||||
createSessionFromAssistantMessage: (sourceMessageId: string) => Promise<void>;
|
createSessionFromAssistantMessage: (sourceMessageId: string) => Promise<void>;
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
sessionActivityPhase: new Map(),
|
sessionActivityPhase: new Map(),
|
||||||
userSummaryTitles: new Map(),
|
userSummaryTitles: new Map(),
|
||||||
pendingInputText: null,
|
pendingInputText: null,
|
||||||
|
newSessionDraft: { open: true, directoryOverride: null, parentID: null },
|
||||||
|
|
||||||
getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => {
|
getSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => {
|
||||||
return useContextStore.getState().getSessionAgentEditMode(sessionId, agentName, defaultMode);
|
return useContextStore.getState().getSessionAgentEditMode(sessionId, agentName, defaultMode);
|
||||||
@@ -109,7 +110,46 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadSessions: () => useSessionManagementStore.getState().loadSessions(),
|
loadSessions: () => useSessionManagementStore.getState().loadSessions(),
|
||||||
|
|
||||||
|
openNewSessionDraft: (options) => {
|
||||||
|
set({
|
||||||
|
newSessionDraft: {
|
||||||
|
open: true,
|
||||||
|
directoryOverride: options?.directoryOverride ?? null,
|
||||||
|
parentID: options?.parentID ?? null,
|
||||||
|
title: options?.title,
|
||||||
|
},
|
||||||
|
currentSessionId: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configState = useConfigStore.getState();
|
||||||
|
const visibleAgents = configState.getVisibleAgents();
|
||||||
|
const agentName =
|
||||||
|
configState.currentAgentName ||
|
||||||
|
visibleAgents.find((agent) => agent.name === 'build')?.name ||
|
||||||
|
visibleAgents[0]?.name;
|
||||||
|
|
||||||
|
if (agentName) {
|
||||||
|
configState.setAgent(agentName);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeNewSessionDraft: () => {
|
||||||
|
const realCurrentSessionId = useSessionManagementStore.getState().currentSessionId;
|
||||||
|
set({
|
||||||
|
newSessionDraft: { open: false, directoryOverride: null, parentID: null, title: undefined },
|
||||||
|
currentSessionId: realCurrentSessionId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => {
|
createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => {
|
||||||
|
get().closeNewSessionDraft();
|
||||||
|
|
||||||
const result = await useSessionManagementStore.getState().createSession(title, directoryOverride, parentID);
|
const result = await useSessionManagementStore.getState().createSession(title, directoryOverride, parentID);
|
||||||
|
|
||||||
if (result?.id) {
|
if (result?.id) {
|
||||||
@@ -179,7 +219,11 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
shareSession: (id: string) => useSessionManagementStore.getState().shareSession(id),
|
shareSession: (id: string) => useSessionManagementStore.getState().shareSession(id),
|
||||||
unshareSession: (id: string) => useSessionManagementStore.getState().unshareSession(id),
|
unshareSession: (id: string) => useSessionManagementStore.getState().unshareSession(id),
|
||||||
setCurrentSession: async (id: string | null) => {
|
setCurrentSession: async (id: string | null) => {
|
||||||
const previousSessionId = get().currentSessionId;
|
if (id) {
|
||||||
|
get().closeNewSessionDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousSessionId = useSessionManagementStore.getState().currentSessionId;
|
||||||
|
|
||||||
const sessionDirectory = resolveSessionDirectory(
|
const sessionDirectory = resolveSessionDirectory(
|
||||||
useSessionManagementStore.getState().sessions,
|
useSessionManagementStore.getState().sessions,
|
||||||
@@ -224,7 +268,64 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
get().evictLeastRecentlyUsed();
|
get().evictLeastRecentlyUsed();
|
||||||
},
|
},
|
||||||
loadMessages: (sessionId: string) => useMessageStore.getState().loadMessages(sessionId),
|
loadMessages: (sessionId: string) => useMessageStore.getState().loadMessages(sessionId),
|
||||||
sendMessage: (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string) => {
|
sendMessage: async (content: string, providerID: string, modelID: string, agent?: string, attachments?: AttachedFile[], agentMentionName?: string) => {
|
||||||
|
const draft = get().newSessionDraft;
|
||||||
|
|
||||||
|
if (draft?.open) {
|
||||||
|
const created = await useSessionManagementStore
|
||||||
|
.getState()
|
||||||
|
.createSession(draft.title, draft.directoryOverride ?? null, draft.parentID ?? null);
|
||||||
|
|
||||||
|
if (!created?.id) {
|
||||||
|
throw new Error('Failed to create session');
|
||||||
|
}
|
||||||
|
|
||||||
|
const configState = useConfigStore.getState();
|
||||||
|
const draftAgentName = configState.currentAgentName;
|
||||||
|
const draftProviderId = configState.currentProviderId;
|
||||||
|
const draftModelId = configState.currentModelId;
|
||||||
|
|
||||||
|
if (draftProviderId && draftModelId) {
|
||||||
|
try {
|
||||||
|
useContextStore.getState().saveSessionModelSelection(created.id, draftProviderId, draftModelId);
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftAgentName) {
|
||||||
|
try {
|
||||||
|
useContextStore.getState().saveSessionAgentSelection(created.id, draftAgentName);
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draftProviderId && draftModelId) {
|
||||||
|
try {
|
||||||
|
useContextStore
|
||||||
|
.getState()
|
||||||
|
.saveAgentModelForSession(created.id, draftAgentName, draftProviderId, draftModelId);
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
useSessionManagementStore
|
||||||
|
.getState()
|
||||||
|
.initializeNewOpenChamberSession(created.id, configState.agents);
|
||||||
|
} catch {
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
get().closeNewSessionDraft();
|
||||||
|
|
||||||
|
return useMessageStore
|
||||||
|
.getState()
|
||||||
|
.sendMessage(content, providerID, modelID, agent, created.id, attachments, agentMentionName);
|
||||||
|
}
|
||||||
|
|
||||||
const currentSessionId = useSessionManagementStore.getState().currentSessionId;
|
const currentSessionId = useSessionManagementStore.getState().currentSessionId;
|
||||||
return useMessageStore.getState().sendMessage(content, providerID, modelID, agent, currentSessionId || undefined, attachments, agentMentionName);
|
return useMessageStore.getState().sendMessage(content, providerID, modelID, agent, currentSessionId || undefined, attachments, agentMentionName);
|
||||||
},
|
},
|
||||||
@@ -309,6 +410,10 @@ export const useSessionStore = create<SessionStore>()(
|
|||||||
setSessionDirectory: (sessionId: string, directory: string | null) => useSessionManagementStore.getState().setSessionDirectory(sessionId, directory),
|
setSessionDirectory: (sessionId: string, directory: string | null) => useSessionManagementStore.getState().setSessionDirectory(sessionId, directory),
|
||||||
getWorktreeMetadata: (sessionId: string) => useSessionManagementStore.getState().getWorktreeMetadata(sessionId),
|
getWorktreeMetadata: (sessionId: string) => useSessionManagementStore.getState().getWorktreeMetadata(sessionId),
|
||||||
getContextUsage: (contextLimit: number, outputLimit: number) => {
|
getContextUsage: (contextLimit: number, outputLimit: number) => {
|
||||||
|
if (get().newSessionDraft?.open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const currentSessionId = useSessionManagementStore.getState().currentSessionId;
|
const currentSessionId = useSessionManagementStore.getState().currentSessionId;
|
||||||
if (!currentSessionId) return null;
|
if (!currentSessionId) return null;
|
||||||
const messages = useMessageStore.getState().messages;
|
const messages = useMessageStore.getState().messages;
|
||||||
@@ -420,9 +525,11 @@ useSessionManagementStore.subscribe((state, prevState) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draftOpen = useSessionStore.getState().newSessionDraft?.open;
|
||||||
|
|
||||||
useSessionStore.setState({
|
useSessionStore.setState({
|
||||||
sessions: state.sessions,
|
sessions: state.sessions,
|
||||||
currentSessionId: state.currentSessionId,
|
currentSessionId: draftOpen ? null : state.currentSessionId,
|
||||||
lastLoadedDirectory: state.lastLoadedDirectory,
|
lastLoadedDirectory: state.lastLoadedDirectory,
|
||||||
isLoading: state.isLoading,
|
isLoading: state.isLoading,
|
||||||
error: state.error,
|
error: state.error,
|
||||||
@@ -535,9 +642,11 @@ usePermissionStore.subscribe((state, prevState) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bootDraftOpen = useSessionStore.getState().newSessionDraft?.open;
|
||||||
|
|
||||||
useSessionStore.setState({
|
useSessionStore.setState({
|
||||||
sessions: useSessionManagementStore.getState().sessions,
|
sessions: useSessionManagementStore.getState().sessions,
|
||||||
currentSessionId: useSessionManagementStore.getState().currentSessionId,
|
currentSessionId: bootDraftOpen ? null : useSessionManagementStore.getState().currentSessionId,
|
||||||
lastLoadedDirectory: useSessionManagementStore.getState().lastLoadedDirectory,
|
lastLoadedDirectory: useSessionManagementStore.getState().lastLoadedDirectory,
|
||||||
isLoading: useSessionManagementStore.getState().isLoading,
|
isLoading: useSessionManagementStore.getState().isLoading,
|
||||||
error: useSessionManagementStore.getState().error,
|
error: useSessionManagementStore.getState().error,
|
||||||
|
|||||||
Reference in New Issue
Block a user