rho-web: UI overhaul + event-driven updates#27
Merged
mikeyobrien merged 71 commits intomainfrom Feb 20, 2026
Merged
Conversation
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR bundles the ui-improvements branch work into main.
What is included
Notes