feat: workspace tags and tag-derived sidebar folders

Merged
KI
2mo ago
+13580-180

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_tags table (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:changed event snapshots. Tag replacement is transactional (delete-then-insert). MCP workspaces_update accepts a tags replacement set.

Desktop, read path

  • workspaceTagFolders module: 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, tag field with null default) so withReadHeal keeps 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 === null is 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's storageArea identity 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-targeting storageEventApi.

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) on ws create and ws update (--clear-tags empties the set), a --tag filter and TAGS column on ws list.

Automations

  • tags on 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; host createSession accepts tags now), a "tagged" chip in the editor's scope sentence (strings translated for fr/ja/ko/zh-CN), --tag on 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_tags table (migration 0026), tags on host create/update/list and workspace:changed snapshots; the MCP workspaces_update tool 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 via ws list --tag and a TAGS column; sessions reject --tag, and ws list validates 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 === null marks 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 automation folder 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.