Skip to content

perf: memoize workspace session rows + paginate + subagent marker - #18

Open
robbe1912 wants to merge 3 commits into
alvarolorentedev:mainfrom
robbe1912:perf/workspace-session-list-memoization
Open

perf: memoize workspace session rows + paginate + subagent marker#18
robbe1912 wants to merge 3 commits into
alvarolorentedev:mainfrom
robbe1912:perf/workspace-session-list-memoization

Conversation

@robbe1912

Copy link
Copy Markdown

Problem

The Workspace Chats panel renders every session row inline. Each parent state change (search typing, menu open, rename on any row) re-renders all visible rows. For large session databases (50+ chats in one workspace) the list is sluggish.

Secondary issues:

  1. No pagination — the entire session list loads at once.
  2. Subagent sessions (sessions with parentID in the OpenCode SDK schema) are visually indistinguishable from primary sessions.

Fix

Memoization

Extract SessionListItem component with React.memo + display-prop comparator. Callback identity intentionally excluded — callbacks only fire on user action, so stale closures are not a correctness issue. Renamed-input state moved into the row component so other rows don't re-render while the user types in any one row's rename field.

Ref-based transition guard prevents the useEffect [isRenaming, session.title] regression where SSE/polling title updates would clobber the user's in-progress rename draft.

Pagination

Default to displaying the last 24 chats with prev/next page controls. Configurable via a new chatsPerPage ChatPreference (5–100, step 5, default 24). Page indicator (Page X of Y · N chats) only renders when totalChatsPages > 1. Auto-clamps when the list shrinks (delete/archive) so the user never lands on an out-of-range page.

Subagent marker

The OpenCode SDK exposes Session.parentID?: string (node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts:469). When present, the session is a child of another session. Visually:

  • Title: prefixed with + italic + muted color
  • Description: prefixed with Subagent · <preview>

Optional hideSubagentChats ChatPreference (default false) filters them from the list entirely. They still get created and run server-side; only the row is filtered.

Files changed

  • app/(tabs)/workspace.tsx (+172 / -32 LOC) — SessionListItem component + areSessionListItemPropsEqual comparator + pagination state + subagent visual treatment
  • components/settings/settings-sections.tsx (+5 LOC) — NumericSlider (5–100, step 5) + SettingSwitchRow for chats-per-page and hide-subagent-chats
  • providers/opencode-provider-utils.ts (+4 LOC) — chatsPerPage: number + hideSubagentChats: boolean ChatPreferences fields + defaults

Validation

  • npm run typecheck
  • npm run lint
  • npm run test:fake-server:self

Settings

In Settings → Voice section:

  • Chats per page (workspace) — slider 5–100, step 5, default 24
  • Hide subagent chats — toggle, default off

renderSessionItem returned a fresh closure tree on every parent state
change (typing in search box, opening any menu, renaming any session,
toggling archived view), forcing every visible row to re-render even
when its own state was unchanged.

Extract SessionListItem component with React.memo and a display-prop
comparator. Rename input state moves into the row itself so other rows
don't re-render during typing. Callback identity is intentionally
excluded from the comparator — they only fire on user action, so
stale closures are not a correctness issue.

Biggest win for sessions lists with many entries (50+); no impact on
small lists.
useEffect depended on [isRenaming, session.title]; if SSE/polling
pushed a title update while the user was typing in the rename box,
the effect would reset renameValue to the new server title, losing
the user's draft.

Switch to ref-based transition detection: only sync renameValue on
the false→true edge of isRenaming. session.title captured via ref so
the effect reads the current value without re-firing on its changes.
Performance for large session DBs: default to displaying the last 24
chats with prev/next page controls. chats per page is configurable in
Settings (5-100, step 5). Subagent sessions (session.parentID set in
the OpenCode SDK schema) are now visually distinct — italic muted
title prefixed with ↳, 'Subagent · <preview>' description — and can
be hidden from the list via a setting.

- ChatPreferences: +chatsPerPage (default 24), +hideSubagentChats (default false)
- workspace.tsx: orderedSessions -> filteredSessions -> visibleSessions
  (sliced by current page); pagination row with prev/next IconButtons
  + 'Page X of Y · N chats' indicator only when totalChatsPages > 1
- SessionListItem title/description updated to show subagent marker
- Settings Voice section: NumericSlider (5-100) + SettingSwitchRow
  for chats-per-page and hide-subagent-chats

Page clamps automatically when list shrinks (delete/archive).
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