Restore the ability for an embedding host to mount individual parts of
the new /workspaces UX (list sidebar, terminal/home/empty surface,
per-item right detail sidebar, empty placeholder) without the
surrounding app chrome, and add a generic project/worktree registry
the First Run Panel and Project Card surfaces depend on. The
standalone /workspaces page is unchanged; embed hosts opt in by
routing to a /workspaces/embed/... path.
Workspace embed surfaces:
- /workspaces/embed/list mounts the workspace list sidebar.
- /workspaces/embed/terminal[/:workspaceId] mounts the terminal
surface with two new WorkspaceTerminalView props
(hideWorkspaceList, hideRightSidebar) that skip the list column,
the right detail sidebar, and the segmented PR/Reviews/Issue
toggle that drives it.
- /workspaces/embed/detail/:itemType/:platformHost/:owner/:name/:number
mounts the per-item WorkspaceRightSidebar with optional branch and
tab query params.
- /workspaces/embed/empty/:reason mounts a small placeholder for
noSelection / noRepo / noWorkspace.
- /workspaces/embed/first-run mounts the WorkspaceFirstRunPanel with
add-existing, clone, and connect-github actions plus a tooling
status block.
- /workspaces/embed/project/:project_id mounts the
WorkspaceProjectCard with a new-worktree CTA scoped to the
project_id and the project's registered worktrees.
Project/worktree registry (migration 000017):
- middleman_projects records a local repository checkout middleman
knows about. Identity (host/owner/name), when present, lives in
middleman_repos and is joined in via the new repo_id FK; projects
with no parseable remote have repo_id NULL. ON DELETE SET NULL on
the FK keeps the on-disk checkout as the source of truth for the
project record - unsyncing a repo turns the project local-only
rather than stranding or deleting it.
- middleman_project_worktrees records worktrees the caller already
created on disk; middleman just persists the metadata. Cascade-
deletes with the project.
- ID generation produces prefixed opaque slugs (prj_/wtr_) so
callers cannot conflate the two record kinds.
- internal/projects/identity.go runs 'git remote get-url origin' and
parses HTTPS, SCP-style, and ssh:// URLs into a host/owner/name
triple. Unparseable, missing, or non-git remotes return
(nil, nil) - unknown identity is allowed and never blocks
registration.
- POST /projects resolves identity (caller-provided wins, otherwise
derived from the path's git origin), upserts a middleman_repos row
via db.UpsertRepo to give the project a stable FK target, and
stores the FK. UpsertRepo is pure DDL (INSERT ON CONFLICT DO
NOTHING + SELECT id) and does NOT subscribe the repo to sync;
sync subscription remains driven by config.toml and the AddRepo
settings handler. The middleman_repos row exists solely as a
non-drifting identity target.
- Six Huma operations under /api/v1 with neutral operation IDs:
register-project, list-projects, get-project, register-worktree,
list-worktrees, list-launch-targets. Routes are keyed on a
server-assigned project_id rather than host/owner/name, so the
no-remote path has a stable key.
- list-launch-targets resolves fresh on every request from config
agents and PATH lookup, so it works whether or not the workspace
runtime manager has been initialized.
Embed-config contract additions (forward-compatible):
- actions.project[]: a separate ProjectActionDef registry whose
handler must return CommandResult. The existing pull-request and
issue registries keep their void/Promise<void> shape; project-
scoped surfaces use the ack-required shape so the firing surface
can render in-flight, success, and failure states.
- embed.tooling: a ToolingStatus block describing git and gh
availability and gh authentication state. Pushed by the embedder
via a new __middleman_update_tooling bridge function.
- invokeProjectAction: an ack-aware runner that awaits the project
action's result, normalizes thrown errors and async rejections
into { ok: false, message }, and returns ok: true for
void-returning handlers so legacy-shaped callers do not regress.
Settings cleanup:
- RepoSettings.svelte previously short-circuited add, remove, and
refresh actions whenever the SPA was running inside an embedder,
which made the buttons appear interactive but do nothing. The
underlying settings API endpoints work the same way standalone or
embedded, so the short-circuits were not protecting any real
invariant. Removing them lets an embedded user manage repo sync
targets with the same UI a standalone user sees.
Test coverage that pins the embed contract so middleman refactors do
not silently break embedders:
- WorkspaceEmbedEmptyState.test.ts: each of the three reason
variants (noSelection / noRepo / noWorkspace) round-trips its
message, so a rename of the EmbedEmptyReason enum without
updating the component fails the build.
- WorkspaceTerminalViewEmbed.test.ts: the new hideWorkspaceList and
hideRightSidebar props are pinned by four cases (default-on/off
for each), so a refactor of the CollapsibleResizableSidebar
wrapping or the segmented PR/Reviews button group cannot regress
without failing the test.
- router.test.ts: every embed-workspace route is asserted to fire
onNavigate with type "workspaces", and the
__middleman_navigate_to_route window bridge is verified to exist
and to drive the SPA route - both are imperative-call surfaces an
embedding host depends on.
- queries_projects_test.go: includes a TestCreateProjectFKSetNullOnRepoDelete
case that pins the ON DELETE SET NULL semantic on the repo FK -
deleting the linked middleman_repos row clears the project's
identity but does not delete the project record.
- projects_handlers_test.go: error paths the embedder can hit are
pinned end-to-end - 422 from Huma's schema validator on missing
required fields, 400 from the handler's TrimSpace check on
whitespace-only fields, 404 when registering a worktree against
a nonexistent project, 400 when local_path is a regular file,
and a contract test that list-projects emits [] (not null) when
empty so embedders can iterate the response safely. The
W1-Slice-A gate test asserts platform_identity is built from the
joined middleman_repos row by re-fetching the project.
- TestRegisterProject_DoesNotSubscribeRepoToSync pins the
load-bearing invariant that registering a project does NOT
subscribe the linked repo to sync. UpsertRepo writes the
middleman_repos FK target, but the syncer's tracked-repos set is
driven exclusively by the user's TOML config via SetRepos. If a
future refactor accidentally couples the two paths, an embedder
could quietly grow the sync set just by registering a project;
this test fails first.
Reason: the prior workspace embedding API was removed in the
issue-backed workspaces rework. Hosts that compose their own layout
around the workspace pieces had no way to render just one component,
so the new top-level Workspaces page rendered inside their custom
panels and produced double chrome. Re-exposing the new components
via dedicated embed routes keeps the standalone UX as the single
implementation while letting hosts pick the slice they need.
The registry uses an FK linkage to middleman_repos rather than
duplicating platform identity columns, so a (host, owner, name)
triple has exactly one source of truth across synced repos and
registered projects. The host TEXT NOT NULL DEFAULT 'local' column,
the all-or-nothing CHECK constraint, and the casefold triggers the
initial registry shipped have been dropped - the FK target already
enforces casefold and uniqueness on its row, and host was
speculative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
/workspaces/embed/*routes that mount individual pieces of the workspaces UX (list, terminal, per-item detail sidebar, empty placeholder, first-run panel, project card) without the surrounding app chrome. Standalone/workspacesis unchanged.WorkspaceTerminalViewprops,hideWorkspaceListandhideRightSidebar, that an embedder can pass to skip the workspace list column, the right detail sidebar, and the segmented Diff/Issue/PR/Reviews button group.000017) backing six Huma endpoints under/api/v1:register-project,list-projects,get-project,register-worktree,list-worktrees,list-launch-targets. Project identity links tomiddleman_reposvia FK; registering a project does not subscribe its repo to sync.embed.tooling(git/gh availability and gh auth state pushed via__middleman_update_tooling) and a separateactions.project[]registry whose handler returnsCommandResultso project-scoped surfaces can render in-flight, success, and failure states.RepoSettingsadd/remove/refresh actions no longer short-circuit when running inside an embedder; the underlying settings API works the same way embedded or standalone.The standalone
/workspacespage is unchanged. Embedders opt in by routing to a/workspaces/embed/...path.🤖 Generated with Claude Code