Files
paseo/plan.md
Mohamed Boudra e8aa08159a Fix useDaemonSession to return combined state + APIs
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>
2025-12-02 14:27:57 +00:00

23 KiB
Raw Blame History

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, and agent/[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 GitDiffContent to accept the computed serverLabel and verified via npm 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.

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/write activeDaemonId, forcing a global active host before routing; they now rely on explicit host IDs instead.
  • 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 own SessionProvider; verified via npm run typecheck --workspace=@paseo/app.
  • 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.
  • 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.

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 useAggregatedAgents no longer fabricates a host id via connectionStates.keys().next(); verified with npm run typecheck --workspace=@paseo/app.

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.tsx so configuration happens per-host within their cards/forms, and ran npm run typecheck --workspace=@paseo/app.
  • Keep the per-host "Test" button in each host row (already exists).
    • Verified SettingsScreen still renders the per-host Test CTA inside each DaemonCard, confirmed the button invokes handleTestDaemonConnection, and ran npm run typecheck --workspace=@paseo/app.

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 useWebSocket so every background session automatically reconnects when the app becomes active again, keeping newly added hosts online without manual intervention; verified with npm run typecheck --workspace=@paseo/app.
  • 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.
  • 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.

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-144 still renders connectionBanner cards that surface per-host offline/error states directly on Home, so host health is still exposed outside Settings.
    • Review (2025-11-26): The connectionIssues filter includes offline, connecting, and error statuses, so any host that isn't online shows 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) from packages/app/src/app/index.tsx; verified with npm run typecheck --workspace=@paseo/app.

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 connectionError text 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 connectionError style unconditionally uses theme.colors.destructive (line 210-213) for lastError messages, even though getConnectionStatusTone correctly returns warning (amber) for offline status. 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.
  • 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.
  • 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): SessionUnavailableState in packages/app/src/app/git-diff.tsx now uses neutral copy/styling that reassures users we auto-reconnect; verified with npm run typecheck --workspace=@paseo/app.
  • Update the File Explorer offline state to match the new philosophy (neutral messaging, no manual connect CTA).
    • Context (update): FileExplorerSessionUnavailable now shows the passive offline message with auto-reconnect guidance instead of destructive styles (packages/app/src/app/file-explorer.tsx).
  • 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): AgentSessionUnavailableState and the delete sheet subtitle in packages/app/src/components/agent-list.tsx now explain that offline hosts reconnect automatically, removing the manual "connect" instruction.

7. Agent Creation Flow

  • Remove reliance on "primary" or "active" daemon for agent creation.
    • Review (2025-11-26): Verified that create-agent-modal.tsx no 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.
  • Require explicit host selection when creating an agent—user must choose where to deploy.
    • Review (2025-11-26): The selectedServerId state controls host selection, and daemonAvailabilityError blocks creation until a host is selected (packages/app/src/components/create-agent-modal.tsx:336-342).
  • 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?.isConnected has a timing issue:
      1. session comes from useSessionForServer(selectedServerId) which reads from sessionAccessors
      2. sessionAccessors is populated via registerSessionAccessor in a useEffect inside SessionProvider
      3. There's a timing window where connectionStates shows "online" but session is still null (SessionProvider hasn't run its registration effect yet)
      4. Additionally, checking both selectedDaemonStatus !== "online" AND !ws?.isConnected is redundant since connectionStates status is derived from ws in SessionProvider
    • Fix: Use connectionStates as the single source of truth for availability. Replace the check with selectedDaemonStatus !== "online" without requiring session to exist for availability purposes.
    • Completed: Changed selectedDaemonIsUnavailable to selectedDaemonIsOffline which only checks selectedDaemonStatus !== "online". The session and ws?.isConnected checks were removed from the availability condition since connectionStates already reflects the true connection state. The isTargetDaemonReady check still requires session for actual operations (creating/resuming agents). Verified with npm 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): HomeScreen still flips directly to the "New/Import Agent" empty state whenever aggregatedAgents is 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 useAggregatedAgents to return { groups, isLoading } where isLoading is true while the daemon registry loads or while hosts are connecting without a session yet. Updated HomeScreen to show an ActivityIndicator during loading, then either the agent list or empty state. Offline hosts don't block loading—only connecting hosts do.
  • Remove grouping of agents by host—show a single flat list.
    • Context (review): packages/app/src/components/agent-list.tsx:67-131 still maps agentGroups into host-specific sections with headers, so grouping hasn't been removed.
    • Review (2025-11-26): Still open. The AgentList component iterates over agentGroups and renders a section header (sectionLabel) per host.
    • Completed: Refactored useAggregatedAgents to return a flat AggregatedAgent[] array (each agent carries serverId and serverLabel). Updated AgentList to 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 hostBadge to each agent row showing serverLabel in a muted badge next to the agent title.
    • Sort agents by recent activity or alphabetically (not by host).
      • Context (review): packages/app/src/hooks/use-aggregated-agents.ts:25-66 sorts 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 useAggregatedAgents hook 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 (lastUserMessageAt or lastActivityAt), not by host.

9. Review: Git Diff Metadata Cleanup

  • Remove the now-unused routeServerId prop from GitDiffContent (packages/app/src/app/git-diff.tsx:84-104) so we aren't plumbing dead state through the component after switching to serverLabel.
    • Context: Deleted the redundant prop/const and updated GitDiffContent to rely solely on serverLabel, removing the final host-id plumbing that was no longer used anywhere in the component (packages/app/src/app/git-diff.tsx).

Review Summary (2025-11-26, final review)

Completed:

  • Tasks 19 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 connectionStates as 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], or connectionStates.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:

  • 23s 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

  1. Make basic navigation (Home ⇄ Agent ⇄ Settings) feel instant by eliminating unnecessary re-renders.
  2. Adopt real Zustand so we can update fine-grained slices instead of copying the entire session payload.
  3. Keep heavy per-agent data inside SessionProvider unless a screen explicitly opts in, reducing memory retention and serialization overhead.
  4. 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 whats needed to validate the perf fix.

Current Pain Points

  • SessionProviders useMemo(value) depends on nearly every hook; each streamed token creates a brand-new object which is immediately spread into the store (updateSession(serverId, payload) at src/contexts/session-context.tsx:1731-1741).
  • useSessionStore lacks structural sharing—selectors always receive fresh references, so memoization never kicks in.
  • useAggregatedAgents recomputes the flattened list on every store update even when Home isnt 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)

  1. Add lightweight logging around useSessionStore updates (count + payload size) and record navigation timings (press → screen transition) using the RN profiler.
  2. Validate assumptions on both an Android dev build and iOS simulator to set measurable targets (e.g., back navigation <300ms).

Phase 1 Extract Lightweight Agent Directory (0.5 day)

  1. Inside SessionProvider, derive a minimal {id, serverId, title, status, lastActivityAt, cwd, provider} array.
  2. Publish only that array into the shared store (or a new AgentDirectory context) so Home/Settings can read it without touching the heavy session object.
  3. Update useAggregatedAgents and 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)

  1. Replace src/stores/session-store.ts with create() from zustand plus the subscribeWithSelector middleware for fine-grained updates.
  2. Model state as { sessions: Record<serverId, SessionSlice> }, where each SessionSlice only includes data that needs to be globally observable (agents map, connection flags, optional metadata). Keep heavyweight transient data (messages, stream buffers) inside SessionProvider.
  3. Refactor SessionProvider mutators (setAgents, setPendingPermissions, etc.) to call the Zustand setters directly instead of copying entire maps.
  4. Update hooks (useSessionDirectory, useDaemonSession, footer) to select slices via useSessionStore((state) => state.sessions[serverId]?.agents) so re-renders are scoped to the relevant server.

Phase 3 Trim Heavy Session Data (0.5 day)

  1. Cap messages and agentStreamState lengths per agent (e.g., last 200 entries) and keep them local to the agent screen unless explicitly required elsewhere.
  2. 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.
  3. Validate memory footprint before/after on a real device (Android Studio profiler or Xcode Instruments).

Phase 4 Polish & Guardrails (0.5 day)

  1. Add React Profiler/Flipper traces to CI or docs so regressions are easier to catch.
  2. Document the new store architecture in docs/perf-session-store.md and note best practices (never spread the entire slice into other contexts, prefer selectors, etc.).
  3. Audit remaining contexts (Realtime, footer) for similar anti-patterns and queue follow-up issues if needed.

Risks & Mitigations

  • Map/Set mutability: Zustand selectors wont 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 <300ms on Android dev build (measure via Performance.now() or React Profiler).
  • useSessionStore updates 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.