Skip to content

feat(desktop): clone V1 new-workspace composer onto V2 modal#3302

Merged
Kitenite merged 46 commits intomainfrom
v2-modal-clone-from-v1
Apr 11, 2026
Merged

feat(desktop): clone V1 new-workspace composer onto V2 modal#3302
Kitenite merged 46 commits intomainfrom
v2-modal-clone-from-v1

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 9, 2026

Summary

Replaces the tab-based V2 create-workspace modal with a clone of the battle-tested V1 composer, rewiring only the backend boundaries. This is the "revert + clone + rewire" approach agreed on after the hand-rewritten V2 modal (#3204) kept accumulating subtle regressions flagged by reviewers.

Supersedes #3204 (now POC).

What changes

Backend boundary swaps (V1 electronTrpc → V2 host-service):

  • Project list: electronTrpc.projects.getRecentsv2Projects + githubRepositories collections
  • Branch list: electronTrpc.projects.getBranches*workspaceCreation.searchBranches
  • Create action: 4 V1 mutations (create / createFromPr / openTrackedWorktree / openExternalWorktree) → single workspaceCreation.create with server-side outcome resolution
  • GH issues list/search: electronTrpc.projects.listIssuesworkspaceCreation.searchGitHubIssues (Octokit)
  • GH PRs list/search: electronTrpc.projects.listPullRequests / searchPullRequestsworkspaceCreation.searchPullRequests (Octokit)
  • GH issue content (for attached markdown): electronTrpc.projects.getIssueContentworkspaceCreation.getGitHubIssueContent (Octokit)
  • Navigation: navigateToWorkspacenavigateToV2Workspace

V2 additions:

  • DevicePicker in the composer footer for host target selection
  • `hostTarget` field in the draft; `compareBaseBranch` resets on both project and host change

Retained app-local `electronTrpc` calls (not GH/FS/git, explicit decision):

  • `settings.getAgentPresets` — local agent config
  • `workspaces.generateBranchName` — local AI helper

Intentionally dropped for Phase 1 (deferred to Phase 2):

  • Branch prefix feature (`projects.get`, `getGitAuthor`, `settings.getBranchPrefix`, `settings.getGitInfo`, `resolveBranchPrefix`) — these all read local-machine state and would be wrong for remote hosts. Will return when host-aware prefix lands in Phase 2.
  • Worktree preflight UI (external/tracked worktree badges, filter tab). `workspaceCreation.create` handles tracked/external/adopt semantics server-side.

New host-service endpoints

All under `workspaceCreation.*`:

  • `getContext({ projectId })` — local repo state + default branch
  • `searchBranches({ projectId, query?, limit? })` — git branches with `hasWorkspace` flag
  • `create({ ... })` — full semantic create; returns `outcome: created_workspace | opened_existing_workspace | opened_worktree | adopted_external_worktree`. Includes path-traversal guard on `branchName`, worktree rollback on cloud-create failure, and distinguishes tracked (opens existing cloud row) vs external (adopts into new row) worktrees.
  • `searchGitHubIssues({ projectId, query?, limit? })` — Octokit via `ctx.github()`, repo resolved via `ctx.api.v2Project.get`
  • `searchPullRequests({ projectId, query?, limit? })` — same
  • `getGitHubIssueContent({ projectId, issueNumber })` — same

Why clone + rewire instead of rewrite

The V1 `PromptGroup` has years of accumulated UX polish (draft preservation on failure, pill key handling, keyboard shortcut routing, attachment lifecycle, animation, agent launch request building, etc.) that a reimplementation kept missing. Cloning V1 and surgically swapping the data boundaries is a smaller diff surface and inherits all of that behavior for free.

Test plan

  • `bun run typecheck` — 25/25 packages pass
  • `bun run lint` — clean
  • Open desktop app → new workspace → verify single composer layout
  • DevicePicker in footer switches between local and remote hosts; selected base branch resets on change
  • Project picker lists V2 projects from collections
  • Prompt + Cmd+Enter creates workspace via host-service
  • Workspace name + branch name inputs accept input
  • Attachments, linked Linear issues, linked GitHub issues, linked PRs all work
  • On create failure: modal stays open with draft intact (V1 behavior preserved)

Summary by cubic

Cloned the V1 new‑workspace composer into the V2 modal and wired it to workspaceCreation.* on the host‑service, adding a pending workspace page with live progress, retry, and clickable sidebar entries. Naming now resolves in the renderer; the server only deduplicates and always creates a new worktree/branch.

  • New Features

    • V1 composer in the V2 modal with a device picker; inline link/search for GitHub issues and PRs with command popovers and pills.
    • Single create via workspaceCreation.create: always new branch using git worktree add -b; server dedupes the branch (truncates base for suffix); optional setup script; returns the workspace, warnings, and initialCommands.
    • Pending flow at /_dashboard/pending/$pendingId that polls workspaceCreation.getProgress; auto‑navigate on success; retry re‑runs create using the pending row + IndexedDB attachments; dismiss on failure; elapsed timer shows “now” for <5s, then seconds; staleness detection; attachments cleared on success; sidebar items are clickable and show “failed” when appropriate.
  • Refactors

    • Data boundary moved to V2/host‑service: projects from v2Projects + githubRepositories; branches via workspaceCreation.searchBranches. Renderer computes names (branch = user input via sanitizeUserBranchName > prompt slug via slugifyForBranch > friendly two‑word; workspace = input > prompt > branch). Host only deduplicates.
    • Draft field renamed to baseBranch; pending state sourced from pendingWorkspaces; attachments persisted in IndexedDB; host calls include organizationId; improved error handling with worktree rollback; composer split into UI vs orchestration; extracted resolveNames, mapLinkedContext, useSubmitWorkspace; formatRelativeTime shows seconds; pending page moved outside the V2 workspace layout.

Written for commit 6ad0bb9. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Redesigned “Create Workspace” modal: editable workspace name, attachments, link GitHub issues/PRs, agent selector, optional setup-script, and proactive agent presets.
    • Prompt input syncs with draft resets; single-submit flow creates workspace and navigates automatically.
    • Server-side branch discovery, sanitization, deduplication, and new create endpoints for robust, deterministic workspace creation and GitHub issue/PR lookups.
  • Documentation

    • Added V1/V2 create-workspace design and decision docs.

Replaces the tab-based V2 create-workspace modal with a clone of the
battle-tested V1 composer, rewiring only the backend boundaries.

Backend boundary changes (V1 → V2):
- Project list: electronTrpc.projects.getRecents → v2Projects +
  githubRepositories collections
- Branch list: electronTrpc.projects.getBranches* →
  workspaceCreation.searchBranches on host-service
- Create action: 4 V1 mutations (create/createFromPr/openTracked/
  openExternal) → single workspaceCreation.create on host-service
- GitHub issues/PRs list + content: electronTrpc.projects.{listIssues,
  listPullRequests, searchPullRequests, getIssueContent} →
  workspaceCreation.{searchGitHubIssues, searchPullRequests,
  getGitHubIssueContent} on host-service (Octokit via ctx.github())
- Navigation: navigateToWorkspace → navigateToV2Workspace

V2 additions:
- DevicePicker in the composer footer for host target selection; on
  host change, compareBaseBranch resets
- hostTarget field in the draft context

Intentionally dropped for Phase 1 (deferred to Phase 2):
- Branch prefix feature (projects.get, getGitAuthor, settings.
  getBranchPrefix, settings.getGitInfo, resolveBranchPrefix) — crosses
  V2 host boundary, needs host-aware prefix in Phase 2
- Worktree preflight UI (getExternalWorktrees, getWorktreesByProject,
  resolveOpenableWorktrees, worktree badges/filter tab) — host-service
  workspaceCreation.create handles tracked/external/adopt server-side

Host-service endpoints added:
- workspaceCreation.getContext — project + default branch
- workspaceCreation.searchBranches — git branches with hasWorkspace
- workspaceCreation.create — semantic create with outcome resolution
  (created_workspace / opened_existing_workspace / opened_worktree /
  adopted_external_worktree) and path-traversal guard on branchName
- workspaceCreation.searchGitHubIssues — Octokit issue list/search
- workspaceCreation.searchPullRequests — Octokit PR list/search
- workspaceCreation.getGitHubIssueContent — Octokit issue body fetch
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Reworks the new-workspace modal into a prompt-first flow: centralized draft context, PromptGroup prompt UI (attachments, linked issues/PRs, inline pickers, AI branch naming), a renderer-side createWorkspace hook, and a new host-service workspaceCreation TRPC router; removes legacy tab/list components and related hooks.

Changes

Cohort / File(s) Summary
Draft Context
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx
Replaced tab/list fields with prompt-centric draft fields (workspaceName, workspaceNameEdited, runSetupScript, compareBaseBranch, linkedIssues, linkedPR), added LinkedIssue/LinkedPR types, resetKey, and exposes createWorkspace in the context; simplified updateDraft logic.
Modal Container
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx, .../DashboardNewWorkspaceModalContent/*
Wraps modal in PromptInputProvider, adds PromptInputResetSync, loads agent presets, replaces old form usage with DashboardNewWorkspaceModalContent that manages recent-project preselection and drives the PromptGroup.
Prompt UI & PromptGroup
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/.../PromptGroup/*
Adds PromptGroup component implementing prompt input, attachments conversion, agent selector, device picker, inline branch/compare selection, workspace name input, issue/PR linking, submit orchestration (validation, blob→data conversion), and invocation of createWorkspace.
GitHub Linkers & Pills
.../GitHubIssueLinkCommand/*, .../PRLinkCommand/*, .../LinkedGitHubIssuePill/*, .../LinkedPRPill/*
New popover command palettes to search/select GitHub issues/PRs (debounced, host-aware) and new pill components to display/remove linked GitHub issues/PRs.
Removed Tab/List Components
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/*
Deleted legacy tabbed DashboardNewWorkspaceForm, DashboardNewWorkspaceFormHeader, list/tab content components and their subcomponents — functionality consolidated into PromptGroup + ModalContent.
Removed List Groups & Selectors
.../IssuesGroup/*, .../BranchesGroup/*, .../PullRequestsGroup/*, .../ProjectSelector/*
Removed separate Issues/Branches/PullRequests group components and the standalone ProjectSelector; listing/selection moved inline to PromptGroup and host-service queries.
Hook Changes
.../hooks/useBranchContext/*, .../hooks/useCreateDashboardWorkspace/*, removed useDashboardNewWorkspaceProjectSelection, useResolvedLocalProject
Added useBranchContext (host-side branch metadata). Refactored useCreateDashboardWorkspace to accept richer CreateWorkspaceInput and return a single async function; removed pending/sidebar sync and several legacy selection hooks.
Backend: Host-Service TRPC
packages/host-service/src/trpc/router/router.ts, packages/host-service/src/trpc/router/workspace-creation/*
Registers new workspaceCreationRouter with procedures: getContext, searchBranches, create (clone/worktree logic, rollback, optional setup script), searchGitHubIssues, searchPullRequests, getGitHubIssueContent; adds branch sanitization/dedup.
Sanitization Utility
packages/host-service/src/trpc/router/workspace-creation/utils/sanitize-branch.ts
New branch sanitization and deduplication utilities (sanitizeBranchNameWithMaxLength, deduplicateBranchName) used by workspace creation.
Barrel / Index Updates
various index.ts files under modal tree
Added and removed re-exports to reflect newly added components/hooks and removal of legacy components.
Docs / Plans
apps/desktop/plans/*
Adds V1 scenario analysis and V2 create-decisions docs describing the V2 create behavior, API boundaries, and UX expectations.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as PromptGroup (renderer)
    participant Draft as Draft Context
    participant Host as Host Service (workspaceCreation)
    participant GitHub as GitHub API
    participant Local as Local Git & DB
    participant Cloud as Cloud Workspace Service

    User->>UI: enter prompt, select project/branch/device, attach/link issues/PRs
    UI->>Draft: updateDraft(workspaceName, linkedIssues, linkedPR, compareBaseBranch, runSetupScript)
    User->>UI: submit
    UI->>UI: validate, convert blobs to data URLs
    alt linked GitHub item present
        UI->>Host: request getGitHubIssueContent / search endpoints
        Host->>GitHub: fetch issue/PR data
        GitHub-->>Host: issue/PR content
        Host-->>UI: content
    end
    UI->>Host: workspaceCreation.create({ projectId, names, composer, linkedContext, runSetupScript })
    Host->>Local: ensure local repo (clone if missing), compute defaultBranch
    Host->>Local: check/create worktree (git worktree add or -b from compareBaseBranch)
    Local-->>Host: worktree created / error
    alt worktree created
        Host->>Cloud: create cloud workspace record
        Cloud-->>Host: workspace created
        Host->>Local: insert workspace DB row, optionally run setup script
        Host-->>UI: return created_workspace
    else failure
        Host->>Local: rollback (git worktree remove) best-effort
        Host-->>UI: error
    end
    UI->>UI: revoke blobs, clear pending, navigate to workspace or show error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 I nibble prompts and stitch branch names neat,
I tuck linked issues into a cozy seat,
With blobs turned tidy and worktrees spun,
A workspace springs up with a joyful run. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adopting a V1 composer pattern for the V2 modal by cloning and rewiring the backend.
Description check ✅ Passed Pull request description is detailed and comprehensive, covering the motivation (revert + clone + rewire approach), backend boundary swaps, V2 additions, retained and deferred features, and new host-service endpoints. It includes a test plan (though unchecked).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v2-modal-clone-from-v1

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR replaces the V2 tab-based create-workspace modal with a clone of the battle-tested V1 PromptGroup composer, surgically rewiring only the backend boundaries from electronTrpc to the new workspaceCreation.* host-service router. The new router adds six tRPC procedures covering branch listing, semantic workspace creation (with path-traversal protection and worktree rollback), and GitHub issue/PR search via Octokit.

Key points:

  • The V1 composer UX (draft preservation, pill key handling, keyboard shortcuts, attachment lifecycle, animation) is preserved by cloning rather than rewriting.
  • V2 data sources are wired in: project list from live v2Projects/githubRepositories collections, branches via workspaceCreation.searchBranches, create via workspaceCreation.create, GitHub issues/PRs via Octokit on the host service.
  • DevicePicker is added to the footer for host target selection, and compareBaseBranch resets on both project and host change.
  • Bug: sanitizeText (an HTML-encoding helper) is applied to the GitHub issue body before attaching it as a text/markdown file. Because the body is Markdown, this corrupts > blockquotes, code containing </>, & operators, etc. before they reach the AI agent.
  • Minor security: input.composer.compareBaseBranch is passed directly as a <commit-ish> git argument without a server-side leading-dash guard; a value like --detach would alter the git command silently.
  • updateDraft no longer short-circuits on no-op patches, causing draftVersion to increment on every call and triggering unnecessary downstream re-renders.

Confidence Score: 4/5

Safe to merge after fixing the sanitizeText corruption bug; the git leading-dash guard is a best-practice hardening that can follow quickly.

The clone-and-rewire approach is sound, the path-traversal guard is in place, and V1 UX behaviour is preserved. One clear functional bug (HTML-encoding Markdown content sent to the AI) will corrupt issue context for every linked GitHub issue. The git leading-dash gap is a minor hardening issue. Both are small, targeted fixes; everything else reviewed is correct.

PromptGroup.tsx (sanitizeText on markdown body) and workspace-creation.ts (compareBaseBranch leading-dash guard) need attention before merging.

Vulnerabilities

  • git argument injection (workspace-creation.ts line 419): input.composer.compareBaseBranch is passed as the <commit-ish> positional argument to git worktree add without a leading-dash check. simple-git avoids shell injection (args passed as an array), but git itself interprets leading - as flags (e.g. --detach). A similar gap exists for branchName on line 413.
  • No secrets exposure, no SQL injection risk (Drizzle ORM with parameterised queries), safeResolveWorktreePath correctly blocks path traversal.

Important Files Changed

Filename Overview
packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts New tRPC router with 6 procedures; good path-traversal guard via safeResolveWorktreePath but compareBaseBranch is passed to git without a leading-dash check, risking flag injection.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx 1103-line V1-clone composer with rewired V2 data sources; contains a bug where sanitizeText (HTML encoding) is applied to a Markdown issue body, corrupting blockquotes, code, and arrows before they reach the AI agent.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx Draft context restructured to track linkedIssues/linkedPR instead of tab state; resetKey added for PromptInputProvider sync; no-op guard removed from updateDraft causes spurious re-renders.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts Simplified to a thin callback over workspaceCreation.create; isPending state and ensureWorkspaceInSidebar removed intentionally (navigation handles sidebar update in V2).
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx New content pane resolving V2 project list from v2Projects + githubRepositories live collections and delegating composer to PromptGroup; pre-selection logic looks correct.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx Modal shell gains PromptInputProvider wrapper and PromptInputResetSync helper; onFocusOutside prevention added; overall clean structural change.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts New hook wrapping workspaceCreation.searchBranches via react-query; correct query key including hostUrl ensures cache separation per host.
packages/host-service/src/trpc/router/router.ts Minimal change: registers the new workspaceCreationRouter under the workspaceCreation key.

Sequence Diagram

sequenceDiagram
    participant UI as PromptGroup (renderer)
    participant Draft as DraftContext
    participant HS as host-service (workspaceCreation)
    participant Git as local git
    participant Cloud as Cloud API

    UI->>Draft: updateDraft(prompt/branch/linkedIssue)
    UI->>UI: handleCreate()
    UI->>UI: generateBranchName (electronTrpc — local)
    UI->>HS: workspaceCreation.getGitHubIssueContent (if linked GH issues)
    HS-->>UI: issue markdown
    UI->>Draft: closeAndResetDraft()
    UI->>HS: workspaceCreation.create(projectId, names, composer, linkedContext)
    HS->>Git: worktree add / worktree add -b
    HS->>Cloud: device.ensureV2Host + v2Workspace.create
    Cloud-->>HS: cloudRow
    HS->>HS: insert local workspace row
    HS-->>UI: {outcome, workspace}
    UI->>UI: navigateToV2Workspace(workspace.id)
Loading

Reviews (1): Last reviewed commit: "feat(desktop): clone V1 new-workspace co..." | Re-trigger Greptile

Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts (1)

48-51: Extract hostTargethostUrl resolution into one helper.

This same branch now exists in several new paths (useBranchContext, GitHubIssueLinkCommand, PRLinkCommand, and here). Pulling it into a shared helper will keep future host-routing changes from drifting across create/search flows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts`
around lines 48 - 51, Extract the hostTarget → hostUrl resolution into a shared
helper (e.g., resolveHostUrl) and replace the duplicated logic in
useCreateDashboardWorkspace (inside useCreateDashboardWorkspace.ts where hostUrl
is computed), useBranchContext, GitHubIssueLinkCommand, and PRLinkCommand with
calls to that helper; the helper should accept the hostTarget object and the
activeHostUrl (and access env.RELAY_URL or accept it as a param) and return
either activeHostUrl for kind==="local" or `${RELAY_URL}/hosts/${hostId}` for
remote hosts so all call sites use the single function.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx (3)

600-622: Consider extracting inline sanitization functions.

The sanitizeText and sanitizeUrl functions are defined inside the map callback, which means they're recreated for each issue. For readability and slight performance improvement, consider extracting them outside handleCreate or at least outside the loop.

♻️ Extract sanitization helpers
// At module level or inside PromptGroupInner (outside handleCreate):
const sanitizeText = (str: string) =>
  str.replace(/[&<>"']/g, (char) => {
    const entities: Record<string, string> = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#39;",
    };
    return entities[char] || char;
  });

const sanitizeUrl = (url: string) => {
  try {
    const parsed = new URL(url);
    if (!["http:", "https:"].includes(parsed.protocol)) {
      return "#invalid-url";
    }
    return url;
  } catch {
    return "#invalid-url";
  }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`
around lines 600 - 622, sanitizeText and sanitizeUrl are currently declared
inside the map callback within handleCreate (in PromptGroup / PromptGroupInner),
causing them to be recreated per iteration; move these helpers out of the loop
by extracting them to module scope or to the PromptGroup/PromptGroupInner outer
scope so the map callback can call the single shared functions sanitizeText and
sanitizeUrl instead of redefining them each time, preserving their
implementation and return semantics (including the "#invalid-url" fallback).

446-459: Consider memoizing serialized hostTarget for comparison.

The JSON.stringify(hostTarget) on every render could be optimized, though the impact is minimal given the object's small size. The reset logic correctly clears compareBaseBranch when project or host changes, as specified in PR objectives.

♻️ Optional: Memoize hostTarget serialization
+	const serializedHostTarget = useMemo(() => JSON.stringify(hostTarget), [hostTarget]);
 	const previousProjectIdRef = useRef(projectId);
-	const previousHostRef = useRef(JSON.stringify(hostTarget));
+	const previousHostRef = useRef(serializedHostTarget);
 	useEffect(() => {
-		const nextHost = JSON.stringify(hostTarget);
 		if (
 			previousProjectIdRef.current !== projectId ||
-			previousHostRef.current !== nextHost
+			previousHostRef.current !== serializedHostTarget
 		) {
 			previousProjectIdRef.current = projectId;
-			previousHostRef.current = nextHost;
+			previousHostRef.current = serializedHostTarget;
 			updateDraft({ compareBaseBranch: null });
 		}
-	}, [projectId, hostTarget, updateDraft]);
+	}, [projectId, serializedHostTarget, updateDraft]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`
around lines 446 - 459, The JSON.stringify(hostTarget) is being called on every
render; memoize the serialized hostTarget with useMemo and use that memoized
string for previousHostRef initialization and in the useEffect comparison to
avoid repeated serialization. Update references to use the memoized value (e.g.,
const memoHost = useMemo(() => JSON.stringify(hostTarget), [hostTarget])) and
then use previousHostRef, previousProjectIdRef, the useEffect, and updateDraft
as before but comparing against memoHost instead of calling JSON.stringify
inline.

849-853: Potential slug collision when removing linked issues.

The removeLinkedIssue function filters by slug, but both internal issues (e.g., "SUP-123") and GitHub issues (e.g., "#123") could theoretically have overlapping slug formats in edge cases. While the current slug formats appear distinct, using a unique identifier would be more robust.

♻️ Use composite key or unique ID for removal
 	const removeLinkedIssue = (slug: string) => {
 		updateDraft({
-			linkedIssues: linkedIssues.filter((issue) => issue.slug !== slug),
+			linkedIssues: linkedIssues.filter(
+				(issue) => (issue.url ?? issue.slug) !== (linkedIssues.find(i => i.slug === slug)?.url ?? slug)
+			),
 		});
 	};

Or pass the full issue object / URL to removeLinkedIssue for unambiguous matching.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`
around lines 849 - 853, The removeLinkedIssue function currently removes items
by comparing slug which can collide across internal and GitHub issues; update
removeLinkedIssue to accept an unambiguous identifier (preferably the issue's
unique id or full URL) or the full issue object and filter linkedIssues using
that unique field (e.g., issue.id || issue.url) instead of slug; update callers
to pass the chosen unique identifier/object and ensure updateDraft({
linkedIssues: linkedIssues.filter(i => i.id !== id) }) (or equivalent URL/object
equality) is used for reliable removal.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx (2)

153-186: Context value memoization depends on entire state object.

The useMemo dependency array includes state, which changes on every draft update. This means the context value is recreated on every state change. While this is functionally correct (the value should change when state changes), the manual spreading of individual state properties in lines 155-167 suggests an intent to optimize.

Consider whether the memoization provides value here, or if the explicit property spreading is sufficient.

♻️ Alternative: Depend on specific state properties

If fine-grained memoization is desired:

 	const value = useMemo<DashboardNewWorkspaceDraftContextValue>(
 		() => ({
 			// ... same object construction
 		}),
 		[
 			closeAndResetDraft,
 			createWorkspace,
 			onClose,
 			resetDraft,
 			runAsyncAction,
-			state,
+			state.selectedProjectId,
+			state.hostTarget,
+			state.prompt,
+			state.compareBaseBranch,
+			state.runSetupScript,
+			state.workspaceName,
+			state.workspaceNameEdited,
+			state.branchName,
+			state.branchNameEdited,
+			state.linkedIssues,
+			state.linkedPR,
+			state.draftVersion,
+			state.resetKey,
 			updateDraft,
 		],
 	);

Or simply remove useMemo since the value needs to update on every state change anyway.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx`
around lines 153 - 186, The useMemo block for value currently depends on the
entire state object causing unnecessary recomputations; update the dependency
array of the useMemo (the value created in useMemo) to list the specific state
properties used (e.g., state.selectedProjectId, state.hostTarget, state.prompt,
state.compareBaseBranch, state.runSetupScript, state.workspaceName,
state.workspaceNameEdited, state.branchName, state.branchNameEdited,
state.linkedIssues, state.linkedPR, state.draftVersion, state.resetKey) instead
of state, and keep the other function deps (closeAndResetDraft, createWorkspace,
onClose, resetDraft, runAsyncAction, updateDraft); alternatively remove useMemo
entirely if you want the context to update on every state change.

13-28: Consider stricter typing for LinkedIssue source and LinkedPR state.

LinkedIssue.source is optional but used as a discriminator in PromptGroup.tsx (e.g., issue.source === "github"). Similarly, LinkedPR.state is a plain string rather than a union type.

♻️ Tighten type definitions
 export type LinkedIssue = {
 	slug: string; // "#123" for GitHub, "SUP-123" for internal
 	title: string;
-	source?: "github" | "internal";
+	source: "github" | "internal";
 	url?: string; // GitHub issue URL
 	taskId?: string; // Internal task ID for navigation
 	number?: number; // GitHub issue number
 	state?: "open" | "closed";
 };

 export type LinkedPR = {
 	prNumber: number;
 	title: string;
 	url: string;
-	state: string;
+	state: "open" | "closed" | "merged";
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx`
around lines 13 - 28, The types are too loose: make LinkedIssue.source a
required discriminant and narrow LinkedPR.state to a union of known values;
specifically update the LinkedIssue type (symbol LinkedIssue) to have source:
"github" | "internal" (remove the optional flag) so consumers like
PromptGroup.tsx can safely discriminate, and change LinkedPR (symbol LinkedPR)
state: "open" | "closed" | "merged" (or the exact set your app uses) instead of
string; after changing the types, run TS checks and adjust any call sites that
relied on source being undefined or used other PR state strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx`:
- Around line 102-110: The PopoverContent uses onFocusOutside={(e) =>
e.preventDefault()} which blocks Tab navigation from leaving the popover; remove
that onFocusOutside handler so focus can move out naturally and rely on existing
handlers onPointerDownOutside and onEscapeKeyDown to close the popover. Update
the same pattern in the PRLinkCommand component as well (remove the
preventDefault in its PopoverContent onFocusOutside) so keyboard users can
escape via Tab as expected.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/LinkedGitHubIssuePill/LinkedGitHubIssuePill.tsx`:
- Around line 35-47: LinkedGitHubIssuePill renders a removal Button that's
hidden via opacity/pointer-events on hover-only, but remains focusable via Tab;
update the Button in LinkedGitHubIssuePill to also reveal itself when
keyboard-focused by adding focus-visible (and focus) utility classes so it
becomes visible and interactive on focus (e.g., add
focus-visible:pointer-events-auto focus-visible:opacity-100 and optionally
focus:pointer-events-auto focus:opacity-100 to the existing className), ensuring
the button is both visually exposed and operable when focused via keyboard.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`:
- Around line 676-689: The call to buildLaunchRequest currently discards its
AgentLaunchRequest | null return, so create explicit handling: call
buildLaunchRequest(trimmedPrompt, convertedFiles.length > 0 ? convertedFiles :
undefined) and if it returns null, treat that as a validation failure
(clearPendingWorkspace(pendingWorkspaceId), show a toast.error with a clear
message) or, if null is acceptable, add a comment documenting that null means
"no agent selected" and do not abort; ensure the catch block remains for thrown
errors but also handle the null branch so a workspace isn't created silently
without a valid launch request when one is required.

In
`@packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts`:
- Around line 267-270: Defaulting both branchName and workspaceName to the
static string "workspace" causes collisions; change the fallback logic in
workspace-creation.ts where branchName and workspaceName are assigned so each
missing name gets a unique default (e.g., use a timestamp/UUID: `branchName =
input.names.branchName || 'branch-' + Date.now()` and `workspaceName =
input.names.workspaceName || 'workspace-' + Date.now()` or similar unique
generator), or alternatively document the intentional behavior; ensure the two
fallbacks are generated independently so they don't both resolve to the same
static value and update any related tests or callers that assume the previous
static default.
- Around line 291-306: There is a race when ensuring the local clone at
repoPath: concurrent requests can both observe !existsSync(repoPath) and start
mkdirSync/clone; wrap the clone logic in a lock and a post-check to avoid
duplicate work and partial clones — e.g., acquire a per-project lock (file lock
or in-memory mutex keyed by input.projectId), re-check existsSync(repoPath)
after acquiring the lock, perform clone into a temp directory and atomically
rename to repoPath or use try/catch to delete partial clones on failure, then
release the lock; also add a short comment near the simpleGit() call explaining
why simpleGit() is used instead of ctx.git(localProject.repoPath) because
ctx.git expects an existing repo.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx`:
- Around line 600-622: sanitizeText and sanitizeUrl are currently declared
inside the map callback within handleCreate (in PromptGroup / PromptGroupInner),
causing them to be recreated per iteration; move these helpers out of the loop
by extracting them to module scope or to the PromptGroup/PromptGroupInner outer
scope so the map callback can call the single shared functions sanitizeText and
sanitizeUrl instead of redefining them each time, preserving their
implementation and return semantics (including the "#invalid-url" fallback).
- Around line 446-459: The JSON.stringify(hostTarget) is being called on every
render; memoize the serialized hostTarget with useMemo and use that memoized
string for previousHostRef initialization and in the useEffect comparison to
avoid repeated serialization. Update references to use the memoized value (e.g.,
const memoHost = useMemo(() => JSON.stringify(hostTarget), [hostTarget])) and
then use previousHostRef, previousProjectIdRef, the useEffect, and updateDraft
as before but comparing against memoHost instead of calling JSON.stringify
inline.
- Around line 849-853: The removeLinkedIssue function currently removes items by
comparing slug which can collide across internal and GitHub issues; update
removeLinkedIssue to accept an unambiguous identifier (preferably the issue's
unique id or full URL) or the full issue object and filter linkedIssues using
that unique field (e.g., issue.id || issue.url) instead of slug; update callers
to pass the chosen unique identifier/object and ensure updateDraft({
linkedIssues: linkedIssues.filter(i => i.id !== id) }) (or equivalent URL/object
equality) is used for reliable removal.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx`:
- Around line 153-186: The useMemo block for value currently depends on the
entire state object causing unnecessary recomputations; update the dependency
array of the useMemo (the value created in useMemo) to list the specific state
properties used (e.g., state.selectedProjectId, state.hostTarget, state.prompt,
state.compareBaseBranch, state.runSetupScript, state.workspaceName,
state.workspaceNameEdited, state.branchName, state.branchNameEdited,
state.linkedIssues, state.linkedPR, state.draftVersion, state.resetKey) instead
of state, and keep the other function deps (closeAndResetDraft, createWorkspace,
onClose, resetDraft, runAsyncAction, updateDraft); alternatively remove useMemo
entirely if you want the context to update on every state change.
- Around line 13-28: The types are too loose: make LinkedIssue.source a required
discriminant and narrow LinkedPR.state to a union of known values; specifically
update the LinkedIssue type (symbol LinkedIssue) to have source: "github" |
"internal" (remove the optional flag) so consumers like PromptGroup.tsx can
safely discriminate, and change LinkedPR (symbol LinkedPR) state: "open" |
"closed" | "merged" (or the exact set your app uses) instead of string; after
changing the types, run TS checks and adjust any call sites that relied on
source being undefined or used other PR state strings.

In
`@apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts`:
- Around line 48-51: Extract the hostTarget → hostUrl resolution into a shared
helper (e.g., resolveHostUrl) and replace the duplicated logic in
useCreateDashboardWorkspace (inside useCreateDashboardWorkspace.ts where hostUrl
is computed), useBranchContext, GitHubIssueLinkCommand, and PRLinkCommand with
calls to that helper; the helper should accept the hostTarget object and the
activeHostUrl (and access env.RELAY_URL or accept it as a param) and return
either activeHostUrl for kind==="local" or `${RELAY_URL}/hosts/${hostId}` for
remote hosts so all call sites use the single function.
🪄 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: dbdf8e32-0d38-4629-98a0-39949c0dbc5f

📥 Commits

Reviewing files that changed from the base of the PR and between d1ea876 and 3b2f16f.

📒 Files selected for processing (43)
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/GitHubIssueLinkCommand.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/GitHubIssueLinkCommand/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/LinkedGitHubIssuePill/LinkedGitHubIssuePill.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/LinkedGitHubIssuePill/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/LinkedPRPill/LinkedPRPill.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/LinkedPRPill/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/PullRequestsGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/useDashboardNewWorkspaceProjectSelection.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/useResolvedLocalProject.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts
  • packages/host-service/src/trpc/router/router.ts
  • packages/host-service/src/trpc/router/workspace-creation/index.ts
  • packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts
💤 Files with no reviewable changes (23)
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/useResolvedLocalProject.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/PullRequestsGroup.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/useDashboardNewWorkspaceProjectSelection.ts

Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

8 issues found across 43 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx:677">
P1: The built `AgentLaunchRequest` is unused, so the selected agent/context never gets handed off after workspace creation.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/PromptGroup.tsx:723">
P1: Send the effective base branch here. Right now the UI shows the default branch, but create falls back to `HEAD`, so new workspaces can fork from the wrong branch.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx:44">
P2: This no longer preserves V1's `getRecents` behavior: `recentProjects` is an unordered map of all `v2Projects`, so the picker and fallback selection can default to an arbitrary project instead of a recent one.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/PromptGroup/components/PRLinkCommand/PRLinkCommand.tsx:76">
P2: Parse pasted GitHub PR URLs before calling `searchPullRequests`. The new endpoint searches `in:title`, so sending the raw URL makes pasted PR links stop resolving.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useBranchContext/useBranchContext.ts:36">
P2: `searchBranches` cannot populate branch context on a host that has not cloned the project yet, so the remote-host branch picker comes up empty on first use.</violation>
</file>

<file name="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts">

<violation number="1" location="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts:296">
P2: Handle concurrent clone attempts for the same repo path; the current check-then-clone sequence can race and fail when requests arrive at the same time.</violation>

<violation number="2" location="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts:339">
P1: Verify an existing path is a real git worktree before adopting it as a workspace.</violation>

<violation number="3" location="packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts:369">
P1: Roll back the newly created worktree when host registration fails.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
Comment thread packages/host-service/src/trpc/router/workspace-creation/workspace-creation.ts Outdated
After merging main, v2Project.get and v2Workspace.create were converted
to jwtProcedure and now require organizationId in their input. The
host-service workspace.create endpoint was already updated in main, but
our new workspaceCreation router still had four call sites missing it.
Kitenite added 27 commits April 10, 2026 11:54
New route: /v2-workspace/pending/$pendingId

- Reads pending workspace from pendingWorkspaces collection via useLiveQuery
- Polls workspaceCreation.getProgress every 500ms for step-by-step progress
- Shows: workspace name, branch name, step checklist (ensuring_repo → creating_worktree → registering)
- On succeeded: auto-navigates to /v2-workspace/$workspaceId, cleans up pending row
- On failed: shows error message with Retry + Dismiss buttons
- Retry resets status to creating (full re-fire is a follow-up TODO)
- Dismiss deletes pending row + clears IndexedDB attachments
- Replace single zustand pendingWorkspace with useLiveQuery on
  pendingWorkspaces collection (supports multiple concurrent creates)
- Pending sidebar items are now clickable — navigate to
  /v2-workspace/pending/$pendingId to view progress
- Add "failed" status to creationStatus type across sidebar components
  (types, icon, collapsed button, status text helper)
- Succeeded pending workspaces are filtered out (replaced by real
  workspace from Electric sync)
PromptGroup.tsx: 890 → 441 lines (UI render only)

Extracted:
- components/AttachmentButtons/ (74 lines)
- components/ProjectPickerPill/ (79 lines)
- components/CompareBaseBranchPicker/ (134 lines)
- hooks/useHandleCreate/ (181 lines) — full create orchestration
- types.ts (15 lines) — shared types + constants

Existing sub-components unchanged:
- GitHubIssueLinkCommand, LinkedGitHubIssuePill, LinkedPRPill, PRLinkCommand
- Failed: red triangle icon + red "Failed" text
- Creating: keeps spinner + gray "Creating..." text
Host-service getProgress now returns fully resolved steps with id,
label, and status (pending/active/done). The renderer maps over
the array directly — no STEP_LABELS, no STEP_ORDER, no string
matching. If the host-service adds/changes steps, the UI updates
automatically.
Was returning "now" for everything under 60s. Now returns "now" for
<5s, then "5s", "30s", etc. Pending page consumes it for the elapsed
timer instead of a custom formatter.
- Delete dev-seed-pending-workspace.ts and DevSeedPendingWorkspace.tsx
- Remove stash restore effect from DashboardNewWorkspaceModalContent
  (draft recovery now handled by pending page retry, not modal reopen)
- Remove DevSeedPendingWorkspace from layout
- Leave zustand store untouched (V1 still uses it)
The pending page was under /v2-workspace/pending/$pendingId which
caused the v2-workspace layout to render bare <Outlet /> during
route transitions. This removed the WorkspaceTrpcProvider from the
tree while TerminalPane was still mounted → crash.

Moved to /_dashboard/pending/$pendingId — completely outside the
v2-workspace layout. The layout reverts to main's version unchanged.
…ayout bug

- Dismiss button always visible on pending page (not just when stale)
- Added TODO comment on v2-workspace layout explaining the bare
  Outlet bug that strips WorkspaceTrpcProvider during transitions
- Removed instrumentation console.logs
Create flow uses `git worktree add -b` directly. No try/catch
fallback to checkout. If branch exists despite dedup, the git
command errors and the create fails cleanly.

Checking out existing branches is a separate intent (createFromPr).
Updated decisions doc (decision #8).
Replace workspace-${uuid} with friendly-words pattern (e.g.
"cheerful-umbrella"). Generate once, use for both branch name and
workspace name when user typed neither. New shared util at
shared/utils/friendly-branch-name.ts.
- Extract useRetryCreate hook that re-fires createWorkspace with
  draft data from the pending collection row + attachments from IndexedDB
- Retry button calls the hook instead of inline logic
- Move elapsed timer to right-aligned end of status line
V1 uses "compareBaseBranch" because it serves double duty as the
git fork point AND the diff comparison base. For V2's create flow
it's just the fork point — "baseBranch" is clearer. The workspace
view can derive its own compare base independently.

Renamed in: draft context, PromptGroup, useHandleCreate,
useCreateDashboardWorkspace, host-service create input schema,
pendingWorkspace collection schema, pending page retry.
V1 code untouched.
…UserBranchName

Two clearly different operations:
- slugifyForBranch: turns arbitrary text (prompts) into a branch slug.
  Lowercases, strips special chars, collapses spaces to dashes.
- sanitizeUserBranchName: strips only what git forbids from a
  user-typed branch name. Preserves case, slashes, underscores.

Host-service no longer sanitizes — it only validates (non-empty) and
deduplicates. The renderer owns all sanitization/slugification.
Removed sanitizeBranchNameWithMaxLength from host-service utils.
…unctions

- resolveNames(draft) — pure. Computes branch + workspace names from
  draft state. User-typed → sanitizeUserBranchName, prompt → slugifyForBranch,
  empty → friendlyBranchName.
- mapLinkedContext(draft) — pure. Maps linked issues/PR to API payload shape.
- useSubmitWorkspace(projectId) — the hook. Orchestrates resolve → store →
  insert → close → navigate → fire create. Calls the pure functions.

Dependency array simplified: just draft object instead of 12 individual fields.
Truncates base name upfront to 94 chars (reserving 6 for suffix
like -99999) before appending dedup suffixes. Strips trailing .-
after truncation. Last-resort fallback uses base36 timestamp
instead of decimal for shorter output.
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