Skip to content

Conversation

@arielahdoot
Copy link
Contributor

@arielahdoot arielahdoot commented Jan 16, 2026

Fix: Update session name in UI in real-time

Problem

Currently, Goose's backend automatically generates descriptive session names (e.g., "PostgreSQL missing index fix" instead of "New session 1") after the first few messages. However, the UI doesn't update to show these names until you close and reopen the session.

This defeats the purpose of having auto-generated names, especially when working with multiple sessions simultaneously. Users see generic "New session 1" titles in the window/dock, making it impossible to distinguish between sessions.

Current Behavior (Before Fix)

Screenshot 2026-01-15 at 16 57 45
  1. User starts a new session → Window title shows "New session 1"
  2. User sends 3 messages about PostgreSQL performance
  3. Backend generates a descriptive name: "PostgreSQL missing index fix"
  4. Window title still shows "New session 1"
  5. User must close and reopen the session to see "PostgreSQL missing index fix"

This is especially problematic because:

  • The only way to see the actual session name is to close the session
  • This goes against the value proposition of being able to use multiple sessions at once
  • Users can't effectively manage multiple concurrent sessions because they all have the same name when right-clicking on the Goose icon in the dock.

Solution

This PR makes the UI update the session name in real-time by:

  1. ui/desktop/src/hooks/useChatStream.ts: After each reply completes, fetch the latest session data from the backend (but only for the first 3 user messages, matching when the backend generates/refines names)

  2. ui/desktop/src/components/BaseChat.tsx: Propagate the updated session name to the global ChatContext so the window title reflects the current name

How It Works

The backend's auto-naming logic (maybe_update_name() in session_manager.rs) generates and refines the session name after each of the first 3 user messages:

  • After 1st message: Initial name based on first question
  • After 2nd message: Refined with more context
  • After 3rd message: Final refined name

The UI now matches this behavior by refreshing the session name after each of the first 3 replies, then stopping. This ensures:

  • ✅ Exactly 3 API calls per session (one after each of the first 3 messages)
  • Zero ongoing overhead after the 3rd message
  • Real-time updates - no need to close/reopen sessions
  • Matches backend behavior - refreshes exactly when the backend updates the name

New Behavior (After Fix)

Before (Initial state):

image

After 1st message:

Screenshot 2026-01-15 at 23 18 32

After 2nd message:

Screenshot 2026-01-15 at 23 19 04 ---

After 3rd message:

Screenshot 2026-01-15 at 23 19 40

After 4th message:

Screenshot 2026-01-15 at 23 26 31 ---

Summary of behavior:

  1. User starts a new session → Window title shows "New session 1"
  2. User sends 1st message: "My queries are slow" → Goose responds → Window title updates
  3. User sends 2nd message: "It's PostgreSQL with 50M rows" → Goose responds → Window title refines
  4. User sends 3rd message: "Missing index causing full table scan" → Goose responds → Window title refines again
  5. User sends 4th message and beyond → Window title stays the same (no more updates)

Technical Notes

ESLint Disable Comment

You'll notice an eslint-disable-next-line react-hooks/exhaustive-deps comment in BaseChat.tsx. This is intentional and necessary. Here's why:

The useEffect that updates the global chat context uses messages, recipe, and sessionId from the component scope, but only includes session?.name and setChat in its dependency array:

useEffect(() => {
  const currentSessionName = session?.name;
  if (currentSessionName && currentSessionName !== lastSetNameRef.current) {
    lastSetNameRef.current = currentSessionName;
    setChat({
      messages,      // Used but not in deps
      recipe,        // Used but not in deps
      sessionId,     // Used but not in deps
      name: currentSessionName,
    });
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [session?.name, setChat]);

Why we can't add them to the dependency array:

  • Adding messages to the deps would cause the effect to run on every message (infinite loop during testing showed 114 triggers)
  • We only want this effect to run when the session name changes, not when messages are added
  • The values captured from the closure are always current when the effect runs

Alternatives we tried:

  1. Using useRef to store values: This worked but added unnecessary complexity (Copilot feedback: "creates unnecessary complexity")
  2. Functional update setChat(prev => ({...prev, name})): TypeScript error - the setChat prop type doesn't support updater functions
  3. Adding all deps to the array: Caused infinite re-render loop (effect fires on every message → updates state → triggers re-render → effect fires again)

Why this approach is correct:

  • The effect is intentionally designed to only run when session?.name changes
  • The values from the closure (messages, recipe, sessionId) are always current at the time the effect runs
  • This is a valid use case for disabling the exhaustive-deps rule

We're open to suggestions from Goose maintainers if there's a better pattern that satisfies both ESLint and avoids the infinite loop!

Testing

Manual Testing Steps

  1. Start a new Goose session
  2. Send your 1st message (e.g., "I have a theoretical question. Do not change anything or look at any code. My queries are really slow")
    • Verify the window title updates from "New session 1" to something like "Query performance optimization"
  3. Send your 2nd message (e.g., "I'm using PostgreSQL with a table that has 50 million rows")
    • Verify the window title updates again to something more specific like "PostgreSQL large table performance"
  4. Send your 3rd message (e.g., "The slow query is doing a full table scan because I forgot to add an index")
    • Verify the window title updates a final time to something very specific like "PostgreSQL missing index fix"
  5. Send a 4th message (e.g., "Can you show me the exact SQL command to create the index?")
    • Verify the window title does NOT change - it should remain the same as after the 3rd message

Expected Behavior

  • Window title updates after each of the first 3 user messages, becoming progressively more specific
  • After the 3rd message, the name is finalized and no more updates occur
  • No need to close and reopen the session to see the name
  • Session name is visible in real-time while working

Impact

  • User Experience: Users can now see meaningful session names while working, making it much easier to manage multiple sessions
  • Multi-Session Workflow: Users can now effectively work with multiple sessions simultaneously without losing track of what each one is about
  • Performance: Minimal impact - exactly 3 lightweight API calls per session, then stops

The backend auto-generates session names after each of the first 3 user
messages to refine the name as more context becomes available. However,
the UI was not updating to show these names until the session was closed
and reopened.

This fix adds two changes:

1. In useChatStream.ts: Refresh session name after each reply for the
   first 3 user messages by counting user messages and fetching updated
   session data. Stops after 3rd message to avoid unnecessary API calls.

2. In BaseChat.tsx: Propagate session name changes to the global
   ChatContext so the window title updates immediately via AppSidebar.

The UI now matches the backend behavior perfectly:
- After 1st message: Name appears (e.g., "Query performance")
- After 2nd message: Name refines (e.g., "PostgreSQL performance")
- After 3rd message: Name finalizes (e.g., "PostgreSQL missing index")
- After 4th+ messages: No more updates (stops permanently)

Performance impact: Exactly 3 API calls per session (~1.5KB, <15ms total),
then zero overhead. Orders of magnitude cheaper than existing operations.
…enders

Changed from updating entire chat object to only updating the name field
using the updater function pattern. This prevents the effect from firing
on every message change.

Benefits:
- Effect only fires when session.name actually changes (3x per session)
- Avoids overwriting messages/recipe that may be updated elsewhere
- Reduces unnecessary re-renders of ChatContext consumers
- Prevents potential stale data bugs from closure captures
@arielahdoot arielahdoot marked this pull request as ready for review January 16, 2026 05:15
Copilot AI review requested due to automatic review settings January 16, 2026 05:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a UX issue where session names generated by the backend weren't reflected in the UI until users closed and reopened sessions. The solution adds real-time session name updates by fetching the session name after each of the first three user messages, matching the backend's name refinement behavior.

Changes:

  • Added session name refresh logic that triggers after replies complete for the first 3 user messages
  • Implemented useEffect hook to propagate session name changes to the global ChatContext for window title updates
  • Added user message counting to limit API calls to exactly 3 per session

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ui/desktop/src/hooks/useChatStream.ts Adds session name refresh logic in onFinish callback with user message counting
ui/desktop/src/components/BaseChat.tsx Propagates session name updates to global ChatContext via useEffect

The setChat prop type signature doesn't support updater functions,
so we need to explicitly set all fields. This approach works correctly
and passes TypeScript validation.
Copilot AI review requested due to automatic review settings January 16, 2026 05:22
Changed from session?.name to session to satisfy react-hooks/exhaustive-deps
rule, since we access session.name inside the effect.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Updated comment from 'when session name changes' to 'keep it in sync
with local state' to accurately reflect that the effect runs whenever
any dependency changes (session, messages, recipe, sessionId, setChat),
not just when the session name changes.
Copilot AI review requested due to automatic review settings January 16, 2026 05:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

The useEffect was firing on every message because messages/recipe/sessionId
were in the dependency array. This caused performance issues and made the
UI sluggish with the dev console open.

Solution: Use refs to store current values of messages, recipe, and sessionId.
The main useEffect only depends on session?.name and setChat, so it only
fires when the session name actually changes (3 times per session).

This eliminates the infinite loop and excessive re-renders while still
keeping the global chat context in sync with the session name.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

- Extract session?.name into currentSessionName at start of useEffect
- Use currentSessionName consistently in condition and setChat call
- Satisfies react-hooks/exhaustive-deps by only accessing session?.name
- Maintains same logic while keeping dependencies clean
This file was for local reference only and shouldn't be in the PR
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

- Move ref updates from useEffect (with no deps) to render body
- More efficient: no extra effect running after every render
- Refs are updated synchronously during render
- Aligns with React best practices for ref management
- Addresses Copilot feedback about unnecessary useEffect
Copilot AI review requested due to automatic review settings January 16, 2026 06:34
@arielahdoot arielahdoot force-pushed the fix/session-name-ui-sync-clean branch from d8a4682 to 1b56b88 Compare January 16, 2026 06:36
This file should remain in the repository as it exists in main
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines 308 to 325
const messagesRef = useRef(messages);
const recipeRef = useRef(recipe);
const sessionIdRef = useRef(sessionId);

messagesRef.current = messages;
recipeRef.current = recipe;
sessionIdRef.current = sessionId;

useEffect(() => {
const currentSessionName = session?.name;
if (currentSessionName && currentSessionName !== lastSetNameRef.current) {
lastSetNameRef.current = currentSessionName;
setChat({
messages: messagesRef.current,
recipe: recipeRef.current,
sessionId: sessionIdRef.current,
name: currentSessionName,
});
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

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

The refs pattern here creates unnecessary complexity and could lead to confusion. Since this effect only triggers on session name changes, you're propagating potentially stale values for messages, recipe, and sessionId to the global chat context.

Consider simplifying to only update the name field, or include all dependencies in the effect's dependency array. If you need to update the entire chat object, you should either:

  1. Add messages, recipe, and sessionId to the dependency array
  2. Or use a functional update: setChat(prev => ({ ...prev, name: currentSessionName }))

The functional update approach would be safer as it preserves any other concurrent updates to the chat state.

Suggested change
const messagesRef = useRef(messages);
const recipeRef = useRef(recipe);
const sessionIdRef = useRef(sessionId);
messagesRef.current = messages;
recipeRef.current = recipe;
sessionIdRef.current = sessionId;
useEffect(() => {
const currentSessionName = session?.name;
if (currentSessionName && currentSessionName !== lastSetNameRef.current) {
lastSetNameRef.current = currentSessionName;
setChat({
messages: messagesRef.current,
recipe: recipeRef.current,
sessionId: sessionIdRef.current,
name: currentSessionName,
});
useEffect(() => {
const currentSessionName = session?.name;
if (currentSessionName && currentSessionName !== lastSetNameRef.current) {
lastSetNameRef.current = currentSessionName;
setChat((prevChat) =>
prevChat
? { ...prevChat, name: currentSessionName }
: { name: currentSessionName },
);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leaving this one open in case individuals prefer how we did this before Copilot gave its opinion. This also worked just fine.

  const messagesRef = useRef(messages);
  const recipeRef = useRef(recipe);
  const sessionIdRef = useRef(sessionId);
  
  messagesRef.current = messages;
  recipeRef.current = recipe;
  sessionIdRef.current = sessionId;
  
  useEffect(() => {
    const currentSessionName = session?.name;
    if (currentSessionName && currentSessionName !== lastSetNameRef.current) {
      lastSetNameRef.current = currentSessionName;
      setChat({
        messages: messagesRef.current,
        recipe: recipeRef.current,
        sessionId: sessionIdRef.current,
        name: currentSessionName,
      });

- Use messages, recipe, sessionId directly from closure
- Effect still only runs when session?.name changes
- Simpler and clearer code
- Add eslint-disable for intentional dependency omission
- Addresses Copilot feedback about unnecessary complexity
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

- Remove 'session' from onFinish dependency array
- Check only sessionId instead of session && sessionId
- Remove name comparison (always update if name exists)
- Makes callback more stable (doesn't recreate on every session change)
- Addresses Copilot feedback about unnecessary callback recreation
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

@taniandjerry
Copy link
Contributor

As mentioned by @arielahdoot on Slack, this fixes a quirk around session names not updating in real-time, which has made switching between goose windows a lot more confusing in their opinion. Tagging for review @block/goose-core-maintainers @block/goose-maintainers

@aharvard
Copy link
Collaborator

Nice, @arielahdoot! This has been bugging me for some time now... thanks for solving a great, little detail like this.

@angiejones angiejones merged commit 9b86d96 into block:main Jan 16, 2026
23 of 24 checks passed
fbalicchia pushed a commit to fbalicchia/goose that referenced this pull request Jan 23, 2026
Signed-off-by: fbalicchia <fbalicchia@cuebiq.com>
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.

4 participants