CodeNomad/packages/ui
Joe Huss 34706228bb
feat(ui): implement recursive nested session expansion (#478)
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

---------

Co-authored-by: Pascal André <pascalandr@gmail.com>
2026-07-12 01:18:31 +02:00
..
scripts refactor(build): share Monaco public asset copy helper 2026-02-10 10:49:05 +00:00
src feat(ui): implement recursive nested session expansion (#478) 2026-07-12 01:18:31 +02:00
.gitignore feat(ui): add Monaco changes/files right drawer viewers 2026-02-09 21:00:40 +00:00
package.json fix(ui): scope project session list requests (#565) 2026-06-19 17:42:12 +01:00
postcss.config.js Split workspace into electron and ui packages 2025-11-17 12:06:58 +00:00
README.md Expose UI logger controls globally 2025-12-06 12:17:33 +00:00
tailwind.config.js Split workspace into electron and ui packages 2025-11-17 12:06:58 +00:00
tsconfig.json Split workspace into electron and ui packages 2025-11-17 12:06:58 +00:00
vite.config.ts perf(ui): lazy-load markdown and defer diff rendering (#215) 2026-03-22 11:54:05 +00:00

CodeNomad UI

This package contains the frontend user interface for CodeNomad, built with SolidJS and Tailwind CSS.

Overview

The UI is designed to be a high-performance, low-latency cockpit for managing OpenCode sessions. It connects to the CodeNomad server (either running locally via CLI or embedded in the Electron app).

Features

  • SolidJS: Fine-grained reactivity for high performance.
  • Tailwind CSS: Utility-first styling for rapid development.
  • Vite: Fast build tool and dev server.

Development

To run the UI in standalone mode (connected to a running server):

npm run dev

This starts the Vite dev server at http://localhost:3000.

Building

To build the production assets:

npm run build

The output will be generated in the dist directory, which is then consumed by the Server or Electron app.

Debug Logging

The UI now routes all logging through a lightweight wrapper around debug. The logger exposes four namespaces that can be toggled at runtime:

  • sse Server-sent event transport and handlers
  • api HTTP/API calls and workspace lifecycle
  • session Session/model state, prompt handling, tool calls
  • actions User-driven interactions in UI components

You can enable or disable namespaces from DevTools (in dev or production builds) via the global window.codenomadLogger helpers:

window.codenomadLogger?.listLoggerNamespaces() // => [{ name: "sse", enabled: false }, ...]
window.codenomadLogger?.enableLogger("sse") // turn on SSE logs
window.codenomadLogger?.disableLogger("sse") // turn them off again
window.codenomadLogger?.enableAllLoggers() // optional helper

Enabled namespaces are persisted in localStorage under opencode:logger:namespaces, so your preference survives reloads.