feat(workspace): show memory and skill-sync state in the sidebar
Issue for this PR
Part of #1272 — the discoverability half.
Was stacked on #1278; retargeted to main after it merged. The branch was
rebuilt as one commit carrying only the sidebar change and its supporting
fixes (the stacked history had #1278's commits interleaved), and rebased over
#1274's clickable workspace names in the same file.
Type of change
- New feature
What does this PR do?
The workspace pane names the workspace but says nothing about whether local state
has drifted from it. So there is no moment at which a user learns they have memory
the workspace never received, or skills that have not synced this session — which
means /workspace sync has no discoverability problem to solve, because nothing
ever suggests running it.
Two lines under the existing name and manage URL:
Workspace
Growth (id 42)
12 memories · 3 not synced
skills synced 6m ago
https://…/w/42
Both are status, not affordances. The pane takes no input and none of the five
sidebar plugins does, so nothing here becomes clickable.
The poller's network use is bounded, not banned. The sidebar refreshes every
30s. Manage.status(dir, { poll: true }) resolves the workspace's memory setting
through memoryEnabledForPoller: one scoped memo per (tenant, API URL, workspace)
that asks at most once per five minutes on a "no" and never once it is "yes",
coalesces overlapping misses, and is the poller's only cache — it does not read
the write path's bare-id positive, which reopened a cross-tenant window. The
/workspace menu is the opposite: awaited before the dialog can open, so it reads
the setting from cache alone and reports unknown as null. A tick that lands
while a refresh is still out is dropped rather than queued, and the sidebar hands
the binding it resolved to status instead of resolving twice — so an outage
costs one bounded refresh per tick, not back-to-back requests.
Unknown is reported as unknown in both places, deliberately. Treating unknown
enablement as enabled shows a backlog on a workspace that has memory off; treating
it as disabled hides a real one. Likewise skillsSyncedAt is null unless a clean
sync wrote a .synced-at marker beside a manifest for this binding — a partial
run, a removed snapshot, a malformed marker, or the previous workspace's snapshot
all read as null, and the line is hidden rather than rendered as "never synced".
The tile hears about a link, unlink, rebind or rename made in this process through
onBindingChanged without waiting for the poll. It is notified only when the
cache on disk actually changed: notifying on a failed write too made a hot loop
with a read-only state directory (Ralph's reproduction), so that case now costs
one poll interval of staleness instead.
How did you verify your code works?
547 pass across test/altimate/workspace and test/altimate/plugin on the
retargeted head; typecheck clean. Every guard has a test that fails without it —
the poller drip, the account-switch memo, the coalesced miss, the marker's
binding check, the hot loop, the double resolve — mutation-checked in the review
rounds that asked for each.
Screenshots / recordings
Text-only sidebar lines; the shape is in the code block above.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
Known gaps
- The rendering itself has no test — the sidebar is a Solid component with no
existing harness in this repo, so coverage stops at the datastatus()returns. - Collides with #1274, which rewrites the same render block to make the workspace
name a link. Whichever lands second takes the rebase.
🤖 Generated with Claude Code
Summary by cubic
Part of #1272: the workspace sidebar shows memory drift and skill-sync age so users can see when /workspace sync is needed. Sync feedback distinguishes refused memories, transport failures, and gated sweeps, so failed or skipped work is never reported as success.
Sidebar
- Shows local memory totals even when memory is disabled; unknown enablement omits the unsynced count.
- Resolves memory enablement at most once per five minutes per tenant, API URL, and workspace, coalesces overlapping refreshes, and retries an unreachable answer after one poll interval.
- Reads a
.synced-atmarker written only after clean runs and swapped with the manifest; missing, malformed, unreadable, or other-binding markers stay unknown, and unchanged runs stamp the snapshot they checked. - Resolves cold-cache bindings, avoids false "Not linked" states during transient errors, and refreshes after binding changes, renames, and account switches. A scope that moves mid-resolve clears the tile and drops that outcome; an unreadable one skips the pending commit but keeps the tile.
- Failed cache writes don't notify, so a read-only state directory costs one poll interval instead of a hot loop; in-flight refreshes queue only for binding-change notifications.
Sync toast
- Reports refused memories separately from failed ones and claims "all" only when nothing failed.
- Names why a sweep was gated: the feature flag, missing binding, disabled memory, or a failed local read.
- Uses warnings for failed local reads and informational toasts for other gated sweeps instead of green success.
Summary by CodeRabbit
-
New Features
- Workspace sidebar shows clearer linked, unlinked, loading, and unavailable states.
- Workspace status includes memory counts and the age of the last successful skills sync.
- Binding and account-scope changes refresh workspace details automatically.
-
Bug Fixes
- Prevented stale workspace and synchronization details after rebinding or account changes.
- Improved sync notifications with clearer success, informational, and warning outcomes.
- Reduced redundant status checks and refresh requests.
- Prevented timestamps from appearing for mismatched, incomplete, or unsuccessful workspace data.
- Preserved workspace identity during synchronization and account transitions.
- Kept existing sidebar state when credentials are temporarily unavailable.