Skip to content

add open chats atom, bust cache#9996

Draft
Light2Dark wants to merge 1 commit into
mainfrom
sham/add-chat-tabs
Draft

add open chats atom, bust cache#9996
Light2Dark wants to merge 1 commit into
mainfrom
sham/add-chat-tabs

Conversation

@Light2Dark

@Light2Dark Light2Dark commented Jun 26, 2026

Copy link
Copy Markdown
Member

📝 Summary

Add chat tabs under chat panel for easy navigation. This does mean busting the cache.

Screen.Recording.2026-06-26.at.11.44.42.AM.mov

📋 Pre-Review Checklist

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

Review in cubic

@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Jun 26, 2026 4:07am

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 5 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/components/chat/chat-tabs.tsx">

<violation number="1" location="frontend/src/components/chat/chat-tabs.tsx:33">
P2: Tab selection is mouse-only on a non-interactive div. Add keyboard semantics so tabs are operable via Enter/Space.</violation>

<violation number="2" location="frontend/src/components/chat/chat-tabs.tsx:40">
P2: Icon-only close button is missing an accessible label. Add `aria-label` so assistive tech can identify the action.</violation>
</file>

<file name="frontend/src/core/ai/state.ts">

<violation number="1" location="frontend/src/core/ai/state.ts:163">
P2: Deserialization does not validate `activeChatId` against loaded chats, so state can contain a dangling active id. Normalize it to `null` when the chat is missing.</violation>
</file>

<file name="frontend/src/components/chat/chat-panel.tsx">

<violation number="1" location="frontend/src/components/chat/chat-panel.tsx:779">
P1: Streaming responses can be persisted to the wrong chat after tab switches. `onFinish` writes by current `activeChatId`, and new tabs make mid-stream chat switches easy.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant User
    participant ChatPanel as ChatPanel (React)
    participant ChatTabs as ChatTabs (React)
    participant atom as Jotai Atoms
    participant localStorage

    User->>ChatPanel: Clicks "New Chat"
    ChatPanel->>ChatPanel: Generate new Chat object
    ChatPanel->>atom: setChatState(addChatAndOpenTab(prev, newChat))
    atom->>atom: Insert new chat into chats Map
    atom->>atom: Append new chat.id to openChatIds
    atom->>atom: Set activeChatId = newChat.id
    atom-->>ChatPanel: Re-render with updated state

    User->>ChatTabs: Clicks a tab
    ChatTabs->>atom: setActiveChat(chatId)
    alt chatId != null
        atom->>atom: openChatTab(): add chatId to openChatIds if absent, set activeChatId
    else chatId == null
        atom->>atom: Set activeChatId = null
    end
    atom-->>ChatTabs: Tab highlighted as active
    atom-->>ChatPanel: Active chat content displayed

    User->>ChatTabs: Clicks X on a tab
    ChatTabs->>atom: setChatState(closeChatTab(prev, chatId))
    atom->>atom: Remove chatId from openChatIds
    alt chatId was activeChatId
        atom->>atom: Set activeChatId to neighbor at closed index, or null if last tab
    else
        atom->>atom: Keep existing activeChatId
    end
    atom-->>ChatTabs: Tab removed, neighbor activated if needed
    atom-->>ChatPanel: Active content updated

    Note over atom,localStorage: On periodic persist
    atom->>atom: toSerializable():
    atom->>atom: Remove empty chats
    alt chats.size > MAX_STORED_CHATS (25)
        atom->>atom: pruneChats(): keep most recent + protected (openChatIds)
    end
    atom->>atom: sanitizeOpenChatIds(filter out missing)
    atom->>localStorage: Write serialized ChatState (version v6)
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

activeChatId={activeChat?.id}
setActiveChat={setActiveChat}
/>
<ChatTabs />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Streaming responses can be persisted to the wrong chat after tab switches. onFinish writes by current activeChatId, and new tabs make mid-stream chat switches easy.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/chat/chat-panel.tsx, line 779:

<comment>Streaming responses can be persisted to the wrong chat after tab switches. `onFinish` writes by current `activeChatId`, and new tabs make mid-stream chat switches easy.</comment>

<file context>
@@ -783,6 +776,7 @@ const ChatPanelBody = () => {
           activeChatId={activeChat?.id}
           setActiveChat={setActiveChat}
         />
+        <ChatTabs />
       </TooltipProvider>
 
</file context>

</span>
<Button
variant="ghost"
size="sm"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Icon-only close button is missing an accessible label. Add aria-label so assistive tech can identify the action.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/chat/chat-tabs.tsx, line 40:

<comment>Icon-only close button is missing an accessible label. Add `aria-label` so assistive tech can identify the action.</comment>

<file context>
@@ -0,0 +1,95 @@
+      </span>
+      <Button
+        variant="ghost"
+        size="sm"
+        className={cn(
+          "h-4 w-4 p-0 shrink-0 opacity-0 group-hover:opacity-100 hover:bg-destructive/20 hover:text-destructive",
</file context>
Suggested change
size="sm"
size="sm"
aria-label={`Close ${chat.title}`}

? "bg-background text-foreground relative z-1"
: "bg-muted/30 text-muted-foreground hover:bg-muted/50 hover:text-foreground",
)}
onClick={() => onSelect(chat.id)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Tab selection is mouse-only on a non-interactive div. Add keyboard semantics so tabs are operable via Enter/Space.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/chat/chat-tabs.tsx, line 33:

<comment>Tab selection is mouse-only on a non-interactive div. Add keyboard semantics so tabs are operable via Enter/Space.</comment>

<file context>
@@ -0,0 +1,95 @@
+          ? "bg-background text-foreground relative z-1"
+          : "bg-muted/30 text-muted-foreground hover:bg-muted/50 hover:text-foreground",
+      )}
+      onClick={() => onSelect(chat.id)}
+    >
+      <span className="truncate flex-1 min-w-0" title={chat.title}>
</file context>

);
return {
chats: [...chats.entries()],
activeChatId: value.activeChatId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Deserialization does not validate activeChatId against loaded chats, so state can contain a dangling active id. Normalize it to null when the chat is missing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/ai/state.ts, line 163:

<comment>Deserialization does not validate `activeChatId` against loaded chats, so state can contain a dangling active id. Normalize it to `null` when the chat is missing.</comment>

<file context>
@@ -64,21 +67,115 @@ function removeEmptyChats(chatState: Map<ChatId, Chat>): Map<ChatId, Chat> {
+      );
+      return {
+        chats: [...chats.entries()],
+        activeChatId: value.activeChatId,
+        openChatIds: sanitizeOpenChatIds(chats, value.openChatIds),
+      };
</file context>
Suggested change
activeChatId: value.activeChatId,
activeChatId: value.activeChatId && chats.has(value.activeChatId) ? value.activeChatId : null,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant