Skip to content

feat(ui): implement recursive nested session expansion#478

Open
detain wants to merge 2 commits into
NeuralNomadsAI:devfrom
detain:feat/recursive-nested-expansion
Open

feat(ui): implement recursive nested session expansion#478
detain wants to merge 2 commits into
NeuralNomadsAI:devfrom
detain:feat/recursive-nested-expansion

Conversation

@detain
Copy link
Copy Markdown

@detain detain commented May 18, 2026

This change enables the UI to display nested sessions within nested sessions in a foldable display, recursively, up to 10 levels deep.

What Changed

Core Data Structure (session-state.ts)

  • Redefined SessionThread type to support true recursive nesting:
    • Old: { parent: Session, children: Session[], latestUpdated: number }
    • New: { session: Session, children: SessionThread[], depth: number, hasChildren: boolean, latestUpdated: number }
  • Renamed expandedSessionParents signal to expandedSessions to reflect that ANY session with children can now be expanded, not just top-level parents
  • Updated getSessionThreads() to build a recursive tree structure using new buildSessionThreadTree() and computeThreadSignature() helpers
  • Updated getSessionFamily() to recursively collect ALL descendants, not just direct children
  • Updated getVisibleSessionIds() with collectVisibleSessionIds() helper to recursively collect visible session IDs based on expansion state
  • Maintained backward compatibility aliases for renamed functions: isSessionParentExpanded, setSessionParentExpanded, etc.

Session State Exports (sessions.ts)

  • Updated imports and exports to use the new function names: ensureSessionExpanded, isSessionExpanded, setSessionExpanded, toggleSessionExpanded

Session Events (session-events.ts)

  • Updated ensureSessionParentExpandedensureSessionExpanded in auto-expand logic for child sessions that start working

Permission Modal (permission-approval-modal.tsx)

  • Updated import and usage of ensureSessionParentExpandedensureSessionExpanded

UI Rendering (session-list.tsx)

  • Updated SessionRow component to accept session object directly instead of sessionId, plus depth and isLastChild props
  • Derived isChild from depth > 0 instead of explicit prop
  • Added depthClass() for CSS depth-based indentation
  • Created new SessionThreadRow recursive component that:
    • Renders the current session via SessionRow
    • If expanded and has children, recursively renders children with increased depth
  • Updated filteredThreads with subtreeHasMatch() and filterThreadTree() helpers for recursive filtering
  • Updated allMatchingSessionIds with collectThreadIds() helper for recursive ID collection
  • Removed child-specific Bot icon - all sessions now use User icon
  • Updated expander visibility to show for ANY session with children, regardless of depth

Styling (session-layout.css)

  • Added depth-based CSS classes .session-item-depth-{1-10} with:
    • Progressive indentation: 2.25rem for depth 1, up to 13.5rem for depth 10
    • Tree connector styling via ::before and ::after pseudo-elements
    • Proper vertical line handling for last-child at each depth level

Tests (session-state.test.ts)

  • Added comprehensive test suite (683 lines) covering:
    • getSessionThreads: empty sessions, single sessions, single-level children, multi-level nested children, sorting, hasChildren computation
    • getSessionFamily: recursive descendant collection
    • Expansion state: toggle, explicit set, ensure logic
    • getVisibleSessionIds: visibility based on expansion state at multiple levels

User-Facing Behavior

  • Nested sessions can now be collapsed/expanded at any depth level
  • A chevron expander appears on any session that has children
  • Children are indented based on their nesting depth
  • Tree lines connect parent-child relationships visually
  • Expanding a parent auto-expands ancestors when selecting a deeply nested child session

Edge Cases Handled

  • Sessions with no children show no expander
  • Last child at each depth level has shortened vertical tree line
  • Thread sorting by latestUpdated works correctly with nested updates
  • Cache invalidation properly tracks thread changes at all depth levels

Implementation Notes

  • Depth is limited to 10 levels to prevent excessive indentation
  • The hasChildren flag is computed once during tree building for performance
  • The Session type's parentId field already supported arbitrary nesting - only the UI rendering needed to be updated

This change enables the UI to display nested sessions within nested sessions
in a foldable display, recursively, up to 10 levels deep.

## What Changed

### Core Data Structure (session-state.ts)
- Redefined `SessionThread` type to support true recursive nesting:
  - Old: `{ parent: Session, children: Session[], latestUpdated: number }`
  - New: `{ session: Session, children: SessionThread[], depth: number,
            hasChildren: boolean, latestUpdated: number }`
- Renamed `expandedSessionParents` signal to `expandedSessions` to reflect
  that ANY session with children can now be expanded, not just top-level parents
- Updated `getSessionThreads()` to build a recursive tree structure using
  new `buildSessionThreadTree()` and `computeThreadSignature()` helpers
- Updated `getSessionFamily()` to recursively collect ALL descendants, not
  just direct children
- Updated `getVisibleSessionIds()` with `collectVisibleSessionIds()` helper
  to recursively collect visible session IDs based on expansion state
- Maintained backward compatibility aliases for renamed functions:
  `isSessionParentExpanded`, `setSessionParentExpanded`, etc.

### Session State Exports (sessions.ts)
- Updated imports and exports to use the new function names:
  `ensureSessionExpanded`, `isSessionExpanded`, `setSessionExpanded`,
  `toggleSessionExpanded`

### Session Events (session-events.ts)
- Updated `ensureSessionParentExpanded` → `ensureSessionExpanded`
  in auto-expand logic for child sessions that start working

### Permission Modal (permission-approval-modal.tsx)
- Updated import and usage of `ensureSessionParentExpanded` →
  `ensureSessionExpanded`

### UI Rendering (session-list.tsx)
- Updated `SessionRow` component to accept `session` object directly
  instead of `sessionId`, plus `depth` and `isLastChild` props
- Derived `isChild` from `depth > 0` instead of explicit prop
- Added `depthClass()` for CSS depth-based indentation
- Created new `SessionThreadRow` recursive component that:
  - Renders the current session via SessionRow
  - If expanded and has children, recursively renders children with
    increased depth
- Updated `filteredThreads` with `subtreeHasMatch()` and `filterThreadTree()`
  helpers for recursive filtering
- Updated `allMatchingSessionIds` with `collectThreadIds()` helper for
  recursive ID collection
- Removed child-specific `Bot` icon - all sessions now use `User` icon
- Updated expander visibility to show for ANY session with children,
  regardless of depth

### Styling (session-layout.css)
- Added depth-based CSS classes `.session-item-depth-{1-10}` with:
  - Progressive indentation: 2.25rem for depth 1, up to 13.5rem for depth 10
  - Tree connector styling via `::before` and `::after` pseudo-elements
  - Proper vertical line handling for last-child at each depth level

### Tests (session-state.test.ts)
- Added comprehensive test suite (683 lines) covering:
  - `getSessionThreads`: empty sessions, single sessions, single-level
    children, multi-level nested children, sorting, hasChildren computation
  - `getSessionFamily`: recursive descendant collection
  - Expansion state: toggle, explicit set, ensure logic
  - `getVisibleSessionIds`: visibility based on expansion state at
    multiple levels

## User-Facing Behavior
- Nested sessions can now be collapsed/expanded at any depth level
- A chevron expander appears on any session that has children
- Children are indented based on their nesting depth
- Tree lines connect parent-child relationships visually
- Expanding a parent auto-expands ancestors when selecting a deeply
  nested child session

## Edge Cases Handled
- Sessions with no children show no expander
- Last child at each depth level has shortened vertical tree line
- Thread sorting by latestUpdated works correctly with nested updates
- Cache invalidation properly tracks thread changes at all depth levels

## Implementation Notes
- Depth is limited to 10 levels to prevent excessive indentation
- The `hasChildren` flag is computed once during tree building for
  performance
- The Session type's `parentId` field already supported arbitrary
  nesting - only the UI rendering needed to be updated
@github-actions
Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/26020161634

Artifacts expire in 7 days.
Artifacts:

  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-tauri-macos
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-tauri-linux
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-tauri-windows
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-electron-macos
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-tauri-macos-arm64
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-electron-windows
  • pr-478-962df25ff6bfe2d2ab4b7f34838a774a72f50ce8-electron-linux

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented May 18, 2026

Hi @detain

Thanks for the PR.
How does this work with Opencode.
As I understand any subagent can't launch another subagent.
How are sessions created under subagent?

Thanks

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

as far as i know, both opencode and this already supported nested recursive sessions just not in the ui ... i dont think ive seen opencode 5 subagents deep but 3 levels for sure all the time and maybe 4 .. through this ui too .. like if you go to the nested agent you can see its calling other subagents ... so at least 3 levels deep.. maybe its a permission thing in opencode, i do tend to have mine a bit open.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented May 18, 2026

@detain

Would you be able to give me a working prompt / agent config / opencode config that allows you to do this please.
I would like to test it before I could merge it

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

it it wasn't until an hour ago i figured out how to run the local dev version to test it and it needs some improvements first so its not ready for merge. i was a bit premature it seems... will respond with a fix and additional info about the nested sub-agents,

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

image im wrong it seems.. it did work fine, i just had to go to packages/ui and run `npm run build` then from the server dir pass ` --ui-auto-update false --ui-dir /home/sites/CodeNomad/packages/ui/dist` to the npx tsc src/index.ts call to get it to read the local changes to ui. after that it worked. finding out if i had anything custom done in my opencode setup, but the UI changes are working it is displaying the recursion properly now

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

err i meant --ui-auto-update false --ui-dir /home/sites/CodeNomad/packages/ui/src/renderer/dist

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

What lets your subagents spawn more subagents is just config — specifically that the task tool is granted to them:
~/.config/opencode/opencode.json has global permission.task: "allow"
agent.build has task: allow (it's an orchestrator)
agent.plan has task: allow
The built-in general agent inherits task: allow from the global permission, which is why your L1-L4 chain (all general) just kept spawning

@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

so just set task to allow and they spawn sub-agents recursively .. to utilize them going more than 3 levels deep on their own though the prompt or instructions would probably be needed to give it some direction.

The previous signature only included direct children's IDs (without their
updated times), so live SSE events for grandchildren and deeper descendants
would not invalidate the memoized thread tree until a page refresh. Walk
the entire descendant subtree and include each node's updated time so any
deep change re-renders correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@detain
Copy link
Copy Markdown
Author

detain commented May 18, 2026

fixed a cache invalidation bug in the nested expansion where it was not fully walking the descendants on updates so you would have to refresh the screen to see the new members, this is now fixed.

@github-actions
Copy link
Copy Markdown

PR builds are available as GitHub Actions artifacts:

https://github.com/NeuralNomadsAI/CodeNomad/actions/runs/26057499810

Artifacts expire in 7 days.
Artifacts:

  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-tauri-macos
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-tauri-linux
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-tauri-windows
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-electron-macos
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-tauri-macos-arm64
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-electron-windows
  • pr-478-474c95b9999f6106a7788eb8a791979a87992dc9-electron-linux

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.

2 participants