Overhauls the message timeline sidebar with segment-level selection,
token-aware xray histogram bars, and messageId-based grouping — replacing
the previous message-level selection and positional adjacency logic.
## Selection System (SELECTION-SYSTEM)
- Dual-level selection: `selectedTimelineIds` (segment IDs) as the
source of truth, bridged to `selectedForDeletion` (message IDs) via
a reactive `createEffect`.
- CTRL+Click: toggles individual segments. Clicking an assistant parent
with unexpanded tools expands the group and selects all members.
Re-clicking collapses and deselects.
- SHIFT+Click: range selection. Direction follows anchor state — if the
anchor is selected the range is additive; if not, subtractive.
- Escape: clears all selection via a global keydown listener.
- Long-press (500ms, 10px jitter tolerance): mobile/touch selection
via pointer events with context-menu suppression.
- Scroll anchor preservation: captures badge offsetTop before toggling
visibility, restores scrollTop after layout shift.
## Token Count Fix (TOKEN-COUNT-FIX)
- New `getPartCharCount()` estimates characters for any `ClientPart`.
Handles text, tool state (input/output/metadata), and content arrays.
- **Skips `filediff` metadata key** — this key contains full before/after
file content that inflated character counts by 10-100x.
- `totalChars` field added to `TimelineSegment` and `PendingSegment`,
accumulated during `buildTimelineSegments()`.
## Scroll Performance (SCROLL-PERF)
- Two-tier positioning replaces per-badge `getBoundingClientRect` on
every scroll event:
1. `computeBadgeLayout()` — expensive pass, runs once on activation,
resize, or expansion. Stores `layoutTop` relative to scroll content.
2. `handleScrollRaf()` — RAF-throttled, reads 1 container rect per
frame. Derives all badge screen positions arithmetically.
- `clipBounds` subtracts delete toolbar height + 16px gap when toolbar
is visible, preventing xray bars from overlapping the toolbar.
## Group Logic (GROUP-LOGIC)
- `getAdjacentGroup()`: changed from backward positional walk to
`segments.filter(s => s.messageId === clicked.messageId)`. Fixes
cross-message group overlap when consecutive tool segments belong to
different assistant messages.
- `groupRole()`: checks for sibling tools via `messageId`.
- `isGroupStart()`: checks previous segment's `messageId`.
- Only assistant badges trigger group selection; tool and user badges
are always standalone.
## Active Highlight (ACTIVE-HIGHLIGHT)
- Renamed `activeMessageId` → `activeSegmentId` (signal, prop, and
comparison). Clicking a badge now highlights only that specific badge,
not all badges sharing the same messageId.
- Intersection observer resolves messageId → first segment's id.
- Auto-scroll effect uses segment id directly (no `.find()` lookup).
## XRay Histogram Bars (XRAY-BARS)
- Portal-based overlay with two bars per segment:
- Relative bar: width = tokens/maxTokens, green-to-red gradient.
- Absolute bar: width = tokens/10000 (capped), grey, with red glow
overflow indicator when tokens exceed ABSOLUTE_TOKEN_CAP (10K).
- Token labels as pill-shaped badges (white bg, dark border, 12px font,
1.5rem height matching badge height) at the left tip of each bar.
- `liveSegmentChars` memo fetches fresh char counts from the message
store to handle stale tool output that arrived after segment creation.
- `aggregateTokensByMessageId` memo: O(n) pre-computation replacing the
previous O(n²) per-segment iteration inside `<For>`.
- `clip-path: inset(...)` clips bars at layout edges.
## Delete Toolbar Token Display (TOKEN-TOTAL-IN-TOOLBAR)
- Removed `outputTokensByMessageId` (backend `entry.outputTokens` only
counted assistant output, missing tool result content entirely).
- `selectedTokenTotal` now sums `seg.totalChars` across all segments
for each selected messageId, divides by 4. Consistent with xray bars.
- Three color-coded pills: Before (muted, current context), Selection
(red, tokens being removed), After (green, remaining after deletion).
Eliminates mental arithmetic for users targeting a context token count.
## Delete Hover Fix
- Removed `selected.has(segment.messageId)` → `return true` from
`isDeleteHovered()`. The red delete overlay now only activates from
actual hover interactions (kind === "message" or "deleteUpTo"), not
from the selection state. This prevents the red overlay from masking
the blue segment-level selection highlight.
## CSS Changes
- message-selection.css: Restyled toolbar with accent-primary scheme,
three-pill token group, button variants (--delete, --cancel), hint.
- message-timeline.css: Selection styling (!important overrides), group
indicators (left border), xray overlay (fixed fullscreen, z-index 40),
rib/bar/label styles, container layout, stacking context isolation.
## Files Changed
- packages/ui/src/components/message-section.tsx (+345/-197)
- packages/ui/src/components/message-timeline.tsx (+671/-199)
- packages/ui/src/lib/i18n/messages/en/messaging.ts (+1/-2)
- packages/ui/src/styles/messaging/message-selection.css (+107/-34)
- packages/ui/src/styles/messaging/message-timeline.css (+146/-0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 handlersapi– HTTP/API calls and workspace lifecyclesession– Session/model state, prompt handling, tool callsactions– 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.