* fix(chat): load older timeline history consistently
Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner.
* fix(chat): keep older history loading reliably
Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
Quitting the desktop app now shuts down the daemon it started, so
"restart the app" is a complete reset users can act on without first
learning what a daemon is. The old default kept it alive; the toggle
under Settings > Host still opts back in.
Existing installs already persisted `keepRunningAfterQuit: true` from
the old default, so a new default alone would only reach fresh installs.
A one-time migration resets it, and an explicit toggle afterwards
persists the migration flag and is never overridden again.
Only a desktop-managed daemon is stopped — one started with
`paseo daemon start` is left alone.
* feat(desktop): open existing agents from links
Register a stable agent deep link and route it through the existing Desktop window. Add a matching CLI command that resolves the local server and activates the requested agent without creating or messaging it.
* fix(desktop): recover agent link delivery
Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access.
* fix(desktop): keep browser input out of the composer
Unhandled webview keys could be redispatched into the active host window, allowing agent Enter to submit a draft prompt. Give pages first refusal for ordinary shortcuts and contain automation at the guest boundary.
* fix(desktop): tighten browser shortcut validation
* fix(desktop): respect browser shortcut ownership
* fix(desktop): retain browser keys across windows
* fix(desktop): scope browser webviews by host
* Reshape browser shortcut ownership
Use the browser webview registry as the single guest identity authority, scope browser operations to their host window, publish chord continuations only while pending, and restore focus to the originating browser after command center dismissal.
* Fix host-scoped browser shortcut follow-ups
* Fix browser keyboard review follow-ups
* Fix browser keyboard lifecycle regressions
* Fix browser shortcut review regressions
* Fix desktop browser review regressions
* Fix browser shortcut frame regressions
* Fix rebase integration regressions
* Preserve browser-native shortcut ownership
* Isolate browser shortcuts and automation by context
* fix(workspace): keep focus mode scoped and easy to exit
Route focus mode through the active workspace so persisted state cannot hide chrome on settings or other screens. Add a visible exit control and keep desktop window chrome aligned and visually quiet.
* fix(app): clarify muted chrome semantics
* feat(desktop): open files in more installed editors
Detect bundled app commands as well as PATH launchers and preserve file positions through editor-specific launches.
* fix(desktop): preserve editor target compatibility
Keep existing file-manager preference ids, retain project context for file launches, and recognize Windows 64-bit IDE commands.
* feat(desktop): share persistent browser data across tabs
Keep browser sign-ins and site storage across tabs and app restarts, with a Settings action to clear the shared profile.
* fix(settings): prevent duplicate browser data clears
* fix(desktop): complete browser data cleanup
Clear legacy per-tab profiles after upgrades and localize the browser data settings in every supported language.
* fix(desktop): close browser profile cleanup races
* fix(app): preserve desktop layout in half-screen windows
Treat native window controls as physical corner obstructions instead of deriving clearance from compact layout state. Keep 751px windows in desktop mode while preserving usable content when persisted sidebars are wide.
* fix(app): retain sidebar width across window resizes
* fix(app): yield app sidebar to narrow settings
* test(app): wait for mobile sidebar motion to settle
* fix(app): yield navigation to narrow workspace explorer
* fix(app): mark sidebar sizing helpers as worklets
* fix(app): hide app navigation when space is constrained
* Fix shortcut capture ignoring - = ; ' keys
KEY_MAP was missing Minus/Equal/Semicolon/Quote, so
keyboardEventToComboString returned null for those physical keys and
the capture UI silently dropped them. Add the four entries with their
shifted variants.
Also suppress the Electron View > Zoom accelerators (Cmd+-/=/0) while
capturing, so those combos reach the renderer instead of zooming the
window. Driven by the existing capturingShortcut flag via a new
paseo:menu:set-capturing-shortcut IPC.
* fix: address review feedback from PR #2047
Trim the capturing useEffect so it only fires when capture is active
(removes the redundant on-mount false and the double-fire on true->false),
and reset the main-process capturingShortcut flag on renderer reload via
browser-window-created + did-finish-load so a Cmd+R mid-capture can't
leave the zoom accelerators disabled.
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* feat(settings): consolidate daemon management into the host page
The app-level Daemon section and the localhost host page rendered the
same daemon card in two places. The host page is now the single home:
the Host sidebar group leads with Overview, and a desktop-only "Enable
built-in daemon" option in the host switcher recovers a removed
localhost connection. Old settings/daemon deep links redirect
(COMPAT-tagged).
* fix(settings): hide daemon recovery option when the server id probe fails
Review feedback: !query.isLoading treated an errored IPC probe as
resolved, so the recovery affordance could show without knowing the
local server id. Replace the resolved-boolean hook with a single
discriminated state hook (loading | error | resolved) — errors hide the
affordance and fall back to the settings root in the redirect. Also
collapses the two query subscriptions per caller into one.
* fix(settings): keep daemon recovery reachable when no hosts remain
Review feedback: removing localhost as the only host takes the settings
sidebar down its no-host branch, which never mounts the host picker —
so the "Enable built-in daemon" option was unreachable in the most
common recovery case. The sidebar now owns the single hook call, passes
it to the picker, and renders the same action as a sidebar row next to
"Add host" when no hosts exist.
* test(e2e): update settings host header to Overview
Test drift: the host section's sidebar/header label was intentionally
renamed to "Overview"; the spec still asserted the old "Host" title.
Also assert the retired app-level Daemon row stays absent.
* fix(diagnostics): include desktop app logs
Settings diagnostics now collect the Electron main-process log separately from daemon.log so desktop startup and environment-probe failures are included in app reports.
* refactor(desktop): share log tail reader
* fix(diagnostics): surface desktop app log read errors
* refactor(diagnostics): tighten desktop report API
Delete the renderer capture prep handshake and keep screenshots on the serialized invalidate/retry path. The capture harness now defaults to the production P1 attach-off parking check.
* feat(browser): inspect, annotate, and grab page elements for the agent
Build a design-review flow on top of the in-app Electron browser so users
can send page elements to their coding agent with context.
- Annotate: pick an element, write a comment, choose an intent
(fix/change/question/approve); the element context + intent + comment go
to the agent as text, and a cropped screenshot rides along as an image.
- Grab: pick an element to copy its info + screenshot straight to the
system clipboard (no comment), with a toast on success/failure.
- Hover inspector: in select mode each element shows a floating label with
tag, id/class, React component name, and pixel size.
- Page markers: annotated elements on the current page get numbered badges
that track scroll/resize.
- Device sizes: a viewport-size menu (responsive + 13 common device presets)
renders the page centered in a fixed-size frame.
- Toolbar buttons now expose hover tooltips and no longer get squeezed out
on narrow panes; the element selector is available to all desktop users
(previously dev-only).
Screenshots and clipboard writes go through new Electron main-process IPCs
(capturePage + clipboard) so they work regardless of webview focus. Element
screenshots are referenced by id in the workspace attachment store and are
protected from the draft-store attachment GC. All new strings are
translated across the six supported locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(browser): write element clipboard exactly once
Resolve the image before writing so a combined text+image grab no longer
does a redundant text-only writeText() first (which flashed an intermediate
clipboard state). Addresses greptile review feedback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* i18n: add browser annotate/grab/devices strings for ja and pt-BR
main added Japanese and Brazilian Portuguese locales after this branch was
created; add the browser annotation, grab, and device-size keys to keep all
locales in sync with en. resources.test.ts parity passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add opt-in browser tools for desktop tabs
Adds the daemon opt-in, desktop tab routing, MCP tools, and real browser automation surfaces for Paseo desktop browser tabs.
* Fix browser tools CI expectations
* Address browser tools review findings
* Restrict browser file automation paths
* Fix browser upload test on Windows
* Harden browser navigation inputs
* Make browser tools create usable tabs
* Update browser MCP empty-state test
* Fail browser tab creation when registration times out
* Fix browser screenshots for agents
* Hide disabled browser tools from agents
* Address browser tools architecture review
* Replace browser tools review tests
* Wrap browser tab registration errors
* Mock Expo Router in app unit tests
* Handle invalid browser automation requests
* Return browser failure on desktop disconnect
* Update browser disconnect websocket test
* Relax browser timeout polling test
* Handle invalid browser responses
* Return browser failure when send fails
* Remove local diagnostics and fixture paths
* Fix dev service home fallback
* Use worktree home for dev services
* Use managed daemon in desktop dev
* fix(browser): keep agent tabs addressable
Track agent-active browser targets separately from human-focused tabs and keep resident webviews alive for automation. Browser tool visibility now comes from registration while the broker reports disabled execution.
* refactor(browser): register tools through catalog
Move browser tool registration onto the shared Paseo tool catalog so the MCP server remains only the transport adapter.
* fix(settings): translate browser tools host error
* fix(desktop): report found app updates during manual checks
Manual checks could reuse cached update state while the renderer discarded pending update details. Keep found update versions visible while downloads prepare, and reserve install affordances for ready updates.
* test(desktop): cover manual update retry after errors
* fix(desktop): keep ready updates ready on recheck
* fix(desktop): show app update check feedback
Manual desktop app update checks now leave visible status feedback even when the shared update state is pending or available. Updater check and preparation errors are carried through the existing result path so the settings row and callout can show the failure instead of only logging it.
* fix(desktop): make update retries perform fresh checks
Manual retries now clear runtime errors emitted by a failed check so the next click calls the updater again. Background checks also skip while a visible manual check is active, and last-checked update copy uses complete localized strings.
* fix(desktop): punctuate update check timestamp copy
* fix(desktop): share runtime update errors
* fix(desktop): preserve update preparation errors
* fix(desktop): settle update check review races
* fix(desktop): settle quiet update check errors
* fix(desktop): handle overlapping update checks
* fix(desktop): preserve preparation errors during checks
* fix(daemon): log stop reasons and client identity
Record websocket client identity, process memory/uptime, and shutdown reasons across CLI, desktop, supervisor, and worker paths so daemon drops can be traced from the triggering client to worker termination.
* fix(daemon): keep shutdown diagnostics in sync
Test drift: supervisor and relay tests still asserted the old log text and metadata shape after shutdown diagnostics started logging structured reasons and relay connection ids. Update those assertions to the new diagnostic contract.
Also centralize client lifecycle reason normalization and derive desktop daemon stop reasons from one tuple so future changes cannot silently drift.
* Merge workspaces across all hosts in the sidebar
Replace per-host sidebar sections with a single merged project list.
Projects that exist on multiple hosts (matched by projectKey) are
collapsed into one row. Host identity is surfaced via:
- A status-only footer pill showing online/offline host counts
- Hover cards with a mandatory host row on every workspace entry
- Subtitles showing the host name when a project spans >1 host
- A host filter in the grouping dropdown (not a sidebar switcher)
Order and group-mode stores migrated from per-server to global keys
with zustand persist migrations. New-workspace screen now shows a
merged project list with a host selector combobox.
Key files:
- workspace-structure.ts: merge algorithm by projectKey
- sidebar-view-store.ts / sidebar-order-store.ts: global state
- sidebar-workspace-list.tsx: context-based subtitle injection
- new-workspace-screen.tsx: host selector in footer row
* Add host filtering to the merged sidebar
The sidebar now merges workspaces from every connected host, so a single
global "active host" no longer fits. Host selection becomes explicit per
action via a host chooser, and agent history pages across all hosts at
once. Removes the active-host / active-server-id model and the
all-agents-list hook that fed it.
* Fix rebase fallout: translate archive host error, update stale workspace-key test
archiveWorkspacesOptimistically used a raw "Host is not connected" string
that the i18n completeness test forbids; route it through i18n.t with the
existing hostDisconnected key. The selectors memoization test asserted
un-prefixed workspace keys, but merged-sidebar keys are serverId-prefixed.
* Fix sidebar review fallout
* Address sidebar review followups
* Fix sidebar workspace identity migration
* Address sidebar review followups
* Handle partial host history failures
* Fix merged sidebar project removal and E2E drift
* Restore history routes and project removal shape
* Remove sidebar host context plumbing
* Tighten sidebar status and host filter state
* Keep new workspace project selection host-compatible
* Migrate sidebar view storage
* Streamline host selection flows
* Update all-host history on archive
* Fix empty project sidebar action
Code regression: the main merge dropped the empty-project New workspace child row while keeping the empty project persistence contract. Restore the row using the project host target so the sidebar stays aligned with the no-host-selection model.
* Fix Japanese shortcut translation key
Stale-base integration: main added the Japanese locale after this branch added cycleAgentMode to the English shortcut help map. Merging main exposed the missing ja key in CI typecheck and resources.test.
* Fix Playwright speech teardown race
* fix(app): add missing i18n keys for host settings
The client UI i18n migration (#1282) referenced translation keys that
were never added to any locale file, so i18next rendered the raw key
string in Settings → Host (e.g. a button labeled
"settings.host.daemon.restart.confirm").
Add the six missing keys to all six locales with real translations:
- settings.host.daemon.rename.submit
- settings.host.daemon.restart.confirm
- settings.host.orchestration.systemPrompt.{edit,reset,save,saving}
The seventh broken reference, in pair-device-modal.tsx, used the wrong
namespace (settings.integrations.pairDevices.rowTitle); the key is
defined at settings.host.pairDevices.rowTitle. Point the reference there.
Reported in https://github.com/getpaseo/paseo/pull/1282#issuecomment-4686672699
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(app): correct machine-translated Save/Saving labels (es/fr/ar)
#1282 left several Save/Saving labels machine-translated into the
"economize money" sense rather than the persist-data sense, so users saw
"Ahorro..." (es), "Économie..." (fr), and "توفير..." (ar) where "Saving..."
was meant.
Fix renameModal.saving and settings.project.actions.{save,saving} in the
three affected locales. ar's actions.save also moves from the conjugated
"يحفظ" to the standard button form "حفظ". Wording matches each locale's
already-correct siblings (e.g. es "Proyecto guardado") and the keys added
in the previous commit. ru and zh-CN were already correct and are untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Automatic desktop update checks still respect rollout admission. Manual checks carry an explicit intent through the app and desktop updater service, and the up-to-date state now shows when the last check completed.
* Add appearance settings for theme, fonts, and syntax highlighting
New Appearance section in Settings to configure the app theme, UI and
mono font family + size, and a syntax-highlighting theme chosen
independently of light/dark. Font family fields default to empty and
show the active system stack as the placeholder; a live split-diff
preview reflects the choices as they change. The theme picker moves
here from General.
Preferences are client-only (AsyncStorage) and applied at runtime by
patching every registered Unistyles theme via
UnistylesRuntime.updateTheme, so existing StyleSheet consumers repaint
with no per-component preference reads and no useUnistyles. No
daemon/protocol change.
* Expand syntax themes and move the preview under the chooser
Replace the small fixed syntax-theme set with eight popular themes — GitHub,
Catppuccin, Dracula, Tokyo Night, One, Nord, Gruvbox, Solarized — built from a
compact per-theme role palette. The app theme and syntax theme stay separate;
the only coupling is the light/dark axis: a theme with both variants uses its
light palette on a light app and its dark palette on a dark app, while the code
frame (gutter, line numbers, background) keeps following the app theme. Default
is now GitHub (GitHub keeps its existing hand-tuned maps for an exact match).
Move the live preview directly under the syntax-theme chooser, drop the
"preview.ts" filename header, and use a clearer code snippet for it.
* Update settings e2e for the moved theme picker and Appearance section
The theme picker moved from General to the new Appearance section, so the
General-content assertion no longer finds "Theme" — point it at "Default send"
(which stays in General) and add Appearance to the e2e section map plus an
expectAppearanceContent check, and exercise the Appearance section in the
sidebar-navigation test.
* Render the appearance preview as a unified diff
Switch the live preview from side-by-side columns to a single unified diff:
unchanged context lines plus "-"/"+" rows for the change, matching how diffs
read elsewhere in the app. Same snippet, mono font, syntax colors, and live
code-font draft behavior.
* Fix code font size change shrinking all UI text
applyAppearance scaled the live (already-patched) theme ramp instead of the
authored FONT_SIZE ramp, so it was not idempotent: every appearance change
re-scaled the current sizes, compounding the whole-UI font scale. With any
non-default UI size, changing the code size — or any setting — shrank (or grew)
all app text, and it persisted into the theme. Build the ramp from the canonical
FONT_SIZE ramp each apply (code size still set absolutely), so applies are
idempotent and a code-size change never touches the UI ramp.
* Apply the interface font across the app on web
react-native-web stamps a default font onto every text element, so the interface
font only reached the few components that set a font explicitly. Inject one rule
that points all text at a CSS variable (updated live as the setting changes),
with high specificity (1,2,0) so it deterministically beats RN-web's base font
and Unistyles' generated classes — no stylesheet-order fragility. Code, diff, and
monospace surfaces carry a `data-pmono` marker (and their subtrees are excluded),
so they keep their code font.
Web-only: React Native has no global font cascade, so native still applies the UI
font only where components opt in. Inline `code` within chat prose is a known
minor gap (its render paths aren't tagged yet).
* Keep preview code in the code font
* Apply code font settings to terminals
* Fix settings host picker import after rebase
* Format terminal webview bundle
* docs(testing): require ports-and-adapters unit tests or real e2e — no in-between
State the end-state explicitly so the test-suite cleanup has a written bar.
* Make useProjects a ports-and-adapters unit test
Extract the per-host workspace aggregation out of useProjects into a
pure fetchAggregatedProjects(input) that takes a typed ProjectsRuntime
adapter. The hook becomes a thin useQuery shim.
The test loses jsdom, @testing-library/react, QueryClientProvider, and
all vi.mock/vi.hoisted of @/runtime/host-runtime. It now exercises the
real aggregation against an in-memory ProjectsRuntime adapter.
* Make useLoadOlderAgentHistory a ports-and-adapters unit test
Extract the load-older sequence into a pure async function that takes
its client, in-flight tracker, toast, and logger as injected
dependencies. The hook reads the session store at call time and wires
the real adapters in. Tests now drive the pure function with typed
fakes — no JSDOM, no @testing-library/react renderHook, no console
spy.
* Make useAgentHistory a ports-and-adapters unit test
Drop the dead __private__ namespace from use-agent-history.ts, expose
fetchAgentHistoryPage and an AgentHistoryClient port as normal exports.
The hook keeps its useInfiniteQuery wiring; the page fetcher is now
testable in isolation.
The test loses jsdom, @testing-library/react, QueryClientProvider, the
vi.hoisted/vi.mock of @/runtime/host-runtime, and the renderHook timing
loops. It exercises the page fetcher directly against an in-memory
AgentHistoryClient adapter that records each call.
* Make usePrPaneData a ports-and-adapters unit test
Extract the timeline fetch into a pure fetchPrPaneTimelinePage that
takes a PrPaneTimelineClient adapter and an UnsupportedTimelineRegistry
port. Extract the rest of the hook's wiring into pure exports:
extractPrRepoIdentity, shouldFetchTimelineFrom, selectPrPaneState. The
module-level unsupported-tuple Set becomes a swappable in-memory
registry; production still wires the same module-level default.
The test loses JSDOM, vi.hoisted + vi.mock of @/runtime/host-runtime,
QueryClientProvider, createRoot, act, focusManager/onlineManager
toggling, and a hand-rolled waitForExpectation polling loop. It drives
each pure function and the timeline fetcher directly against typed
fakes and asserts on recorded state.
* Make useProvidersSnapshot a ports-and-adapters unit test
Extract the network and cache work into pure functions taking a
typed ProvidersSnapshotClient adapter: fetchProvidersSnapshot,
refreshAndApplyProvidersSnapshot, applyProvidersSnapshotUpdate, and
selectorOpenRefetchDecision. The hook still drives them through
react-query and the host-runtime websocket subscription; production
wires the same DaemonClient as before.
The test loses @vitest-environment jsdom, vi.hoisted + vi.mock of
@/runtime/host-runtime, renderHook + QueryClientProvider, act, and a
hand-rolled listener-bucket trap that reached into the spied
client.on subscriber. It drives each pure function directly against
a typed FakeProvidersSnapshotClient and a real QueryClient.
* Make useSettings a ports-and-adapters unit test
Extract the pure load/save logic into use-settings.pure.ts taking a
typed SettingsDeps adapter: a KeyValueStorage port and a
DesktopSettingsBridge port. The hook file keeps the public 0-arg API
(loadAppSettingsFromStorage, loadSettingsFromStorage, persistAppSettings,
saveAppSettings) by wrapping the pure functions with productionDeps that
wire AsyncStorage, isElectronRuntime, loadDesktopSettings, and
migrateLegacyDesktopSettings as before.
The test loses three vi.mock blocks (async-storage, @/desktop/host,
@/desktop/settings/desktop-settings), two vi.hoisted blobs, and the
vi.resetModules + await import("./use-settings") per-test pattern. It
drives the pure functions directly against an in-memory
InMemoryKeyValueStorage and a FakeDesktopBridge that records applied
migrations, asserting on observable storage state rather than mock
function calls.
* Make useAgentCommandsQuery a ports-and-adapters unit test
Extract the daemon call into fetchAgentCommands, a pure async
function taking a typed AgentCommandsClient adapter. The hook still
wraps it in useQuery and resolves the host-runtime client; production
wires the same DaemonClient as before.
The test loses @vitest-environment jsdom, vi.hoisted + vi.mock of
@/runtime/host-runtime, renderHook + QueryClientProvider, and
waitFor. It calls fetchAgentCommands directly against a typed
FakeAgentCommandsClient.
* Make useChangesPreferences a ports-and-adapters unit test
Extract the AsyncStorage-touching load and save logic into
use-changes-preferences.pure.ts taking a typed KeyValueStorage adapter.
The hook file keeps the public API (useChangesPreferences,
loadChangesPreferencesFromStorage) by wrapping the pure functions with a
productionStorage wired to AsyncStorage as before.
The test loses vi.hoisted + vi.mock of
@react-native-async-storage/async-storage, vi.resetModules, and the
per-test dynamic await import("./use-changes-preferences") pattern. It
drives the pure functions directly against an in-memory
InMemoryKeyValueStorage, asserting on observable storage entries rather
than mock function calls. Adds coverage for saveChangesPreferences,
including the no-prior-cache fallback path.
* Make useArchiveAgent a ports-and-adapters unit test
Extract the queryClient-only pure helpers (toArchiveKey,
selectPendingArchiveAgentIds, setAgentArchiving, isAgentArchiving,
removeAgentFromListPayload, markAgentArchivedInHistoryPayload, and the
queryClient-mutating cache helpers) into use-archive-agent.pure.ts. The
hook file keeps the React surface (usePendingArchiveAgentIds,
useArchiveAgent, applyArchivedAgentCloseResults) by importing from the
pure module, and the __private__ reach-around export is gone.
The test loses @vitest-environment jsdom, @testing-library/react, the
renderHook/act/waitFor imports, and the __private__ reach-around. It
calls the pure helpers directly against a real QueryClient and the real
session store. The renderHook test of usePendingArchiveAgentIds is
dropped — it asserted on react-query's subscription mechanics rather
than on our logic.
* Make useSidebarWorkspacesList a ports-and-adapters unit test
Extract the pure pieces — types, applyStoredOrdering,
appendMissingOrderKeys, buildSidebarProjectsFromStructure, and the new
computeSidebarOrderUpdates + deriveSidebarLoadingState helpers — into
use-sidebar-workspaces-list.pure.ts. The hook reads its persistent
sidebar order via the pure helper inside useEffect and derives its
loading state via the pure helper, instead of inlining the logic across
two effects. createSidebarWorkspaceEntry stays in the hook file since
it pulls selectPrHintFromStatus, but the hook re-exports it for
existing callers.
The test loses @vitest-environment jsdom, react-dom/client,
@testing-library/react, and the three Probe components that mounted
React just to assert on effect mechanics. It calls
computeSidebarOrderUpdates and deriveSidebarLoadingState directly, with
no module mocks or hoisted globals. The "does not subscribe while
disabled" assertion is dropped — it was testing useSyncExternalStore's
subscribe gate, not our logic.
* Make useAgentInitialization a ports-and-adapters unit test
Extract ensureAgentIsInitialized and refreshAgent — the entire
imperative bodies of the hook's two callbacks — into
use-agent-initialization.pure.ts, taking setAgentInitializing as an
injected port. Add createSetAgentInitializing as a factory that binds
serverId to the zustand setInitializingAgents action. The hook itself
collapses to ~20 lines of useMemo + useCallback bindings.
The test drops @vitest-environment jsdom, @testing-library/react,
renderHook, and act. It calls ensureAgentIsInitialized and refreshAgent
directly with a bound setAgentInitializing fake. No React mounting, no
module mocks, no hoisted globals.
* Make useClientActivity a ports-and-adapters unit test
Extract the activity-tracker state machine — lastActivityAt bookkeeping,
heartbeat throttling, app-visibility transitions, system-idle monotonic
update, and focused-agent change handling — into
use-client-activity.pure.ts. createClientActivityTracker takes the
heartbeat client, deviceType, and a now() port; the hook wires DOM /
AppState / Electron-idle listeners to tracker methods.
The test drops @vitest-environment jsdom, react-dom/client mounting,
act, and four module mocks (@/constants/platform, @/desktop/electron/idle,
react-native, @getpaseo/client/internal/daemon-client). It calls the
tracker directly with a fake heartbeat client and a test clock; the
fake records emitted heartbeats as observable state.
* Make useHoverSafeZone a ports-and-adapters unit test
Extract the safe-zone state machine — wasInside dedupe, bridge-rect
geometry between trigger and content, and inside/outside transitions —
into use-hover-safe-zone.pure.ts. createHoverSafeZoneTracker takes
getTriggerRect / getContentRect / onEnterSafeZone / onLeaveSafeZone;
the hook just wires document pointermove, window pointerout, and
window blur listeners to tracker methods.
The test drops @vitest-environment jsdom, react-dom/client mounting,
act, @testing-library/react renderHook, the vi.mock("@/constants/platform")
shim, the IS_REACT_ACT_ENVIRONMENT stub, and the getBoundingClientRect
patching helper. It calls the tracker directly with fake rect getters
and asserts on recorded enter/leave counts.
* Make useArchiveSubagent a ports-and-adapters unit test
* Make openImagePathsWithDesktopDialog a ports-and-adapters unit test
Inject the DesktopDialogBridge into openImagePathsWithDesktopDialog instead
of reaching for getDesktopHost() inside the function. useImageAttachmentPicker
passes getDesktopHost()?.dialog at the call site; the native sibling matches
the new signature.
The test drops vi.mock("@/desktop/host", ...) and vi.hoisted() in favor of a
typed in-memory fake dialog that records the options it was called with. No
global module substitution, no spies — the test reads the fake's recorded
state.
* Make useIosHardwareKeyboardSubmit a ports-and-adapters unit test
* Make UpdateCalloutSource a ports-and-adapters unit test
Extract resolveUpdateCalloutDescriptor as a pure function that maps
updater state to a structured callout descriptor. UpdateCalloutSource
becomes a thin React shim that materializes the descriptor's icon and
description as ReactNodes before registering with the sidebar callout
API.
Replaces a 259-line JSDOM + react-dom/client test that mocked five
modules (unistyles theme, lucide icons, async-storage, openExternalUrl,
useDesktopAppUpdater) and mounted SidebarCalloutProvider/Slot just to
assert deterministic title/description/action/dismissal-key derivations.
The new test exercises the resolver directly with zero React, zero DOM,
zero mocks.
* Delete dead useWorkspaceNavigation hook + collapse re-export indirection
The hook had zero production callers — only its own test, which used
vi.hoisted + vi.mock + jsdom + @testing-library/react/renderHook to
verify a useCallback wrapper. The file also re-exported navigateToWorkspace
from the navigation store, so five production importers and two sibling
tests reached the store through a hook-module path that had nothing to
do with hooks.
Retargets every importer to @/stores/navigation-active-workspace-store
directly and removes the indirection module + its slop test.
* Make useCheckoutStatusQuery a ports-and-adapters unit test
Extract peekOrFetchCheckoutStatus and applyCheckoutStatusUpdate to a
sibling checkout-status-cache.ts so both pure functions operate on an
injected QueryClient and CheckoutStatusClient, with no React or host
runtime imports. useCheckoutStatusQuery becomes a thin shell that
composes useQuery + useEffect and delegates the cwd-filter + cache write
to applyCheckoutStatusUpdate.
Replaces a 318-line JSDOM + react-dom/client + fake-timers test that
mocked @/runtime/host-runtime via vi.hoisted, mounted a Probe component
to read the hook's data, and captured the subscription handler in a
hoisted Set. The new test exercises both functions directly against a
real QueryClient — zero React, zero DOM, zero mocks, zero fake timers.
* Make workspace-navigation a ports-and-adapters unit test
* Make navigateToAgent a ports-and-adapters unit test
* Make redirectIfArchivingActiveWorkspace a ports-and-adapters unit test
* Make openProjectDirectly a ports-and-adapters unit test
* Make navigation-active-workspace-store a ports-and-adapters unit test
* Make desktop-attachment-store a ports-and-adapters unit test
* Make readDesktopSystemIdleTimeMs a ports-and-adapters unit test
Rename getDesktopSystemIdleTimeMs to readDesktopSystemIdleTimeMs and
take the desktop IPC invoker as a parameter. The single caller in
use-client-activity.ts now imports invokeDesktopCommand directly and
passes it; the test wires a typed fake invoker.
Drops vi.mock("@/desktop/electron/invoke"), vi.hoisted, and the four
vi.spyOn(console, "warn") log assertions. Tests now assert the
documented behaviour (returns ms, or null) against an in-memory fake.
* Make useDesktopAppUpdater a ports-and-adapters unit test
Extract the check/install state machine from the React hook into a pure
createDesktopAppUpdater runtime that exposes getSnapshot/subscribe/
checkForUpdates/installUpdate. The hook now wires real production deps
into the runtime and bridges its snapshot via useSyncExternalStore;
React-driven concerns (pending-update interval, initial silent check)
stay in the hook.
Drops JSDOM, @testing-library/react, renderHook, the three vi.mock
calls and the vi.hoisted state shim from the test. The new test wires
a typed FakeDesktopAppUpdaterPort recording recordedChecks/recordedInstalls
and exposes deferNextCheck/failNextCheck/nextInstallResult so the
behaviour assertions read as plain English (status transitions through
checking, available, pending, up-to-date, error; race cancellation drops
older results; install errors get reported once).
* Make sidebar-collapsed-sections-store a ports-and-adapters unit test
* Make session-store-hooks a ports-and-adapters unit test
* Make panel-store a ports-and-adapters unit test
* Make workspace-tabs-store a ports-and-adapters unit test
* Make desktop-preview-url a ports-and-adapters unit test
* Make client-id a ports-and-adapters unit test
* Make browser-store a ports-and-adapters unit test
* Make local-file-attachment-store a ports-and-adapters unit test
* Make draft-store a ports-and-adapters unit test
* Make rich-clipboard a ports-and-adapters unit test
* Make desktop-daemon-transport a ports-and-adapters unit test
* Make image-attachment-picker.native a ports-and-adapters unit test
Extract the pure normalize logic into image-attachment-picker.native.pure.ts
and take the PNG exporter as a port. The .native.ts entry wires the real
expo-image-manipulator adapter; the test wires a fake exporter and asserts
on its recorded uris.
Drops vi.mock("expo-image-manipulator") and the inline ImageManipulator
stub from the test. Production callers of normalizePickedImageAssets are
unchanged.
* Make tool-call-icon a ports-and-adapters unit test
Split the pure icon-identity decision into tool-call-icon-name.ts and
keep the lucide/PaseoLogo component lookup in tool-call-icon.ts. The
resolver returns a ToolCallIcon string ("bot", "brain", "paseo", ...);
componentForToolCallIcon does the React component mapping; the existing
resolveToolCallIcon is the composition.
Drops vi.mock("lucide-react-native") from the test and the brittle
"expect(icon).toBe(iconMocks.Bot)" pattern — the unit project couldn't
evaluate lucide-react-native, which is why the mock existed in the
first place. The test now asserts on string identifiers and imports
only the pure module.
buildToolCallPresentation's resolveIcon port is unchanged; the single
caller (components/message.tsx) keeps passing resolveToolCallIcon and
gets the same component back.
* Make desktop-permissions a ports-and-adapters unit test
The test was the heaviest globalThis-juggling test in the app package:
vi.doMock("react-native") + vi.resetModules() per case to swap the
platform, ensureWindow/setNavigator/restoreGlobals to swap
globalThis.Notification, globalThis.navigator, and window.paseoDesktop.
That whole setup existed because the production module reached into
four ambient sources (Notification, navigator, getDesktopHost(),
isWeb/isNative) without a port.
Extract a DesktopPermissionEnvironment interface — { isWeb,
getDesktopHost, getNotification, getNavigator } — and rebuild the
module around a createDesktopPermissions(env) factory. The real
environment binds to the actual globals at module load and the
existing top-level exports (shouldShowDesktopPermissionSection,
getDesktopPermissionSnapshot, requestDesktopPermission) are thin
references onto it, so the only caller (use-desktop-permissions.ts) is
unchanged.
The test now constructs a fakeEnvironment per case and calls into
createDesktopPermissions directly — no JSDOM, no vi.doMock, no
vi.resetModules, no globalThis writes. The eight behaviors are
preserved.
* Make provider-icons a ports-and-adapters unit test
Split the pure provider→icon-identity decision into provider-icon-name.ts
and keep the lucide/SvgXml/catalog component lookup in provider-icons.ts.
resolveProviderIconName returns a ProviderIconName ({kind:"builtin"|"catalog"|"bot", id?}),
and getProviderIcon composes it with the catalog/builtin component maps.
Drops vi.mock("lucide-react-native") from the test — the previous
"expect(icon).toBe(iconMocks.Bot)" pattern only existed because the unit
project couldn't evaluate lucide-react-native. The new test asserts on
discriminated-union identifiers and imports only the pure module.
The 13 outside callers of getProviderIcon are unchanged.
* Make crypto polyfill a ports-and-adapters unit test
* Delete redundant JSDOM Index route test
packages/app/src/app/index.test.tsx mounted the Index component through
JSDOM + createRoot + @testing-library/react, mocked five modules
(expo-router, _layout, desktop-daemon, startup-splash-screen,
navigation-active-workspace-store), and asserted that <Redirect> got
rendered with the right href across six scenarios.
Each of those six scenarios is a one-to-one duplicate of a pure case
already covered in host-runtime-bootstrap.test.ts, which tests the same
two decision functions (resolveStartupRedirectRoute,
resolveStartupWorkspaceSelection) directly. The component is pure
wiring: it reads four hooks, calls the two decision functions, and
renders the result. There is no logic in Index to verify that the pure
tests do not already cover.
Drop the JSDOM file; the wiring is covered by app E2E.
* Make review-draft-store a ports-and-adapters unit test
* Make new-workspace-empty a ports-and-adapters unit test
* Extract shared drag-reorder state machine for web sortable lists
* Extract pure subagents track presentation helpers
Move formatHeaderLabel and resolveRowLabel out of track.tsx into a
colocated track-presentation.ts. Six header-copy tests that previously
mounted React under JSDOM with eight vi.mocks now run as plain unit
tests against the pure helpers.
* Make terminal-file-drop a ports-and-adapters unit test
* Extract pure import-session-sheet view-model helpers
Move resolveProvidersToFetch, buildProviderLabelMap, aggregateSessionEntries,
sumFilteredAlreadyImportedCount, collectErroredProviderLabels, getSessionTitle,
getPromptPreview, and computeEmptyState out of import-session-sheet.tsx into a
colocated import-session-sheet.pure.ts. Add 28 pure unit tests that exercise
provider resolution, dedupe/sort, error label fallback, title fallback, and
the empty-state state machine directly — no JSDOM, no vi.mock.
The existing 859-line JSDOM import-session-sheet.test.tsx stays for now; next
ticks can replace its status-message and empty-state cases with the pure
coverage.
* Make subagents track a ports-and-adapters unit test
Move the row-presentation data builder from track.tsx into the colocated
pure track-presentation.ts and cover statusBucket, titleState, and label
in plain node tests. Delete the 235-line JSDOM track.test.tsx with its
eight vi.mocks and createRoot harness — the remaining behaviours (empty
returns null, useState toggle, onPress wiring) are React idioms covered
by the framework, not domain logic.
* Make isolated-bottom-sheet-modal a ports-and-adapters unit test
* Stop UI from leaking through subagents barrel
The subagents/index.ts barrel re-exported SubagentsTrack (the React
component) alongside pure logic like selectSubagentsForParent. Any pure
consumer of the barrel transitively pulled in lucide-react-native and
react-native-unistyles, forcing tests that only touch pure logic to
declare cosmetic vi.mock blocks for icons, theme, tooltip, and provider
icons just to get the import graph to load.
Drop SubagentsTrack from the barrel. The one external caller
(panels/agent-panel.tsx) deep-imports from @/subagents/track instead.
The workspace-subagents-integration test loses 56 lines of pretend-UI
mocks; only the AsyncStorage mock remains because workspace-layout-store
uses it directly through persist middleware.
* Collapse use-hover-safe-zone.pure.ts into hover-safe-zone-tracker.ts
The `.pure.ts` suffix mimics tooling-resolved variants (`.web.ts`,
`.native.ts`, `.test.ts`) without being one. The file exports a single
tracker; rename it after its role. Test sits next to its subject by name.
* Promote use-settings to a directory module
* Promote sidebar-collapsed-sections-store to a directory module
* Promote navigate-to-agent to a directory module
The `.pure.ts` filename suffix mimics tooling-resolved variants
(`.web.ts`, `.native.ts`, `.test.ts`) without being one. Move the pure
resolver and its wired wrapper into `utils/navigate-to-agent/`, where
the directory carries the domain and `resolve.ts` names the role. The
pure function is renamed `resolveNavigateToAgent` so it no longer
collides with the wrapper's exported `navigateToAgent`.
* Collapse use-client-activity.pure.ts into client-activity-tracker.ts
* Collapse open-project.pure.ts into open-project.ts
* Promote use-changes-preferences to a directory module
Replaces the .pure.ts / .test-utils.ts double-suffix with a
directory home that mirrors hooks/use-settings/:
hooks/use-changes-preferences/
index.ts ← React hook; wires AsyncStorage
storage.ts ← pure load/save
storage.test.ts ← tests against storage
fakes.ts ← in-memory KeyValueStorage adapter
External callers continue importing @/hooks/use-changes-preferences
unchanged (resolves to index.ts).
* Promote draft-store to a directory module
* Collapse use-archive-agent.pure.ts into use-archive-agent.ts
* Rename import-session-sheet.pure.ts to import-session-sheet-view-model.ts
* Rename image-attachment-picker.native.pure.ts to picked-image-normalizer.ts
* Inline use-agent-initialization.pure.ts into use-agent-initialization.ts
* Rename use-sidebar-workspaces-list.pure.ts to sidebar-workspaces-view-model.ts
* Rename review/store.pure.ts to review/state.ts
* Promote browser-store to a directory module
* Promote navigation-active-workspace-store to a directory module
* Promote panel-store to a directory module
* Promote session-store-hooks to a directory module
* Promote workspace-tabs-store to a directory module
* Rename use-archive-subagent.pure.ts to archive-subagent.ts
* Rename sidebar-workspace-archive-redirect.pure.ts to workspace-archive-redirect.ts
* Rename workspace-navigation.pure.ts to prepare-workspace-tab.ts
* Extract client SDK package
* Polish SDK client identity defaults
* Build client before dependent CI jobs
* Restore daemon client server export
* Extract protocol and client SDK packages
* Fix provider override schema validation
* Fix app test daemon client imports
* Simplify workspace build targets
* Fix CLI test server build bootstrap
* Run SDK package tests in CI
* Fix rebase package split drift
* Restore lockfile registry metadata
* Update SDK config test for prompt default
* Move terminal stream router test to client package
* Fix rebase drift for protocol imports
* Fix SDK agent capability fixture
* Restore legacy server client exports
* Fix server export compatibility test
* Advertise custom mode icon client capability
* Remove server daemon-client exports
* Format rebased mode control import
* Fix rebase drift for protocol imports
Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split
shared/ paths that this branch moves into @getpaseo/protocol. Redirect
those imports to the protocol package so typecheck stays green after the
rebase.
Keep sheet text inputs native-owned to avoid React Native controlled input flicker during fast typing. Add reset keys for fields that need programmatic clears, and move model, mode, and thinking pickers onto shared combobox rows with fuzzy ranked search and native virtualization.
The "shows only the changelog action once the update is installed"
case in update-callout-source.test.tsx pinned the rendering path that
2d9c7747 deleted. The callout now early-returns for any status other
than available/installing/error, so the assertion that the container
contains "Update installed" can never be satisfied.
The install flow briefly flashed an "Update installed / Restart to use
the new version" callout between clicking Install & restart and the
actual app quit. The button implies action; the restart is already in
flight.
Run quitAndInstall inline (no 1.5s setTimeout) so the IPC sequences
download → install → daemon stop → app quit before returning. Drop the
"installed" rendering branch from the callout so the success path goes
straight from "Installing..." to the window unmounting.
* Replace skills auto-sync with explicit install/update/uninstall
Skills no longer sync silently on every desktop launch. Settings now
shows Install (fresh), Update (with a confirm dialog listing per-skill
add/update/delete ops), or Uninstall depending on what's on disk.
A manifest at ~/.agents/skills/.paseo-manifest.json tracks Paseo-owned
skills so uninstall removes only what we wrote, never user content.
Existing users migrate transparently — first read synthesises a
manifest from on-disk hashes, so an unchanged install flips silently
to up-to-date.
Also restructures the integrations layer: skills/ and cli-install/
each own their files behind a designed index.ts. The shared
integrations-manager.ts is gone.
* Rewrite skills install flow with disk-truth model
Drops the manifest sidecar, migration logic, and the
agentsDirHasSkillContent heuristic that misread user-authored skills
as Paseo content. Replaces them with a hardcoded PASEO_SKILL_NAMES
list (current bundle + paseo-chat for retired-skill cleanup); the
disk is the source of truth for what's installed.
Renames the SkillsState "fresh" → "not-installed" everywhere
(server, IPC parser, hook test mocks). Install and update share an
applySkills body. Errors propagate instead of being swallowed.
Fixes the case where Uninstall left state at "drift" because the
user had personal skills under ~/.agents/skills/.
* Stop symlinking claude skills, write real files
If the user has ~/.claude/skills symlinked to ~/.agents/skills, the
per-skill symlink path resolved through the parent symlink and fs.rm'd
the agents-side files we'd just written. Mirror real files to claude
the same way as codex; agents stays the canonical source-of-truth dir.
* Make Uninstall button visible (outline, not ghost)
Cmd+R was scoped through a stale Electron-side active browser value. Opening or focusing a browser pane set activeBrowserPaneId, but switching to an agent tab only updated workspace layout state. Electron still remembered the old browser and the app menu reloaded that hidden/previous browser instead of the app.
Make workspace layout the authority. Derive the active browser from focusedPaneId plus the focused pane's focusedTabId, and sync browserId|null to Electron from one WorkspaceScreen effect. BrowserPane no longer marks itself active from URL-bar or webview focus handlers, so call sites do not have to remember to clear Electron state when focus moves to an agent, terminal, or another pane.
Electron now treats the synced value as the workspace-active browser, not an independently observed focus source. Menu reload uses that workspace-derived browser when present; otherwise it reloads the app window. Direct Cmd+R events that originate inside a browser guest still reload that guest through the guest before-input-event path.
Browser webviews now own their guest context menu in the main process. Add copy/paste/select-all and a development Inspect Element action for attached browser guest WebContents.
DevTools opening is routed through the main process by browserId. Docked-right DevTools did not become visible for browser guest contents in practice, even though inspect mode activated, so the final behavior opens detached DevTools for both the toolbar button and Inspect Element. The renderer logs bridge/result failures, and main logs registration/open/inspect outcomes for future debugging.
Regression coverage is at the workspace layout derivation layer: focused browser id follows the focused pane's active browser tab and becomes null when the active tab is not a browser. Verified with targeted layout-store tests, navigation E2E, settings E2E, npm run typecheck, and npm run lint.
* test(app): triage desktop test files — delete 16-mock component slop, tighten attachment store assertion
Removes desktop-updates-section.test.tsx (16 internal mocks, JSDOM, toHaveBeenCalledWith chains on component renders).
Tightens desktop-attachment-store.test.ts: toHaveBeenCalled() → toHaveBeenCalledWith(attachment).
* refactor(app): extract daemon management toggle coordinator with unit tests
Fills the coverage gap left by deleting desktop-updates-section.test.tsx.
Extracts executeDaemonManagementToggle from useDaemonManagementToggle — pure
async coordinator with injected deps (no vi.mock). Unit tests verify the three
key invariants: settings persist before stop, stop is skipped when not
desktop-managed, and start/stop are invoked on enable/disable.
Add oxlint-tsgolint and configure typescript/no-unnecessary-type-assertion
to flag redundant `!` and `as Foo` casts. Type-aware mode is left off by
default to keep `npm run lint` fast; the rule sits configured for when we
turn type-aware on intentionally. Auto-fix removed ~283 redundant casts;
two manual touch-ups: a real tsgolint false positive in split-container.tsx
and a stale ChildProcess import after a double-cast collapsed.
Adds a Service URLs setting (ask / in-Paseo / external browser) that
controls where URLs from running scripts open on desktop. First click
prompts with a "don't ask again" checkbox; in-Paseo opens a workspace
browser tab. Closing a browser tab now clears its session partition.
- Refresh icon: use single-arrow RotateCw instead of two-arrow RefreshCw
- URL bar selects all text on focus
- Cmd+R now reloads the active browser pane (tracked via IPC) when
the URL bar has focus, instead of reloading the Electron window
- Forward Paseo shortcut keys (b, e, w, t, k, /, \, comma, digits,
enter, arrows) from the embedded webview to the main window so
sidebar/tab/etc. shortcuts still fire when focus is inside the
loaded page
* feat: add browser pane element picker
Port the browser pane from PR #198 and route picked elements through workspace attachments so the context follows every agent composer in a workspace.
Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
* docs: document electron platform overlays
* fix: harden browser pane navigation
* fix: polish browser tab interactions
* fix: route browser pane focus shortcuts
---------
Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>