fix(gui-app): surface the reason a create failed, and close its ghost tab - #1297
Conversation
… 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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
💤 Files with no reviewable changes (1)
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
WalkthroughThe 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. ChangesEpic chat creation and error handling
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to The PR is merge-ready after normal checks and review; no actionable merge-blocking risk remains. Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
…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>
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 409RPC_ERRORcarrying
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_FAILEDis a chat-stream reject code only), so the messagetext is the only place that reason exists.
useEpicCreateChatForHostClientnow toasts throughtoastFromHostErrorWithDetail, which appends the host's text only when nomapped copy claimed the error — so
E_HOST_UNSUPPORTED,WORKTREE_BUSYandthe 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 loadedfrom
<host>yet. That host hasn't answered." The host had answered — itrefused.
The mechanism to take that tab down already existed: a
failedhandoff isterminal, which releases
pendingCreateArtifactIdsand letsuseEpicRouteSynchronization's record sweep close the tile. But the modalmarked the failure from
mutate's per-callonError, and TanStack Query v5gates
mutateOptionson the observer still having listeners. This modalrenders its body behind
props.openand closes itself synchronously incleanupAfterSubmit(), so both per-call callbacks were dead code. OnlyuseInitialChatHandoff's 60s orphan deadline — a backstop written for a hostthat says nothing — eventually cleared it.
The submit now uses
mutateAsync+ a promise chain, which survives theunmount.
use-landing-composer-actions.tsalready submits this way, for thesame reason. The
onSuccessleg (markInitialTurnStarted, the turn-overlapshortcut) was dead for the same reason and moves with it.
The failure arm uses
markFailedByActionrather thanmarkFailed: the handoffkey is
{user, epic}, so a second create in the epic replaces the entry —and now that this arm actually runs, an unguarded
markFailedwould close thesecond 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 averbatim worktree-failure message, and a mapped code's copy is left alone.
new-conversation-create-rejection.test.tsx: the harness reproduces the realdialog's unmount-on-submit. A rejected create marks the handoff
failedafter 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
markFailedByAction→markFailedsubstitution.
bun run compile(all 5 projects),bun run lint(--max-warnings 0) andprettier are clean; 1428 tests pass across the epic-canvas sidebar, chat,
host-workspace-selector, landing-composer and host-error-toast suites.