Skip to content

feat(agents): add server-side projects with working directory support#4273

Open
KyleAMathews wants to merge 27 commits intomainfrom
projects
Open

feat(agents): add server-side projects with working directory support#4273
KyleAMathews wants to merge 27 commits intomainfrom
projects

Conversation

@KyleAMathews
Copy link
Copy Markdown
Contributor

@KyleAMathews KyleAMathews commented May 4, 2026

Agents now have projects (named directories) that set the agent's working directory and group sessions in the sidebar. Also adds a 404 page, UI polish, and AGENTS.md context loading. Previously, all sessions shared the server's startup cwd and appeared in a flat timeline.

image

Approach

Projects — three layers, bottom-up

1. Server-side project store (project-store.ts)
Projects persist to ~/Library/Application Support/electric-agents/projects.json (or OS equivalent) via env-paths. Plain JSON read/write — no database needed for a handful of user-defined entries. Path validation expands ~ and resolves symlinks so stored paths are always canonical absolute paths.

2. REST API (project-routes.ts)
ProjectRoutes class following the existing ElectricAgentsRoutes pattern:

  • GET/POST /_electric/projects — list and create
  • PATCH/DELETE /_electric/projects/:id — update and delete
  • POST /_electric/validate-path — pre-submit validation for immediate UI feedback

Wired into server.ts before entity-type routes so the path prefix doesn't collide.

3. UI + agent wiring

  • useProjects hook rewritten from localStorage to server API
  • NewSessionPage redesigned with a hero heading (rotating verbs: "Let's ship", "Let's build", etc.) and a popover project picker with inline create form
  • On spawn, the active project's path is passed as workingDirectory in spawn args and the session is tagged with the project ID for sidebar grouping
  • Horton's creationSchema accepts an optional workingDirectory; the handler computes effectiveCwd (prefer spawn arg, fall back to server default) and validates the directory exists before running — writes a clear error to the stream if invalid

404 page

EntityPage now detects missing entities: if 2 seconds pass with the entities collection loaded and no match, it shows a "Not found" page instead of spinning "Loading entity..." forever. Unknown routes are caught by notFoundComponent on the root route. Removed the silent redirect-on-error from GenericEntityBody — errors now surface in the timeline instead of silently navigating home.

AGENTS.md context

Horton now reads AGENTS.md from the working directory (if it exists) and includes it as a stable context source, wrapped in XML tags. This gives agents project-specific instructions without manual prompting.

UI polish

Warmer light theme, tool trace refinements, focus fixes, wider chat column, font switch from Inter to Figtree.

Key invariants

  • Stored project paths are always resolved absolute paths (validated + expanded at creation time)
  • activeProjectId in localStorage is reconciled against the server list on load — stale references to deleted projects are cleared
  • Horton validates effectiveCwd exists before tool setup; invalid directories produce a user-visible error instead of mysterious tool failures
  • workingDirectory is filtered from the composer's inline pill controls so it doesn't leak as a user-facing schema field
  • Entity 404 only triggers after 2s with the collection loaded — avoids false positives during initial sync

Non-goals

  • No file-level locking on the project store. Concurrent writes from multiple tabs could race, but for a single-user dev tool this is acceptable.
  • No shared Project type. The interface is duplicated between server and client. Will be unified when the projects data model is redesigned.
  • Error handling on fetch/delete/rename is minimal. These hooks will be rewritten when projects move to a different storage layer.

Verification

pnpm vitest run packages/agents/test/

Manual:

  • Create a project via the popover (try ~/some/path and absolute paths)
  • Verify project persists across page refresh
  • Start a session with a project selected — check horton operates in that directory
  • Start a session without a project — verify default cwd behavior
  • Delete a project's directory on disk, start a session — verify clear error message
  • Navigate to a nonexistent entity URL — verify 404 page appears
  • Navigate to a garbage URL — verify catch-all 404

Files changed

File What
agents-server/src/project-store.ts New — JSON file store with env-paths, ~ expansion
agents-server/src/project-routes.ts New — REST CRUD + path validation
agents-server/src/server.ts Wire project routes before entity-type routes
agents/src/agents/horton.ts workingDirectory spawn arg, effectiveCwd validation, AGENTS.md context
agents-server-ui/src/router.tsx 404 page for missing entities and unknown routes
agents-server-ui/src/hooks/useProjects.tsx Rewrite from localStorage to server API, stale ID reconciliation
agents-server-ui/src/components/NewSessionPage.tsx Hero layout, rotating verbs, popover project picker
agents-server-ui/src/components/NewSessionPage.module.css Styles for hero + project picker
agents-server-ui/src/ui/tokens.css Switch body font to Figtree, bump --ds-text-sm, warmer palette
agents-server-ui/src/components/toolBlock.module.css Tool trace polish
agents-server-ui/src/components/EntityTimeline.module.css Timeline style tweaks
agents-server-ui/src/router.module.css Wider chat column

🤖 Generated with Claude Code

KyleAMathews and others added 9 commits May 4, 2026 16:41
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces localStorage-only implementation with server-backed CRUD.
Projects are now persisted on disk via the agents-server API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…icker

Replaces the old Select-based project picker with a centered hero layout:
"Let's build <project-name> ^" with a Popover for project selection and
inline creation with path validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cycles through verbs like "Let's ship", "Let's create", "Let's explore"
every 4 seconds. Bumps heading to size 7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds @fontsource-variable/figtree and imports it in main.tsx so the
font actually loads. Updates --ds-font-body token accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix useRotatingVerb off-by-one (last verb never appeared as initial)
- Reconcile stale activeProjectId after fetching projects from server
- Validate workingDirectory exists before running horton agent
- Expand ~ in paths server-side (validatePath)
- Use <Button> component for project create form
- Simplify readProjects ENOENT check, updateProject find, error parsing
- Change "Let's hack on" to "Let's hack"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 21.38365% with 375 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.50%. Comparing base (a3cee92) to head (a078c5f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...agents-server-ui/src/components/NewSessionPage.tsx 0.00% 85 Missing ⚠️
packages/agents-server/src/project-routes.ts 0.00% 67 Missing ⚠️
...ackages/agents-server-ui/src/hooks/useProjects.tsx 0.00% 60 Missing ⚠️
packages/agents-server/src/project-store.ts 4.34% 44 Missing ⚠️
...ages/agents-server-ui/src/lib/entity-connection.ts 0.00% 27 Missing ⚠️
...es/agents-server-ui/src/hooks/useEntityTimeline.ts 0.00% 23 Missing ⚠️
packages/agents-server-ui/src/router.tsx 0.00% 21 Missing ⚠️
packages/agents/src/agents/horton.ts 36.66% 18 Missing and 1 partial ⚠️
packages/agents-runtime/src/pi-adapter.ts 77.27% 10 Missing ⚠️
...nts-server-ui/src/components/MarkdownCodeBlock.tsx 0.00% 9 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4273      +/-   ##
==========================================
- Coverage   63.06%   62.50%   -0.57%     
==========================================
  Files         184      186       +2     
  Lines       19932    20274     +342     
  Branches     4962     5052      +90     
==========================================
+ Hits        12571    12673     +102     
- Misses       7358     7597     +239     
- Partials        3        4       +1     
Flag Coverage Δ
packages/agents 60.62% <35.48%> (-0.58%) ⬇️
packages/agents-runtime 79.64% <86.17%> (+0.40%) ⬆️
packages/agents-server 64.18% <8.13%> (-1.86%) ⬇️
packages/agents-server-ui 1.58% <0.00%> (-0.07%) ⬇️
packages/electric-ax 38.59% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 94.27% <ø> (ø)
packages/y-electric 56.05% <ø> (ø)
typescript 62.50% <21.38%> (-0.57%) ⬇️
unit-tests 62.50% <21.38%> (-0.57%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

KyleAMathews and others added 15 commits May 4, 2026 17:36
… routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warmer page/sidebar backgrounds, softer tool-call cards with subtle
shadows and rounder corners, pill-shaped badges, gentler sidebar
selection tint, composer elevation, and cleaner spacing/hierarchy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace browser-default blue focus outline on tool card toggles
  with subtle accent bg on focus-visible
- Section labels (Command, Output, etc.) use small-caps + 600 weight
  so they read as proper section headers
- Warmer code block background via color-mix
- Jump-to-bottom button: smaller, translucent, anchored bottom-right
- Spawned/stopped timestamps left-aligned with border-left rail
- Sidebar type labels dropped to xs, project headers bolder
- Wider transcript column (72ch / 88ch) for code-heavy sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blue-charcoal dark mode palette, 4-level text hierarchy, radial wash,
brighter pill controls, raised composer contrast, and tighter center
stack spacing. Completes the dark mode visual pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…iling padding

Shiki's multi-theme mode sets `color: var(--shiki-light)` as an inline
style, which always beats the CSS dark-mode override. Strip the inline
color and let CSS rules handle theme switching. Also trim the trailing
empty line Shiki appends to tokenized output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nk + route loader

- connectEntityStream is now cached (baseUrl+entityUrl key) so the
  route loader and useEntityTimeline share the same db instance
- registerActiveBaseUrl/getActiveBaseUrl lets the route loader read
  the active server URL outside React context (set by useServerConnection)
- entityRoute gets a loader that awaits connectEntityStream so the db
  is ready before the component renders
- defaultPreload:'intent' on the router fires the loader on Link hover
- SidebarRow swapped from div[role=button] to <Link preload='intent'>
  so hovering a session row starts the stream connection immediately
- useEntityTimeline accepts an optional preloadedDb arg; when provided
  it skips connectEntityStream and uses the loader-supplied instance
- closeEntityStream evicts the cache and closes the db on cleanup
- SidebarRow.module.css: text-decoration:none to suppress link underline
@KyleAMathews KyleAMathews requested a review from samwillis May 5, 2026 03:21
KyleAMathews and others added 3 commits May 4, 2026 21:25
…dget truncation

Budget truncation was dropping individual messages by token size, which could
split tool_call/tool_result pairs — leaving orphaned tool_use or tool_result
blocks that violate the Claude API. Now oversized tool_call/tool_result messages
get their content replaced with a truncation stub pointing to load_timeline_range,
preserving pairing while still saving budget.

Also merges consecutive assistant messages (text + tool_call) in toAgentHistory
to prevent consecutive assistant blocks that the Claude API rejects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… truncation

The stub approach alone doesn't handle the case where budget exhaustion drops
a tool_call while its tool_result survives (or vice versa). After the budget
loop, scan accepted messages and drop any tool_call without a matching
tool_result and any tool_result without a matching tool_call.

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.

1 participant