Repository navigation
feat(core): make worktree APIs project-based - #48867
Conversation
| create: (input) => worktrees.create(input, currentWorktreeStrategies), | ||
| refresh: (input) => worktrees.refresh(input, currentWorktreeStrategies).pipe(Effect.asVoid), | ||
| remove: (input) => worktrees.remove(input, currentWorktreeStrategies), | ||
| reload: worktreeStrategies.reload, |
There was a problem hiding this comment.
what is reload here
There was a problem hiding this comment.
reload() rebuilds the strategy registry by replaying the active ctx.worktree.transform(...) callbacks against fresh defaults. That recomputes the available strategies, selected default, and configured parent directory.
It is the existing State.reload() behavior, moved with the registration state. The config adapter also calls it when its loaded config entries change, so those transforms see the new values.
The distinction is:
ctx.worktree.reload() // replay registration/config transforms
ctx.worktree.refresh({ projectID }) // discover worktrees and update saved inventoryreload() does not rerun plugin setup or discover worktrees. It remains a plugin-only method; it is not an additional HTTP/SDK worktree endpoint.
Implementation: State.reload and the config adapter.
| @@ -1,28 +0,0 @@ | |||
| export * as WorktreeRefresh from "./refresh.js" | |||
There was a problem hiding this comment.
why is this deleted, where did this move to
There was a problem hiding this comment.
This file only scheduled a background refresh() whenever a location booted. The discovery/reconciliation implementation itself is still in Worktree.refresh.
I removed the location-boot trigger and replaced it with explicit project-based calls at the UI demand points:
- App inventory loader: first demand for a project starts discovery;
worktree.updatedcauses a saved-inventory re-read. - TUI worktree picker: opening it or pressing refresh runs discovery.
- The TUI Open dialog and app session-workspace menu also explicitly refresh when opened for a project.
The reason was to stop every location boot from independently discovering the same shared project. The behavior change is that simply loading a location no longer discovers external worktrees. Non-UI callers use worktree.refresh({ projectID }) explicitly; list stays a database read.
| }) | ||
|
|
||
| const ops = { | ||
| const inventory = (projectID: Project.ID) => ({ |
There was a problem hiding this comment.
I don't like this rename, change it back to ops and have these individual ops take a projectID if needed as an arg
There was a problem hiding this comment.
Agreed. The project-bound wrapper is unnecessary; this should remain a single ops object with explicit project arguments:
ops.list(projectID)
ops.find(projectID, directory)
ops.create(projectID, input, tx?)
ops.remove(projectID, directory, tx?)I introduced inventory(projectID) just to capture the ID for the SQL predicates. Passing it directly is simpler, preserves the existing naming, and avoids recreating the helper object and functions for each binding.
There was a problem hiding this comment.
Implemented in 519de27. The project-bound inventory(projectID) wrapper is gone; this is a single ops object again, with explicit arguments: ops.list(projectID), ops.find(projectID, directory), ops.create(projectID, input, tx?), and ops.remove(projectID, directory, tx?). Core typecheck and all 35 worktree tests pass.
| const sourceDirectory = yield* source(input.from) | ||
| const create = Effect.fn("Worktree.create")(function* (input: CreateInput, current?: WorktreeStrategies.Interface) { | ||
| const row = yield* project(input.projectID) | ||
| const settings = yield* load(row.worktree, current) |
There was a problem hiding this comment.
so because this awaits plugins we can't call this in a plugin setup right
do we currently have any patterns in opencode to detect this and throw a friendly error?
There was a problem hiding this comment.
Calling ctx.worktree.create(...) from plugin setup in the canonical checkout currently works because this PR has an explicit fast path:
// PluginHost passes its existing registry:
worktrees.create(input, currentWorktreeStrategies)
// load() returns before awaiting activation:
if (current?.directory === directory) return current.get()That uses registrations made so far; later plugins have not necessarily registered yet. There is a server test for this setup call.
Your concern applies when that fast path is bypassed—for example, calling the ordinary SDK/HTTP endpoint from setup—or when setup calls form a cycle between projects. Those can still wait on their own activation.
I checked the plugin loader, readiness service, and plugin bridge, and did not find a reusable guard that turns this into a friendly error. Currently awaitActivation is just ready.await; the latch is held closed during activation. The existing approach here is avoiding the same-runtime wait, rather than detecting and rejecting it.
A friendly-error guard would need to identify a call originating from the activation it is about to await. Checking only whether plugins are loading would also reject normal API callers that should legitimately wait.
There was a problem hiding this comment.
Updated in b051e91: remove now loads the canonical runtime and awaits plugin activation before resolving the worktree’s recorded strategy. It still ignores the currently selected default and returns StrategyUnavailableError when the recorded owner did not register. I also updated the HTTP description/docs and changed the server coverage to evict the canonical runtime first, then verify removal reloads the custom strategy.
80fbcf0 to
29fcfe7
Compare
Summary
First pass at making worktree management project-based. All four operations now require
projectIDinstead of a caller-selected location.worktree.updatedtells clients to list saved state again. Log individual discovery failures while continuing other sources.Implementation
Worktree.Serviceis now global and project-ID-based. The existing plugin registration state lives inWorktreeStrategies.Service: available implementations, selected default, directory settings, and transform lifecycle.Strategy registration remains scoped to the canonical checkout's plugin runtime. The SDK/plugin registration API stays
ctx.worktree.transform(editor => editor.add(strategy)). Canonical plugin setup can use registrations made so far without waiting on its own activation.Update the Promise/Effect clients, plugin bridge, app/TUI callers, inventory cache keys and invalidation, documentation, and OpenAPI artifacts. Refresh is triggered explicitly after listing a single project rather than by every location boot. The desktop all-projects page only lists saved inventory.
worktree.updatedevents re-list saved data.Validation
The broader SDK suite has two existing failures (plugin-backed web search and Promise event cancellation), also reproduced on a clean checkout of the base
v2commit. The dedicated worktree SDK test passes.