Adjust the manual create-issue dialog toolbar so the add-label entry is
surfaced directly, and move the lower-frequency due date into the ⋯ menu:
- Add a Labels picker in the slot Due date used to occupy. LabelPicker
gains a draft mode (no issueId): selection is held via selectedIds /
onSelectedIdsChange and attached to the issue right after it's created
(the create endpoint takes no labels), mirroring the sub-issue linking
pattern already in this dialog.
- Collapse Due date into the ⋯ overflow menu with the same reveal rule as
Start date (inline only when it has a value or was just opened). Give
DueDatePicker the controlled open/onOpenChange props StartDatePicker
already had.
- Persist chosen labels in the issue draft store (labelIds) like every
other draft field.
- Add useAttachLabelToIssue (variables-based attach) so labels can be
attached to a just-created issue.
- i18n: add create_issue.set_due_date and toast_link_labels_failed across
en / zh-Hans / ja / ko.
MUL-3971
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* 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>
* 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>
* fix: keep issue draft attachment records
Co-authored-by: multica-agent <github@multica.ai>
* fix: avoid persisting signed draft attachment urls
Co-authored-by: multica-agent <github@multica.ai>
* fix: reuse resolved media url for draft previews
Co-authored-by: multica-agent <github@multica.ai>
* fix: address draft attachment review nits
- Backfill an empty caller download_url from the in-session upload on id
collision so a just-pasted image first-paints from the signed URL
instead of detouring through markdown_url.
- Prune draft attachments no longer referenced by the persisted
description when the create dialog reopens.
- Backfill EMPTY_DRAFT defaults on draft-store rehydrate so drafts
persisted before the attachments field existed get a stable shape.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
cleanupDeletedIssueCaches now also calls a new
useRecentIssuesStore.forgetIssue(wsId, issueId) action so the persisted
Recent Issues bucket no longer keeps deleted ids around. Both the delete
mutation and the WS delete event flow through the same cleanup, so this
covers self-delete and cross-client delete. Without this, Cmd+K fires a
detail query for every recent id on open and returns a steady stream of
404s for issues the user has deleted (#3413).
MUL-2765
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(views): swimlane supports parent / project / assignee grouping (MUL-2711)
The swimlane view was hard-coded to group by parent issue. This adds a
display dropdown so users can pick parent (default), project, or
assignee — analogous to how the board view exposes its grouping option.
- Generalise the lane builder in swimlane-view.tsx behind a `LaneGroup`
abstraction (matcher + per-grouping `moveUpdates` payload) so the
drag-end handler no longer branches on grouping. Cell ids gain a
`<grouping>:<rawId>` prefix and lane sortable ids include the
grouping so dnd-kit cannot collide entries from different groupings.
- Extend the view store with `swimlaneGrouping`, `swimlaneOrders` (one
saved order per grouping), and a grouping-keyed `collapsedSwimlanes`.
The persist `merge` defends against the old `string[]` shape so a
pre-upgrade snapshot doesn't crash on first read.
- Wire `setSwimlaneGrouping` into the issues display popover next to
the existing board grouping control. Add en / zh-Hans copy for the
three swimlane buckets (Parent issue / Project / Assignee) and the
two new pinned lanes (No project / Unassigned).
- Expand swimlane tests with parent / project / assignee smoke cases
and update existing mocks to the new lane-id format. Add stable
`useActorName` / `projectListOptions` mocks to avoid the
set-state-in-effect loop that an unstable `getActorName` would
trigger via the cells-rebuild memo.
Co-authored-by: multica-agent <github@multica.ai>
* feat(views): default swimlane grouping to assignee
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* 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>
* 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>
* feat(projects): add Project Gantt view (MUL-1881)
Adds Gantt as a third option in the Project page's view toggle (Board /
List / Gantt). Bars span start_date → due_date; issues with only one
date render as markers, issues with neither are collapsed into an
Unscheduled section. Toolbar exposes day/week/month zoom and a
show-completed toggle. The Gantt view shares the existing IssuesHeader
filters/sort.
Implementation is self-rendered SVG/HTML — no new dependencies. UTC
day-aligned date math keeps bars on the right columns regardless of
viewer timezone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): scope Gantt to project surface + warn on hidden pages
- IssuesHeader / IssueDisplayControls now take `allowGantt` (default false);
only Project Detail opts in. /issues, /my-issues and the actor panel no
longer expose a Gantt option that silently fell through to List, and the
toggle icon falls back to List when a stored `viewMode === "gantt"` lands
on a surface that doesn't render it.
- Project Gantt now surfaces a banner with hidden-issue count plus a
Load-all action that drains every remaining paginated page into the
cache via the new `useLoadAllRemaining` helper. Pagination summary comes
from `myIssueListPaginationOptions`, which shares the existing cache key
with `myIssueListOptions` so totals stay in sync with Board/List.
- ScheduledRow normalizes a `start_date > due_date` anomaly to min/max and
outlines the bar with a destructive ring + tooltip note, instead of
silently dropping the row.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(agents): list-only tasks panel with issue search (MUL-2391)
Replace the agent detail tasks view-mode toggle with a fixed list view and
add a search bar that filters by issue title, identifier, or pinyin.
Co-authored-by: multica-agent <github@multica.ai>
* fix(actor-issues): only show search empty state when searching
Previously the panel rendered the search empty state whenever the
filtered issue list was empty, which masked ListView's own status-based
empty states when status/priority/assignee/project/label filters
narrowed the list to 0. Now search_empty only renders when
`search.trim()` is non-empty and results are 0; otherwise ListView
takes over and shows its native empty states.
Refs MUL-2391
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Sidebar "新建 issue" button, command palette "New Issue", and the `c`
shortcut all hard-coded which create modal to open, ignoring the
persisted lastMode in useCreateModeStore. Pressing `c` after switching
from agent → manual reverted to agent on the next open.
Add `openCreateIssueWithPreference(data?)` helper next to the store.
Generic entries call it; entries that pre-seed manual-only fields
(status, project_id, parent_issue_id from board / list / project /
sub-issue actions) keep opening "create-issue" directly because agent
mode does not honour those seeds.
Co-authored-by: multica-agent <github@multica.ai>
* fix: execution log name rendering and squad assignee support
- Strip mention markdown in trigger_summary ([@Name](mention://...) → @Name)
so execution log rows show clean text instead of raw markdown
- Add squad to ActorFilterValue type so squad assignees are filterable
- Add squad section to assignee filter dropdown in issues-header
- Add i18n keys for squads_group (en/zh-Hans)
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR #2575 review feedback
1. Extract stripMentionMarkdown as reusable helper with proper regex
- Handles escaped brackets in names (e.g. David\[TF\])
- Skips backslash-escaped mentions (\[@...])
- Handles issue mentions (no @ prefix)
- Does not touch regular markdown links
- 10 unit tests added
2. Squad only appears in Assignee filter, not Creator
- Added showSquads prop to ActorSubContent (default true)
- Creator filter passes showSquads={false}
3. Squad included in Agents scope
- issues-page scope filter now includes squad in agents scope
- 2 regression tests added for scope coverage
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(quick-create): searchable actor picker + squad support (MUL-2163)
- Replaces the flat agent dropdown in the "Create with agent" modal with a
searchable PropertyPicker that lists Agents and Squads in separate
sections, so users can filter by name and pick a squad as the creator.
- Persists the selection as (lastActorType, lastActorId), removing the
agent-only lastAgentId field on the quick-create store.
- Adds squad_id to the quick-create API request and stamps it onto the
task's QuickCreateContext. The handler resolves the squad to its leader
agent (re-using validateAssigneePair) and the daemon claim path injects
the squad-leader briefing when the task carries a squad hint, matching
the behavior of issue-bound squad tasks.
Co-authored-by: multica-agent <github@multica.ai>
* fix(create-issue): forward squad picks across manual→agent switch
Manual mode → agent mode previously only carried `agent_id`, so picking
a squad and then flipping to agent silently fell back to the persisted
actor / first visible agent and lost the user's choice. Carry `squad_id`
on the same branch so the agent panel honors the squad pick.
Adds a sibling test alongside the existing project-carry case.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* perf(views): virtualize issue detail timeline with react-virtuoso
The unvirtualized timeline at issue-detail.tsx full-mounted every
entry, freezing first paint for several seconds at 500+ comments
(markdown parse + lowlight per CommentCard on mount). Production p99
is ~30 comments but the all-time max is ~1.1k and the server hard-caps
at 2000 — long-tail issues were unusable.
Swap the inline `.map` for `<Virtuoso customScrollParent>` driven by a
flattened TimelineItem discriminated union. TanStack Query stays the
source of truth; existing memo machinery (`prevThreadRepliesRef`,
`EMPTY_REPLIES`) and WS handlers are untouched. `followOutput="auto"`
matches Slack/Discord — users at the bottom auto-follow new comments,
users mid-scroll are not yanked back down.
Comment drafts move to a new persisted Zustand store
(`comment-draft-store`) so virtualization-driven unmount can no longer
drop in-progress edits or new comments. Hydrates via ContentEditor
`defaultValue`, flushes on update / blur / visibilitychange.
Deep-link from inbox is rewritten from `getElementById` +
`scrollIntoView` to `virtuosoRef.scrollToIndex` with a double-rAF
mitigation for the Virtuoso #883 initial-scroll race. Highlight flash
bumped 2s→3s to outlast mount latency on cold cards.
Cmd-F shows a once-per-session toast on long timelines since browser
find-in-page can't reach off-screen virtualized items. Real in-app
search lands in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(views): repair deep-link scroll and isolate comment drafts
The first virtualization landing had three latent issues that runtime
testing on perf fixtures (10 → 5000 comments) exposed:
1. Deep-link landing position was wrong by ~380px on every issue.
In customScrollParent mode Virtuoso computes scrollTop from the
list's internal coordinate space only — it doesn't account for
sibling content (title editor, description, sub-issues, agent
card) sitting above the list inside the same scroll parent. The
useEffect now uses Virtuoso scrollToIndex only to MOUNT the
target into the DOM, then polls a `data-comment-id` anchor and
delegates positioning to the browser's scrollIntoView, which
honors getBoundingClientRect and lands accurately every time.
2. Scroll-up was being yanked back to the deep-link anchor on every
ResizeObserver tick. Root cause was `followOutput="auto"`, which
stays "stuck to bottom" once the deep-link lands there and resets
scrollTop to maxScrollTop on each height change. Issue detail is
document-shaped, not chat-shaped, so removing followOutput
altogether is the right tradeoff. Likewise `initialTopMostItemIndex`
acts as a persistent anchor in customScrollParent mode (Virtuoso
#458) — dropped entirely and replaced with imperative scroll.
`defaultItemHeight` is also dropped so Virtuoso probes real
heights instead of estimating + correcting visually.
3. Reply-comment deep-links from the inbox would short-circuit
because the reply id isn't in the flat items[] array. Added a
replyToRoot map so deep-link falls back to the enclosing thread's
root index, scrolls there, and lets the reply's own ring fire
once the thread is in view.
Also fixes a latent cross-issue draft leak in `<CommentInput>`:
web's /issues/[id] route doesn't remount IssueDetail on issueId
change, so without an explicit `key={id}` the editor kept the
previous issue's in-memory content and the next keystroke would
flush it under the new issue's draft key. The same fix incidentally
repairs the pre-existing "submit composer from issue A while viewing
issue B" submit-target bug.
Highlight UX polish: bg-brand/5 was too faint to notice; ring upgraded
to ring-brand/60 as the sole signal. transition-colors didn't actually
animate ring/box-shadow — switched to transition-shadow duration-500
ease-out so highlight has visible fade in / fade out. Flash duration
3s → 4s. Polling failure now still sets highlight + warns so a manual
scroll to the target still flashes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): namespace recent-issues by workspace id in state
The recent-issues store was using createWorkspaceAwareStorage, which
namespaces the storage key by the current slug. That broke whenever a
setter ran before WorkspaceRouteLayout's mount-effect set the slug —
child effects fire before parent effects in React, so recordVisit from
issue-detail wrote to the un-namespaced bare key, leaking visits across
workspaces. The /<slug>/issues page then fanned out a per-id GET for
each leaked id, mostly 404s.
Move the namespacing into the store state itself (byWorkspace keyed by
wsId), so reads/writes pick the right bucket at call time and don't
depend on a singleton being set before module hydration. Drop the
storage-level namespacing and the rehydration registration for this
store.
Add pruneWorkspaces to evict buckets for workspaces the user is no
longer a member of, wired into useDashboardGuard so it runs whenever
the workspace list resolves. As a defense against the prune never
firing, cap the total tracked workspaces at 50 (LRU on oldest visit).
Bump persist version to 1; the v0 entries don't know which workspace
they belonged to, so migrate drops them and the cache repopulates as
the user visits issues.
* fix(core): fail closed on null slug in workspace-aware storage
createWorkspaceAwareStorage used to fall back to the un-namespaced bare
key when no workspace was active. That fallback let any setter firing
before WorkspaceRouteLayout's mount-effect (e.g. a child component's
own mount-effect) leak workspace-scoped data into a global slot
visible to every workspace. Initial zustand persist hydration also ran
in this null-slug window, so every store would read the polluted bare
key on first load.
Drop the fallback: null slug → getItem returns null, setItem/removeItem
are no-ops. Stores still get a correct read via their registered
rehydrate fn once setCurrentWorkspace fires. The remaining nine stores
using this storage no longer rely on the bare-key path either; their
data has always been intended to be workspace-scoped.
---------
Co-authored-by: YYClaw <yyclaw0@gmail.com>
* feat(quick-create): add project picker that remembers last pick
Quick-create users targeting one project repeatedly had to restate "in
project X" in every prompt. The modal now exposes a project picker beside
the agent picker, persists the selection per-workspace, and pins the
agent's `multica issue create` invocation to that project so the prompt
text doesn't have to.
The picked project also flows to the daemon as ProjectID/ProjectTitle and
its github_repo resources override the workspace repo fallback — same
treatment issue-bound tasks already get.
Co-authored-by: multica-agent <github@multica.ai>
* fix(quick-create): move project picker into property pill row
Reviewer feedback: the picker felt out of place wedged next to the agent
header. Move it into a property toolbar row above the footer, reusing the
shared `ProjectPicker` + `PillButton` so its placement and styling line up
exactly with the manual create panel.
This also drops the bespoke dropdown / aria / label strings that were only
needed while the picker rendered inline beside "Created by".
Co-authored-by: multica-agent <github@multica.ai>
* fix(quick-create): clear stale persisted project + carry across mode switch
Two review-blocking bugs in PR #2321:
1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0
and only cleared local state, leaving lastProjectId pointing at a deleted
project. The next open re-seeded the dead UUID and submit hit the server's
`project not found` rejection. Gate on the query's `isSuccess` so we can
tell "loading" apart from "loaded as empty", and clear both local state
and the persisted preference when the selection isn't in the resolved list.
2. ManualCreatePanel's switchToAgent dropped the picked project from the carry
payload, so flipping manual → agent silently fell back to the agent panel's
own lastProjectId — potentially routing the issue to a different project
than the one shown in manual mode. Forward project_id alongside prompt /
agent_id, and add a regression test.
Co-authored-by: multica-agent <github@multica.ai>
* test(quick-create): pass new isExpanded props in stale-project tests
Main got an expand button on AgentCreatePanel via #2320 while this branch
was open, adding `isExpanded` / `setIsExpanded` to the panel's required
props. The two new stale-project tests still passed `{ onClose }` only,
which CI's typecheck (run on the main+branch merge) caught while my
local run did not.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
When creating an issue with agent, the input content was lost when
navigating away (e.g., to view a ticket) and returning. Manual create
already persisted its draft - now agent create does too.
Changes:
- Add prompt field to useQuickCreateStore (persisted with workspace)
- AgentCreatePanel reads initial prompt from draft store if no transient
data.prompt is provided
- onUpdate now saves prompt to draft store (not just hasContent)
- clearPrompt() called after successful submit
Fixes: #1957
* feat(views): keep quick capture open after submit for continuous creation
After successfully sending a prompt to the agent, the dialog now clears
the editor and stays open instead of closing. This lets users create
multiple issues in quick succession without reopening the dialog each
time. The user can still close manually via X or Escape.
* feat(views): add success feedback for quick capture continuous mode
After each successful submit, the Create button briefly flashes green
with a checkmark "✓ Sent" for 1.5s, then reverts. A persistent counter
("N sent") appears in the footer so the user knows how many prompts
they've dispatched in this session. No explicit mode toggle needed —
the counter implicitly signals continuous mode is active.
* feat(views): add "Create another" toggle to quick capture (Linear-style)
Replace always-on continuous mode with an opt-in toggle switch in the
footer, matching Linear's "Create more" pattern. The preference is
persisted per-workspace via the quick-create store so it remembers
across sessions.
- Toggle OFF (default): submit closes the dialog (original behavior)
- Toggle ON: submit clears the editor and stays open; button flashes
green "✓ Sent" and a counter shows how many have been dispatched
* fix(views): remove stale breadcrumb identifier test
PR #1872 removed the issue identifier from the breadcrumb but the
corresponding test was not updated, causing CI to fail.
Recasts Quick/Advanced as Agent/Manual and lets users flip between modes
in-place from a footer switch button instead of a separate Advanced
shortcut. The two old modal types now route through one CreateIssueDialog
shell that owns the single <Dialog> and <DialogContent> — only the inner
panel body swaps on mode change, so the Portal/Backdrop/Popup stay
mounted and the switch is instant (no close→open animation flash).
Mode preference is persisted globally in localStorage via a small
useCreateModeStore, so the `c` shortcut always opens whichever mode the
user last used (or switched to). Carry payload (description / agent /
prompt) hands off through the shell's local state plus the existing
issue-draft store, so nothing the user typed is lost across switches.
Also drops the Shift+C → manual branch — `c` is now mode-agnostic and
the in-modal switch covers the same intent without users having to
remember a second shortcut.
Visible labels: "Quick create" → "Create with agent",
"New issue" → "Create manually".
* feat(server): add quick-create issue async task path
Adds POST /api/issues/quick-create which validates the picked agent's
reachability up front (not archived, has runtime, runtime online) then
queues an issue-less agent task whose context JSONB carries the user's
natural-language prompt + requester + workspace. Daemon claim resolves
the workspace from the context, and the prompt builder switches to a
quick-create template instructing the agent to translate the prompt
into a single multica issue create call.
Task completion writes a success inbox item to the requester pointing at
the newly-created issue (located by querying the agent's most recent
issue in the workspace since task start, so we don't depend on agent
stdout shape). Failures write an action_required inbox item carrying the
original prompt + agent id so the frontend can offer "Edit as advanced
form" without losing input.
* feat(views): quick-create issue modal + inbox failure CTA
Adds a streamlined create-issue UI bound to the c shortcut: pick an
agent, type one line, submit. The modal closes immediately and the
agent translates the prompt into a multica issue create call in the
background. Shift+c keeps the legacy advanced form for users who want
every field. The "Advanced" button inside the new modal seeds the
shared issue-draft store with the prompt + picked agent so switching
mid-flow doesn't lose input.
Last-used agent persists per (user, workspace) via a workspace-aware
zustand store so frequent users skip the picker on every open.
Inbox renders quick_create_done items with a status pin to the new
issue and quick_create_failed items with an "Edit as advanced form"
CTA that re-seeds the legacy modal with the original prompt.
ApiError now carries the parsed JSON body so the modal can branch on
the structured agent_unavailable code without parsing the error
message.
* fix(quick-create): execenv injection, claim race, private-agent permission
Addresses GPT-Boy review on #1786:
1. execenv was rendering the assignment-task issue_context.md / runtime
workflow even for quick-create, telling the agent to call
`multica issue get/status/comment add` against an empty IssueID.
Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create
branch in renderIssueContext + the runtime_config workflow that
instructs the agent to run a single `multica issue create` and
exit, with explicit "do NOT call issue get/status/comment add"
guards.
2. ClaimAgentTask serialized only on issue_id / chat_session_id, so
concurrent quick-creates on the same agent (both NULL on those
columns) ran in parallel — making the success-inbox lookup race
over "most recent issue by this agent". Adds a third OR clause
that treats "all four FKs NULL" as a serialization key for the
same agent, so quick-create tasks on a given agent run one at a
time.
3. QuickCreateIssue handler bypassed the private-agent ownership rule
that validateAssigneePair enforces elsewhere — a user could POST a
private agent_id they didn't own and trigger it. Now routes the
picked agent through validateAssigneePair before the runtime
liveness check.
4. Clarifies the quick-create-store namespacing comment to match the
actual workspace-aware StateStorage convention used by the other
issue stores (per-user is browser-profile-local).
* fix(quick-create): branch Output section + deterministic origin lookup
Addresses GPT-Boy's second-pass review on #1786:
1. The runtime_config.go Output section forced "Final results MUST be
delivered via multica issue comment add" for every non-autopilot
task — quick-create still got this conflicting instruction even
though there's no issue to comment on. Switched the Output block
to a three-way switch so quick-create gets a tailored "stdout is
captured automatically; do NOT call comment add" branch matching
the autopilot variant.
2. Completion lookup was "most recent issue created by this agent
since task.started_at", which races against concurrent issue
creates by the same agent (assignment task running alongside
quick-create when max_concurrent_tasks > 1). Replaced with a
deterministic origin link:
- Migration 060 extends issue.origin_type CHECK to allow
'quick_create'.
- Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a
quick-create task.
- multica issue create CLI reads the env var and stamps the new
issue with origin_type=quick_create + origin_id=<task_id>.
- Server CreateIssue handler accepts (origin_type, origin_id)
from trusted callers (only "quick_create" is allowed; the pair
is rejected unless both fields are provided together).
- notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on
(workspace_id, "quick_create", task.ID) — no more time-window
racing against parallel agent activity.
The old GetRecentIssueByCreatorSince query is removed.
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.
* 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".
The create-issue modal now remembers the assignee picked at submit
time and prefills the picker with that value when the modal next
opens. Implemented by tracking lastAssigneeType/Id alongside the
draft and seeding clearDraft's reset with those values.
- my-issues page lost labels because myIssuesViewStore cherry-picked
name/storage/partialize from viewStorePersistOptions and dropped the
cardProperties-aware merge. Persisted snapshots predating the labels
toggle had cardProperties.labels = undefined, falsy-shorting the chip
render. Extracted mergeViewStatePersisted as a generic and wired it
into both stores.
- list-row chips now render right after the title (with a small left
margin for breathing room) instead of in the right-aligned cluster.
* feat(issues): render labels on list/board with bulk server-side fetch
ListIssues / ListOpenIssues / GetIssue now bulk-fetch labels per response
via a new ListLabelsForIssues query so the client gets labels in a single
round-trip instead of N requests per visible issue. List-row and board-card
read issue.labels directly; an issue_labels:changed WS handler patches the
list and detail caches in place so chips stay live across tabs, and
attach/detach mutations mirror their result into the same caches for
immediate same-tab feedback.
Adds a "Labels" toggle to the card properties dropdown (defaults on).
* fix(issues): preserve cached labels and refresh on label edit/delete
Three fixes from gpt-boy's review of #1741:
1. IssueResponse.Labels was a non-omitempty slice, so paths that didn't
load labels (UpdateIssue, batch updates, the issue:updated WS broadcast)
serialized labels:null. onIssueUpdated then merged that null into the
list/detail caches, wiping chips on every other tab whenever any non-
label field changed. Switched to *[]LabelResponse + omitempty: nil =
field absent (client merge keeps existing labels); non-nil (incl. empty
slice) = authoritative.
2. issue.labels is a denormalized snapshot, but useUpdateLabel /
useDeleteLabel and the WS label:* prefix only touched labelKeys, leaving
stale chips in list/board after rename/recolor/delete. Mutations now
also invalidate issueKeys.all(wsId), and the realtime refreshMap maps
the label prefix to both labels and issues invalidation for cross-tab.
3. Persisted cardProperties from before this branch lacks the new `labels`
key. Render fell back to `?? true` but the dropdown switch read it raw
and showed unchecked. Added a custom Zustand merge that deep-merges
cardProperties so newly added toggles inherit defaults for existing
users; dropped the `?? true` fallbacks now that the store guarantees
the key.
Adds two new toggleable card properties that surface issue context at a glance:
- Project: shows the parent project icon + title when the issue belongs to one.
- Sub-issue progress: gates the existing progress ring behind a card property
so users can hide it when not useful.
Both default to on; toggled via the existing "Display" popover.
* feat(issues): persist comment collapse state across page reloads
Store collapsed comment IDs in a workspace-scoped Zustand store backed
by localStorage, replacing the transient useState(true) default.
Comments now remember their collapsed/expanded state per issue.
* test(issues): add useCommentCollapseStore mock to issue-detail tests
The existing vi.mock for @multica/core/issues/stores didn't include the
newly exported useCommentCollapseStore, causing CommentCard to throw at
render time.
* 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>
* 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>
Recent issues store was duplicating server data (title, status, identifier)
in Zustand, violating the single-source-of-truth architecture. Now the store
only tracks visit records (id + visitedAt), and the search command joins
fresh data from the TanStack Query issue list cache at render time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
* feat(search): show recent issues list when cmd+k opens
When opening the cmd+k search dialog, display a list of recently visited
issues instead of the empty placeholder. Visits are tracked via a
workspace-scoped persisted Zustand store (max 20 items).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(search): close cmd+k dialog on single ESC press
cmdk was consuming the first ESC to clear internal state, requiring a
second press to close the dialog. Intercept ESC on the CommandPrimitive
and close the dialog directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(search): move ESC handler to input to prevent double-ESC
The previous handler on CommandPrimitive didn't fire because cmdk
intercepts ESC at the input level. Moving the onKeyDown to
CommandPrimitive.Input ensures it fires before cmdk processes it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(search): use capture-phase ESC listener to close dialog reliably
The previous onKeyDown approach on the Input didn't work because
base-ui Dialog's internal focus management handled ESC before the
React synthetic event. Use a document-level capture-phase listener
that fires before all other handlers and stops propagation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(search): cover single-escape command palette close
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create createWorkspaceAwareStorage that dynamically namespaces
localStorage keys by workspace ID (e.g. "multica_issue_draft:ws_abc").
Wire setCurrentWorkspaceId into workspace store lifecycle methods and
migrate all workspace-scoped stores (draft, view, scope) to use it.
Navigation store intentionally left user-scoped without namespace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>