Skip to content

Comments

rho-web: UI overhaul + event-driven updates#27

Merged
mikeyobrien merged 71 commits intomainfrom
ui-improvements
Feb 20, 2026
Merged

rho-web: UI overhaul + event-driven updates#27
mikeyobrien merged 71 commits intomainfrom
ui-improvements

Conversation

@mikeyobrien
Copy link
Owner

@mikeyobrien mikeyobrien commented Feb 20, 2026

Summary

This PR bundles the ui-improvements branch work into main.

What is included

  • Major rho-web UI overhaul (composer, tool rendering, queue, layout, mobile polish)
  • Review workflow integration + deferred review inbox support
  • Event-driven updates for reviews and sessions (WebSocket push)
  • Server/client refactors to keep web files under 500 lines
  • Pre-commit backpressure gates (strict Biome + web 500-line limit)
  • RPC and memory API correctness fixes

Notes

  • Branch was pushed after final sync with main.
  • Local untracked planning folders under .agents/planning/2026-02-18-rho-web-multi-session were not included in this PR.

- User messages: right-aligned, green-tinted background, max 75% width
- Assistant messages: left-aligned, cyan border, max 85% width, min 40%
- System/error banners: centered
- Rounded corners (6px) on all message bubbles
- User message meta row reversed for natural right-align flow
- Mobile: wider bubbles (88-92% width)
- Cache-busted CSS version
Use fixed width instead of content-based sizing so all assistant
messages are the same width regardless of content length.
Move timestamp below the role name as a smaller subtitle instead of
floating on the opposite side. Applies to both user and assistant
messages. Fork button stays in the meta-right area.
Call ctx.ui.setStatus() with formatted usage bars so the data flows
through RPC to the web UI footer. Shows provider name, 5h/7d usage
bars with percentages, and extra spend if enabled.
Codex API returns 0-100 (used_percent), Claude returns 0-1
(utilization). Added toPct() to normalize both to 0-100, and clamped
formatBar() input to prevent negative repeat count crash.
scrollThreadToBottom was firing after Alpine's nextTick but before
the browser had laid out the new content. Adding requestAnimationFrame
ensures the DOM has painted before reading scrollHeight, so the scroll
target is the actual bottom — not the stale height from mid-render.
- File picker button (📎) and clipboard paste (Ctrl+V) for images
- Thumbnail preview strip with remove buttons above composer
- Images sent as base64 via RPC prompt command (same format as
  telegram extension)
- Inline image display in user message bubbles
- Falls back to 'Describe this image.' when sending images with no text
- Submit enabled when images attached even without text
Use align-items:stretch on the composer row so the textarea fills the
full height of the actions column (attach + send buttons).
The _programmaticScroll boolean flag was consumed by the first scroll
event, but setting scrollTop fires multiple scroll events (browser
reflows). With the rAF delay, the flag could also get consumed between
nextTick and the actual scroll. Replaced with a 150ms time window —
all scroll events within that window after a programmatic scroll are
ignored, preventing false userScrolledUp detection.
When the user types '/' in the composer textarea, a dropdown appears
showing available commands (skills, extensions, prompt templates)
filtered by the typed query. Supports:

- Substring filtering as the user types
- Arrow key navigation with wrap-around
- Enter/Tab to select, Escape to dismiss
- Click to select with mouseenter highlight
- Source badge (skill/extension/prompt) per command
- 'No matching commands' empty state
- Closes on prompt send and session switch

Data source is the existing slashCommands array populated via
the get_commands RPC call on session start.

🤖 Assisted by the code-assist SOP
…image reload fix

UX improvements:
- Sessions sidebar → slide-out overlay panel (☰ toggle in thread header)
  - Backdrop click / Escape to dismiss, auto-close on session select
  - Same behavior desktop + mobile, frees full width for chat thread
- Chat thread fills available viewport height (removed 65vh/60vh caps)
- Removed redundant // CHAT + Session viewer headers
- Wider messages: assistant 90%, user 82%
- Subtler grid background (rgba 0.02 vs hard border color)
- Compact thread header (tighter gap, centered alignment)
- Session titles allow 2-line wrapping with line-clamp
- Stripped double-box: .chat-view has no border/padding/bg

Bug fixes:
- Messages typed during streaming now queue and auto-send on agent_end
  instead of routing to sendSteer() which races with agent completion
  and silently discards the message
- Images in session history now render properly on reload instead of
  showing raw JSON (added image handler to normalizeContentItem)

Cleanup:
- Removed dead isMobileViewport(), sendSteer(), mobile-panel-toggle CSS
- Moved inline styles to .chat-thread-info CSS class
- queuedPrompt cleared on session switch
Part of rho-web no-build performance sweep (Phase 3)
- Add CSS variables for light theme in style.css
- Add theme toggle button in nav (sun/moon icons)
- Persist theme preference in localStorage
- Load theme on chat init
- Detect edit/write tools and parse output for diff info
- Add toggle to show diff view with syntax highlighting
- Show file path and line change stats
- Color-code additions (green) and deletions (red)
- Stop polling when user is idle for 5 minutes
- Stop polling when tab is hidden (Page Visibility API)
- Resume polling when user becomes active or tab becomes visible
- Reduces unnecessary network traffic when user is away
…post-submit redirect

1. Add rho home + review lobby links in both lobby header and review nav bar
2. Add cancel (✕) button on active sessions in lobby, with DELETE endpoint
3. Fix comment form spacing — inherited white-space:pre from code-block caused
   excessive gaps between form elements
4. Redirect to /review lobby after submit (1.5s) or cancel (1s) instead of
   showing dead 'close this tab' page
5. Apply biome formatting to server.ts and review.js, fix import sort order
6. Add scripts/check-line-limit.sh for pre-commit line limit gate
- Import proper types from brain-store (BehaviorEntry, BrainEntry, etc.)
- Import Hono Context type for requireReviewToken parameter
- Replace MemoryEntries any[] with concrete entry types
- Replace baseEntries: any[] with BrainEntry[]
- Add field() helper to safely access union properties via Record<string, unknown>
- Replace (e as any).field patterns with field(e, key) calls
- Type WebSocket message as { type?: string; comments?: unknown[] }
- Type new entries as BrainEntry | undefined with guard
- Add scripts/check-line-limit.sh for pre-commit gate

0 errors, 0 warnings.
The flex chain was broken at .app — it wasn't a flex container,
so .main's flex:1 had no effect and nothing filled the viewport.
The height:100vh on chat-thread/chat-layout was a workaround that
didn't propagate properly through the intermediate containers.

Fix: make .app a proper flex participant (display:flex, flex:1)
so the full chain body→app→main→view→chat-layout→chat-thread
resolves correctly. Replace height:100vh hacks with flex:1.
The sessions overlay was width:320px / max-width:85vw, leaving a
useless strip of chat visible on the right side of mobile screens.

At ≤720px: use calc(100vw - 3rem) — nearly full width, leaving
just enough backdrop visible to tap-to-close.
At ≤400px: calc(100vw - 2.5rem) — tighter but still tappable.
The 720px and 400px media queries were overriding card padding to
0.3rem, squashing the title into the meta row. Fixed by:
- Restoring proper padding and gap in mobile breakpoints
- Clamping title to 1 line on mobile (was 2, causing bleed)
- Adding explicit max-height as a safety net for -webkit-line-clamp
The template called formatTimestampShort() but only
_formatTimestampShort() existed as a module-private function —
never exposed on the Alpine component. The call silently returned
undefined, so timestamps were blank. Now exposed properly;
shows relative times like '3h ago', '2d ago' in session cards.
Redundant now that sessions use a hamburger popover.

Removes:
- Maximized top bar overlay and exit button
- Fullscreen toggle button in thread header
- chatMaximized state, toggleMaximized/enter/exitMaximized methods
- Escape key handler for exiting maximized mode
- localStorage persistence for maximized state
- All body.chat-maximized CSS overrides (~90 lines)
Pi's SessionStats.tokens is { input, output, cacheRead, cacheWrite, total },
not a plain number. The nullish coalescing chain passed the object through
directly, rendering as '[object Object]' in the footer.

Now destructures the tokens object when present, extracting .total for the
aggregate display and individual fields for the breakdown stats.
- Fix nav tab href: /review/lobby.html → /review (matched wrong route)
- Fix reviewApp function name: _reviewApp → reviewApp (Alpine couldn't init)
- Move cancel button outside <a> tag in lobby to prevent navigation on click
- Use event delegation with data attributes instead of inline onclick
- Add .session-row wrapper for proper flex layout with sibling button
Two bugs fixed:

1. User message not scrolled to: _programmaticScrollUntil was only set
   inside the $nextTick->rAF callback, so scroll events fired during
   Alpine's DOM rerender (between push and rAF) slipped through the
   guard and re-set userScrolledUp=true. Fix: set the guard immediately
   in scrollThreadToBottom (300ms), then refresh it in the rAF (150ms).

2. Autoscroll stops working: handleThreadScroll set userScrolledUp based
   purely on distance from bottom. During streaming, content grows below
   the viewport, so any scroll event after the 150ms guard (even a tiny
   trackpad impulse) would permanently stick userScrolledUp=true. Fix:
   track scroll direction -- only set userScrolledUp when the user
   actively scrolled upward by >=10px. Also re-enable autoscroll when
   user scrolls back near the bottom (<=80px), eliminating the need to
   click the New messages button.
- Composer now stacks vertically on mobile: textarea gets full width,
  Send/attach/follow-up buttons row below it
- Reduced message text from 0.85rem to 0.78rem (720px) / 0.72rem (400px)
- Smaller usage line and message meta text
- Composer font-size 14px (was 16px) — adequate on modern Android
- Textarea gets more vertical room (min 2.5rem, max 8rem)
…ayout

Composer:
- Move send/clip buttons below textarea (matches mobile layout)
- Bump base font-size from 15px to 18px
- Double send button width for better touch target
- Remove follow-up button, unify into queue

Tool rendering:
- Semantic views render immediately on tool completion (not next message)
- Tool outputs carry over through handleMessageEnd finalization
- toolResult messages merge into assistant tool_call parts during streaming
- Edit tools show colored +N / −N diff pills (green/red background)
- Full paths in collapsed headers on desktop, filename-only on mobile
- Fix tool-path-badge stretching (remove flex:1)

Message queue:
- Replace single queuedPrompt with proper promptQueue array
- Queue bar UI above composer: collapsible, editable items
- Per-item image attachments with add/remove
- Merge-down button to combine adjacent queue items
- Queue drains in order on agent_end

Layout unification:
- Full-width user/assistant message blocks on desktop
- Show footer on mobile (was hidden)
- Show nav-title and thread-meta on mobile
- Bold role labels (ASSISTANT/USER)
- Stronger green background on user messages
- Remove redundant mobile CSS overrides

Context usage:
- Show ctx % on each assistant message (model context window lookup)
- Desktop: model · ctx% · tokens · cost · cache
- Mobile: model · ctx% · cost

Other:
- Fix brain/config view scroll (was clipped by overflow:hidden)
- PWA manifest: add display_override
When a duplicate RPC command is detected and a cached response is
returned, the response message now includes the original sequence
number. This ensures consistent event ordering on the client and
prevents potential issues with lastRpcEventSeq tracking during
reconnection scenarios.

Found during fresh-eyes bug sweep.
- Wrap stdin.write() in try-catch to handle EPIPE and other write failures
- Handle backpressure by listening for drain event when write returns false
- Emit rpc_error event on write failure and stop the session gracefully

Found during fresh-eyes bug sweep.
RPC children were incorrectly inheriting RHO_SUBAGENT=1 from the
parent process, which caused a test failure. Explicitly set
RHO_SUBAGENT=undefined to ensure RPC workers don't incorrectly
identify as subagents.

Found during fresh-eyes bug sweep.
- brain-tool.ts: Don't overwrite 'created' when updating entries
  - handleUpdate: preserved original created date
  - handleTaskDone: preserved original created date
  - handleReminderRun: preserved original created date
- brain-store.ts: Fix getInjectedIds budget handling for preferences
  - Now adds IDs before checking budget (matches buildBrainPrompt)
  - Properly redistributes unused budget to learnings

This fixes decay logic which relies on entry age calculated from created date.
- server.ts: PUT /api/memory/:id was only searching learnings + preferences,
  preventing updates to behaviors, identity, user, contexts, tasks, reminders
- memory.js: isStale() was checking entry.last_used which API never returns,
  so it always returned false. Added comment explaining this.
- treat empty sessions with a backing file as interactive in chat UI

- send prompt RPCs using sessionFile when no rpc session id exists yet

- return session file from /api/sessions/:id so the client can bootstrap RPC

- force revalidation for /js modules to avoid stale browser module graphs
- add ws ui_event broadcaster and server-side review/git change emits

- replace review badge/dashboard polling with event-driven refresh

- force reconnect banner retry to replace stale/open sockets

- remove legacy /review lobby page and related css
- Server broadcasts sessions_changed event on session create/fork
- Client listens for rho:ui-event and refreshes session list
- Completes Phase 3 of no-build performance sweep

This replaces the 15-second polling interval with server-push updates,
significantly reducing unnecessary network requests when sessions change.
- add rough idea + requirements honing notes for rho-web multi-session

- add chat.js refactor plan

- update no-build perf sweep task status to complete
- apply stored light/dark theme to standalone review page and switch hljs styles\n- fix excessive vertical spacing in review code lines\n- add favicon links to review page\n- add UI improvements changelog section to release-notes draft
@mikeyobrien mikeyobrien merged commit b4a891c into main Feb 20, 2026
1 check passed
@mikeyobrien mikeyobrien deleted the ui-improvements branch February 20, 2026 16:12
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