mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
transcript-all
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40f9ecdd56 |
MUL-5202: unify Issue Query across List, Board, and Swimlane (#5820)
* MUL-5202: migrate status issue surfaces to table query Co-authored-by: multica-agent <github@multica.ai> * MUL-5202: unify grouped issue surfaces Co-authored-by: multica-agent <github@multica.ai> * MUL-5202: cover move safety boundaries Co-authored-by: multica-agent <github@multica.ai> * MUL-5202: preserve server swimlane semantics Co-authored-by: multica-agent <github@multica.ai> * MUL-5202: keep grouped surface facets exact Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b85bb71a58 |
feat: custom issue properties — typed workspace-defined fields with list-surface support (MUL-4463) (#5335)
* feat(server): custom issue properties — definitions, typed values, CLI (MUL-4463)
Workspace-level property definitions (issue_property table; 7 types:
text/number/select/multi_select/date/checkbox/url) plus a typed value bag
on each issue (issue.properties JSONB keyed by definition UUID, mirroring
the metadata machinery: single-key atomic writes, 16KB cap, GIN index).
- Definitions: owner/admin only; agent actors rejected (agents propose,
humans confirm). 20 active per workspace, 50 options per select,
reserved built-in names blocked, archive instead of delete.
- Values: any member or agent; per-type validation with self-correcting
error messages that enumerate legal option ids.
- API: /api/properties CRUD + PUT/DELETE /api/issues/{id}/properties/{propertyId};
issue responses always emit the properties bag.
- CLI: multica property list/get/create/update/archive/unarchive and
multica issue property list/set/unset with name→id translation.
- Events: property:created/updated, issue_properties:changed.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom properties settings tab + issue sidebar editors (MUL-4463)
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 1 — mobile CI, option guard, mutation safety, schema tolerance
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): renumber issue properties to 179 and build the GIN index concurrently
main's migration sequence advanced twice under this PR (167 collision, then
an upstream renumber wave that claimed 178), so issue properties now sits at
179 — verified against main's current tip by the prefix-uniqueness lint.
The properties GIN index moves to its own single-statement migration (180)
using CREATE INDEX CONCURRENTLY — a plain CREATE INDEX on the hot issue
table would block writes for the duration of the build. Mirrors the
119_user_created_at_index pattern; full-chain dry-run on a fresh database
passes through 180.
Co-authored-by: multica-agent <github@multica.ai>
* feat(web): custom-property list surfaces — filter, cards, sort, board grouping (MUL-4463 M2)
Brings custom properties to the issue list surfaces on top of the M1
definitions/values core:
- Filter: per-definition sections in the Filter dropdown (select /
multi_select options with color dots and counts; checkbox as Yes/No
pseudo-options). OR within a definition, AND across definitions;
client-side in applyIssueFilters, mirrored into filterAssigneeGroups
for the assignee-grouped board. Included in active-filter count and
Clear all.
- Cards: per-property Display toggles (cardPropertyIds) render value
chips on board cards and list rows via CustomPropertyValueDisplay.
- Sort: SortField gains property:<id> for number/date definitions.
Server keeps position order (fixed sort enum); the surface controller
re-sorts client-side, swimlane/gantt reuse the same comparator.
Date-only strings compare lexically; missing values sort last.
- Board grouping: IssueGrouping gains property:<id> for select
definitions — one column per option (definition order) plus a
trailing No-value column, option-colored headings. Drag-drop moves
position via UpdateIssue and applies the value through
useSetIssueProperty/useUnsetIssueProperty (properties are not part
of UpdateIssueRequest). Stale persisted property groupings fall back
to status columns.
View-store: propertyFilters + cardPropertyIds persisted via the
partialize allowlist; clearFilters resets property filters; new fields
deep-merge cleanly into pre-existing persisted snapshots.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address MUL-4463 review round 2 — desc sort, option bucketing, archived-state reconciliation
- sort: direction now applies to value comparison only; issues without a
value sort last in BOTH directions (the whole-array reverse flipped them
to the front on desc). Test covers the desc+missing case.
- board: values referencing an option removed from the definition bucket
into the No-value column instead of vanishing (unmatched column ids
dropped the issue entirely). Defense-in-depth behind the new server-side
in-use guard; drag-utils test locks both behaviors.
- controller: persisted propertyFilters keyed by archived/deleted
definitions are stripped before reaching the filter predicates, and a
persisted property sort on a non-active definition degrades to manual
order — previously both kept silently applying while the header claimed
otherwise. The filter badge counts only active-definition filters.
Co-authored-by: multica-agent <github@multica.ai>
* feat(properties): server-side property filtering and sorting on list endpoints
Property filter/sort now execute in the database, so results are correct
across the full issue set — not just the loaded 50-per-status window
(closes MUL-4493 item 1's filter/sort half; requested on MUL-4463).
- New `properties` query param on ListIssues and ListGroupedIssues:
JSON {definitionId: [values]} compiled to an AND-of-ORs containment
check (double NOT EXISTS over jsonb_array_elements). One value expands
to every storage shape it could match — string (select), array element
(multi_select), boolean (checkbox) — so the handler stays type-agnostic.
Guarded at 20 definitions / 50 values.
- `sort=property:<definitionId>` resolves the definition and orders by a
typed expression (numeric CASE cast for number, NULLIF text for
date/text/url); missing values sort last in both directions. Malformed
ids 400; unknown/archived definitions degrade to position order instead
of breaking stale clients.
- Frontend: the property filter and property sort ride the IssueSortParam
window bag, so every surface (workspace + my-issues variants), query
key, and per-status load-more page carries them automatically. The
client-side re-sort layer is gone; applyIssueFilters keeps its property
predicate as an optimistic-update backstop.
- Regression test seeds 55 issues and proves a match at position 55 is
returned by a filtered 50-row page, plus sort order/missing-last,
AND-across-definitions, and the 400/fallback sort paths.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): address review round 3 — cache reconcile, merged-scope order, GIN-indexable filter, pool loader
- Cache reconciliation: property value writes (mutation settle + WS event)
now invalidate every issue window whose server-side shape depends on
property values — queries filtered by `properties` or sorted by
`property:<id>` (detected via query-key predicate), covering flat lists,
assignee groups, and my-issues variants. Windows without property params
keep the cheap in-place patch. Fixes stale ordering/membership/counts
under staleTime:Infinity.
- My Issues "All" scope: merged assigned/created/involves results are
re-sorted with a comparator mirroring the server ORDER BY semantics
(including property sorts and missing-last, created_at DESC tiebreak) in
both the flat and assignee-grouped merge paths — relation concatenation
no longer overrides the user's sort.
- Filter predicate rebuilt as plain bind-parameter containment ORs
(AND across definitions): EXPLAIN now shows BitmapOr over
idx_issue_properties_gin (the correlated jsonb_array_elements form
defeated the index). Alternatives capped at 256 bind params.
- Property-grouped board gains a pool loader strip: one sentinel per
status that still has server rows, keeping every issue reachable until
per-column pagination lands (MUL-4493).
- Windowing regression test hardened: explicit positions + an assertion
that the unfiltered first page excludes the target (the old fixture tied
at position 0 and the created_at DESC tiebreak put the target on page
one, proving nothing).
- Rollback safety: /api/properties 404 (old server) degrades to an empty
catalog instead of a query error, which also keeps property params from
ever being sent to pre-property servers; migration 179's CHECK
constraints switch to NOT VALID + VALIDATE so the exclusive lock is
instantaneous.
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): harden concurrency and cache coordination from clean-room review
Backend (MUL-4762 F1/F4/F5):
- withPropertyLock: pg_advisory_xact_lock helper; definition create/update
and value writes now serialize config-vs-value and cap-vs-insert races
(workspace-level 'props:' lock + per-definition 'prop:' lock, ordered).
- propertySortExpr degrades archived definitions to position sort.
Frontend (F2/F3/F6):
- onIssuePropertiesChanged invalidates plain assignee-group caches too.
- Property value mutations cancel list refetches in onMutate and roll back
only the touched key against the current bag (concurrent WS writes to
other keys survive a failed write).
- useUpdateIssue reconcile drops the stale properties bag from the server
snapshot; the property pipeline owns that field.
- Surface controller passes persisted property filters/sorts through
until the catalog query settles (cold cache no longer strips them).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): open_only branch honors the properties filter
ListOpenIssues takes the parsed AND-of-ORs containment groups as a single
jsonb properties_filter param and unrolls them with a static double
NOT EXISTS; previously the open_only path parsed the properties param and
silently dropped it (clean-room review F7a).
Co-authored-by: multica-agent <github@multica.ai>
* fix(properties): toast on failed board drag to a property column
Property-column drags rolled the card back silently on failure; mirror
the status/assignee drag path (use-issue-surface-actions) so the
snap-back is explained (clean-room review F3, drag half).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
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> |
||
|
|
37d9fafda6 |
feat(issues): add Remove parent issue action (MUL-3764) (#4630)
* feat(issues): add Remove parent issue action to promote a sub-issue to standalone (MUL-3764) Surfaces a discoverable UI affordance for clearing an issue's parent — the backend and CLI (multica issue update --parent "") already support it, but the Official App only exposed Set parent. Adds: - A 'Remove parent issue' item in the issue actions menu (dropdown + right-click), shown only when the issue has a parent. - A hover unlink button on the parent card in the issue detail sidebar. - A removeParent handler that clears parent_issue_id and stage in one write (stage only orders sub-issues under a parent) with a success toast. Closes #4629 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): toast remove-parent on success only, prune old parent's children cache (MUL-3764) Addresses review feedback on #4630: - use-issue-actions.ts: the remove-parent success toast fired eagerly after mutate(), so a request that failed on permission/network/validation would flash "removed" before the error toast and optimistic rollback. Move it to onSuccess so only a server-confirmed detach is announced. - mutations.ts: when a write re-parents an issue away from its current parent, prune it from the old parent's children cache instead of patching it to parent_issue_id: null in place. The parent's sub-issues list renders that array directly, so the orphaned row used to linger until the settle refetch. onError still restores prevChildren, so the prune rolls back on failure. Adds cache-prune coverage (optimistic remove / rollback / non-reparenting no-op) and onSuccess-vs-onError toast coverage. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e7d28bff1 |
fix(issues): emit project_changed so moved issues leave the old project list (MUL-3669) (#4571)
The per-project issue list rides the filtered myAll cache. Changing an issue's project is a membership change, but the surgical patch (patchIssueInBuckets) is filter-blind and never removes a card that no longer matches the list's project filter — so a moved issue stayed visible in the old project's list until a manual refetch (#4548 / MUL-3669). Root cause: project_id was the only membership-affecting field with no server *_changed flag. The WS handler fell back to diffing project_id against its own cache, which breaks once onMutate has optimistically overwritten the cached value on a local move. - server: stamp project_changed on issue:updated (UpdateIssue + Batch), alongside status_changed / assignee_changed. - events.ts: surface project_changed (optional, additive — old clients ignore). - ws-updaters: prefer the server flag, fall back to the cache diff only when absent (older backend) so a new frontend on an old backend does not regress. - mutations: onSettled invalidates myAll when project_id changed — a local safety net that never depends on the WS echo (update + batch). Tests: WS flag wins over a matching cache (local-move repro), explicit false suppresses the legacy diff, the cache-diff fallback still fires, and both mutations invalidate myAll on a project change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
45dae3185f |
fix(issues): eliminate optimistic-update drag flicker (board, list, batch, WS) (#4415)
* fix(issues): stop kanban card snapping back on drag A cross-column drag on a non-position-sorted board left the card in its origin column for the whole request, then jumped to the target only when the mutation settled — the "snaps back, then moves" glitch. Root cause was three coupled choices in the optimistic path: - board-view never updated local columns on drop for sortBy != "position" (onDragOver is a no-op there), so the card relied on the settle refetch to move across. - useUpdateIssue invalidated the whole list on settle, replacing the column and re-landing the card even on success. - patchIssueInBuckets appended a moved card to the column tail instead of its position slot, so any later cache refresh teleported it to the end. Fixes: - board-view: optimistically move the card into the target column on drop for the non-position path (insertIdByPosition), and reconcile local columns from the cache on settle for both paths (revert on error now that the list is no longer refetched). - mutations: reconcile via onSuccess surgical patch of the returned entity; drop the list/detail invalidation from onSettled (aggregates still flush). - cache-helpers: patchIssueInBuckets inserts the moved/reordered card at its position slot; a plain field update still keeps its slot. Adds cache-helpers and drag-utils unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): patch My-Issues / Project board caches on move too The drag fix made the board reconcile local columns from its feeding cache on settle. The workspace board rides issueKeys.list (patched by onMutate), but the My-Issues and Project boards ride the myList cache, which the mutation did not patch — so a successful move snapped back on those boards. useUpdateIssue now patches/snapshots/rolls back every bucketed list cache (workspace list + myList), selected by the ListIssuesCache `byStatus` shape so grouped (assignee) and flat (gantt) caches are skipped. Adds renderHook regression tests covering both-cache optimistic move, both-cache rollback, and no-list-invalidation-on-settle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): drop redundant WS position->list invalidate onIssueUpdated already surgically patches the non-filtered workspace board via patchIssueInBuckets (cross-status move + same-column reorder). The extra `if (position) invalidateQueries(list)` re-pulled the whole board on top of that, re-introducing drag flicker through the echoed-back WS event. Removed. Filtered myAll lists still invalidate (membership can change there) — the client-side membership reconciliation for those is a separate follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): batch update patches myList + stops list refetch on settle - onMutate now patches both issueKeys.list and the filtered issueKeys.myAll bucketed caches, so a batch edit on a My-Issues / Project board is optimistic too. Previously only the workspace board was patched, so batch edits on those boards relied entirely on the settle refetch. - onSettled no longer invalidates issueKeys.list: the optimistic patch is a complete reconcile for these bucketed boards (batch changes status / priority / project, never a server-computed value), so a full-board refetch only re-introduced the flicker the single-issue path removed. Aggregate / grouped caches still refresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): list view optimistically moves row on non-position drag The sortBy != "position" branch called onMoveIssue without moving the row in local columns, so the row sat in its origin group for the whole request and only jumped across on settle -- the same snap-back the board view had before its fix. Now mirrors board-view: setColumns(insertIdByPosition) on drop so the settle rebuild is a visual no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep My-Issues/Project boards in place on non-membership WS change onIssueUpdated now surgically patches the filtered myList (myAll) caches and only invalidates them when the change can actually move an issue in/out of the filter: an assignee change (covers My-Issues direct-assignee + the involves leg + actor panels) or a project change (Project board). A pure status / position / priority / label change reconciles in place -- no refetch -- removing the last drag flicker on filtered boards. Uses the assignee_changed flag the server already sends on issue:updated (surfaced on IssueUpdatedPayload + forwarded by the realtime dispatch); project change is diffed client-side against the cached value. No predicate replication, no backend change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): add settle-lock to swimlane drag (no clobber mid-flight) The swimlane drag had no settle window: the resync useEffect (and the issueMap freeze) guarded only isDraggingRef, so a cache change landing after drop but before the move settled could rebuild localCells out from under the optimistic move. Adds isSettlingRef + settleVersion (mirroring board-view / list-view): the lock is held from drop until onMoveIssue settles, then released, forcing a single resync from the reconciled cache. onMoveIssue now accepts the same optional onSettled callback board/list already use; the parent handleMoveIssue supplies it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): extract shared useDragSettle hook for board + list board-view and list-view carried byte-identical drag/settle scaffolding (the local columns mirror, the dragging/settling locks, the post-move animation-frame throttle, and the settle callback). That duplication is exactly what let list-view silently drift earlier (it had lost the optimistic-move half of the fix, and its position-branch settle callback omitted the settleVersion bump). Extract the primitive into useDragSettle so both surfaces share one implementation and can't drift again. Behavior-preserving for board-view. For list-view the one intended alignment: its position-branch failed move now reverts, gaining the settleVersion bump board-view already had. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a123dfc2df |
MUL-3508: stage sub-issues so the parent wakes per stage, not per child (#4410)
* feat(issues): stage sub-issues so the parent wakes per stage, not per child Sub-issues under a parent can be grouped into ordered stages (issue.stage). The child-done -> parent notification + assignee wake now fire only when a stage barrier closes: every sub-issue in the lowest unfinished stage has reached a terminal status (done/cancelled). An unstaged sibling set is one implicit stage, so the parent is woken once when the last sub-issue finishes instead of on every child — the default fix for the fire-on-every-child cascade reported in discussion #4320 / MUL-3508. Stage advancement stays agent-driven: the server only detects the closed barrier and wakes the parent assignee, who decides whether to promote the next stage. - DB: nullable issue.stage (CHECK >= 1) + sqlc regen - API: stage on issue create/update/response and batch update - CLI: `issue create`/`issue update` --stage; new `issue children` command that lists sub-issues grouped by stage (table + json) - stageBarrierClosed / stageProgressSummary in issue_child_done.go, with the wake comment now stage-aware, plus unit tests - skill docs (multica-working-on-issues SKILL.md + source map) Web UI (create-form stage picker, sidebar edit, group-by-stage display) is a follow-up; the API already returns stage for it to consume. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address review on stage barrier (cancel, batch, unstaged) Resolves the three blockers from the PR review: 1. Cancel can close a stage. The child-done barrier now fires on any non-terminal -> terminal transition (done OR cancelled), not just done. isTerminalChildStatus already treats cancelled as terminal (a cancelled sibling never finishes, so it must not hold a stage open), so a cancelled last-open child now closes its stage and wakes the parent. Keying on the transition also makes a later cancelled -> done edit a no-op, avoiding a lagging duplicate wake. 2. Batch update of stage no longer no-ops. `hasMutation` now includes "stage", so `{"updates":{"stage":N}}` persists instead of returning {"updated": 0}. 3. Unstaged children no longer participate in the staged frontier. In a staged sibling set, NULL-stage children neither hold a stage open nor fire on their own completion, and the wake comment no longer renders "Stage 0". This matches migration 123 ("NULL does not participate in staged grouping") and the CLI's separate unstaged group, removing the footgun where an unstaged backlog child silently blocked Stage 1. Tests: cancellation closes a stage (staged + unstaged), unstaged ignored in a staged set, stage summary skips unstaged, and a stage-only batch update persists. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * feat(web): stage UI — create picker, sidebar edit, group sub-issues by stage Frontend for the sub-issue stage feature (web + desktop, shared via packages): - core: `stage` on the Issue type + create/update request types; zod IssueSchema parses it (defaults to null for older backends) with schema tests for the numeric and omitted cases. - StagePicker component (mirrors the other property pickers): "No stage" + Stage 1..N, offering one beyond the current/sibling max. - Create-issue modal: a Stage pill, shown only when a parent is selected, threaded into the create payload. - Issue detail sidebar: an editable Stage row + "add property" entry, gated to sub-issues (issues with a parent). - Sub-issue list grouped by stage with per-stage headers (flat when unstaged). - i18n: stage keys across en / zh-Hans / ja / ko (parity test passes). Verified: full typecheck (6/6), core (591) + views (1433) vitest suites, lint clean (no new findings). Backend/CLI shipped earlier in this PR. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * test(issues): add stage to Issue fixtures merged from main The merge brought in new Issue fixtures that predate the required `stage` field: core issues/batch.test.ts, views batch-action-toolbar.test.tsx, and the mobile EMPTY_ISSUE_FALLBACK sentinel. Add `stage: null` so they satisfy the Issue type (mobile reuses core's IssueSchema for parsing, so only the sentinel needs it). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(web): feed StagePicker the sibling max stage so higher stages stay selectable The StagePicker accepts maxStage to extend its option list beyond the floored Stage 1-3, but neither call site passed it, so a parent with an existing Stage 4/5 child could not pick that stage when creating a new sub-issue or editing one in the sidebar. - Compute the sibling max stage at both call sites: the create modal now loads the parent's children (childIssuesOptions) and the detail sidebar reuses the already-loaded parentChildIssues. - Extract maxSiblingStage + stageOptions as pure helpers on stage-picker and unit-test them (the regression: a Stage 5 sibling keeps Stage 5 selectable and offers Stage 6). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
34c68e1e4c |
fix(comments): enforce single resolution per thread (#3984)
A thread could hold multiple resolved comments at once: ResolveComment was a plain per-row setter that never cleared the prior resolution, and "replacing" one was a display-only illusion (deriveThreadResolution picks the max resolved_at). The stale rows stayed resolved in the DB and the optimistic update flashed the new resolution, then reverted. Make single-resolution-per-thread a write invariant: - ClearOtherThreadResolutions: thread-scoped clear via a RECURSIVE CTE (root + descendants of the target, id <> target), returns each cleared row. - ResolveComment handler runs the clear + set in one tx so the replace is atomic. It emits comment:unresolved per cleared sibling (granular realtime consumers patch a single comment in place and would otherwise keep showing the stale resolution). Target keeps its COALESCE idempotency and the re-resolve event suppression. - Frontend optimistic update mirrors the invariant: resolving clears every other resolution in the same thread, so the cache never shows two at once. Unresolve still only clears its own row. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |