Key label rows by the verbatim name: item-side `labels text[]` stores whatever
was typed, so trimming the key colored a different label — `" release "` wrote a
row for `"release"` and the vocabulary then listed both. The trim now only
answers whether the name is blank.
Cache labels per workspace with a generation guard, so a response that lost the
race no longer replaces a newer workspace's entry, and thread the operating
workspace through `LabelsInput` instead of reading `$workspaceStore`: a session
editor works on a fork while the sidebar still browses the origin, and a color
must be written where the item lives.
Also: carry `label_settings` into a forked workspace, bound the name length
server-side, refetch the vocabulary on every suggestion-list open, and drop the
banned `$bindable(false)` on an optional prop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNUCuRqmzNukjR7FDewN7W
Labels were bare strings with nowhere to hang metadata, so a color had no
home. Add a sparse `label_settings` registry keyed by (workspace, name): a
label only gets a row once someone picks a color, and one without a row stays
an ordinary label rendered in the blue it has always had.
`labels/list` now returns `{name, color?}` and unions the registry into the
vocabulary, so a label whose last item was deleted keeps its color, and labels
that live only on jobs or triggers become colorable without widening the
seven-table union. `labels/update` upserts, and a null color drops the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QNUCuRqmzNukjR7FDewN7W