2 Commits
Author SHA1 Message Date
7c186016a5 feat(core): multica 1.0 part 1 (MUL-8317) (#9635)
* feat(core): add app space model for rail navigation

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): replace left sidebar with app rail and space panels

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(e2e): drive navigation through the app rail

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): finish the rail and panel shell against the mock

- The sidebar toggle always works: in Inbox, Chat and Settings it collapses
  the page's own list column instead of being disabled.
- Move create workspace from the rail into the account menu, and complete
  the menu: profile, theme and language submenus, shortcuts, docs, change
  log, Discord, desktop download, log out.
- Add an explicit New issue button to the Issues and My issues headers.
- Autopilot panel gets a create button and template shortcuts; Team panel
  gets an invite-member button for owners and admins.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(desktop): draw tabs as pills on the title bar

The active tab no longer merges into the content card: with the space panel
inside the card, the merged tab landed over whatever column happened to sit
below it. Every tab is now a pill on the title bar; the active one uses the
card's own surface, inactive ones a lighter tint.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): give list columns the space panel's width

Inbox and Chat defaulted to 260px with their own saved widths, Settings was a
fixed 240px, and the space panel used the sidebar width. They now share one
ListColumn sized by --sidebar-width with the panel's resize edge, so the
divider stays in place across spaces and one drag resizes them all.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): return each rail space to where you left it

Rail items always linked to the space's home, so switching spaces dropped
the project, squad, chat session or settings tab you had open. The shell
now remembers the last page (path and query) per space and workspace, and
the rail links there; the current space's item still leads home, so a second
click resets it.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): animate the list column when the sidebar toggles

Inbox, Chat and Settings unmounted their list column on toggle, so it
snapped away while the space panel slid. ListColumn now stays mounted and
eases its width like the panel (200ms, off under reduced motion and while
dragging), which also keeps the list's scroll position across a collapse.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(layout): one title band for every left column

The space panels used PanelHeader (title-sm, no rule), Inbox and Chat used
the page header (body size, bottom border) and Settings its own 64px title.
PanelHeader now takes a heading level, a leading slot and a meta slot, and
Inbox, Chat and Settings render their list title with it.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(layout): collapse the whole sidebar with one toggle across spaces

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): switch workspaces from one rail tile

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): preview Inbox, Chat and Settings lists on rail hover

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(inbox): keep the list column's own loading skeleton

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(desktop): lay out the shell for the page on screen during rail switches (MUL-8317)

The tab session URL moves before the router commits the next page (it renders
in a transition), so for a few frames the shell already showed the incoming
space's layout around the outgoing page: switching into Inbox or Chat dropped
the Work panel and the old page's header icon jumped to the top-left.

The router now publishes its committed pathname from a layout effect and the
shell's panel and list columns follow it; space memory keeps following the
navigation URL.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): flatten the settings nav into one level of section headings (MUL-8317)

Scope header rows, sub-group headings and dividers stacked three levels into
the 240px list column. Every section is now a peer with the space panels'
section label and spacing; each page already names its scope in a badge.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): expand projects inline in the work panel (MUL-8317)

Project rows in the Issues panel get a chevron that lists the project's open issues in place, so the sidebar can move between issues without opening the project page. The issue on screen reveals its project and stays lit; expansion persists per workspace. A dedicated open-issues cache follows realtime title patches and refetches only when status or project membership moves.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): sidebar tree settings, load more, expandable views and project order (MUL-8317)

- Expanded projects and views list issues from the server (updated date,
  finished statuses hidden) with Load more / Show less.
- One shared issue display popover on hovered expanded projects; a project
  display popover on the Projects header (ordering, statuses, only mine,
  counts); drag to reorder; collapsible section; no project cap.
- Issue rows shimmer and show agent avatars while an agent works or waits.
- Settings, order and expansion persist per workspace.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): show sidebar status filters as checkboxes (MUL-8317)

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(ui): auto-collapse the nav only below lg (MUL-8317)

The rail now stays on screen beside the space panel, so laptop-sized
windows (1024-1279px, just under the 1280 desktop default) have room for
the panel; they no longer open Issues with it collapsed.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): one Projects display popover, higher-contrast filter chips (MUL-8317)

Issue settings are shared by every project, so they move from each expanded
row into the Projects header popover beside the project settings. Status
filters go back to chips: a solid outline and tint when on, dashed and
faded when off.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(server): list views across scopes and store sidebar view order

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): starred views sidebar, unified view header and All views page

- Sidebar Views lists only starred views (built-in and saved, user-ordered)
  with an All views row; order lives in a per-user sidebar preference.
- Issues, My issues and project pages share one header: view name menu,
  folding filter chips (+N), assignee-type / relation chip, Reset and Save.
- Saved-view filters are fully editable; new views are created directly
  and renamed inline instead of through the save dialog.
- New All views directory with tabs, search and star toggles.
- Project page drops the breadcrumb and hides the redundant project chip.
- Remove the view bar, save/manage dialogs and the old headers.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(inbox): switch between inbox and archive with header tabs

Replace the Archived row that trailed the notification list (and the back
row inside the archive) with Inbox | Archived tabs pinned under the list
header. The unread count moves onto the Inbox tab, and an empty archive no
longer says the inbox is empty.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* Revert "feat(inbox): switch between inbox and archive with header tabs"

This reverts commit e8a8e1bcea1bf5b45c4e5830d654523a0b77daff.

Co-authored-by: multica-agent <[email protected]>

* fix(inbox): drop the count beside the no-agents-working filter

The count widened the whole filter menu for a toggle whose result is
visible in the list as soon as it is applied.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(inbox): switch to the archive from the page title

The Archived entry trailed the notification list and scrolled away with
it. The list title is now a menu (Inbox / Archived), so the switch stays
in reach without adding a row, and the archive's back row is gone. An
empty archive no longer says the inbox is empty.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): keep a pinned issue lit after its URL turns canonical

The issue route replaces /issues/<uuid> with /issues/MUL-123, but a pin
only matched its own UUID href, so clicking a pinned issue lit the same
issue under its project instead. Issue pins now match the open issue by
id, a lit pin no longer auto-expands its project, and pinned issues show
the running agent the same way tree rows do.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): line pinned agent avatars up with the tree rows

The avatar sat inside the gap reserved for the unpin button, so pinned
avatars landed left of the project tree's and the label lost room. A
pinned issue with agents now puts the avatar in the row's right edge and
hands that slot to the unpin button on hover.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): raise the contrast of the working-agent shimmer

ShimmerText now reads --shimmer-text-base, --shimmer-text-highlight and --shimmer-text-spread, defaulting to the previous values. Sidebar issue rows (tree and Pinned) rest on faint-foreground and use a 22% highlight band so the sweep is visible at sidebar size; chat, the issue page and other shimmer hosts are unchanged.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): keep the view header from overlapping on narrow widths

The view-name button kept the Button base shrink-0, so once the h1 hit its min width the name painted over the add-filter trigger (visible on a 390px web viewport). The button now shrinks and truncates, the add-filter label becomes screen-reader-only in a narrow header, and FoldingFilterChips re-measures its reserved width when its leading or trailing controls resize instead of only when the chips change.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): one Filter dropdown and a header that adapts to its width

The view header no longer lays filters out as chips with a +N overflow. A single Filter button carries the active count and opens the filter menu, which now lists the dimensions already filtering first, each showing its values, with the scope axis (assignee type, relation to me) at the top. The split is taken when the menu opens so a row never moves mid-edit.

The header measures itself: when the title side runs out of room it first folds the display controls to icons, then the agents chip and New issue, before the title truncates, and only releases a step once the row is wider than the width that forced it. This fixes the overlap in saved views with table controls and after toggling agents working.

FoldingFilterChips and single-row-fit had no other users and are removed.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): keep the highlight on the row that was clicked

An issue that is both pinned and listed in the tree always lit its pin, so clicking it under a project moved the highlight away from the row just clicked. The work panel now remembers which section a click on an issue link came from: a click in the tree keeps the tree row lit and the pin unlit, a click on the pin keeps the pin. Arriving any other way (search, a link, a shortcut) still lights the pin.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(sidebar): let Views and Pinned collapse like Projects

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): keep a dragged row at its own height while reordering (MUL-8354) (#9662)

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(sidebar): create an issue in a project from its row

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): put the workflow beside the title and set the panel toggle apart

The project workflow now sits on the left with the title and Filter, in
the same borderless style. New issue matches Display's height, and a
divider separates the right-sidebar toggle from the header actions.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): fold the layout switcher into the Display panel

One Display button whose icon follows the layout. The panel opens with
the layouts as a segmented row, then grouping, ordering and sub-issues,
then the current layout's own options. The table's grouping and column
pickers move in from the header.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): keep hovered Display chips from looking selected

Light mode's muted and accent are the same grey, so a hovered chip
matched a pressed one. Hover now only darkens the text and border, and a
pressed chip fills with the stronger input tone.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(sidebar): give the Projects settings the Display panel's controls

Both panels now share one chip and one sort-direction button. The
sidebar popover drops its dividers and dashed off-chips, takes the same
section headings, and its selects match the Display panel's width.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): quiet the issue view header down to its title

The header carried six pieces of text, half of which said nothing: the
project's workflow name, "0 agents working" and "Display".

- Workflow moves into the project title menu as a submenu; the properties
  panel keeps its row.
- Filter becomes an icon beside the title, followed by Reset / Save, so the
  count sits next to the actions it causes. Its tooltip names the active
  filter fields.
- Display and New issue become ghost icon buttons named by tooltips, with
  New issue showing its C shortcut.
- The agents chip shows only while someone is working or its filter is on,
  as avatars and a number.

With every control already an icon, the crowding steps are gone.

MUL-8361

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): is / is not per filter, Relation to me, and a hover summary (MUL-8353) (#9663)

* feat(issues): is / is not per filter, a Relation to me filter, and a hover summary (MUL-8353)

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): match the Relation filter's Chinese labels to My issues (MUL-8353)

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): advanced filter mode, menu search, and due date filter

Advanced mode turns the simple filters into a rule tree (match all/any,
is/is not per condition, one level of groups) edited in a panel behind
the same Filter trigger. Switching back is offered when the tree is
something simple mode can say.

The filter menu gets a search box that matches fields and values; a
value hit reads as the condition it adds. Due date joins as a dimension
with relative buckets (overdue, today, this week, next 7 days, none).

The issue table query accepts filters.advanced, filters.due_dates and
filters.today, compiled to SQL with the same IS NOT TRUE negation.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(e2e): menu search, due date and advanced filter

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Cursor <[email protected]>

* test(e2e): let menus settle before filter screenshots

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): advanced filter keeps status columns, property filters and negated totals

- Columns follow a status the rule tree requires, and a status it names
  un-hides its column, so switching to advanced no longer drops rows.
- Status branch totals drop the excluded keys for "is not" instead of
  falling back to the loaded count.
- The advanced panel shows property filters as editable rows with an
  "Add property filter" entry, and Clear all clears them too.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): make custom properties ordinary advanced filter conditions

Properties are fields in the advanced tree: they sit in the field picker
after the built-in fields, can live in groups and follow all/any, and
edit their values with the property's own options. The server compiles a
property:<id> condition with the existing property predicate. Entering
advanced mode moves property filters into the tree, so the separate
property section and its Add property filter button are gone.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): typed filter operators and direct condition groups

- Add condition group inserts an empty group and opens its own add menu,
  instead of asking for a field first. The group's header carries its
  match ("Match any of these"); rows only show the joining word.
- A row's menu can turn it into a group, and a tree with groups reads
  back as one sentence under the rows.
- Operators follow the field's type: is / is not, includes any / none of
  for labels and multi-value properties, contains for text, comparisons
  for numbers, before / after for dates, and is empty / is not empty for
  anything that can be unset. Text, number and date values are typed in
  the row. Every operator is a reading of the stored negated + values, so
  saved views and the server query are unchanged.
- Actor value headings sit in a menu group, which Base UI requires.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(layout): New issue and search at the far end of the desktop title bar

New issue lived only as a pen in the Issues panel header, so Inbox, Team,
Chat and Auto had no way to start an issue but the C shortcut. Search sat
in the rail between the workspace tile and the spaces.

- Desktop: New issue and search are icon buttons at the right end of the
  title bar, styled like Back/Forward, with search last. The tabs give way
  to them. The rail drops search.
- Web has no title bar, so its rail keeps both under the workspace tile,
  New issue first.
- The button does what C does: a project page files the issue into that
  project. Both now share one helper.
- The Issues panel header loses its pen; the page header keeps New issue.

MUL-8359

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): remove horizontal scrollbar from the filter menu

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(sidebar): every view expands in the sidebar

Saved views preview through the table query, which expresses every view filter (is not, relation, due date, rule trees, my-any). Built-in views expand too, scoped like their page and listed with the sidebar issue settings.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(sidebar): group expanded rows by spacing, folder icon for projects

Issues under an expanded row drop to 26px rows with no gap and a 12px status
icon, an open group ends with 8px of space, and the expanded row's name goes
full-strength. Sections sit 20px apart. Sidebar projects show a folder icon
instead of their emoji.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): expandable rows give their label the full width

The hidden chevron, an empty spacer and their gaps reserved about 90px at the
end of every expandable row, so names truncated with space to spare. The
chevron now takes the icon's place on hover.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): tree row chevron swaps back to the icon immediately

Drop the opacity fades, and show the chevron for keyboard focus only, so a
clicked row no longer keeps it until focus moves.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): drop the hint beside Switch to advanced filter

The grey text sat where the menu shows current values, so it read as a
setting rather than an explanation.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(layout): tighten the gap under panel titles, keep selected rows at regular weight

The first section kept the 20px that separates sections, so it sat far
below the panel title. Selected and expanded rows, desktop tabs, the
Settings nav, agent tabs and the skills file tree no longer switch to
medium weight: the wider glyphs made the label grow when it was clicked.
Selection reads from the background and text colour.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(sidebar): keep stars while loading, leave view state to in-place clicks, page past 100

- Star, unstar and reorder write the stored order, not the resolved items,
  and wait until that order is known, so views still loading are kept.
- Sidebar rows switch the active view only when the click navigates this
  page; a new tab takes its target from the URL.
- Sidebar issue lists walk 100-row pages so Load more keeps growing.
- Drop a trailing blank line in advanced-filter-panel.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): open the clicked view in desktop tabs, prepare view state only on in-place navigation

- Desktop tabs carry their own ?view=: list pages adopt it on mount and on
  back/forward, write view changes back with replace, and the active-view
  map is swapped with the active tab. A saved view is part of tab identity,
  so cmd+clicking view B from a tab on view A opens B in a new tab.
- AppLink and useRowLink call onNavigateInPlace right before they push, so
  sidebar and All views rows prepare view state exactly when the page
  navigates in place (desktop shift-click included, web shift-click not).
- Rail items and the workspace tile use named radius tokens.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): one page system for lists and objects

Every page now follows the Issues page: one 48px header, nothing stacked
under it.

- Lists entered from the sidebar (agents, squads, skills, runtimes,
  projects, views, autopilots, issue wakeups) title themselves with an
  icon and a scope menu, and keep only icon tools: filter with its count
  and Reset, display, and "+". Counts left the header; list search is
  removed.
- Objects (agent, squad, skill, runtime, autopilot, member) open under a
  breadcrumb with at most one solid action and a ⋯ menu. Their tabs became
  body sections with actions on the section titles, in a 760px column,
  with a 300px properties panel the header toggles.
- Analytics keeps its tabs, now in the header; settings, chat and the
  create-agent flow share the same header.
- Shared CollectionPage* and ObjectPage* primitives in views/layout.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): one CollectionList for every list page

Replace the per-page tables and ListGrid with a shared CollectionList in
packages/views/layout: single-line 36px rows with inline grey
descriptions, a shared column/grid template, header sort, row selection
with shift-range, keyboard navigation (arrows, J/K, Enter, X, Cmd+A,
Esc), right-click and kebab row menus, load-more, and a shared batch bar.

- createCollectionViewStore factory in core; display menus are generated
  from the column definitions.
- Agents, Squads, Skills, Projects, Autopilots, Runtimes (page and
  RuntimeList), Views and Wakeups migrate to it; Wakeups pages with
  load-more instead of prev/next.
- The Issues batch toolbar uses the shared CollectionBatchBar.
- Agents hides owner and access by default so the default columns fit at
  1280px; persisted v1 defaults migrate, custom choices are kept.
- Remove ListGrid and the now-unused per-page locale keys.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(layout): smooth the nav panel toggle

- Ease the panel and list columns with a soft-start curve (220ms ease)
  instead of ease-out, whose first frame jumped a quarter of the width
  and, when that frame dropped, nearly half of it.
- End-align the panel content and clip it while it grows, so it slides
  in with the edge instead of spilling under the page; the resize handle
  moves to a zero-width sibling so the clip does not cut it.
- The floating chat stays mounted while closed and re-rendered on every
  frame of the toggle as its container resized; it now re-renders only
  when the clamped window size changes.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): give My agents' work its own URL

My agents' work shared /my-issues with My issues and was told apart only by
a persisted global scope, so it could not open in its own tab, and switching
one tab flipped every other. It now lives at /my-issues?scope=agents: the
page, header and sidebar read the scope from the URL, desktop tab dedupe
keys on it, and the tab gets its own title and icon.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): preserve My agents' work in copied links

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-31 15:26:51 +08:00
Naiyuan QingandClaude Fable 5 984a2c2bff MUL-5954: feat: saved issue views V1 (MUL-4796) (#6516)
* feat(issues): server-backed saved issue views API (MUL-4796)

issue_view table (migrations 262-264: table + two CONCURRENTLY partial
indexes) stores named filter definitions: query jsonb is the shared
identity, display jsonb only seeds a user's first open. Scope model is
workspace / my (with scope_variant, forced private) / project (validated
scope_id, deleted with the project in the same app transaction).

issue_view_preference (migration 265) holds each user's view-bar layout
(hidden + order doc) per surface container, last-write-wins.

CRUD + GET/PUT preference endpoints are gated on the saved_issue_views_v1
feature flag (404 while off, key published to the frontend config).
Authorization: private views 404 for everyone but the owner; shared views
editable by owner or workspace admin; updates use expected_revision -> 409.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(core): saved issue views client core (MUL-4796)

- zod schemas + parseWithFallback for views and preferences (definition
  blobs stay loose records interpreted per definition_version), with
  malformed-response tests per the API-compat rules
- issue-views module: list/create/update/delete + preference queries with
  wsId-scoped keys; preference upsert is optimistic (toggle-grade rollback)
- active-view store (zustand client state, URL is the durable carrier) and
  useActiveIssueView with missing-view detection
- baseline.ts: enum-sanitized FilterSnapshot of a view's query for
  value-level locking, delta chips, and baseline-aware resets
- view-store: clearFilterDimension + resetFiltersTo actions; surface store
  seeding routes server blobs through mergeViewStatePersisted

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issues): three-bar priority icons with a distinct urgent badge

low/medium/high share one three-bar frame (fill level = severity, ghost
bars at 35%); urgent breaks out of the scale as a filled badge with an
exclamation cutout — it is an interrupt, not a fourth bar. All glyphs
share the same 2-14 optical frame.

Board card: PickerWrapper's bare block div gave its inline-flex trigger a
line box (line-height 24px > 20px button), floating icons above the text
midline — the wrappers are flex containers now, and the priority trigger
gets a constant size-5 box so the row's rhythm doesn't depend on the glyph.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(views): saved views UI — view bar, save/edit dialog, locked view mode (MUL-4796)

View bar replaces the built-in tab row on Issues and My Issues: built-in
tabs and saved views render as one flat, per-user ordered row that wraps
instead of overflowing, with a menu for new-view / manage. The manage
dialog does drag reorder (translate-only transform, vertical axis lock),
show/hide (anchor built-in unhidable), and edit/delete for owned views.

Open-view semantics: the view's query is the baseline — its values render
checked-and-disabled in the filter menu, never as chips; chips show only
the user's additions and every reset path (chip x, clear, menu reset,
filtered-empty CTA, board/swimlane hide-column) returns to the baseline,
not to empty. Display seeds once per user (view:<id> surface key) and is
free afterwards. Built-in tab click exits; deleted/revoked views fall back
with a toast; ?view= deep links sync on web via a platform hook.

Save dialog doubles as the edit dialog (PATCH with expected_revision ->
409 toast); manager edits seed from the view definition, in-view edits
from the live panel. Filter chips bar, extracted IssueFilterMenu (virtual
frozen anchor), and the reworked display panel round out the header.

Also: filter menu consistency fixes (closeOnClick, calendar apply,
TableColumnPicker checkbox items), SelectGroup padding, dropdown anchor
passthrough, four-locale i18n.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(views): neutralize workspace tab filter while a saved view is open

The Members/Agents tabs are a coarse assignee-type filter carried in the
surface scope, outside the filter-menu dimensions a view can express. It
kept applying underneath an open view, so the same shared view returned
different rows depending on which tab the user stood on when opening it —
with nothing on screen to explain the difference (tabs dimmed, no chip).

While a view is active the workspace scope now resolves to actorKind
"all": a view's results are defined by the view alone, matching what the
dimmed tabs already imply. Deliberately NOT captured into saved queries —
a view condition must be visible and lockable in the filter menu, and
assignee-type has no menu representation (add the menu dimension first if
that need ever materializes; the loose schema takes the field additively).

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): workspace scope_variant captures and applies the assignee-type tab

A workspace view now saves which built-in tab it was created from
(members/agents, NULL = all) as scope_variant, alongside the existing
my-scope variants. The save dialog shows a scope selector so the
variant can be switched later; scope_type stays immutable. While a
view is open, its own variant drives the workspace scope's
assignee-type axis instead of whichever tab the user stood on.

- migration 266 widens the scope_variant CHECKs (members/agents on
  workspace, NULL = all; my keeps its four variants; project stays NULL)
- UpdateIssueView persists scope_variant; handler validates the variant
  against the view's scope_type on create and update
- issue-surface applies activeView.scope_variant to effectiveScope,
  replacing the blanket tab neutralization
- issues-header seeds the dialog scope from the live tab

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): project pages get a real assignee-type axis and clearer variant copy

The Members/Agents tabs on project pages were decorative: they wrote the
global tab store but the project query never read it. The project scope
now carries actorKind like the workspace scope (assignee_types composes
with project_id server-side), so the tabs filter for real, and project
views join the same optional scope_variant vocabulary — saved from the
tab, switchable in the dialog, applied while the view is open.

- migration 266 (unmerged) widened in place: project allows
  members/agents, NULL = all
- handler treats workspace and project variants identically; new
  TestProjectIssueViewVariant covers persist / normalize / reject
- project scope key gains a per-tab segment (members/agents) so each tab
  keeps its own display state; the unrestricted tab keeps the old key
- save dialog shows the variant selector for project views; the surface
  now provides the dialog's default variant directly (header-side
  injection removed)
- variant labels spell out the meaning (Assigned to members/agents, Any
  assignee, Any relation) and project hints are variant-aware, x4 locales

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-table): project table scope honors assignee_types

The table/rows channel dropped the Members/Agents narrowing on project
pages: the client's project scope never sent assignee_types and the
server's project branch never compiled it, so switching tabs only
changed the highlight while table rows stayed put (the board/list
channel already filtered). Both sides now share the workspace scope's
optional assignee-type narrowing, with a compile test covering the
predicate and the invalid-type rejection.

Co-Authored-By: Claude Fable 5 <[email protected]>

* refactor(issues): single assignee-type mapping + per-page tab state

One core helper pair (assigneeTypesForActorKind / actorKindForViewVariant)
replaces the four inlined tab->assignee_types literal mappings across
query-plan, the table spec compiler, and the view-variant application.

The assignee-type tab store becomes page-keyed (issues / project:<id>,
persisted with a v0 migration carrying the old global tab to the Issues
page): switching tabs inside a project no longer drags the Issues page or
other projects along. The save-view dialog's default variant now comes
from the header's own page tab, restoring the /issues default that the
surface-side injection had dropped.

Co-Authored-By: Claude Fable 5 <[email protected]>

* refactor(issues): converge all issue surfaces onto the table query channel

The legacy list/grouped query layer duplicated the table channel's scope
semantics behind enable-conditions that were provably always false: list
and status-board ride server status branches, every non-status grouping
and swimlane ride server group branches, and the assignee-board /
per-status load-more / flat-list / flat-export / client-side my:any
3-request merge fallbacks could never execute. Delete the whole layer:

- queries: myIssueListOptions, myIssueAssigneeGroupsOptions,
  issueAssigneeGroupsOptions, issueFlatListOptions/ExportOptions,
  fetchAllFlatPages / fetchAllMyFlatIssues / fetchAllMyFirstPages /
  fetchAllMyAssigneeGroups, compareIssuesForSort (server owns ordering)
- mutations: useLoadMoreByStatus, useLoadMoreByAssigneeGroup
- board/swimlane: the never-taken client-paginated columns, property
  pool loader, hidden-column client counts, legacy footer rows
- surface data hook / controller: dead queries and passthrough fields
- gantt keeps its scheduled-only endpoint and now compiles the
  assignee-type tab into it, restoring the change alongside the cleanup

query-plan shrinks to the scope's non-Table residue: scopeKey, the
gantt filter, and create defaults. issueKeys and cache invalidation
prefixes are untouched; mobile's own data layer is unaffected.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issues): review fixes — active view's variant wins in save dialog, stable dialog scope, dead mocks

Post-review pass on the convergence refactor:

- The save dialog's default variant now prefers the OPEN view's
  scope_variant over the page tab: saving a copy while an agents-scoped
  view is open must describe the rows on screen, not the tab underneath.
- The dialog scope prop is memoized on primitive projections — a fresh
  object per header render re-armed the dialog's draft-reset effect, so
  any background refetch wiped a half-typed name.
- Tests: persist migration coverage for the page-keyed tab store; gantt
  assignee_types reaches the request and forks the cache key.
- Dead legacy load-more mocks removed from three view test files; stale
  hidden-columns doc comment; unknown hidden-column totals render no
  count instead of a blank; my-relation plan branch is return-exhaustive;
  leftover blank lines.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issues): keep the grabbing cursor for the whole row drag in manage views

The handle's cursor classes only apply while the pointer hovers the
handle — the vertical-locked drag loses it immediately, and buttons the
pointer crosses flip it to pointer. Mirror the sidebar-resize cursor
contract: an html[data-dnd-dragging] rule forces cursor: grabbing (and
no text selection) document-wide for the drag's duration, toggled by the
DndContext lifecycle with an unmount safety net.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issues): my views apply their relation at the query layer, not by switching the tab

Opening a my-view used to overwrite the user's relation tab with the
view's variant (VARIANT_TO_TAB), so when an open view vanished
(deleted / access revoked) the fallback landed on the view's tab
instead of where the user actually was. Views are now applied the same
way on all three surfaces: while a view is open, effectiveScope
substitutes the view's variant — assignee-type for workspace/project,
relation for my — and the user's own tab state is never touched.
myRelationForViewVariant joins its sibling mapping in scope.ts with
unit coverage.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issues): ease the save-view dialog's expand and validation transitions

The display-defaults section snapped open/closed and the name-required
hint popped in. The panel now runs the Base UI height transition
(--collapsible-panel-height + data-starting/ending-style — the
tw-animate collapsible keyframes only know Radix's variable, so the
accordion's animate-* classes cannot be reused here), and the hint eases
in with the house animate-in vocabulary. Scoped to this dialog: the
shared Collapsible primitive stays untouched because chat/transcript
panels grow while open and a pinned panel height would clip them.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issues): filter chips ease in

Chips popped into the bar and the save-view dialog's embedded list with
no transition. Each chip now enters with the house fade+zoom vocabulary,
keyed per dimension so value edits inside an existing chip don't
re-animate. No exit animation by design.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): drag-reorder the view bar, tab context menu, pin views to sidebar

The bar's tabs (built-ins and saved views alike) now reorder by drag,
writing the same per-user preference document the manage dialog edits —
one list, two editors. Saved-view tabs gain a context menu:

- Edit: visible to everyone, disabled without manage permission (the
  greyed row is the signal); Delete: rendered only for the owner or a
  workspace admin on shared views — mirroring the server's
  canManageIssueView — and routed through the shared confirm dialog now
  extracted from the manage dialog. The manage dialog's own affordances
  follow the same widened rule (admins included).
- Pin to sidebar: pinned_item grows a 'view' type (migration 267; create
  validates read access so foreign private views 404). Sidebar pin rows
  resolve the view's name via the new GET-by-id options, render the
  view-bar icon, activate the view in the store and navigate to its
  owning surface on click. Flag-off clients keep view pins dormant
  instead of auto-unpinning; deleted views follow the existing
  404-auto-unpin pattern.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): review fixes — guard preference wipes, never auto-unpin views

Two data-loss paths from review, plus hardening:

- A drag landing before the views list loaded pruned the preference
  document against an empty list, silently erasing every saved-view
  entry from order and hidden. savePrefs now refuses to write until the
  list has actually loaded (viewsReady threads down from
  useActiveIssueView).
- View pins are exempt from 404-auto-unpin: the detail endpoint also
  404s when the feature flag is switched off server-side, and a client
  that booted with the flag on cannot tell the difference — a flag flip
  would have deleted every view pin permanently. Deleted views' rows
  hide instead.
- POST /api/pins with item_type=view now sits behind the same feature
  gate as every other view endpoint.
- SortableBarTab: drop dnd-kit's aria attributes (they nested a second
  focusable role=button around the tab), move the wasDragged ref into an
  effect, and clear it after a cancelled drag so the next real click
  isn't swallowed.
- Sidebar view pins carry ?view= in their href so a web reload stays on
  the view; scope resolution collapsed to one branch driving both the
  path and the container key.
- Malformed GET /api/issue-views/{id} responses pinned by test as null
  (hide the row), never an error (which alone may unpin).

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): single-row view bar with overflow popover

The bar no longer wraps: tabs that don't fit collapse into a trailing
popover, Linear-style, with two differences — built-ins are first-class
orderable entries, and the whole list (hidden rows dimmed) lives in the
pulled-down panel with drag reorder, per-row actions (edit/delete under
the same permission rule, pin, hide/show) and a new-view entry.

- useSingleRowFit (views/common): hidden mirror row measures natural tab
  widths; ResizeObserver + change-guarded per-commit remeasure computes
  the fitting prefix with trigger reservation. First reusable
  collapse-into-menu primitive in the repo.
- The open view is always visible: when it overflows it takes the
  trigger slot, showing its name + chevron (wider reservation; the
  two-pass promotion settle is monotonic, no oscillation).
- Every tab now shares one max width (max-w-40) and truncates with the
  full name in tooltips/rows.
- ManageViewsDialog retired: the popover carries all of its duties
  (reorder, hide/show incl. built-ins, edit/delete); hiding stays
  impossible for the anchor built-in and still exits a hidden open view.
  DeleteViewConfirm and ViewBarItem move to view-bar-popover.tsx;
  orphaned i18n keys dropped, row action keys added x4 locales.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): restore [layers] menu + manage dialog; 'more' appears only on overflow; fix zero-width bar

The single-row rework overreached: it replaced the existing new/manage
entries and dialog with an all-in-one popover, and the bar's container
collapsed to zero width (content-sized parent + measurement feedback),
rendering an empty header. Back to the intended shape:

- The [layers] menu (new view / manage views) and the manage dialog are
  restored exactly as before; DeleteViewConfirm and ViewBarItem stay
  shared from view-bar-popover.
- The 'more' trigger exists ONLY while tabs overflow, showing just the
  overflowed entries (click to open; per-row edit/delete/pin/hide with
  the same permission rules — those tabs have no rendered surface to
  right-click). The open view still takes the trigger slot when it
  overflows.
- Header view-bar slots gain flex-1 so the fit measures a real width;
  reserve tiers (menu / +more / +promoted) settle monotonically.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): list panel shows ALL tabs with drag reorder; trigger always visible

The pulled-down panel now lists every bar entry (built-ins included) in
bar order with vertical drag reorder — since the bar renders the longest
fitting prefix, reordering in the panel directly decides what sits on
the bar versus tucks away, with a separator marking the current fold.
The trigger no longer appears/disappears with overflow: it is a fixed
part of the bar's right side (still replaced by the open view's own tab
when that view overflows). Row actions (edit/delete/pin/hide) and the
[layers] new/manage entries are unchanged.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): kill the post-drop bounce with a synchronous order mirror

The preference mutation is optimistic but its onMutate is async: on drop,
dnd-kit clears the drag transform in the same frame while the reordered
list lands a beat later, so the dropped tab flashed back to its old slot
and then jumped — the manage dialog never bounced because it mirrors the
new order synchronously in the drag handler. Both bar drag and panel
drag now route through applyMove: set a local order mirror in the same
tick, persist, and clear the mirror once the preference data catches up.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): panel row menu is always visible, not hover-revealed

Hover-reveal made the three-dot row menu unreachable on touch devices;
it is a primary affordance in the list panel, so it renders always.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): long view names truncate/wrap everywhere

The manage dialog's row strip is a grid item — min-width:auto let an
80-char name blow past the dialog frame, carrying the row controls with
it. Pin the wrapper (min-w-0 + overflow-hidden) and the rows (min-w-0)
so labels truncate as designed. The delete confirmation and the save
dialog's scope hint interpolate names/titles into prose — break-words
so an unbroken name wraps instead of overflowing. Audited every other
name surface (bar tabs, promoted trigger, panel rows, sidebar pins,
context menus): all already bounded.

Co-Authored-By: Claude Fable 5 <[email protected]>

* refactor(issue-views): remove the saved_issue_views_v1 feature flag

The feature ships unconditionally. The flag never reached any released
client, so — unlike the retired flags that live on as always-on compat
keys — the key is removed outright on both sides:

- server: the issueViewsEnabled 404 gate drops from every view endpoint,
  the preference endpoints and the view pin type; the key leaves the
  registry and the frontend-public list; gate tests and the per-test
  enable helper go with it
- frontend: the === true checks, the pre-view-bar fallback tab rows in
  both headers, and useActiveIssueView's enabled parameter are deleted;
  the view bar is now the only rendering
- compat holds by construction: a NEW client against an OLD backend gets
  404s on the list/preference queries → empty views, viewsReady stays
  false (no preference writes), sidebar view pins hide without
  auto-unpinning (comment updated to name this as the reason); an OLD
  client against the NEW backend reads an absent flag as false and keeps
  the feature hidden

Full-suite run also surfaced that the workspace deletion manifest never
classified issue_view / issue_view_preference: both now fall with the
other issue roots in DeleteWorkspaceIssueRoots (no-FK policy: explicit
teardown), and the manifest records the decision.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): fit reserve applies even when every tab fits

The all-fit fast path skipped the reserve — a leftover from when the
overflow trigger only rendered on overflow. With the list trigger and
the [layers] menu now permanent, a row whose tabs exactly filled the
container declared everything fitting and overflow-hidden clipped the
trailing chrome. The reserve is now subtracted unconditionally.

Co-Authored-By: Claude Fable 5 <[email protected]>

* feat(issue-views): a freshly created view opens itself

Creating a view now activates it on its surface immediately — the view
you just saved is the one you meant to be looking at. Skipped only when
the create response fails to parse (the list refetch still shows it).

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): creating a view no longer bounces the surface to the default tab

Activation raced the list invalidation: the fresh view's id was set
active before the refetched list contained it, the stale list read as
'view deleted', and the missing-view fallback kicked the surface back to
the anchor tab (with an 'unavailable' toast). Two-sided fix: the create
mutation seeds the created view into its scope's list cache before
invalidating, and the missing verdict now requires the list to not be
mid-refetch.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): release-gate fixes — rollback safety, limits, old-client pin compat, revoke sweep

Addresses the pre-ship review blockers:

- CI styles: text-muted-foreground/70 -> text-faint-foreground (solid
  tone rule) in the header hint and the dialog my-note; the chip emoji
  stack's text-[9px] -> text-micro (type-scale rule). apps/web
  text-contrast + type-scale suites pass.
- migration 269 down normalizes workspace/project members/agents
  variants to NULL before restoring the old constraints — verified on a
  scratch DB with real variant data (up -> write -> down).
- view pins are withheld from the legacy /api/pins contract unless the
  client opts in (?include=view): old Desktop builds classified any
  non-issue pin as a project pin and permanently auto-unpinned it on
  404. New clients opt in; regression test pins one and asserts the
  legacy list never leaks it.
- write hardening: MaxBytesReader (128KB) on view create/update and
  preference PUT; isJSONObject rejects JSON null (was a DB-CHECK 500);
  per-owner view quota (100/workspace); list query LIMIT 200. Boundary
  tests for null blob, oversized body, and quota.
- member revoke now sweeps the departed member's PRIVATE views and their
  view-bar preferences in the same transaction (shared views stay, same
  rule as private quick actions) — the migration comments promised this;
  test covers private-gone/shared-stays/prefs-gone.
- sidebar comment no longer claims project ?view= reload support.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(issue-views): deleting a view sweeps its sidebar pins on every path

View pins never auto-unpin client-side (old-backend safety), so a pin
whose view is gone was invisible in the UI and unremovable forever —
and could leave an empty-looking Pinned group. All three deletion paths
now sweep matching pinned_item rows atomically in the same statement
via CTEs: direct view delete, project deletion (its scoped views), and
member revoke (their private views). Regression tests cover each path.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Fable 5 <[email protected]>
2026-08-10 11:55:05 +08:00