Skip to content

fix(gui-app): surface the reason a create failed, and close its ghost tab - #1297

Merged
tanveergill merged 2 commits into
mainfrom
traycer/chat-create-failure-ux
Aug 19, 2026
Merged

tanveergill merged 2 commits into
mainfrom
traycer/chat-create-failure-ux

Conversation

@tanveergill

Copy link
Copy Markdown
Contributor

Fixes two GUI defects on the chat-creation failure path, found in today's
staging regression pass. One flow, one handler family.

Repro (staging, verified live): new-agent modal → workspace selector →
"New worktree" → give it a branch name that already exists → submit. The host
correctly hard-fails epic.createChat (internal #5073) with 409 RPC_ERROR
carrying git worktree add failed for traycer/tidy-badger …: fatal: a branch named 'traycer/tidy-badger' already exists.

1. The reason never reached the user

The GUI showed only "Couldn't create agent." — the cause was reachable only by
opening host.log. Worktree-creation failure deliberately mints no wire code
(WORKTREE_CREATE_FAILED is a chat-stream reject code only), so the message
text is the only place that reason exists.

useEpicCreateChatForHostClient now toasts through
toastFromHostErrorWithDetail, which appends the host's text only when no
mapped copy claimed the error — so E_HOST_UNSUPPORTED, WORKTREE_BUSY and
the transport arm keep their written-for-a-person sentences and never gain a
raw suffix. No protocol change.

2. The rejected create left a ghost tab

The eager-opened "Untitled agent" tab lingered: "Loading this agent from
<host>…", then, once the 15s tile budget elapsed, "This agent hasn't loaded
from <host> yet. That host hasn't answered." The host had answered — it
refused.

The mechanism to take that tab down already existed: a failed handoff is
terminal, which releases pendingCreateArtifactIds and lets
useEpicRouteSynchronization's record sweep close the tile. But the modal
marked the failure from mutate's per-call onError, and TanStack Query v5
gates mutateOptions on the observer still having listeners. This modal
renders its body behind props.open and closes itself synchronously in
cleanupAfterSubmit(), so both per-call callbacks were dead code. Only
useInitialChatHandoff's 60s orphan deadline — a backstop written for a host
that says nothing — eventually cleared it.

The submit now uses mutateAsync + a promise chain, which survives the
unmount. use-landing-composer-actions.ts already submits this way, for the
same reason. The onSuccess leg (markInitialTurnStarted, the turn-overlap
shortcut) was dead for the same reason and moves with it.

The failure arm uses markFailedByAction rather than markFailed: the handoff
key is {user, epic}, so a second create in the epic replaces the entry —
and now that this arm actually runs, an unguarded markFailed would close the
second agent's tab when the first one's rejection landed.

Tests

  • use-epic-create-chat-error-policy.test.ts (renamed from
    …-inline-fork-refusal.test.ts, its fork cases intact): the toast forwards a
    verbatim worktree-failure message, and a mapped code's copy is left alone.
  • new-conversation-create-rejection.test.tsx: the harness reproduces the real
    dialog's unmount-on-submit. A rejected create marks the handoff failed
    after the modal has closed, and fails only the handoff it was rejected
    for.

Each assertion was ablation-verified — removing the fix turns exactly the
matching test red, including the markFailedByActionmarkFailed
substitution.

bun run compile (all 5 projects), bun run lint (--max-warnings 0) and
prettier are clean; 1428 tests pass across the epic-canvas sidebar, chat,
host-workspace-selector, landing-composer and host-error-toast suites.

… tab

New agent -> New worktree -> a branch name that already exists: the host
hard-fails epic.createChat (409 RPC_ERROR carrying the git worktree add
stderr) and the GUI answered with "Couldn't create agent." plus an
"Untitled agent" tab that spun and then said the host had not answered.
The host had answered - with a refusal.

Two fixes, one flow:

- The create hook toasts through toastFromHostErrorWithDetail, so an
  RPC_ERROR no mapped copy claims appends the host's own text. Worktree
  creation failure mints no wire code by design (WORKTREE_CREATE_FAILED
  is a chat-stream reject code), so the message is the only place the
  reason exists; every mapped branch keeps its written-for-a-person
  sentence.

- The new-conversation modal submits with mutateAsync and a promise
  chain instead of mutate's per-call callbacks. It closes itself
  synchronously on submit, and TanStack Query v5 drops mutateOptions
  once the observer has no listeners - so both callbacks were dead
  code. The failure one is what marked the handoff terminal, releasing
  pendingCreateArtifactIds and letting the record sweep close the tile;
  without it only the 60s orphan deadline (a backstop for a host that
  says nothing) eventually cleared it. The landing composer already
  submits this way for the same reason.

The failure arm uses markFailedByAction rather than markFailed: the
handoff key is {user, epic}, so a second create in the epic replaces the
entry, and now that this arm runs an unguarded markFailed would close
the second agent's tab when the first one's rejection landed.

Tests: the toast forwards the worktree reason and leaves mapped copy
alone; a rejected create marks the handoff failed after the modal has
unmounted, and fails only the handoff it was rejected for.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 53550e97-f2c9-43dd-9447-c47ba779b7f1

📥 Commits

Reviewing files that changed from the base of the PR and between 7469861 and 9dceb13.

📒 Files selected for processing (11)
  • clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-create-rejection.test.tsx
  • clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-placement.test.tsx
  • clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-submit-gate.test.tsx
  • clients/gui-app/src/components/epic-canvas/sidebar/new-conversation-modal.tsx
  • clients/gui-app/src/hooks/epic/__tests__/use-epic-create-chat-error-policy.test.ts
  • clients/gui-app/src/hooks/epic/__tests__/use-epic-create-chat-inline-fork-refusal.test.ts
  • clients/gui-app/src/hooks/epic/use-epic-chat-mutations.ts
  • clients/gui-app/src/lib/__tests__/host-error-toast.test.ts
  • clients/gui-app/src/lib/chats/recoverable-fork-refusal.ts
  • clients/gui-app/src/lib/commands/actions/clone-chat-on-host-switch.ts
  • clients/gui-app/src/lib/host-error-toast.ts
💤 Files with no reviewable changes (1)
  • clients/gui-app/src/hooks/epic/tests/use-epic-create-chat-inline-fork-refusal.test.ts

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour.


Summary by CodeRabbit

  • Bug Fixes

    • Improved conversation creation handling when requests fail or are interrupted.
    • Prevented outdated errors from affecting newer conversations.
    • Pending handoffs now correctly transition to failed or sending states.
  • Improvements

    • Recoverable latest-fork issues no longer trigger unnecessary error notifications.
    • Host error messages are clearer and more concise, with long or multi-line details summarized.
    • Fork recovery now consistently recognizes missing checkpoints and unsupported host versions.

Walkthrough

The PR changes Epic conversation creation to use asynchronous mutation handling, adds handoff failure coverage across unmount and race conditions, centralizes recoverable fork refusal classification, and bounds host error details shown in toasts.

Changes

Epic chat creation and error handling

Layer / File(s) Summary
Shared refusal and host error contracts
clients/gui-app/src/lib/chats/recoverable-fork-refusal.ts, clients/gui-app/src/lib/commands/actions/clone-chat-on-host-switch.ts, clients/gui-app/src/lib/host-error-toast.ts, clients/gui-app/src/lib/__tests__/host-error-toast.test.ts
Recoverable latest-fork refusals now use a shared classifier. Host error details remove blank lines, truncate long lines, and count additional entries.
Create-chat mutation error policy
clients/gui-app/src/hooks/epic/use-epic-chat-mutations.ts, clients/gui-app/src/hooks/epic/__tests__/use-epic-create-chat-error-policy.test.ts
Recoverable latest-fork refusals suppress generic toasts. Other failures use detailed host error messages. Tests cover cleanup and terminal error cases.
Async modal and handoff lifecycle
clients/gui-app/src/components/epic-canvas/sidebar/new-conversation-modal.tsx, clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-create-rejection.test.tsx, clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-placement.test.tsx, clients/gui-app/src/components/epic-canvas/sidebar/__tests__/new-conversation-submit-gate.test.tsx
Conversation creation now uses mutateAsync. Handoffs transition after promise settlement and target only the matching create action. Tests cover unmounts, rejection, initial-turn state, and stale rejection races.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 9dceb

The PR is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Possibly related PRs

Suggested reviewers: hdkshingala

Poem

A rabbit watched the promise hop,
From pending burrow to success or stop.
Old refusals now know where to land,
Toasts keep details neat and planned.
Each handoff guards its matching track,
While stale errors cannot hop back.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes both primary fixes: exposing creation failure reasons and closing ghost tabs.
Description check ✅ Passed The description directly explains the two GUI defects, their causes, implemented fixes, and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch traycer/chat-create-failure-ux

Comment @coderabbitai help to get the list of available commands.

…ts detail

Follow-ups from the cold review of the previous commit.

1. A recoverable clone attempt no longer emits a detailed terminal error
   before succeeding. `cloneChatOnHostSwitch` treats two failures of its
   latest-checkpoint fork as recoverable - the source has no assistant
   checkpoint, or the target host predates epic.createChat@1.1 - and
   retries without forkSource after narrating the history downgrade. The
   shared onError toasted anyway, so a clone that WORKS was preceded by
   "Couldn't create agent. Cannot fork chat ...". Harmless while that
   toast was generic; the new detail policy made it read as a specific,
   terminal failure. The hook now stays silent on exactly those two
   codes, and only for a request carrying boundary: "latest" - the
   manual fork dialog's precise boundary has no retry behind it and
   still reports.

   The classifier moves to lib/chats/recoverable-fork-refusal.ts and is
   read by both seams. Duplicating it would fail silently: a clone flow
   that stopped recovering from one arm would leave the suppression
   swallowing a failure nobody reports.

2. The detail is bounded. worktreeCreateFailed joins one line per failed
   workspace, each an absolute path plus raw git stderr, so appending it
   verbatim made the toast an arbitrarily tall surface. It now shows the
   first non-empty line, capped at 240 characters with an ellipsis, and
   counts what was dropped ("(+2 more)") rather than hiding it. The cap
   is sized so a realistic single-entry worktree refusal survives whole.

3. Adds the symmetric success-after-unmount test. markInitialTurnStarted
   was dead for the same observer-unmount reason as the failure arm and
   had no test guarding its revival; a regression would leave the
   handoff short of `sending` and cost a redundant send. The suite now
   signs a profile in, so the request carries an initialMessage and the
   host answering initialTurnStarted: true is a state the system can
   actually produce.

Signed-off-by: Tanveer Gill <tanveer@traycer.ai>
@tanveergill
tanveergill merged commit d8ae05b into main Aug 19, 2026
20 checks passed
@tanveergill
tanveergill deleted the traycer/chat-create-failure-ux branch August 19, 2026 21:21
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