Skip to content

Multi chat#6428

Merged
zanesq merged 45 commits intomainfrom
zane/multichat-rebased
Jan 21, 2026
Merged

Multi chat#6428
zanesq merged 45 commits intomainfrom
zane/multichat-rebased

Conversation

@zanesq
Copy link
Collaborator

@zanesq zanesq commented Jan 9, 2026

Summary

Multi chat in one window working with existing sidebar navigation.

  • Shows up to 10 recent chats on the left side with link to session history.
  • Active sessions are simply hidden from view using css to keep running in the background.
  • Scrollable sidebar to view bottom menu items.
  • Session names are defaulted to "New Chat X" until they are automatically renamed from the backend using basic polling.
  • Progress indicators for in progress, done and error states (cleared upon session selection in sidebar).
  • Handles delete and renames from session history
  • Handles edits/forks
  • Shows chef icon for recipes (we can add to these icons later for other types of chats/forks etc)

closes #4437
closes #6406

Kapture.2026-01-09.at.15.05.21.mp4

zanesq added 8 commits January 9, 2026 09:01
Rebased multichat feature from zane/multichat-demo onto main.

Key changes:
- Add activeSessions state to track multiple chat sessions
- Add SessionStatusContext for tracking session streaming states
- Add ChatSessionsContainer to render all active sessions (hidden when not active)
- Add SessionIndicators component for showing streaming/unread status in sidebar
- Modify AppLayout to accept activeSessions prop
- Modify BaseChat to support isActiveSession prop and auto-focus
- Modify PairRouteWrapper to manage active sessions instead of rendering Pair directly
- Add optimized-spinner component for better performance
- Update sidebar to show session status indicators

Preserves main branch features:
- Working directory support per session
- Extension overrides support
- Analytics tracking
…ased

* 'main' of github.com:block/goose:
  feat: add hotkey to toggle full tool output display (#6067)
@zanesq zanesq requested review from DOsinga and alexhancock January 9, 2026 23:07
COUNT(m.id) as message_count
FROM sessions s
INNER JOIN messages m ON s.id = m.session_id
LEFT JOIN messages m ON s.id = m.session_id
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Left sidebar needs to show sessions without messages for "New Chat"

Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm, yeah, but we can't do this like this. we create sessions without messages too often and it will just overwhelm all the overviews that we have.

alternatives: make this a flag to this method? probably end up with a convenience method for the current behavior.

or do this at the client. the client knows about the started sessions that don't have messages yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok good call, changed back and now tracked on the client instead

// Store current scroll position to prevent jump
const scrollTop = element.scrollTop;

// Temporarily set to auto to measure natural height, but use minHeight to prevent collapse
Copy link
Collaborator Author

@zanesq zanesq Jan 12, 2026

Choose a reason for hiding this comment

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

Fixed a pre-existing bug where the chat input placeholder was disappearing/jumping when starting a new chat

…ased

* 'main' of github.com:block/goose:
  chore: break up process agent response (#6348)
  More 3.7 removal (#6414)
  CLI show extension errors (#6398)
  fix[desktop]: Improve UX for ExtensionItem component (#6443)
  update[doc]: Add tip for GitHub Copilot Provider (#6441)
  Avoid using cliclack.confirm in non-interactive session (#6412)
  docs: claude prompt caching note (#6429)
  Restore task completion notification (#6427)
  docs: stream-json and auth-token cli options (#6426)
Copy link
Collaborator

@DOsinga DOsinga left a comment

Choose a reason for hiding this comment

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

I think we need to think more about how the data flows work. maybe set up a chat? the query parameter passing, the useRefs introduced, the context manager, and the event posting, it all feels icky.

COUNT(m.id) as message_count
FROM sessions s
INNER JOIN messages m ON s.id = m.session_id
LEFT JOIN messages m ON s.id = m.session_id
Copy link
Collaborator

Choose a reason for hiding this comment

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

hmm, yeah, but we can't do this like this. we create sessions without messages too often and it will just overwhelm all the overviews that we have.

alternatives: make this a flag to this method? probably end up with a convenience method for the current behavior.

or do this at the client. the client knows about the started sessions that don't have messages yet.

// Build URL with search params if resumeSessionId is provided
const searchParams = new URLSearchParams();
if (options?.resumeSessionId) {
searchParams.set('resumeSessionId', options.resumeSessionId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

this seems unrelated to the current change and makes me feel uneasy; we play fast and loose with routing here already. maybe we can let this one slide if we pinkie promise to clean this up once and for all?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Its actually needed so the sidebar can look up the current session and for the refresh case. Open to suggestions butnot really seeing another option here its pretty standard to use a query param for navigation related ids and refresh. Added comments to make it a bit clearer why we do it.

.reverse();
}, [messages]);

// Auto-submit initial message for new sessions or forked sessions
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we can do it this way. this has the different scenarios, the multi facetted if statements that try to mimic a specific situation the app might be useRef global variables that we used to have in the recipe manager with all the hard to read code and hard to main aspects of it. we need to rethink what we're doing here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I pulled this into a hook to be more manageable for now


useEffect(() => {
chatStateRef.current = chatState;
}, [chatState]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah this is getting out of hand. we now have 4 refs for stale state? we have to refactor this. maybe useReducer

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good call changed to use a reducer for all these states

…ased

* 'main' of github.com:block/goose: (23 commits)
  Use Intl.NumberFormat for token formatting in SessionsInsights (#6466)
  feat(ui): format large and small token counts for readability (#6449)
  fix: apply subrecipes when using slash commands (#6460)
  Fix: exclude platform_schedule_tool in CLI (#6442)
  Fix: Small update in how ML-based prompt injection determines final result (#6439)
  docs: remove SSE transport and rename to Streamable HTTP (#6319)
  fix: correct Cloudinary extension command and env variable (#6453)
  fix: add gap between buttons in MacDesktopInstallButtons.js (#6452)
  refactor: include hidden dotfiles folders in file picker search (#6315)
  upgraded safe npm packages (#6450)
  chore(deps): bump react-router and react-router-dom in /ui/desktop (#6408)
  chore(deps): bump lru from 0.12.5 to 0.16.3 (#6379)
  chore(deps-dev): bump @modelcontextprotocol/sdk from 1.24.0 to 1.25.2 in /ui/desktop (#6375)
  fix: inconsistent API url requirement between desktop and CLI versions (#6419)
  feat(vertexai): Add streaming support (#6409)
  fix deeplink recipe launch cold start (#6210)
  Spell check setting (#6446)
  File bug directly (#6413)
  fix(cli): incorrect bin name in shell completions (#6444)
  Use crunchy from crates instead of git fork (#6415)
  ...
@zanesq
Copy link
Collaborator Author

zanesq commented Jan 13, 2026

@DOsinga thanks for the feedback! Agreed on a lot of those points and refactored, pls take a second look. Ultimately we need to add a global state library to reduce our components and get rid of the window events and conditionals and refs everywhere. I will follow up with that next to make all this a lot more manageable but I think this is good enough for now with the current architecture to get the feature live.

@zanesq zanesq requested a review from DOsinga January 13, 2026 19:13
…ased

* 'main' of github.com:block/goose:
  fix(code_execution): serialize record_result output as JSON (#6495)
  perf(google): avoid accumulating thoughtSignatures across conversation history (#6462)
  fix(openai): make tool_call arguments optional and fix silent stream termination (#6309)
  fix: Improve error messages for invalid tool calls (#6483)
  fix: require auth when running goose on non loopback address (#6478)
  chore(deps): bump hono from 4.11.3 to 4.11.4 in /ui/desktop (#6485)
  feat(cli): graceful fallback for keyring failures (#5808)
  fix: support global .gooseignore and negation patterns (#6157)
  docs: manual config for jetbrains (#6490)
  fix: Recipe slash command doesn't work with single optional parameter (#6235)
  fix(openrouter): Handle Gemini thoughtSignature for tool calls (#6370)
  docs: fix extensions page (#6484)
  Allow customizing the new line keybinding in the CLI (#5956)
  Ask for permission in the CLI (#6475)
  docs: add Ralph Loop tutorial for multi-model iterative development (#6455)
  Remove gitignore fallback from gooseignore docs (#6480)
  fix: clean up result recording for code mode (#6343)
  fix(code_execution): handle model quirks with tool calls (#6352)
  feat(ui): support prefersBorder option for MCP Apps (#6465)
  fixed line breaks (#6459)
showPopularTopics?: boolean;
suppressEmptyState: boolean;
sessionId: string;
isActiveSession?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is a pattern the AIs like very much - optional booleans that then get a default assigned. let's watch out for it! in general, no optional booleans unless undefined has a particular meaning (in which case we'd probably want to ask ourselves, do we need an enum instead?)

// LRU eviction: keep only the last MAX_ACTIVE_SESSIONS
if (updated.length > MAX_ACTIVE_SESSIONS) {
updated = updated.slice(updated.length - MAX_ACTIVE_SESSIONS);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can move this in the if - if we don't append there's no reason to slice. also, if we move the existing session, we drop the initialMessage on the floor. is that intended?

AppEvents.CLEAR_INITIAL_MESSAGE,
handleClearInitialMessage as unknown as (event: Event) => void
);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

these type cast strike me as rather ugly; wouldn't you say that doing the casting inside the event handlers would be better? or use a type map.

});
setTimeout(() => {
isProcessingRef.current = false;
}, 1000);
Copy link
Collaborator

Choose a reason for hiding this comment

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

wow. timeout and a useRef? that's a tad smelly. now that we have multi-chat, can we not just send the message to the chat and then we get to pair it will already be there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah this is hacky, I tried using the existing system and it would load multiple chat sessions so its to get around the quick launcher sending multiple events from the other electron window.. will come back to this also

setSessions((prevSessions) =>
prevSessions.map((s) => (s.id === sessionId ? { ...s, name: newDescription } : s))
);
// Notify sidebar of the rename
Copy link
Collaborator

Choose a reason for hiding this comment

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

are you? we're just telling anybody, no? in fact we should update the title of the current conversation too - there's an issue for that

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok removed comment, it does update the title also, I merged in that issue/fix from main already and verified its working


const sessionWithDefaultName = apiSessions.find((s) => shouldShowNewChatTitle(s));

const shouldContinue = pollCount < maxPolls && (sessionWithDefaultName || pollCount < 5);
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is confusing to me, why keep polling if the name has changed?

Copy link
Collaborator Author

@zanesq zanesq Jan 21, 2026

Choose a reason for hiding this comment

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

the name gets updated again as the session progresses but yeah agreed its confusing, I'll come back to this in another pr with a refactor to move to streaming and remove the polling like you mentioned in the other comment

const maxPolls = maxPollDurationMs / pollIntervalMs;
let pollCount = 0;

const pollForUpdates = async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

so this whole polling buseiness is always suspicious. I get why we do this, since the backend changes the name out of the critical path. we should be able to solve this with a message in the stream though. can be done in a follow up. :pinky_swear:

return {
hasAutoSubmitted: hasAutoSubmittedRef.current,
};
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know man, I feel like we might be making this whole pending message business way too complicated and react is just getting in our way here.

what if we just had something super simple like a global(!) variable that is a map from sessionId to pending message and then when you reach BaseChat, we check that, send that message out and clear the entry in the map?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point but we might be able to handle this better with a global state library so I will follow up with that also

tokenState: state.tokenState,
notifications: notificationsMap,
onMessageUpdate,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm still not wild about this. I think we can turn this in a true multi chat state component that just listens to multiple conversations, but I guess that is out of scope for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

noted, will consider in the upcoming refactor to global state

responseStyleChanged: CustomEvent;
'session-created': CustomEvent<{ session?: import('./api').Session }>;
'session-deleted': CustomEvent<{ sessionId: string }>;
'session-renamed': CustomEvent<{ sessionId: string; newName: string }>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

huh. if we have a window event map, why the casting on top?

Copy link
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

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

Nice! This is a big lift.

I don't really understand the dataloading and state management at the top level. I have a hard time working out where the activeSessions are populated from. I do like the reducer for useChatState.tsx as that seems like a nice way to manage the state modifications for each chat, but I wonder if an evolution of this can involve a rust integration that supports the data model with multiple chats more natively?

if (chatState === ChatState.LoadingConversation) {
streamState = 'loading';
} else if (
chatState === ChatState.Streaming ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we just define a new attribute on ChatState?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ChatState doesn't quite fit because ChatState is an enum for UI state transitions, not a data container but good point about simplifying the data flow. I'll look at this with the global state update next. Also good suggestion about better multi chat rust integration, I'll think about what that looks like and get a plan together.

Copy link
Collaborator

@DOsinga DOsinga left a comment

Choose a reason for hiding this comment

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

We should probably just merge this and fix the state management and auto submit, which seem like the hairy bits still.

I saw two things:

the logo looks weird:
image

and I can only start one new session. not something too worried about but it probably indicates something

…ased

* 'main' of github.com:block/goose:
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
@zanesq
Copy link
Collaborator Author

zanesq commented Jan 21, 2026

Sounds good will merge asap after some final testing. The goose logo was an upstream change that added it to the sidebar so I moved it to the top right above chat in my latest commit.

Not seeing the only one session issue will try to repro that real quick.

@zanesq zanesq merged commit a7699e1 into main Jan 21, 2026
15 checks passed
@zanesq zanesq deleted the zane/multichat-rebased branch January 21, 2026 17:13
katzdave added a commit that referenced this pull request Jan 21, 2026
…ovider

* 'main' of github.com:block/goose:
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
michaelneale added a commit that referenced this pull request Jan 21, 2026
* main:
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
  Fix path for global agent skills (#6591)
  recipes: add mcp server (#6552)
  feat(gcp-vertex): add model list with org policy filtering (#6393)
  chore: encourage extension searching (#6582)
  blog: mobile apps consolidation and roadmap (#6580)
  chore: remove unused dependencies in cargo.toml (#6561)
  resolved all the extensions to load in cli (#6464)
michaelneale added a commit that referenced this pull request Jan 21, 2026
* main:
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
michaelneale added a commit that referenced this pull request Jan 21, 2026
* main:
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
lifeizhou-ap added a commit that referenced this pull request Jan 22, 2026
* main: (41 commits)
  chore: tweak release docs (#6571)
  fix(goose): propagate session_id across providers and MCP (#6584)
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
  Fix path for global agent skills (#6591)
  recipes: add mcp server (#6552)
  feat(gcp-vertex): add model list with org policy filtering (#6393)
  chore: encourage extension searching (#6582)
  blog: mobile apps consolidation and roadmap (#6580)
  chore: remove unused dependencies in cargo.toml (#6561)
  ...
wpfleger96 added a commit that referenced this pull request Jan 22, 2026
* main: (68 commits)
  fix(docs): use dynamic import for globby ESM module (#6636)
  chore: trigger CI
  Document tab completion (#6635)
  Install goose-mcp crate dependencies (#6632)
  feat(goose): standardize agent-session-id for session correlation (#6626)
  chore: tweak release docs (#6571)
  fix(goose): propagate session_id across providers and MCP (#6584)
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
  Fix path for global agent skills (#6591)
  ...
wpfleger96 added a commit that referenced this pull request Jan 22, 2026
* main: (68 commits)
  fix(docs): use dynamic import for globby ESM module (#6636)
  chore: trigger CI
  Document tab completion (#6635)
  Install goose-mcp crate dependencies (#6632)
  feat(goose): standardize agent-session-id for session correlation (#6626)
  chore: tweak release docs (#6571)
  fix(goose): propagate session_id across providers and MCP (#6584)
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
  Fix path for global agent skills (#6591)
  ...
wpfleger96 added a commit that referenced this pull request Jan 22, 2026
* main: (68 commits)
  fix(docs): use dynamic import for globby ESM module (#6636)
  chore: trigger CI
  Document tab completion (#6635)
  Install goose-mcp crate dependencies (#6632)
  feat(goose): standardize agent-session-id for session correlation (#6626)
  chore: tweak release docs (#6571)
  fix(goose): propagate session_id across providers and MCP (#6584)
  increase worker threads for ci (#6614)
  docs: todo tutorial update (#6613)
  Added goose doc map md file for goose agent to find relevant doc easily. (#6598)
  add back goose branding to home (#6617)
  fix: actually set the working dir for extensions from session (#6612)
  Multi chat (#6428)
  Lifei/fixed accumulated token count (#6587)
  Dont show MCP UI/Apps until tool is approved (#6492)
  docs: max tokens config (#6596)
  User configurable templates (#6420)
  docs: http proxy environment variables (#6594)
  feat: exclude subagent tool from code_execution filtering (#6531)
  Fix path for global agent skills (#6591)
  ...
fbalicchia pushed a commit to fbalicchia/goose that referenced this pull request Jan 23, 2026
Signed-off-by: fbalicchia <fbalicchia@cuebiq.com>
raj-subhankar pushed a commit to raj-subhankar/goose that referenced this pull request Feb 14, 2026
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.

Goose session tabs (single window) UI for managing multiple agent tasks in one place

3 participants