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:
Bohdan Triapitsyn
2025-12-21 20:18:51 +02:00
parent 75ba954307
commit 7699aab123
12 changed files with 273 additions and 152 deletions
@@ -27,6 +27,7 @@ export const ChatContainer: React.FC = () => {
messageStreamStates,
trimToViewportWindow,
sessionActivityPhase,
newSessionDraft,
} = useSessionStore();
const streamingMessageId = React.useMemo(() => {
@@ -35,6 +36,7 @@ export const ChatContainer: React.FC = () => {
}, [currentSessionId, streamingMessageIds]);
const { isMobile } = useDeviceInfo();
const draftOpen = Boolean(newSessionDraft?.open);
const sessionMessages = React.useMemo(() => {
@@ -143,7 +145,7 @@ export const ChatContainer: React.FC = () => {
void load();
}, [currentSessionId, loadMessages, messages, scrollToBottom]);
if (!currentSessionId) {
if (!currentSessionId && !draftOpen) {
return (
<div
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) {
const hasMessagesEntry = messages.has(currentSessionId);
if (!hasMessagesEntry) {
+15 -10
View File
@@ -61,6 +61,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const sendMessage = useSessionStore((state) => state.sendMessage);
const currentSessionId = useSessionStore((state) => state.currentSessionId);
const newSessionDraftOpen = useSessionStore((state) => state.newSessionDraft?.open);
const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation);
const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort);
const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId);
@@ -229,7 +230,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const handleSubmit = async (e?: React.FormEvent) => {
e?.preventDefault();
if (!hasContent || !currentSessionId) return;
if (!hasContent || (!currentSessionId && !newSessionDraftOpen)) return;
const messageToSend = message.replace(/^\n+|\n+$/g, '');
@@ -542,7 +543,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
return;
}
if (!currentSessionId) {
if (!currentSessionId && !newSessionDraftOpen) {
return;
}
@@ -572,7 +573,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
if (attachedCount > 0) {
toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`);
}
}, [addAttachedFile, currentSessionId, insertTextAtSelection]);
}, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]);
const handleFileSelect = (file: { name: string; path: string }) => {
@@ -660,7 +661,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (currentSessionId && !isDragging) {
if ((currentSessionId || newSessionDraftOpen) && !isDragging) {
setIsDragging(true);
}
};
@@ -678,7 +679,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
e.stopPropagation();
setIsDragging(false);
if (!currentSessionId) return;
if (!currentSessionId && !newSessionDraftOpen) return;
const files = Array.from(e.dataTransfer.files);
let attachedCount = 0;
@@ -838,13 +839,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
) : (
<button
type={isMobile ? 'button' : 'submit'}
disabled={!hasContent || !currentSessionId}
disabled={!hasContent || (!currentSessionId && !newSessionDraftOpen)}
onPointerDownCapture={(event) => {
if (!isMobile || event.pointerType !== 'touch') {
return;
}
if (!hasContent || !currentSessionId) {
if (!hasContent || (!currentSessionId && !newSessionDraftOpen)) {
return;
}
@@ -868,7 +869,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}}
className={cn(
iconButtonBaseClass,
hasContent && currentSessionId
hasContent && (currentSessionId || newSessionDraftOpen)
? 'text-primary hover:text-primary'
: 'opacity-30'
)}
@@ -1080,8 +1081,12 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
onKeyDown={handleKeyDown}
onPaste={handlePaste}
onPointerDownCapture={handleTextareaPointerDownCapture}
placeholder={currentSessionId ? "# for agents; @ for files; / for commands" : "Select or create a session to start chatting"}
disabled={!currentSessionId}
placeholder={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(
'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',