feat: multi-directory workspaces#16
Conversation
|
Warning Review limit reached
Your plan includes 1 review of capacity. Refill in 38 minutes and 45 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (16)
📝 WalkthroughWalkthroughThis PR implements multi-directory workspace support, allowing users to define multiple working directories per workspace and bind each agent to a specific directory. The feature includes schema extensions, database migration with backfill for existing workspaces, directory CRUD endpoints, agent-directory binding through forms, and runtime cwd resolution with separate workspace memory handling. ChangesMulti-Directory Workspace Feature
Sequence DiagramsequenceDiagram
participant User
participant WorkspacePage
participant DirectoriesAPI
participant AgentAPI
participant AgentRunAPI
User->>WorkspacePage: Load workspace
WorkspacePage->>DirectoriesAPI: GET /api/workspaces/{id}/directories
DirectoriesAPI-->>WorkspacePage: [directories]
WorkspacePage->>WorkspacePage: Preload directories
User->>WorkspacePage: Add new directory
WorkspacePage->>DirectoriesAPI: POST /api/workspaces/{id}/directories
DirectoriesAPI-->>WorkspacePage: {directory}
WorkspacePage->>WorkspacePage: Refresh directory list
User->>WorkspacePage: Create agent (choose directory)
WorkspacePage->>AgentAPI: POST /api/agents {directoryId}
AgentAPI-->>WorkspacePage: {agent}
WorkspacePage->>WorkspacePage: Show agent with directory
User->>WorkspacePage: Run agent
WorkspacePage->>AgentRunAPI: POST /api/agents/{id}/run
AgentRunAPI->>AgentRunAPI: resolveAgentCwd(agent)
AgentRunAPI->>AgentRunAPI: runAgent(cwd, memoryCwd)
AgentRunAPI-->>WorkspacePage: streaming output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate 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 |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/api/workspaces/route.ts (1)
29-37:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftCreate workspace and default directory in one transaction.
If
ensureDefaultDirectoryfails after insert, the API can return an error while the workspace row already exists, leaving partial state and making client retries unsafe.Suggested direction
- const [row] = await db - .insert(schema.workspaces) - .values({ name, cwd }) - .returning() - await ensureDefaultDirectory(row.id, cwd) + const row = await db.transaction(async (tx) => { + const [created] = await tx + .insert(schema.workspaces) + .values({ name, cwd }) + .returning() + // ideally accept tx in ensureDefaultDirectory to keep this atomic + await ensureDefaultDirectory(created.id, cwd) + return created + })🤖 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 `@src/app/api/workspaces/route.ts` around lines 29 - 37, The workspace insert followed by ensureDefaultDirectory can leave partial state if ensureDefaultDirectory fails; wrap both the insert into schema.workspaces and the subsequent ensureDefaultDirectory call in a single DB transaction so they either both commit or both roll back. Use the DB transaction API around the block that calls db.insert(...).values({ name, cwd }).returning() and then calls ensureDefaultDirectory(row.id, cwd) (or change ensureDefaultDirectory to accept a transaction/context and call its transactional variant), and only return NextResponse.json({ workspace: row }, { status: 201 }) after the transaction completes successfully.
🤖 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 `@src/app/api/agents/route.ts`:
- Around line 47-54: directoryId is being saved without ensuring it belongs to
the same workspace (workspaceId), enabling cross-workspace associations; before
calling db.insert on schema.agents, if directoryId is non-null query the
directories table (e.g. via db.select().from(schema.directories).where({ id:
directoryId, workspaceId })) and if no row is returned reject the request
(throw/return 400) so only directories that match workspaceId are allowed, then
proceed with the existing db.insert into schema.agents; reference symbols:
directoryId, workspaceId, db, schema.agents, schema.directories, and the insert
call.
In `@src/app/api/workspaces/`[id]/directories/[dirId]/route.ts:
- Around line 22-43: The current check (listDirectories) and the delete
(db.delete on schema.workspaceDirectories) are separate and race-prone; make the
last-directory enforcement atomic by moving the validation and delete into a
single database transaction: inside a transaction (using your DB client's
transaction API) select the count of workspaceDirectories for workspaceId with a
row-level lock (or run a COUNT(*) WHERE workspaceId = id FOR UPDATE),
abort/rollback and return 400 if count <= 1, otherwise perform the delete WHERE
id = dirId AND workspaceId = id and commit; update the route handler to use this
transactional flow instead of listDirectories followed by db.delete to prevent
concurrent deletions from removing the last directory.
In `@src/app/api/workspaces/`[id]/directories/route.ts:
- Around line 49-55: Update the validation in the POST handler in route.ts to
require an absolute path: after trimming the incoming body.path (variable path)
ensure it is absolute (use Node's path.isAbsolute or check leading '/' on POSIX)
and return a 400 JSON error if not; keep the existing empty check but replace
the generic error with a clear message like "path must be absolute" and ensure
subsequent logic (e.g., any use of path or label) only runs when the
absolute-path check passes.
In `@src/components/CreateAgentForm.tsx`:
- Around line 25-27: The component's directoryId state can become stale if the
available directory options change (e.g., the selected directory is removed);
update CreateAgentForm to reconcile directoryId whenever the directory options
list changes by validating the current directoryId against the new options and
setting it to '' (or null-equivalent) if it's no longer present; locate the
useState declaration for directoryId and its setter setDirectoryId as well as
any similar state usages later in the component (lines referenced around the
second occurrence) and add an effect (or inline check where options are derived)
to clear or replace directoryId when the options array changes.
In `@src/components/WorkspaceDirectories.tsx`:
- Around line 28-34: The add handler should early-return if an add is already in
progress to prevent duplicate requests: in the add function (async function
add(e: React.FormEvent)) check the busy flag at the top (if (busy) return)
before calling e.preventDefault() or performing validation, ensuring you still
call e.preventDefault() when appropriate and keep using setBusy(true) when
starting the operation; update references to busy and setBusy accordingly so
rapid or programmatic invocations are ignored.
- Around line 60-64: In the remove function, add a defensive busy guard at the
top (similar to add) to return early if busy is true to prevent races, and
update the confirmation prompt text to start with a capital letter (e.g.,
"Remove this directory? agents bound to it will fall back to the first
directory.") while keeping the existing fallback behavior and setBusy(true) call
intact; modify the remove function and reference the directories array, busy
state, setBusy, and the confirmation call.
---
Outside diff comments:
In `@src/app/api/workspaces/route.ts`:
- Around line 29-37: The workspace insert followed by ensureDefaultDirectory can
leave partial state if ensureDefaultDirectory fails; wrap both the insert into
schema.workspaces and the subsequent ensureDefaultDirectory call in a single DB
transaction so they either both commit or both roll back. Use the DB transaction
API around the block that calls db.insert(...).values({ name, cwd }).returning()
and then calls ensureDefaultDirectory(row.id, cwd) (or change
ensureDefaultDirectory to accept a transaction/context and call its
transactional variant), and only return NextResponse.json({ workspace: row }, {
status: 201 }) after the transaction completes successfully.
🪄 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: f96936f1-a8e3-4b12-8a76-66b0756c9708
📒 Files selected for processing (13)
src/app/api/agents/[id]/run/route.tssrc/app/api/agents/route.tssrc/app/api/workspaces/[id]/directories/[dirId]/route.tssrc/app/api/workspaces/[id]/directories/route.tssrc/app/api/workspaces/route.tssrc/app/workspace/[id]/page.tsxsrc/components/AgentPanel.tsxsrc/components/CreateAgentForm.tsxsrc/components/WorkspaceDirectories.tsxsrc/lib/claude.tssrc/lib/db.tssrc/lib/directories.tssrc/lib/schema.ts
| const directoryId = | ||
| typeof body.directoryId === 'string' && body.directoryId | ||
| ? body.directoryId | ||
| : null | ||
| const [row] = await db | ||
| .insert(schema.agents) | ||
| .values({ workspaceId, name, systemPrompt, model, skills }) | ||
| .values({ workspaceId, directoryId, name, systemPrompt, model, skills }) | ||
| .returning() |
There was a problem hiding this comment.
Validate directoryId against the agent’s workspace before insert.
directoryId is persisted without verifying it belongs to workspaceId. That allows cross-workspace bindings and can produce unexpected FK failures.
Suggested fix
import { NextResponse } from 'next/server'
import { db, schema } from '`@/lib/db`'
-import { eq, desc } from 'drizzle-orm'
+import { and, eq, desc } from 'drizzle-orm'
@@
- const directoryId =
- typeof body.directoryId === 'string' && body.directoryId
- ? body.directoryId
- : null
+ const rawDirectoryId =
+ typeof body.directoryId === 'string' ? body.directoryId.trim() : ''
+ let directoryId: string | null = null
+ if (rawDirectoryId) {
+ const [dir] = await db
+ .select({ id: schema.workspaceDirectories.id })
+ .from(schema.workspaceDirectories)
+ .where(
+ and(
+ eq(schema.workspaceDirectories.id, rawDirectoryId),
+ eq(schema.workspaceDirectories.workspaceId, workspaceId),
+ ),
+ )
+ if (!dir) {
+ return NextResponse.json(
+ { error: 'invalid directoryId for workspace' },
+ { status: 400 },
+ )
+ }
+ directoryId = dir.id
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const directoryId = | |
| typeof body.directoryId === 'string' && body.directoryId | |
| ? body.directoryId | |
| : null | |
| const [row] = await db | |
| .insert(schema.agents) | |
| .values({ workspaceId, name, systemPrompt, model, skills }) | |
| .values({ workspaceId, directoryId, name, systemPrompt, model, skills }) | |
| .returning() | |
| const rawDirectoryId = | |
| typeof body.directoryId === 'string' ? body.directoryId.trim() : '' | |
| let directoryId: string | null = null | |
| if (rawDirectoryId) { | |
| const [dir] = await db | |
| .select({ id: schema.workspaceDirectories.id }) | |
| .from(schema.workspaceDirectories) | |
| .where( | |
| and( | |
| eq(schema.workspaceDirectories.id, rawDirectoryId), | |
| eq(schema.workspaceDirectories.workspaceId, workspaceId), | |
| ), | |
| ) | |
| if (!dir) { | |
| return NextResponse.json( | |
| { error: 'invalid directoryId for workspace' }, | |
| { status: 400 }, | |
| ) | |
| } | |
| directoryId = dir.id | |
| } | |
| const [row] = await db | |
| .insert(schema.agents) | |
| .values({ workspaceId, directoryId, name, systemPrompt, model, skills }) | |
| .returning() |
🤖 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 `@src/app/api/agents/route.ts` around lines 47 - 54, directoryId is being saved
without ensuring it belongs to the same workspace (workspaceId), enabling
cross-workspace associations; before calling db.insert on schema.agents, if
directoryId is non-null query the directories table (e.g. via
db.select().from(schema.directories).where({ id: directoryId, workspaceId }))
and if no row is returned reject the request (throw/return 400) so only
directories that match workspaceId are allowed, then proceed with the existing
db.insert into schema.agents; reference symbols: directoryId, workspaceId, db,
schema.agents, schema.directories, and the insert call.
| const existing = await listDirectories(id) | ||
| if (!existing.find((d) => d.id === dirId)) { | ||
| return NextResponse.json( | ||
| { error: 'directory not found in this workspace' }, | ||
| { status: 404 }, | ||
| ) | ||
| } | ||
| if (existing.length <= 1) { | ||
| return NextResponse.json( | ||
| { error: 'cannot remove the last directory' }, | ||
| { status: 400 }, | ||
| ) | ||
| } | ||
| await db | ||
| .delete(schema.workspaceDirectories) | ||
| .where( | ||
| and( | ||
| eq(schema.workspaceDirectories.id, dirId), | ||
| eq(schema.workspaceDirectories.workspaceId, id), | ||
| ), | ||
| ) | ||
| return NextResponse.json({ ok: true }) |
There was a problem hiding this comment.
Make last-directory enforcement atomic.
The guard (existing.length <= 1) and the DELETE run in separate queries. Concurrent delete requests can both pass validation and remove all directories.
Suggested direction
export async function DELETE(
@@
- const existing = await listDirectories(id)
- if (!existing.find((d) => d.id === dirId)) {
- return NextResponse.json(
- { error: 'directory not found in this workspace' },
- { status: 404 },
- )
- }
- if (existing.length <= 1) {
- return NextResponse.json(
- { error: 'cannot remove the last directory' },
- { status: 400 },
- )
- }
- await db
- .delete(schema.workspaceDirectories)
- .where(
- and(
- eq(schema.workspaceDirectories.id, dirId),
- eq(schema.workspaceDirectories.workspaceId, id),
- ),
- )
+ const result = await db.transaction(async (tx) => {
+ const existing = await tx
+ .select({ id: schema.workspaceDirectories.id })
+ .from(schema.workspaceDirectories)
+ .where(eq(schema.workspaceDirectories.workspaceId, id))
+
+ if (!existing.find((d) => d.id === dirId)) return { status: 404 as const }
+ if (existing.length <= 1) return { status: 400 as const }
+
+ await tx
+ .delete(schema.workspaceDirectories)
+ .where(
+ and(
+ eq(schema.workspaceDirectories.id, dirId),
+ eq(schema.workspaceDirectories.workspaceId, id),
+ ),
+ )
+ return { status: 200 as const }
+ })
+
+ if (result.status === 404) {
+ return NextResponse.json(
+ { error: 'directory not found in this workspace' },
+ { status: 404 },
+ )
+ }
+ if (result.status === 400) {
+ return NextResponse.json(
+ { error: 'cannot remove the last directory' },
+ { status: 400 },
+ )
+ }
return NextResponse.json({ ok: true })
}🤖 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 `@src/app/api/workspaces/`[id]/directories/[dirId]/route.ts around lines 22 -
43, The current check (listDirectories) and the delete (db.delete on
schema.workspaceDirectories) are separate and race-prone; make the
last-directory enforcement atomic by moving the validation and delete into a
single database transaction: inside a transaction (using your DB client's
transaction API) select the count of workspaceDirectories for workspaceId with a
row-level lock (or run a COUNT(*) WHERE workspaceId = id FOR UPDATE),
abort/rollback and return 400 if count <= 1, otherwise perform the delete WHERE
id = dirId AND workspaceId = id and commit; update the route handler to use this
transactional flow instead of listDirectories followed by db.delete to prevent
concurrent deletions from removing the last directory.
| const path = | ||
| body && typeof body.path === 'string' ? body.path.trim() : '' | ||
| const label = | ||
| body && typeof body.label === 'string' ? body.label.trim() : '' | ||
| if (!path) { | ||
| return NextResponse.json({ error: 'path is required' }, { status: 400 }) | ||
| } |
There was a problem hiding this comment.
Enforce absolute paths in directory creation.
POST currently accepts any non-empty string. That allows relative paths, which can resolve against process cwd unpredictably and break the workspace directory contract.
Suggested fix
+import path from 'node:path'
import { NextResponse } from 'next/server'
import { db, schema } from '`@/lib/db`'
import { eq } from 'drizzle-orm'
import { listDirectories } from '`@/lib/directories`'
@@
- const path =
- body && typeof body.path === 'string' ? body.path.trim() : ''
+ const directoryPath =
+ body && typeof body.path === 'string' ? body.path.trim() : ''
const label =
body && typeof body.label === 'string' ? body.label.trim() : ''
- if (!path) {
+ if (!directoryPath) {
return NextResponse.json({ error: 'path is required' }, { status: 400 })
}
+ if (!path.isAbsolute(directoryPath)) {
+ return NextResponse.json(
+ { error: 'path must be absolute' },
+ { status: 400 },
+ )
+ }
const [row] = await db
.insert(schema.workspaceDirectories)
- .values({ workspaceId: workspace.id, path, label })
+ .values({ workspaceId: workspace.id, path: directoryPath, label })
.returning()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const path = | |
| body && typeof body.path === 'string' ? body.path.trim() : '' | |
| const label = | |
| body && typeof body.label === 'string' ? body.label.trim() : '' | |
| if (!path) { | |
| return NextResponse.json({ error: 'path is required' }, { status: 400 }) | |
| } | |
| import path from 'node:path' | |
| import { NextResponse } from 'next/server' | |
| import { db, schema } from '`@/lib/db`' | |
| import { eq } from 'drizzle-orm' | |
| import { listDirectories } from '`@/lib/directories`' | |
| const directoryPath = | |
| body && typeof body.path === 'string' ? body.path.trim() : '' | |
| const label = | |
| body && typeof body.label === 'string' ? body.label.trim() : '' | |
| if (!directoryPath) { | |
| return NextResponse.json({ error: 'path is required' }, { status: 400 }) | |
| } | |
| if (!path.isAbsolute(directoryPath)) { | |
| return NextResponse.json( | |
| { error: 'path must be absolute' }, | |
| { status: 400 }, | |
| ) | |
| } | |
| const [row] = await db | |
| .insert(schema.workspaceDirectories) | |
| .values({ workspaceId: workspace.id, path: directoryPath, label }) | |
| .returning() |
🤖 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 `@src/app/api/workspaces/`[id]/directories/route.ts around lines 49 - 55,
Update the validation in the POST handler in route.ts to require an absolute
path: after trimming the incoming body.path (variable path) ensure it is
absolute (use Node's path.isAbsolute or check leading '/' on POSIX) and return a
400 JSON error if not; keep the existing empty check but replace the generic
error with a clear message like "path must be absolute" and ensure subsequent
logic (e.g., any use of path or label) only runs when the absolute-path check
passes.
| // Empty string => "use the workspace's default directory" (null on | ||
| // the wire). Only meaningful when there is more than one directory. | ||
| const [directoryId, setDirectoryId] = useState('') |
There was a problem hiding this comment.
Reconcile directoryId when directory options change.
If the selected directory is removed, stale directoryId can remain in component state and still be posted.
Suggested fix
const [directoryId, setDirectoryId] = useState('')
@@
+ useEffect(() => {
+ if (directoryId && !directories.some((d) => d.id === directoryId)) {
+ setDirectoryId('')
+ }
+ }, [directories, directoryId])Also applies to: 139-157
🤖 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 `@src/components/CreateAgentForm.tsx` around lines 25 - 27, The component's
directoryId state can become stale if the available directory options change
(e.g., the selected directory is removed); update CreateAgentForm to reconcile
directoryId whenever the directory options list changes by validating the
current directoryId against the new options and setting it to '' (or
null-equivalent) if it's no longer present; locate the useState declaration for
directoryId and its setter setDirectoryId as well as any similar state usages
later in the component (lines referenced around the second occurrence) and add
an effect (or inline check where options are derived) to clear or replace
directoryId when the options array changes.
| async function add(e: React.FormEvent) { | ||
| e.preventDefault() | ||
| if (!path.trim()) { | ||
| setError('path is required') | ||
| return | ||
| } | ||
| setBusy(true) |
There was a problem hiding this comment.
Add defensive busy check at function start.
While the submit button is disabled when busy, the function could theoretically be invoked programmatically or via rapid events before the state update takes effect. Adding a guard at the start prevents potential duplicate requests.
🛡️ Suggested defensive check
async function add(e: React.FormEvent) {
e.preventDefault()
+ if (busy) return
if (!path.trim()) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function add(e: React.FormEvent) { | |
| e.preventDefault() | |
| if (!path.trim()) { | |
| setError('path is required') | |
| return | |
| } | |
| setBusy(true) | |
| async function add(e: React.FormEvent) { | |
| e.preventDefault() | |
| if (busy) return | |
| if (!path.trim()) { | |
| setError('path is required') | |
| return | |
| } | |
| setBusy(true) |
🤖 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 `@src/components/WorkspaceDirectories.tsx` around lines 28 - 34, The add
handler should early-return if an add is already in progress to prevent
duplicate requests: in the add function (async function add(e: React.FormEvent))
check the busy flag at the top (if (busy) return) before calling
e.preventDefault() or performing validation, ensuring you still call
e.preventDefault() when appropriate and keep using setBusy(true) when starting
the operation; update references to busy and setBusy accordingly so rapid or
programmatic invocations are ignored.
| async function remove(dirId: string) { | ||
| if (directories.length <= 1) return | ||
| if (!confirm('remove this directory? agents bound to it will fall back to the first directory.')) | ||
| return | ||
| setBusy(true) |
There was a problem hiding this comment.
Fix grammar and add defensive busy check.
Two minor issues:
- The confirmation message should start with a capital letter for proper grammar.
- Add a busy guard at the start to prevent potential race conditions (same as in
addfunction).
📝 Suggested fixes
async function remove(dirId: string) {
+ if (busy) return
if (directories.length <= 1) return
- if (!confirm('remove this directory? agents bound to it will fall back to the first directory.'))
+ if (!confirm('Remove this directory? Agents bound to it will fall back to the first directory.'))
return🤖 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 `@src/components/WorkspaceDirectories.tsx` around lines 60 - 64, In the remove
function, add a defensive busy guard at the top (similar to add) to return early
if busy is true to prevent races, and update the confirmation prompt text to
start with a capital letter (e.g., "Remove this directory? agents bound to it
will fall back to the first directory.") while keeping the existing fallback
behavior and setBusy(true) call intact; modify the remove function and reference
the directories array, busy state, setBusy, and the confirmation call.
6de416a to
54968db
Compare
54968db to
438c551
Compare
Closes #2.
Summary
workspace_directoriestable (id, workspace_id, path, label, created_at). A workspace can now have many directories instead of just one.agents.directory_id(nullable FK, ON DELETE SET NULL). Binds an agent to one specific directory. Null means "use the workspace's first directory at run time".workspace_directoriesrow mirrored from its legacyworkspaces.cwd, labeled "default". Idempotent. Existing agents keep null directory_id and resolve to that same default at run time, so behavior is unchanged.workspaces.cwdkept for back-compat as the canonical "default directory" path.src/lib/directories.tswithlistDirectories(workspaceId),resolveAgentCwd(agent, workspace), andensureDefaultDirectory(workspaceId, cwd)(called from the workspace POST handler so freshly created workspaces are uniform from day one).GET / POST /api/workspaces/[id]/directoriesandDELETE /api/workspaces/[id]/directories/[dirId]. Delete refuses to remove the last directory.directoryId.resolveAgentCwdand uses it for the spawn cwd AND forwriteAgentSkillConfig. Shared MEMORY.md now reads from the workspace's default cwd via the newmemoryCwdopt inclaude.ts, so memory stays workspace-scoped instead of per-directory.CreateAgentFormgains a Directory dropdown when the workspace has more than one directory. With one directory, the dropdown is hidden and the agent uses the default automatically.AgentPanelshows a chip with the agent's effective directory (amber when explicitly bound, neutral when defaulted).Test plan
pwdor any file-touching prompt).Summary by CodeRabbit