Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
3b2f16f
feat(desktop): clone V1 new-workspace composer onto V2 modal
Kitenite Apr 9, 2026
be044fe
Merge remote-tracking branch 'origin' into v2-modal-clone-from-v1
Kitenite Apr 10, 2026
f427c40
fix: pass organizationId to cloud API calls in workspaceCreation router
Kitenite Apr 10, 2026
e72bc92
Merge remote-tracking branch 'origin' into v2-modal-clone-from-v1
Kitenite Apr 10, 2026
79ad35e
Merge remote-tracking branch 'origin' into v2-modal-clone-from-v1
Kitenite Apr 10, 2026
a41b12f
chore: add instrumentation to workspace creation flow
Kitenite Apr 10, 2026
334b9fc
Update docs
Kitenite Apr 10, 2026
e9e0200
docs: clean up plan files, keep only final decisions + scenario analysis
Kitenite Apr 10, 2026
1e924fc
feat: implement v2 create decisions — always create, never collide
Kitenite Apr 10, 2026
35c28cd
feat: draft stash for failure recovery
Kitenite Apr 10, 2026
b412ec5
chore: add logging + error handling to cloud API calls in create
Kitenite Apr 10, 2026
d9bca55
Merge remote-tracking branch 'origin' into v2-modal-clone-from-v1
Kitenite Apr 10, 2026
0365275
docs: finalize v2 workspace creation status + pending workspace design
Kitenite Apr 10, 2026
4e9ed30
docs: switch from EventBus to polling for create progress
Kitenite Apr 10, 2026
531aa39
docs: add TODO for cleaning up stale createProgress map entries
Kitenite Apr 10, 2026
303477b
chore: add TODO to migrate chat pane uploads to IndexedDB blob pattern
Kitenite Apr 10, 2026
665b49a
chore: move IndexedDB migration TODO to ChatLaunchConfig where base64…
Kitenite Apr 10, 2026
534c043
feat: add create progress infrastructure
Kitenite Apr 10, 2026
2da8212
feat: rewrite PromptGroup submit to use pendingWorkspaces collection …
Kitenite Apr 10, 2026
b41eb2b
feat: add pending workspace page with live progress polling
Kitenite Apr 10, 2026
42aeb35
feat: sidebar renders pending workspaces from collection, clickable
Kitenite Apr 10, 2026
aed9c3d
refactor: split PromptGroup into focused files
Kitenite Apr 10, 2026
9857aa8
fix: sidebar pending workspace visual states
Kitenite Apr 10, 2026
d1bc3e7
fix: center pending workspace page layout
Kitenite Apr 10, 2026
f5f6c21
fix: pending page centered on page, left-aligned text
Kitenite Apr 10, 2026
a4e915a
fix: pending page takes full width of content area
Kitenite Apr 10, 2026
f75ffa9
refactor: host-service defines step labels, renderer just renders
Kitenite Apr 10, 2026
7d3dce6
fix: pending page top-aligned with padding, update dev seed scripts
Kitenite Apr 10, 2026
5021f86
feat: elapsed timer + staleness detection on pending page
Kitenite Apr 10, 2026
6e6002f
fix: coerce createdAt to timestamp regardless of string/Date type
Kitenite Apr 10, 2026
f0e003c
fix: timer inline before status text
Kitenite Apr 10, 2026
c982fb0
fix: formatRelativeTime shows seconds under 1 minute
Kitenite Apr 10, 2026
72382a1
chore: remove dev seed files and dead stash restore code
Kitenite Apr 10, 2026
02b1132
Merge remote-tracking branch 'origin' into v2-modal-clone-from-v1
Kitenite Apr 10, 2026
a044ef0
fix: move pending page out of v2-workspace layout
Kitenite Apr 10, 2026
96c4f3f
docs: add comment explaining why pending page lives outside v2-workspace
Kitenite Apr 10, 2026
d58c880
chore: remove instrumentation console.logs from workspaceCreation.create
Kitenite Apr 10, 2026
3b10a95
fix: always show dismiss on creating page, add TODO on v2-workspace l…
Kitenite Apr 10, 2026
233443a
fix: always create new branch, never try checkout existing
Kitenite Apr 10, 2026
9f4aa66
feat: use friendly two-word names for fallback branch/workspace names
Kitenite Apr 10, 2026
55aaffb
feat: wire up retry from pending page, move timer to end
Kitenite Apr 10, 2026
5874dc5
refactor: rename compareBaseBranch to baseBranch in V2 create flow
Kitenite Apr 10, 2026
dd76439
refactor: split branch name handling into slugifyForBranch + sanitize…
Kitenite Apr 10, 2026
e319c08
refactor: rename useHandleCreate → useSubmitWorkspace, extract pure f…
Kitenite Apr 10, 2026
78a14bf
fix: dedup suffix can no longer exceed max branch length
Kitenite Apr 11, 2026
6ad0bb9
lint
Kitenite Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions apps/desktop/plans/v1-create-scenario-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# V1 Workspace Creation — Scenario Analysis

Walks through every user scenario in the V1 create flow. Identifies what works, what's wrong, and what V2 should do differently.

---

## Scenario 1: Prompt only (most common)

**User action:** Types "fix the login bug", hits Cmd+Enter. No workspace name, no branch name.

**Renderer** (`PromptGroup.tsx:740-806`):
1. `displayName = "fix the login bug"` (from `trimmedPrompt`)
2. `willGenerateAIName = true` (no branchNameEdited, has prompt, no PR)
3. Shows pending workspace with "generating-branch" status
4. Calls `generateBranchNameMutation.mutateAsync({ prompt, projectId })` with 30s timeout
5. **AI succeeds** → `aiBranchName = "fix-login-bug"`
6. Sends to server: `{ name: undefined, prompt: "fix the login bug", branchName: "fix-login-bug" }`

**Server** (`create.ts:369-374`):
1. `input.branchName` is set → `branch = sanitizeBranchNameWithMaxLength(withPrefix("fix-login-bug"))` → e.g. `"kiet/fix-login-bug"`
2. Collision check runs (line 382): `input.branchName?.trim()` is truthy
3. No existing workspace on that branch → creates new worktree + workspace
4. `workspace.name = input.name ?? branch = "kiet/fix-login-bug"` (since `name` is undefined)
5. `isUnnamed: true`

**Post-create** (`useCreateWorkspace.ts:79-100`):
1. `wasExisting = false` → sets up pending terminal, runs setup script
2. Navigates to workspace

**UX issues:**
- ✅ Works well when AI succeeds
- ❌ **Workspace name becomes the branch name** (`"kiet/fix-login-bug"`) because `input.name` was undefined. User sees a slash-separated branch string as their workspace title instead of their prompt.
- The `isUnnamed: true` flag triggers a post-create auto-rename via `attemptWorkspaceAutoRenameFromPrompt` in `initializeWorkspaceWorktree` (`create.ts:523`), which is ANOTHER AI call. So there are TWO serial AI calls: one for branch name, one for workspace display name.

---

## Scenario 2: Prompt only, AI branch gen fails

**User action:** Same as Scenario 1 but AI times out or auth fails.

**Renderer** (`PromptGroup.tsx:780-806`):
1. Catches error, shows `"Using random branch name"` toast
2. `aiBranchName = null`
3. Sends to server: `{ name: undefined, prompt: "fix the login bug", branchName: undefined }`

**Server** (`create.ts:376-380`):
1. `input.branchName` is undefined → hits the `else` branch
2. `branch = generateBranchName({ existingBranches, authorPrefix })` → e.g. `"kiet/cheerful-umbrella"`
3. Collision check at line 382: `input.branchName?.trim()` is **falsy** → **collision check SKIPPED entirely**
4. Creates new worktree + workspace
5. `workspace.name = "kiet/cheerful-umbrella"`

**UX issues:**
- ✅ Always creates a new workspace (random name can't collide)
- ❌ Workspace name is `"kiet/cheerful-umbrella"` — meaningless to the user
- ❌ Post-create auto-rename (another AI call) may also fail, leaving the random name permanently

---

## Scenario 3: Explicit workspace name, no branch name

**User action:** Types workspace name "Login Fix", types prompt, no branch name.

**Renderer** (`PromptGroup.tsx:984-989`):
1. `workspaceNameEdited = true`, `workspaceName = "Login Fix"`
2. `branchNameEdited = false`
3. AI branch gen runs (same as Scenario 1)
4. Sends: `{ name: "Login Fix", prompt: "...", branchName: "fix-login-bug" }`

**Server**:
1. Branch from AI name → `"kiet/fix-login-bug"`
2. Collision check runs (branchName was set)
3. Creates workspace with `name: "Login Fix"` (input.name is set)
4. `isUnnamed: false`

**UX:** ✅ Works correctly. User sees "Login Fix" as workspace name.

---

## Scenario 4: Explicit branch name, no workspace name

**User action:** Types branch name "feature/auth-fix" in the branch input, types prompt.

**Renderer** (`PromptGroup.tsx:990-999`):
1. `branchNameEdited = true`, `branchName = "feature/auth-fix"`
2. `willGenerateAIName = false` (branchNameEdited is true)
3. AI branch gen does NOT run
4. Sends: `{ name: undefined, prompt: "...", branchName: "feature/auth-fix" }`

**Server** (`create.ts:369-374`):
1. `branch = sanitizeBranchNameWithMaxLength(withPrefix("feature/auth-fix"))` → `"kiet/feature/auth-fix"`
2. Collision check runs (branchName was set)
3. If branch already has a workspace → returns `{ wasExisting: true }`, navigates to existing
4. If no collision → creates new, `workspace.name = "kiet/feature/auth-fix"`, `isUnnamed: true`

**UX issues:**
- ❌ Collision check fires because `input.branchName?.trim()` is truthy — **even though the user might not intend to open an existing workspace**. They typed a branch name for a NEW workspace and it silently opens something else.
- ❌ Workspace name is the prefixed branch string, not the prompt
- ❌ No user confirmation: "This branch already has a workspace, open it?" — just silently navigates

---

## Scenario 5: No prompt, no name, no branch (empty create)

**User action:** Hits Cmd+Enter with nothing filled in.

**Renderer** (`PromptGroup.tsx:740-746`):
1. `displayName = "New workspace"`
2. `willGenerateAIName = false` (no trimmedPrompt)
3. No AI branch gen
4. Sends: `{ name: undefined, prompt: undefined, branchName: undefined }`

**Server** (`create.ts:376-380`):
1. All undefined → `branch = generateBranchName(...)` → random `"kiet/cheerful-umbrella"`
2. Collision check skipped (branchName wasn't set)
3. Creates workspace with `name: "kiet/cheerful-umbrella"`, `isUnnamed: true`

**UX issues:**
- ✅ Always works (random name)
- ❌ Meaningless workspace name
- ❌ Post-create rename has no prompt to derive from, so the auto-rename AI call has nothing to work with → stays as random name

---

## Scenario 6: PR link (create from PR)

**User action:** Links a PR, types a prompt, hits create.

**Renderer** (`PromptGroup.tsx:960-978`):
1. `linkedPR` is set → takes a completely different code path
2. Calls `createFromPr.mutateAsyncWithSetup({ projectId, prUrl }, launchRequest)`
3. Does NOT call `createWorkspace` at all

**V1 `createFromPr`** (`useCreateFromPr.ts`):
1. Calls `electronTrpc.workspaces.createFromPr.mutateAsync({ projectId, prUrl })`
2. Server clones the PR's head branch, creates worktree
3. Workspace name = PR title
4. Branch name = PR head branch

**UX:** ✅ Works well. PR provides all naming context.

---

## Scenario 7: Branch selected from base-branch picker, then create

**User action:** Opens base-branch picker, selects `feature/existing`, then hits create with a prompt.

**Renderer**:
1. `compareBaseBranch = "feature/existing"` is set
2. This is the BASE branch (what the new branch forks from), NOT the workspace branch
3. AI branch gen runs normally, creates a new branch from `feature/existing`

**UX:** ✅ Works correctly. The base-branch picker only sets the fork point.

---

## Scenario 8: Branch selected from base-branch picker, "Open" action on existing workspace

**User action:** Opens base-branch picker, sees a branch with an active workspace, clicks "Open".

**Renderer** (`PromptGroup.tsx:1111-1117`):
1. Calls `handleOpenActiveWorkspace(workspaceId)`
2. Closes modal, navigates to existing workspace
3. Does NOT call create at all

**UX:** ✅ Works correctly. Clear intent from user action.

---

## Summary of V1 issues

### Naming
1. **Workspace display name is the branch name when user didn't type a name.** The user typed a prompt but the workspace gets named `"kiet/fix-login-bug"` or `"kiet/cheerful-umbrella"` instead of their prompt or a human-friendly derivative.
2. **Two serial AI calls** — one for branch name (renderer), one for auto-rename (server). Both can fail independently, and the auto-rename runs after create, so the user sees the branch name flash then change.
3. **Random names are meaningless** when AI fails — `"cheerful-umbrella"` tells you nothing about the workspace.

### Collision behavior
4. **Silent open on branch collision** — when user types a branch name that already has a workspace, V1 silently navigates to the existing one with `wasExisting: true` and toast still says "Workspace created." No confirmation dialog, no visual indication that the user's prompt/attachments/agent selection were all ignored.
5. **Collision check gate is fragile** — it's based on `input.branchName?.trim()`, which means collision check only runs for user-typed branch names. But the USER might have typed a branch name intending to create a new workspace on that branch. The condition conflates "user provided a name" with "check for collisions."

### Architecture
6. **Branch name generation split across renderer + server** — the renderer does AI generation, the server does random fallback. The server also does prefix application. Two different processes own parts of the branch name logic, making it hard to reason about what name you'll get.
7. **`useExistingBranch` boolean is a separate code path** — adds complexity to the input schema and collision logic for what could be a single `behavior.onExistingBranch: "use" | "error"` flag.
8. **`sourceWorkspaceId` adds another code path** for forking from an existing workspace's branch — but none of this is exercised by the modal UI. It's dead surface area in the create endpoint.

---

## What V2 should do differently

| V1 problem | V2 approach |
|------------|-------------|
| Workspace name = branch name | `workspaceName = input.prompt \|\| branchName` — prompt is always preferred for display name |
| Two serial AI calls | Single AI call (renderer) for branch name; workspace display name derived from prompt synchronously (no second AI call) |
| Silent open on collision | When branch collision detected and user provided explicit branch: return `opened_existing_workspace` outcome + renderer shows distinct toast "Opened existing workspace" (not "Workspace created") |
| Random names when AI fails | Derive from prompt slug (`sanitizeBranchNameWithMaxLength(prompt)`) before falling back to random. Random is last resort, not first fallback. |
| Collision check gate tied to `input.branchName` | Gate on a semantic flag: was the branch name auto-generated or user-provided? Only run collision check on user-provided names. |
| Branch name logic split renderer/server | Server owns all branch name resolution. Renderer sends `prompt` + optional `branchName`. Server derives branch from prompt, applies deduplication, skips collision check on auto-generated names. |
| `useExistingBranch` / `sourceWorkspaceId` dead paths | Not in V2 schema. Single `behavior.onExistingWorkspace` / `behavior.onExistingWorktree` flags. |
124 changes: 124 additions & 0 deletions apps/desktop/plans/v2-create-decisions-final.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# V2 Create Workspace — Final Decisions

## 1. Branch name generation

**Owner:** Renderer.

**Phase 1 flow:**
1. User typed a branch name → use it
2. No branch name → derive from prompt slug (`sanitizeBranchNameWithMaxLength(prompt)` → `fix-the-login-bug`)
3. No prompt either → `workspace-${crypto.randomUUID().slice(0, 8)}`

Host-service receives a branch name every time. Never `undefined`. Host-service deduplicates it (decision #8).

**Phase 2:** AI branch gen runs async in parallel — fire at submit, don't block create. If AI returns before the host-service call, swap in the better name. Requires `workspaceCreation.generateBranchName` on host-service (decision #3).

## 2. Workspace display name

**Owner:** Renderer.

Renderer sends `workspaceName` explicitly:
- User typed a name → use it
- No name → use prompt text (truncated)
- No prompt → use branch name

No post-create AI rename. No flash. What you see in the modal is what you get.

## 3. AI branch name generation

**Behavior:** Not in Phase 1. Use prompt slug (`sanitizeBranchNameWithMaxLength(prompt)`) — purely client-side, no backend call, no project ID needed.

**No `electronTrpc` calls from V2 modal for workspace operations.** The existing `electronTrpc.workspaces.generateBranchName` is a V1 endpoint that requires a V1 local project ID. The V2 modal must not call it — that's a boundary violation.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

**Phase 2 migration:** Add `workspaceCreation.generateBranchName({ projectId, prompt })` to host-service. It has the repo (for branch dedup) and needs a model provider for the AI call. The gap is the host-service doesn't have access to the user's AI credentials today (`callSmallModel` reads from Electron settings). Either extend the host-service model provider for utility calls, or proxy through it to the user's config.

**How V1's AI branch gen works** (for reference):
1. `callSmallModel` picks user's configured provider (OpenAI/Anthropic)
2. Sends prompt with instruction: "Generate a concise git branch name (2-4 words, kebab-case)"
3. Sanitizes + deduplicates against existing branches
4. Returns name without prefix (server applies prefix)
5. Needs: repo path (for branch list), git author config (for prefix), AI credentials

**File:** `apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-branch-name.ts`

## 4. Collision detection

**None.** No collision detection. No `opened_existing_workspace` outcome from the create modal flow.

The host-service receives a branch name and deduplicates it against existing branches. If `fix-the-login-bug` exists, it becomes `fix-the-login-bug-2`. Always creates a new workspace. Never silently opens an existing one.

If the user wants to open an existing workspace, they use the sidebar.

## 5. Collision UX

**N/A.** There is no collision — the branch name is always unique after dedup. The create modal always creates.

Remove the `opened_existing_workspace`, `opened_worktree`, and `adopted_external_worktree` outcome paths from the create flow. The only outcome is `created_workspace`.

## 6. Modal close timing

**Behavior:** Close immediately on submit. Show pending workspace skeleton in sidebar. Navigate when create succeeds.

**Draft preservation:** Stash a snapshot of the draft into a zustand atom before closing. Close + reset the modal normally. On create failure, restore the stash and reopen the modal so the user can retry. On create success, clear the stash.

This doesn't depend on the context provider staying mounted — the zustand atom survives route changes and component lifecycle.

## 7. Pending workspace phases

**Single phase:** `creating`. That's it.

No `generating-branch` (no blocking AI), no `preparing` (no renderer-side prep between close and API call). Skeleton appears, host-service call runs, skeleton resolves to workspace or error.

## 8. Worktree creation always creates a new branch

**Behavior:** Always `git worktree add -b branchName worktreePath baseBranch`. If the branch name already exists (despite dedup), fall back to a new deduplicated name — never check out the existing branch.

Checking out an existing branch into a worktree is a separate intent (e.g. "import existing branch" or `createFromPr`). The create flow should never silently check out someone else's branch. If the `-b` fails because the branch exists, append a suffix and retry — don't switch to a checkout.

V1's try/catch pattern (`worktree add` then fallback to `worktree add -b`) conflates "create" and "checkout" intents. V2 keeps them separate.

## 9. Branch dedup

**Owner:** Host-service (at create time).

Host-service has the authoritative branch list from git at create time. It deduplicates the incoming branch name against existing branches. `fix-the-login-bug` → `fix-the-login-bug-2` if it exists. Renderer sends its best-effort name; host-service guarantees uniqueness.

## 9. `sanitizeBranchNameWithMaxLength` location

**Decision:** Copy into host-service for dedup. Renderer keeps its copy for the branch name preview in the UI. Both need it — renderer for preview, host-service for dedup + sanitization of the final name.

## 10. Host-service input schema

`names.branchName` is always provided by renderer. `names.workspaceName` is always provided by renderer. Host-service sanitizes + deduplicates `branchName` before creating the worktree. Uses `workspaceName` as-is for the cloud row display name.

## 11. Return shape

Simplified. No `outcome` field — create always creates.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

```ts
{ workspace: { id, branch, ... }, warnings: string[] }
```

Remove `opened_existing_workspace`, `opened_worktree`, `adopted_external_worktree` outcomes and all their code paths from the host-service. The create endpoint does one thing: create a workspace on a deduplicated branch name.

## 12. PR create path

**Separate endpoint:** `workspaceCreation.createFromPr({ projectId, prUrl })`.

The PR flow is fundamentally different from normal create — it parses a PR URL, fetches metadata (title, head branch, fork info), checks out the PR's branch with remote tracking, and handles cross-repo fork PRs. None of this overlaps with the normal branch-name-from-prompt flow.

V1 does this as a separate `createFromPr` mutation that takes just `{ projectId, prUrl }` and the server does all resolution. V2 should do the same, using Octokit instead of `gh` CLI.

**Not in Phase 1.** For now, when user links a PR, the renderer uses the PR's head branch as `branchName` and PR title as `workspaceName`, then calls the normal `create` endpoint. This creates a worktree on a new local branch — it doesn't check out the actual PR with remote tracking. Good enough for Phase 1, but loses fork PR support and proper remote setup.

Phase 2 adds `workspaceCreation.createFromPr` with full PR checkout semantics.

## 13. Init/setup flow

**Streamlined.** After worktree + cloud row creation, if `runSetupScript` is true, the host-service runs the setup script (`.superset/setup.sh` or equivalent) inside the worktree before returning. No background job manager, no progress events, no separate init system.

Create blocks until setup is done. The workspace is fully ready when the renderer navigates to it.

V1's complexity (workspaceInitManager, initializeWorkspaceWorktree, async AI rename, progress tracking) is unnecessary. The only post-create work is running a shell script in the worktree directory.

If setup scripts turn out to be too slow (>10s), we can split later: return immediately, run setup async, show "running setup..." in the workspace view. But start simple.
Loading
Loading