Skip to content

feat(chat): surface failed project switch inline (5-2) - #459

Merged
gnoviawan merged 4 commits into
devfrom
feat/drawer-project-switcher-5-2
Jul 29, 2026
Merged

feat(chat): surface failed project switch inline (5-2)#459
gnoviawan merged 4 commits into
devfrom
feat/drawer-project-switcher-5-2

Conversation

@gnoviawan

@gnoviawan gnoviawan commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Story 5-2 (Drawer Project Switcher) was a verify-and-close-gaps story. The drawer (ProjectSwitcherDrawer.tsx), its MobileChatShell mounting, the switch_project WS handler, the project registry mirror, the projects_changed live-sync, the queuedProjectSwitchId store field, and the "Queued" badge were already delivered by #449 (mobile ACP chat layout + web/remote project-list mirror, switch & live-sync) and #456 (multi-project switching and session persistence). The BMAD sprint still marked 5-2 backlog/ready-for-dev because the formal story was never "started" even though its deliverables shipped.

This PR verifies the existing implementation against the 5-2 ACs and closes the remaining gaps — it does not create a second drawer, switch mechanism, or data path.

The real problem it solves: a rejected/failed project switch was toast-only — the "Queued" badge just disappeared and the only failure signal was a transient toast, which is easy to miss on mobile. AC2 requires the drawer to surface the queued/rejected state clearly. This PR adds a transient inline "Failed" badge on the failed project row, plus closes the test-coverage gaps for the shell trigger and the failed mid-turn state.

Related Issue

No related issue — this is BMAD sprint story 5-2 (Epic 5: Mobile/Responsive ACP Chat). Builds directly on #449 and #456 (the PRs that delivered the existing drawer + switch pipeline). No prior PR addresses the inline failed-state gap or the two test gaps closed here (searched open + closed PRs to dev).

Type of Change

  • feat: new feature
  • test: adds or updates tests

What Changed

1. Inline "Failed" indicator for a rejected/failed project switch (T2.3, option a)

The existing queuedProjectSwitchId field surfaced the queued state inline (Clock3 + "Queued"). Failure was toast-only. Added a symmetric failedProjectSwitchId field on useAcpStore (no second state mechanism — mirrors the existing queued pattern):

  • src/renderer/stores/acp-store.ts
    • New state field failedProjectSwitchId: string | null + a setFailedProjectSwitch(projectId) action.
    • applyFailedProjectSwitch (the project_switch_failed event handler) now sets failedProjectSwitchId: event.projectId alongside clearing queuedProjectSwitchId + toast.error (covers the async queued-then-failed path).
    • switchProject clears failedProjectSwitchId at the start (a retry self-heals the stale badge); applyCompletedProjectSwitch clears it on success.
  • src/renderer/components/chat/ProjectSwitcherDrawer.tsx
    • Reads failedProjectSwitchId/setFailedProjectSwitch; on a sync rejection (switchProject throws — e.g. no_agent or a rejected reply) the handleSwitch catch now sets the failed id + still toasts.
    • Renders a red AlertCircle + "Failed" label on that row (priority: switching > queued > failed > active). The failed row stays enabled so it's retryable.
    • Transient: a useEffect clears the badge when the drawer closes, so a stale red indicator doesn't reappear on the next open.

Reuses existing lucide-react AlertCircle and the text-destructive Tailwind token — no new dependencies, no new UI library.

2. Test-coverage gaps closed (AC3)

  • src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx
    • Extended the useAcpStore mock with failedProjectSwitchId/setFailedProjectSwitch and added a sonner toast.error spy.
    • New: a rejected sync switch → setFailedProjectSwitch(id) + toast.error + drawer stays open + inline "Failed" badge on a retryable (non-disabled) row.
    • New: a queued→failed transition replaces the "Queued" badge with a "Failed" badge and re-enables the row.
  • src/renderer/components/mobile/MobileChatShell.test.tsx
    • Made isTauriContext mutable per-test (via a tauriRef) and stubbed ProjectSwitcherDrawer so the shell test focuses on trigger wiring.
    • New (web mode): the FolderGit2 "Switch project" trigger exists, opens the drawer on click, and onOpenChange(false) closes it.
    • New (Tauri mode): the trigger is hidden — desktop non-regression at the shell level.

Unchanged (verified against ACs, no edits): MobileChatShell.tsx, use-mobile-web-shell.ts, WorkspaceLayout.tsx, use-projects-persistence.ts, ChatInputBar.tsx, AgentChatPanel.tsx, web-projects.types.ts, all src-tauri/ server code.

How It Was Tested

  • bun run ci (Biome lint + format + imports, strict mode) — changed files pass Biome ci --diagnostic-level=error (the pre-commit hook also confirmed "Biome check passed" on staged files). The repo has pre-existing diagnostics in untouched files, none introduced here.
  • bun run typecheck — green (typecheck:node + typecheck:web + typecheck:test).
  • bun run test — green: 2478 passed, 43 skipped. One unrelated WorkspaceLayout.test.tsx "Color theme picker" waitFor timeout under heavy parallel load; it passes in isolation (15 passed) and is unrelated to project switching.
  • cargo clippy --all-targets -- -D warnings (in src-tauri) — N/A, no Rust changes (the optional server-side fail-first, T2.4, was explicitly skipped / supervisor-gated).
  • cargo test (in src-tauri) — N/A, no Rust changes.
  • Manual verification completed — partially: the desktop app builds and launches cleanly (no desktop regression — useMobileWebShell() is false in Tauri, FolderGit2 hidden in Tauri) and the web bundle (dist-web/) builds. The inline "Failed" badge is web/remote-only (!isTauriContext()), so a browser click-through at ≤767px via the in-process Remote Access server is pending; behavior is covered by the new renderer tests.

Screenshots or Recordings

The "Failed" badge is web/remote-only (mounted only !isTauriContext() at ≤767px) and requires the in-process Remote Access server + a mobile-width browser to view. Will add a recording on request; the render logic + transitions are asserted in ProjectSwitcherDrawer.test.tsx.

CI & Review Gate

I'll wait for all CI checks (PR Validation, Rust Checks, Build Verification, Security Scans) and the CodeRabbit/Claude review to complete, then address every finding before this is ready for merge.

  • All CI checks pass (PR Validation, Rust Checks, Build Verification, Security Scans)
  • Code review comments from CodeRabbit / Claude have been addressed or resolved
  • No unresolved review findings remain

Checklist

  • My PR title follows the conventional commit format used by this repo (feat(chat): …)
  • I linked the related issue or explained why none exists (BMAD story 5-2; no GitHub issue)
  • I updated docs when needed — no user-facing doc changes; inline badge is self-explanatory
  • I added or updated tests when needed — 5 new tests (3 drawer failed-state, 2 shell web/Tauri-mode)
  • I verified the change does not introduce unrelated modifications — 4 files, all in scope of story 5-2
  • I read AGENTS.md and followed the contributor guidelines
  • A human reviewed the complete diff before submission — diff is small (+166/−12 across 4 files); reviewers please confirm

Summary by CodeRabbit

  • New Features

    • Added inline Failed status for projects when switching fails.
    • Failed switches now trigger an error notification with the rejection message.
    • Failed switches are retryable while the project switcher drawer stays open.
    • Web/remote modes show the “Switch project” entry; Tauri mode hides it.
  • Bug Fixes

    • Cleared failed/queued indicators when the drawer closes or a switch succeeds.
    • Queued switches now transition to Failed correctly, and stale failure states don’t persist.
  • Tests

    • Expanded coverage for switch-failure, retry behavior, and mobile drawer open/close wiring.

Story 5-2 (Drawer Project Switcher) was a verify-and-close-gaps story:
the drawer, its MobileChatShell mounting, the switch_project WS handler,
the project registry mirror, projects_changed live-sync, and the
queuedProjectSwitchId "Queued" badge were already delivered (PRs #449,
#456). This commit verifies them against the 5-2 ACs and closes the
remaining gaps.

Gap closed (T2.3, option a): a rejected/failed project switch was
toast-only and the "Queued" badge just disappeared. Add a transient
inline "Failed" badge:
- useAcpStore gains a failedProjectSwitchId field (mirrors
  queuedProjectSwitchId; no second state mechanism) + a
  setFailedProjectSwitch action.
- applyFailedProjectSwitch sets failedProjectSwitchId on the
  project_switch_failed event (alongside clearing queued + toast).
- switchProject clears it at the start (retry self-heals);
  applyCompletedProjectSwitch clears it on success.
- ProjectSwitcherDrawer renders a red AlertCircle + "Failed" label
  (switching > queued > failed > active), keeps the failed row retryable,
  and clears the badge when the drawer closes.

Test gaps closed:
- ProjectSwitcherDrawer.test.tsx: add failedProjectSwitchId/
  setFailedProjectSwitch to the store mock, a sonner toast spy, and two
  tests (rejected sync switch → inline Failed + toast + stays open +
  retryable; queued→failed transition replaces Queued with Failed).
- MobileChatShell.test.tsx: flip isTauriContext via a mutable ref so a
  web-mode test asserts the "Switch project" trigger toggles the drawer
  open/closed, and a Tauri-mode test asserts the trigger is hidden
  (desktop non-regression).

No new dependencies, no server-side changes, no second drawer/switch/
data-path. bun run lint/typecheck/test green (one unrelated
WorkspaceLayout waitFor flake under heavy load; passes in isolation).
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

✅ PR description satisfies the template

Thanks for filling it in.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Project switching now records failed attempts, clears failure state across retries and drawer closure, and displays a retryable Failed badge with an error toast. Tests cover failure transitions and web-versus-Tauri project drawer behavior.

Changes

Project switch failure state

Layer / File(s) Summary
Failure state lifecycle
src/renderer/stores/acp-store.ts, pr-body-current.md
ACP state now stores failedProjectSwitchId, clears it during new and completed switches, and sets it when a project_switch_failed event arrives.
Drawer failure feedback
src/renderer/components/chat/ProjectSwitcherDrawer.tsx, src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx, pr-body-current.md
Rejected switches set failure state, show a destructive Failed badge, trigger an error toast, keep the drawer open, clear stale state on close, and restore retry behavior after queued failures.
Web project switching coverage
src/renderer/components/mobile/MobileChatShell.test.tsx, pr-body-current.md
Tests cover hiding the switch control in Tauri mode and opening and closing the project drawer in web mode.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProjectSwitcherDrawer
  participant acp-store
  participant ACP
  participant sonner
  ProjectSwitcherDrawer->>acp-store: Start project switch
  acp-store->>ACP: Process switch request
  ACP-->>acp-store: project_switch_failed
  acp-store-->>ProjectSwitcherDrawer: failedProjectSwitchId
  ProjectSwitcherDrawer->>sonner: Show rejection message
  ProjectSwitcherDrawer-->>ProjectSwitcherDrawer: Render Failed badge and retry action
Loading

Possibly related PRs

  • gnoviawan/termul#449: Added the web/remote project switching and switchProject wiring extended by this failure-state implementation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: inline failed project-switch feedback in chat.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/drawer-project-switcher-5-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/components/chat/ProjectSwitcherDrawer.tsx`:
- Around line 45-47: Update the cleanup effect in ProjectSwitcherDrawer to also
react to failedProjectSwitchId, clearing the stored failure when the drawer is
closed even if the failure arrives after closure. Add a regression test covering
a queued project switch that fails after closing, ensuring no stale failure
badge appears when reopening.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b66cee09-42c8-4a95-abc8-81c4c93e8e2e

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf17a1 and 0209e93.

📒 Files selected for processing (4)
  • src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx
  • src/renderer/components/chat/ProjectSwitcherDrawer.tsx
  • src/renderer/components/mobile/MobileChatShell.test.tsx
  • src/renderer/stores/acp-store.ts

Comment thread src/renderer/components/chat/ProjectSwitcherDrawer.tsx Outdated
gnoviawan added a commit that referenced this pull request Jul 29, 2026
Address the CodeRabbit review on #459: the inline-Failed-badge cleanup
effect now also depends on `failedProjectSwitchId`, so a failure that
arrives AFTER the drawer closes (e.g. a queued switch rejected while
closed) is cleared immediately — no stale red badge resurfaces on
reopen. The guard now only fires when there is an actual failure to
clear (avoids redundant `setFailedProjectSwitch(null)` calls) and
references the dep so `useExhaustiveDependencies` is satisfied.

Adds a regression test: a queued switch that fails after closure leaves
no stale "Failed" badge on reopen.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx (1)

5-12: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the failure-state mock update its observed state.

setFailedProjectSwitch is only a spy, while Line 212 manually sets failedRef.current = null. This means the test can pass even if the cleanup action does not actually clear the store state. Make the mock action update failedRef.current, remove the manual reset, and assert that the ref is null after waitFor.

Also applies to: 189-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx` around lines 5 -
12, The failure-state mock does not update the observed ref, allowing cleanup
tests to pass without clearing store state. Update setFailedProjectSwitch in the
hoisted mock to assign failedRef.current, remove the manual failedRef.current
reset in the affected test, and assert that failedRef.current is null after
waitFor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pr-body-current.md`:
- Line 74: Update the checklist entry to report five new tests, reflecting three
ProjectSwitcherDrawer tests and two MobileChatShell tests instead of four.

---

Outside diff comments:
In `@src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx`:
- Around line 5-12: The failure-state mock does not update the observed ref,
allowing cleanup tests to pass without clearing store state. Update
setFailedProjectSwitch in the hoisted mock to assign failedRef.current, remove
the manual failedRef.current reset in the affected test, and assert that
failedRef.current is null after waitFor.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 91938a64-6f23-4c9a-b8aa-d52859e4cb0c

📥 Commits

Reviewing files that changed from the base of the PR and between 0209e93 and 0545bd4.

📒 Files selected for processing (3)
  • pr-body-current.md
  • src/renderer/components/chat/ProjectSwitcherDrawer.test.tsx
  • src/renderer/components/chat/ProjectSwitcherDrawer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/components/chat/ProjectSwitcherDrawer.tsx

Comment thread pr-body-current.md Outdated
Address the CodeRabbit review on #459: the inline-Failed-badge cleanup
effect now also depends on `failedProjectSwitchId`, so a failure that
arrives AFTER the drawer closes (e.g. a queued switch rejected while
closed) is cleared immediately — no stale red badge resurfaces on
reopen. The guard now only fires when there is an actual failure to
clear (avoids redundant `setFailedProjectSwitch(null)` calls) and
references the dep so `useExhaustiveDependencies` is satisfied.

Adds a regression test: a queued switch that fails after closure leaves
no stale "Failed" badge on reopen.
@gnoviawan
gnoviawan force-pushed the feat/drawer-project-switcher-5-2 branch from 0545bd4 to 788aa1c Compare July 29, 2026 08:41
@gnoviawan

Copy link
Copy Markdown
Owner Author

Addressed the remaining CodeRabbit test-mock finding in e6be5ea: setFailedProjectSwitch now mutates failedRef.current, the manual reset was removed, and the regression test asserts the observed store state is null before reopening. Also merged current dev, which contains the ACP registry refresh and Rust needless_return fix that were failing the prior CI run.

@gnoviawan
gnoviawan merged commit 36b7bc1 into dev Jul 29, 2026
21 of 29 checks passed
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