feat: improve workspace handling#1993
Conversation
Greptile SummaryThis PR overhauls workspace lifecycle management: workspaces get their own DB table (migration 0011), a new
Confidence Score: 3/5Safe to merge with the provision-failure cleanup fix applied; without it a failed first-provision leaves the task in an unrecoverable state. The workspace table creation and multi-step bootstrap flow are structurally sound, but in createTask.ts a provision failure deletes the workspace row while leaving tasks.workspaceId intact. Any subsequent re-provision attempt through resolveBootstrap finds the non-null UUID, skips the legacy migration branch, and throws Workspace not found — the task cannot be provisioned again without deletion. Primary concern is src/main/core/tasks/operations/createTask.ts for the workspace cleanup on provision failure. Secondary attention to src/main/core/search/workspace-file-index-service.ts for the destroyed-workspace eviction and FTS5 operator handling.
|
| Filename | Overview |
|---|---|
| src/main/core/tasks/operations/createTask.ts | Adds workspace row creation ahead of provision, but leaves tasks.workspaceId pointing to a deleted workspace on provision failure |
| src/main/core/workspaces/controller.ts | New workspace bootstrap controller handling legacy ID migration and worktree path resolution |
| src/main/core/search/workspace-file-index-service.ts | New FTS5-backed workspace file indexer; onWorkspaceDestroyed refreshes staleness timer instead of evicting, and FTS5 operators can leak through the term filter |
| src/main/core/tasks/provisionTask.ts | Refactored to read WorkspaceRow before provisioning and write path/key/data back afterwards; logic looks correct |
| src/renderer/features/tasks/stores/task-manager.ts | Provision flow split into resolveBootstrap + createWorktree + finishProvision; continueProvision adopt branch silently skips adoptWorktree when candidatePath is missing |
| src/renderer/features/tasks/stores/workspace-view-model.tsx | New WorkspaceViewModel: stable view state across provision/unprovision cycles with clean initialize/suspend/dispose lifecycle |
| src/main/core/git/impl/git-service.ts | Adds headKind/shortHash to FullGitStatus and fires status:updated hook for line-count caching |
| drizzle/0011_chunky_scarecrow.sql | Adds workspaces table with partial unique index on key; schema matches Drizzle definition |
| src/renderer/features/tasks/workspace-resolution-view.tsx | New UI for branch_elsewhere/path_missing resolution states; delegates correctly to continueProvision |
| src/main/db/initialize.ts | Adds ensureFileIndex to create FTS5 virtual tables for workspace file search; version-gated via kv table |
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
src/main/core/tasks/operations/createTask.ts:234-240
When `provisionTask` fails, the workspace row is deleted but `tasks.workspaceId` is not cleared. The task row now holds a UUID pointing to a non-existent workspace. If the user triggers re-provisioning, `resolveBootstrap` will find the non-null `workspaceId`, skip the legacy migration branch, and throw `"Workspace not found: {id}"` — making the task permanently unrecoverable without deletion.
```suggestion
if (!provisionResult.success) {
await Promise.all([
db.delete(workspaces).where(eq(workspaces.id, workspaceId)).catch(() => {}),
db.update(tasks).set({ workspaceId: null }).where(eq(tasks.id, params.id)).catch(() => {}),
]);
return err(mapProvisionError(provisionResult.error));
}
```
### Issue 2 of 4
src/main/core/search/workspace-file-index-service.ts:73-75
`onWorkspaceDestroyed` calls `touchMeta`, which sets `indexed_at = unixepoch()` — the current time. This resets the 14-day staleness clock rather than marking the workspace for cleanup. Every workspace destroy event extends the life of its file-index entries by another 14 days, so the FTS5 table grows unboundedly for frequently-cycled workspaces.
```suggestion
onWorkspaceDestroyed(workspaceId: string): void {
try {
sqlite
.prepare(
`INSERT OR REPLACE INTO workspace_file_index_meta (workspace_id, indexed_at)
VALUES (?, 0)`
)
.run(workspaceId);
} catch (e) {
log.warn('WorkspaceFileIndexService: onWorkspaceDestroyed failed', {
workspaceId,
error: String(e),
});
}
}
```
### Issue 3 of 4
src/main/core/search/workspace-file-index-service.ts:85
User-supplied terms that are 3+ characters and happen to be uppercase FTS5 boolean operators (`AND`, `NOT`) pass the length filter and land verbatim in the `MATCH` expression. A search for `foo NOT bar` produces `ftsQuery = 'foo AND NOT AND bar'` — invalid FTS5 syntax that silently returns `[]`. Wrapping each term in double quotes forces FTS5 to treat them as phrase literals.
```suggestion
const ftsQuery = terms.map((t) => `"${t.replace(/"/g, '')}"`).join(' AND ');
```
### Issue 4 of 4
src/renderer/features/tasks/stores/task-manager.ts:443-449
When `action === 'adopt'` but `candidatePath` is `undefined`, neither branch executes — `adoptWorktree` is silently skipped — and `_finishProvision` is called with the workspace path still unset. Raising a clear error makes the failure visible rather than causing a confusing downstream provision with a missing `workDir`.
```suggestion
try {
if (action === 'adopt') {
if (!candidatePath) throw new Error('adopt action requires a candidatePath');
await rpc.workspaces.adoptWorktree({ projectId: this.projectId, taskId, candidatePath });
} else if (action === 'create') {
await rpc.workspaces.createWorktree({ projectId: this.projectId, taskId });
}
} catch (err) {
```
Reviews (1): Last reviewed commit: "fix: format" | Re-trigger Greptile
Uh oh!
There was an error while loading. Please reload this page.