9.0 KiB
9.0 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/tasks/requirements/link-existing-task-github-issue.md | 2026-07-31 | implemented |
Implementation Plan: Unlink A Task GitHub Pull Request
Overview
Add a workspace-authorized unlink contract for one persisted GitHub task-PR association, broadcast the removal to connected clients, and expose it through the existing multi-PR tab strip. The backend lands first so frontend state can consume a stable API and WebSocket contract; desktop and mobile behavior then share one mutation path and receive focused Playwright coverage.
Backend
Durable association detachment
- Update
apps/backend/internal/github/store.goandapps/backend/internal/github/models.gosogithub_task_prsgains an idempotently migrated nullabledetached_attimestamp on SQLite and Postgres-compatible startup paths. - Keep detached rows as durable tombstones so a poller or later session does
not silently rediscover an explicitly removed PR. Active association reads,
task search, review sources, and automation list paths must filter
detached_at IS NULL. - Add an exact association lookup by ID and a workspace-scoped detach mutation.
The mutation stamps only the requested row. Explicit URL linking of the same
(task_id, repository_id, pr_number)clears the tombstone; automatic watch discovery does not. - Preserve task deletion cleanup and existing task/repository/PR uniqueness. No GitHub API mutation occurs during detach.
Service and HTTP contract
- Add a service entry point in
apps/backend/internal/github/service_pr_watch.go(or a focused sibling file) that resolves the association, authorizes its stored workspace, and detaches it without leaking whether a cross-workspace ID exists. - Register
DELETE /api/v1/github/task-prs/:associationId?workspace_id=<workspaceId>inapps/backend/internal/github/controller.go. Return{ "deleted": true }on success and404for unknown or workspace-mismatched associations. - Publish a typed removal payload containing
workspace_id,task_id, andassociation_idonly after the detach transaction commits.
Live update contract
- Add
github.task_pr.deletedtoapps/backend/internal/events/types.goandapps/backend/pkg/websocket/actions.go. - Subscribe it in
apps/backend/internal/gateway/websocket/task_notifications.go; the payload's workspace ID keeps routing scoped to clients for the owning workspace.
Frontend
API, store, and live updates
- Add
deleteTaskPR(associationId, workspaceId)toapps/web/lib/api/domains/github-api.ts. - Extend the GitHub Zustand slice in
apps/web/lib/state/slices/github/{types.ts,github-slice.ts}withremoveTaskPR(taskId, associationId), removing only the selected row and deleting an empty task bucket. - Type
github.task_pr.deletedinapps/web/lib/types/backend.tsand handle it inapps/web/lib/ws/handlers/github.tsso other tabs and windows converge. - Expose a shared unlink mutation from
apps/web/hooks/domains/github/use-task-pr.ts. It waits for the HTTP success before removing local state and reports failure without hiding the PR.
Multi-PR tab close action
- Refactor
apps/web/components/github/multi-pr-ci-popover.tsxso the tab label and close action are sibling semantic buttons rather than nested buttons. Render the close action only while the task has multiple PRs. - Give each close action an association-specific accessible name, stop its pointer/click activation from selecting or opening the PR, disable it while its request is pending, and retain the tab with an error toast on failure.
- After a successful removal, select/focus a deterministic adjacent remaining tab when the multi-PR surface remains mounted. When two PRs become one, the existing parent surface collapses naturally to its single-PR variant and restores focus to that surviving trigger.
- Wire the same mutation into both
apps/web/components/github/pr-topbar-button.tsxandapps/web/components/github/pr-status-chip.tsx.
Mobile design contract
- Desktop outcome: hovering the topbar's multi-PR control shows the existing tabbed CI popover; each tab has a compact close action.
- Mobile entry point: tap the existing PR status chip to open
PRStatusChipMultiDrawer; no new route or overlay is introduced. - Nearest shipped exemplar: the current
PRStatusChipMultiDrawer+MultiPRCIPopovercomposition supplies the inset bottom drawer, fixed header, tab hierarchy, and single internally scrolling body. - Hierarchy and action: the selected PR remains the focal content; unlink is a secondary action attached to each PR tab. Mobile close targets use an actual minimum 44px hitbox and an association-specific accessible label.
- Presentation rationale: unlink is a short contextual action inside an already-open status surface, so the existing drawer is more direct than a second confirmation drawer or route.
- Shared behavior: API mutation, pending state, error handling, selected-PR fallback, and store removal are shared; only responsive geometry differs.
- Viewport behavior: retain the drawer's
min-h-0internal scroll owner, safe-area behavior, focus return, and zero document-level horizontal overflow.
Tests
- What: detaching one association is workspace-scoped, persists across a
store reopen/replayed migration, active lists omit it, siblings remain, and
explicit relinking restores it.
File: focused tests beside
apps/backend/internal/github/store.goandservice_pr_watch.go. How: table-driven store/service tests using the real test database and a workspace authorizer. - What: the HTTP endpoint returns success for the owning workspace and
fails closed for unknown/cross-workspace IDs without mutating rows.
File:
apps/backend/internal/github/controller_test.go. How: Gin handler integration tests against the real GitHub store/service. - What: a committed detach broadcasts the typed workspace-scoped removal
notification.
File:
apps/backend/internal/gateway/websocket/task_notifications_test.go. How: event-bus notification test. - What: API URL/method, exact store removal, WebSocket removal, pending
close state, selected fallback, focus behavior, and failure retention.
Files:
apps/web/lib/api/domains/github-api.test.ts,apps/web/lib/state/slices/github/github-slice.test.ts,apps/web/lib/ws/handlers/github.test.ts, and the focusedapps/web/components/github/pr-ci-popover.automation.test.tsx. How: Vitest unit/component tests with mocked API and store state.
E2E Tests
- Scenario: GIVEN a desktop task with two linked PRs, WHEN the user hovers
the topbar PR control and closes the older PR tab, THEN only the newer PR
remains before and after reload.
File:
apps/web/e2e/tests/pr/pr-multi-popover.spec.ts. What to verify: close action is reachable, no sibling tab is removed, the topbar changes to the single-PR state, and persistence survives reload. - Scenario: GIVEN the same task on a coarse-pointer phone, WHEN the user
opens the PR status drawer and taps one tab's unlink target, THEN the same
association is removed with a touch-sized control and no horizontal page
overflow.
File:
apps/web/e2e/tests/pr/mobile-pr-ci-chip.spec.ts. What to verify: drawer entry, accessible close action, minimum hitbox, resulting single-PR chip, reload persistence, and document containment.
Public Documentation
- Update
docs/public/sessions-and-review.mdbeside the existing multi-PR selector guidance. Explain where unlink is available and that it removes the Kandev association only, leaving the remote PR and branch untouched.
Implementation Waves And Parallel Candidates
The default execution order is sequential in the primary conversation.
- Task 01: Backend unlink contract
- Task 02: Frontend multi-PR unlink UI
- Task 03: Desktop and mobile E2E coverage
- Task 04: Public documentation
Tasks 01 and 04 are parallel-safe because they own disjoint implementation and documentation files, but parallel execution still requires explicit user authorization. Tasks 02 and 03 are sequential after the backend contract and UI respectively.
Risks
- Filtering detached rows must cover every task-PR consumer; a missed query could leave an old PR visible in search, review, or automation even when the topbar is correct.
- Automatic PR-watch discovery and explicit manual relinking need different tombstone behavior so background sync cannot resurrect a user removal while deliberate relinking still works.
- A close button nested inside the current tab button would produce invalid interactive markup and broken keyboard behavior; the tab composition must be split without regressing roving tabindex.
- Removing the selected tab can unmount the multi-PR surface when only one PR remains, so focus management must tolerate both the retained and collapsed cases.