feat(workspace): identity in the prompt, and a /workspace menu for refresh, sync and unlink

Merged
SA
1mo ago
+2236-89

Issue for this PR

Closes #1269
Closes #1270
Closes #1272
Closes #1273

Type of change

  • Bug fix
  • New feature

What does this PR do?

Four workspace follow-ups from field testing, in four commits.

1. The agent could not say which workspace it was linked to (#1269). Not a
dropped field — identity had been folded into the routing directive in
awareness.ts, which is correctly silent when nothing is being routed. So a
workspace with no integrations put nothing in the system prompt, not even its own
name. bindingSection now renders identity; routingSection keeps its contract
byte-for-byte. A Record over the disabledReason union decides which states may
name a binding, so a new reason is a compile error rather than a silent choice.
The identity line is charged against MAX_SECTION_CHARS rather than added on top.

2. manage.ts — status, refresh, sync, unlink as one
transport-agnostic module. No TUI or CLI imports, no printing, directory and
session passed in. Two callers are in view: the slash command, and the IDE
extension, which runs this CLI headless via serve and will reach these over an
HTTP route rather than the tool catalog.

refresh pulls skills and memory. sync pushes, and is a repair rather than a
routine counterpart — blocks mirror as they are written, so a healthy project
sends nothing. It exists for the two states that strand blocks with no other
remedy: memory enabled after the bind (backfillOnBind is reached from one
place, the bind path, and nothing hooks the enable), and a mirror that failed and
is never retried.

unlink asks the server first, because the binding is re-read whenever the local
cache misses — clearing local state after a failed delete produces a project that
looks unlinked and silently re-links itself. It identifies the binding by what it
was recorded with rather than by re-detecting the project, which a unit test
caught: the server normalises remotes, and a repo whose remote was renamed
re-detects as something else.

3. The /workspace action menu. One palette entry rather than a command per
verb. /workspace <verb> is not expressible today: typed arguments route to
session.command, which renders a template into a prompt for the model, while
local execution is a palette command whose run() is nullary. Unlink is confirmed
before it runs — it is the only one of the three that re-running does not undo.

4. A gate mismatch found end-to-end. status counted unsynced blocks against
the pilot flag while backfill also refuses when the bound workspace has memory
off, so a workspace with memory disabled reported a backlog no action could clear.

How did you verify your code works?

Unit: 445 pass across test/altimate/workspace, 486 including the plugin suite,
1517 with test/session. Typecheck clean, no lint findings in new code.

Mutation-checked rather than trusted green. #1269: 9 mutations, 9 killed — two
survived the first pass and both were real holes (JSON.stringify was silently
providing the line-break protection, so the inertness test never exercised the
sanitiser's length bound). manage.ts: 6 mutations, 6 killed — clearing local
before the server call, cleaning up only on a 204, identifying by detection,
sending both identifiers, treating 404 as an error, and reporting sync as empty
rather than gated each fail a test.

End-to-end against a live local backend running the companion endpoint, on a
throwaway tenant with an isolated credentials path:

  • create binding → 201; the real client's unlink → removedServerSide: true,
    local cache cleared
  • server GET /by-remote afterwards → 404; the DB row is soft-deleted with
    created_at preserved
  • re-linking the same remote → 201, proving the partial unique index was freed
  • a second unlink → 404, the deliberate non-idempotent answer
  • status / refresh / sync all exercised against live data — which is how the
    gate mismatch in commit 4 surfaced

Screenshots / recordings

Not a visual change beyond a palette dialog.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Known gaps

  • The skill-snapshot purge on unlink is exercised end-to-end but has no unit test;
    a mutation aimed at it produced code that did not compile, so it proved nothing.
  • The commit-4 fix has no unit regression guard — the unit suite's temporary
    project has no memory blocks, so an assertion there would pass whatever the gate
    did.
  • Requires the companion DELETE /datamate-project-bindings/ on the backend.
    unlink is the only operation that needs it.

🤖 Generated with Claude Code


Summary by cubic

Fixes workspace follow-ups from field testing: the agent can now name the workspace it's linked to, and a new /workspace TUI menu offers refresh, sync, and unlink. Also aligns status with what sync will actually do, hardens unlink against races and symlinked purges, and makes the menu and toasts report only what really happened. Closes #1269, #1270, #1272, #1273.

  • The system prompt names the bound workspace and its stable id even when no integrations are routed; an unnamed workspace renders as "(unnamed)" and the identity line is charged against the existing section cap.
  • /workspace is a single palette entry backed by a new transport-agnostic manage.ts; unlink is confirmed before running because re-running cannot undo it, and the dialog header bounds the workspace name via a realm-neutral name utility.
  • status stays off the network: it reads only cached memory enablement (with a 5-minute "no" memo), trusts a cached binding row without revalidating, adopts server-side bindings only when no row exists, and reports "unknown" rather than a false zero.
  • status and sync share the same gate and the same definition of "deferred", and gatedBecause lets the toast say why a sweep never ran — memory off, missing binding, or build flag — instead of guessing.
  • unlink calls the server first with its scope pinned, propagates lookup failures without touching local state, removes only the row it started from (identified by workspace and link time), clears pre-canonical aliases, runs git detection only when no cached row exists, refuses a symlinked or otherwise unremovable skill purge, and warns when the workspace's skills were left on disk. A relink mid-delete survives, snapshot included, unless the server confirms the project is unbound — a cache-file scope change counts as a relink only when its row differs from the snapshot taken before the first await, while an unchanged foreign row is stale and is removed; the overlay resets regardless, and the guard judges the canonical row, not every alias.
  • The sync toast names refused, deferred, and gated counts; only blocks already present at their current payload read as "everything is already in the workspace."
  • Requires the companion DELETE /datamate-project-bindings/ endpoint on the backend.

Summary by CodeRabbit

  • New Features

    • Added a /workspace menu showing connection and memory-sync status.
    • Added actions to refresh resources, sync local memory, and unlink a project with confirmation.
    • Added success and error notifications for workspace management actions.
    • Workspace-aware responses now identify the linked workspace when applicable.
    • Added clearer reporting for deferred, declined, failed, and completed memory sync operations.
  • Bug Fixes

    • Improved handling of unlinked projects and missing connections.
    • Prevented memory synchronization when disabled or unbound.
    • Prevented unlinking from following symlinked workspace directories.