feat: workspace tags and tag-derived sidebar folders
A sidebar folder is a tag. Folder existence and membership derive from host-side workspace tags, so any actor that can tag a workspace can file it: the CLI, an MCP agent, an automation, including on a machine that has never seen the tag. Local rows are presentation only (color, label, order, collapse) and appear only once someone customises a folder.
Design doc: plans/20260828-workspace-tags-design.md (steps 1 to 6; the doc distills the discarded workspace-tags branch so this could be built clean from main).
What's here
Shared (@superset/shared/workspace-tags)
- One definition of normalization (trim + lowercase), caps (32/workspace, 64 chars), and a router-boundary zod schema that rejects rather than drops invalid input, parsing to the normalized sorted set.
Host service
workspace_tagstable (migration 0026): plain strings, no tag entity,UNIQUE(workspace_id, tag), cascade delete.- Tags on create/update/list, on the update result, and on
workspace:changedevent snapshots. Tag replacement is transactional (delete-then-insert). MCPworkspaces_updateaccepts a tags replacement set.
Desktop, read path
workspaceTagFoldersmodule:deriveTagFolders(union of stored rows and tag-only folders) plus one membership resolver used by the sidebar builder, the top-level lane, and the flatten pass. The three passes deriving membership independently was the most expensive mistake in the discarded branch.- Persisted schemas widened (sectionId uuid to string,
tagfield with null default) sowithReadHealkeeps every pre-existing row.
Desktop, mutations (signatures unchanged, storage moved)
- Create mints a tag from the name (collisions get
-2); move/ungroup/delete write host tags through the optimistic path; rename retags members before rekeying the row; color/collapse/reorder materialize a presentation row on first interaction, never up front.
Migration of legacy folders
- Background pass converts uuid-keyed groups in place: visible members tagged on their host first, then the row swap and pointer clears.
tag === nullis itself the not-converted marker (idempotent, resumable). Offline host defers the folder; a rejected write parks it for the session.
Fixed along the way (found by CDP passes, not review)
- Move-to-group menus listed stored rows only, so agent-created folders could not be targeted. Both menus now share
useProjectTagFolderSections(the derived union). - DnD lane drops persisted through local-only writes, so dragging a row out of a folder snapped back. The lane branch now strips the project's folder tags on the host.
- Cross-window sync for every localStorage-backed collection was dead:
withQuotaGuard's storage wrapper broke@tanstack/db'sstorageAreaidentity check, so no window ever saw another window's writes (symptom: renaming a folder in one window left a ghost empty folder in the other). The guard now installs a re-targetingstorageEventApi.
Verification
- Unit: 39 tag-folder tests (derivation, resolver, minting, migration runner including absent-field fixtures per doc trap list), 14 shared, 7 host-service store tests; host-service suite 1347 pass; desktop suite has zero non-baseline failures; all typechecks clean; drizzle re-generate reports no schema changes.
- CDP against the live app: the doc's canonical check (tag two workspaces over the host service with no local row anywhere, folder appears holding both, untag and it disappears); live legacy-folder migration; a 9-phase edge matrix (multi-tag single-container, colon tags, materialize-on-interaction, menu move/ungroup, rename + collision, migration defer/resume, empty-folder row, pinned member); a trusted-input DnD battery (into/out/between folders, member reorder, folder-header drag, boundary oscillation with dwells, induced jank, autoscroll sweep); and a two-window matrix (tags plane, presentation plane, migration race, literal rename repro). Zero console errors across all passes.
CLI (step 7)
--tag(repeatable) onws createandws update(--clear-tagsempties the set), a--tagfilter and TAGS column onws list.
Automations
tagson the automation entity (jsonb, migration 0097): each run's created workspace carries the automation's tags, so scheduled runs file themselves into the matching folders. Threaded through create/update inputs, dispatch (both the project and session branches; hostcreateSessionaccepts tags now), a "tagged" chip in the editor's scope sentence (strings translated for fr/ja/ko/zh-CN),--tagon the automations CLI, the MCP tools, and the SDK types. A pinned per-run workspace is reused, never retagged.
Not in this PR
- Host-side presentation row (
workspace_tag_settings, doc §7), which makes rename a one-row update instead of O(members) retagging.
https://claude.ai/code/session_01Scu1EHDFD5QL6ryyxHCx72
Summary by cubic
Sidebar folders are now backed by host-side workspace tags: a folder exists because a workspace in the project carries the tag, and membership is the tag itself. Any actor that can tag a workspace — the CLI, MCP agents, automations — can now file one, even on machines that never saw the tag. Local rows hold only presentation state (color, label, order, collapse) and appear on first customization.
- Adds a
workspace_tagstable (migration 0026), tags on host create/update/list andworkspace:changedsnapshots; the MCPworkspaces_updatetool accepts a tags replacement set. - Derives folder existence and membership from one shared resolver used by the sidebar builder, top-level lane, and flatten pass.
- Mutations keep their signatures: creating a folder mints a tag (respecting the length cap), move/ungroup/rename write host tags through the optimistic path — rename only commits once the host accepts the write — and color/collapse/reorder materialize a local row only on first interaction.
- Move-to-group menus now list the derived folder union so agent-created folders can be targeted; DnD lane drops strip folder tags on the host; cross-window sync for localStorage-backed collections is restored with deduped listeners.
- The CLI tags workspaces at create/update (repeatable
--tag,--clear-tags) and filters or lists them viaws list --tagand a TAGS column; sessions reject--tag, andws listvalidates the filter. - Automations carry a tag set (migration 0098) that defaults to
["automation"]and is backfilled for existing automations, so each run's created workspace self-files into an "automation" folder unless the set is cleared in the editor; the picker suggests the project's existing tags, and a pinned per-run workspace is reused without retagging.
Migration
- Converts legacy uuid-keyed folders in place in a background pass;
tag === nullmarks not-yet-converted, so the pass is idempotent and resumable. It reserves derived tags so minting never collides and skips members whose workspace no longer exists. - Offline hosts defer the folder, and a rejected write parks it for the session.
Summary by CodeRabbit
-
New Features
- Added workspace tags for organizing sidebar folders across projects.
- Create, rename, move, delete, retag, filter, and list tagged workspaces.
- Added tag-based automation grouping, with an
automationfolder by default. - Added tag controls in the automation editor and CLI support for assigning, replacing, and clearing tags.
- Tag-based folders can appear without a manually stored sidebar row.
-
Bug Fixes
- Preserved tags when workspace updates omit tag information.
- Improved legacy folder membership and sidebar ordering.
-
Chores
- Legacy sidebar folders now migrate to tag-based folders in the background.