Skip to content

feat: worktree UX, PR status badges, reliable restart, model-spawn fix#69

Open
kovtcharov-amd wants to merge 37 commits into
mainfrom
feat/worktree-support
Open

feat: worktree UX, PR status badges, reliable restart, model-spawn fix#69
kovtcharov-amd wants to merge 37 commits into
mainfrom
feat/worktree-support

Conversation

@kovtcharov-amd

@kovtcharov-amd kovtcharov-amd commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

This branch started as a worktree UX redesign and now bundles several related fixes and a new PR-status feature.

Worktree UX

  • Child worktree workspaces render as collapsible inline groups under their parent (branch name, active indicator, remove button) instead of separate sidebar entries.
  • Per-task isolate toggle in the task input row; backend honors the isolate flag on task:create.
  • Worktree isolation guidance added to the MCP orchestration prompt.

Model / spawn fix

  • resolveClaudeSpawn now spawns the native bin/claude.exe shipped by newer @anthropic-ai/claude-code directly. The old code looked only for cli.js, fell back to cmd.exe /c claude.cmd, and that re-parsed the command line — corrupting the multiline --system-prompt and the --model flag after it, so spawned tasks ignored the configured model.

Task names

  • Decode HTML entities in task display names (e.g. notes & CInotes & CI) at rename and on serialization.

Backend restart (no-watch friendly)

  • start.ps1/start.sh run the backend in a relaunch loop: exit code 75 (from POST /api/server/restart) restarts it, any other code stops. Fixes the restart button, which previously killed the backend with no recovery.
  • start.ps1 caches the process handle before WaitForExit so the exit code is readable; full process-tree kill on exit frees ports and fixes stuck Ctrl+C.
  • dev script sets CLAUDIA_WATCH_MODE; the endpoint touches index.ts under tsx watch but exits 75 in no-watch. gracefulShutdown takes an exit code and guards against double-shutdown.
  • Default remains no-watch (stable); -Watch / --watch opt-in.

PR status badges

  • Backend resolves a workspace branch's GitHub PR via gh (getPrForBranch), cached in-memory (never persisted), refreshing only active/unseen workspaces every 90s with reentrancy + per-workspace in-flight guards to avoid gh storms on slow-auth machines. Also refreshes on task create and workspace reset.
  • PrBadge renders #number tinted by PR state (draft/open/merged/closed) with a subtle CI status mark, in the worktree group header and the main workspace header. Tooltip shows PR number, title, state, and CI status.

Misc

  • TerminalView guards scrollToLine against a non-integer viewportY (xterm threw "This API only accepts integers" on NaN), stopping an error loop.
  • Orchestration prompt biases agents toward spawning Claudia tasks over internal subagents.

Test plan

  • Worktree groups render inline, collapsible, with branch name; isolate toggle creates a worktree task.
  • Spawned tasks run the configured model (verify via session JSONL = configured model id).
  • Task names with &/entities display as plain text.
  • Launch via .\start.ps1 (no-watch); the in-app restart button restarts the backend and it recovers; Ctrl+C stops cleanly and frees ports 4001/5173.
  • PR badge appears on a worktree group and a workspace whose branch has a PR; color matches state; CI mark matches gh pr checks; hover shows number + title + state + CI; click opens the PR.
  • Reset a workspace to main → PR badge clears.
  • No gh / no PR / gh unauthenticated → no badge, no backend errors.

Ovtcharov and others added 16 commits May 11, 2026 15:40
showBrowseButton was hardcoded to false, preventing users from using
the native folder picker. Set to true so the Browse button appears
and opens the OS folder dialog via the existing WebSocket handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add drag-and-drop support on the workspace panel: drop a folder from
  the OS file explorer to add it as a workspace. In Electron, the full
  path is extracted directly. In the browser, opens the path input modal.
- Fix Browse button: use REST endpoint instead of blocking WebSocket
  execFileSync which froze the server. Only show Browse in Electron mode
  where the native dialog works reliably.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: syncWorkspaceMcpConfigs wrote .mcp.json to the claudia
project root on every startup, triggering tsx watch to restart the
server in an infinite loop. Now skips syncing to Claudia's own
workspace directory.

Also:
- Fix Browse button: add -STA flag for Windows PowerShell folder dialog,
  remember last browsed path across sessions, kdialog fallback on Linux
- Re-enable Browse button in Add Workspace dialog
- Fix drag-and-drop: only activate for external OS drops (Files type),
  internal workspace reordering drags pass through unaffected

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 512KB caps were too aggressive — users lost scrollback history
after rotation. Disk files now cap at 10MB (rotate keeping 5MB tail),
and clients receive up to 2MB of history for scrollback. Memory loading
on reconnect remains capped at 512KB to prevent OOM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a scrollbar appears/disappears during active output, the container
width changes by ~15px, flipping cols by 1-2. This caused Claude Code's
TUI to re-render at alternating widths, producing overlapping garbled
text.

Fix:
- Skip resize events where cols changed by <= 2 (scrollbar noise)
- Track last sent cols/rows to deduplicate
- Increase ResizeObserver debounce from 50ms to 150ms
- Use fitTerminal() (fit + refresh) to clear artifacts after resize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
References #59

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

The previous policy only reconnected tasks with shouldContinue=true
(busy/starting at time of shutdown). This meant every tsx watch restart
disconnected all idle tasks (34 tasks showing 'disconnected'), requiring
manual clicks to reconnect each one.

Now reconnects ALL tasks that were active within the last 2 hours and
have a sessionId. Mid-turn tasks still get priority in reconnect order.
Batching (2 at a time with delays) prevents resource exhaustion.
1. TaskInputBar: queue messages when WebSocket is not open instead of
   silently dropping them. Retries every 500ms until WebSocket reconnects.
   Previously typing 'continue' while disconnected did nothing — the
   message stayed in the input but was never sent.

2. Start scripts: switch from 'dev' (tsx watch) to 'dev:no-watch' to
   prevent spurious server restarts from file changes (Claude Code tasks
   editing source files, antivirus, Windows indexer). The UI restart
   button still works for manual restarts.

3. CLAUDE.md: updated with current architecture, no-push-to-main rule,
   no attribution comments rule.
…disconnect

# Conflicts:
#	CLAUDE.md
#	frontend/src/components/TerminalView.tsx
Re-pasting on retry caused duplicate content because Claude Code wraps
long prompts across multiple lines — Ctrl+U only clears the current line,
leaving the rest. Now the prompt is written once; only Enter is retried.
…e, MCP guidance

- Worktree child workspaces are hidden from the sidebar and rendered
  inline within their parent workspace as collapsible groups
- Each group shows the branch name (or task name after Claude auto-titles)
  with a green dot for active tasks and a remove button
- Added isolate toggle button in the task input row (GitBranch icon)
  that creates a worktree for the next task when enabled
- Backend now reads `isolate` flag from task:create messages and creates
  a worktree when set (previously only workspace-level autoWorktree worked)
- When a task in a worktree is renamed, the worktree workspace displayName
  updates to match so the group header shows a human-readable label
- Added worktree isolation guidance to the MCP orchestration system prompt
  instructing Claude to use isolate:true for file-modifying parallel tasks
- Silent worktree creation failures now send a WS error to the client
Spawned tasks inherited ANTHROPIC_MODEL from the shell that launched the
server (e.g. a corporate proxy profile), which in interactive PTY mode
beat the --model flag -- so tasks silently ran the inherited model
regardless of the Claudia setting. getTaskEnvironment now overrides
ANTHROPIC_MODEL with the configured defaultModel so the Claudia setting
is authoritative.

Also: validate effortLevel/defaultModel config switches, and add a
-Watch switch to start.ps1 to opt into tsx auto-reload (default stays
no-watch for stability).
…croll

- resolveClaudeSpawn now detects the native bin/claude.exe shipped by newer
  @anthropic-ai/claude-code and spawns it directly. The old code looked only
  for cli.js, didn't find it, and fell back to `cmd.exe /c claude.cmd`, which
  re-parsed the command line and corrupted the multiline --system-prompt and
  the --model flag after it -- so spawned tasks ignored the configured model.
  Reverts the earlier ANTHROPIC_MODEL env override, which was the wrong fix.
- decodeHtmlEntities normalizes task display names (e.g. "notes &amp; CI" ->
  "notes & CI") at rename and on serialization, fixing legacy/agent-encoded
  names rendered as plain text in the UI.
- TerminalView guards scrollToLine against a non-integer viewportY (xterm
  throws "This API only accepts integers" on NaN), stopping an error loop.
Updates the injected orchestration guidance and the claudia_create_task
tool description to instruct agents to prefer spawning Claudia tasks over
launching their own internal subagents for delegatable work — Claudia
tasks are user-visible, monitorable, resumable, and worktree-isolated.
Internal subagents remain the fallback for quick inline lookups or when a
task would clearly produce a worse outcome.
PR badges:
- Resolve a workspace branch's GitHub PR via `gh` (getPrForBranch), cached
  in-memory on the workspace store (never persisted) and pushed to the UI.
- Refresh only active/unseen workspaces every 90s with reentrancy and
  per-workspace in-flight guards, to avoid `gh` process storms on slow-auth
  machines; also refresh on task create and workspace reset.
- New PrBadge component renders #number tinted by PR state (draft/open/
  merged/closed) with a subtle CI status mark, in the worktree group header
  and the main workspace header. Tooltip shows PR number, title, state, CI.

Backend restart:
- start.ps1/start.sh run the backend in a relaunch loop; exit code 75 (from
  POST /api/server/restart) restarts it, any other code stops. Fixes the
  restart button, which previously killed the backend with no recovery.
- start.ps1 caches the process Handle before WaitForExit so ExitCode is
  readable; full process-tree kill on exit frees ports and fixes stuck Ctrl+C.
- `dev` script sets CLAUDIA_WATCH_MODE so the endpoint touches index.ts under
  tsx watch but exits 75 in no-watch. gracefulShutdown takes an exit code and
  guards against double-shutdown.
@kovtcharov-amd kovtcharov-amd changed the title feat: worktree UX redesign with inline grouping and per-task isolation feat: worktree UX, PR status badges, reliable restart, model-spawn fix Jun 2, 2026
Ovtcharov added 13 commits June 2, 2026 14:36
When a task runs raw `git worktree add` (instead of Claudia's auto-worktree
path), the new worktree wasn't registered as a workspace and never showed in
the sidebar. discoverWorktrees() scans repos that have Claudia tasks, finds
git worktrees not yet known, and registers them via addWorktreeWorkspace +
broadcast. Triggered when a task goes idle and on a 60s periodic sweep, with
a concurrency guard. Scoped to repos with active tasks to avoid surfacing
unrelated pre-existing worktrees.
A worktree with a single task now renders inline as a normal task row with
a small worktree icon badge (branch in tooltip) plus a PR badge for its
branch, instead of a collapsible group section. Worktrees with 2+ tasks keep
the group (where the PR badge lives on the group header, so the per-task
badge would be redundant).
Rework worktree discovery: instead of mass-registering every worktree in a
repo (which floods repos with dozens of worktrees), detect when a task's
Claude session creates a NEW worktree by diffing the repo's worktree list
against a per-repo baseline taken at first scan. A branch that appears while
a task runs in that repo is attributed to the most-recently-active task and
the task row is annotated with sessionWorktreeBranch + PR info, showing the
worktree badge in place. The task's PTY cwd stays at the parent repo, so the
session's branch isn't otherwise visible.
Long task prompts sometimes arrived truncated at the start ("began
mid-sentence") because Claude Code's TUI input box repaints while the prompt
is being written, dropping leading characters. Wrap the initial-prompt write
in bracketed paste (ESC[200~ ... ESC[201~) so the terminal delivers it as one
atomic paste. Claude Code enables bracketed-paste mode (ESC[?2004h), so the
markers are consumed. Strip any stray end-marker in the prompt defensively.
A worktree workspace stored the branch it was created with, but Claude often
switches to or creates a different branch inside the worktree and opens the PR
there (e.g. claudia/task-xxxx -> fix/my-feature). PR lookup queried the stale
stored branch and found no PR, so the badge stayed empty. Now resolve the
worktree's CURRENT branch via getCurrentBranch(worktreePath), falling back to
the stored worktreeBranch.
The PR refresh only polled active-task workspaces on the 90s interval, so
when a task switched branches and went idle the badge stayed stale. Now
trigger refreshPrInfoFor on idle, same as worktree discovery, so the badge
updates promptly after Claude finishes a step that changed the branch.
Previously the PR badge only refreshed on the 90s interval, so branch
switches mid-task showed stale badges. Now refreshPrInfoFor does a fast
local getCurrentBranch check on every idle/busy state change and only
calls the expensive gh API when the branch actually changed. The periodic
interval uses force=true to also catch CI status updates on unchanged
branches.
The dropdown menu used position:absolute inside an overflow-y:auto container,
so it got clipped when near the bottom. Switched to position:fixed and
calculate coordinates from the trigger button's viewport rect. The menu
closes on scroll since its fixed position would detach from the trigger.
… prompt

The initial-prompt path used bracketed paste to prevent front-truncation,
but the writeToTask follow-up path (line 3404) still wrote raw text. Large
pasted messages (e.g. release notes with markdown/emoji) got truncated the
same way. Now both paths wrap in ESC[200~...ESC[201~.
Archived tasks were rendered in insertion order (oldest first) and showed
the raw prompt text instead of the displayName set by the agent. Now sorted
by lastActivity descending and uses displayName when available.
Messages sent while a task is busy were written as raw text without
bracketed paste, causing the same truncation/loss as the initial-prompt
bug. Now all multi-char message writes use bracketed paste regardless of
task state. Reverts the queuing approach (the TUI does accept input while
busy when delivered correctly).
Adds four MCP tools so Claude Code sessions can self-schedule:
- claudia_cron_create: schedule a recurring or one-shot prompt on any task
- claudia_cron_list: list all scheduled tasks (optionally filtered by taskId)
- claudia_cron_delete: cancel a scheduled task by cronId
- claudia_cron_pause: pause/resume a scheduled task

The orchestration system prompt is updated so each session knows its own
task ID and how to use cron expressions for periodic checks, delayed
follow-ups, or recurring monitoring.
gh pr list --head main returns unrelated PRs (e.g. from forks) that aren't
the workspace's own PR, so a workspace on main incorrectly showed a badge.
Now skip the PR lookup entirely when the branch matches the repo's default
branch and clear any stale prInfo.
Ovtcharov added 8 commits June 5, 2026 10:09
The claudia MCP server needs per-task env vars (CLAUDIA_WORKSPACE_ID,
CLAUDIA_TASK_ID) to function. It was being included in the workspace
.mcp.json sync WITHOUT those vars, and Claude Code loaded that env-less
copy instead of the per-task --mcp-config copy -- causing "Claudia MCP
tools aren't available." Only inject the claudia server entry when a task
context (workspaceId) is present, so it only appears in --mcp-config.
The PR badge <a> tag is natively draggable, which intercepted dragenter
events when dragging workspaces over a header containing a PR badge,
preventing the drop-target highlight. Set draggable={false} on the link.
PR info broadcasts were sending the entire workspaces array via
workspace:updated, which triggered setWorkspaces() on the frontend and
re-rendered the full workspace list. This killed in-progress workspace
drag-and-drop because the browser's native drag tracking lost its DOM
references. Now broadcast only the affected workspace (singular payload),
which uses updateWorkspace() — a targeted merge that doesn't re-render
unrelated workspace sections.
Inline worktree task items were draggable={true} with no-op drag handlers,
which let them initiate spurious drags and intercept dragenter events from
neighboring real task drags. Set draggable={false} when worktreeInfo is
present so they don't interfere with task reordering.
The system prompt now explicitly tells Claude to keep the title current
whenever the focus of work shifts (not just "if work evolves significantly").
The mid-session title instruction is injected every 5th follow-up message
instead of only once, reinforcing that stale titles should be updated.
Resumed sessions (--resume) don't load MCP servers from --mcp-config,
only from the workspace .mcp.json. The previous fix excluded claudia
from .mcp.json entirely, breaking MCP tools on resumed tasks. Now build
a per-workspace .mcp.json with CLAUDIA_WORKSPACE_ID set so the claudia
MCP server works on both new and resumed sessions. Also debounce the
worktree discovery trigger and cache non-git repos to reduce log spam.
The four claudia_cron_* tools were registered twice, causing the MCP
server to crash on startup with "Tool claudia_cron_create is already
registered". This silently broke ALL claudia MCP tools for every task
session — the root cause of the persistent "Claudia MCP tools aren't
available" reports.
Critical: parseWorktreeListOutput had isFirst inside the loop, so every
worktree got isMain=true. discoverWorktrees filtered !isMain which always
returned empty -- session worktree attribution never worked. Fixed by
setting isMain=false for all, then marking only results[0] as true.

Also: task:rename workspace broadcast uses singular update (not full array)
to avoid killing drag; removed double-decode in toPublicTask; clean up
prInfoCache on workspace delete.
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