Commit Graph

40 Commits

Author SHA1 Message Date
Naiyuan Qing
ade6b34e5f MUL-3903: Extract shared issue surfaces (#4774)
* MUL-3903 refactor project issue surface state

Co-authored-by: multica-agent <github@multica.ai>

* Refactor project issue surface ownership

Co-authored-by: multica-agent <github@multica.ai>

* Extract shared issue surface entrypoints

Co-authored-by: multica-agent <github@multica.ai>

* Fix issue surface create defaults and selection reset

Co-authored-by: multica-agent <github@multica.ai>

* test(editor): add missing AbortSignal to suggestion items() calls

The suggestion items() contract gained a required signal param; the
mention/slash test call sites were never updated, breaking pnpm typecheck
for @multica/views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(issues): server-side assignee_types filter on ListIssues

ListGroupedIssues has taken assignee_types since squads shipped, but
ListIssues never did — so the workspace Members/Agents tabs had to fetch
the unfiltered workspace list and post-filter loaded pages client-side,
which made column totals and load-more pagination reflect the unfiltered
counts.

Add the same parse + WHERE clause to ListIssues (count query shares the
WHERE, so totals agree), thread the param through the TS client, and
widen MyIssuesFilter so scoped list caches can carry it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(issues): route issue cache writes through a membership-aware coordinator

useUpdateIssue, useBatchUpdateIssues, and the WS issue:updated handler
each maintained their own similar-but-diverging patch/invalidate rules.
Consolidate them into cache-coordinator.ts (applyIssueChange /
rollbackIssueChange / invalidateIssueDerivatives) so local writes and
remote echoes follow one rules table by construction.

The coordinator is membership-aware via surface/membership.ts
(true | false | unknown against each list cache's own filter contract):

- a change that moves an issue off a filtered surface removes the card
  surgically (bucket total decremented) — fixes assignee changes leaving
  stale cards on My Assigned with no local safety net (previously only
  the WS echo recovered it), and replaces the blanket invalidate-myAll
  net for project moves (MUL-3669) with per-key precision
- possible entry into a loaded list marks that key stale — never
  hard-insert; page/slot is server knowledge
- stale keys flush on settle for mutations (a mid-flight refetch would
  stomp the optimistic state) and immediately for WS
- batch updates now patch detail + inbox like single updates; the
  off-screen bucket-count recovery previously exclusive to the WS path
  now covers local mutations too

Preserved invariants: synchronous optimistic patches (dnd-kit), MUL-3375
control-field stripping, and no refetch of surgically reconciled lists
(the drag-flicker fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(issues): resolve surfaces via core query plan/repository with window-keyed remount

Read-path convergence and the loading/empty semantics that fall out of
it:

- scope -> API params moves from scope.ts helpers into
  surface/query-plan.ts; workspace members/agents become server-filtered
  scoped plans (assignee_types) and the client postFilter machinery is
  deleted — tab counts and load-more are now exact
- query selection moves behind surface/repository.ts; the views data
  hook no longer branches on workspace-vs-scoped plumbing
- IssueSurfaceContent remounts on data-window change (wsId + scope):
  keepPreviousData placeholders keep sort/filter changes flicker-free
  within one window but must never let project A's (or workspace A's)
  cards impersonate B's with no loading state — cold window shows the
  skeleton, warm window hits cache instantly
- isEmpty is only asserted from full-window data; the gantt
  scheduled-only projection can't prove the window is empty, so GanttView's
  own "no scheduled issues" empty state renders instead of the generic
  create-issue one
- per-card project lookups hoist into a surface-level projectMap (drops
  a per-card useQuery), create-defaults typing tightens to
  IssueCreateDefaults

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(issues): count-only arithmetic for off-window status/membership changes

An issue beyond a list's loaded page window used to force a full
first-page refetch just to fix two column counts. When the change is
CERTAIN (base entity known, membership definitive) the coordinator now
does the arithmetic locally:

- stayed a member + status changed: move one unit of total between the
  two buckets (loaded arrays untouched; hasMore stays consistent)
- left the list (reassigned / re-projected): old status bucket total -1
- member-to-member reassignment: counts unaffected, not even a stale key

Entering a list and any uncertainty (no base, unknown membership) still
refetch — the right page/slot is server knowledge. Branches on membership
OUTCOMES, not on which field changed, so future dimensions (team) join
automatically. Biggest win is the WS path: agents flipping off-screen
statuses no longer trigger refetch storms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(issues): deferred view-refresh indicator during placeholder revalidation

Sort/date changes (and any grouped-board filter change) revalidate behind
the previous snapshot — correct, but on a slow network the click felt
dead: content stays put and isLoading never fires. Surface the state as
isRefreshing (isPlaceholderData of the active query) and render a shared
ViewRefreshIndicator in every issues header: a fixed-width slot (zero
layout shift) whose spinner fades in after 300ms, so sub-second responses
show nothing (NN/g) while slow ones get a working signal.

Bound to the revalidation STATE, not to any particular control — any
current or future server-side view change lights it automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 14:11:10 +08:00
Bohan Jiang
8c3745dc7e feat(issues): add 'Show sub-issues' display toggle (MUL-3923) (#4801)
* feat(issues): add 'Show sub-issues' display toggle (MUL-3923)

Add a 'Show sub-issues' switch to the issue display-settings menu. When
turned off it hides sub-issues (issues with a parent) from the board,
list, swimlane and gantt views so users can focus on top-level / parent
issues. It is a pure display filter and never changes parent/child
relationships. Defaults to on and persists per view store, so /issues,
/my-issues, project detail and the actor tasks panel each remember it.

- view-store: new showSubIssues state + toggleShowSubIssues action,
  persisted; propagates to my-issues and actor view stores via the shared
  slice.
- filter: optional showSubIssues on IssueFilters; drop issues with a
  parent when explicitly false (undefined keeps show-all, so existing
  callers and mobile's positional variant are unaffected).
- wire the toggle into every surface that renders the display menu.
- i18n for en / zh-Hans / ja / ko.
- filter unit tests for the new toggle.

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): apply Show sub-issues to assignee board & swimlane extra path (MUL-3923)

Address review of #4801 — two paths bypassed the new toggle:

1. Assignee-grouped board rendered straight from the server 'groups'
   array, skipping the flat filterIssues() output, so sub-issues stayed
   visible in assignee grouping on /issues, /my-issues and project detail.
   Added filterAssigneeGroups() which re-applies the client-only display
   filters (Show sub-issues + the agents-working quick filter) to each
   group, recomputes total and drops emptied groups. Wired into all three
   surfaces. Generalizes and replaces the old filterRunningAssigneeGroups.

2. Parent swimlane's batch/per-parent extra-children merge rebuilt its
   internal activeFilters without showSubIssues, so lazily-loaded
   sub-issues reappeared even with the toggle off. Carry showSubIssues
   through.

Tests: filterAssigneeGroups unit tests (sub-issue hide, running filter,
AND composition, empty-group drop, by-reference passthrough) and a
swimlane test covering the batch-children path with the toggle off.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-01 19:18:06 +08:00
Naiyuan Qing
149cc9bd0a fix(issues): reflect real common value in batch toolbar pickers (#4403)
The batch action toolbar hardcoded status="todo", priority="none", and a
null assignee, so the status/priority/assignee pickers always checked a
fixed row regardless of the selected issues. The batch write itself worked,
but the picker mis-reported the current value, surfacing as "status always
defaults to todo" (MUL-3510). The same defect applied to priority and
assignee, across all five toolbar mount points.

Derive the shared status/priority/assignee of the selected issues via a new
commonIssueFields helper and feed it to the pickers; when the selection is
mixed, pass an empty value so no row is checked. Pickers now accept a
nullable current value, and AssigneePicker gains a `mixed` flag to
distinguish an all-unassigned selection (check "No assignee") from a mixed
one (check nothing). Each call site passes its issue universe, mirroring the
skill list's selected-rows approach.

Adds unit tests for commonIssueFields and a toolbar picker-wiring test.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-22 18:23:05 +08:00
Anderson Shindy Oki
f8fb3fdcd1 fix: swimlane view filters (MUL-3072) (#3645)
* fix: Swinlane view filters

* refactor: Comments
2026-06-06 22:34:15 +08:00
Naiyuan Qing
6f38891665 Improve mobile issue header controls (#3602)
* Improve mobile issue header controls

Co-authored-by: multica-agent <github@multica.ai>

* Fix desktop issue header control sizing

Co-authored-by: multica-agent <github@multica.ai>

* Fix issue scope tab test for mobile trigger

Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): keep header controls horizontally scrollable

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-01 16:33:30 +08:00
Naiyuan Qing
645af40ed9 refactor(views): unify detail/list headers into shared BreadcrumbHeader (#3510)
* refactor(views): unify detail/list headers into shared BreadcrumbHeader

Replace four hand-rolled, divergent header styles (workspace-name root,
"/" separator, back-arrow, raw div) with one shared BreadcrumbHeader
component. The mental model is now identical everywhere: leading crumbs
are the thing's real containers and clicking one navigates up.

- New packages/views/layout/breadcrumb-header.tsx (segments/leaf/actions)
- Detail pages (issue, project, runtime, skill, autopilot, agent, squad)
  now render `{Section} › name`; org name removed as a breadcrumb root
- Issue breadcrumb shows the single most-direct container only (parent
  wins over project; they are orthogonal columns), never a fabricated
  chain; bare issue shows just its title
- Issue leaf (identifier + title) is now a clickable link to the issue
  detail page with a subtle hover:opacity-80
- Issues / My Issues list headers drop the workspace prefix, matching the
  icon + title style of the other list pages

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(views): update breadcrumb tests for unified header behavior

The header unification changed three observable behaviors the tests
asserted against:
- issue detail no longer renders the workspace name as a breadcrumb root
- bare issue shows only its (now clickable) title leaf, no ancestor crumbs
- the project "Unknown project" error placeholder was removed

Rewrite the two affected issue-detail tests to assert the new leaf-link
and no-project-crumb behavior, drop the obsolete Unknown-project test, and
update the issues-page header test to assert the workspace prefix is gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 14:27:51 +08:00
Naiyuan Qing
30a7da6d10 feat(list): drag-and-drop reordering + loadMore sort fix (#3284)
* feat(list): drag-and-drop reordering + shared drag utils

- List view drag-and-drop: reorder within/across status groups, drop into
  collapsed groups, DraggableListRow with useSortable + checkbox protection
- Extract shared drag utilities (computePosition, findColumn, buildColumns,
  makeKanbanCollision, issueMatchesGroup, getMoveUpdates) to drag-utils.ts
- Reuse IssueDisplayControls in MyIssues header (dedup ~380 lines)
- Radio check marks for grouping/sort dropdowns
- Remove outer p-1 wrapper, unify board view to p-2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(my-issues): move MyIssuesHeader inside ViewStoreProvider

IssueDisplayControls calls useViewStore which requires the provider context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(header): restore swimlane option in view toggle dropdown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:09:42 +08:00
Bohan Jiang
057cb2ff34 fix(issues): thread sort through load-more so appended pages render (MUL-2678) (#3279)
The recent server-side-sort change (#3228) keyed the issue-list cache by
sort but did not update the load-more hooks: useLoadMoreByStatus used a
prefix-match that could pick a stale cache variant, and neither hook
forwarded sort to the API request. As a result, scroll-to-load-more
fired its request, but the response was either appended to a cache no
useQuery was subscribed to, or it appended rows in an unsorted order
into a sorted bucket.

Pass `sort` explicitly through Board/List/Swimlane and into the hooks.
The hook now targets the full sorted key via setQueryData and forwards
sort to the listIssues / listGroupedIssues calls so the appended page
lines up with the existing items.

Also adds focused tests for both load-more hooks: stale-sort cache is
untouched, sort is forwarded to the API, and sort-less callers still hit
the {} key path used by actor-issues-panel.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 16:27:33 +08:00
Anderson Shindy Oki
65ed5744a6 feat: add issue view swimlane (MUL-2607) (#3149)
* feat: Add swimlane view

* refactor

* test: add tests

* wip

* wip

* wip

* refactor

* refactor and fixes

* refactor

* fix: parent empty cells

* chore: Remove meanless comments

* remove meanless comments
2026-05-26 15:22:16 +08:00
Naiyuan Qing
612ac8f28e feat(issues): server-side sort + fix drag position corruption (#3228)
* refactor(editor): split rich text styles

* feat(issues): server-side sort + fix drag position corruption in non-manual sort

Backend: ListIssues and ListGroupedIssues now accept `sort` and `direction`
query params (position/priority/title/created_at/start_date/due_date).
ListIssues converted from sqlc to hand-written SQL for dynamic ORDER BY.
Priority sort uses CASE expression for semantic ordering.

Frontend: query keys include sort so changing sort triggers server refetch.
Client-side sortIssues() removed from board-view and list-view.

Drag-and-drop: non-manual sort disables within-column reorder (prevents
silent position corruption). Cross-column drag only updates status/assignee,
preserves original position. Column overlay shows current sort during drag.

Cache: query key split into prefix (list) for invalidation and full key
(listSorted) for queryOptions. All optimistic update paths use prefix
matching via getQueriesData to work with any active sort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(board): prevent drag flicker by settling columns until mutation refetch

After drag-and-drop, the optimistic cache patch updates position values
without reordering the bucket array. The useEffect that rebuilds columns
from TQ data would overwrite the correct local drag order, causing cards
to snap back then forward. Fix: isSettlingRef blocks column rebuilds
between drag end and mutation onSettled.

Also invalidate issueKeys.list on WS position changes so other windows
refetch correctly sorted data instead of showing stale bucket order.

Includes debug logs (to be removed after verification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(board): stabilize drag-and-drop for non-manual sort modes

Three behavioral fixes for board drag when sort != position:

1. Settling: isSettlingRef + settleVersion blocks column rebuilds
   between drag-end and mutation settle, preventing the optimistic
   cache patch (which updates position values without reordering the
   bucket array) from overwriting the correct local column state.

2. Non-manual cross-column: handleDragOver returns prev (no visual
   card movement — column highlight + sort label is sufficient).
   handleDragEnd uses overCol directly instead of findColumn on the
   card's current position (which would be the source column).
   Cards use useSortable({ disabled: { droppable: true } }) to
   suppress within-column insertion indicators.

3. Collision detection: when no card droppables exist (disabled in
   non-manual sort), return column droppables from pointerWithin
   instead of falling through to closestCenter, so isOver reflects
   the column the pointer is actually inside.

Also: WS position changes now invalidate issueKeys.list so other
windows refetch correctly sorted data.

Insertion-position prediction intentionally omitted — PostgreSQL's
en_US.utf8 collation (glibc) cannot be faithfully replicated in
JavaScript (ICU/V8), and an inaccurate indicator is worse than none.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sort): manual sort ignores direction param on both ends

Manual sort (position) is user-defined order via drag-and-drop —
reversing it has no product meaning.

Backend: sort=position now skips the direction query param and
always uses ASC. Both ListIssues and ListGroupedIssues handlers.

Frontend: sort object omits sort_direction when sortBy is position.
Direction toggle hidden in the display popover for manual mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(board): memo columns + stabilize references to reduce re-renders

- BoardColumn, PaginatedBoardColumn, PaginatedAssigneeBoardColumn
  wrapped in memo() — only columns with changed props re-render
- IssueAgentActivityIndicator wrapped in memo() — 111 snapshot
  subscribers no longer trigger full re-render on every WS task event
- buildColumns rewritten from O(groups × issues) to single-pass O(n)
- EMPTY_IDS constant replaces ?? [] fallbacks (stable reference)
- EMPTY_CHILD_PROGRESS constant replaces new Map() default
- BOARD_COL_WIDTH / BOARD_CARD_WIDTH constants shared between
  column and DragOverlay for consistent card dimensions
- issueListOptions + issueAssigneeGroupsOptions use
  placeholderData: keepPreviousData so sort/filter changes don't
  flash a full-page skeleton
- Loading skeleton scoped to content area only — header stays
  rendered during data transitions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: remove outdated server-side sort implementation plan

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:50:25 +08:00
Naiyuan Qing
e33b893c3f feat(views): sticky group headers in list view (#3221)
* feat(views): add sticky positioning to list-view group headers

Group headers now stay pinned at the top of the scroll viewport so users
always know which status group they are looking at. Background changed
from semi-transparent to opaque to prevent content bleeding through.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): remove top padding from list-view scroll container for sticky headers

The `p-2` padding on the scroll container caused an 8px gap above sticky
group headers. Replace with `px-2 pb-2` to keep horizontal and bottom
padding while allowing headers to stick flush to the top. Sync skeleton
containers in issues-page and my-issues-page to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): use p-2 pt-0 instead of px-2 pb-2 for list-view scroll container

Reporter preferred adding pt-0 to override the top padding from p-2,
keeping the original p-2 shorthand intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): opaque sticky header hover + cursor-pointer on trigger

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 08:27:57 +08:00
Naiyuan Qing
fedd0f1694 feat(issues): live agent activity chip + per-issue indicator + filter (#3058)
* feat(server): broadcast task:running event

The dispatched → running transition was silent: only task:queued,
task:dispatch, task:cancelled, task:completed and task:failed
broadcast over WS. Any UI that distinguishes "queued" from "running"
(e.g. the new issue-card agent activity indicator) would lag by up to
the 30s agentTaskSnapshot staleTime on the most user-visible
transition. StartTask now broadcasts task:running so the workspace
snapshot invalidates immediately, keeping the agent activity UI live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(issues): live agent activity chip + per-issue indicator + filter

Surfaces "which agents are working on what, right now" in the Issues
and My Issues views, with a one-click filter to narrow the list to
issues that have a running agent task.

Two visual surfaces:

- **Workspace chip** in the header (left of Filter). Shows the
  brand-tinted avatar stack of agents currently running on visible
  issues. Click toggles a page-scoped filter; idle state renders a
  static "0 working" button with a hover-card placeholder. When the
  filter is active the chip pins to brand fill across hover and popover
  states (the Button outline variant otherwise repaints back to
  neutral). A muted "Viewing only working agents" hint sits to the
  left of the chip whenever the filter is on, so users notice the
  active state without having to hover.

- **Per-issue indicator** on every board card and list row (top-right
  of the identifier line). Renders the avatar stack of agents in
  running or queued state on that issue, full-opacity ring at brand/70
  when ≥1 is running, half-opacity stack when only queued. Returns
  null when nothing is in flight.

Both surfaces open the same hover-card body that lists each active
task with the agent avatar, status dot (composed via the existing
availability + workload tokens), and a live-ticking duration.

Adds a new "All" scope to /my-issues that unions assignee, creator,
and involves_user_id via three parallel fetches deduped on the
client — no backend changes for this part. The chip's count and the
quick-filter both use the page's currently visible issue ids so they
stay in sync with the active scope.

State is per-user (Zustand + localStorage) and the agentRunningFilter
is intentionally omitted from partialize — running state changes
second-to-second and a stored toggle would land users in an
unexplained empty list. WS task:running, already added in the
preceding commit, drives real-time updates without polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(issues): swap indicator ring pulse for shimmer text label

Earlier iterations layered a brand ring with various opacity-pulse
cadences around the per-issue avatar stack. Every tuning attempt was
either invisible (transparent ring + faded pulse) or oppressive (a
visible ring that flashed on a dense board). Moves the "alive" signal
onto a small text label and reuses chat's existing
`animate-chat-text-shimmer` utility — a soft light sweep across the
glyphs that already powers the ChatGPT-style "thinking" cue in
task-status-pill.

Indicator now reads as a 12 px avatar stack + 10 px label:

- Running → full-opacity avatars + shimmering localized "Working"
- Queued  → half-opacity avatars + muted static "Queued"
- Idle    → render nothing (unchanged)

Avatars and the surrounding card stay completely still; only the few
glyphs animate. The label is i18n-driven via the existing
`status_running` / `status_queued` keys, so no locale changes are
required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:20:42 +08:00
Naiyuan Qing
85e363370e Revert "feat(issues): Working filter + agent-working badge on board (MUL-2452…" (#2927)
This reverts commit dee5c7cf50.
2026-05-20 16:47:41 +08:00
Naiyuan Qing
dee5c7cf50 feat(issues): Working filter + agent-working badge on board (MUL-2452) (#2924)
* feat(issues): surface "agent working" on board + add Working filter (MUL-2452)

Adds a brand-color "agent working" badge to board cards / list rows so
users can see at a glance which issues have an active agent task, plus a
new "Working" toggle on the `/issues` and `/my-issues` headers (next to
the existing scope segmented control) that filters to those issues. The
toggle shows an avatar stack of the agents currently active on the
current surface + scope. Pure frontend: re-shapes the existing
workspace-wide `agentTaskSnapshot` cache via two new selectors
(`activeTasksByIssueOptions` / `workingIssueIdsOptions`), no new SQL,
endpoint, or DB field; WS `task:*` events already invalidate the
snapshot so the badge / filter update in realtime.

Project detail page keeps the per-card badge but intentionally omits the
header toggle (`showWorkingToggle={false}`) to leave the project
surface's filter dimensions unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(issues): working filter column header reflects filtered count (MUL-2452)

Assignee-grouped board column headers kept showing the unfiltered cache
total when Working was on, because `PaginatedAssigneeBoardColumn` passed
`useLoadMoreByAssigneeGroup`'s cache-derived `total` straight to
`BoardColumn`. The hook still needs the cache total for hasMore, but the
displayed count must follow the visible-after-filter set.

Split the two: when Working is active the column header now uses
`group.totalCount` (set by applyWorkingFilterToGroups) for the assignee
path, and `issueIds.length` for the status path. Load-more keeps reading
from cache so paginated columns still see the full server total.

Regression tests cover applyWorkingFilterToGroups (total rewrite +
empty-group preservation), filterIssues workingOnly combinations, and an
end-to-end assertion via IssuesPage that proves the column header equals
the filtered count, not the cached value.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-20 16:35:58 +08:00
Naiyuan Qing
93153d08b7 feat(my-issues): cover squad assignees via involves_user_id (MUL-2397) (#2829)
Re-introduces the `involves_user_id` filter on the issues list / open-list /
count / grouped paths, but with the semantics nailed down for the second time
around: tab 3 surfaces issues whose assignee is an *indirect* extension of the
user (owned agent, or a squad they're a human member of / lead via owned agent
/ have an owned agent inside) — and explicitly NOT direct member assignment,
which is tab 1's meaning.

- server/pkg/db/queries/issue.sql: 4-branch filter on ListIssues /
  ListOpenIssues / CountIssues. Each subquery clamps workspace_id because
  issue.assignee_id is polymorphic with no FK. Leader resolution reads
  squad.leader_id directly, not the squad_member copy row (squad.go ignores
  errors when seeding that copy, so it can be missing). FindActiveDuplicateIssue
  switched from positional $2/$3/$4 to named sqlc.arg() — pure hygiene so the
  generated struct field names don't drift when new nargs are added.
- server/internal/handler/issue.go: parse involves_user_id and plumb it into
  the three sqlc params; ListGroupedIssues (hand-written dynamic SQL) gets a
  mirrored 4-branch fragment, no shortcut.
- packages/core: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter /
  api.listIssues / api.listGroupedIssues all carry the new param through.
- packages/views/my-issues: tab 3 switches from client-side agent-fanout to
  involves_user_id=user.id. agentListOptions import and the myAgentIds memo
  go away.
- server/internal/handler/issue_involves_test.go: 13 integration tests cover
  every branch (positive + cross-workspace negatives) plus the critical
  ExcludesDirectMemberAssignee negative on BOTH the sqlc and the grouped paths,
  locking tab 3 ∩ tab 1 = ∅.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 10:37:38 +08:00
Naiyuan Qing
5476e7678d Revert "feat(my-issues): cover squad assignees via involves_user_id (MUL-2364…" (#2828)
This reverts commit 3c510c31ed.
2026-05-19 09:31:43 +08:00
Naiyuan Qing
3c510c31ed feat(my-issues): cover squad assignees via involves_user_id (MUL-2364) (#2801)
* feat(my-issues): cover squad assignees via involves_user_id (MUL-2364)

The "My Agents" tab on /my-issues only resolved agents owned by the
caller, so issues assigned to squads (member, leader, or agent-member of
mine) never surfaced. This added a UNION-based involves_user_id filter
that the backend expands to "me + agents I own + squads I relate to" in
a single query.

- SQL: ListIssues / ListOpenIssues / CountIssues accept narg
  involves_user_id and OR a workspace-scoped 3-branch UNION on the
  squad assignee subquery. Leader is sourced from canonical
  squad.leader_id (not the best-effort squad_member copy row whose
  AddSquadMember error is dropped in squad.go:177-188 and :259-263).
- Handler: parses involves_user_id via parseUUIDOrBadRequest, plumbs
  into all three list params, and mirrors the same UNION fragment into
  the grouped dynamic SQL path.
- Frontend: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter
  gain involves_user_id; api client forwards it to the querystring.
- My Issues page: "agents" scope now passes involves_user_id instead of
  fanning out owned-agent IDs client-side. Tab label widens to
  "我的智能体 / 小队" / "My Agents / Squads".
- Tests: Go suite covers all three squad relations including the
  canonical-leader-without-squad_member-copy variant, cross-workspace
  isolation for agent / leader / squad_member branches, combination
  with creator_id, and the malformed-UUID 400 path. Client test pins
  the involves_user_id querystring wiring for both list endpoints.

The FindActiveDuplicateIssue query gets explicit sqlc.arg() names so
sqlc regeneration keeps the existing struct field names regardless of
the local sqlc version (no behavior change).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* test(my-issues): tighten cross-workspace negatives for involves_user_id UNION

Cross-workspace negative tests previously put both the foreign actor and the
foreign issue in the foreign workspace, so the outer i.workspace_id = $1
already excluded the row before the UNION branches were exercised. Stripping
a.workspace_id = $1 / s.workspace_id = $1 from any of the UNION subqueries
would not have failed the tests.

Rewrite the three existing negative cases to seed the issue in
testWorkspaceID with a polymorphic assignee_id pointing at a foreign-workspace
agent or squad (issue.assignee_id has no FK per migrations/001_init.up.sql:61).
Now each UNION branch must enforce its own workspace scoping for the issue to
stay out of the result.

Also add ExcludesOtherWorkspaceSquadAgentMember: the squad_member.agent UNION
branch had only positive coverage; this test pins that s.workspace_id = $1
and a.workspace_id = $1 must both hold there too.

Verified by mutation: stripping the workspace clause from each branch makes
the corresponding test fail.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-19 09:01:51 +08:00
Naiyuan Qing
20c2f45b4a fix(views): surface backend error messages on mutation failures (MUL-2317) (#2772)
* fix(views): surface backend error messages on mutation failures (MUL-2317)

Mutation toasts across the views package were swallowing the backend
`error` string and showing only a generic i18n fallback. This made it
impossible for users to see why an operation failed (most visibly:
creating an issue with a duplicate title produced a vague "Failed to
create issue" toast).

The fix has three pieces:

1. Create-issue duplicate branch (A段)
   - New schema `DuplicateIssueErrorBodySchema` in core/api/schemas.ts.
   - `create-issue.tsx` parses `ApiError.body` via `parseWithFallback`
     and renders a dedicated amber-toned toast with a "view existing"
     link when the server returns `{ code: "active_duplicate_issue",
     issue: {...} }`. Schema drift downgrades to the normal error toast.
   - Schema intentionally omits `issue.status` so the toast does not
     depend on `StatusIcon`, which has no fallback for unknown enums.

2. User-facing mutation failure toasts (B段)
   - 47 sites converted to `err instanceof Error && err.message ?
     err.message : <existing fallback>` — preserves all existing
     code-specific branches (slug conflict, agent_unavailable,
     daemon_version_unsupported) and i18n keys.
   - Covers Type 1 (onError) and Type 2 (catch block) patterns across
     issues, projects, autopilots, inbox, runtimes, squads, comments,
     batch actions, workspace create, and agent config tabs.

3. Autopilot partial-success (Type 3)
   - New i18n keys `toast_create_partial_with_reason` /
     `toast_update_partial_with_reason` (double-brace `{{reason}}`).
   - `autopilot-dialog.tsx` captures `err.message` in the schedule
     `catch` and routes to the `_with_reason` variant when present,
     preserving the partial-success semantic (autopilot saved, schedule
     failed) while exposing the actual reason.

Explicitly out of scope:
- `packages/core/` mutation hooks (no global onError, no UI dependency)
- No `toastApiError` helper (matches existing 14+ correct sites)
- Sub-issue link aggregate `Promise.allSettled` keeps count-based toast
  (N independent requests cannot collapse to one err.message); only
  added a dev-side `console.error` per rejection.
- Clipboard catches and `useUpdateChatSession` (not API mutation toasts)

Tests:
- `packages/core/api/schemas.test.ts` — schema contract (valid body,
  forward-compat fields, rename rejection, missing issue, wrong types).
- `packages/views/modals/create-issue.test.tsx` — duplicate toast +
  view link, schema-drift fallback, err.message surfacing, non-Error
  fallback (4 new cases).
- `packages/views/autopilots/components/autopilot-dialog-i18n.test.ts`
  — real i18next, asserts rendered text contains the reason verbatim
  (guards against `{reason}` vs `{{reason}}` regression).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(autopilots): unify rotate-token catch + cover dialog partial-success render

Address reviewer feedback on PR #2772:

1. webhook-token rotate (`autopilot-detail-page.tsx`) now follows the
   `err.message ?? fallback` ternary used by the sibling trigger
   delete/add paths, instead of swallowing the error.

2. Extract `formatSchedulePartialFailureToast` so the dialog's
   partial-success branches and the i18n test exercise the same
   helper. The test now drives the actual format function, so a
   variable-name typo at the call site (e.g. `{ msg }` instead of
   `{ reason }`) fails the substring assertion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* test(modals): drop user.type for title in success path to dodge CI 5s timeout

The success-path test typed the 42-character title via userEvent which
triggers a controlled re-render per keystroke. On the slower CI runner
the whole test crept up to ~5s and intermittently tripped the default
vitest timeout. Setting the value in one shot via fireEvent.change cuts
the cost while leaving the submit + toast interactions on userEvent.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-18 13:44:10 +08:00
Jiayuan Zhang
8e88156356 Add assignee grouping for issue boards (#2693) 2026-05-15 18:44:08 +08:00
Naiyuan Qing
3447764b03 feat(i18n): full rollout — 21 namespaces translated (en + zh-Hans) (#1853)
* feat(i18n): rollout phase — translate 9 namespaces (WIP)

Phase 1 complete (基建 + login + Settings language switcher),
phase 2 partial (Wave 4 done, search done). Pending namespaces
documented inline; another developer can pick up from here.

Infrastructure
--------------
- server: add users.language column + extend PATCH /api/me
  (TestUpdateMeAcceptsLanguage / TestUpdateMePreservesLanguage)
- packages/core/i18n: types / pickLocale (intl-localematcher) /
  browser-cookie-adapter / createI18n (initAsync false +
  useSuspense false) / I18nProvider / LocaleAdapterProvider
- Split server-safe vs React entries:
    @multica/core/i18n        — for proxy/RSC/middleware (no React)
    @multica/core/i18n/react  — for client trees (createContext)
  (RSC vendored React lacks createContext; mixed import would crash
  proxy.ts at module load.)
- packages/views/i18n: useT hook + selector API augmentation
  (i18next v26 default; auto-propagates to apps via the side-effect
  import in use-t.ts).
- apps/web: proxy.ts (Next 16 renamed middleware) merges existing
  legacy/root redirects with x-multica-locale header forwarding;
  layout.tsx reads locale via headers() and pre-loads RSC resources.
- apps/desktop: webPreferences.additionalArguments injects
  systemLocale (no sendSync — avoids main-thread blocking IPC);
  renderer adapter reads via process.argv.
- ESLint: i18next/no-literal-string at file-scope for translated
  files via packages/views/eslint.config.mjs TRANSLATED_FILES.
- glossary.md (packages/views/locales/) freezes term policy:
  Issue / Workspace / Agent / Skill / Autopilot / Daemon / Runtime
  stay English; Inbox / Project / Comment / Member translate.

Translated namespaces (9 / 19)
------------------------------
- auth: login page (web wrapper含 desktop-handoff 文案) + Settings
  Appearance language switcher
- editor: 9 .tsx (bubble-menu / link-hover-card / readonly-content /
  title-editor / extensions: code-block / file-card / image-view /
  mention-suggestion) + 32 keys
- invite: 25 keys
- labels / members / my-issues: Wave 4 全部
- search: command palette 35 keys
- navigation: no user-facing strings (no-op)

Pending (10 / 19)
-----------------
issues (46 files / ~210 keys)
agents (29 files / ~155 keys; presence.ts + config.ts label maps
  允许进 i18n)
onboarding (22 files / ~150 keys)
settings rest / skills / modals / workspace / chat / inbox /
projects / autopilots / layout

Workflow for picking up
-----------------------
- Glossary: packages/views/locales/glossary.md (mandatory read)
- Reference impls: auth/login-page.tsx + editor/* (selector API +
  i18n-provider test wrapper pattern)
- Per namespace:
    1. create locales/{en,zh-Hans}/{ns}.json
    2. add to packages/views/i18n/resources-types.ts
    3. useT('{ns}') + t($ => $.foo) in components
    4. add files to TRANSLATED_FILES in eslint.config.mjs
    5. typecheck + test + lint must pass
- Subagents currently CANNOT write files (sandbox deny). Run as
  hybrid: subagent researches + outputs full JSON + tsx diff,
  controller writes.

Other
-----
- scripts/init-worktree-env.sh: default
  MULTICA_DEV_VERIFICATION_CODE=888888 in dev for deterministic
  login (gated by isProductionEnv).

Verified: pnpm typecheck (6 pkgs ok), pnpm test (232 pass),
make test (Go).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(i18n): rewrite glossary aligned with docs zh voice

Switch translation policy to match the canonical CN voice already
established in apps/docs/content/docs/*.zh.mdx (20+ files). The new
rule splits product nouns into two classes:

- Typed entities (issue / project / skill / autopilot / task) — kept as
  lowercase English in CN text, visually marking them as system types.
- Concepts (workspace / agent / daemon / runtime / inbox) — fully
  translated (工作区 / 智能体 / 守护进程 / 运行时 / 收件箱).

Previous glossary kept Workspace / Agent / Daemon / Runtime as English
on "工程惯例" grounds, but docs zh and CN AI ecosystem (Coze / 腾讯元器
/ 百度) consistently translate these. App UI now matches docs voice so
users don't see split personality between the app and its own docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): register 6 namespaces and retrofit zh strings to new glossary

Two fixes that were blocking the previously-translated namespaces from
actually rendering in CN:

1. RESOURCES gap — locales/index.ts only loaded common/auth/settings,
   but resources-types.ts declared 12 namespaces and 6 of them had real
   translation content. At runtime i18next would fall back to raw keys
   for editor / invite / labels / members / my-issues / search.
   Register all 9 currently-translated namespaces.

2. Retrofit zh strings to the docs-aligned glossary:
   - "Issue" → "issue" (lowercase entity)
   - "Workspace" → "工作区"
   - "Agent" → "智能体"
   - "Runtime" → "运行时"
   - "Skill" → "skill" (lowercase)
   - "项目" → "project" (lowercase)

Touched: editor.json (sub_issue + mention.group_issues), invite.json
(3 Workspace occurrences), members.json (agents_section / more_agents),
my-issues.json (8 retrofits across page/header/errors), search.json
(13 retrofits across groups/pages/commands/empty).

Verified: pnpm typecheck (6/6) + pnpm test (238/238) all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate inbox namespace

First namespace through the sub-agent → main-agent integration pipeline.

JSON: en/inbox.json + zh-Hans/inbox.json — 60 keys across page / menu /
list / detail / types / labels / errors. Time-formatter labels are kept
compact in EN ("5m" / "3h" / "2d") and use full units in zh ("5 分钟" /
"5 小时" / "5 天") since raw "5 分" reads as "5 marks/points" in CN.

Component changes converted two module-level statics into hooks so the
strings can flow through i18next:

- inbox-list-item.tsx: `timeAgo` (pure fn) → `useTimeAgo` (hook
  returning a fn). The local copy is a duplicate of @multica/core/utils
  `timeAgo` that is only used by inbox-page; other consumers across
  chat/agents/skills/issues stay on the core util for now and will be
  translated when their namespaces land.

- inbox-detail-label.tsx: `typeLabels` (static const Record) →
  `useTypeLabels` (hook returning the same Record shape). Call sites
  keep the existing `typeLabels[type]` access pattern.

inbox-page.tsx now uses both hooks and `useT('inbox')` selector calls
for all hardcoded strings (~24 sites: header / dropdown menu / list
empty state / detail panel / mobile back / quick-create-failed flow /
all error toasts).

Wired up: resources-types.ts, locales/index.ts RESOURCES, ESLint
TRANSLATED_FILES (3 inbox tsx files now lint-protected).

Verified: pnpm typecheck (6/6) + pnpm --filter @multica/views test
(238/238) + ESLint clean on inbox/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate workspace namespace

Translates the three workspace shell views: create-workspace-form,
new-workspace-page, no-access-page. Also fixes the prior-art
no-unescaped-entities lint errors in no-access-page.tsx — the
apostrophes in "doesn't" / "don't" were JSX text literals that move
into JSON values after translation, so the lint rule no longer fires.

Tests wrapped: workspace/create-workspace-form.test.tsx,
workspace/no-access-page.test.tsx, modals/create-workspace.test.tsx
all now wrap render() with <I18nProvider locale="en"> so the en values
in workspace.json drive the rendered text and the existing assertions
continue to match.

Slug constants kept: WORKSPACE_SLUG_FORMAT_ERROR /
WORKSPACE_SLUG_CONFLICT_ERROR exports in workspace/slug.ts are still
imported by onboarding/steps/step-workspace.tsx (out of scope here).
The workspace shell now reads its strings from workspace.json directly.

Multica.ai brand prefix in the slug input affordance is wrapped with
an inline `// eslint-disable-next-line i18next/no-literal-string` per
glossary policy on brand names.

Renamed sign_in_other → sign_in_different to avoid colliding with
i18next's `_other` plural-suffix convention which the selector-API
typings treated as a plural form of `sign_in`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate projects namespace

Translates the projects list page, project detail page, project picker
dropdown, and project chip — all four user-facing surfaces under
packages/views/projects/components/.

New file: projects/components/labels.ts exposes three hooks that
replace the static `.label` field on PROJECT_STATUS_CONFIG /
PROJECT_PRIORITY_CONFIG and the previous module-level
`formatRelativeDate` helper. Core's `.label` stays untouched (it's
still consumed by search and the create-project modal, both
out-of-scope for this namespace) — those will flip when their
respective namespaces translate.

In zh, the "project" entity stays lowercase English per glossary
(`新建 project`, `还没有 project`, `从 project 移除`). Status / priority /
table column labels translate fully.

The cancelled / done / paused etc. status labels duplicate per-
namespace as `projects.status.*` rather than reading from a future
shared status namespace. This matches the auth/inbox/workspace
pattern of self-contained namespaces. If a generic "issue/project
status" pool emerges later, these can collapse.

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on projects/ (1 pre-existing warning
about useEffect/sidebarRef dep, unrelated to i18n).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate autopilots namespace

Six tsx files: autopilots-page (list + 6 templates), autopilot-detail-page
(properties / triggers / run history / delete), autopilot-dialog
(create + edit dialog), trigger-config (cron form), and the agent /
timezone pickers.

Hook conversions for module-level helpers that need t():
- summarizeTrigger / describeTrigger → useSummarizeTrigger /
  useDescribeTrigger (no external callers, removed the plain exports)
- formatRelativeDate → useFormatRelativeDate (per-component hook)
- formatCountdown → useFormatCountdown (per-component hook)
- TEMPLATES array now keyed by id; titles + summaries pull from
  templates/{id}/{title,summary} JSON. Prompts stay raw EN since
  they're injected directly into the agent task — translating them
  would translate the agent's instructions, not the user's UI.

Status / execution-mode / run-status enums render via t($ => $.status[k])
with k typed against the core type (no separate hook needed).

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on autopilots/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate skills namespace

Seven tsx files: skills-page (list + filters + intro banner),
skill-detail-page (the giant — properties + file tree + sidebar +
conflict banner + delete dialog, ~963 lines), create-skill-dialog
(chooser + manual + URL forms), runtime-local-skill-import-panel
(local runtime browse + import), skill-columns, file-tree, file-viewer.

Notable patterns:
- `createSkillColumns` factory → `useSkillColumns` hook so column
  headers flow through useT. Column identity changes per render is
  fine — DataTable handles it.
- `validateNewFilePath` (pure helper) → `useValidateNewFilePath` hook
  so the 5 validation error messages can be translated.
- skill_files / used_by / description_with_agents use i18next plural
  keys (`_one` / `_other`) — the type system collapses these into a
  single PluralValue access, so call sites use
  `t($ => $.foo, { count })` and i18next picks the form.
- Per glossary, "skill" stays lowercase EN in zh ("新建 skill",
  "已删除 skill", "未找到该 skill").

Test wrapper: runtime-local-skill-import-panel.test.tsx now wraps
render() with <I18nProvider> so the assertion on /Import to Workspace/i
matches the EN translation.

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on skills/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate chat namespace

Translates all 10 chat surfaces: FAB tooltip, input placeholders,
message list (replied-in / failed-after / tools group / show-details
/ tool result preview), session history (header + time-ago labels),
chat window (new-chat / restore / expand / minimize / agent + session
dropdowns / starter prompts / empty states), context-anchor button +
card tooltips, no-agent banner, offline / unstable banner, and the
task-status pill (queued / starting up / thinking / typing + tool
labels: running command / reading files / searching code / making
edits / searching web).

Hook conversions:
- formatTimeAgo (chat-session-history) → useFormatTimeAgo
- ElapsedCaption now takes a typed `variant` ("replied" | "failed")
  instead of a free-text `verb` so the i18n key is enumerable
- pickStage (task-status-pill) refactored: pure pickStageKeys returns
  StageKey + optional ToolKey; useResolveStage maps to localized labels

Translation policy notes:
- Starter prompts ("List my open tasks by priority", etc.) are user
  UI when displayed AND the user's input when clicked — translating
  them sends the agent the user's locale-native phrasing, which is
  the right UX for a CN user using a CN agent.
- buildAnchorMarkdown (chat-window) stays in English: it's an
  agent-bound markdown prefix injected into the outgoing message,
  not user-facing UI.

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate modals namespace

Translates all 11 modal sources: registry (no UI text), backlog-agent-hint,
set-parent-issue, add-child-issue, delete-issue-confirm, feedback,
issue-picker, create-workspace, create-project, create-issue (manual),
quick-create-issue (agent panel).

Notable patterns:
- create-project re-uses useProjectStatusLabels / useProjectPriorityLabels
  hooks from views/projects/components/labels — same translation source
  as the projects list / detail, no duplication.
- create-issue.tsx: renamed `toast.custom((t) => ...)` callback param to
  `toastId` to avoid shadowing the closure-captured useT() `t` function.
- Test wrapper added to modals/create-issue.test.tsx so the two assertions
  on rendered modal text (success toast + Create another) match the EN
  bundle. modals/create-workspace.test.tsx was already wrapped (workspace
  ns commit).

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate settings namespace (rest of tabs)

Builds on the appearance-tab + language switcher already shipped in
Phase 0. Translates the remaining 8 settings surfaces: settings-page
shell (left nav + tab keys), account / profile, notifications-tab
(5 group labels + descriptions), tokens-tab (create / list /
revoke / created dialog), workspace-tab (general fields + danger
zone + leave/delete confirmations), members-tab (invite + role
config + revoke / remove flows), repositories-tab, labs-tab,
delete-workspace-dialog.

Hook conversion: members-tab `roleConfig` static const → `useRoleLabels`
hook returning a Record<MemberRole, {label, description, icon}>. The
icon stays as a typed React component (Crown / Shield / User), so
rendering pattern is unchanged at call sites.

Test wrapper: settings/components/delete-workspace-dialog.test.tsx
now wraps render() with <I18nProvider> (custom render() helper)
because the test asserts on rendered button labels ("Delete workspace",
"Cancel", "Deleting...").

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate runtimes namespace (entry surfaces)

Translates the user-facing runtime list page surfaces:
runtimes-page (header / search / filters / chips / empty / no-matches /
bootstrapping), runtime-detail (topbar + delete dialog + delete toasts),
runtime-detail-page (not-found state), shared.tsx (4-state HealthBadge
labels).

Hook conversion: shared `healthLabel(health)` was a pure module-level
function. Added `useHealthLabel` hook for translated call sites; kept
`healthLabel` as an EN-only fallback for non-component callers (column
factory in runtime-columns).

Deferred:
- runtime-list / runtime-columns (data table column headers + cell
  bodies) — large surface, not in the page-load critical path.
- connect-remote-dialog / update-section / usage-section — secondary
  flows, English remains acceptable until a focused pass.
- charts/* — primarily numeric tooltips and axes; minimal user-visible
  text.

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate layout namespace (sidebar nav, help, loader)

Translates the cross-cutting layout chrome:
- 9 sidebar nav labels (inbox / my issues / issues / projects /
  autopilots / agents / runtimes / skills / settings) — driven by
  labelKey instead of inline strings, resolved via useT at render.
- HelpLauncher dropdown (trigger aria + 3 items: Docs / Change log
  / Feedback)
- WorkspaceLoader (named + unnamed loading states)
- SortablePinItem unpin tooltip

Pattern shift in app-sidebar.tsx: nav arrays carry `labelKey: NavLabelKey`
(typed against the layout JSON) instead of `label: string`. The string
comparison checks (`item.label === "Inbox"`) became cleaner ID-based
checks (`item.key === "inbox"`).

Deferred: deeper sidebar surfaces — workspace switcher dropdown,
"New Issue" CTA, "Pinned" / "Workspace" / "Configure" group labels —
remain English. The 9 nav labels are the ones that read in every
session.

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate onboarding namespace (welcome + step header)

Translates the user-first-impression surfaces of the onboarding flow:

- step-welcome.tsx (the wordmark, headline, lede paragraphs, all CTAs:
  Download Desktop / Continue on web / Start exploring / I've done
  this before, illustration caption)
- step-header.tsx ("Step N of M" counter + matching aria-label)
- onboarding-flow.tsx (skip-onboarding error toast)

Test wrapper added to onboarding/components/step-header.test.tsx —
custom render() helper wraps with <I18nProvider> so the "Step 2 of 5"
assertions match the EN bundle.

Deferred (acceptable English fallback for now): step-questionnaire,
step-workspace, step-runtime-connect, step-platform-fork, step-agent,
step-first-issue, cli-install-instructions, option-card, runtime
aside panels, starter-content-prompt, cloud-waitlist-expand. These
are deeper steps with significant copy that would benefit from a
focused dedicated pass — voice on each is more nuanced (questionnaire
options, runtime install instructions, agent template recommendations).

Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(i18n): add EN/zh-Hans key parity guard

Schema-level vitest that walks RESOURCES.en and RESOURCES["zh-Hans"]
namespace by namespace and asserts both bundles cover the same key
set. i18next plural rule is normalized before compare (`_one` /
`_other` collapse to a single logical key) so EN's plural pair
matches zh's `_other`-only form.

Catches retrofit drift where a new EN key lands without zh —
previously this would silently fall back to the English string in
production. Cheap to keep green: 39 tests across 21 namespaces in
under a second.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate issues namespace

Translates the entire issues surface — list / board / detail / comments /
sub-issues / activity feed / batch toolbar / pickers / context menu /
backlog-agent hint dialog / labels panel.

Component coverage:
- issues-page (page header, empty state, move-failed toast)
- issues-header (scope tabs, filter dropdowns w/ status/priority/
  assignee/creator/project/label, display settings, sort, view toggle)
- issue-detail (page header, breadcrumb, properties / parent issue /
  details / token usage sections, sub-issues, activity timeline,
  formatActivity for status/priority/assignee/title/due-date changes,
  subscribe/subscriber popover)
- comment-card + comment-input + reply-input (delete dialog, edit/save,
  copy/edit/delete row, reply count, placeholders, expand/collapse)
- agent-live-card (is-working banner, tool count, stop / transcript)
- execution-log-section (section header, show/hide past runs, trigger
  text builder, status labels, cancel-task)
- batch-action-toolbar (selected count, delete dialog with plurals)
- backlog-agent-hint-dialog (full dialog content)
- labels-panel (intro, create form, list, delete dialog)
- pickers (status / priority / assignee / due-date / label / property
  search placeholder + no-results)
- issue-actions-menu-items (all dropdown / context menu items)
- use-issue-actions / use-issue-timeline (toast strings)

STATUS_CONFIG / PRIORITY_CONFIG label rendering routed through
$.status[enum] / $.priority[enum] at every call site; the core config
keeps its English fallback for non-i18n consumers but UI never reads
.label directly anymore.

Tests retrofitted: issues-page, issue-detail, and issue-actions-menu
RTL specs now wrap renders in <I18nProvider> with the EN bundle, so
their string assertions match the bundle (not hardcoded literals).

ESLint i18next allow-list extended to 24 issues files. Verified:
pnpm --filter @multica/views typecheck + test (277/277) all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate agents namespace

Translates the agents listing + detail surface and the create/duplicate
flow. Covers the high-frequency surfaces; deeper sub-tab editors
(activity / instructions / skills / env / custom-args bodies, and the
hooks-buggy runtime/model/concurrency pickers) are deferred — they
have their own pre-existing react-hooks rule violations and benefit
from a focused dedicated pass.

Component coverage:
- agents-page (page header w/ tagline + new button, scope segment,
  search, sort dropdown, availability chips, archived toolbar, empty
  state, no-matches messaging w/ search interpolation, list-load
  error)
- agent-detail-page (back link, archived banner, archive dialog,
  not-found state, all 4 toast strings)
- agent-detail-inspector (avatar editor, name + description popover,
  description dialog, every PropRow label, validation message,
  presence badge label sourced from $.availability[enum])
- agent-overview-pane (tab labels, discard-unsaved-changes dialog)
- create-agent-dialog (title / description / labels / placeholders /
  duplicate-suffix / runtime filter buttons / runtime status copy)
- agent-row-actions (full dropdown items + cancel-tasks dialog with
  pluralized "N running + M queued" summary + archive dialog + 6 toasts)
- agent-columns (every header cell, You / Archived chips, runtime
  fallback labels, availability + workload labels via $.availability /
  $.workload, activity tooltip body w/ created_today / created_days_ago
  / runs / failed-percent interpolation)
- inspector/skill-attach (Attach trigger label + aria)

availabilityConfig and workloadConfig now keep colors only — the
display label lives in the bundle, sourced via $.availability[enum]
and $.workload[enum] at every call site. Same pattern as
STATUS_CONFIG/PRIORITY_CONFIG in the issues namespace.

ESLint i18next allow-list extended to 8 agents files.
Verified: pnpm --filter @multica/views typecheck + test (277/277)
all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): clear 30 stray EN strings in translated files

Tail of literal strings missed in earlier passes — the ESLint i18next
allow-list flagged them but they slipped through review. Files touched:

- layout/app-sidebar.tsx (10 keys: Workspaces / Pending invitations /
  Create workspace / Join / Decline / Log out / New Issue + shortcut /
  Pinned / Workspace / Configure)
- runtimes/components/runtime-detail.tsx (Serving header + serving_count
  pluralization, no_agents copy, running/queued chips with count
  interpolation, Diagnostics header, CLI label, Delete runtime button,
  Technical details toggle, last seen interpolation)
- onboarding/steps/step-welcome.tsx (entire WelcomeIllustration mock —
  5 cards × actor names + body copy + 3 mention chips + 2 timestamps;
  zh translation reads naturally instead of leaving the demo English)
- settings/components/labs-tab.tsx (`Co-authored-by: ...` git trailer
  wrapped in {} so linter sees a JS string, not JSX text — magic
  identifier git relies on, must not translate)
- settings/components/members-tab.tsx (✓ glyph wrapped in {})
- modals/feedback.tsx (⌘↵ shortcut wrapped in {})

ServingAgentsCard now reads availability/workload labels from
`agents` namespace (cross-namespace useT) so the bundle-truth pattern
holds: presenceConfig keeps colours only, label text comes from the
shared bundle.

Verified: typecheck + 277/277 tests + lint (only the pre-existing
react-hooks rule-of-hooks errors remain, which task #6 addresses).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agents): rules-of-hooks + translate 4 model/runtime pickers

Three pre-existing react-hooks/rules-of-hooks violations + one missing
useMemo dep cleared, then the four pickers wired through useT.

Hook order fixes:
- concurrency-picker: useEffect now runs before the !canEdit early
  return. Stale-draft reset still works the same way.
- runtime-picker: useMemo for the filtered list moved above the
  !canEdit branch.
- model-dropdown: `models = data?.models ?? []` was minting a fresh
  array each render and tripping the deps lint of the downstream
  useMemo. Wrap in useMemo so the reference is stable.

Translation coverage:
- concurrency-picker: tooltip ("Concurrency · N max..."), range
  helper text, Save button.
- runtime-picker: trigger label fallback ("No runtime"), tooltip
  text composed from {{name}} + status, Mine/All filter buttons,
  empty-list copy, "owned by {{name}}" + status fragments in row
  tooltip, Cloud badge, online/offline aria.
- model-picker: trigger label, tooltip, "Managed by runtime"
  fallback, search placeholder, "Discovering models…", default
  badge, "No models available", "Use \"X\"" custom-id flow, Clear
  button + its title.
- model-dropdown: every label string including the "Select a runtime
  first" / "Default (provider)" / "Runtime offline — enter manually"
  trigger fallbacks, the supported=false explanation block, discovery
  failed badge, all popover items.

ESLint allow-list extended to 4 picker files. Verified: typecheck +
277/277 tests + lint (0 errors, only pre-existing react-hooks warnings
in unrelated files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate runtimes list + connect dialog + CLI updater

Three deep runtime surfaces wired through useT, with the agents
namespace doing double duty for shared availability/workload labels.

runtime-columns:
- 7 column headers via t-augmented createRuntimeColumns({ t }).
- HealthCell now reads from useHealthLabel() (already translation-aware)
  instead of the EN-only healthLabel() helper.
- WorkloadCell sources the label from $.workload[enum] (cross-namespace
  to agents) — colour stays via workloadConfig.
- CostCell delta "flat" copy + CLI cell "Desktop" badge + update-
  available aria/tooltip + RowMenu's full delete dialog (title /
  description with {{name}} interpolation / cancel / confirm /
  deleting state) plus its admin-permission hint.

connect-remote-dialog:
- Three steps fully translated: instructions (header + 4 numbered
  steps + security warning + troubleshooting list with mono code
  snippets escaped as JS strings), waiting (loader + hint), success
  (CTA pair).
- Mono CLI commands wrapped in {} so linter sees JS strings — those
  are literal commands that must stay untranslated for the user to
  paste into a terminal.

update-section:
- statusConfig collapsed to icon+colour only; labels move to
  $.update.status[enum] for proper translation per-state.
- "CLI Version:" / "Latest" / "available" / "Update" / "Retry"
  copy + the "Managed by Desktop" tooltip and disabled hint.

Layout helpers tagged: runtime-list passes `t` through to the column
factory the same way agent-columns does.

ESLint allow-list extended with the 4 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. usage-section.tsx
(KPI cards / WhenChart / TopUsageBreakdown / receipt table) is the
remaining runtimes surface — chart-heavy and benefits from a focused
pass next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate 5 agent detail tabs + skill-add dialog

The 5 tabs that fill the agent detail right pane plus the shared
skill picker dialog. Agents bundle gains a `tab_body` block with
sub-namespaces per tab + a `common` slot for save/add/unsaved.

Tab coverage:
- instructions-tab: intro paragraph, multi-line example placeholder
  (full 18-line zh translation), Save / Unsaved.
- env-tab: read-only intro / empty state, editable intro with two
  inline `<code>` env-var examples kept English (mono terminal
  payloads), KEY / value placeholders, Show/Hide value aria, Add /
  Remove aria, all 3 toasts (duplicate keys / saved / save failed).
- custom-args-tab: intro about whitespace splitting, launch-mode
  prefix line + `<your args>` placeholder, --flag value placeholder,
  Add, Remove aria, both toasts.
- skills-tab: intro, Add skill button, import-hint callout, empty
  state title + hint + add-CTA, remove-failed toast.
- activity-tab: 3 section titles (Now / Last 30 days / Recent work),
  active-task pluralization, performance subtitle, all 3 empty
  states, runs/success%/avg-duration/failed pluralization with
  interpolation, source labels (Issue / Chat / Autopilot / Untracked),
  source fallbacks (Quick create / Creating issue / Chat session /
  Autopilot run), issue-short fallback, "Triggered by" tooltip
  header, open-issue / transcript / cancel-task tooltips and ARIAs,
  cancelling state, started/dispatched/queued time prefixes, show
  more.
- skill-add-dialog: dialog title + description, empty list copy,
  Cancel button, add-failed toast.

skills-tab.test.tsx wrapped in <I18nProvider> with the EN bundle so
its `Local runtime skills are always available` assertion still
matches the resolved translation instead of the raw key path.

ESLint allow-list extended with the 6 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. Only the per-test
mock for skills-tab needed wrapping; the other 4 tabs ship without
test files of their own and inherit the I18nProvider chain via
agent-overview-pane / agent-detail-page test renders (when those
exist later).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate onboarding step-questionnaire + option-card

The user-profile step (3 questions) is the first deferred onboarding
deep step now wired through useT.

step-questionnaire:
- Eyebrow + headline + answered-progress counter with {{count}}
  interpolation
- All 3 questions and their option labels (team size / role / use case)
- All 3 "Other" placeholders for free-text fallback
- Right-rail "Why three questions" / "What you get" panel: 2 eyebrow
  rows, 2 unlock-item title+body pairs, learn-more link
- Back / Continue buttons via shared `common` block

option-card: shared "Other" radio label and aria.

Test wrapped in <I18nProvider>. EN value of `other_label` kept as
"Other" so the existing /^other$/i regex in step-questionnaire.test
keeps matching after the rendering pipeline switched from a hardcoded
literal to a bundle lookup.

ESLint allow-list extended with these 2 files. The remaining 4 deep
steps (workspace / runtime-connect / platform-fork / agent), the
2 ancillary surfaces (cli-install-instructions / starter-content-
prompt), and the 3 side panels (runtime-aside-panel / cloud-waitlist-
expand / compact-runtime-row) will be surfaced + swept by the global
ESLint switch (next commit).

Verified: typecheck + 277/277 tests + 0 i18n lint errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): flip ESLint to glob + drain remaining hardcoded EN

ESLint i18next/no-literal-string now applies to **/*.tsx by default
instead of an explicit allow-list. Files that genuinely still need
hardcoded EN are listed in STILL_HARDCODED — concrete, finite, and
the goal is to drain that list to zero.

Tail strings translated in this commit (surfaced by the global flip):

- common/task-transcript/agent-transcript-dialog.tsx — full dialog:
  status badge (Running / Completed / Failed), sr-only DialogTitle,
  Filter dropdown trigger + Clear filters, Copy all / Copy filtered /
  Copied, tool-calls + events metadata chips with pluralization,
  events-filtered "{{shown}} of {{total}}" interpolation, "Waiting
  for events..." live state, "No execution data recorded." past
  state. New `transcript` block in agents namespace.
- runtimes/components/charts/activity-heatmap.tsx — Less / More
  legend labels around the contribution-style heat squares.
- search/search-trigger.tsx — sidebar Search... button label.
  ⌘ glyph wrapped in {} to satisfy the linter (mono shortcut symbol,
  not translatable).

Holdouts (STILL_HARDCODED, ~14 files): the deep onboarding steps
(workspace / runtime-connect / platform-fork / agent / first-issue /
cli-install-instructions, plus 4 ancillary panels), the runtimes
usage-section + KPI cards, and 5 minor agent visual primitives
(sparkline / agent-presence-indicator / agent-profile-card /
visibility-badge / char-counter). Each one gets a dedicated future
pass; the global rule prevents new hardcoded strings from landing
elsewhere.

Verified: typecheck + 277/277 tests + 0 i18n lint errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): drain agent visual primitives + onboarding small components

8 files removed from STILL_HARDCODED:

agents/components/:
- char-counter — over-limit text with {{count}} interpolation
- visibility-badge — uses new agents.visibility.{private,workspace}.
  {label,tooltip} block; drops VISIBILITY_LABEL/TOOLTIP imports from
  core in favour of bundle-driven copy
- agent-presence-indicator — availability + workload labels via
  $.availability[enum] / $.workload[enum] (cross-namespace),
  queue-badge "+N queued" with pluralization
- agent-profile-card — Agent unavailable / Detail link / Owner /
  Skills / Runtime / Unknown runtime / Archived chip / availability
  line via cross-namespace lookup

agents.json: new presence + visibility + profile_card + char_counter
blocks.

onboarding/components/:
- compact-runtime-row — online/offline aria via agents.availability
- runtime-aside-panel — full content (What's a runtime / Good to
  know / Swap anytime / Add more later / docs link)
- starter-content-prompt — full dialog (title / description with
  inline emphasis / both buttons / 3 toasts)
- cloud-waitlist-expand — intro paragraph + warning span / email
  + reason labels + placeholders + Optional badge / Join + on-list
  states / both toasts

onboarding/steps/:
- cli-install-instructions — copy aria + intro + 2 step labels

onboarding.json: new runtime_aside / cli_install / starter_content /
cloud_waitlist blocks.

Tests for step-platform-fork + step-runtime-connect wrapped in
<I18nProvider> with EN bundle so /you're on the list/i etc. still
matches the resolved translations.

Verified: typecheck + 277/277 tests + 0 i18n lint errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate onboarding deep steps

The 5 large onboarding steps that were deferred from earlier passes,
plus their support helpers, all wired through useT.

step-first-issue (final beat — flips onboarded_at):
- error_title / Retry / retry_failed toast / finishing / opening
  states.

step-agent (creates the user's first agent):
- Templates moved from a module-level const to a useT-driven
  useAgentTemplates() hook. Names + emoji stay constant (visual
  identity), labels + blurbs + instructions resolve from the
  bundle. coding / planning / writing / assistant — all four
  templates ship a full zh translation that reads naturally.
- Recommended badge, eyebrow + headline + lede, footer hint,
  Create {{name}} CTA, create_failed toast.
- Right-rail "About agents" panel (4 way-items + headline +
  add-more hint + docs link).

step-workspace (create or pick existing):
- 5 footer states (open / creating / creating-pending / name-first
  / pick), all hint + CTA strings via interpolation.
- Name + URL + slug placeholders, issue-prefix preview spans,
  Create-new card title + subtitle.
- 8-row WorkspacePreviewCard sidebar (Inbox / Issues / Agents /
  Projects / Autopilot / Runtimes / Skills / And more) — every
  label + meta strapped to bundle keys.
- 4 perks (assign / chat / invite / switch) + 3 next-steps
  (runtime / agent / starter), 2 toasts (slug-conflict / failed).
- `multica.ai/${slug}` mono URL escaped via template-literal
  expression so the linter sees a JS string.

step-runtime-connect (desktop scan flow):
- 3 phase headlines + ledes (scanning / found / empty), trust-strip
  status (all online / N online / none online) with pluralization,
  online/offline labels, Skip / Continue / Selected hint.
- Empty-view 2 cards (skip + waitlist) and the cloud waitlist
  dialog wrapper.

step-platform-fork (web fan-out):
- Eyebrow + headline + lede, footer hint with 3 phase variants.
- Primary download card (before/after click) + 2 alt cards (CLI /
  cloud) + CLI dialog with 4 elapsed-time stages (normal / midway /
  slow / stalled), live-listening header, runtime-connected
  pluralization, cloud waitlist dialog.

ESLint: STILL_HARDCODED list shrunk from 14 entries to 1 — only
runtimes/components/usage-section.tsx (chart-heavy KPI panel)
remains.

Verified: typecheck + 277/277 tests + 0 i18n lint errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate runtimes usage panel + drop STILL_HARDCODED

Final i18n holdout: the runtimes usage panel (KPI hero, WHEN chart
tabs, cost-by breakdowns, daily breakdown table) is wired through
useT("runtimes"). With this drained, the eslint scaffolding for
explicit holdouts is removed — every JSX text node in @multica/views
now flows through i18n.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): drain rollout gaps + add cross-device sync

Lands the post-review punch list for the i18n rollout: closes correctness
gaps that would have shipped silently, and adds the missing cross-device
locale sync the rollout's docs already promised.

Coverage:
- Register issues + agents namespaces in RESOURCES (90 useT call sites
  were rendering keys-as-text in production)
- Harden parity test to compare RESOURCES keys against on-disk JSON
  files, so a future missing namespace registration fails loudly
- Server-side language whitelist in UpdateMe + reject-unsupported test
- Safe SupportedLocale resolution in appearance-tab (no more `as` cast
  on a region-tagged BCP-47 string)
- HTML lang attribute uses zh-CN (not zh-Hans) for screen reader / CJK
  font-stack compatibility
- Cookie Secure flag on https
- Pulled createBrowserCookieLocaleAdapter out of the server-safe entry
  into a new @multica/core/i18n/browser subpath; document.cookie access
  can no longer leak into Edge middleware imports

Cross-device sync:
- New UserLocaleSync component mounted in CoreProvider; on login, if
  user.language differs from the active i18n.language, persist via the
  adapter and reload. Both apps benefit
- Desktop main process tracks system locale and emits IPC on focus when
  it changes; renderer reloads only when the user has no explicit
  Settings choice (their preference still wins)

Tests:
- pickLocale / matchLocale (11 cases incl. region-tagged BCP-47, malformed
  tags, zh-Hant collapse-to-zh-Hans semantics)
- browser-cookie-adapter (6 cases under jsdom)
- Shared renderWithI18n helper at packages/views/test/i18n.tsx that wraps
  the real RESOURCES map; future tests opt in instead of inlining a
  per-file TEST_RESOURCES slice that goes stale silently

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(conventions): consolidate naming + i18n glossary into docs site

Single source of truth for code naming, i18n translation glossary, and
Chinese voice rules. Previously split between packages/views/locales/glossary.md
and scattered comments — now lives at apps/docs/content/docs/developers/conventions.{mdx,zh.mdx}
with both English and Chinese versions kept in sync.

Three sections per page:
1. Code naming — routes, packages, files, DB, Go, TS, commits
2. i18n translation glossary — entity vs concept rule, what to translate,
   word combination, plurals, interpolation, key naming
3. Chinese voice + style — punctuation, principles, where to look in doubt

Side effects:
- packages/views/locales/glossary.md collapses to a stub redirecting to
  the docs page; do not edit it
- CLAUDE.md gets a new top-level "Conventions reference" section so any
  Claude session sees the pointer before any other rule
- apps/docs/content/docs/developers/ gets a stub English meta.json so the
  conventions page is reachable on the EN side (contributing.zh.mdx /
  architecture.zh.mdx remain ZH-only — separate work)
- Both root sidebars get a new "Developers" group

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(i18n): apply zh voice rules + translate project/autopilot

Two-part cleanup driven by the conventions doc landed last commit:

Voice violations (mechanical sweep across 10 zh-Hans namespaces):
- 「」 (Japanese-style brackets) → \" to match the EN source's straight
  double quotes (~13 sites)
- … (single-char ellipsis) → ... three dots (~43 sites)
- Drop translation-ese pronoun "我们" where it's a pure narrator
  ("我们已发送" → "已发送", "我们替你托管" → "由 Multica 托管"); keep
  "告诉我们" where "we" is the legitimate brand recipient
- "作为父级 / 作为子级" → "设为父级 / 设为子级"
- "任务" mistranslated as the task entity → `task` (lowercase EN entity)
- Dialog title "Autopilot" → "autopilot"

Translate project / autopilot per industry consensus:
- `project` → 「项目」 (~42 value sites). Feishu / Tower / Teambition /
  PingCode / GitHub Projects all translate; no Chinese product keeps
  `project`.
- `autopilot` → 「自动化」 (~34 value sites). Avoids the Tesla-style
  「自动驾驶」 association; matches Notion / Feishu's industry term.
- Issue / skill / task remain lowercase EN per dev-team familiarity.
- Sidebar nav-label entities get Title Case ("Issue" / "Skill" / "我的
  Issue") so the entry-point label reads as a proper UI signal; body
  prose stays lowercase.

Conventions doc (EN + ZH) reflects the decision and adds a "why these
translate but issue/skill/task don't" rationale block.

Verification: parity test 45/45, full monorepo typecheck green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(i18n): translate chat session delete + project resources section

Two features main shipped while this branch was idle never went through
the i18n pass:

- Chat session delete confirmation dialog (#2115) and history toggle
  tooltip (#2117): adds session_history.delete_dialog.* and
  session_history.row_delete_*, plus window.history_show_tooltip /
  history_back_tooltip.
- Project resources sidebar (#1926/#2080/#2111): entire component
  including toasts, popover form, attach/remove tooltips. New
  projects.resources subtree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:16:12 +08:00
Jiayuan Zhang
050a2f0a5b fix(views): preserve kanban display settings when dragging issues (#1971)
Dragging an issue between kanban columns was forcefully switching the
sort mode to "position" (manual), resetting any user-chosen display
settings like sorting by title. Remove the auto-switch so the sort
preference is preserved across drag operations.

Fixes multica-ai/multica#1960

Co-authored-by: multica-agent <github@multica.ai>
2026-05-01 15:55:01 +02:00
Bohan Jiang
0236e409e4 feat(issues): client-side label filter on the issues list (#1782)
Adds a Label submenu to the workspace issues filter dropdown, backed by
labelFilters in the shared issue view store. The filter is OR'd within
itself (issue matches if it carries any of the selected labels) and
AND'd with the existing status / priority / assignee / creator /
project dimensions, mirroring the multi-select semantics already in
place. Each label row renders via LabelChip for color parity with the
sidebar picker, and each row's count comes from the same
useIssueCounts pass that drives the other filter chips.

Filtering stays client-side, consistent with all other filters today.
The pagination caveat is a known limitation we'll revisit if real
workspaces start hitting it; this PR intentionally does not change the
fetch path.
2026-04-28 16:47:33 +08:00
Bohan Jiang
abd69890a8 Revert "feat(issues): server-side filters incl. label, fixing pagination drop…" (#1779)
This reverts commit 246fcd4ce4.
2026-04-28 16:29:42 +08:00
Bohan Jiang
246fcd4ce4 feat(issues): server-side filters incl. label, fixing pagination drops (#1776)
* feat(issues): server-side label + filter querying for issue list

Extends GET /api/issues with label_ids, priorities, creator_ids,
project_ids, include_no_assignee, and include_no_project params, and
moves the existing single-value filters onto array-form. Each filter
becomes part of the SQL WHERE clause so paginated buckets reflect the
user's selection — fixes the bug where client-side filtering hid
matches sitting past the first page (#1491).

CLI gains a repeatable --label flag; legacy --priority/--assignee/
--project keep working via the single-value compatibility paths.

* feat(issues): drive workspace + my-issues filters from the server

issueListOptions and myIssueListOptions now key the React Query cache
on a normalized filter object, so each filter combination has its own
cache entry and a filter change re-fetches with the wire-shape filter
applied server-side. Drops the client-side filterIssues step on the
issues page, my-issues page, and project detail — that step silently
hid matches that lived past the first paginated page (#1491).

Adds a Label submenu to the workspace issues filter dropdown, plus
labelFilters in the view store. Mutations and ws-updaters fan their
optimistic patches across every filter-keyed list cache via
qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's
mention-suggestion reads from any matching list cache for instant
first paint regardless of which filter is active.

* fix(issues): route Members/Agents scope through server-side filter

The Members/Agents scope tabs on the workspace issues page were still
narrowing client-side via `assignee_type === 'member'`. That hits the
exact pagination-blind bug this PR is meant to fix: if the first 50
issues per status don't include the right assignee type, the tab
shows "No issues" while later pages have matches.

Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues /
CountIssues, threads it through the API client, normalizer and view
filter, and maps the scope tab to it. Each scope now keys its own
list cache and refetches with the correct first page.

Also disables the My Issues "My Agents" query when the user owns no
agents — `assignee_ids: []` was getting dropped by both the API client
and the query-key normalizer, so the request went out unfiltered and
surfaced unrelated issues under "My Agents".
2026-04-28 16:13:56 +08:00
Bohan Jiang
642844c736 feat(issues): paginate every status column, not just done (#1422)
* feat(issues): paginate every status column, not just done

Previously the workspace issues list fetched all non-done/cancelled
issues in a single unbounded `open_only=true` request and only
paginated the done column. In workspaces with many open issues this
ballooned the initial payload and skipped pagination entirely.

Restructure the issue list cache into per-status buckets
(`{ byStatus: { [status]: { issues, total } } }`) fetched in parallel,
generalize `useLoadMoreDoneIssues` into `useLoadMoreByStatus(status,
myIssuesOpts?)`, and render an infinite-scroll sentinel inside every
accordion group and kanban column. Sort and filter stay client-side,
matching the done column's existing behavior.

Backend `ListIssues` already supports per-status pagination, so no
API changes are required.

* fix(issues): handle project / hidden-column / lookup regressions from paginated list cache

After bucketing the issue list cache by status, three consumers that
treated `issueListOptions()` as a complete local index broke:

- `project-detail.tsx` filtered the workspace list by `project_id`
  client-side, so projects whose issues sat past the first 50-per-status
  page rendered empty. Switch to `myIssueListOptions(wsId,
  'project:<id>', { project_id })` so the server returns only this
  project's issues; add `project_id` to `ListIssuesParams` /
  `MyIssuesFilter` / api client.
- `board-view.tsx` HiddenColumnsPanel read counts from the in-memory
  `issues` array — a paginated fragment. Pass `myIssuesOpts` through to
  a per-row subcomponent that reads the real per-status total from the
  cache.
- `tasks-tab.tsx` and `search-command.tsx` used the list as a global
  lookup for task titles / Recent items / current-issue chrome. Switch
  both to per-id `issueDetailOptions` via `useQueries` so they're
  independent of which page the issue lands on.

Drop the now-redundant `doneTotal` override prop on BoardView/ListView
and the `allIssues` prop on BoardView (only HiddenColumnsPanel consumed
it).

Tests updated: tasks-tab now mocks `api.getIssue`; search-command mocks
`issueDetailOptions` + `useQueries`; project-issue-metrics drops the
`doneColumnCount` assertion.
2026-04-21 16:48:55 +08:00
Naiyuan Qing
c15212c0e4 fix(views): align skeleton loading states with actual page layouts (#1251)
- Issues/MyIssues: remove incorrect border-b from toolbar skeleton, add
  viewMode-aware skeleton (list vs board)
- Issue Detail: fix content padding (max-w-4xl mx-auto) and sidebar
  width (w-80), remove independent reactions/subscribers/timeline
  skeleton flashes — components now render with empty defaults
- Agents/Skills: gate skeleton on data query isLoading instead of auth
  store isLoading so skeleton covers actual data fetch
- Projects/Autopilots: add sticky column header skeleton row
- Autopilot Detail: add PageHeader skeleton, flesh out section structure
- Invite: replace plain text with Card-shaped skeleton
- Chat: migrate ChatMessageSkeleton to use Skeleton component
- Workspace layout: show MulticaIcon loading indicator instead of blank

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 14:21:10 +08:00
Naiyuan Qing
fe358feff0 Reapply "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139) (#1141)
This reverts commit b30fd98605.
2026-04-16 13:16:35 +08:00
Naiyuan Qing
b30fd98605 Revert "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139)
This reverts commit 75d12c26c5.
2026-04-16 12:26:40 +08:00
Naiyuan Qing
75d12c26c5 feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)
* Reapply "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)

This reverts commit 9b94914bc8.

* compat: legacy URL redirect + localStorage double-write for safe rollback

The first attempt at this refactor (#1131) was reverted because existing
users on old URLs (/issues, /projects, etc.) hit 404 immediately after
deploy, and rolling back left them with empty dashboards — the legacy
code reads localStorage["multica_workspace_id"] to attach a workspace
to API requests, but the new code had stopped writing that key.

Two compat layers added on top of the restored refactor:

1. proxy.ts now intercepts legacy route prefixes (/issues/*, /projects/*,
   /agents/*, /inbox/*, /my-issues/*, /autopilots/*, /runtimes/*,
   /skills/*, /settings/*). Logged-in users with a last_workspace_slug
   cookie are 302'd to /{slug}/{rest}, preserving their deep link. Users
   without the cookie bounce through / where the landing page picks a
   workspace client-side. Unauthenticated users go to /login.

2. Both layouts now double-write the workspace id to the legacy
   localStorage key on every workspace entry. New code ignores this key
   — it exists solely so that if this PR ever gets reverted again, the
   legacy build reading the key would still find the correct workspace
   and avoid the empty-dashboard symptom users saw during the rollback.

Net effect: any direction of deploy ↔ rollback is now cache-compatible,
and any direction of old bookmark → new route resolves without 404.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(platform): defer rehydrateAllWorkspaceStores to a microtask

Same React 19 render-phase restriction that forced setCurrentWorkspace
to defer its subscriber notifications. rehydrateAllWorkspaceStores
synchronously calls each persist store's rehydrate, which setState()s
the store, which schedules updates on any subscribed component. When
the workspace layout's render-phase ref guard invoked this, React
complained that SearchCommand (a store subscriber) couldn't be
re-rendered while WorkspaceLayout was still rendering.

Fix: queueMicrotask the rehydrate loop and add a pending-flag guard so
rapid workspace switches coalesce into one rehydrate on the final slug.
Persist stores tolerate one microtask of staleness — they hold UI
preferences, not correctness-critical state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:23:41 +08:00
Naiyuan Qing
9b94914bc8 Revert "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit 59ace95a1e.
2026-04-16 11:56:15 +08:00
Naiyuan Qing
59ace95a1e feat: workspace URL refactor + slug-first API identity (#1131)
* feat: workspace URL refactor + slug-first API identity

Make the URL the single source of truth for workspace identity.
All workspace-scoped URLs now carry the workspace slug as the first
path segment (/{slug}/issues, /{slug}/projects, etc.), matching the
industry standard (Linear, Notion, Vercel, GitHub).

## Key architectural changes

**URL-driven workspace identity:**
- Web routes moved under app/[workspaceSlug]/(dashboard)/
- Desktop routes nested under /:workspaceSlug
- paths.ts builder centralises all URL construction
- reserved-slugs validation (backend + frontend + DB migration audit)

**Slug-first API contract:**
- Frontend sends X-Workspace-Slug header (from URL) instead of X-Workspace-ID (UUID)
- Backend middleware resolves slug → UUID via GetWorkspaceBySlug, falls back to
  X-Workspace-ID for CLI/daemon backwards compatibility
- WebSocket auth accepts ?workspace_slug query param with SlugResolver callback

**State cleanup:**
- Deleted: useWorkspaceStore (Zustand mirror), switchWorkspace/hydrateWorkspace/
  clearWorkspace, localStorage["multica_workspace_id"], api._workspaceId
- useCurrentWorkspace() derives from URL slug + React Query workspace list
- useWorkspaceId() is now a bridge hook (no Context, derives from useCurrentWorkspace)
- WorkspaceIdProvider removed from DashboardGuard
- Paired module vars (slug + UUID) in workspace-storage.ts for non-React consumers

**Layout simplified:**
- Render-phase ref guard sets workspace context synchronously (no async gate)
- DashboardGuard handles auth redirect, loading state, and workspace resolution
- Subscriber notifications deferred via queueMicrotask (React 19 compat)
- persist namespace uses slug (immutable) instead of UUID

## Issues resolved

MUL-43 (share links), MUL-509 (mobile workspace switch), MUL-723 (workspace in URL),
MUL-727 (create workspace flash), MUL-728 (delete workspace no-navigate),
MUL-820 (sidebar Join not switching)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve code review C3/C4/C5/C6 — desktop deadlock + hardcoded paths

C3: Desktop OnboardingGate was calling useCurrentWorkspace() outside
WorkspaceSlugProvider → always null → permanent onboarding deadlock.
Rewrite to use useQuery(workspaceListOptions()) which reads React Query
cache directly without slug context. Remove DashboardGuard from
DesktopShell (auth gating handled by AppContent, workspace routing by
WorkspaceRouteLayout per-tab).

C4: Landing page "Dashboard" links hardcoded /issues (no longer valid).
Changed to / — proxy handles redirect to /{lastSlug}/issues.

C5: autopilots-page.tsx had one hardcoded /autopilots/${id} link.
Changed to wsPaths.autopilotDetail(id).

C6: inbox-page.tsx hardcoded /inbox paths. Changed to wsPaths.inbox().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(desktop): wrap shell in WorkspaceSlugProvider from module var

AppSidebar calls useWorkspacePaths() → useRequiredWorkspaceSlug() which
throws outside WorkspaceSlugProvider. In the desktop shell, the sidebar
renders at the shell level (outside any tab's WorkspaceRouteLayout).

Fix: DesktopShell reads the current slug via useSyncExternalStore on
the workspace-storage singleton. When slug is available, wraps the
entire shell in WorkspaceSlugProvider. When null (first mount before
any tab's WorkspaceRouteLayout sets it), shows a loading spinner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(desktop): migrate old tab paths + fix shell slug deadlock

Tab store rehydration: old-format paths like "/issues/abc" (missing
workspace slug prefix) are reset to "/" so IndexRedirect picks the
correct workspace. Detection: if the first segment is a known route
name (issues, projects, etc.) rather than a workspace slug, it's an
old-format path.

Desktop shell: TabContent must always render (not gated behind slug
check) so WorkspaceRouteLayout can mount and call setCurrentWorkspace.
Only sidebar and shell-level UI (chat, modals, search) gate on slug
being present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:53:09 +08:00
Naiyuan Qing
c38af55a8e refactor(ui): comprehensive UI craft review — sidebar, headers, detail panels (#1087)
Sidebar:
- Pinned items: StatusIcon for issues, emoji for projects, sm size, mask gradient text fade
- Pinned items: inline X close button (hidden → flex on hover, desktop tab pattern)
- Pinned section: collapsible with chevron + hover count
- Remove unused canvas token

Global components:
- PageHeader: shared component with built-in mobile SidebarTrigger (md:hidden)
- Replace header divs in all 11 dashboard pages with PageHeader
- Remove standalone mobile trigger bar from DashboardLayout
- Tooltip: 200ms delay, remove arrow, popover/border style
- Search dialog: add finalFocus={false}
- SidebarInset: remove shadow-sm
- Button sizing: icon-xs → icon-sm across all non-editor contexts

Issue Detail:
- Simplify breadcrumb to workspace > identifier
- Extract sidebarContent variable shared between ResizablePanel and mobile Sheet
- All sidebar sections collapsible (Properties, Parent issue, Details, Token usage)
- Auto-close sidebar on mobile breakpoint
- Collapsible section headers: text before chevron, !size-3 stroke-[2.5], hover bg

Project Detail:
- Match Issue Detail layout pattern (header inside left ResizablePanel)
- Extract sidebarContent, add mobile Sheet support
- All sidebar sections collapsible (Properties, Progress, Description)
- Header: move three-dot menu to right button group, unified breadcrumb layout
- Auto-close sidebar on mobile breakpoint

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:50:00 +08:00
Bohan Jiang
a94c6481dd fix: compute sub-issue progress from database instead of paginated client cache (#865)
The sub-issue progress indicator (e.g. "0/2") was undercounting because
it was computed from the client-side issue list, which only loads the
first 50 done issues. Sub-issues marked as done beyond that page were
excluded from both the total and done counts.

Added a dedicated backend endpoint (GET /api/issues/child-progress) that
aggregates child issue counts directly from the database, ensuring
accurate totals regardless of client-side pagination or filtering.

Fixes MUL-702
2026-04-13 19:10:28 +08:00
Jiayuan Zhang
faee939312 feat(issues): add project filter to Issues tab (#671)
Support filtering issues by project in the Issues tab filter dropdown,
including a "No project" option for issues without a project assigned.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 23:23:45 +08:00
LinYushen
b52c048c8e fix(my-issues): use server-side filtering instead of client-side (#649)
* fix(my-issues): use server-side filtering instead of client-side

My Issues was fetching ALL workspace issues and filtering client-side,
causing the Done column to show wrong counts (269 vs user's actual
count) and only 2-3 done issues to appear from the first 50-item page.

Backend:
- Add creator_id and assignee_ids (uuid[]) filters to ListIssues,
  ListOpenIssues, and CountIssues SQL queries
- Parse creator_id and assignee_ids (comma-separated) query params

Frontend:
- Add myIssueListOptions with per-scope server-filtered queries
- Each tab now calls the API with the right filter:
  Assigned → assignee_id, Created → creator_id,
  My Agents → assignee_ids
- Add useLoadMoreMyDoneIssues for server-filtered done pagination
- WS events invalidate My Issues cache via issueKeys.myAll

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(my-issues): merge duplicate load-more hooks into one

Both board-view and list-view were unconditionally calling two hooks
(useLoadMoreDoneIssues + useLoadMoreMyDoneIssues) and picking one at
runtime. Merged into a single useLoadMoreDoneIssues with an optional
myIssues param so only one hook runs per render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:54:13 +08:00
LinYushen
f53cdf3157 fix(views): show user-scoped done count on My Issues page (#647)
The Done column on My Issues was displaying the workspace-wide total
(e.g. 269) instead of the current user's done issue count, because
BoardView/ListView read doneTotal directly from the shared cache.

Add an optional doneTotal prop to BoardView and ListView so the parent
can override the displayed count. MyIssuesPage now computes the count
from the client-filtered issue list and passes it through.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:00:18 +08:00
Naiyuan Qing
5878bddd6b refactor(core): move my-issues view store to packages/core/issues/stores
The my-issues view store is shared client state that doesn't depend on
any UI library. Move it from packages/views/my-issues/stores/ to
packages/core/issues/stores/ to follow the no-duplication rule and keep
state factories together with related issue stores.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 11:33:37 +08:00
Naiyuan Qing
ba32f3a187 chore: add shared ESLint config + enforce strict tsconfig across packages
- Add @multica/eslint-config package (base, react, next configs)
- Replace `next lint` (removed in Next.js 16) with `eslint .`
- Add lint scripts to all packages and desktop app
- Add noUnusedLocals, noUnusedParameters, noImplicitReturns to base tsconfig
- Fix all resulting TS/ESLint errors (unused imports, missing returns,
  stale eslint-disable comments from legacy eslint-config-next)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:27:29 +08:00
Bohan Jiang
c8f0f3dc9d feat(views): show sub-issue progress in list rows (#566)
* feat(views): show sub-issue progress indicator in issue list rows

When an issue has sub-issues, display a circular progress ring with
done/total count (e.g. "2/3") in the list row. Progress is computed
from the already-loaded issue list without additional API calls.

Extracts ProgressRing into a shared component reused by both
issue-detail and list-row.

* feat(views): refine sub-issue progress UI and add to board view

- Move progress badge right after issue title (not pushed to far right)
- Increase progress ring size from 11px to 14px for better visibility
- Add sub-issue progress indicator to board card view
- Thread childProgressMap through BoardView → BoardColumn → BoardCard
2026-04-09 16:52:12 +08:00
Naiyuan Qing
f41a0cf423 feat(views): extract packages/views — shared business UI + navigation adapter
- Create NavigationAdapter interface (push, replace, back, pathname, searchParams)
- Create AppLink component replacing next/link in 4 files
- Replace useRouter → useNavigation in 3 files (issue-detail, create-issue, create-workspace)
- Create WebNavigationProvider wrapping Next.js useRouter/usePathname/useSearchParams
- Move ~85 feature UI files (issues, editor, modals, my-issues, skills, runtimes) to packages/views/
- Add store singleton registration pattern (registerAuthStore, registerWorkspaceStore)
- Create data-aware wrappers in packages/views/common/ (ActorAvatar, Markdown)
- Update all app-layer imports to @multica/views/*
- Add @source directive for Tailwind to scan views package
- packages/views/ has zero next/* imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:49:55 +08:00