Repository navigation
feat(desktop): headless v1→v2 migrator core + first migration-ledger writers - #5814
Conversation
📝 WalkthroughWalkthroughAdds a ledger-backed v1-to-v2 migration library with project, workspace, and preset helpers, a headless migration runner, organization-scoped persistence APIs, and integration into the existing import wizard. ChangesV1 migration flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant MigrationRunner
participant HostServiceClient
participant ElectronTRPC
participant MigrationLedger
MigrationRunner->>ElectronTRPC: Load organization ledger
ElectronTRPC->>MigrationLedger: List migration rows
MigrationRunner->>HostServiceClient: Resolve and import projects
MigrationRunner->>HostServiceClient: Plan and adopt workspaces
MigrationRunner->>ElectronTRPC: Record migration outcomes
ElectronTRPC->>MigrationLedger: Upsert outcome rows
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 adds the shared core for migrating desktop data from v1 to v2. The main changes are:
Confidence Score: 4/5Successful migrations can be recorded as failures, and concurrent passes can repeat side effects. These paths need fixes before merging.
apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts | Adds the migration orchestrator, but callback failures corrupt successful outcomes and concurrent passes are not serialized. |
| apps/desktop/src/lib/trpc/routers/migration/index.ts | Adds organization-filtered ledger reads and conflict-keyed outcome upserts. |
| apps/desktop/src/renderer/lib/v1-migration/projects.ts | Extracts project discovery, linking, creation, and relocation handling. |
| apps/desktop/src/renderer/lib/v1-migration/workspaces.ts | Extracts workspace planning and adoption with the existing missing-path fallback. |
| apps/desktop/src/renderer/lib/v1-migration/presets.ts | Extracts preset collision handling and v2 preset row construction. |
| apps/desktop/src/renderer/lib/v1-migration/ledger.ts | Adds ledger loading, terminal-state checks, batch recording, and advisory UI recording. |
| apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx | Moves workspace deduplication from stale cloud state to the host-local workspace list. |
| packages/local-db/src/schema/schema.ts | Extends migration kinds for the planned settings and terminal stages. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant Migrator as runV1Migration
participant Host as Host Service
participant Ledger as Migration Ledger
Caller->>Migrator: Run for organization
Migrator->>Ledger: Load outcomes
Migrator->>Host: Import projects
Migrator->>Host: Adopt workspaces
Migrator->>Caller: Run import callbacks
Migrator->>Caller: Insert presets
Migrator->>Ledger: Upsert outcomes
Migrator-->>Caller: Return summaries and gate
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Caller
participant Migrator as runV1Migration
participant Host as Host Service
participant Ledger as Migration Ledger
Caller->>Migrator: Run for organization
Migrator->>Ledger: Load outcomes
Migrator->>Host: Import projects
Migrator->>Host: Adopt workspaces
Migrator->>Caller: Run import callbacks
Migrator->>Caller: Insert presets
Migrator->>Ledger: Upsert outcomes
Migrator-->>Caller: Return summaries and gate
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts:170
**Callback Reverses Project Success**
When `onProjectImported` throws after the host mutation succeeds, the surrounding catch records an error after the queued success. The later upsert leaves the project non-terminal, counts the same project as both migrated and failed, and retries an already-created project on the next run.
```suggestion
try {
deps.onProjectImported?.(result);
} catch (err) {
console.error("[v1-migration] project callback failed", {
v1ProjectId: project.id,
err,
});
}
```
### Issue 2 of 3
apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts:288
**Callback Reverses Workspace Success**
When `onWorkspaceAdopted` throws after adoption commits, the outer catch appends an error for the same workspace. The error overwrites its success in the ledger, prevents the completion gate, and causes a later run to retry a workspace that was already adopted.
```suggestion
try {
deps.onWorkspaceAdopted?.(result.workspace.id, entry.v2ProjectId);
} catch (err) {
console.error("[v1-migration] workspace callback failed", {
v1WorkspaceId: entry.v1WorkspaceId,
err,
});
}
```
### Issue 3 of 3
apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts:86
**Concurrent Passes Share Empty Ledger**
If two app instances run this function for the same organization, both can load the same ledger state and perform mutations before either writes outcomes. The final upserts deduplicate ledger rows but not the preceding side effects, so concurrent preset inserts can create separate UUID-backed rows and callbacks can run twice.
Reviews (1): Last reviewed commit: "feat(desktop): headless v1-to-v2 migrato..." | Re-trigger Greptile
| status: "success", | ||
| v2Id: result.v2ProjectId, | ||
| }); | ||
| deps.onProjectImported?.(result); |
There was a problem hiding this comment.
Callback Reverses Project Success
When onProjectImported throws after the host mutation succeeds, the surrounding catch records an error after the queued success. The later upsert leaves the project non-terminal, counts the same project as both migrated and failed, and retries an already-created project on the next run.
| deps.onProjectImported?.(result); | |
| try { | |
| deps.onProjectImported?.(result); | |
| } catch (err) { | |
| console.error("[v1-migration] project callback failed", { | |
| v1ProjectId: project.id, | |
| err, | |
| }); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts
Line: 170
Comment:
**Callback Reverses Project Success**
When `onProjectImported` throws after the host mutation succeeds, the surrounding catch records an error after the queued success. The later upsert leaves the project non-terminal, counts the same project as both migrated and failed, and retries an already-created project on the next run.
```suggestion
try {
deps.onProjectImported?.(result);
} catch (err) {
console.error("[v1-migration] project callback failed", {
v1ProjectId: project.id,
err,
});
}
```
How can I resolve this? If you propose a fix, please make it concise.| status: "success", | ||
| v2Id: result.workspace.id, | ||
| }); | ||
| deps.onWorkspaceAdopted?.(result.workspace.id, entry.v2ProjectId); |
There was a problem hiding this comment.
Callback Reverses Workspace Success
When onWorkspaceAdopted throws after adoption commits, the outer catch appends an error for the same workspace. The error overwrites its success in the ledger, prevents the completion gate, and causes a later run to retry a workspace that was already adopted.
| deps.onWorkspaceAdopted?.(result.workspace.id, entry.v2ProjectId); | |
| try { | |
| deps.onWorkspaceAdopted?.(result.workspace.id, entry.v2ProjectId); | |
| } catch (err) { | |
| console.error("[v1-migration] workspace callback failed", { | |
| v1WorkspaceId: entry.v1WorkspaceId, | |
| err, | |
| }); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts
Line: 288
Comment:
**Callback Reverses Workspace Success**
When `onWorkspaceAdopted` throws after adoption commits, the outer catch appends an error for the same workspace. The error overwrites its success in the ledger, prevents the completion gate, and causes a later run to retry a workspace that was already adopted.
```suggestion
try {
deps.onWorkspaceAdopted?.(result.workspace.id, entry.v2ProjectId);
} catch (err) {
console.error("[v1-migration] workspace callback failed", {
v1WorkspaceId: entry.v1WorkspaceId,
err,
});
}
```
How can I resolve this? If you propose a fix, please make it concise.| deps: RunV1MigrationDeps, | ||
| ): Promise<V1MigrationSummary> { | ||
| const { organizationId } = deps; | ||
| const ledger = await loadV1MigrationLedger(organizationId); |
There was a problem hiding this comment.
Concurrent Passes Share Empty Ledger
If two app instances run this function for the same organization, both can load the same ledger state and perform mutations before either writes outcomes. The final upserts deduplicate ledger rows but not the preceding side effects, so concurrent preset inserts can create separate UUID-backed rows and callbacks can run twice.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts
Line: 86
Comment:
**Concurrent Passes Share Empty Ledger**
If two app instances run this function for the same organization, both can load the same ledger state and perform mutations before either writes outcomes. The final upserts deduplicate ledger rows but not the preceding side effects, so concurrent preset inserts can create separate UUID-backed rows and callbacks can run twice.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Known and deliberately out of scope for this PR: the per-org single-flight lock lands with the boot trigger (PR 4 of the plan, see plans/20260716-v1-to-v2-auto-migration.md edge-case table). Until then the only caller is the manual wizard, where concurrent same-org runs aren't a realistic path. The other two findings (callback throw reversing success, per-stage ledger flush) are fixed in 9aae067.
🧹 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: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportPresetsPage/ImportPresetsPage.tsx (1)
34-65: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
alreadyImporteddedupe can race ahead ofv2Presetssync, risking duplicate preset creation.
v2Presetscomes fromuseLiveQuery(line 29-32), butisLoading(line 34) only trackspresetsQuery.isPending. Before the Electric/TanStack DB collection finishes its initial sync,v2Presetsis[]by default, soresolvePresetImport(line 57) can computealreadyImported: falsefor a preset that was already imported in a prior session — letting the user click "Import" again and insert a duplicate row via the non-idempotentcollections.v2TerminalPresets.insert(row)at line 104.Contrast with
ImportWorkspacesPage.tsx, where the equivalent host query'sisPendingis folded into the page's aggregateisLoading, gating the dedupe check on readiness.As per coding guidelines: "TanStack DB/Electric
useLiveQueryrendering must be cache-first... Wait for strict readiness before deriving missing rows or writing defaults unless the write is provably idempotent." Inserting a new preset row is not idempotent, so readiness should gate this derivation.🛡️ Proposed fix: include v2Presets readiness in isLoading
const { data: v2Presets = [] } = useLiveQuery( (query) => query.from({ v2TerminalPresets: collections.v2TerminalPresets }), [collections], ); - const isLoading = presetsQuery.isPending; + const isLoading = presetsQuery.isPending || v2Presets === undefined; // adjust based on actual useLiveQuery readiness field (e.g. isReady/isLoading)🤖 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/components/V1ImportModal/ImportPresetsPage/ImportPresetsPage.tsx` around lines 34 - 65, Include the v2Presets live-query readiness state in the aggregate isLoading value of ImportPresetsPage, matching the readiness handling in ImportWorkspacesPage. Keep resolvePresetImport and preset rendering gated until the v2 preset collection has completed its initial sync, so alreadyImported is not derived from the default empty array and duplicate non-idempotent inserts cannot be initiated.Source: Coding guidelines
🧹 Nitpick comments (5)
apps/desktop/src/lib/trpc/routers/migration/index.ts (1)
44-89: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider using a transaction for multiple inserts.
While the current loop approach works fine for a small number of entries, wrapping the loop in a database transaction ensures atomicity and can improve performance by avoiding multiple implicit transactions in SQLite.
💡 Proposed refactor to use a transaction
- .mutation(({ input }) => { - for (const entry of input.entries) { - localDb - .insert(v1MigrationState) - .values({ - organizationId: input.organizationId, - v1Id: entry.v1Id, - kind: entry.kind, - status: entry.status, - v2Id: entry.v2Id ?? null, - reason: entry.reason ?? null, - }) - .onConflictDoUpdate({ - target: [ - v1MigrationState.organizationId, - v1MigrationState.v1Id, - v1MigrationState.kind, - ], - set: { - status: entry.status, - v2Id: entry.v2Id ?? null, - reason: entry.reason ?? null, - migratedAt: Date.now(), - }, - }) - .run(); - } - }), + .mutation(({ input }) => { + localDb.transaction((tx) => { + for (const entry of input.entries) { + tx + .insert(v1MigrationState) + .values({ + organizationId: input.organizationId, + v1Id: entry.v1Id, + kind: entry.kind, + status: entry.status, + v2Id: entry.v2Id ?? null, + reason: entry.reason ?? null, + }) + .onConflictDoUpdate({ + target: [ + v1MigrationState.organizationId, + v1MigrationState.v1Id, + v1MigrationState.kind, + ], + set: { + status: entry.status, + v2Id: entry.v2Id ?? null, + reason: entry.reason ?? null, + migratedAt: Date.now(), + }, + }) + .run(); + } + }); + }),🤖 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/lib/trpc/routers/migration/index.ts` around lines 44 - 89, Wrap the entry loop in the ledgerRecord mutation with a localDb transaction so all inserts or conflict updates execute atomically as one operation. Preserve the existing per-entry insert and onConflictDoUpdate behavior and input validation.apps/desktop/src/renderer/lib/v1-migration/v1-migration.test.ts (1)
1-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo coverage for
importV1Project/adoptV1Workspacebranching logic.Tests here only cover the pure decision helpers. The retry/fallback branches in
importV1Project(relocate detection) andadoptV1Workspace(NOT_FOUNDfallback) are untested; ahostClientmock withmock.fnfor.mutatewould let you assert the fallback path without real I/O.🤖 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/lib/v1-migration/v1-migration.test.ts` around lines 1 - 153, Add tests covering the branching logic in importV1Project and adoptV1Workspace, not just the pure helpers. Use a hostClient mock with mock.fn for mutate to verify importV1Project’s relocate-detection retry path and adoptV1Workspace’s NOT_FOUND fallback, asserting the expected fallback mutations and outcomes without real I/O.apps/desktop/src/renderer/lib/v1-migration/projects.ts (1)
76-86: 🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy liftEmit
existingPathas structured error data
apps/desktop/src/renderer/lib/v1-migration/projects.ts:76-86still depends on the host-service message text to decide whether to prompt for relocate.packages/host-service/src/trpc/router/project/project.ts:529already throwsCONFLICT; includeexistingPathin the error payload so the renderer can branch on typed data instead of regexing the message.🤖 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/lib/v1-migration/projects.ts` around lines 76 - 86, Update the host-service project conflict error at the existing CONFLICT throw to include existingPath as structured error data. Then revise isAlreadySetUpElsewhereError and extractExistingPath in the renderer to consume the typed conflict payload instead of matching the host-service message text, preserving the relocate prompt behavior.apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts (2)
108-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRedundant
readV1Projectsquery on every run.
migrateProjects(line 108) andmigrateWorkspaces(line 193) each independently callelectronTrpcClient.migration.readV1Projects.query(). Since this function is designed to run on every boot per the migration plan, threading the already-fetchedv1Projectsarray frommigrateProjectsintomigrateWorkspaces(e.g. via a return value or parameter) avoids a duplicate IPC round-trip on the hot path.Also applies to: 185-198
🤖 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/lib/v1-migration/runV1Migration.ts` at line 108, Reuse the single v1Projects result across migrateProjects and migrateWorkspaces instead of querying electronTrpcClient.migration.readV1Projects.query() in both functions. Thread the already-fetched array through the migrateProjects-to-migrateWorkspaces call, preserving existing migration behavior while removing the redundant IPC request.
226-243: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSwallowed error hides real failures, not just "not found".
The bare
catch {}treats any failure fromlistProjectWorktrees(auth error, timeout, etc.) the same as "no worktrees exist", silently lettingplanWorkspaceAdoptionstreat the project as adoptable. A transient host-service error here could be indistinguishable from a legitimately empty result. Consider at least logging the error for observability, even though the fallback behavior itself may be intentional.🤖 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/lib/v1-migration/runV1Migration.ts` around lines 226 - 243, Update the catch block in the listProjectWorktrees flow to log the caught error with sufficient context before leaving onDiskBranchesByV2ProjectId unset. Preserve the existing fallback behavior used by planWorkspaceAdoptions and adopt(), while ensuring failures such as authentication or timeout errors are observable.
🤖 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/lib/v1-migration/runV1Migration.ts`:
- Around line 82-100: Update runV1Migration to persist accumulated outcomes in a
finally block so recordV1MigrationOutcomes runs even when migrateProjects,
migrateWorkspaces, or migratePresets throws. Preserve normal error propagation
and return behavior, while ensuring all outcomes pushed before the failure are
recorded.
---
Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportPresetsPage/ImportPresetsPage.tsx`:
- Around line 34-65: Include the v2Presets live-query readiness state in the
aggregate isLoading value of ImportPresetsPage, matching the readiness handling
in ImportWorkspacesPage. Keep resolvePresetImport and preset rendering gated
until the v2 preset collection has completed its initial sync, so
alreadyImported is not derived from the default empty array and duplicate
non-idempotent inserts cannot be initiated.
---
Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/migration/index.ts`:
- Around line 44-89: Wrap the entry loop in the ledgerRecord mutation with a
localDb transaction so all inserts or conflict updates execute atomically as one
operation. Preserve the existing per-entry insert and onConflictDoUpdate
behavior and input validation.
In `@apps/desktop/src/renderer/lib/v1-migration/projects.ts`:
- Around line 76-86: Update the host-service project conflict error at the
existing CONFLICT throw to include existingPath as structured error data. Then
revise isAlreadySetUpElsewhereError and extractExistingPath in the renderer to
consume the typed conflict payload instead of matching the host-service message
text, preserving the relocate prompt behavior.
In `@apps/desktop/src/renderer/lib/v1-migration/runV1Migration.ts`:
- Line 108: Reuse the single v1Projects result across migrateProjects and
migrateWorkspaces instead of querying
electronTrpcClient.migration.readV1Projects.query() in both functions. Thread
the already-fetched array through the migrateProjects-to-migrateWorkspaces call,
preserving existing migration behavior while removing the redundant IPC request.
- Around line 226-243: Update the catch block in the listProjectWorktrees flow
to log the caught error with sufficient context before leaving
onDiskBranchesByV2ProjectId unset. Preserve the existing fallback behavior used
by planWorkspaceAdoptions and adopt(), while ensuring failures such as
authentication or timeout errors are observable.
In `@apps/desktop/src/renderer/lib/v1-migration/v1-migration.test.ts`:
- Around line 1-153: Add tests covering the branching logic in importV1Project
and adoptV1Workspace, not just the pure helpers. Use a hostClient mock with
mock.fn for mutate to verify importV1Project’s relocate-detection retry path and
adoptV1Workspace’s NOT_FOUND fallback, asserting the expected fallback mutations
and outcomes without real I/O.
🪄 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: 26b33b7e-f3a9-4aa2-b057-f470dce159e4
📒 Files selected for processing (13)
apps/desktop/src/lib/trpc/routers/migration/index.tsapps/desktop/src/renderer/lib/v1-migration/index.tsapps/desktop/src/renderer/lib/v1-migration/ledger.tsapps/desktop/src/renderer/lib/v1-migration/presets.tsapps/desktop/src/renderer/lib/v1-migration/projects.tsapps/desktop/src/renderer/lib/v1-migration/runV1Migration.tsapps/desktop/src/renderer/lib/v1-migration/v1-migration.test.tsapps/desktop/src/renderer/lib/v1-migration/workspaces.tsapps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportPresetsPage/ImportPresetsPage.tsxapps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportProjectsPage/ImportProjectsPage.tsxapps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsxpackages/local-db/src/schema/schema.tsplans/20260716-v1-to-v2-auto-migration.md
Summary
PR 1 of the v1→v2 auto-migration plan (SUPER-1534 / SUPER-1577, design doc
plans/20260716-v1-to-v2-auto-migration.md): extract the V1ImportModal wizard's import logic into a shared headless module and give the dormantv1_migration_stateledger its first readers/writers.renderer/lib/v1-migration/module:runV1Migration()orchestrator (projects → workspaces → presets, every outcome ledger-recorded, idempotent per boot) plus the extracted pure logic:decideProjectImport,planWorkspaceAdoptions,resolvePresetImport(keep-v2 preset collision policy),importV1Project,adoptV1Workspace(worktree-path NOT_FOUND fallback preserved).migration.ledgerList/migration.ledgerRecordelectron tRPC procedures (upsert on(org, v1Id, kind));V1MigrationKindextended withsettings+terminalfor the follow-up PRs.ImportWorkspacesPagededups against host-localworkspace.listinstead of the stale cloudcloudList(post-local-first, newly adopted workspaces never appear in cloud, so the old "Imported" badge misreported); removed the now-moot cloud tiebreak in v1→v2 project mapping.The boot trigger + flip gate land in a later PR — nothing runs automatically yet; the wizard behavior is unchanged apart from the badge fix.
Test Plan
bun test src/renderer/lib/v1-migration/— 12 tests over the extracted decision logic (each mutation-verified to fail on a seeded regression)bun run typecheck(desktop) and rootbun run lintcleanv1_migration_staterows land and re-running shows Imported/LinkedSummary by cubic
Built the headless v1→v2 migrator core and activated the migration ledger to prepare desktop for automated migration (SUPER-1534, SUPER-1577). Added per-stage ledger flushing and guarded UI callbacks; the wizard now uses the shared module with fixed workspace dedup. Nothing runs automatically yet.
New Features
renderer/lib/v1-migration/withrunV1Migration()(projects → workspaces → presets; idempotent; records outcomes).decideProjectImport,planWorkspaceAdoptions,resolvePresetImport,importV1Project,adoptV1Workspace.migration.ledgerListandmigration.ledgerRecordtRPC procedures; upsert on(org, v1Id, kind).V1MigrationKindwithsettingsandterminalfor follow-up migrations.Bug Fixes
onProjectImported/onWorkspaceAdoptedcallbacks to avoid dropping committed results on later errors.ImportWorkspacesPagededups against host-localworkspace.list, andImportPresetsPageuses the shared resolver and writes ledger results.Written for commit 9aae067. Summary will update on new commits.
Summary by CodeRabbit