Problem: After migrating to pure Zustand, useDaemonSession returned only SessionState from the store, but components need both state AND imperative APIs (like sendAgentMessage, createAgent, etc.). Solution: Create DaemonSession type that combines: - SessionState (from Zustand store) - Imperative APIs (from SessionContext) - Wrapped store actions (getDraftInput, setFocusedAgentId, etc.) Changes: - Added DaemonSession type to use-daemon-session.ts - useDaemonSession now merges sessionState + context + store actions - Wraps store actions to bind serverId automatically - Updated prop types in components to use DaemonSession - Fixed use-session-directory.ts to use SessionState This restores component compatibility while keeping pure Zustand architecture. Still TODO: Fix remaining components that use SessionContext directly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
23 KiB
Host Management UX Overhaul
Completed Work (Summary)
The multi-daemon infrastructure is in place: session directory with daemon-scoped subscriptions, aggregated agent views, daemon-aware routing for agent/diff/file screens, React Query for connection state persistence, background reconnection with exponential backoff, useDaemonRequest for consistent async flows, structured logging, and architecture docs in docs/multi-daemon.md.
Guiding Principles
- Hosts are always connected when added—no manual "connect" action, no toggles.
- A stopped host is not an error—it's just a state. Errors only surface when the user tries to interact with an agent whose host is stopped.
- No "active" or "primary" concepts—actions that need a host require the user to choose explicitly.
- The home screen shows a flat list of agents with host name as metadata, not grouped by host.
Tasks
1. Rename "Daemon" to "Host" in UI
- Rename all user-facing labels, messages, and UI text from "daemon" to "host" throughout the app.
- Settings screen, connection banners, error messages, modals, etc.
- Internal code can keep "daemon" terminology; this is UI-only.
- Updated every user-facing string (settings, modals, placeholders, defaults) to say "host" and spot-checked via targeted searches; no automated tests were run.
- Context: Remaining "daemon" labels in git diff, file explorer, and agent detail screens now say "host" (updated
packages/app/src/app/git-diff.tsx,file-explorer.tsx,agent/[id].tsx, andagent/[serverId]/[agentId].tsx). - Review: Confirmed the agent redirect, file explorer, and git diff screens reflect the updated host wording with no lingering user-facing "daemon" strings.
- Review follow-up: Git Diff screen still showed the selected host as "Server"; updated the meta labels to "Host" to keep UI text consistent (
packages/app/src/app/git-diff.tsx). - Review follow-up (current): Found that the Git Diff header still surfaced the internal host ID instead of the friendly label; updated
GitDiffContentto accept the computedserverLabeland verified vianpm run typecheck --workspace=@paseo/app.
- Review follow-up: Remove the lingering "Server" wording on the Settings add-host form and restart alert so everything says "host."
- Context: The add-host input placeholder still reads "My Server" and the restart success alert title says "Server reachable" (
packages/app/src/app/settings.tsx:609,packages/app/src/app/settings.tsx:804-820). - Updated placeholder from "My Server" to "My Host" and alert title from "Server reachable" to "Host reachable"; verified with
npm run typecheck --workspace=@paseo/app.
- Context: The add-host input placeholder still reads "My Server" and the restart success alert title says "Server reachable" (
2. Remove Active/Primary/Auto-Connect Concepts
- Remove the concept of "active daemon" from the UI and simplify to just "hosts".
- Summary: Home/footer actions, create/import flows, and agent navigation now route directly via explicit host IDs with analytics/docs/aggregated list updates, verified with
npm run typecheck --workspace=@paseo/app. - Context:
GlobalFooter, the create/import modals, the agent screen, and the agent list previously read/writeactiveDaemonId, forcing a global active host before routing; they now rely on explicit host IDs instead.
- Summary: Home/footer actions, create/import flows, and agent navigation now route directly via explicit host IDs with analytics/docs/aggregated list updates, verified with
- Remove the concept of "primary daemon"—no default host for actions.
- Eliminated the persisted
activeDaemonId, flattened the session directory, and refactored_layout, realtime, settings, and agent UI to always resolve hosts explicitly so every host gets its ownSessionProvider; verified vianpm run typecheck --workspace=@paseo/app.
- Eliminated the persisted
- Remove the "auto-connect" toggle from host settings—hosts always auto-connect when added.
- Removed the toggle from the host form so entries always auto-connect and verified with
npm run typecheck --workspace=@paseo/app.
- Removed the toggle from the host form so entries always auto-connect and verified with
- Clean up any related state/UI that exposes these concepts to users.
- Removed the legacy autoConnect state from daemon profiles and session hosts so every host hydrates automatically, refreshed the host-unavailable messaging/docs, and verified via
npm run typecheck --workspace=@paseo/app.
- Removed the legacy autoConnect state from daemon profiles and session hosts so every host hydrates automatically, refreshed the host-unavailable messaging/docs, and verified via
Review: No Silent Defaults
- Review the changes to ensure we didn't just replace "active daemon" with "first host" (e.g.,
hosts[0]}). The goal is explicit user choice, not a hidden default.- Create/import modal now requires explicit host selection (no first entry default) and
useAggregatedAgentsno longer fabricates a host id viaconnectionStates.keys().next(); verified withnpm run typecheck --workspace=@paseo/app.
- Create/import modal now requires explicit host selection (no first entry default) and
3. Simplify Settings Screen
- Remove the standalone "Test Connection" form/URL input at the top of settings.
- Removed the global host selector/Test UI from
packages/app/src/app/settings.tsxso configuration happens per-host within their cards/forms, and rannpm run typecheck --workspace=@paseo/app.
- Removed the global host selector/Test UI from
- Keep the per-host "Test" button in each host row (already exists).
- Verified
SettingsScreenstill renders the per-host Test CTA inside eachDaemonCard, confirmed the button invokeshandleTestDaemonConnection, and rannpm run typecheck --workspace=@paseo/app.
- Verified
4. Transparent Connection Management
- When a host is added, the app auto-connects and keeps the connection alive.
- Added an AppState-aware reconnect path in
useWebSocketso every background session automatically reconnects when the app becomes active again, keeping newly added hosts online without manual intervention; verified withnpm run typecheck --workspace=@paseo/app.
- Added an AppState-aware reconnect path in
- Users should never need to manually "connect" to a host—the app handles it.
- Reworded realtime, settings restart, and agent creation/import flows so they explain hosts reconnect automatically instead of asking users to connect manually; verified with
npm run typecheck --workspace=@paseo/app.
- Reworded realtime, settings restart, and agent creation/import flows so they explain hosts reconnect automatically instead of asking users to connect manually; verified with
- Remove any UI that asks users to "connect" before performing actions.
- Updated the host-unavailable alert, agent-not-found message, and settings restart failure copy to reassure that hosts reconnect automatically instead of asking users to connect manually; verified with
npm run typecheck --workspace=@paseo/app.
- Updated the host-unavailable alert, agent-not-found message, and settings restart failure copy to reassure that hosts reconnect automatically instead of asking users to connect manually; verified with
5. Remove Host Status from Home Screen
- Remove connection status banners/indicators from the home screen—settings is the place to check host health.
- Context (review):
packages/app/src/app/index.tsx:47-144still rendersconnectionBannercards that surface per-host offline/error states directly on Home, so host health is still exposed outside Settings. - Review (2025-11-26): The
connectionIssuesfilter includesoffline,connecting, anderrorstatuses, so any host that isn'tonlineshows up in the banner. This contradicts the guiding principle that a stopped host is not an error. - Removed the connection status banner, associated state (
connectionIssues,statusColors), styles, and unused imports (useDaemonConnections,formatConnectionStatus,getConnectionStatusTone,Text,useUnistyles) frompackages/app/src/app/index.tsx; verified withnpm run typecheck --workspace=@paseo/app.
- Context (review):
6. Fix Error Philosophy
- A stopped/disconnected host is NOT an error—don't show error states on home screen just because a host is offline.
- Context (review): The home connection banner still prints destructive red
connectionErrortext for every offline host entry, so the screen treats normal downtime as an error (packages/app/src/app/index.tsx:123-140). - Review (2025-11-26): The
connectionErrorstyle unconditionally usestheme.colors.destructive(line 210-213) forlastErrormessages, even thoughgetConnectionStatusTonecorrectly returnswarning(amber) forofflinestatus. The dot color respects the tone, but the error text does not. - Review (2025-11-26, follow-up): N/A—Task 5 removed the connection banner entirely, so no error styling appears on the home screen now.
- Context (review): The home connection banner still prints destructive red
- Only show errors when the user tries to interact with an agent whose host is stopped.
- Review (2025-11-26): Verified in
agent-list.tsx:148-163—the action sheet shows a neutral "offline" message and disables the delete button; no error styling is used.
- Review (2025-11-26): Verified in
- Update connection banners/indicators to show neutral "offline" state instead of error styling.
- Review (2025-11-26): N/A—connection banners no longer exist on the home screen (removed in Task 5). Settings still shows host status but that's expected (settings is the place to check host health per the guiding principles).
- Make the Git Diff offline/unavailable state neutral and stop instructing users to "connect" manually.
- Context (update):
SessionUnavailableStateinpackages/app/src/app/git-diff.tsxnow uses neutral copy/styling that reassures users we auto-reconnect; verified withnpm run typecheck --workspace=@paseo/app.
- Context (update):
- Update the File Explorer offline state to match the new philosophy (neutral messaging, no manual connect CTA).
- Context (update):
FileExplorerSessionUnavailablenow shows the passive offline message with auto-reconnect guidance instead of destructive styles (packages/app/src/app/file-explorer.tsx).
- Context (update):
- Audit the agent detail flows for the same issue (offline agent screen + delete sheet) and replace the "connect this host" requirement with passive/offline messaging.
- Context (update):
AgentSessionUnavailableStateand the delete sheet subtitle inpackages/app/src/components/agent-list.tsxnow explain that offline hosts reconnect automatically, removing the manual "connect" instruction.
- Context (update):
7. Agent Creation Flow
- Remove reliance on "primary" or "active" daemon for agent creation.
- Review (2025-11-26): Verified that
create-agent-modal.tsxno longer uses or falls back to any "active" or "primary" daemon; the modal requires explicit host selection and shows "Select a host before creating or importing agents" when none is chosen.
- Review (2025-11-26): Verified that
- Require explicit host selection when creating an agent—user must choose where to deploy.
- Review (2025-11-26): The
selectedServerIdstate controls host selection, anddaemonAvailabilityErrorblocks creation until a host is selected (packages/app/src/components/create-agent-modal.tsx:336-342).
- Review (2025-11-26): The
- Fix the contradictory error message "Daemon is online, connect to it before creating"—this should never appear.
- Review (2025-11-26): Searched the codebase for this message pattern—no matches found. The message has been removed or never existed in the current code.
8. Fix Create Agent Modal Availability Check
- The create modal shows "[Host Name] is offline. We'll reconnect automatically..." even when the host is online.
- "Primary Daemon" is the host's label, not app copy—the availability check itself is broken.
- Both hosts are online but the modal thinks they're offline and blocks creation.
- Review (2025-11-26): Root cause identified at
packages/app/src/components/create-agent-modal.tsx:333-334. The check!session || selectedDaemonStatus !== "online" || !ws?.isConnectedhas a timing issue:sessioncomes fromuseSessionForServer(selectedServerId)which reads fromsessionAccessorssessionAccessorsis populated viaregisterSessionAccessorin auseEffectinsideSessionProvider- There's a timing window where
connectionStatesshows "online" butsessionis still null (SessionProvider hasn't run its registration effect yet) - Additionally, checking both
selectedDaemonStatus !== "online"AND!ws?.isConnectedis redundant sinceconnectionStatesstatus is derived fromwsinSessionProvider
- Fix: Use
connectionStatesas the single source of truth for availability. Replace the check withselectedDaemonStatus !== "online"without requiringsessionto exist for availability purposes. - Completed: Changed
selectedDaemonIsUnavailabletoselectedDaemonIsOfflinewhich only checksselectedDaemonStatus !== "online". Thesessionandws?.isConnectedchecks were removed from the availability condition sinceconnectionStatesalready reflects the true connection state. TheisTargetDaemonReadycheck still requiressessionfor actual operations (creating/resuming agents). Verified withnpm run typecheck --workspace=@paseo/app.
9. Home Screen Agent List
- Show a loading indicator while agents are being fetched (not while waiting for hosts to connect).
- Don't block the home screen for offline hosts.
- Handle edge cases: no hosts configured, no hosts connected, partial host connectivity.
- Context (review):
HomeScreenstill flips directly to the "New/Import Agent" empty state wheneveraggregatedAgentsis empty, so offline hosts with agents appear as if there are zero agents and there's no neutral loading indicator (packages/app/src/app/index.tsx:109-116). - Review (2025-11-26): This task is still open. The home screen shows an empty state immediately if
aggregatedCount === 0, with no distinction between "loading" and "truly empty." Offline hosts that may have agents will show zero agents until they reconnect. - Completed: Updated
useAggregatedAgentsto return{ groups, isLoading }whereisLoadingis true while the daemon registry loads or while hosts areconnectingwithout a session yet. UpdatedHomeScreento show anActivityIndicatorduring loading, then either the agent list or empty state. Offline hosts don't block loading—onlyconnectinghosts do.
- Remove grouping of agents by host—show a single flat list.
- Context (review):
packages/app/src/components/agent-list.tsx:67-131still mapsagentGroupsinto host-specific sections with headers, so grouping hasn't been removed. - Review (2025-11-26): Still open. The
AgentListcomponent iterates overagentGroupsand renders a section header (sectionLabel) per host. - Completed: Refactored
useAggregatedAgentsto return a flatAggregatedAgent[]array (each agent carriesserverIdandserverLabel). UpdatedAgentListto render a flat list with a host badge in each row's title area. Removed section headers and grouping logic entirely. - Each agent row displays its host name as metadata (badge, subtitle, etc.).
- Context (review): Agent rows only render cwd/provider/status/time (
packages/app/src/components/agent-list.tsx:87-125), so there's no host metadata visible per row yet. - Review (2025-11-26): Still open. To flatten the list, host metadata must move into each row since section headers will be removed.
- Completed: Added
hostBadgeto each agent row showingserverLabelin a muted badge next to the agent title.
- Context (review): Agent rows only render cwd/provider/status/time (
- Sort agents by recent activity or alphabetically (not by host).
- Context (review):
packages/app/src/hooks/use-aggregated-agents.ts:25-66sorts sections by host registration order and only orders agents within a host, so we still bias the list ordering by host rather than recency across all agents. - Review (2025-11-26): Still open. The current
useAggregatedAgentshook returns grouped data; it needs refactoring to return a flat, globally-sorted array. - Completed: Agents are now sorted globally by status (running first) then by most recent activity (
lastUserMessageAtorlastActivityAt), not by host.
- Context (review):
- Context (review):
9. Review: Git Diff Metadata Cleanup
- Remove the now-unused
routeServerIdprop fromGitDiffContent(packages/app/src/app/git-diff.tsx:84-104) so we aren't plumbing dead state through the component after switching toserverLabel.- Context: Deleted the redundant prop/const and updated
GitDiffContentto rely solely onserverLabel, removing the final host-id plumbing that was no longer used anywhere in the component (packages/app/src/app/git-diff.tsx).
- Context: Deleted the redundant prop/const and updated
Review Summary (2025-11-26, final review)
Completed:
- Tasks 1–9 are complete
- Internal code correctly uses "daemon" terminology while UI says "host"
- Connection banners removed from home screen
- Error philosophy fixed for Git Diff, File Explorer, and agent action sheets
- Settings screen retains host status indicators (correct per guiding principles)
- Create agent modal availability check fixed (uses
connectionStatesas single source of truth) - Home screen agent list is now a flat list sorted by activity with host badges per row
- Typecheck passes for both @paseo/server and @paseo/app workspaces
Final Review (2025-11-26):
- Verified no duplicate types or code patterns
- Confirmed no silent defaults (no
hosts[0],daemonEntries[0], orconnectionStates.keys().next()patterns) - Verified availability logic in create-agent-modal correctly uses
selectedDaemonStatus !== "online"without timing issues - Confirmed all offline states use neutral passive messaging about auto-reconnection
- All user-facing strings properly say "Host" (Settings add form, restart alerts, offline messages)
- No regressions or cut corners identified
All tasks complete. No follow-up items required.
Session State Performance Plan (2025-12-02)
Background
The Expo client exposes each daemon session (one per host) through SessionProvider, which builds a massive SessionContextValue object containing agents, stream timelines, messages, git/file explorer caches, audio flags, and all websocket helpers. Every render of that provider pushes the entire object into a hand-rolled global store (useSessionStore) that mimics Zustand but always replaces state.sessions[serverId] wholesale. Home, Settings, and the global footer subscribe to the whole sessions map, so any websocket tick forces those screens to recompute their derived data. On-device this manifests as:
- 2–3 s delays when navigating back from the agent view because Home must rebuild the aggregated agent list while streaming updates continue.
- Perceived jank on every button press (Settings, Import, etc.) due to large, synchronous object copies on the JS thread.
- Memory growth / eventual OOM because
messages,agentStreamState, and explorer caches never trim yet remain resident in the shared store even when no UI needs them.
Goals
- Make basic navigation (Home ⇄ Agent ⇄ Settings) feel instant by eliminating unnecessary re-renders.
- Adopt real Zustand so we can update fine-grained slices instead of copying the entire session payload.
- Keep heavy per-agent data inside
SessionProviderunless a screen explicitly opts in, reducing memory retention and serialization overhead. - Preserve multi-host support and current feature set (agent list, realtime, file explorer) while refactoring.
Non-Goals
- Changing server APIs or backend session semantics.
- Rewriting the React Navigation / expo-router structure.
- Shipping UI redesigns beyond what’s needed to validate the perf fix.
Current Pain Points
SessionProvider’suseMemo(value)depends on nearly every hook; each streamed token creates a brand-new object which is immediately spread into the store (updateSession(serverId, payload)atsrc/contexts/session-context.tsx:1731-1741).useSessionStorelacks structural sharing—selectors always receive fresh references, so memoization never kicks in.useAggregatedAgentsrecomputes the flattened list on every store update even when Home isn’t visible; JSX renders block navigation events.- Maps/Sets stored in Context mutate in-place, so cloning them for the store creates additional GC churn and prevents fine-grained equality checks.
- AsyncStorage persistence saves the full snapshot per host, so restoring a single host involves parsing huge JSON blobs regardless of what the user plans to view.
Proposed Approach
Phase 0 – Instrument & Baseline (0.5 day)
- Add lightweight logging around
useSessionStoreupdates (count + payload size) and record navigation timings (press → screen transition) using the RN profiler. - Validate assumptions on both an Android dev build and iOS simulator to set measurable targets (e.g., back navigation <300 ms).
Phase 1 – Extract Lightweight Agent Directory (0.5 day)
- Inside
SessionProvider, derive a minimal{id, serverId, title, status, lastActivityAt, cwd, provider}array. - Publish only that array into the shared store (or a new
AgentDirectorycontext) so Home/Settings can read it without touching the heavy session object. - Update
useAggregatedAgentsand any other consumers to rely on the minimal shape. This alone should make Home navigation responsive and provides a safety net while the full Zustand migration happens.
Phase 2 – Introduce Real Zustand Store (1 day)
- Replace
src/stores/session-store.tswithcreate()fromzustandplus thesubscribeWithSelectormiddleware for fine-grained updates. - Model state as
{ sessions: Record<serverId, SessionSlice> }, where eachSessionSliceonly includes data that needs to be globally observable (agents map, connection flags, optional metadata). Keep heavyweight transient data (messages, stream buffers) insideSessionProvider. - Refactor
SessionProvidermutators (setAgents,setPendingPermissions, etc.) to call the Zustand setters directly instead of copying entire maps. - Update hooks (
useSessionDirectory,useDaemonSession, footer) to select slices viauseSessionStore((state) => state.sessions[serverId]?.agents)so re-renders are scoped to the relevant server.
Phase 3 – Trim Heavy Session Data (0.5 day)
- Cap
messagesandagentStreamStatelengths per agent (e.g., last 200 entries) and keep them local to the agent screen unless explicitly required elsewhere. - Gate AsyncStorage snapshots to only persist the lightweight agent metadata that Home needs. Provide an opt-in debug toggle to persist everything for dev builds.
- Validate memory footprint before/after on a real device (Android Studio profiler or Xcode Instruments).
Phase 4 – Polish & Guardrails (0.5 day)
- Add React Profiler/Flipper traces to CI or docs so regressions are easier to catch.
- Document the new store architecture in
docs/perf-session-store.mdand note best practices (never spread the entire slice into other contexts, prefer selectors, etc.). - Audit remaining contexts (Realtime, footer) for similar anti-patterns and queue follow-up issues if needed.
Risks & Mitigations
- Map/Set mutability: Zustand selectors won’t detect deep mutations on Maps. Mitigation: convert to plain objects or always replace the Map when updating a slice (e.g.,
set(state => ({ ...state, agents: new Map(state.agents) }))). - AsyncStorage compatibility: Persisted snapshots may need migration logic to handle the new lightweight format. Solution: version the payload and fall back to a fresh session when parsing fails.
- Time overruns: If Phase 2 proves longer than expected, we can still ship Phase 1 (lightweight directory) to immediately unblock users, then continue iterating.
Success Metrics
- Back navigation from
/agent/[serverId]/[agentId]to/completes in <300 ms on Android dev build (measure viaPerformance.now()or React Profiler). useSessionStoreupdates drop by >80% when idle (no active runs), confirmed via instrumentation logs.- Heap usage on Android no longer grows unbounded during a 10-minute session that opens/closes multiple agents.
Meeting these targets should eliminate the “3 second back press” symptom and provide a scalable foundation for future multi-host features.