mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 04:25:46 +02:00
main
313 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ecce589867 |
MUL-5265: GitHub API-snapshot PR cards — CI status + mergeability (#5889)
* feat(github): API-snapshot PR cards — CI status + mergeability (MUL-5265) Fetch each linked PR's CI checks and mergeability from the GitHub GraphQL API as the single source of truth (Plan C). Webhooks, page visits and a bounded TTL sweep are refresh triggers only; nothing is inferred from webhook payloads anymore. Backend (server/internal/integrations/ghsnapshot): - installation-token cache + GraphQL client (private key / tokens never logged) - one paginated pullRequest query -> normalized per-check snapshot - outbound queue: (installation,repo,PR) dedup + single in-flight per PR, bounded worker pool, Retry-After / rate-limit backoff, jitter - head-SHA-guarded atomic batch replace (a slow response for an old head can never overwrite a newer head's snapshot) - bounded chase window (30s->5m, stops on terminal/closed) + page-visit + TTL refresh; clean degradation when no App private key is configured Removes the old suite-level webhook aggregation display path (query + handlers + tests). check_suite / check_run / status are now pure triggers. Frontend: PR card shows two independent tri-state elements (CI status + mergeability). "Ready to merge" only when merge state is clean; no-checks and unknown-mergeable never assert a positive verdict; progress strip removed; four locales; stale marker. Docs: github-integration + environment-variables (four languages) — now required App private key, read-only Checks/Commit-statuses permissions, new event subscriptions, capability boundaries and troubleshooting. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address PR snapshot review blockers Co-authored-by: multica-agent <github@multica.ai> * fix(github): bound snapshot refresh scheduling Co-authored-by: multica-agent <github@multica.ai> * fix(github): concurrent check-run index migration + singleflight token mint Address Elon's third-round review on the MUL-5265 PR snapshot pipeline. Must-fix — migration built a non-concurrent index. The github_pull_request_check_run table declared PRIMARY KEY (pr_id, ordinal) inside CREATE TABLE, which builds a unique index synchronously and violates the repo rule that every migration-created index (including on a new table) use CREATE UNIQUE INDEX CONCURRENTLY in its own single-statement file. Split: 222 now creates the table without a primary key; new 223 adds the (pr_id, ordinal) unique index CONCURRENTLY. The atomic delete-all/insert write path already guarantees ordinal uniqueness, so a plain unique index is sufficient; the index also serves the pr_id-prefix list aggregation and the workspace/PR cleanup deletes. Nit — token mint now singleflights per installation. installationToken released the lock before minting, so the N workers of one installation could mint N tokens on a cold cache or a simultaneous renew. Concurrent callers for the same installation are now collapsed via singleflight into one HTTP mint; added a -race concurrent-mint test asserting a single mint under 16 callers. Verified: fresh DB migrates through 223 (table has no PK, concurrent unique index present); ghsnapshot suite + new test pass under -race; migration lint and handler github/workspace-delete tests pass; sqlc produced no diff; go build / vet / gofmt / git diff --check clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f599fe3b85 |
fix(views): board/list load-more no longer flashes the skeleton + friendlier footer (#5893)
* fix(views): board/list load-more stops flashing the full skeleton Tail (load-more) page fetches were folded into the surface-level isLoading through the per-branch aggregate, so scrolling to load more in List and status-grouped Board replaced the already-rendered rows with the full-surface skeleton. Track head-page pending separately and gate the surface isLoading/isRefreshing on heads only; per-branch pagination state that drives the load-more sentinel is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(views): friendlier infinite-scroll footer for board/list/swimlane Collapse four copies of the error/has-more/reached-end ternary into a shared ListLoadMoreFooter: the loading spinner now carries a "Loading…" label so a slow fetch no longer reads as stuck, and a column that actually paginated shows a muted "No more" marker at the end instead of stopping silently. InfiniteScrollSentinel gains an optional label; adds the table.no_more string in en/zh-Hans/ja/ko. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
00e658401b |
feat(views): revamp execution log — virtualized, readable, two-tier header (#5890)
Combines the transcript work (previously split across #5860 virtual scroll and #5871 reading hierarchy) into one branch on current main, folding in the runtime-alias display from #5881. - Virtualize the event list (react-virtuoso) so a multi-thousand-event run mounts a bounded number of DOM rows (#5733), with firstItemIndex anchoring for newest-first live prepends. - Reading hierarchy via a pure trace-event-presenter: agent text and errors render expanded in place through RichContent (compact, log-scale markdown); thinking/tool rows fold to one line; tool detail expands into a quiet surface with "show all". - Persisted 3-way expand density (smart/expanded/collapsed) with per-row overrides; legacy defaultExpanded boolean migrated. Filters always persist (dropped the preserve-filters toggle). - Two-tier header: identity row (status, agent, trigger source, triggered-by) + list toolbar (created/duration/events facts left, shared Button controls right); runtime/provider/workdir/timestamps move to an ⓘ popover, runtime shown via runtimeDisplayName (#5881). - Copy-all exports full event bodies (redacted) with RFC 3339 timestamps (#5873), not the truncated summary. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2aafa41a5f |
MUL-5258: feat(issues): show agent-working indicator in Table view (#5875)
* feat(issues): show agent-working indicator in Table view Reuse the self-contained IssueAgentActivityIndicator (already in List and Board views) in the Table view's Issue column, rendered right after the identifier and before the title — matching the List view placement so the 'agent working' cue sits in the same spot across all three views. The indicator returns null when no agent is active, so inactive rows are unchanged. MUL-5258 Co-authored-by: multica-agent <github@multica.ai> * test(issues): cover Table agent-working badge insertion The previous stub returned null with no assertion, so deleting the badge insertion in table-view.tsx would still pass every test here. Render a marker carrying the issue id and assert the Table cell mounts it for the row's issue, positioned between the identifier and the title (identifier → activity → title, matching List/Board). Verified the new test fails when the insertion is removed. MUL-5258 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8d18d3a9ec |
Revert "MUL-5180: fix(github): surface CI status on PR cards (#5811)" (#5855)
This reverts commit
|
||
|
|
139cc89200 |
MUL-5180: fix(github): surface CI status on PR cards (#5811)
* fix(github): surface CI status on PR cards (MUL-5180) The CI mirroring pipeline (MUL-2228, MUL-2392) has never received a single event in production. The GitHub App setup docs only ever asked operators to grant `pull_requests: read` and subscribe to `pull_request`, so GitHub never delivered `check_suite` — `handleCheckSuiteEvent` sat dead behind a subscription nobody was told to enable. Every linked PR reports checks_passed/failed/pending = 0 and the sidebar row falls through to "Checks haven't reported yet" forever. Docs (the root cause), all four locales: - add `Checks: Read-only` permission + `Check suite` event to the App setup table - drop the stale "CI check states are not modeled" claim, which predates MUL-2228 and is what let the setup table stay incomplete - add a "PR rows show no CI status" troubleshooting entry with the public `/apps/<slug>` probe to confirm what an App is actually subscribed to, and a warning that existing installations must accept the new permission before any `check_suite` is delivered UI: - give the actionable status kinds (checks failed/pending/passed, conflicts, ready) their own icon + color. CI outcome previously rendered as plain muted 11px text, visually identical to the diff stats beside it — a failing build read the same as "+437 −6 · 6 files". Terminal and unknown kinds stay muted; the row's state icon already carries that meaning. Co-authored-by: multica-agent <github@multica.ai> * fix(github): unbreak docs build, stop overclaiming CI completeness (MUL-5180) Both must-fixes from review. 1. docs production build failed. `<your App>` in prose was parsed as a JSX tag, so `pnpm --filter @multica/docs build` died with `Expected a closing tag for <your>`. Dropped the angle brackets. Repo CI never caught this because no workflow runs the docs production build — only Vercel does, which is why the PR's GitHub checks were green while the deployment errored. 2. `Checks: Read-only` cannot support the pending status the docs promised. GitHub's webhook contract delivers `check_suite.requested` / `.rerequested` only to Apps holding Checks *write*; read-level access receives `completed` only. Verified against GitHub's published docs. Direction chosen: keep read-only, degrade honestly to final-results-only. Checks *write* is a repo-write capability (create/update check runs), not a wider read — escalating every installation to it just to render an in-flight spinner is not a trade to make on the operator's behalf, and it contradicts the integration's read-only posture. The concrete bug this leaves is premature green: with two reporting apps, the first to complete makes total=1/passed=1 and the row claimed "All checks passed" while the second was still running and might fail. Copy is now "Checks passed" in all four locales — it reports what reported and never asserts completeness. `derivePullRequestStatusKind` documents why. Docs gain a "what CI status can and cannot tell you" section (all four locales) with the read-vs-write delivery table, both consequences stated plainly, and the opt-in path for teams that do want in-flight status: set Checks to Read and write on their own App and the existing pending code lights up with no code change. The pending promise is removed from the read-only setup path. Co-authored-by: multica-agent <github@multica.ai> * fix(github): ignore non-completed check_suite actions (MUL-5180) Review was right: the `Read and write` opt-in the previous commit documented does not produce reliable pending, and following it would break the card. `check_suite.requested` / `.rerequested` are not observations that some CI provider started. GitHub sends them only to Apps holding Checks write, and per the CI-checks App docs they mean "GitHub has created a check suite for YOUR app on this commit; now add your check runs to it". Multica observes other apps' results and never creates check runs. Recording such a suite parks a `queued` row nothing can ever complete, and since `checks_pending` outranks `checks_passed` in derivePullRequestStatusKind, one stuck row freezes every PR on that installation at "checks running" and hides the real pass/fail result. Any self-hoster who already grants Checks write hits this on every push, so the gate is on the action, not the permission. - handleCheckSuiteEvent drops every action except `completed`, with the reasoning and the "don't resurrect requested as a running signal" warning recorded at the gate. - TestWebhook_CheckSuite_QueuedCountsAsPending encoded the wrong delivery semantics (two external apps sending `requested`, which GitHub never does). Replaced by TestWebhook_CheckSuite_NonCompletedActionsIgnored, which pins the drop and checks a later `completed` suite still lands. - The two out-of-order stash tests used `requested` payloads to exercise paths that are really about completed suites; both now use `completed` and assert the same guarantees. - Docs (four locales): the write opt-in is gone. In-flight CI is documented as unsupported at any permission level, with the actual reason and the note that real running status needs polling or a check_run model instead. Co-authored-by: multica-agent <github@multica.ai> * fix(github): make legacy non-completed check suites inert (MUL-5180) Review was right again: the previous commit gated the webhook entry point but left the pre-upgrade state — and the people it was meant to protect (self- hosters who already granted Checks write) are exactly the ones holding it. Two leftovers, both now closed: 1. Rows already in github_pull_request_check_suite. The old handler stored GitHub's `requested` suites as `queued`; nothing will ever complete them. ListPullRequestsByIssue still counted them, so `checks_pending` kept outranking `checks_passed` and the PR stayed pinned to "checks running" for as long as its head SHA stood. The aggregation now selects only `completed` suites. Filtering beats deleting here: recovery is automatic on deploy, needs no migration over a table that can be large, and holds for any writer that misses a gate — not just for today's legacy rows. DISTINCT ON runs after the filter, so an app whose newest suite is a stuck `queued` still reports its most recent completed verdict instead of disappearing. 2. Rows already in github_pending_check_suite. replayPendingCheckSuitesForPR is a second write path into the live table that never passes through handleCheckSuiteEvent, so the next `pull_request` event would re-inject a permanently-queued suite after the fix shipped. It now skips non-completed rows; the drain is DELETE ... RETURNING, so skipping discards them. Both are covered by regression tests that seed the legacy row directly — the fixed handler can no longer produce one — and both were confirmed to fail with their respective fix reverted. The stash test additionally asserts its fixture landed under the repo address the drain keys on; the first draft used the wrong owner and passed vacuously. Also corrects the aggregateChecksConclusion doc comment, which still described "pending" as a not-yet-completed suite. It is now reachable only for a completed suite carrying a null conclusion, and is explicitly not a "CI is running" signal. Co-authored-by: multica-agent <github@multica.ai> * test(github): assert the legacy stash row is consumed by the drain (MUL-5180) Review nits. The stash test proved its fixture existed before the webhook but never that the drain consumed it, so a future change to firePullRequestWebhookWithHead's repo address would make the assertions pass for the wrong reason again — the same way the first draft of this test did. Asserting the stash is empty afterwards closes that gap from the other side. Also fixes two comment typos: `an "CI is running"` -> `a`, and drops the "merged-but-open PR" state, which cannot exist. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
98072e2e56 |
fix(issues): filter working agents by active task issues (#5839)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
423a5c59cb |
MUL-5200: unify working-agent filters across issue views (#5819)
* fix(issues): query workspace working agents independently Co-authored-by: multica-agent <github@multica.ai> * feat(agents): filter working agents by source type Co-authored-by: multica-agent <github@multica.ai> * feat(issues): scope working agents to My Issues Co-authored-by: multica-agent <github@multica.ai> * test(agents): cover My Issues squad relations Co-authored-by: multica-agent <github@multica.ai> * fix(issues): unify working-agent filters across views Co-authored-by: multica-agent <github@multica.ai> * fix(issues): preserve empty working-agent filters Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e3ebf317ae | fix(i18n): capitalize on-behalf-of attribution label (MUL-5026) (#5670) | ||
|
|
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> |
||
|
|
8065cead85 |
MUL-5198: Restore server-backed issue table grouping (MUL-5100) (#5817)
* revert(issues): restore server-backed table grouping Co-authored-by: multica-agent <github@multica.ai> * test(skills): stabilize import completion coverage Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0d8a7ce5c2 |
fix(inbox): drop the agent activity hover card from inbox rows (MUL-5189) (#5813)
* fix(views): raise hover delay on the agent activity badge The per-issue agent activity badge sits on the right edge of dense scrolling lists (inbox rows, issue rows, board cards) and appears on every issue an agent currently touches. Base UI's 600ms default opened the 288px activity card on pointer travel rather than on intent. Raise the open delay to 900ms and drop the close delay to 150ms. The card body is read-only, so there is no hover bridge to protect. MUL-5189 Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): drop the agent activity hover card from inbox rows The badge already shows who is running and whether they are working or queued. On a triage surface the card's only incremental fact is elapsed time, which never changes the one decision an inbox row supports — do I open this? The row also carries the ActorAvatar hover card on the left, so a second popup was mostly noise. Add an opt-out `hoverCard` prop to IssueAgentActivityIndicator and pass false from the inbox row. Issue lists and board cards keep the card: monitoring work in flight is what those views are for, and they keep the 900ms dwell delay from the previous commit. MUL-5189 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e0e9383efe |
Improve UI animation transitions (MUL-5172) (#5718)
* Improve UI animation transitions * Remove implementation plan documents Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4d74db89cb |
feat(issues): richer sub-issue rows in issue detail (MUL-5098) (#5721)
* feat(issues): richer sub-issue rows in issue detail (MUL-5098) The sub-issues panel showed only status, identifier, title and assignee — priority, due dates, labels, live agent activity and nested breakdowns were invisible without opening each child. - SubIssueRow now shows priority (checkbox-slot swap like list rows), the agent-activity indicator, label chips (+n overflow), the child's own done/total progress ring, and an inline-editable due date with overdue emphasis (muted when the child is done/cancelled) - Right-click opens the shared issue actions menu via a section-level IssueContextMenuProvider — parity with list/board surfaces - ListChildIssues + ListChildrenByParents now bulk-load labels (same labelsByIssue pattern as the other list endpoints) - patchIssueLabels patches per-parent children caches; invalidateIssueLabelDerivatives refetches the Map-shaped batched children caches so label changes stay live everywhere Co-authored-by: multica-agent <github@multica.ai> * feat(issues): customizable property display for sub-issue rows (MUL-5098) The enriched sub-issue rows were a fixed field set — no way to trim them or surface workspace custom properties. - New user-level persisted preference (useSubIssueDisplayStore): built-in field toggles (priority / labels / sub-issue progress / due date / assignee) + opted-in custom property ids. Defaults match the previous fixed layout, so existing users see no change. - SubIssueDisplayPopover on the section header — same switch-row interaction as the main views' Display panel, reusing its card_* locale keys (no new translations needed). - Rows render opted-in custom property chips (PropertyIcon + CustomPropertyValueDisplay, list-row parity) only when the child carries a value; ids resolve against live non-archived definitions, so foreign-workspace or archived ids are inert. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): reconcile sub-issue cache updates Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
dd45f30553 |
MUL-5164: refactor(issues): drop the table toolbar loaded-count and structure-paused copy (#5778)
* test(issues): repair table-view editing tests orphaned by the grouping revert Reverting "Move issue table grouping to the server" (#5777) removed the server-driven `serverIssues` fixture, but two tests still assigned to it, so packages/views typecheck and vitest have been failing on main. Pass the issue through the Harness `issues` prop like the sibling tests do. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): drop the table toolbar loaded-count and structure-paused copy The table toolbar rendered a permanently visible "Loaded X of N" counter plus a long "Grouping and hierarchy are paused — ..." notice. Neither is worth the horizontal space it took between search and Export, so remove both strings and their locale keys. The load-failure Retry button stays: it only renders on a failed window fetch and is the explicit resume path for the infinite-scroll sentinel. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4dc47ef113 |
Revert "MUL-5100: Move issue table grouping to the server" (#5777)
This reverts commit
|
||
|
|
15326c5090 |
fix(issues): open table issues in new tabs (#5767)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d43e500ff6 |
MUL-5100: Move issue table grouping to the server
Merge approved after review; CI checks are green. |
||
|
|
fe12278863 |
feat(issues): add table sub-issue action (#5738)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1a37063bba |
fix(issues): keep table cell editors open across refreshes; title click opens the issue (MUL-5108) (#5730)
* fix(issues): keep table cell editors open across refreshes; title click opens the issue (MUL-5108) Table view interaction fixes: - Cell/header renderers are now stable module-level components reading data via table.options.meta. flexRender mounts function cells as component types, so the per-render closures rebuilt on every data refresh (new childProgressMap / property / actor identities) remounted every cell and closed any open picker popup. - One hoisted editingCellKey drives all cell editors (controlled open), and while an editor is open the row structure renders from a frozen snapshot (live issue values, frozen order) so window materialization, hierarchy assembly, and realtime reorders cannot move or unmount the popup's anchor row mid-interaction. Structure catches up on close. - Clicking an issue title now opens the issue (it previously started inline rename despite the link-style hover). Renaming moved to a hover pencil affordance; dead space in the title cell navigates too. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address table editor review — blur-click nav, virtual-unmount key release, deterministic test (MUL-5108) Follow-up to the MUL-5108 table fixes, per code review on PR #5730: - R1#2 (P1): committing a rename by clicking away also navigated into the issue. onBlur flips `editing` off synchronously before the commit-click lands, stripping the click-time guard so the click bubbled to row navigation. Record whether the gesture began while editing (mousedown, before blur) and swallow that click in the capture phase, before it can reach the row or the title's open handler. Dead-space clicks while not editing still open the issue. New blur-click regression test. - R1#3 (P2): the hoisted editingCellKey (and the frozen row structure keyed off it) never cleared when row virtualization unmounted the anchor cell — Base UI does not fire onOpenChange(false) on unmount, so the table stayed frozen and the picker silently reopened / dropped the rename draft on scroll-back. Add useReleaseEditingCellOnUnmount: an unmount responder that releases the key iff the unmounting cell still owns it. Focused tests. - R1#1 (P1): the new integration test exceeded the 5s default under concurrent CI worker load (real-timer gaps between userEvent steps). Drive userEvent with delay:null and give the heavy full-mount test explicit headroom so it is deterministic. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
41315989bd | fix(editor): prevent incorrect comment autolinks (#5665) | ||
|
|
1be1fd3be1 |
fix(issues): label board display-settings button as Display (MUL-5011) (#5664)
The display-settings trigger showed the current sort mode (e.g. "Manual") as its label, which reads as an unclear button name. Show a fixed "Display" label with the sliders icon instead, matching the existing "Display settings" tooltip. The sort mode remains visible inside the popover's Ordering section. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fcb4798f12 |
fix(issues): stop cold-load render loop on the Issues route (MUL-4985) (#5643)
Board and Swimlane crashed with "Maximum update depth exceeded" on first paint of the Issues route. During cold load the member/agent/squad directory queries are unresolved, so useActorName's `= []` defaults allocated a fresh array every render and its `getActorName` memo changed identity each render. BoardView's `groups` (and SwimLaneView's `laneGroups`) list `getActorName` as a dep, so they churned every render and re-fired the column-resync effect without end; react-virtuoso escalated the loop into the reported crash. This predates MUL-4797 — it was exposed when board/swimlane columns were virtualized with a real <Virtuoso>. - Share stable empty references for the loading directory snapshot so getActorName is referentially stable across cold-load renders (root cause). - Equality-guard the shared column setter in useDragSettle so a content-equal rebuild returns the previous reference and cannot spin the resync effect (defense-in-depth). Regression coverage: useActorName stability during cold load; the column-setter equality guard; and Board (40 cards) + Swimlane rendered with the REAL react-virtuoso under pending directories, which reproduce "Maximum update depth exceeded" without the fix and pass with it. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
002ea0d879 |
MUL-4797: add configurable issue table view (#5454)
* feat(issues): add configurable table view Co-authored-by: multica-agent <github@multica.ai> * test(issues): cover table columns in page fixture Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make table column picker interactive Co-authored-by: multica-agent <github@multica.ai> * fix(issues): repair quick create and virtualize table rows Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep pinned table cells opaque Co-authored-by: multica-agent <github@multica.ai> * fix(issues): anchor full-width table rows Co-authored-by: multica-agent <github@multica.ai> * fix(issues): consolidate table controls Co-authored-by: multica-agent <github@multica.ai> * fix(issues): harden table pagination and export Co-authored-by: multica-agent <github@multica.ai> * feat(issues): add table quick search Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make table window filters, selection, and export authoritative Round-2 review fixes for the issues table (MUL-4797): - Send the agents-working filter as a server ids facet so matches on unfetched pages surface and total/pagination/export agree; a present- but-empty id list yields an empty window instead of an unfiltered one. - Reset surface selection when the membership window changes and act on selection ∩ visible rows in the batch toolbar, so batch actions, Export selected, and the count all share one authoritative set. - Materialize the full flat window while table grouping is active, and suspend hierarchy nesting / parent-based grouping until the window is complete so structure cannot reshuffle as pages arrive; suppress header facet-count badges while the table window is partial. - Resolve actor directories and the property catalog at export time and fail the export instead of writing Unknown* actors or dropping configured property columns on cold/errored lookups. - Append a unique id tie-break to the list/grouped ORDER BY and mirror it in compareIssuesForSort so offset pages are stable across same-timestamp ties. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): bound table structure window and align chip/transport/selection Round-3 review fixes for the issues table (MUL-4797): - Cap whole-window materialization at TABLE_STRUCTURE_MAX_WINDOW (1000): below it the remaining pages load automatically — hierarchy applies without scrolling to the last page — and above it grouping/hierarchy suspend with an explicit toolbar notice instead of triggering an unbounded workspace download from a persisted view option. - Give the agents-working chip the authoritative in-window running set (the ids-facet window query, shared key with the filter-on state) so its badge can no longer say 0 while the filter would find matches on unfetched pages; falls back to loaded-row scoping elsewhere. - Route ids-facet windows through a new POST /api/issues/query twin — hundreds of running-issue UUIDs overflow the ~8 KB GET request-line budget of common proxies. The body carries the same key/value pairs; the handler rebuilds the query string and delegates to ListIssues. - Reset surface selection during render (key-change pattern) instead of a post-commit effect, so no frame ever pairs new membership with the old selection. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): harden table auto-pagination against errors and stale totals Round-4 review fixes for the issues table (MUL-4797): - Stop the structure materialization loop (and the scroll sentinel) when the window query is in error state — a persistently failing page left hasNextPage true and isFetchingNextPage false after every attempt, so the ungated effect refired forever. Resuming is an explicit toolbar Retry. The advancement decision now lives in a pure, tested shouldAutoLoadNextStructurePage helper. - Make the structure ceiling a hard stop: the ceiling check reads the LATEST page's total (pagination already advances on it, so a stale small page-1 total could re-open unbounded materialization), and the loop additionally halts on loaded count >= ceiling regardless of any reported total. - Drive the working (ids-facet) window to completion — it is inherently bounded by the running set — and treat it as the chip's authoritative scope only when complete, so >100 running issues no longer under-count as a single page. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): make working-window pagination capped and unknown-aware Round-5 (final) review fixes for the issues table (MUL-4797): - The working (ids-facet) window now advances through the same shouldAutoLoadNextWindowPage gates as the structure loop — it shares the main table's cache key while the agents-working filter is on, so an uncapped chip-driven loop re-opened the very ceiling the table just enforced. An over-ceiling window stops after page one. - A cold-load failure of the flat window is an ERROR state, not an empty workspace: isEmpty only claims empty on a successful zero-result fetch, and the surface renders a dedicated failed-to-load state with a reachable Retry (the in-table Retry never mounted without data). - The chip scope is now tri-state honest: a COMPLETE window (or an empty running set) yields a precise count, keepPreviousData carries the last-known-complete set across re-keys, and everything else — cold resolving, failed, over the ceiling — presents as an explicit unknown ('Agents working: —') instead of a number derived from whichever incomplete window happened to be loaded. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): single pagination owner and placeholder-honest chip scope Round-6 review fixes for the issues table (MUL-4797): - Exclude placeholder data from the working-window completeness gate: on a re-key (running set or facet change) keepPreviousData leaves the OLD key's rows visible, and pairing them with the new task snapshot published a precise-looking number for a scope nobody fetched. The scope now reads unknown until the new key resolves. - Make the shared table query single-owner while the agents-working filter is on: the chip's background loop no longer answers the same render snapshot as TableView's structure loop, and every auto caller (structure loop, working loop, scroll sentinel, retry) now uses fetchNextPage({cancelRefetch: false}) so a concurrent responder no-ops instead of cancel/restarting a fetch whose HTTP request is not abortable — which had been duplicating every offset. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ce15300493 |
MUL-4884: feat(issues): anchor the working chip on agents + real colour tiers (#5540)
* fix(issues): anchor the working chip on issues the filter actually shows (MUL-4884)
The header chip showed three units at once: the number counted distinct
issues, the avatar stack counted agents (with a rival "+N"), and the hover
card counted tasks — under a label with no noun at all. Each figure was
self-consistent; together they read as a miscount.
|
||
|
|
ed9adc2bbe |
feat: improve create issue field controls (#5532)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3b08456264 |
feat(views): block draft-fixing actions while attachments upload (#5465)
Submitting a composer mid-upload silently lost the file: the editor holds a `blob:` placeholder that gets stripped during serialization, and the attachment id does not exist yet, so the send succeeded without the file. Chat and Quick Create gated this; manual issue create, Feedback's button, comments, replies and comment edit did not. Gate every action that FIXES a draft — Send/Create/Save, Cmd/Ctrl+Enter, Enter on the title, and the manual/agent mode switches — behind one source of truth: the editor document, which IS the upload queue. ContentEditor publishes queue transitions via `onUploadingChange` (a transaction listener, not `onUpdate` — that path is debounced and skips no-change emissions, and a failed upload leaves byte-identical markdown, so the un-gate would never fire). `useUploadGate` pairs that render state with an `isBlocked()` re-read at submit time, since the shortcut paths never consult the button. The publisher emits its current answer on subscribe rather than only on flips: hosts outlive editor instances (comment edit remounts on cancel, chat swaps by key on agent switch), and an editor torn down mid-upload would otherwise leave submit wedged shut with no pending node left to reopen it. Also fixes the failure toast that never fired: `uploadWithToast` only reported errors if a caller passed `onError`, and none did, so failed uploads removed their placeholder and vanished unexplained. `useEditorUpload` now supplies it once for every composer. Per MUL-4808: drops Quick Create's upload-time lock on the attach button (files can queue), and leaves description autosave ungated by design. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0ec7c22901 | fix(issues): render create picker trigger content (#5491) | ||
|
|
ea8511340e |
MUL-4820: support custom property icons (#5468)
* feat(properties): add custom icons Co-authored-by: multica-agent <github@multica.ai> * fix(migrations): use unique property icon prefix Co-authored-by: multica-agent <github@multica.ai> * fix(properties): replace emoji icons with Lucide picker Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ea03912baf |
perf(desktop,issues): single-router tab sessions (MUL-4741 Phase 2) + trace-driven surface mount/render overhaul (MUL-4474/4750 reland) (#5403)
* Reapply "perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#…" (#5395)
This reverts commit
|
||
|
|
50e28d539c |
feat(settings): open issue links in new tab by default, configurable in preferences (#5445)
Issue mention chips in descriptions (tiptap), comments (readonly markdown), and chat now open the linked issue in a new tab on plain click — a browser tab on web, a foreground app tab on desktop. A new Settings → Preferences switch (default on) restores in-place navigation when turned off; the preference persists via a zustand store. AppLink now honors target="_blank": desktop delegates to the navigation adapter's openInNewTab with activate, web leaves the native anchor behavior intact. This also fixes dead cmd/ctrl-click on web for comment and editor mentions, which previously preventDefault'd without an adapter fallback. MUL-4793 Co-authored-by: Lambda <lambda@multica.ai> 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>
|
||
|
|
8f92b5fdeb |
feat(search): add fold/unfold all comments commands to the command palette (#5417)
On an issue page, Cmd+K now offers Fold All Comments / Unfold All Comments. Folding collapses every thread card via the persisted comment-collapse store; unfolding also expands resolved threads, whose session-only expand state moves from issue-detail useState into a new core resolved-expand store so the palette can drive it (MUL-4763). Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5999eabd92 |
fix(views): stop showing backfilled attribution as a warning (MUL-4768) (#5421)
* fix(views): stop showing backfilled attribution as a warning (MUL-4768) The transcript/activity AttributionBadge colored the "on behalf of <name>" chip yellow (text-warning) whenever attribution.precise === false. That flag is the backend's attribution-*coverage* health bit — owner_fallback, backfill, and unattributed all fail it — but coverage is an ops metric, not a reader-facing signal. A backfilled attribution names a human the same waterfall resolved, just retroactively, so it is confident; rendering it identically to a genuine owner_fallback guess made a correct "on behalf of Bohan" read like an error. Fire the cautionary tone only for a fallback guess (any non-precise source except backfill). Keeping the precise === false base means a future unknown degraded source still warns (fail-safe). The backfill nuance stays in the tooltip. Co-authored-by: multica-agent <github@multica.ai> * docs(views): correct backfill wording in AttributionBadge (nit MUL-4768) Review nit: describing backfill as "confident / same waterfall resolved" overstated the backend contract, which defines backfill as a historical, non-realtime, non-compliance-grade source. Reword the docblock, the tone rationale, and the test note to the accurate framing: backfill does not mean the displayed name is wrong (so no warning tone), but its historical origin is still preserved in the tooltip and the raw source field. Comments only; no behavior change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
09f69dfa05 |
refactor(attribution): drop on-behalf badge from execution log rows (MUL-4766) (#5419)
The "on behalf of <member>" attribution chip on each execution-log row added visual noise to the dense run list. Remove it from both the active and past run rows and restore the original layout. The attribution stays discoverable where it belongs: the task transcript header and the agent detail page's recent-work list still render AttributionBadge. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0276704323 |
fix(attribution): hide unattributed chip when a task has no responsible member (MUL-4765) (#5418)
The Human Attribution work (MUL-4302) showed a yellow "No responsible member" warning chip in the execution log and working-status surfaces whenever a task had no resolved responsible member. An unassigned run is a normal state, not something to flag, so the badge variant now renders nothing in that case — matching the avatar variant, which already stayed silent. Removes the now-dead `unattributed` locale string across all bundles. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fac8d772f |
feat(attribution): Human Attribution Phase 1 (MUL-4302) (#5150)
* feat(attribution): Phase 1 foundation — provenance schema + resolver (MUL-4302)
Human Attribution, Phase 1 (地基) first increment. Every agent run must be
traceable to exactly one accountable human AND record at which waterfall level
that human was resolved, so a NULL originator can be told apart from a genuine
'no human in the chain'.
- migration 149: add originator_source (waterfall label) + delegation/retry/
rerun/rule-version lineage + kind-tagged trigger evidence to agent_task_queue.
No FK, no cascade, no CHECK on the source enum (MUL-4302 §7); nullable ADD
COLUMNs = fast metadata-only change on the hot queue table.
- internal/attribution: the accountable-human vocabulary (Source, EvidenceKind,
TriggerKind) + pure, unit-tested classification rules (ClassifyComment/
ClassifyDirect). No DB, no authorization — provenance labeling only.
- service: attributionFor{IssueTask,TriggerComment} gather facts and delegate to
the pure classifier; the legacy originator resolvers now delegate here so
there is one source of truth. originator_user_id's VALUE is unchanged, so the
Composio-overlay and canInvokeAgent A2A authorization boundaries are
byte-for-byte preserved (MUL-4302 §1.3).
- enqueueIssueTask / enqueueMentionTask stamp originator_source + evidence;
CreateRetryTask carries the parent attribution forward and records
retry_of_task_id so retry and manual rerun stay separable (MUL-4302 §5).
Verified: go build ./..., go vet, gofmt clean; new attribution unit tests +
enqueue stamping integration test green; existing resolve_originator tests
unchanged.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): split accountable_user_id from originator, close enqueue bypasses (MUL-4302)
Phase 1, per Bohan's decision on the MUL-4302 thread: audit and authorization
answer different questions and get different columns.
- Migration 151 adds agent_task_queue.accountable_user_id (no FK, no cascade).
Authorization keeps reading ONLY originator_user_id (canInvokeAgent A2A gate,
Composio overlay); audit/UI/usage read accountable_user_id + source + evidence.
- Invariant (finalizeAttribution, single chokepoint + §11 tests): originator
non-null ⟹ accountable equals it. The two diverge only when originator is null
(autopilot / degraded fallback), which is the deferred rule_owner/owner_fallback
increment; this lands the column + mirror-write so that split has a home.
- Close the NULL-source enqueue bypasses Elon flagged: chat, quick-create,
deferred-fallback and run_only-autopilot now stamp originator_source + evidence
(+ accountable where a human exists). Autopilot stays unattributed until the
rule-version snapshot table lands, but is no longer a silent NULL-source row.
Retry inherits accountable_user_id like the rest of the attribution lineage.
- Fix assign/promote attribution (§4): a member who assigns/promotes an existing
issue is now the accountable human (and, by the invariant, originator) ahead of
the issue creator. Threaded as an OPTIONAL actor override, so comment/rerun/
autopilot paths keep today's resolution and create-with-assignee (creator ==
actor) is unchanged. The squad leader gate already judged the same member.
Also merges origin/main: renumbers the attribution migration 149→150 (main took
149 for issue_origin_agent_create) and folds agent_create into ClassifyDirect's
origin inheritance.
go build/vet/gofmt clean; attribution unit tests + service stamp/actor tests +
handler suite pass on a fresh DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* docs(attribution): fix accountable NULL semantics + close chat/quick-create evidence boundary (MUL-4302)
Addresses Elon's 2nd-round review on PR #5150 (pre-merge doc/evidence items):
- Migration 151 no longer overclaims NULL. accountable_user_id is NULL not only
on pre-migration rows but on NEW rows whose audit source resolved no human yet
(run_only autopilot writes originator_source='unattributed' with NULL
accountable until rule_owner lands). Header + COMMENT ON COLUMN reworded so a
schema reader does not misjudge the invariant.
- Chat now uses the UNIFORM evidence pair (kind=chat, ref=chat_session_id), like
autopilot_run/issue_assignment, instead of relying only on the dedicated
chat_session_id column — new EvidenceChat kind. Added a service test asserting
chat stamps direct_human + chat evidence.
- Quick-create is documented as the ONE intentional no-antecedent-row path: no
comment/issue/session/run exists at enqueue time (the run creates the issue), so
trigger_evidence_kind/ref stay NULL while the human rides originator/accountable
and source is direct_human — not a NULL-source bypass.
No authorization behavior change. attribution + service + handler suites pass on a
DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): renumber migrations 150/151 → 157/158 after merging main (MUL-4302)
main's #5162 ("unblock release migrations") renumbered the chat migrations and
took 150 (agent_task_coalesced_comments) and 151 (chat_read_cursor), colliding
with this branch's attribution migrations. Renumber them above main's new highest
(156) so TestMigrationNumericPrefixesStayUniqueAfterLegacySet passes:
- 150_agent_task_attribution → 157_agent_task_attribution
- 151_agent_task_accountable_user → 158_agent_task_accountable_user
Fixed the internal "migration 150" references in 158's header to 157. Migrations
apply cleanly through 158 on a fresh DB; migration lint green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot rule_owner — accountable = rule version publisher (MUL-4302)
Implements rule_owner (MUL-4302 §3.4), the first attribution source where the
accountable human diverges from the (NULL) authorization originator.
- Migration 159 adds the append-only autopilot_rule_version snapshot table (no FK,
no cascade); migration 160 adds its CONCURRENTLY lookup index.
- Write-on-publish: CreateAutopilot appends v1 (publisher = creator); UpdateAutopilot
appends a new version when a SUBSTANTIVE autopilot-row field changes (assignee /
status / execution_mode) — cosmetic edits (title/description/template) write none.
Both run inside the existing handler tx (atomic with the autopilot write).
- Dispatch resolution: both autopilot execution modes now resolve the active rule
version and stamp originator_source='rule_owner', accountable_user_id=publisher,
rule_version_id=<snapshot>, with originator_user_id left NULL (authorization
unchanged). run_only stamps CreateAutopilotTask directly; create_issue resolves in
attributionForIssueTask so both modes attribute identically. A missing version /
non-member publisher degrades to unattributed — never fabricates a human.
- finalizeAttribution now enforces the invariant ONE-WAY: it mirrors originator onto
accountable only when originator is valid, leaving an explicitly-set accountable
(rule_owner / future owner_fallback) intact when originator is NULL. Added
rule_version_id to CreateAgentTask so the create_issue path persists it too.
Also merges origin/main and renumbers this branch's attribution migrations
150/151 → 157/158 (main's #5162 took 150/151); rule_version table is 159/160.
Tests: attribution unit RuleOwner + one-way invariant table; service integration
tests proving an autopilot-origin issue stamps rule_owner + rule_version_id (and
degrades to unattributed with no version). Full service/attribution/handler/
migration suites pass on a DB migrated through 160; build/vet/gofmt clean.
Deferred (same PR): trigger-table republish (cron/webhook/event_filters) and
system-pause/archive versioning; owner_fallback + fail-closed; manual rerun.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): manual autopilot trigger → direct_human to the triggering member (MUL-4302)
Elon's blocking finding: a member manually triggering an autopilot was attributed
rule_owner (accountable = rule publisher, originator NULL) like a schedule/webhook
run, so member B triggering member A's autopilot landed accountable=A and carried
no originator authorization context for the run. Per MUL-4302 §4 a manual "run now"
is a direct human action and must attribute direct_human to the triggering member.
- Thread the triggering member from TriggerAutopilot into dispatch: new
DispatchAutopilotManual carries actorUserID (resolved via resolveActor +
memberActorUserID, so only a member actor is a human; an A2A agent actor falls
back to rule_owner). DispatchAutopilot / DispatchAutopilotForPlan keep their
public signatures (pass an invalid actor); only the internal dispatchAutopilot /
dispatchCreateIssue / dispatchRunOnly gained the param, so the many existing
callers are untouched.
- run_only: dispatchRunOnly stamps direct_human (originator == accountable ==
actor, no rule_version) for a manual actor, else rule_owner. CreateAutopilotTask
gains an originator_user_id param for the manual case.
- create_issue: dispatchCreateIssue enqueues a manual trigger via the actor-carrying
*WithHandoff entry points; attributionForIssueTask's autopilot-origin rule_owner
branch is now guarded on !actorUserID.Valid, so a valid actor falls through to the
direct_human override. Both execution modes attribute identically.
- schedule / webhook keep rule_owner (no actor). Trigger-table + system-pause/archive
versioning remain the pre-merge follow-ups.
Tests: the run_only row assertion Elon asked for (schedule → rule_owner row on
CreateAutopilotTask), plus manual direct_human on BOTH modes (run_only and
create_issue), including a manual actor distinct from the rule publisher. Full
service/attribution/handler/migration/scheduler/cmd suites pass on a DB migrated
through 160; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).
owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
accountable = agent owner, originator stays NULL (audit-only, authz untouched),
Source.Precise()==false. finalizeAttribution's one-way invariant already allows
accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
run does not start). So no run is left without an accountable human, and a
compliance workspace can block unattributable runs instead.
manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
daemon's prompt context but must attribute to whoever clicked rerun, not the
original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
the rerun path only (keeping the shared CreateAgentTask insert untouched), so
system retry (retry_of_task_id) and human rerun stay separable in reporting.
Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): close fail-open holes + move rerun_of_task_id into creation snapshot (MUL-4302)
Addresses Elon's two must-fixes on PR #5150.
1. accountable-never-null / fail-closed had fail-open holes. applyAttributionFallback
now, for an UNATTRIBUTED run, refuses the enqueue (ErrAttributionFailClosed) in
THREE cases instead of silently degrading to a runnable NULL-accountable task:
- workspace policy read fails (or no workspace) → fail closed; we cannot confirm
fallback is permitted, so we don't run an unattributable task on a DB hiccup.
(Only the rare unattributed path pays this; precise runs never read the policy.)
- workspace is fail-closed → refuse (unchanged).
- owner_fallback has no valid agent owner → refuse rather than enqueue a task with
a NULL accountable_user_id.
ErrAttributionFailClosed's doc now covers all three "cannot guarantee an
accountable human" refusals. Added missing-owner / policy-read-failure /
precise-passthrough tests.
2. manual rerun rerun_of_task_id was a post-notify UPDATE (race: the queued event /
daemon claim could see rerun_of_task_id = NULL, and a failed update degraded the
run to a plain direct_human). It now rides the CreateAgentTask insert — threaded
through enqueueIssueTask / enqueueMentionTask as a creation param (like
retry_of_task_id) so it is written in the same statement before the daemon is
notified. Removed the SetAgentTaskRerunOf follow-up query.
Also merges origin/main (unrelated CLI fix #5167, no conflict). Full service /
attribution / handler / migration / scheduler / cmd suites pass on a DB migrated
through 161; build / vet / gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): rule_owner versioning on trigger edits + system-pause/archive (MUL-4302)
The final remaining Phase 1 item: substantive publishes beyond the autopilot row now
republish the rule version, so a run's rule_owner accountable follows whoever last
changed what the rule does.
- Extracted the config-summary + insert into service.RecordAutopilotRuleVersion so
the handler and the (different-package) failure monitor share one writer; the
handler's recordAutopilotRuleVersion is now a thin wrapper.
- Trigger edits: UpdateAutopilotTrigger and DeleteAutopilotTrigger republish the rule
version with the acting member as publisher, ATOMICALLY (tx-wrapped mutation +
version write, mirroring CreateAutopilot/UpdateAutopilot). CreateAutopilotTrigger
republishes best-effort — the webhook path mints its token with a retry loop that
cannot share one tx, and a create is usually initial setup already covered by v1;
a failed write there is benign (active version stays the current publisher, the new
trigger fires under it, no immediate daemon claim rides it).
- Archive (DeleteAutopilot) republishes (member, status=archived), tx-wrapped.
- System auto-pause (failure monitor) republishes with a 'system' publisher,
best-effort — a background sweep to a non-dispatching state (a paused autopilot
never dispatches; a later member resume supersedes).
- RotateWebhookToken / SetSigningSecret deliberately do NOT version: they rotate
credentials, not the rule's behavior (not §3.4 substantive).
Semantics: a system-published (no-member) active version degrades dispatch to
unattributed → owner_fallback, never fabricating a human.
Tests: republish-reattributes (member A → member B supersedes → dispatch resolves to
B; system publisher → unattributed). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Also merges origin/main (unrelated frontend feature #5074).
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): make trigger-create rule-version republish atomic (MUL-4302)
Addresses Elon's final Phase 1 blocking finding: CreateAutopilotTrigger recorded the
rule-version republish best-effort AFTER the trigger insert. If member B added a
schedule/webhook trigger to member A's autopilot and the version write failed, future
schedule/webhook dispatches would keep attributing to A — violating the rule_owner
invariant that the last member to substantively change the rule owns future runs
("no immediate daemon claim" doesn't save it, since the miss surfaces at the LATER
trigger firing).
Both create paths now write the version in the SAME tx as the trigger INSERT:
- schedule create: wrap CreateAutopilotTrigger + recordAutopilotRuleVersion in one tx.
- webhook create: each mint-with-retry attempt runs in its own tx (insert + version
commit together; a token collision rolls that attempt back and retries with a fresh
token; a version-write failure rolls the trigger back). Passes ap + the acting
member id into the helper.
- removed the best-effort recordTriggerRuleVersionBestEffort helper (and the now-unused
slog import).
Test: TestCreateTrigger_RepublishesRuleVersionAtomically drives both create paths
through the handler and asserts a rule version is published by the acting member.
Existing webhook/trigger/archive handler tests still pass. Also merges origin/main
(unrelated avatar feature #5074). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.1 — surface run attribution on the task API (MUL-4302 §9)
First Phase 2 (visibility) increment: the agent-task API now returns the resolved
accountable-human provenance so the UI can render an "on behalf of" badge.
- AgentTaskResponse gains an `attribution` object: source label (never blank —
pre-migration NULL renders "unattributed") + `precise` flag (false for the degraded
owner_fallback / backfill / unattributed sources), the initiator (accountable) and
originator (authorization) user refs, the evidence {kind, ref_id} pointer, and the
rule_version / delegated / retry / rerun lineage ids.
- The label + evidence + raw ids are built in the PURE taskToResponse (no DB), so
every task response carries them. Names are hydrated separately, only on the
user-facing surfaces (ListAgentTasks, ListWorkspaceAgentTaskSnapshot, RerunIssue,
CancelTaskByUser) — daemon-claim paths stay lean.
- Hydration resolves initiator/originator from the GLOBAL user table (departed-member
safe) via a new batch GetUsersByIDs query (no N+1); best-effort, so a lookup hiccup
leaves the raw ids intact.
Tests: pure taskAttributionBase (direct_human / rule_owner NULL-originator /
owner_fallback degraded / pre-migration→unattributed) + DB hydration (fills known
ref, leaves unknown id un-filled, skips nil). Full handler/service/attribution/
migration/scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt
clean. The field is additive — the frontend's parseWithFallback ignores unknown keys,
so nothing breaks until the UI increment consumes it.
Also merges origin/main (unrelated editor feature #5090).
Remaining Phase 2 (next increments, same PR): frontend zod schema + "on behalf of"
badge + evidence-chain jump; append-only correction events (write + display).
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.2 — on-behalf-of badge in the execution log (MUL-4302 §9)
Surface the accountable human on every agent run row:
- AttributionBadge composes Badge + ActorAvatar, shows "on behalf of <member>"
with the resolution source as a tooltip; degraded (non-precise) attribution
gets a warning tone, and an unresolved initiator renders an explicit
"no responsible member" chip.
- Wire the badge into both active and past rows of the execution log.
- Mirror the attribution shape into AgentTaskResponseSchema (defensive, .loose())
so the cancel-task path carries it through zod; add parse tests.
- Export TaskAttribution/AttributionUser/TaskEvidence from @multica/core/types
and add the attribution block to all four issues.json locales.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): hydrate initiator names on issue-facing task endpoints + bound the badge (MUL-4302 §9)
Address Elon's PR #5150 review:
- ListTasksByIssue (the execution-log data source), GetActiveTaskForIssue and the
issue-scoped CancelTask now call hydrateTaskAttributions, so the "on behalf of
<member>" badge shows the real member name on issue detail instead of falling
back to "someone". Mirrors the existing ListAgentTasks / snapshot behavior.
- AttributionBadge: cap width (max-w-40, min-w-0) and truncate the name span so a
long name / narrow right column can't squeeze out trigger/status/actions; keep
the avatar shrink-0.
- Add a handler test asserting the issue task list returns a hydrated
attribution.initiator.name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): use semantic AvatarSize 'xs' for the badge avatar
main refactored ActorAvatar.size from a raw pixel number to the semantic
AvatarSize union (packages/ui/lib/avatar-size). Switch the on-behalf-of badge
avatar from size={14} to size="xs" (16px) after merging main.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): stage-cascade falls back to parent-issue provenance, not agent owner (MUL-4302)
When closing the last sub-issue in a Stage wakes the parent's assignee agent, the
run was enqueued via a system-authored child-done comment with no actor, which the
resolver classified as unattributed and then degraded to owner_fallback (the agent's
own owner). That is the wrong accountable human: the woken run should be accountable
to whoever caused the parent issue to exist.
attributionForIssueTask now detects a system-authored trigger comment and falls
through to the parent issue's own provenance — the same creator / agent_create-origin
/ autopilot-origin chain a direct enqueue resolves (so an agent-decomposed parent
attributes via delegation to the human who drove it; a member-created parent to that
member; an autopilot parent to the rule publisher). owner_fallback is now only the
last resort when the parent provenance itself has no human.
- Extract attributionFromComment so attributionForIssueTask can inspect author_type
without a second GetComment; authorization resolution stays byte-identical.
- Add a DB-backed test asserting a system child-done comment resolves to the parent
issue's origin human (delegation), not owner_fallback.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)
Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)
- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
FK/cascade). Capture the creating member at both trigger-create sites (schedule +
webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
carries no human authorization — same authz-safe divergence as rule_owner),
accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
dispatch and the create_issue path (bridging issue → active run → trigger_id).
Legacy triggers with no recorded creator, and agent-created triggers, degrade to
rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
tests (member creator → trigger_owner; creatorless → rule_owner fallback).
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): re-trigger CI (dropped synchronize event on
|
||
|
|
c10bfa8f56 |
Revert "perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#…" (#5395)
This reverts commit
|
||
|
|
40da795f6c |
perf(issues): virtualize inbox/list/board/swimlane (MUL-4474, 方案2) (#5349)
* perf(inbox): virtualize notification list (MUL-4474) The inbox notification list rendered every item at once. Each row mounts an avatar + hover card, so a long inbox inflates the tab-switch commit — the same render-amplifier class this issue targets. Extract an InboxList component that virtualizes the rows via react-virtuoso (customScrollParent over the existing overflow-y-auto element, same pattern as the issue-detail timeline). Only the visible window plus a small overscan is mounted; everything else — selection, hover, archive, scroll semantics, the row component and callbacks — is unchanged. Virtualization changes exactly one thing: whether an off-screen row is in the DOM. Slice 2a of MUL-4474 (inbox is the no-DnD surface, done first to prove the Virtuoso + scroll + keyboard harness before the drag surfaces). Draft: must pass the manual zero-functional-change pass on a real Desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(board): virtualize board columns (MUL-4474) Each board column rendered every card at once; cards carry pickers, avatars, and a per-issue activity indicator, so a tall column inflates the tab-switch commit. Virtualize the cards within each column via react-virtuoso, using the column's own scroll container as customScrollParent. The dnd-kit droppable stays on the always-mounted column scroll container (merged callback ref feeds both dnd-kit and Virtuoso), and SortableContext still wraps the full id list. So cross-column drops (status/assignee change) and reorder among on-screen cards are unchanged; reordering to an off-screen target relies on drag auto-scroll to mount it — the documented virtualization tradeoff, to be confirmed in the manual pass. The infinite-scroll sentinel rides Virtuoso's Footer slot so loadMore still fires at the bottom, and a per-item pt-2 reproduces the previous space-y-2 gap with padding inside the measured item box. issues-page.test.tsx: mock react-virtuoso to render items inline (jsdom has no layout), and make the useDroppable mock's setNodeRef referentially stable to match real dnd-kit — the board's merged customScrollParent ref would otherwise loop on a fresh ref each render. Slice 2b of MUL-4474 on the shared inbox/list/board/swimlane branch. Draft: requires the manual zero-functional-change pass on a real Desktop build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(list): virtualize issue list rows (MUL-4474) The status-grouped list rendered every row in every expanded section at once; each row carries a sortable, context menu, tooltip, and activity indicator, so a long list inflates the tab-switch commit. Virtualize each expanded section's rows with react-virtuoso, all instances sharing the page's single scroll container as customScrollParent. Everything structural is preserved by construction: the Base UI accordion, sticky status headers, collapse, the per-section useDroppable, the per-section SortableContext, and the load-more sentinel (now Virtuoso's Footer). The Virtuoso only mounts for an expanded section (a collapsed/hidden panel has no viewport to measure). Virtualization changes exactly one thing: whether an off-screen row is in the DOM. issue-surface.test.tsx: mock react-virtuoso inline (jsdom has no layout) so the surface-level loading-semantics assertions still observe the list's rows. Slice 2c of MUL-4474 on the shared branch. Draft: requires the manual zero-functional-change pass on a real Desktop build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * perf(swimlane): virtualize lanes (MUL-4474) The swimlane rendered every lane (each a full row of status cells) at once. Virtualize the vertical lane axis with react-virtuoso over the board's outer scroll box (customScrollParent), so only on-screen lanes stay mounted. Behavior is preserved: pinned lanes keep their leading position, the SortableContext still wraps the lane set for grip-drag reorder (its items are only the non-pinned lane ids), per-cell droppables and per-cell card SortableContexts are unchanged (cells live on mounted lanes), the sticky status header stays above the list, and the per-status load-more sentinels ride Virtuoso's Footer. pt-4 per lane reproduces the previous gap-4. swimlane-view.test.tsx: mock react-virtuoso inline so the ~47 lane/cell/DnD assertions still see the lanes the virtualized list renders. Slice 2d of MUL-4474 on the shared branch — this completes the four surfaces (inbox/board/list/swimlane). Draft: requires the full manual zero-functional-change pass on a real Desktop build before merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): don't pass undefined to Virtuoso `components` (MUL-4474) react-virtuoso seeds its `components` prop with an internal `{}` default; passing `components={undefined}` (which the list and board did when there was no Footer — hasMore false / no column footer) overwrites that default with undefined, so Virtuoso's startup destructure of `EmptyPlaceholder`/`Footer` throws and the surface crashes. jsdom tests mock react-virtuoso so this only surfaced on a real Desktop build (found in manual perf testing). Return a stable module-level empty object instead of undefined. Inbox (omits the prop entirely) and swimlane (always supplies a Footer) never hit this and are unchanged. 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> |
||
|
|
3a493ab417 |
perf(issues): de-amplify per-row agent activity indicator (MUL-4474) (#5338)
Each issue row's IssueAgentActivityIndicator subscribed to the whole workspace agent-task snapshot via useQuery. Any task change swaps the snapshot array reference, so every observing row re-rendered — on a busy workspace the snapshot changes constantly, turning one task update into a full-list re-render and inflating the tab-switch commit. Narrow each row's subscription to this issue's tasks with a `select` (selectIssueTasks). React Query's structural sharing keeps the selected value referentially stable when the issue's own tasks are unchanged, so a snapshot invalidation now only re-renders the rows whose tasks actually moved. This is slice 1 of MUL-4474 (render de-amplification). Virtualization of list/board/swimlane/inbox and the non-position useSortable mount change are tracked separately — they need interactive drag + DevTools Performance verification that the headless runtime can't provide. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47e3fdedd0 |
feat(projects): start_date / due_date UI + create-project footer aligned with create-issue (MUL-4388) (#5331)
* feat(projects): add start_date / due_date pickers to project create modal and sidebar #5313 landed the backend start_date/due_date fields + types but deliberately shipped no UI. Wire up the two editor surfaces users expect: - ProjectStartDatePicker / ProjectDueDatePicker mirror the issue pickers (same calendar-day contract, clear idiom, shared @multica/core/issues/date helpers) but are typed to UpdateProjectRequest and scoped to the "projects" i18n namespace. One component serves both surfaces via a custom trigger. - Create-project modal: two date pills; values flow into the create payload and the persisted draft (draft-store gains startDate/dueDate). - Project sidebar (project-detail): two PropRows after Lead, wired to the update mutation, with clear support (send null). - i18n: prop_start_date / prop_due_date / clear_date across en/zh-Hans/ja/ko, reusing the existing issue date wording. Tests: picker display + clear behavior (real popover), and the create modal renders both pills. typecheck + lint + i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(projects): align create-project footer with create-issue Restructure the create-project modal footer to match the create-issue pattern (per design feedback): the primary action moves out of the cramped single justify-between row into its own border-t action strip, and the property pills sit in a dedicated wrapping toolbar above it. Low-frequency fields (start/due date) collapse into a ⋯ overflow via progressive disclosure — a pill only renders inline once its date is set or the user opens it from the menu — so the default toolbar stays a clean single row (Status · Priority · Lead · Repos · ⋯). - Use the shared PillButton (../common/pill-button) instead of the modal-local copy, gaining the data-popup-open styling create-issue uses. - ProjectStartDatePicker / ProjectDueDatePicker gain controlled open props so the overflow menu can reveal + open them (mirrors the issue pickers). - i18n: create_project.set_start_date / set_due_date / more_options_aria across en / zh-Hans / ja / ko, reusing the create-issue wording. Test updated to assert the dates are revealed from the overflow rather than shown inline by default. typecheck / lint / i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(views): extract shared DateOnlyPicker base for date pills (Elon nit2) The issue and project start/due-date pickers were near-complete copies of the same Popover + Calendar + clear wiring, so they could drift in behaviour or formatting. Extract that into one entity-agnostic DateOnlyPicker (packages/views/common/date-only-picker.tsx); each of the four pills is now a thin wrapper supplying only its field name (via onChange), icon, overdue flag, and localized copy. -264 lines of duplication, single source of truth. Behaviour is unchanged: the issue pickers keep their full API (trigger / triggerRender / open / onOpenChange / align / defaultOpen — all still used by board-card, issue-detail, create-issue) and the calendar-day contract stays in @multica/core/issues/date. The en-US display format now lives in one place rather than being duplicated per entity. Full views test suite (1857 tests) + typecheck + lint pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
80e54094a8 |
feat(issues): sticky setting for the issue comment bar (MUL-4435) (#5293)
* feat(issues): add sticky setting for the issue comment bar (MUL-4435) The bottom comment composer can now pin itself to the scroll viewport so it stays reachable while reading a long timeline. A pin toggle on the bar itself persists the preference (default: on) via a new localStorage-backed useCommentComposerStore. While pinned, the editor area is height-capped so long drafts scroll internally instead of covering the timeline. Co-authored-by: multica-agent <github@multica.ai> * refactor(settings): move the sticky comment bar toggle to Settings > Preferences (MUL-4435) Per review feedback: pinning the comment bar is a low-frequency choice, so the toggle moves off the bar itself into Settings > Preferences as a Switch row. The composer keeps reading the store for the sticky behavior and the 40vh editor cap; the pin button and its issues-locale tooltips are removed. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b64ebd60b5 | feat: add customizable keyboard shortcuts (#5294) | ||
|
|
d8165bac4e |
feat(views): unify reply and comment send buttons to the circular chat style (#5288)
SubmitButton is now always circular — the shape prop is removed since every composer uses the same silhouette. ReplyInput drops its inline icon-xs Button for the shared SubmitButton, gaining the same size, disabled state, and send tooltip as the comment composer and chat. MUL-4433 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c377d7fb4f |
feat(labels): add scoped label management (#5279)
* feat(labels): add scoped label management * fix(labels): address review feedback * fix(migrations): use unique label migration prefix |
||
|
|
65ccab172a |
fix(issues): float find bar above sticky resolve collapse bars (MUL-4414) (#5264)
The in-page find bar (absolute, z-20) and the resolve collapse bars pinned at the timeline's top-0 (sticky, z-20) tied on z-index, so the later-in-DOM collapse bar painted over the find bar, half-hiding it and orphaning its close button. Raise the find bar to z-30 so the transient overlay reliably paints above every sticky affordance in the content column (comment headers z-10, collapse bars z-20). Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b9b9e73e49 |
fix(issues): keep detail content column centered under classic scrollbars (#5255)
On platforms where scrollbars take layout space (macOS with a mouse or 'always show', Windows, Linux), the global 'scrollbar-width: thin' reserves ~11px on the right edge of the issue detail scroll container only, so the centered max-w-4xl column reads 32px left vs 43px right whitespace. Mirror the gutter with 'scrollbar-gutter: stable both-edges' on the detail scroller and its loading skeleton; overlay-scrollbar platforms reserve nothing and are unchanged. Measured before: 32px / 43px. After: 43px / 43px. Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
4efcfb96e3 | feat(ui): establish surface system (#5248) | ||
|
|
835b1d5e4f |
feat(issues): thread quick-jump minimap on issue detail (MUL-4389) (#5234)
* feat(issues): add thread quick-jump minimap to issue detail (MUL-4389) A Linear-style rail of tick marks overlaid on the left edge of the issue detail scroll area, one tick per comment thread (folded resolved bars included). Ticks whose thread intersects the viewport render darker, so the rail doubles as a scroll minimap. Hovering a tick grows it and opens a preview card (bold first line + muted body excerpt, both clamped); clicking jumps the timeline to the thread and flashes it like an inbox deep-link landing. Jumps go through Virtuoso's scrollToIndex in virtualized mode (the target row may be unmounted) and direct container scrollTop math in the flat deep-link/find modes, never native scrollIntoView (#3929). Viewport tracking reads DOM rects on scroll/resize instead of an IntersectionObserver because Virtuoso mounts/unmounts rows while scrolling. Hidden on mobile: no hover, and the gutter is too tight. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): Dock-style hover wave on the thread minimap (MUL-4389) Hovering the rail now magnifies ticks with a cosine falloff of their distance to the cursor — the hovered tick peaks at 1.7x and neighbours taper off across ~4 tick pitches, following the pointer continuously. Driven per-pointermove with direct style writes on the native `scale` property (compositor-friendly, no React re-render), batched read-then-write inside one rAF; a 100ms ease-out transition smooths between pointer samples and settles the collapse on leave. Clearing the inline value hands control back to the CSS floor states (popup-open, focus-visible), and prefers-reduced-motion swaps the wave for a plain hover grow. Only the hovered tick darkens — neighbours grow but keep their color. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): single glide-follow preview card on the thread minimap (MUL-4389) Scanning the rail continuously re-paid the 150ms open delay plus the close/open animation on every tick crossed, because each tick owned an independent PreviewCard popover — hover felt laggy while gliding. Replace the per-tick popovers with ONE card owned by the rail, driven by the same rAF rect pass as the hover wave: the intent delay is paid once when the pointer enters the rail; after that, gliding retargets the card instantly (~1 frame) and slides it to the hovered tick with a 150ms transform transition. Leaving starts a grace timer long enough to travel onto the card (which keeps it open for text selection); keyboard focus anchors the card immediately. The anchor is clamped so the card never sticks out of the column at the rail's extremes, and previews are cached per thread content so unrelated timeline updates don't re-flatten every comment. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bf161f2f9c |
fix(tasks): preserve merged comment delivery (#5192)
Track actual claim-time delivery, support legacy daemons, and repair comment batches across claim, retry, edit, and delete races. MUL-4348 Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |