feat(ui): sessions sidebar redesign with agent sections, smart groups, and draft sessions#103498
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bec30b1dff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let names = readGroupNames(listed); | ||
| // One-time migration: browser-local catalogs predate the gateway store. | ||
| const legacy = readLegacyStoredGroups(); | ||
| if (names.length === 0 && legacy.length > 0) { |
There was a problem hiding this comment.
Merge legacy groups into non-empty catalogs
When a browser still has legacy localStorage custom groups and the gateway already has at least one group, this condition skips migration and the later removeItem clears the local catalog. That loses local-only empty groups and ordering for that browser instead of merging the legacy names into the gateway catalog before deleting the old storage.
Useful? React with 👍 / 👎.
| const cached = this.sessionRowsByAgent[normalized] ?? []; | ||
| return filterVisibleSessionRows(cached, { | ||
| agentId: normalized, | ||
| defaultAgentId: navigationState.selectedAgentId, |
There was a problem hiding this comment.
Use the real default agent for cached-row filtering
When the current chat belongs to a non-default agent and the user expands the default agent section, cached legacy/unprefixed rows are filtered with defaultAgentId set to the current selected agent. isSessionKeyTiedToAgent then treats those unprefixed default-agent sessions as belonging to the non-default agent and drops them; this should pass navigationState.defaultAgentId.
Useful? React with 👍 / 👎.
bec30b1 to
04016c5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04016c5670
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const keyChannel = key.match(CHANNEL_SESSION_KEY_RE)?.[1]; | ||
| const channel = | ||
| normalizeOptionalString(keyChannel && keyChannel !== "direct" ? keyChannel : undefined) ?? | ||
| normalizeOptionalString(rowChannel); | ||
| return { channel, channelSession: Boolean(channel) }; |
There was a problem hiding this comment.
Exclude dashboard keys from channel grouping
For persistent dashboard sessions with keys such as agent:ops-agent:dashboard:direct:subagent-orchestrator (covered by src/gateway/server.sessions.create.test.ts), this parser extracts dashboard as the channel and returns channelSession: true. The new sidebar then places those non-message sessions under a bogus Dashboard channel section instead of the normal chat/dashboard area, despite the comment saying dashboard sessions stay out. Please short-circuit dashboard keys before peer-channel classification or teach the parser that dashboard is not a transport channel.
Useful? React with 👍 / 👎.
| const rows = this.sessionRowsByAgent[normalizeAgentId(agentId)] ?? []; | ||
| return rows.filter((row) => row.unread === true && row.archived !== true).length; |
There was a problem hiding this comment.
Populate unread state before rendering collapsed agents
In multi-agent sidebars, this only counts rows already present in sessionRowsByAgent, but that cache is filled for the currently loaded/expanded agent and expandAgent fetches one agent at a time. A collapsed agent with unread sessions that has not been expanded in this browser therefore gets [] here, so the advertised unread dot is missing until after the user opens that agent. Please hydrate per-agent unread state up front or derive it from an unscoped result when available.
Useful? React with 👍 / 👎.
38123d3 to
a6afa58
Compare
04016c5 to
0ee43cb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ee43cb85d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| saveStoredSessionCustomGroups(this.customGroups); | ||
| const known = this.knownCategories(); | ||
| if (!known.includes(name)) { | ||
| void this.context?.sessions.groupsPut([...this.customGroups(), name]); |
There was a problem hiding this comment.
Re-render when the group catalog changes
When the Sessions page toolbar creates an empty group (onRequestNewCategory() without a session key), this only calls groupsPut. That publishes state.groups without changing state.result, but this page's sessions subscription only schedules work when the result object changes, so the new catalog entry does not appear as an empty category section or menu option until some unrelated session refresh occurs. Please request an update or keep a local catalog snapshot when groupsPut completes.
Useful? React with 👍 / 👎.
| private canSubmit(): boolean { | ||
| if (this.submitting || !this.message.trim() || !this.context?.gateway.snapshot.connected) { | ||
| return false; |
There was a problem hiding this comment.
Gate draft creation while sessions are loading
When a foreground sessions refresh is running, this still enables the draft Start button as long as the gateway is connected. SessionCapability.create returns null while state.loading is true (ui/src/lib/sessions/index.ts:761), so submitting a draft during a Sessions page load or similar refresh shows a generic create failure without sending sessions.create. Either keep the loading gate here or allow create to proceed/queue during refresh.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee148418a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const eventReason = (event.payload as { reason?: unknown } | null)?.reason; | ||
| if (eventReason === "groups") { | ||
| groupsLoadedEpoch = -1; | ||
| void groupsLoad(); |
There was a problem hiding this comment.
Refresh rows on remote group mutations
When another browser renames or deletes a group that has member sessions, the gateway emits a keyless sessions.changed event with reason: "groups". This branch only reloads the catalog; the keyless payload is not reconciled into state.result.sessions, so this client keeps rendering member rows with the old category and the old group section until some unrelated full refresh happens. Please force a sessions refresh for group events that can mutate member categories.
Useful? React with 👍 / 👎.
| showDraft: | ||
| Boolean(this.draftSessionAgentId) && | ||
| normalizeAgentId(this.draftSessionAgentId) === agentId, | ||
| showFallback: true, |
There was a problem hiding this comment.
Target the empty agent fallback at that agent
In multi-agent setups, an expanded agent with zero visible sessions reaches this showFallback: true path, which renders renderChatFallback(). That fallback calls onNavigate("chat") without an agent-specific session search, so app-host fills in the current active session and clicking the only row under an empty/non-default agent reopens the previous chat instead of that agent's main chat. Pass the target agent/main session into the fallback.
Useful? React with 👍 / 👎.
| if (!repoRoot || (usesWorkspace && agent?.workspaceGit !== true)) { | ||
| this.branches = null; | ||
| return; |
There was a problem hiding this comment.
Cancel stale branch lookups on invalid targets
If the dialog starts a branch lookup for a git-backed workspace, then the user switches to a non-git agent before that request resolves, this early return leaves the old request token valid. The stale response can later populate baseRef for the previous repo; if an admin then enters a custom folder and submits before its fresh branch lookup completes, worktreeBaseRef is sent for the wrong checkout. Bump the token and clear baseRef/loading whenever branch loading is abandoned.
Useful? React with 👍 / 👎.
ee14841 to
d79a2a6
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d79a2a6bf1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ...(normalizeOptionalString(draft.execNode) | ||
| ? { execNode: normalizeOptionalString(draft.execNode) } | ||
| : {}), |
There was a problem hiding this comment.
Do not combine node execution with gateway worktrees
When an operator selects a node and also enables Worktree/custom folder, these params submit both execNode and the worktree fields. The create handler provisions the managed worktree on the gateway host, but host=node exec workdir resolution ignores the session default cwd unless the model passes an explicit workdir (src/agents/bash-tools.exec-workdir.ts:347-350), so the new session is displayed as a branch-backed Work session while commands run on the node outside that checkout. Disable this combination or provision/route the checkout on the selected node before sending execNode with worktree.
Useful? React with 👍 / 👎.
| const groups = this.knownSessionGroups(); | ||
| if (!groups.includes(name)) { | ||
| saveStoredSessionCustomGroups([...groups, name]); | ||
| void this.context?.sessions.groupsPut([...groups, name]); | ||
| } |
There was a problem hiding this comment.
Load groups before replacing the catalog
If the user creates or reorders a group before groupsLoad() has completed (or after a transient list failure), knownSessionGroups() is only the current list window plus an empty state.groups, and sessions.groups.put replaces the entire gateway catalog. In that state this call can drop existing empty groups or groups outside the current agent/limit, even though the user only added one name; wait for the catalog or merge against a fresh sessions.groups.list before issuing the replace.
Useful? React with 👍 / 👎.
|
Codex review: found issues before merge. Reviewed July 10, 2026, 1:35 PM ET / 17:35 UTC. Summary Reproducibility: yes. The exact PR head provides deterministic source-level reproduction paths for the remaining migration, navigation, cache, key-parsing, and asynchronous draft-state defects, although this read-only review did not execute the UI. Review metrics: 2 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Keep browse expansion local to the sidebar, make group migration and replace-all writes lossless against a fully loaded canonical catalog, and validate agent, branch, folder, worktree, and node selections against the runtime's actual execution semantics before creating a session. Do we have a high-confidence way to reproduce the issue? Yes. The exact PR head provides deterministic source-level reproduction paths for the remaining migration, navigation, cache, key-parsing, and asynchronous draft-state defects, although this read-only review did not execute the UI. Is this the best way to solve the issue? No. The feature direction is valuable and supported by the merged foundation, but the current patch is not the best implementation until browse/selection ownership, catalog integrity, and execution-target semantics are corrected. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 5aea34ad8c21. Label changesLabel changes:
Label justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (4 earlier review cycles)
|
d79a2a6 to
8a9b34a
Compare
19a4731 to
1f875b8
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f875b8585
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const fallback = this.context?.agents.state.agentsList?.defaultId ?? agents[0]?.id ?? "main"; | ||
| this.agentId = agents.some((agent) => normalizeAgentId(agent.id) === requested) | ||
| ? requested | ||
| : normalizeAgentId(fallback); |
There was a problem hiding this comment.
Preserve the requested agent before the list loads
When the dialog opens before agents.ensureList() has populated agentsList, agents is empty, so even a non-main initialAgentId from the expanded/deep-linked agent fails this membership check and falls back to main; because openedFor then suppresses another reset, the draft can submit sessions.create for the wrong agent until the user closes and reopens it. Keep the requested agent id as the fallback when the list is not loaded yet, and only reject it once there is an actual list to validate against.
Useful? React with 👍 / 👎.
| this.branches = result ? { ...result, repoRoot } : null; | ||
| this.baseRef = result?.defaultBranch ?? result?.headBranch ?? ""; |
There was a problem hiding this comment.
Do not overwrite typed base refs after branch load
If worktrees.branches is still pending after the user enables Worktree and types a custom base ref, the async response unconditionally resets baseRef to the default/head branch. On slow branch lookups this silently changes the checkout that will be sent in worktreeBaseRef; only apply the discovered default while the field is still untouched for that request.
Useful? React with 👍 / 👎.
487f718 to
9a35da8
Compare
…, and draft sessions - agents become collapsible top-level sidebar sections; expanding an agent browses its sessions without navigating, replacing the hidden scope select - built-in smart groups: one section per channel (rows keep their chat titles), a Work section for worktree/exec-node sessions with a repo/branch/node subtitle, custom groups, and Chats - session names never show raw keys or peer ids; DM fallbacks shorten ids, dashboard sessions read 'New session', unnamed work sessions read as their checkout - one + opens the new-session draft dialog: agent, exec host (paired system.run nodes), folder, worktree toggle with base-branch picker (worktrees.branches) and optional name; the first message creates the session and starts the run in one sessions.create call - custom group catalog/order moves to the gateway (sessions.groups.*) with a one-time localStorage migration; rename/delete update members server-side instead of client-side paging Part of #103431
Dropping a row dragged out of a non-active agent section now finds the session in the per-agent row cache instead of only the active scope, so the category patch is applied instead of silently doing nothing. Part of #103431
sessions.groups.put/rename/delete now always broadcast a groups-change event, and the session capability reloads the gateway-owned catalog when one arrives, so another browser's group create/reorder/rename/delete no longer leaves this client on a stale snapshot for the rest of the connection. Part of #103431
Real locale translations for the new-session dialog and sidebar keys (the fallback gate requires zero recorded English fallbacks), plus the regenerated docs map for the new Control UI section. Part of #103431
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 06c824d5c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ${multiAgent | ||
| ? agents.map((agent) => this.renderAgentSection(agent, navigationState)) | ||
| : this.renderSessionListBody(visibleSessions, { |
There was a problem hiding this comment.
Render the active unconfigured agent session
When the current chat/deep link is for an agent that is not present in agentsList while a multi-agent config exists, resolveSessionNavigation() still adds a fallback active row to visibleSessions, but this branch ignores visibleSessions and renders only agents.map(...). That makes stale/unconfigured-agent chats disappear from the sidebar entirely in multi-agent setups instead of showing the active fallback row the navigation layer prepared.
Useful? React with 👍 / 👎.
06c824d to
11ea65a
Compare
|
Merged via squash.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 11ea65ad15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.error = context.sessions.state.error ?? t("newSession.createFailed"); | ||
| return; | ||
| } | ||
| this.onCreated?.(key); |
There was a problem hiding this comment.
Surface create-time send failures
When the draft includes an initial message and sessions.create creates the session but the embedded chat.send fails (for example provider/model unavailable), the gateway still returns an ok payload with a key plus runError; this path treats any key as success, closes the dialog, and navigates away without showing that the submitted prompt did not start. Message-backed creates need to inspect the create result/run error (or have SessionCapability.create return null for that case) before calling onCreated.
Useful? React with 👍 / 👎.
| } catch { | ||
| // Older gateways without the groups RPC keep observed-category grouping. | ||
| } |
There was a problem hiding this comment.
Clear stale groups when group RPCs are unavailable
When a browser has already loaded a group catalog, then reconnects to an older/different Gateway where sessions.groups.list is unavailable or fails, this catch leaves the previous state.groups intact even though the comment says older Gateways should fall back to observed categories. Since the disconnect path preserves groups: state.groups, the sidebar and Sessions page can render an empty catalog from the previous server and later replace the wrong catalog; publish an empty catalog on this failure or reset groups when the client changes.
Useful? React with 👍 / 👎.
|
Before/after screenshots for this PR (deterministic mock-gateway harness, identical seeded sessions): Before — agent dropdown, separate "New session" entry + worktree button, second plus on the group row, one flat "Ungrouped" list: After — agent sections, single plus on the Sessions header, Slack/Telegram smart groups, Work section with New Session dialog (agent · where · folder · worktree + base branch · name · first message): |
…, and draft sessions (openclaw#103498) * feat(ui): sessions sidebar redesign with agent sections, smart groups, and draft sessions - agents become collapsible top-level sidebar sections; expanding an agent browses its sessions without navigating, replacing the hidden scope select - built-in smart groups: one section per channel (rows keep their chat titles), a Work section for worktree/exec-node sessions with a repo/branch/node subtitle, custom groups, and Chats - session names never show raw keys or peer ids; DM fallbacks shorten ids, dashboard sessions read 'New session', unnamed work sessions read as their checkout - one + opens the new-session draft dialog: agent, exec host (paired system.run nodes), folder, worktree toggle with base-branch picker (worktrees.branches) and optional name; the first message creates the session and starts the run in one sessions.create call - custom group catalog/order moves to the gateway (sessions.groups.*) with a one-time localStorage migration; rename/delete update members server-side instead of client-side paging Part of openclaw#103431 * fix(ui): resolve dragged sessions across browsed agent sections Dropping a row dragged out of a non-active agent section now finds the session in the per-agent row cache instead of only the active scope, so the category patch is applied instead of silently doing nothing. Part of openclaw#103431 * fix(ui): propagate group catalog changes to open clients sessions.groups.put/rename/delete now always broadcast a groups-change event, and the session capability reloads the gateway-owned catalog when one arrives, so another browser's group create/reorder/rename/delete no longer leaves this client on a stale snapshot for the rest of the connection. Part of openclaw#103431 * docs: restore new session dialog section after rebase * fix(ui): translate new sidebar/session strings and refresh docs map Real locale translations for the new-session dialog and sidebar keys (the fallback gate requires zero recorded English fallbacks), plus the regenerated docs map for the new Control UI section. Part of openclaw#103431 * fix(ui): repair locale metadata after rebase conflict resolution * fix(ui): merge mainline locale keys with the sidebar redesign strings * fix(ui): refresh raw-copy baseline for mainline tool-card strings * fix(ui): reconcile generated locale artifacts after rebase



Related: #103431
Stacked on #103432 (protocol/server foundation).
What Problem This Solves
The sessions sidebar mixed three create affordances (New session, a git-branch worktree button, and a New group prompt), hid agent switching in a compact
<select>that teleported navigation on change, and rendered channel-originated sessions in one flat list with raw peer ids ("Telegram · 491234567890") and token names ("slack:g-general"). Custom group names/order lived in browser localStorage, so they never followed the operator across devices. Worktree sessions were indistinguishable from plain chats, and there was no way to pick a branch, folder, or node when starting work.Why This Change Was Made
Agents are now collapsible top-level sidebar sections: expanding an agent browses its sessions (scoped list fetch, cached per agent) without navigating away from the open chat, and collapsed agents show an unread indicator; single-agent installs keep the flat list. Within an agent, rows classify into built-in smart sections — one per channel (rows keep their human chat titles), a Work section for worktree/exec-node sessions with a
repo ⎇ branch(plus node) subtitle — followed by custom groups and Chats; an explicit user category always beats smart classification. Naming never falls back to raw keys: DM fallbacks shorten peer ids, dashboard sessions read "New session" until their generated title lands, and unnamed work sessions read as their checkout. The three create buttons collapse into one + that opens a draft dialog (agent, exec host from pairedsystem.runnodes, folder, worktree toggle with aworktrees.branches-backed base-branch picker and optional name); the first message creates the session and starts the run in a singlesessions.createcall, so abandoned drafts leave nothing behind. The custom group catalog moves onto the gateway (sessions.groups.*) with a one-time localStorage migration; rename/delete now update member sessions server-side, deleting the old client-side paging loop.User Impact
Multi-agent operators see every agent in the sidebar and can browse one without losing their place. Telegram/Slack/WhatsApp sessions group under their channels with real names; work sessions surface their branch at a glance. Starting a session on a specific branch, folder, or node is a first-class flow. Custom groups follow you across browsers. Plain New Chat semantics (
/new, main-session reset) are unchanged.Evidence
OPENCLAW_STATE_DIR, two agents, seeded Telegram/Slack/WhatsApp sessions): sidebar rendered agent sectionsMain/Work agent, channel groups with titles "Acme #general", "Peter Steinberger", "OpenClaw Devs", "WhatsApp · …345678"; the + dialog listed branchesmain/feature-base/topic-xfromworktrees.branches, and submitting with basefeature-base+ namelive-testcreated branchopenclaw/live-test, checked out the worktree under the state dir, navigated to the new session, and listed it under Work with subtitleworkspace-main ⎇ live-test. Agent expansion switched session lists without changing the URL; zero console errors.pnpm test ui/src/lib/sessions ui/src/lib/session-display.test.ts ui/src/components/app-sidebar.test.ts ui/src/components/new-session-dialog.test.ts ui/src/lib/agents/index.test.ts— green, including new coverage for smart-section classification, channel-session detection, name fallbacks, and draft→create param mapping.ui/src/pages/sessions,ui/src/components, chat page/pane sweep: green exceptlobster-pet/lobster-dex, which fail identically on the base branch (pre-existing, unrelated).pnpm ui:buildclean;env -u OPENAI_API_KEY pnpm ui:i18n:checkclean (locale bundles regenerated raw-copy, matching the secretless CI lane); tsgo core + test lanes green.