feat(vscode): dev-workflow batch — Open Dev URL rows, per-engineer config, Builders list/tree toggle#785
Merged
Conversation
The Workspace view's Start/Stop row was scoped to *this workspace's*
dev target — `devTarget.id` (`main` or a worktree basename). A dev
started via the builder-row right-click context menu registers under
`builder.id` from Tower's overview, which doesn't match any workspace
target's id, so the row stayed stuck on "Start Dev Server" even with
a dev actively running.
Contradicts the single-dev-slot model in dev-shared.ts that enforces
"one dev at a time across {main + all builders}". The slot was
occupied; the UI just didn't reflect it.
Three-way render now:
- No dev running anywhere → Start Dev Server (for this workspace's
target). Today's behaviour.
- This workspace's dev is the one running → Stop Dev Server via
codev.stopWorkspaceDev. Today's behaviour.
- A *different* target's dev is running → Stop Dev Server via
codev.stopWorktreeDev. Tooltip names the running target so the
user sees what they're stopping. (Single-slot invariant means
there's at most one to stop, so stopWorktreeDev's "kill all in
the local registry" semantics are exactly "stop the slot.")
The existing onDidChangeDevTerminals subscription already fires from
context-menu-started devs (TerminalManager.openDevTerminal fires it
unconditionally), so no event-plumbing changes are needed — only the
check that consumes the event widened from per-target to per-slot.
Mutually-exclusive row-count invariant preserved: still exactly one
Start-or-Stop row, never both.
Adds a one-click affordance to open the dev server's URL in the
user's default browser, surfaced as a Workspace-view row below
Start/Stop Dev Server.
Schema (.codev/config.json): worktree.devUrl (legacy single) coexists
with worktree.devUrls: [{ label, url }, ...] (multi, labeled). The
canonical resolved shape is always a WorktreeDevUrl[]; a legacy single
normalizes to [{ label: 'Open Dev URL', url }] so consumers don't
branch. devUrls (if set) wins over devUrl.
VSCode UX: one workspace-view row per resolved entry, visible whenever
any URL is configured (independent of dev-PTY state). Row click and
palette 'Codev: Open Dev URL' both route through vscode.env.openExternal
— a real default browser gives DevTools, a real cookie jar, and OAuth
that actually works, unlike VSCode's Simple Browser webview which has
none of those (rationale recorded in #780). Palette invocation without
an argument picks via QuickPick if multiple URLs are configured.
Reads .codev/config.json directly from the extension (sync, small file)
rather than adding a Tower endpoint just for one field. Existing 81
codev tests around the resolved-config shape updated for the schema
change.
Appends a 5th layer to lib/config.ts:loadConfig. Reads <workspaceRoot>/.codev/config.local.json (if present) and deep-merges it on top of the committed .codev/config.json. The existing 4-layer pipeline (defaults -> cache -> ~/.codev/config.json -> .codev/config.json) continues to apply below it. The use case the previous layers couldn't express: different engineers working on the same repo wanting different defaults (web dev defaults to 'pnpm dev', mobile dev to 'pnpm dev:mobile', backend to 'pnpm dev:api'). Layer 3 (~/.codev/config.json) spans every project on the machine, so it can't say 'in *this* repo only.' Layer 5 fixes that without inventing new machinery — deepMerge / readJsonFile are reused; a new resolveLocalConfigPath helper mirrors resolveProjectConfigPath so the layer-loading code stays uniform. agent-farm/utils/config.ts: loadUserConfig already calls loadConfig, so every worktree.* consumer (including the just-shipped devUrl/devUrls) automatically picks up local overrides with no further wiring. Recommended user action: add .codev/config.local.json to your project's .gitignore so per-engineer overrides aren't accidentally committed. Tests: 5 new cases in __tests__/config.test.ts — 3 covering loadConfig layer-5 semantics (local-only, local-overrides-project, missing-noop) and 2 covering resolveLocalConfigPath directly. All 26 config tests pass.
… defaults Drops the legacy worktree.devUrl (legacy single-URL string) shipped one commit ago in bf7a2e7, before any external user could depend on it. Sole form is now worktree.devUrls: Array<{ label, url }>; both fields are mandatory per schema. Entries missing either field are silently filtered out by the resolver and the VSCode-side reader. Why: with two coexisting shapes (devUrl vs devUrls), label-defaulting to "Open Dev URL" was a holdover from devUrl's literal semantic. Unlabeled multi-URL configs produced duplicate "Open Dev URL" rows distinguishable only by tooltip — confusing. Requiring label removes the ambiguity at the source: every row has a human-recognizable name picked by the user. Files: schema in types.ts; resolveDevUrls in utils/config.ts; readWorktreeDevUrls in vscode/commands/open-dev-url.ts. 107 tests still pass; tsc + lint clean on both packages.
…on edits
The workspace view's rows are config-driven (Open Dev URL entries from
worktree.devUrls, more to come) but until now the only refresh triggers
were Tower connection state changes and dev-terminal lifecycle events.
Editing .codev/config.json or .codev/config.local.json required a window
reload to see the new rows.
Adds a vscode.workspace.createFileSystemWatcher on .codev/config{,.local}.json
under the active workspacePath. Any create/change/delete fires the
provider's existing changeEmitter; getChildren() then re-reads the
config on next render. The watching primitive is extracted to
src/watch-codev-config.ts so future config-driven views can wire it in
as a one-liner instead of duplicating the boilerplate.
WorkspaceProvider's install is lazy because workspacePath isn't set at
construction time (the Tower connection lands async). The first
onStateChange that exposes a workspacePath installs the watcher and a
flag pins it for the session. Disposables flow through
context.subscriptions so cleanup is standard on extension deactivate.
The pattern mirrors Tower's
agent-farm/servers/tower-tunnel.ts:startConfigWatcher (which watches
~/.codev/cloud.json via node:fs.watch for OAuth credential changes) but
this is the first vscode.workspace.createFileSystemWatcher in the
extension — prior code reacted only to Tower / VSCode events, never to
filesystem events.
The earlier glob pattern '.codev/config{,.local}.json' relied on an
empty-string alternative inside the braces. VSCode's glob matcher
inconsistently honors that — in practice events fired for config.json
but not for config.local.json, so per-engineer override edits silently
failed to refresh the view.
Switching to two explicit non-empty alternatives
'.codev/{config.json,config.local.json}' is unambiguous across glob
implementations. Both files now consistently trigger the watcher's
create/change/delete events.
Both rows were rendering with the globe icon, making them visually identical in the Workspace view though they do different things — 'Open Web Interface' opens the Tower dashboard, 'Open Dev URL' opens the user's running app. Switch the Open Dev URL rows to 'link-external' (square + outgoing arrow) — VSCode's conventional 'opens outside the editor' glyph, which is also a more precise match for what vscode.env.openExternal does. 'Open Web Interface' keeps the more abstract 'globe' since the Tower dashboard is the closest thing to a 'project's web presence' the extension surfaces.
Closes the architecture gap where the VSCode extension was parsing and
merging .codev/config(.local).json itself instead of consuming the
canonical resolved view from Tower. The merge logic now lives in one
place (lib/config.ts:loadConfig); every client just asks the API.
Tower (codev):
- New GET /api/worktree-config route + handler. Returns the resolved
ResolvedWorktreeConfig for the requested workspace (defaults / cache
/ global / project / project-local, deep-merged) by delegating to
the existing getWorktreeConfig(workspaceRoot).
- New worktree-config-watcher.ts: per-workspace node:fs.watch on
.codev/{config.json,config.local.json}, debounced, fans out a
'worktree-config-updated' SSE event on change. Mirrors the existing
tower-tunnel.ts:startConfigWatcher pattern for ~/.codev/cloud.json.
The watcher is lazily installed by the route handler on first
request and torn down by graceful shutdown.
Types (codev-types):
- ResolvedWorktreeConfig + WorktreeDevUrl promoted from a codev-
internal interface to wire-contract types since they now cross
HTTP. codev's utils/config.ts re-exports them so existing internal
callers keep working.
Core (codev-core):
- TowerClient.getWorktreeConfig(workspacePath?) — same shape as
getOverview/getIssue.
VSCode extension:
- open-dev-url.ts: removed fs.readFileSync + the inline two-file
merge; readWorktreeDevUrls (sync) replaced with async
loadWorktreeDevUrls(connectionManager) calling
client.getWorktreeConfig.
- workspace.ts: getChildren is async; constructor subscribes to
connectionManager.onSSEEvent for 'worktree-config-updated'. Tower
emits envelopes on the SSE 'data:' field with no 'event:' name (see
builder-spawn-handler.ts:20 for the precedent comment), so the
subscription JSON-parses the data and matches on envelope.type.
Removed the context parameter — no longer needed.
- watch-codev-config.ts: deleted. The extension never watches local
filesystems anymore; Tower is the only thing that watches the
config files and tells the extension via SSE.
Why this matters: the extension is now tunnel-safe (Tower can be
remote and config-change events flow over the existing SSE
transport), there's exactly one watcher per workspace regardless of
how many VSCode windows are open, and the merge semantics can't
drift between client and server because the client doesn't merge
anymore. The 'one place to merge config' invariant is finally true.
Action-bound config consumers (devCommand, postSpawn, symlinks) were
already live via Tower's per-invocation getWorktreeConfig() reads —
this change adds the matching liveness for UI-bound consumers
(devUrls today, anything we add to the workspace view later).
107 codev tests still pass. tsc + lint clean across all packages.
… unset Previously the row appeared unconditionally — clicking it with no devCommand configured produced a clear-but-still-pointless 'devCommand isn't set' toast from Tower. Now the row is gated on devCommand presence in the resolved config (Tower-merged across all 5 layers), so unconfigured workspaces simply don't show the row. Stop Dev Server is left alone — it stays visible whenever a dev is actually running, so the user can always kill a manually-started dev even if devCommand was later unset. Tooltip on the Start row now also previews the command itself (e.g. 'Run worktree.devCommand (`pnpm dev:local`) for this workspace…') so the user can see what'll fire before clicking. Along the way: extracted loadWorktreeConfig out of commands/open-dev-url.ts (which is now back to being command-specific) into a new top-level src/load-worktree-config.ts helper. The workspace view's getChildren does one fetch per render and reads both devCommand (for the Start gate) and devUrls (for the Open Dev URL rows) off the same response — no duplicate HTTP.
…yle) Each builder row in the Builders view now renders its expanded changed-files list as either a flat list (today's behaviour) or as a folder tree (default, matching VSCode's Source Control panel), with single-child folder chains compacted into one row — e.g. `packages/codev/src` displayed as a single folder, not three nested. Title-bar button toggles between the two; mirrors the accordion- toggle precedent (`codev.buildersAutoCollapse`). Setting `codev.buildersFileViewAsTree` (default true) backs the toggle and is honoured by the provider's `fileChildren` render branch. Tree-mode mechanics: - `file-path-tree.ts` builds a nested FilePathNode[] from the flat BuilderFileChange[]; compacts any folder whose lone child is also a folder. Folders sort before files at each level; case- insensitive alphabetical within each group. - `BuilderFolderTreeItem` is a collapsible row with the native Folder ThemeIcon and a stable id (`<builderId>::folder::<fullPath>`), Expanded by default so the tree opens out on first render. The stable id persists user expand/collapse across the 60s overview-poll refreshes. - `BuildersProvider.getChildren` learns a folder-dispatch branch: expanding a folder row materialises its children (each either another folder or a leaf BuilderFileTreeItem). - `BuilderFileTreeItem` itself is unchanged — the leaf shape and click action are identical in both modes; only the grouping around the leaves differs. Wiring follows the accordion precedent at extension.ts: read setting → set context key → listen for config change → flip key + call `buildersProvider.refresh()` so the tree redraws in the new mode. A new public `refresh()` method on BuildersProvider exposes the change-emitter for this purpose. 10 new test cases in `file-path-tree.test.ts` cover empty input, flat files, deep single-file compaction, shared-prefix grouping, diverging branches (independent compaction), mixed folder/file ordering at one level, renames (oldPath survives), folder-with- single-file-child staying uncompacted, case-insensitive sorting, and a realistic monorepo PR shape end-to-end. vscode tests: 67 → 77 passing.
…json is honored The dev-launch path (codev.runWorkspaceDev / codev.runWorktreeDev) was reading worktree.devCommand directly from .codev/config.json via fs.readFileSync, bypassing the 5-layer config merge. Result: if the devCommand was set in .codev/config.local.json (the per-engineer override layer) but not in the committed config.json, clicking 'Start Dev Server' surfaced 'Configure worktree.devCommand …' even though the resolved config had it. Switches dev-shared.ts to loadWorktreeConfig(), which calls Tower's GET /api/worktree-config — the same source the Workspace tree view uses for the Start row's visibility/tooltip. The launch path and the row visibility now agree on what 'configured' means.
After the accordion fires (a user expanded a different builder, so the current one collapsed via `workbench.actions.treeView.codev. builders.collapseAll`), the subsequent reveal at the new builder used `expand: true` — which expands only the target row, not its descendants. VSCode had just persisted "collapsed" against each folder's stable id during the collapseAll step, so re-expanding the builder restored the persisted state for every child level and the file tree appeared closed. VSCode's TreeView.reveal clamps `expand` at 3 in mainThreadTreeViews.ts (`Math.min(expand, 3)`); `true` maps to 1. Switching `expand: true` → `expand: 3` asks for the API ceiling — the re-expansion cascades through the descendant folders, restoring the default "folders expanded" look that fresh-rendered BuilderFolderTreeItems carry via TreeItemCollapsibleState.Expanded. With single-child folder compaction (from the SCM-style toggle that just landed), the practical worktree depth fits comfortably in 3 levels — e.g. `packages/codev/src/commands/consult/index.ts` compacts to builder → `packages` → `codev/src/commands/consult` → file leaves, exactly 3 levels below the builder row. Trees with 4+ distinct branching folder levels after compaction would have their deepest level remain collapsed (VSCode's hard cap, not addressable via a single reveal call); rare in the codebases this surface targets, and the user can still click into them manually. If it ever bites, the workaround is iterating reveal() over the builder's descendant folders to compound 3-level windows — left as follow-up work.
Four user-facing features and four bug fixes covering the 12-commit batch on this branch: What's new — `worktree.devUrls` config-driven "Open Dev URL" rows in the Workspace view; per-engineer `.codev/config.local.json` overrides layered on top of the shared config; live-refresh of the sidebar on config edits (driven via Tower so multiple windows stay in sync); title-bar toggle on the Builders view to switch the changed-files list between a folder tree (default, SCM-style) and a flat list. Bug fixes — Workspace view now detects dev servers started from the builder right-click context menu (not just its own row); the "Start Dev Server" row is hidden on workspaces without a `worktree.devCommand`; `config.local.json` overrides now apply when running the dev command, not only to the sidebar display; re-expanding a builder after the accordion's auto-collapse now restores its folder tree, not just the top row.
waleedkadous
approved these changes
May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A batch of VSCode-extension changes centred on the Workspace view's dev-server workflow, plus a Builders-view list/tree toggle mirroring VSCode's native Source Control panel.
What's new
Workspace view live-refreshes on config edits. Editing `.codev/config.json` or `.codev/config.local.json` re-renders every open VSCode window's sidebar immediately; no reload needed. Driven via Tower so multiple windows stay in sync.
"Open Dev URL" rows in the Workspace view. Add an array of `{ label, url }` entries under `worktree.devUrls` in `.codev/config.json` to surface dev/staging/preview links as one-click rows in the Workspace view. Clicking opens the URL in the user's default browser. Distinct from "Open Web Interface", which always points at Tower's dashboard.
Bug fixes
Verification
Branch notes
Branch is `feat/dev-workflow-and-vscode-updates-3`. 12 commits — most are extension-side; a couple touch the codev package (the new `/api/worktree-config` endpoint + SSE event that backs the live-refresh, and the layered config-load that backs the `.local.json` override).