Skip to content

Run tweaks generation in background agent jobs with shadow-workspace apply - #94

Open
jamesrochabrun wants to merge 1 commit into
mainfrom
easel-background
Open

Run tweaks generation in background agent jobs with shadow-workspace apply#94
jamesrochabrun wants to merge 1 commit into
mainfrom
easel-background

Conversation

@jamesrochabrun

Copy link
Copy Markdown
Owner

Summary

The Tweaks popover's Ideas and Describe a tweak… actions previously injected a prompt into the live interactive chat session, blocking the chat for the entire (slow) generation. They now run as background headless agent jobs — the chat stays fully interactive while tweaks generate, with progress, cancel, and undo surfaced on the canvas.

How it works

  1. Shadow workspace — the project is copied into .easel/tweaks/<jobId>/shadow (ignored by the preview file observers) with a SHA-256 manifest of every file. The agent only ever edits the copy.
  2. Headless run — a one-shot agent run (ClaudeCodeSDK.runSinglePrompt / codex exec) executes against the shadow, configured identically to the interactive runtimes (binary resolution, model, sandbox/permissions, env).
  3. Diff + validate — changed files are found by re-hashing the shadow against the manifest (provider-agnostic; catches Bash-made edits). The output must declare a parseable dc_set_props schema (with a changed-files fallback scan for dev-server projects where a component gets instrumented instead of index.html).
  4. Auto-apply when safe — if no chat turn is in flight and no changed file drifted in the real tree, changes apply atomically with per-file backups. If chat is busy, the job waits and re-checks drift after the turn ends; drift is re-checked once more immediately before writes.
  5. Conflicts ask — concurrent edits to the same files (chat agent or tweak-knob writes) surface a conflict pill: Regenerate on latest / Apply anyway / Discard. Apply-anyway remains undoable because the backup captures the drifted content.
  6. Cancel & undo — cancel works at any stage (Claude kills the process; Codex is flag-cancelled and its result discarded — safe since it only writes to the throwaway shadow, with a timeout backstop). Undo restores pre-apply content and warns if files changed again after apply. Stale artifacts from crashes are swept on next use.

Architecture

  • EaselKit — shared vocabulary: job model/status/failure enums, BackgroundAgentJobCoordinating (UI surface), BackgroundAgentRunning (runner), BackgroundJobValidating, FileContentHasher, and ProjectScanIgnoreList (hoisted from the preview observer so scan scope and apply scope stay in lockstep).
  • ClaudeCodeCoreClaudeBackgroundAgentRunner, CodexBackgroundAgentRunner, and CodexClientFactory (extracted from CodexChatRuntime.makeClient() with a parity test; the runtime now delegates to it).
  • EaselChatShadowWorkspaceManager, BackgroundJobApplyEngine, and the @Observable BackgroundAgentJobService (FIFO, one running job at a time); ChatService.isAnySessionBusy(workingDirectory:) + makeBackgroundJobService(validator:).
  • EaselWebInspectorTweaksSchemaJobValidator (Canvas parser), TweaksJobRequestFactory, BackgroundJobStatusPill, and WebInspectorPreviewView wiring (badge on the Tweaks button, pill overlay, popover status).
  • CanvasTweaksGenerationStatus + host-driven status row and control-disabling in TweaksPanelView (additive, source-compatible API).

Provider behavior

  • Claude: real cancellation via client.cancel(); .bypassPermissions matching the interactive path.
  • Codex: options mirror the interactive first-turn (--sandbox workspace-write --full-auto --json, stdin prompt) pointed at the shadow root; cancellation is abandon-and-flag (CodexSDK has no process handle) with the job timeout as kill switch.
  • Local / API (new on main): no headless runner yet — jobs fail gracefully with a message pointing at the pill's Send to chat fallback, which works with that provider.

The legacy chat path is kept as a silent fallback whenever no coordinator is wired or the preview URL can't be mapped into the project — the buttons never go dead.

Dependency note

EaselWebInspector and EaselPreview pin Canvas to revision 62c3cce (the background-tweaks-status branch, Canvas repo). Once Canvas 1.4.0 is tagged, both pins should move to exact: "1.4.0".

Validation

  • Canvas: 161 tests pass (swift test), including the new TweaksGenerationStatus suite.
  • EaselKit: 17 tests pass — hasher vectors (streaming file/data parity), job-model semantics.
  • ClaudeCodeCore: 192 tests pass — includes CodexClientFactory precedence parity, both runners' option construction, activity-event parsing, and error normalization; existing Codex runtime/mapper tests unaffected by the makeClient extraction.
  • EaselChat: 181 tests pass (xcodebuild -scheme EaselChat-Package) — real-filesystem suites for the shadow manager (ignore-list exclusion, manifest hashing, modified/created/deleted classification, stale sweep) and apply engine (drift, backup+atomic apply, undo, binary round-trip); 19 orchestrator tests with scripted mocks (FIFO, busy-wait→apply, drift→conflict, all three resolutions, cancel queued/running, timeout/failure mapping, retry, undo force semantics, terminal auto-dismiss); plus 3 end-to-end integration tests running the real pipeline on a temp project, including concurrent-edit → conflict → apply-anyway → undo.
  • EaselWebInspector: 213 tests pass — validator (target hit, component fallback, no-schema throw) and request factory (dev-server root/nested paths, file URLs, unmappable rejection).
  • Full Easel app scheme builds; branch is up to date with main (883b8ff, agent-harness merge) and the new .api provider case is handled.
  • Remote revision pin verified: fresh checkout of Canvas 62c3cce resolves and builds from GitHub.

Remaining manual QA (needs the GUI)

  • Ideas on a static HTML project: pill progress while chatting mid-generation; auto-apply reloads preview with knobs
  • Undo restores the file
  • Conflict prompt by chatting an edit to the same file mid-generation; exercise all three resolutions
  • Cancel mid-run under both providers
  • Busy-wait: job holds at "Waiting for chat…" during a long turn, applies after
  • Failure fallback: bogus codex command → failed pill → "Send to chat" lands the prompt in chat
  • React/Vite project Ideas run (component instrumentation + validator fallback)
  • Quit mid-job, relaunch → .easel/tweaks swept

🤖 Generated with Claude Code

…apply

The Tweaks popover's Ideas / describe-a-tweak actions no longer occupy
the interactive chat session. They now run as headless one-shot agent
jobs (claude runSinglePrompt / codex exec) against a shadow copy of the
project under .easel/tweaks/<jobId>/, then auto-apply when safe:

- BackgroundAgentJobService (EaselChat): FIFO pipeline with statuses
  queued -> preparing -> generating -> validating -> waitingToApply ->
  applying -> applied/conflict/failed, cancel anytime, undo after apply.
- Shadow workspace + hash-manifest diff detects exactly what the agent
  changed; the apply engine backs up real files before atomic writes.
- Drift safety: files edited concurrently (chat agent, knob writes)
  surface a conflict with Regenerate / Apply anyway / Discard; apply
  waits for in-flight chat turns and re-checks drift before writing.
- Providers: Claude and Codex runners in ClaudeCodeCore mirror the
  interactive runtimes' configuration; the Local/API provider fails
  gracefully with a send-to-chat fallback until it gains a runner.
- UI: floating status pill on the canvas (progress, elapsed, cancel,
  undo, conflict actions), badge on the Tweaks button, and status row
  in the popover (Canvas 62c3cce revision pin, 1.4.0 to follow).
- Legacy chat path kept as fallback when no coordinator is wired or
  the preview URL can't be mapped into the project.
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