Skip to content

feat(agents): tile-based workspace + Electron desktop shell#4276

Open
samwillis wants to merge 40 commits intomainfrom
samwillis/electron-app
Open

feat(agents): tile-based workspace + Electron desktop shell#4276
samwillis wants to merge 40 commits intomainfrom
samwillis/electron-app

Conversation

@samwillis
Copy link
Copy Markdown
Contributor

@samwillis samwillis commented May 5, 2026

Summary

A long-running branch that lands two related shipments on the Electric Agents stack:

Screenshot 2026-05-05 at 11 55 20 AM

1. Tile-based workspace in @electric-ax/agents-server-ui

Replaces the single-pane chat surface with a Cursor / Zed-style tile workspace:

  • View registry — Chat, StateExplorer, NewSession, Settings, etc. all live behind a single registry so tiles can host any view.
  • Workspace tree + reducer — splits, groups, and tabs persisted to localStorage and encoded into shareable layout URLs.
  • DnD — drag tiles between groups with a 5-zone drop overlay (left / right / top / bottom / center-tab).
  • Hotkeys + View ▸ menu — split horizontally / vertically, focus next pane, close pane, reset layout.
  • Refreshed chrome — theme aligned with the Electric website, centralised hover / chip surfaces, rounded badges, sentence-case section headings (no more all-caps labels).
  • Sidebar filter & view menu — group sessions by date / type / status / working directory, hide types or statuses, expand / collapse all.
  • Working-directory pickerWorkingDirectoryPicker in the new-session composer with recents (localStorage) and a native folder picker on Electron, backed by a new Combobox UI primitive that mirrors Select's typing.
  • Full Settings screen — General / Appearance / Local Runtime, with the cog menu restructured to surface Theme, Local Runtime, and Settings.

2. New @electric-ax/agents-desktop package — Electron shell

A desktop app that bundles a local Horton runtime (no Postgres / Electric / agents-server bundled — talks to those over HTTP):

  • Multi-window, frameless windows with in-app title bars (sidebar toggle + search live next to the macOS traffic lights).
  • Native task bar / menu bar tray icon for the runtime status.
  • Native application menus with sensible Window / Help entries and keyboard shortcuts.
  • About dialog (text from https://electric.ax/agents/) + Window menu listing open windows by active session.
  • On-launch API key prompt for Anthropic / OpenAI / Brave Search; settings persisted to userData/settings.json and mirrored into process.env.
  • Localhost agent-server discovery — saved + discovered servers surface in the sidebar's server picker.
  • Proper HMR via vite-plugin-electron (replaces the brittle tsdown + electronmon setup that caused restart loops in StrictMode).
  • Full Electric-branded application icon set (rasterised from the website SVG).

Supporting fixes pulled in

  • @electric-ax/agents — Horton accepts an optional workingDirectory spawn arg so each session can run against its own project root without restarting the runtime; web_search worker tool name fix.
  • @electric-ax/agents-runtime — preserve tool pairs during compaction, match tool-call events by id (both surfaced while building the desktop UI).
  • useDocumentTitle — uses eq() from @tanstack/react-db and drops a limit(1) that needed an orderBy.

Changeset

.changeset/electron-desktop-and-agents-ui-tiles.md — patch bumps for @electric-ax/agents-desktop, @electric-ax/agents-server-ui, @electric-ax/agents-runtime, and @electric-ax/agents. (@electric-ax/agents-server is in a fixed group with the UI so will track automatically.)

Test plan

  • pnpm -C packages/agents-server-ui typecheck — clean.
  • pnpm --filter @electric-ax/agents-desktop dev — Electron window opens against a local agents-server, tray icon shows runtime status, HMR works on UI edits.
  • On first launch with no API keys in env, the API key dialog appears and persisted keys are picked up by the bundled Horton runtime.
  • Sidebar filter menu — switch grouping modes (date / type / status / working dir), hide a status, expand / collapse all.
  • New-session screen — pick a working directory (recents + native picker on Electron), confirm the Horton session runs in that cwd; recent dirs persist across reloads.
  • Tile workspace — drag a tile between groups, split horizontally / vertically, reload to confirm layout persists, share the URL and confirm the layout reproduces.
  • Web build (pnpm -C packages/agents-server-ui build) still works — desktop-only chrome stays out of the web bundle.

Made with Cursor

samwillis and others added 28 commits May 4, 2026 20:49
Plans a VS Code/Cursor-style splittable workspace where the workspace
is a recursive tree of Splits → Groups → Tiles, each tile rendered
through a pluggable view registry (Chat, State Explorer, future
Logs/Inspector/etc.).

Splits and views stay orthogonal — splitTile() and setTileView() are
the two primitives every menu item composes from.

URL strategy is hybrid: clean default URL (active tile only) plus
localStorage layout persistence per server, with an opt-in
?layout=<DSL> import param for shareable layouts.

Migration ships in five sequential PRs starting with the view
registry, then workspace skeleton, SplitMenu, drag-and-drop, and
finally persistence + URL polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
…Explorer views

Stage 1 of the tile-based layout refactor (see TILE_LAYOUT_PLAN.md).

Views are now first-class, registered into a tiny in-memory registry at
app boot:

- 'chat'           → ChatView       (polymorphic on entity type;
                                     embeds CodingSessionView when
                                     applicable, else generic timeline)
- 'state-explorer' → StateExplorerView (thin wrapper over the existing
                                       StateExplorerPanel)

Adding a new view is a single registerView({…}) call plus a *View.tsx
file — no changes to routing or chrome.

EntityHeader's bespoke 'Show state explorer' toggle is replaced by a
generic, registry-driven view-switcher: an inline icon strip plus
matching menu items, generated automatically from listViews(entity).

The /entity/$splat route gains a ?view=<id> query param so non-default
views are deep-linkable. The default view (chat) is implicit and never
shown in the URL.

Stage 1 still renders one view at a time — splits arrive in stage 3.
The bespoke right-drawer / statePanelWidth splitter in router.tsx is
removed; the State Explorer temporarily opens in-place via view-swap
until the workspace skeleton (stage 2) and SplitMenu (stage 3) bring
proper splits back.

Typecheck + tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stage 2 of the tile-based layout refactor (see TILE_LAYOUT_PLAN.md).

Introduces the recursive workspace data model and renders entities
through it, replacing the bespoke route handler that previously
rendered a single entity directly.

Data model (src/lib/workspace/types.ts):
- Workspace { root: WorkspaceNode | null, activeGroupId }
- WorkspaceNode = Split | Group
- Split    { direction, children: { node, size }[] }
- Group    { tiles: Tile[], activeTileId }
- Tile     { entityUrl, viewId }

Reducer (workspaceReducer.ts) is pure and side-effect-free, with
invariants enforced on every mutation:
- splits with ≤1 child collapse / unwrap
- nested same-direction splits flatten
- empty groups are removed; sibling sizes re-normalised
- activeGroupId always references a group present in the tree
Covered by 15 Vitest cases for the tricky paths (open / close last
tile / move-tile / split-with-view / resize / active bookkeeping).

Components (src/components/workspace/):
- Workspace        — top-level renderer + URL ↔ workspace sync
- NodeRenderer     — pure dispatch from node kind to container
- SplitContainer   — N panes + N-1 splitters, fractional sizing
- Splitter         — drag-to-resize with px → fraction conversion
- GroupContainer   — tab strip + active tile body, with
                     focus-follows-click group activation
- TabStrip         — tabs (hidden when group has only one tile),
                     middle-click closes

useWorkspace (src/hooks/useWorkspace.tsx):
- WorkspaceProvider — wraps useReducer
- useWorkspace      — exposes { workspace, dispatch, helpers }
- helpers wraps dispatch for ergonomics + computes activeTile

Router (src/router.tsx):
- WorkspaceProvider mounted under SearchPaletteProvider
- /entity/$splat ?view=<id> route delegates entirely to <Workspace>;
  the route component is now a one-liner returning <Workspace />

URL behaviour preserved from Stage 1 (single-tile workspaces look
identical to before): URL → workspace effect refocuses the matching
tile, swaps view in place when same-entity-different-view, or opens
a new tile in the active group otherwise. Workspace → URL effect
mirrors the active tile back to the URL with replace:true to avoid
double-pushing history entries.

Stage 2 ships with single-tile workspaces by default — splits
become user-driven in Stage 3 via the SplitMenu.

Typecheck + tests green (21 passing).

Co-authored-by: Cursor <cursoragent@cursor.com>
Stage 3 of the tile-based layout refactor (see TILE_LAYOUT_PLAN.md).

Adds the unified per-tile '…' menu (SplitMenu), driven entirely by
two reducer primitives — setTileView() and splitTileWithView() — so
every menu item composes from those.

SplitMenu structure (matches the Cursor screenshot):
- Inspect (entity JSON dialog)
- View ▸ <viewId> ▸ Open here / Split right / down / left / up
    - parent-row click runs each view's defaultSplit (e.g. 'right'
      for State Explorer, restoring the muscle-memory of "drawer
      pops out to the right" from before stage 1)
- Split right / down / left / up           (duplicates the active tile)
- Move tile to ▸ Group N                   (only shown when ≥2 groups)
- Copy URL · Pin · Fork subtree            (entity-level actions)
- Close tile (⌘W)
- Kill entity                              (with confirmation dialog)

EntityHeader is now display-only:
- title + status + view-toggle icon strip
- a generic `menu` slot that the workspace fills with <SplitMenu>
The Inspect / Kill confirm dialogs and all entity-action props
(onKill, onFork, pin) move into SplitMenu. EntityHeader no longer
knows about tiles, groups, or splits.

Workspace hotkeys (useWorkspaceHotkeys, mounted in RootShell):
- ⌘D       Split active tile right
- ⇧⌘D      Split active tile down
- ⌘W       Close active tile
- ⌘\       Cycle to next group
- ⌘1..9    Focus group N

State Explorer regains its "drawer to the right" UX as the *default*
action of `View ▸ State Explorer` thanks to defaultSplit: 'right' on
its registry entry — clicking the parent row splits it right; the
deeper sub-menu lets power users put it elsewhere or open in place.

Typecheck + tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
…erlay)

Stage 4 of the tile-based layout refactor (see TILE_LAYOUT_PLAN.md).

Native HTML5 drag-and-drop, no react-dnd. Two payload kinds carried
under a custom `application/vnd.electric-tile+json` MIME type:

- sidebar-entity { entityUrl }
- tile           { tileId, sourceGroupId }

DropOverlay (per-group):
- mounted on every group, position:absolute
- pointer-events default to none; window-level dragstart/dragend
  listeners arm/disarm the overlay so splitter drags / text selection
  in the body aren't interrupted when no drag is in progress
- on dragover, computes the active zone (centre 25% inset square +
  4 edge slabs joined at the centre) and highlights it
- on drop, dispatches:
    - moveTile(tileId, { groupId, position })          for tile drags
    - openEntity(entityUrl, { target: { groupId, position }})
                                                         for sidebar drags
- silently no-ops when dropping a tile back onto its source group's
  centre (avoids a redundant reducer round-trip)

Sidebar rows:
- now `draggable` with the sidebar-entity payload
- ⌘/Ctrl-click + middle-click open the entity in a new split right of
  the active group (matches VS Code's "open to side")
- routed through `helpers.openEntity({ target: { position: 'split-right' }})`
  in RootShell

Tabs (TabStrip):
- now `draggable` with the tile payload
- middle-click already closes (preserved from stage 2)
- click activates (preserved from stage 2)

GroupContainer:
- gains a position:relative wrapper for the overlay
- adds a subtle inset ring on the active group when there's >1 group
  (so the user knows where new tiles will land for a sidebar click)

Drop semantics summary:
- centre  → append as new tab (or replace if dropping on self)
- north   → new horizontal split, this tile on top
- east    → new vertical split,   this tile on the right
- south   → new horizontal split, this tile on the bottom
- west    → new vertical split,   this tile on the left

Typecheck + tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Stage 5 of the tile-based layout refactor (see TILE_LAYOUT_PLAN.md).
Implements the §3.4 hybrid URL strategy end-to-end.

Layout codec (lib/workspace/layoutCodec.ts):
Compact, human-readable, URL-safe DSL for serialising the workspace
tree. Distinct separators remove parse ambiguity:

  ',' = split-sibling   ';' = group-tab   '.' = entityUrl/viewId

Examples (canonical encoded form):

  horton%2Ffoo.chat
  horton%2Ffoo.chat;horton%2Ffoo.state-explorer@1
  H(horton%2Ffoo.chat:60,horton%2Ffoo.state-explorer:40)
  H(horton%2Ffoo.chat,V(horton%2Fbar.chat,horton%2Fbaz.logs))

Encoder strips the conventional leading '/' on entity URLs and
omits sizes that match the natural even share — keeps URLs short.
Decoder mints fresh ids (so two decodes don't collide) and
renormalises malformed sizes. 10 Vitest cases cover round-trips,
nesting, error paths, and id freshness.

Persistence (hooks/useWorkspacePersistence.ts):
- key: `electric-agents-ui.workspace.<encoded-server-url>.v1`
- value: `{ v: 1, workspace: <Workspace> }` (versioned envelope)
- 250ms debounced write on every workspace change
- one-shot hydration per (server, mount); restores only when the
  current workspace is empty so it doesn't fight the URL → workspace
  effect
- prune-on-load: tiles whose entity is no longer in the live
  entitiesCollection are dropped; cascade-collapses empty
  groups / single-child splits / dead root
- silently no-ops in environments where localStorage throws
  (Safari private browsing, sandboxed iframes)

URL hydration (Workspace.tsx):
- ?layout=<DSL> takes priority over localStorage; once consumed we
  navigate({ replace: true }) to strip the param so the address bar
  settles back to "active tile only" — passes the
  open-shared-link-then-clean-URL acceptance check from the plan
- entity route's validateSearch now accepts both `view` and `layout`

Copy layout link (SplitMenu):
- new menu item between "Copy URL" and the separator
- builds a `?layout=<encoded>` URL relative to the current hash
  history; copies to clipboard

Wired up in RootShell:
- useWorkspacePersistence() runs alongside useWorkspaceHotkeys()

Typecheck + tests green (31 passing). Build passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tile

Replace the group-and-tabs layout model with a flat Split | Tile tree:
each leaf is a single tile, no tabs within a tile, and dividers / drop
targets share the sidebar's hairline + accent-on-hover styling.

The new-session screen is now a first-class standalone tile rather
than a separate route page. View registry distinguishes entity views
(chat, state-explorer) from standalone views (new-session); standalone
tiles carry entityUrl: null and render a tile chrome (header + split
menu + drop overlay) just like an entity tile. Both / and /entity/$
mount the same Workspace component and the URL <-> workspace sync
maps standalone tiles back to /.

Drag-and-drop covers all three sources (sidebar entity row, sidebar
new-session button, existing tile header) into the four edge
quadrants of any tile. openTile now focuses the freshly-created
tile in all paths so drop-to-side gives immediate visual feedback
and the URL follows. Multiple new-session tiles can coexist via
drag (the click flow keeps focus-existing-or-replace semantics).

The SplitMenu's view rows render inline with [->][down] icon
buttons for split-this-view-to-the-side; entity-only items (Inspect,
Pin, Fork, Kill, Copy URL) and "Close tile" (when sole tile) are
hidden contextually. The State Explorer's draggable divider switched
to the shared Splitter component for visual consistency.

Persistence layer: SCHEMA_VERSION bumped to v2; pruneNode keeps
standalone tiles intact. Layout codec encodes standalone tiles as
".viewId" (empty entity-path segment) so layout links can carry
mixed standalone+entity workspaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ver/chip surfaces

Aligns the agents UI's typography and palette with the marketing site
(self-hosted OpenSauceOne + Source Code Pro, website surface ladder
mapped onto --ds-bg / --ds-bg-subtle / --ds-surface / --ds-surface-raised,
website text/accent values).

Introduces two centralised semantic tokens to fix muddy dark-mode
surfaces:
  --ds-chip-bg      — solid raised surface for pill triggers, inline
                      code chips, kbd keys, code wells etc. (matches
                      the marketing site's --vp-code-bg pattern)
  --ds-bg-hover     — universal interactive hover lift. Per-theme:
                      light = --ds-gray-a3 (alpha-black tint composes
                      cleanly on warm-white surfaces), dark = solid
                      #2d3142 (one clean cool-grey step above
                      --ds-surface-raised, no muddy compositing on
                      navy page bg).

Routes drop-down items, sidebar rows, search palette, ghost icon
buttons (Button.module.css ghost variant + tone-neutral soft fill),
chip triggers, code wells and similar through these tokens so every
surface in the same family reads consistently.

Ports chat-log + markdown rhythm refinements from the projects branch
(without bringing across the 13→14px body bump or the Figtree font
swap):
  - EntityTimeline statusPill switches from a centred chip to a
    left-bordered log line; jump-to-bottom moves to bottom-right as
    a small bordered surface
  - Composer gets a hairline shadow + border-1 edge; user bubble
    border lightened
  - Markdown rhythm: container gap 12→14, list padding 1.5→1.75em,
    li gap 4→6, heading top margins bumped (h1 4→10, h2 4→8, h3 2→6)
  - Tool blocks get a softer shadow, mono header strip with a faint
    band, recessed code well, new .sectionLabel small-caps style
    (consumed by ToolCallView for Command/Output/Content/Input)
  - MarkdownCodeBlock strips the trailing empty line Shiki appends
    when source ends with a newline

Sidebar rows: type label drops to 10px lowercase (cap-height ≈ title
x-height) with a 1px translateY so it shares a baseline with the
title; title line-height bumped to 1.3 so descenders aren't clipped
by the ellipsis box.

Also drops the projects/tagging feature from this branch (App,
Sidebar, NewSessionPage, useProjects hook) — this branch is being
reset to a fresh-app baseline before re-landing those features.

Co-authored-by: Cursor <cursoragent@cursor.com>
…de blocks

Shiki's `codeToTokens` defaults to `defaultColor: 'light'`, which
emits the light theme's hex directly on each token's `color`
property and only sets `--shiki-dark` as a CSS variable.
`--shiki-light` was therefore never set, so the
`var(--shiki-light, inherit)` rule in `markdown.css` was falling
back to `inherit` and tokens rendered uncoloured in light mode.

Pass `defaultColor: false` so Shiki emits BOTH themes as CSS
variables, letting the existing per-theme CSS rules pick the right
one via `[data-theme]`.

Co-authored-by: Cursor <cursoragent@cursor.com>
…n-app

Brings the tile-based workspace (view registry, splittable tiles,
drag-and-drop layout, persistence, shareable layout URLs) into the
electron-app branch on top of the website-aligned theme.

Conflict resolution:
- Sidebar.tsx: keep the `treeProps` spread cleanup from electron-app and
  add `onOpenEntityInSplit` to the spread (introduced by tile branch for
  ⌘/Ctrl-click + middle-click into a new split).
- views/NewSessionView.tsx (renamed from NewSessionPage.tsx): adopt the
  tile branch's workspace flow (`useWorkspace` + `helpers.openEntity`
  with `tileId` `replace` target, `StandaloneViewProps`) and drop the
  `CODING_SESSION_ENTITY_TYPE` / `CodingSessionSpawnForm` references —
  the coder entity was removed on main (#4272) so those imports no
  longer resolve.
- router.tsx: replace the in-router `EntityPage` + `GenericEntityBody`
  with the tile branch's tiny `WorkspacePage` shell that just renders
  `<Workspace />` (which now owns all entity rendering and URL ↔ tile
  syncing).
- views/ChatView.tsx: drop the `CODING_SESSION_ENTITY_TYPE` /
  `CodingSessionView` polymorphism for the same reason as above; chat
  view is now a single generic timeline + composer.

Verified with a clean `pnpm -C packages/agents-server-ui build`.

Co-authored-by: Cursor <cursoragent@cursor.com>
Picks up minor version bumps that were already resolved during install
(`@antfu/ni`, `@react-grab/cli`, `ora`, `log-symbols` and their
transitive deps). No package.json changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a new `@electric-ax/agents-desktop` Electron package that reuses
`@electric-ax/agents-server-ui` as its renderer and bundles the
`BuiltinAgentsServer` runtime from `@electric-ax/agents` so a local
Horton runtime can register against any Agents server selected in the
UI. The Electron main process owns servers/active-server/working-dir
settings (persisted to userData), spawns multiple windows from a
tray/menu-bar icon, and orchestrates the runtime lifecycle.

UI integration in `agents-server-ui`:
- New `build:desktop` Vite mode that emits `dist-desktop/` with
  relative `base` and stamps `<html data-electric-desktop="true">` so
  desktop-only CSS matches from the first paint (more reliable than
  preload's isolated-world DOM mutation).
- `window.electronAPI` typings + `loadDesktopState` / `saveActiveServer`
  / `onDesktopStateChanged` helpers so the existing
  `useServerConnection` hook transparently uses the IPC bridge in
  Electron and `localStorage` on the web.
- `SettingsMenu` shows a desktop runtime status group (status, URL,
  errors, restart/stop actions) when running in Electron.
- macOS `hiddenInset` titlebar with traffic lights positioned to align
  with the existing 44px header. `SidebarHeader` and every tile's
  `MainHeader` become `-webkit-app-region: drag` strips with a 84px
  left inset on the leading edge so the toggle/search icons sit beside
  the lights; buttons / links / inputs / `data-no-drag` opt back out
  so they stay clickable.

`AGENTS_DESKTOP_PLAN.md` documents the architecture, scope, and the
phased rollout (this is phase 1: bundled runtime only, server stays
remote).

Co-authored-by: Cursor <cursoragent@cursor.com>
Builds out the Electron shell on top of the bundled-runtime base:

- Application menu (File/Edit/View/Window/Help) and tray menu wired to
  a `desktop:command` IPC channel so menu items, on-screen buttons and
  hotkeys all go through the same renderer actions. Window submenu
  rebuilds on focus/blur and lists open windows by their session
  document title (driven by a new `useDocumentTitle` hook).
- Custom branded About dialog (a small frameless `BrowserWindow`) so
  the app icon and copy are consistent across platforms — the native
  macOS panel ignores `iconPath`. App icon, tray template icon (1x/2x
  black-on-transparent), and `app.dock.setIcon` round out the
  branding.
- First-launch API keys dialog: on startup the renderer asks main for
  the saved/suggested key set; if no Anthropic or OpenAI key is
  configured it pops a modal pre-filled from `process.env.*` (snapshot
  taken at launch). Saved values are persisted in `settings.json`,
  mirrored back into `process.env` for Horton's
  `createBuiltinAgentHandler`, and the runtime is restarted so the
  next request picks them up. Optional `BRAVE_SEARCH_API_KEY` is
  captured in the same flow.
- Localhost server discovery: main probes a focused port set
  (4437/4438/4439/3000/4000/8080) for `GET /_electric/health` on a
  30 s background loop and broadcasts the set via `desktop:state-
  changed`. The renderer's `ServerPicker` polls every 5 s while its
  menu is open and surfaces matching servers as one-click "add"
  rows under the saved-server list (no header, consistent row
  height with the trash-button rows).
- Bug fix: `Add server` dialog cancel was disabled whenever no
  servers were saved — a holdover from the web build's auto-seeded
  `This Server` fallback that doesn't exist in desktop. Cancel /
  Esc / backdrop click now always dismiss.

Co-authored-by: Cursor <cursoragent@cursor.com>
The active-entity lookup was using a raw `===` comparison inside
`.where()`, which TanStack DB rejects (the predicate evaluates to a
boolean at build time instead of producing a query expression). The
trailing `.limit(1)` then tripped the "LIMIT/OFFSET require ORDER BY"
guard once the predicate was fixed. Switch to `eq(e.url,
activeEntityUrl)` and drop the limit — `url` is the primary key, so
the predicate already constrains the result to at most one row, the
same pattern the other entity-by-url queries (Workspace,
TileContainer) already use.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restructure the settings cog dropdown into a launcher with cascading
submenus (Theme, Local Runtime) plus a "Settings…" link that opens a
full settings screen at /settings/<category>. The screen mirrors the
macOS System Settings layout: a categories sidebar on the left and
bordered section cards on the right.

- Categories: General (provider API keys), Appearance (theme tile
  picker), Local Runtime (status badge + start/restart/stop, desktop
  only).
- Extract ApiKeysForm into a shared component reused by the
  first-launch modal and the General page.
- RootShell swaps the workspace sidebar for the settings sidebar
  while on /settings/* so the experience reads as part of the same
  shell.
- useDocumentTitle now recognises the settings route so the Electron
  Window menu reflects the active settings page rather than the
  previously-active session.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the hand-rolled tsdown + electronmon + wait-on chain with
vite-plugin-electron, which builds main + preload in watch mode and
manages the Electron child process with proper debouncing — no more
restart loop.

- agents-server-ui's Vite dev server runs in `--mode desktop` on a
  pinned port (5183) so the desktop main process can wait on it
  deterministically and load the renderer with full React Refresh /
  CSS HMR. The desktopHtmlMarker plugin runs in dev too, so
  `data-electric-desktop="true"` is on `<html>` from the first byte.
- Main / preload now build via Vite. All bare imports are
  externalised so Node resolves `node_modules` at runtime — fixes
  the jsdom→canvas build error and keeps `dist/main.js` at ~94 kB.
- `setActiveServer` no longer calls `restartRuntime()` when the
  active server is unchanged, so the renderer mount (which always
  fires `saveActiveServer(active)`, doubled by React 19 StrictMode
  in dev) doesn't trigger a triple Horton bootstrap on every window
  open.
- DevTools no longer auto-open on each window — multi-window setups
  get noisy fast. The standard View → Toggle Developer Tools menu
  item (Cmd+Opt+I / Ctrl+Shift+I) still works in every window.

Drop tsdown / electronmon / cross-env devDeps; add vite +
vite-plugin-electron. Concurrently + wait-on are kept for
orchestrating the parallel UI / desktop dev servers.

Co-authored-by: Cursor <cursoragent@cursor.com>
New ≡ dropdown between the server picker and settings cog. Group
the session list by Date / Type / Status, hide noisy types or
statuses via Show submenus, and Expand/Collapse all in one click.
Prefs persist to localStorage.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a way to choose a `workingDirectory` spawn arg for each new
Horton session, without touching the runtime's global cwd.

  • horton: accepts an optional `workingDirectory` spawn arg and
    routes it into the system prompt + filesystem tools, with
    fallback to the runtime's configured cwd.
  • agents-desktop: new `desktop:pick-directory` IPC for one-shot
    native folder picks (no persistence, no runtime restart).
  • new-session composer: a `WorkingDirectoryPicker` pill sits
    next to the model / reasoning controls, defaulting to the
    most-recently-used path.
  • sidebar filter menu: adds a "Working dir" group-by mode
    backed by `groupByWorkingDirectory` in `sessionGroups`.
  • shared UI: new `Combobox` primitive (Base UI wrapper, types
    mirror `Select<V extends string>`) used by the picker for
    typeahead + recents + native browse, with ServerPicker-style
    row geometry and a check-↔-IconButton swap on hover for
    removing recents.

Co-authored-by: Cursor <cursoragent@cursor.com>
Store provider tool-call IDs on tool call events and use them to match overlapping tool starts and completions reliably.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep tool_call/tool_result pairs valid under context budget truncation and merge adjacent assistant history blocks so resumed prompts remain API-compatible.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Align worker tool selection and prompts with the actual web_search tool name so spawned agents receive consistent instructions.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Use the full-radius token for badges so status labels render as rounded pills.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Warm entity stream connections through the route loader on sidebar intent so session timelines can reuse a preloaded StreamDB when opened.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Use the static hero title from the projects design and remove duplicate new-session title chrome from standalone tiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
Pick one of the hero title phrases when the new-session view mounts, without rotating or animating after load.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…dings

Two related polish passes:

  • Sidebar working-dir grouping now labels each bucket with a
    tildified, head-truncated path (`~/Code/electric`,
    `…/projects/acme`) instead of the bare basename, with the
    full absolute path surfaced as a `title` tooltip on hover.
    Truncation drops *leading* segments so the project folder
    stays visible — CSS ellipsis would lose it from the end.
    Path helpers extracted to `lib/pathDisplay.ts` and shared
    with `WorkingDirectoryPicker`.

  • Removed `text-transform: uppercase` (and paired
    `letter-spacing`) from every section/group heading across
    the app — sidebar, search palette, new-session screen,
    state-explorer headers, split menu, dropdown group labels.
    Bumped `font-weight: 500` (and 10 → 11px on the small
    sidebar / search labels) to keep heading prominence without
    relying on uppercase letterforms.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Patch release across the four touched packages: new
`@electric-ax/agents-desktop` Electron shell, the tile-based
workspace + dropdown / settings rework in `agents-server-ui`,
Horton's new `workingDirectory` spawn arg, and the runtime
tool-pair / event-matching fixes surfaced while building it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1417 1 1416 26
View the top 1 failed test(s) by shortest run time
test/integration.test.ts > HTTP Sync > multiple clients can get the same data in parallel (liveSSE=true)
Stack Traces | 30s run time
Error: Test timed out in 30000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".
 ❯ test/integration.test.ts:492:21

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Co-authored-by: Cursor <cursoragent@cursor.com>
KyleAMathews and others added 3 commits May 5, 2026 11:42
- Add resolve aliases to compile workspace packages from source TS
- Invert externalization: bundle all deps except native addons
  (better-sqlite3, sqlite-vec), optional native peer deps (canvas,
  bufferutil, utf-8-validate), filesystem-dependent (jsdom), and
  worker-thread-based (pino, pino-pretty)
- Switch main process output from ESM to CJS (array-wrap output
  config to override vite-plugin-electron's forced ESM format)
- Add externalized packages as direct deps for runtime resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KyleAMathews KyleAMathews force-pushed the samwillis/electron-app branch from 05e5c48 to 5068685 Compare May 5, 2026 19:28
KyleAMathews and others added 8 commits May 5, 2026 14:49
Generate unified diff patches in the edit and write tools using the
diff package, and render them in the UI with syntax-colored lines.
Replaces the old separate "Removed"/"Added" blocks with a single
diff view. Edit and write tool calls now default to expanded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When no STREAMS_DATA_DIR env var is set, persist embedded durable
streams data under ${cwd}/.streams-data instead of leaving the
directory undefined.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants