feat(desktop): wire Link task command to v2 workspaces#4493
Conversation
Closes SUPER-685. The command palette Link task picker now actually links the chosen task to the current v2 workspace, optimistically through tanstack-db and persisted via the v2Workspace.update trpc mutation (which now accepts taskId). The hover card surfaces the linked task with a StatusIcon and links to its detail page. Picker UI uses the hybrid search hook and useDeferredValue, matched to the tasks list view; the same deferred pattern is applied to the tasks view search so heavy re-renders yield to keystrokes.
|
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
📝 WalkthroughWalkthroughThis PR implements workspace task linking by adding a ChangesWorkspace task linking
Sequence DiagramsequenceDiagram
participant User
participant LinkTaskFrame
participant useDeferredValue
participant useHybridSearch
participant v2Workspaces
participant TRPC
participant UI as Hover Card
User->>LinkTaskFrame: Type search query
LinkTaskFrame->>useDeferredValue: Create deferredQuery
useDeferredValue->>useHybridSearch: Pass deferred query
useHybridSearch->>LinkTaskFrame: Return filtered results
LinkTaskFrame->>User: Display status icons & task details
User->>LinkTaskFrame: Select task
LinkTaskFrame->>v2Workspaces: updateWorkspace(workspaceId, {taskId})
v2Workspaces->>TRPC: Call router.update with taskId
TRPC->>TRPC: Validate task exists & belongs to org
TRPC->>v2Workspaces: Return updated workspace
v2Workspaces->>UI: Workspace data includes taskId
UI->>LinkedTaskSection: Render with taskId
LinkedTaskSection->>User: Display linked task info
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR wires the Link Task command palette picker to v2 workspaces end-to-end: selecting a task optimistically sets
Confidence Score: 4/5Safe to merge — the link/unlink data flow is correct end-to-end and the server-side org validation mirrors the create path. The implementation is solid: null correctly bypasses org-validation on the server, Drizzle treats undefined fields as no-ops, and taskId is projected through all four workspace-building paths. The only items worth a follow-up are a slightly noisy useMemo dependency (statusMap included in both search and sort branches when only needed for sort) and the LinkedTaskSection query depending on the full collections object rather than the two specific sub-collections it actually reads. LinkedTaskSection.tsx (dependency array) and LinkTaskFrame.tsx (filtered memo deps) are the two spots worth a quick look; all other files are straightforward.
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsx | Rewrites the picker UI: replaces Fuse with useHybridSearch, adds status-aware sorting, wires updateWorkspace for real linking, and defers the search query for responsiveness |
| packages/trpc/src/router/v2-workspace/v2-workspace.ts | Adds taskId (uuid, nullable, optional) to the update mutation with correct org-scoped validation; null correctly bypasses the validation check for the unlink path |
| apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts | Extends V2WorkspacePatch with taskId and patches the optimistic draft correctly inside updateWorkspace |
| apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts | Extends the onUpdate handler to destructure and forward taskId to the TRPC mutation; correctly handles null (unlink) and undefined (not changed) |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsx | New component rendering a linked task row with status icon, slug, truncated title, in-app link, and optional external-link icon; uses [collections, taskId] as query dependency instead of specific sub-collections |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts | Projects taskId through all four workspace-building code paths; pending workspaces correctly default to taskId: null |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts | Adds taskId: string |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsx | Adds useDeferredValue to the search query and threads deferredSearchQuery to all four content components for better input responsiveness |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx | Destructures taskId from workspace and conditionally renders LinkedTaskSection; straightforward integration |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/index.ts | Barrel export for LinkedTaskSection |
Sequence Diagram
sequenceDiagram
participant User
participant LinkTaskFrame
participant OptimisticActions
participant LocalCollection as Local Collection
participant onUpdate as collections.ts onUpdate
participant TRPC as TRPC v2Workspace.update
User->>LinkTaskFrame: selects a task
LinkTaskFrame->>OptimisticActions: "v2Workspaces.updateWorkspace(workspaceId, { taskId })"
OptimisticActions->>LocalCollection: "immer draft → draft.taskId = taskId"
LocalCollection-->>LinkTaskFrame: optimistic update applied
LinkTaskFrame->>User: toast.success("Linked … to workspace") + close palette
LocalCollection->>onUpdate: "fires async with { original, changes: { taskId } }"
onUpdate->>TRPC: "v2Workspace.update({ id, taskId })"
TRPC->>TRPC: validate taskId belongs to org
alt success
TRPC-->>onUpdate: "{ txid }"
onUpdate-->>LocalCollection: confirmed
else failure
TRPC-->>onUpdate: TRPCError
onUpdate-->>LocalCollection: rollback optimistic change
LocalCollection-->>User: toast.error("Failed to update workspace")
end
Note over User,LocalCollection: Sidebar hover card reads taskId from DashboardSidebarWorkspace and renders LinkedTaskSection while taskId is set
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsx:126
`statusMap` is included in the dependency array but is only consulted in the no-query branch of the sort. When `deferredQuery` is truthy the memo takes the `search(deferredQuery)` branch, which doesn't touch `statusMap`, so any status data update will needlessly re-derive `filtered` and re-render the list mid-keystroke — undercutting the `useDeferredValue` intent.
```suggestion
}, [deferredQuery, search, tasks, statusMap]); // statusMap is only used in the sort path (no query); consider splitting into two memos if status churn causes visible re-renders
```
### Issue 2 of 2
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsx:35
**Overly broad query dependency**
The live query only reads `collections.tasks` and `collections.taskStatuses`, but `collections` (the whole object) is listed as the dependency. `LinkTaskFrame` uses the more specific pattern — `[collections.tasks]` and `[collections.taskStatuses]` — for exactly this reason. If the `collections` reference is not perfectly stable, the join will re-run whenever any collection changes (PRs, hosts, members, …), not just on task or status changes. Consider `[collections.tasks, collections.taskStatuses, taskId]` to stay consistent with the rest of the file.
Reviews (1): Last reviewed commit: "feat(desktop): wire Link task command to..." | Re-trigger Greptile
| return search(deferredQuery) | ||
| .slice(0, MAX_RESULTS) | ||
| .map((r) => r.item); | ||
| }, [deferredQuery, search, tasks, statusMap]); |
There was a problem hiding this comment.
statusMap is included in the dependency array but is only consulted in the no-query branch of the sort. When deferredQuery is truthy the memo takes the search(deferredQuery) branch, which doesn't touch statusMap, so any status data update will needlessly re-derive filtered and re-render the list mid-keystroke — undercutting the useDeferredValue intent.
| }, [deferredQuery, search, tasks, statusMap]); | |
| }, [deferredQuery, search, tasks, statusMap]); // statusMap is only used in the sort path (no query); consider splitting into two memos if status churn causes visible re-renders |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsx
Line: 126
Comment:
`statusMap` is included in the dependency array but is only consulted in the no-query branch of the sort. When `deferredQuery` is truthy the memo takes the `search(deferredQuery)` branch, which doesn't touch `statusMap`, so any status data update will needlessly re-derive `filtered` and re-render the list mid-keystroke — undercutting the `useDeferredValue` intent.
```suggestion
}, [deferredQuery, search, tasks, statusMap]); // statusMap is only used in the sort path (no query); consider splitting into two memos if status churn causes visible re-renders
```
How can I resolve this? If you propose a fix, please make it concise.| statusColor: s?.color ?? null, | ||
| statusProgress: s?.progressPercent ?? null, | ||
| })), | ||
| [collections, taskId], |
There was a problem hiding this comment.
The live query only reads collections.tasks and collections.taskStatuses, but collections (the whole object) is listed as the dependency. LinkTaskFrame uses the more specific pattern — [collections.tasks] and [collections.taskStatuses] — for exactly this reason. If the collections reference is not perfectly stable, the join will re-run whenever any collection changes (PRs, hosts, members, …), not just on task or status changes. Consider [collections.tasks, collections.taskStatuses, taskId] to stay consistent with the rest of the file.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsx
Line: 35
Comment:
**Overly broad query dependency**
The live query only reads `collections.tasks` and `collections.taskStatuses`, but `collections` (the whole object) is listed as the dependency. `LinkTaskFrame` uses the more specific pattern — `[collections.tasks]` and `[collections.taskStatuses]` — for exactly this reason. If the `collections` reference is not perfectly stable, the join will re-run whenever any collection changes (PRs, hosts, members, …), not just on task or status changes. Consider `[collections.tasks, collections.taskStatuses, taskId]` to stay consistent with the rest of the file.
How can I resolve this? If you propose a fix, please make it concise.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsx`:
- Around line 128-131: The handler handleSelect currently calls
v2Workspaces.updateWorkspace and immediately shows toast.success and calls
setOpen(false) without awaiting or handling errors; change it to await the
updateWorkspace call (or handle its returned promise), wrap it in try/catch,
only call toast.success and setOpen(false) on success, and on failure call
toast.error with the error message (or a friendly message) and do not close the
frame; apply the same pattern to the other similar handler referenced (the one
at the other occurrence around line 147) so both updateWorkspace usages handle
async failures properly.
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsx`:
- Around line 70-79: The external icon-only link in the LinkedTaskSection
component lacks an accessible name; update the anchor element (the <a> wrapping
the LuExternalLink icon in LinkedTaskSection) to include an explicit aria-label
(for example "Open task externally" or include the task title like `Open
{task.title} externally`) so screen readers can announce the purpose; keep the
existing title/onClick/rel attributes and ensure the aria-label value is
meaningful and localized if needed.
🪄 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
Run ID: b27d7e0f-006a-46fb-8fbc-4530c89e49ab
📒 Files selected for processing (10)
apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/TasksView.tsxapps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.tsapps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.tspackages/trpc/src/router/v2-workspace/v2-workspace.ts
| const handleSelect = (taskId: string, slug: string) => { | ||
| v2Workspaces.updateWorkspace(workspaceId, { taskId }); | ||
| toast.success(`Linked ${slug} to workspace`); | ||
| void linkTaskToWorkspace(taskId, workspaceId); | ||
| setOpen(false); |
There was a problem hiding this comment.
Handle mutation failure before showing success and closing the frame.
updateWorkspace is a remote write path, but the UI always shows success and closes immediately. If the mutation fails (e.g., org/task validation), users get a false success state.
Proposed fix
- const handleSelect = (taskId: string, slug: string) => {
- v2Workspaces.updateWorkspace(workspaceId, { taskId });
- toast.success(`Linked ${slug} to workspace`);
- setOpen(false);
- };
+ const handleSelect = async (taskId: string, slug: string) => {
+ try {
+ await v2Workspaces.updateWorkspace(workspaceId, { taskId });
+ toast.success(`Linked ${slug} to workspace`);
+ setOpen(false);
+ } catch {
+ toast.error(`Failed to link ${slug} to workspace`);
+ }
+ };
...
- onSelect={() => handleSelect(task.id, task.slug)}
+ onSelect={() => void handleSelect(task.id, task.slug)}Also applies to: 147-147
🤖 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 `@apps/desktop/src/renderer/commandPalette/ui/LinkTask/LinkTaskFrame.tsx`
around lines 128 - 131, The handler handleSelect currently calls
v2Workspaces.updateWorkspace and immediately shows toast.success and calls
setOpen(false) without awaiting or handling errors; change it to await the
updateWorkspace call (or handle its returned promise), wrap it in try/catch,
only call toast.success and setOpen(false) on success, and on failure call
toast.error with the error message (or a friendly message) and do not close the
frame; apply the same pattern to the other similar handler referenced (the one
at the other occurrence around line 147) so both updateWorkspace usages handle
async failures properly.
| <a | ||
| href={task.externalUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="shrink-0 text-muted-foreground hover:text-foreground" | ||
| title="Open task externally" | ||
| onClick={(e) => e.stopPropagation()} | ||
| > | ||
| <LuExternalLink className="size-3" /> | ||
| </a> |
There was a problem hiding this comment.
Add an explicit accessible name to the external icon link.
Use aria-label on this icon-only <a> so screen readers have a reliable name.
Suggested patch
{task.externalUrl && (
<a
href={task.externalUrl}
target="_blank"
rel="noopener noreferrer"
+ aria-label="Open task externally"
className="shrink-0 text-muted-foreground hover:text-foreground"
title="Open task externally"
onClick={(e) => e.stopPropagation()}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <a | |
| href={task.externalUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="shrink-0 text-muted-foreground hover:text-foreground" | |
| title="Open task externally" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <LuExternalLink className="size-3" /> | |
| </a> | |
| <a | |
| href={task.externalUrl} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| aria-label="Open task externally" | |
| className="shrink-0 text-muted-foreground hover:text-foreground" | |
| title="Open task externally" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <LuExternalLink className="size-3" /> | |
| </a> |
🤖 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
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/components/LinkedTaskSection/LinkedTaskSection.tsx`
around lines 70 - 79, The external icon-only link in the LinkedTaskSection
component lacks an accessible name; update the anchor element (the <a> wrapping
the LuExternalLink icon in LinkedTaskSection) to include an explicit aria-label
(for example "Open task externally" or include the task title like `Open
{task.title} externally`) so screen readers can announce the purpose; keep the
existing title/onClick/rel attributes and ensure the aria-label value is
meaningful and localized if needed.
Summary
Closes SUPER-685.
taskIdto the current v2 workspace viauseOptimisticCollectionActions().v2Workspaces.updateWorkspace; thev2WorkspacePatchand the collection'sonUpdatehandler both carrytaskIdend-to-end.v2Workspace.updatetrpc acceptstaskId(uuid, nullable, optional) and validates it belongs to the workspace's org (mirroringcreate).nullis the unlink path.LinkedTaskSectionrenders a "Task" row withStatusIcon, slug, truncated title, an in-app<Link to="/tasks/$taskId">, and an external-link icon whenexternalUrlis set.taskIdis projected through all three sidebar workspace query/build sites inuseDashboardSidebarData.IssueLinkCommand.StatusIcon+ two-line title/slug+status layout. TheShow closedcheckbox was dropped in favor of sorting in Linear list-view order (started → unstarted → backlog → completed → canceled, thenstatus.position, then priority) — closed tasks naturally fall to the bottom and remain searchable.useHybridSearchhook (exact on slug/labels, fuzzy on title/description) instead of a single permissive Fuse. Both the picker and the mainTasksViewuseuseDeferredValueon the search query so heavy re-renders yield to keystrokes.Test plan
Link task→ pick a task → workspace getstaskId, sidebar hover card shows it./tasks/$taskId; external-link icon opens the Linear URL when present.bun run typecheckandbun run lintpass.Summary by cubic
Links tasks to v2 workspaces from the command palette and shows the linked task in the sidebar hover card. Closes SUPER-685 and aligns the picker UI and search with the tasks list.
New Features
taskIdto the current v2 workspace viauseOptimisticCollectionActions().v2Workspaces.updateWorkspace; passingnullunlinks.v2Workspace.updatenow acceptstaskId(uuid, nullable, optional) and validates it belongs to the workspace’s org.StatusIcon, slug, truncated title, an in-app link to/tasks/$taskId, and an external link whenexternalUrlexists. Picker UI matchesIssueLinkCommand.Performance
useHybridSearchand tasks list sort order (started → unstarted → backlog → completed → canceled, then status.position, then priority); closed tasks naturally sink but stay searchable.TasksViewuseuseDeferredValuefor smoother typing under heavy re-renders.Written for commit cf223da. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
New Features
Improvements