mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
fix/transcript-virtual-scroll
1501 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6045a5dc80 |
fix(runtimes): use official Qwen Code logo (#5713)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f8bf6cd8b9 |
feat(runtime): add Qwen Code runtime (MUL-5015)
Merge approved PR #5666. |
||
|
|
5a11232c47 |
feat(rich-content): unify Chat and Issue/Comment on one RichContent renderer (MUL-4922) (#5578)
* refactor(markdown): single canonical sanitize schema for both renderers (MUL-4922) Phase 1 of the RichContent convergence: collapse the duplicated security base shared by the two product-level Markdown chains. Chat (packages/ui/markdown/Markdown.tsx) and Issue/Comment (packages/views/editor/readonly-content.tsx) each carried a verbatim fork of the rehype-sanitize schema and urlTransform, and the forks had already drifted: readonly whitelisted <mark> for `==highlight==`, chat did not. A security-relevant allow-list maintained in two places means every future XSS fix has to land twice, and missing one is a hole — this is the hardest reason for the sweep, ahead of the user-visible feature drift. - Extract markdownSanitizeSchema + markdownUrlTransform into packages/ui/markdown/sanitize.ts and export from the package index. Both chains now import the single copy; no local forks remain. - The canonical schema is the union, so chat gains the <mark> tag name. This is the one intentional behavior delta: <mark> is inert and admits no attributes, and chat needs it anyway once ==highlight== converges. - Annotate the schema as rehype-sanitize's Options: exporting it makes the previously-inferred hast-util-sanitize type unnameable across packages. Adds a cross-surface contract test that runs one set of security fixtures (script, event handlers, javascript: href, data:image vs data:text/html, mark, slash://) through BOTH surfaces and asserts identical outcomes — the mechanism that stops a third fork from growing back. Code-block rendering is deliberately not asserted cross-surface yet: chat highlights with Shiki, readonly with lowlight, so emitted class tokens still differ. Converging them is the RichCodeBlock phase and needs the highlight-engine decision first; only the schema-level allow-list is shared here. Verified: pnpm typecheck (6 workspaces), pnpm lint (0 errors), pnpm vitest run in packages/views (228 files, 2665 tests, all passing). Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(rich-content): one RichContent renderer for Chat and Issue/Comment (MUL-4922) Phase 2+3 of the convergence. Chat now renders agent output through the same product renderer as Issue descriptions and Comments, so a ```mermaid fence an agent emits is a diagram in Chat — not a dead code block. Before this there were two product Markdown chains. Issue/Comment had Mermaid, HTML preview, lowlight code, product Mention/Attachment/LinkHover; Chat went through a generic renderer with no Mermaid/HTML dispatcher at all. Chat is where agents emit the most diagrams, so the gap was most visible exactly where it mattered. Architecture - packages/views/rich-content/ holds the ONE product renderer: a single ReactMarkdown pipeline, one sanitize config, one components map, one fenced -code dispatcher. Public API is content/attachments/density/phase — no `surface` prop, no renderMention override, no custom code-renderer hook, because each is a door a per-surface fork walks back through. - `density` is CSS only; `phase` is lifecycle only. Neither switches parser, plugins or the semantic DOM, so all surfaces emit the same blocks. - ReadonlyContent is now a ~40-line compatibility wrapper (was 501 lines); its consumers are untouched. views/common/markdown.tsx is deleted. - Leaf components (Mermaid, HTML preview, lowlight code) stay in editor/ and are imported by direct path, so Tiptap keeps reusing them and Chat does not pull in the editor's Tiptap graph. Moving them is a later mechanical commit. Streaming fence gate Rich blocks upgrade only when the fence is CLOSED, judged by a real CommonMark parse (mdast) rather than a `startsWith("```")` scan. A half-streamed fence renders as source, so Mermaid never parses a partial diagram and no iframe is created for HTML still arriving. Closedness — not `phase` — is the gate, so a settled-but-malformed fence stays source too. The gate returns offsets only; it never renders, because a gate that rendered would be the second renderer this change exists to delete. Verified that source offsets survive rehype-raw + sanitize + katex before relying on them. Live -> persisted row identity The live timeline moved out of Virtuoso's Footer into a real row keyed `task:<taskId>`, and persisted assistant rows key on the same task instead of `message.id`. Completion is now an in-place data swap through one AssistantMessage component, so an already-rendered diagram or iframe stays mounted and is not re-run. The process fold previously relied on that remount to re-collapse, so the collapse is now expressed directly. Notes - Chat code blocks move from Shiki to lowlight (Howard's ratified choice), matching Tiptap/Readonly and the existing hljs CSS. Ligature suppression carries over via code.css instead of utility classes. - Chat message tests now stub react-virtuoso: real Virtuoso renders its Footer but no data rows under jsdom's zero-height viewport, and the live timeline is a row now. Tests: five-surface Mermaid parity (Chat user / live / persisted, Issue description, Comment), streaming open->closed->settled with mount counting, live->persisted no-remount, htmlbars/mermaidx not misdispatched, sandbox and data-URI security regressions, CommonMark fence edge cases (shorter closer, tilde, indented, nested, in-list), and source-level import boundary guards. Verified: pnpm typecheck (6 workspaces), pnpm lint (0 errors), packages/views vitest 240 files / 2743 tests all passing. Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(rich-content): drop dangling export, add export guard + lazy rich blocks (MUL-4922) Two review follow-ups. 1. Dangling package export `packages/views` still exported "./common/markdown" after the file was deleted in the RichContent convergence. TypeScript resolves against the source tree so typecheck and unit tests both passed; the subpath would only fail when a consuming app bundled it. No consumer imports it, so this was latent rather than broken in practice. Removes the entry and adds packages/views/rich-content/package-exports.test.ts, which walks every workspace package.json and asserts each export target exists (wildcards checked by directory prefix). Scoped repo-wide because the failure mode belongs to package.json, not to this package. Verified the guard actually fails by re-adding the dangling entry before committing. 2. Near-viewport lazy shell for Mermaid / HTML Implements the deferred performance contract rather than requesting an exemption. LazyRichBlock defers each rich leaf until it is within 800px of the viewport, then latches: once mounted it is never unmounted, so scrolling past does not re-run Mermaid, rebuild the sandboxed iframe, or discard the viewer's pan/zoom state. The stable-size requirement is handled by reserving the block's expected height before AND after mount, so a block never measures 0px off-screen and then jumps — the churn that makes a virtualized list mis-estimate item sizes. The reservation is not a local guess: reservedMermaidHeightPx() reuses the existing session layout cache (real height on a cache hit, else the documented 280px skeleton) and HTML_BLOCK_PREVIEW_HEIGHT_PX is the pixel twin of the preview iframe's fixed h-[480px]. Both are exported from the leaves so there is one source of truth per block type. Environments without IntersectionObserver (jsdom, SSR) mount eagerly, which is today's behaviour; rendering nothing would not be. Verified in real Chromium (jsdom has no IntersectionObserver, so unit tests only exercise the eager fallback): - Non-virtualized Issue/Comment with 25 diagrams: 3 mounted, 22 deferred on load; after scrolling, mounted grows 3 -> 6 and never drops, confirming the latch. This is where the win is real. - Chat gains less: Virtuoso already caps the DOM to ~4 rows, so only 1 of 4 shells was deferred. Stated rather than overclaimed. - Live -> persisted handoff re-checked with the shell in place: scrollTop 220 -> 220 (delta 0), the tagged diagram DOM node survived, and the run reached the persisted state. Contract 1 is not regressed. Tests: 6 lazy-shell cases (defer, mount-on-approach, latch/no-unmount, equal reserved height before and after mount, root margin exceeding the chat list's own overscan, no-IntersectionObserver fallback) plus 3 export-guard cases. Verified: pnpm typecheck (6 workspaces), pnpm lint (0 errors), packages/views vitest 242 files / 2752 tests all passing. Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(rich-content): SSR-deterministic lazy state, mention a11y, drop type casts (MUL-4922) Three review blockers. 1. Hydration mismatch in the lazy shell LazyRichBlock derived its initial `mounted` state from feature detection inside useState. On the server (no window) that resolved true and rendered the whole Mermaid/HTML subtree; in the browser (IntersectionObserver present) the hydration pass resolved false and rendered a placeholder — different markup for the same component, and an SSR path that silently bypassed the lazy gate. `"use client"` does not opt a component out of Next's server render, so this was reachable. Initial state is now unconditionally false on both sides. The eager fallback for environments without IntersectionObserver moved into the effect, which never runs on the server, so the first committed frame is identical everywhere and the latch is unchanged. The first version of the SSR test passed against the buggy component: jsdom always provides `window`, so server and client took the same detection branch and the mismatch was unobservable. The suite now removes IntersectionObserver for the duration of renderToString to reproduce the real asymmetry. Verified by reinstating the bug: 2 of the 3 SSR tests fail and React raises its own "Hydration failed" error. 2. Project mention lost keyboard access The unified renderer inherited the readonly surface's `<span onClick>` around ProjectChip, while Chat had previously used AppLink — so converging the surfaces regressed Chat from a focusable anchor to a mouse-only span. A span and an anchor are visually identical and behave the same under a mouse, which is why only an assertion on the emitted element catches it. Now rendered through AppLink, which also owns plain-click, modifier-click and the desktop new-tab adapter, so none of that is reimplemented. The wrapper keeps only stopPropagation, matching IssueMentionCard. Adds project-mention-a11y.test.tsx using the REAL AppLink and NavigationProvider — mocking AppLink to emit an anchor would assert the mock. Covers anchor + href, chip's nearest interactive ancestor, Tab focus, Enter activation, click, and modifier-click labelling. All 6 fail on the old span. 3. Type suppressions in the production renderer `as never` on the plugin lists and `as NonNullable<Components[...]>` on the code/pre overrides silenced real type errors, against the repo's strict-TS rule. Replaced with react-markdown's own types: RichCode/RichPre now derive their props from `ComponentPropsWithoutRef<tag> & ExtraProps`, and the plugin lists use `satisfies NonNullable<Options["remarkPlugins" | "rehypePlugins"]>` so a bad plugin tuple still fails to compile. Also removed the remaining casts this exposed: hast property reads went through `as string` even though hast property values are a union, so a non-string `data-*` attribute would have been typed as a lie — replaced with a runtime-narrowing helper. `toHtml()` already returns string; its cast was redundant. Node position reads are narrowed in one helper. No cast or ts-ignore remains in production rich-content. Verified: pnpm typecheck (6 workspaces), pnpm lint (0 errors), packages/views vitest 243 files / 2780 tests all passing. Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(rich-content): cached-height hydration, scroll-root + recycle latch, CDN reactivity (MUL-4922) Three review blockers. 1. Cached reserved height still reached the first frame The previous fix made `mounted` deterministic but left the *height* reading sessionStorage during render: RichFenceBlock called reservedMermaidHeightPx() inline. Server has no sessionStorage so it emitted 280px, while a browser with a warm cache emitted the real height — a differing style="min-height:…" on the frame React hydrates, which React reports as an attribute mismatch and does not repair. Same bug class as the one already fixed, one layer up. RichFenceBlock now splits into Mermaid/HTML components (so the height hook is never conditional) and reserves the skeleton default on the first frame, adopting the cached height in an effect. Zero-shift on a warm cache is kept; only the read moves after hydration. The earlier SSR suite passed a fixed reservedHeightPx straight to the lazy shell, bypassing this path — it could not have caught this. New tests drive the real RichFenceBlock with a real prefilled sessionStorage entry, and simulate the server by removing sessionStorage for the renderToString call (jsdom provides one, so without that the "server" takes the browser branch and the mismatch is invisible). 2. Wrong observer root, and a latch that died with the row The IntersectionObserver set only rootMargin, so it clipped against the viewport — but Chat scrolls inside its own element (Virtuoso customScrollParent). Expanding the viewport box says nothing about a nested scroller, so Chat blocks only loaded once already visible and the preload was effectively dead there. Surfaces now publish their scroll container through RichContentScrollRootProvider and the observer uses it as `root`; page-scrolled surfaces keep the viewport root. Separately, the mount latch was component state, so Virtuoso recycling a row discarded it and scrolling back re-ran Mermaid, rebuilt the iframe and dropped viewer pan/zoom — the per-pass cost the shell exists to prevent. The latch moved to a module-level registry keyed by a hash of the content, bounded at 500 entries with oldest-first eviction. Restore happens in a layout effect (before paint, so no placeholder flash) and never in initial state, keeping the server/client first frame identical. Scope was not narrowed. 3. CDN config arriving late never reprocessed content preprocessMarkdown read configStore.getState().cdnDomain imperatively and RichContent's memo depended only on `content`, so content rendered before the async config landed kept legacy CDN links as plain anchors permanently. cdnDomain is now an explicit parameter: RichContent subscribes via useConfigStore and puts it in the memo deps, while the Tiptap editor — which genuinely preprocesses once at load — reads the store at its own call sites. No fallback branch. Each fix was verified by reinstating its bug and confirming the new tests fail (2/5, 2/14 and 1/3 respectively) rather than trusting a green run. Also fixes a false positive in the boundary guard: the Tiptap check read raw file text instead of using the suite's stripComments helper, so a doc comment mentioning RichContent counted as a violation. Verified: pnpm typecheck (6 workspaces), pnpm lint (0 errors), packages/views vitest 245 files / 2793 tests all passing. 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> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
dbb515b7b4 |
fix(views): close project picker dropdown after selecting a project (#5663)
The create-issue dialogs wire pickers with open={cond ? true : undefined}. Base UI latches a controlled open={true} and does not treat a later undefined as closed, so the project dropdown remains visible after selection. Normalize the picker to an always-boolean open state, matching the other field pickers.
Co-authored-by: 余剑剑 <clydeyu@lilith.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
|
||
|
|
a56f41cf03 |
feat(projects): switch project list view control to a dropdown menu (MUL-5030) (#5677)
Align the project list view control with the issue list: instead of a button that flips table <-> cards on click, the trigger now opens a dropdown menu with a 'View' header and a radio group of view modes (Table / Cards). Using DropdownMenuRadioGroup keeps it trivial to add future view modes as new menu items. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d3fac023c9 |
feat(issues): live-resort board/list on updated_at drift (MUL-5016) (#5671)
* feat(issues): live-resort board/list on updated_at drift (MUL-5016) An open Kanban board / list sorted by "Updated date" did not re-sort in real time when a card's updated_at advanced — it only picked up the new order on the next fetch (navigation, invalidation, etc.). Two triggers now re-sort live: - comment:created: a new comment bumps the parent issue's updated_at server-side (MUL-5009), but the WS handler only invalidated the per-issue timeline cache. It now also invalidates loaded updated_at- sorted issue-list/board/flat keys via invalidateUpdatedAtSortedIssue Lists, so the commented card re-sorts (and an off-window card can surface). Only updated_at-sorted keys are touched. - same-status field edit: applyIssueChange already reconciled flat windows on updated_at-sort drift but the bucketed board only marked stale on membership/status change. It now marks an updated_at-sorted bucketed key stale whenever the patch advances updated_at, mirroring the flat-window rule. Deferred to onSettle for mutations (WS flushes immediately), respecting the existing timing contract. Extracted patchChangesAnyIssueField shared helper. Tests cover the coordinator drift/targeting and the comment:created wiring. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): cover assignee-grouped boards and property/metadata events in updated_at re-sort (MUL-5016) Addresses Elon's review of #5671. Must-fix 1: invalidateUpdatedAtSortedIssueLists missed assignee-grouped boards. They fold the sort into their filter bag (issueAssigneeGroupsOptions does `{ ...filter, ...sort }`) rather than a standalone sort key, so the old bucketed+flat enumeration skipped them. Replace it with a single predicate invalidateQueries over issueKeys.all(wsId) that matches any query key with an object part carrying sort_by: "updated_at" — covering status boards, flat tables, and assignee-grouped boards (workspace + My Issues) with one rule. Must-fix 2: custom-property and metadata edits also bump issue.updated_at server-side (SetIssuePropertyValue / DeleteIssuePropertyValue / SetIssueMetadataKey / DeleteIssueMetadataKey) but flow through issue_properties:changed / issue_metadata:changed, not applyIssueChange. Their handlers patched cards in place and only invalidated property/My-Issues/ grouped windows, so an updated_at-sorted workspace board or flat table stayed in the old order. Call invalidateUpdatedAtSortedIssueLists from both onIssuePropertiesChanged and onIssueMetadataChanged. Both are committed-only paths (WS event + mutation onSuccess); the optimistic leg still uses patchIssueProperties, so no premature refetch. Tests: grouped-board targeting in cache-coordinator; updated_at vs position re-sort for onIssueMetadataChanged / onIssuePropertiesChanged (incl. the optimistic leg not refetching) in ws-updaters. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ad75977eb |
fix(agents): reset model and thinking level on runtime change (MUL-4963) (#5615)
Closes #5614 |
||
|
|
a0ec1da425 |
fix(settings): restore mounted flag on remount so auto-save can settle (#5613)
The auto-save mount effect only cleared `mountedRef` in its cleanup and never re-set it to true on setup. Under React StrictMode the initial setup/cleanup/setup cycle therefore leaves `mountedRef.current` pinned to false for the component's whole life. A successful save then never reaches the terminal branch (`succeeded && mountedRef.current`), so the status is stranded on "saving" and the success toast never fires — the settings save indicator spins forever even though the PATCH returned 200. Set `mountedRef.current = true` on every mount so the flag reflects the live component. Shared by all auto-saved settings (repositories, GitHub, etc.). Add a StrictMode regression test asserting onSuccess still fires. |
||
|
|
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> |
||
|
|
1483ce0825 |
fix(agents): always enable AI creation (MUL-4998) (#5660)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fae7b046f2 |
feat(views): support the send shortcut in manual issue create (MUL-4931) (#5583)
* feat(views): support the send shortcut in manual issue create (MUL-4931) Agent create has had Cmd/Ctrl+Enter all along; manual create had no submit shortcut at all, in either the title or the description. Reuse the configurable `send` action rather than hardcoding the chord, so a rebound or unbound shortcut follows the user's setting and the IME guards and Shift+Enter replay come along for free: - Description: pass `onSubmit` to ContentEditor, which already carries the extension. - Title: add an opt-in `onSubmitShortcut` to the shared TitleEditor. Plain Enter stays inert there — #5532 removed that trigger a day ago because it created from half-typed titles — and a single-line title has no newline to trade Enter against, so the chord path refuses a plain-Enter `send` binding. Hosts relying on plain-Enter submit (create-project, autopilot-dialog) keep `onSubmit` and are untouched. Also fixes a real double-create: `submitting` is state, so two chord presses in one tick both read the stale value and fired two creates. A ref flips synchronously and single-flights both create panels. Accessibility: the empty-title Create button moves from native `disabled` to `aria-disabled`, so it stays focusable and keyboard/SR users can finally reach the "Enter a title to create" tooltip. Keycaps are decorative, keeping the accessible name "Create Issue". Empty-title chord submits now focus the title instead of silently no-oping. Widen the `send` setting description across all four locales — it claimed to cover only chat, comments, replies, feedback and prompts. Tests: real-ProseMirror coverage for the title chord (every other editor test mocks Tiptap, so nothing verified extension ordering), plus chord/plain-Enter/ empty-title/upload-gate/single-flight/keycaps/aria-disabled cases. The single-flight test dispatches both presses inside one act(); it fails against the old state-based guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(views): cover quick-create single-flight + aria-disabled visuals (MUL-4931) Review follow-up on #5583. The quick-create ref guard shipped without a regression test, so the claim that both create panels were covered was only true of manual create. That path files a real issue, so a double-fire is a duplicate issue rather than a glitch. Add the same-tick regression: both presses dispatch inside one act(), and it fails against the old state-based guard (expected 1 call, got 2). The Button base only styles native `disabled` (disabled:opacity-50 / disabled:pointer-events-none), so the aria-disabled empty-title button stayed a fully lit, pressable-looking primary. Add local aria-disabled opacity, cursor, and press-animation styles, with no pointer-events-none — that would break the tooltip hover and the click that focuses the title. Verified against compiled Tailwind output: aria-disabled:active:translate-y-0 and the base's active:not-aria-[haspopup]:translate-y-px have equal specificity and the override emits later, so it wins without !important. 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> |
||
|
|
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> |
||
|
|
28969a8493 |
fix(autopilots): clarify Chinese schedule day-pattern labels (MUL-4927) (#5576)
* fix(autopilots): clarify Chinese schedule day-pattern labels (MUL-4927) Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): make weekly day-pattern label more explicit (每周特定几天) (MUL-4927) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
71141559c2 |
fix(shortcuts): ignore synthetic key events without a key
Guard shortcut parsing against synthetic key events that do not expose a string key. |
||
|
|
465546b83b |
feat(autopilots): redesign the autopilot schedule editor (#5457)
Replace the free-form trigger-config form with a structured schedule editor built on an orthogonal cron model: separate frequency, time, and day-of-week/day-of-month dimensions map to and from cron expressions via a dedicated grammar and mapping layer, with validation and a human-readable describe() summary. The grammar suite drives the editor against a combinatorially generated corpus of 51,755 distinct cron expressions - every token form of every field, crossed - each judged against a reference robfig/cron v3 parser. Add a server-side /autopilot/cron-preview endpoint (plus schema and React Query hook) so the editor shows upcoming run times, and echo wildcard-carrying cron lists correctly instead of collapsing them. Supporting pieces: timezone-aware formatting helper, segmented-toggle and debounced-value utilities, a reworked time-input, and refreshed en/ja/ko/zh-Hans locale strings. |
||
|
|
17ee59ccc5 |
feat(editor): standard Mermaid viewer with pan/zoom, exports and a real dialog (MUL-4908) (#5564)
* feat(editor): standard Mermaid viewer with pan/zoom, exports and a real dialog (MUL-4908) Mermaid's fullscreen view was a bespoke lightbox: no visible close button, no canvas interaction, and a toolbar that scrolled away with wide diagrams. Escape also stopped working once the iframe took focus, which is why it read as "impossible to close". Replace it with a viewer built on the shared Dialog, so it inherits the backdrop, focus trap, scroll lock, focus restore and Escape handling that HTML preview already had. The diagram stays in an `sandbox=""` iframe — isolation is not relaxed to buy interactivity. Instead the iframe is `pointer-events: none` and pan/zoom is applied from the host document as a transform on its wrapper. That inversion is also what keeps Escape working: the iframe can never take focus. - Viewer: fit-on-open, drag pan, wheel/pinch/keyboard zoom (25%-400%) anchored at the pointer, Fit / 100% / Reset, and a header toolbar that cannot scroll away. Panning is clamped so the canvas can never be lost. - Export: `.mmd`, `.svg` and `.png`. Requires `htmlLabels: false` — browsers do not rasterize Mermaid's default HTML-in-foreignObject labels through an `<img>`; verified in Chromium that they paint zero pixels AND taint the canvas, so PNG export silently produced nothing. - Inline: toolbar lifted out of the scroll container, natural-size rendering (small diagrams centered, wide ones scroll at a readable size with edge fades), copy source, and a parser message on the error fallback. - Theme switches no longer close the viewer or discard zoom/position; the previous diagram stays on screen until its replacement is ready. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): stop Mermaid drags selecting text and misfiring the viewer (MUL-4908) Two gesture defects on the inline diagram, reported by Naiyuan. 1. Dragging a diagram started a native text selection. The iframe is a replaced element, so the whole diagram box got painted with the selection highlight and the drag ran on into the surrounding comment text. Fixed with `user-select: none` on the inline scroller and the viewer canvas. Deliberately NOT by preventDefault-ing pointerdown: verified in Chromium that it also drops the default focus, which silently kills the viewer's keyboard controls (+/-, 0, arrows). 2. Any drag ended in a `click`, so trying to look along a wide diagram opened the viewer on top of the user. The inline diagram had no drag handling at all — only `onClick`. Suppressing the selection alone would have made this fire more reliably, not less, so both had to land together. Past a 5px threshold the gesture is a drag for good: releasing no longer taps, and a horizontal drag pans the scroller instead. This holds even when the diagram has no room to scroll, so an unscrollable diagram cannot open on release either. A still click, a sub-threshold jitter, and the expand button all still open the viewer. Touch is left alone — it already pans natively and its vertical drags belong to the page; the browser announces its takeover with pointercancel. Mouse and pen have no native drag-to-scroll and are panned here. Verified in Chromium: still click opens; a 150px drag pans and does not open; a drag on an unscrollable diagram does not open; 3px jitter still opens; and no gesture selects text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d1c7e1c56d |
fix(chat): always scroll to bottom when switching chat session (#5566)
The Chat tab (and floating chat window) both render the shared
ChatMessageList, re-keyed by activeSessionId so it fully remounts on
every session switch. The Virtuoso had no initial-position prop, so a
fresh mount rendered from the top; the only downward scroll driver was
followOutput, which reacts to post-mount data growth and is gated on
isNearBottom. Whether it landed at the bottom depended on a race
(cached sessions resolve synchronously and stuck at the top; fetched
ones sometimes caught a growth tick), producing the inconsistent
top-vs-bottom behavior.
Pin the newly-mounted list to the newest message with
initialTopMostItemIndex={{ index: 'LAST', align: 'end' }} so switching
sessions always lands at the bottom. align: 'end' also bottom-aligns a
last message taller than the viewport.
Co-authored-by: Bohan-J <bohan@devv.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.
|
||
|
|
82906bd9e9 |
feat(settings): move create-issue field config into a Settings Issue tab (#5556)
The quick-create field toggles used to live inside the quick create popover; per feedback on MUL-4873 they belong in the Settings page. Adds a My Account 'Issue' tab with one group per create mode backed by a new per-workspace issue-create-settings store, extends field visibility to the manual dialog (status/priority/assignee/labels/ project/due date/start date with value-reveal and overflow re-entry), and routes both dialogs' 'Customize fields' items to the tab. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ed9adc2bbe |
feat: improve create issue field controls (#5532)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d833ef520d |
fix(editor): preserve ordered list numbering on rich paste (#5538)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a18cc65b35 |
fix(inbox): render archived rows as read (MUL-4893) (#5549)
Archiving deliberately leaves `read` untouched so unarchiving can restore the real unread state (MUL-3736). InboxListItem did not account for the view, so archived rows kept an unread dot and bold title that no action in the archived view can clear. Gate the unread affordance on the view at the display layer, matching the precedent set for chat in MUL-4360. The `read` field and all archive write paths are unchanged, so unarchiving still restores real unread state. Route all four read-driven styles (dot, title weight, detail label, timestamp) through one `showUnread` flag so an archived row renders consistently as read rather than half-dimmed. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c52ebeaa0f |
MUL-4864 fix(chat): use a single New Chat draft, keyed by session not agent (#5519)
* fix(chat): use a single New Chat draft, keyed by session not agent (MUL-4864)
An uncreated chat kept one draft per agent (`__new__:<agentId>`), so
switching the agent picker mid-compose swapped the draft out from under
the user and left invisible per-agent drafts behind. That slot shape was
never a product decision: it fell out of
|
||
|
|
6b2097ccbb |
feat(inbox): archived notifications sub-view (MUL-3736) (#5518)
Adds an "Archived" sub-view to the Inbox, reachable from an entry at the
bottom of the main list, with per-row unarchive. Mirrors chat's archived
sub-view so the two surfaces share one mental model.
Backend:
- GET /api/inbox/archived and POST /api/inbox/{id}/unarchive. Kept off the
existing GET /api/inbox so installed clients keep their contract and the
unbounded archive never rides along with the main list.
- The archived query excludes any issue that still has an active row. Archiving
is issue-level, so a new notification on an archived issue leaves old archived
rows beside a fresh active one — without the guard the issue renders in BOTH
lists. The exclusion lives in SQL so neither list depends on the other's cache.
- Unarchive is issue-level (mirroring archive) and leaves `read` untouched, so a
restored unread item raises the unread badge again.
- v1 ships no pagination: LIMIT 200, newest-first, so truncation drops the
oldest rows and never hides a group's newest one.
- inbox:unarchived event, fanned out to the recipient like the other personal
inbox events.
- Two CONCURRENTLY-built indexes; inbox_item previously had none covering
workspace/archived/created_at.
Frontend:
- Separate TanStack cache per list; every inbox event invalidates the workspace
prefix, since any of them can move an item across the boundary.
- View persisted as ?view=archived, so refresh, back/forward, and the mobile
detail-back all return to the list the user was in.
- Batch actions stay main-view only — they archive from the MAIN inbox, so
offering them over the archived list would do the opposite of what it reads.
- Mobile subscribes to inbox:unarchived (its list gains the restored row); its
own archived view remains follow-up.
Known debt: no pagination, so an archive past ~200 rows is truncated silently
in the UI; the entry's count is the deduplicated count of the rows returned.
Verified: pnpm typecheck/test/lint (0 errors), go build/vet, Go inbox suite
against a real Postgres, migrations up+down, and EXPLAIN confirming both new
indexes serve the query.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
10391939c0 |
test(chat): de-flake use-chat-draft-restore reconciliation waitFor (MUL-4856) (#5516)
The reconciliation test drives real react-query fetches and mutations, so each waitFor gates on a multi-hop async chain (fetch -> effect -> mutate -> settle -> refetch). The 're-consumes on the next fetch after the consume failed' case chains the most hops and timed out under the default 1s waitFor budget when the full Vitest suite saturated the CI runner (whole run 405s, import 364s), failing with 'expected 2 times, but got 1 times' on the main merge of an unrelated Go-only PR. Widen this file's async-utility and per-test timeouts so whole-suite contention can't starve the chain. Test-only; defaults stay in force elsewhere (Vitest isolates config per file). Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2d9cf045d0 |
MUL-4861: align runtime CLI update permissions (#5514)
* fix(runtimes): align CLI update permissions Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): address update review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4fcb27a729 |
fix(issues): attach labels in the create transaction (MUL-4832) (#5510)
* fix(issues): attach labels in the create transaction (MUL-4832) Labels chosen at issue creation were attached in a second, non-atomic round-trip after the issue was already committed (web modal looped attachLabelToIssue per label; the create endpoint took no labels). A partial failure of that follow-up left the issue committed but mis-categorized, surfaced only as a toast. Carry label_ids through the create request instead: the service validates them (workspace + resource_type='issue') and attaches them inside the same transaction as the issue insert, so the issue and its labels commit together or not at all. An unknown or wrong-scope label id now fails the whole create with 400 rather than being silently dropped. Duplicate ids are idempotent (dedupe + ON CONFLICT DO NOTHING). - server: IssueCreateParams.LabelIDs + ErrIssueLabelNotFound; validate and attach in IssueService.Create; handler parses label_ids and maps the error to 400. - web: create-issue modal forwards label_ids and drops the post-create attach loop and its dead toast key. - tests: handler coverage for atomic attach, stale-id 400 (no issue left behind), duplicate-id idempotency, wrong-scope rejection; web test asserts label_ids is forwarded. Scope is deliberately labels-only. The contributor PR #5475 also auto-parsed an acceptance_criteria field from description text; that introduces a new user-facing data contract with no defined edit/display rules and is left out for a separate product decision. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): echo created labels + guard old-backend compat (MUL-4832) Addresses review of #5510. 1. Create response + issue:created event now carry the labels attached in the create transaction. IssueService.Create returns the authoritative label snapshot; the handler sets it on both the HTTP response and the issue:created broadcast payload. Without this, online members other than the creator saw the new issue unlabeled indefinitely (staleTime: Infinity, no invalidation) because this PR removed the old post-create issue_labels:changed broadcast. 2. New web + old backend compatibility. During the rolling deploy window the web app can run ahead of the backend (web auto-deploys on merge, backend deploys manually). An older backend ignores label_ids and returns an issue with no labels field. The create modal now falls back to the legacy per-label attach only when the response omits labels, so labels aren't silently dropped; when labels is present the atomic path already ran and no fallback fires. The backend always returns an explicit labels array (empty when none) as the detection signal. Tests: handler issue:created-carries-labels + response-carries-labels + response-always-includes-labels; web modal no-fallback (new backend) and fallback (old backend); ws-updaters keeps the label snapshot in list cache. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): validate create response through a schema (MUL-4832) Addresses review of #5510 (must-fix 3). The create modal keys its label-attach compatibility fallback off `issue.labels` being absent (older backend) vs a validated Label[] (current backend). But api.createIssue cast the raw JSON straight to Issue, so a malformed labels value (null, an object, a garbage array) would be !== undefined and wrongly suppress the fallback, caching a bad shape — violating the repo's API Compatibility rule. Parse the create response through CreateIssueResponseSchema: - labels absent -> undefined (older-backend signal; fallback runs) - labels valid -> Label[] (fully validated elements, not z.unknown()) - labels malformed -> undefined via .catch (safe: never masquerades as handled; worst case a redundant re-attach, never a silent drop) A whole-body parse failure degrades to EMPTY_ISSUE (never throws into React), matching the existing parseWithFallback contract. Tests: schema.test.ts covers absent / valid / null / wrong-element-shape labels and the empty-issue degrade. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): reject a malformed create response instead of faking success (MUL-4832) Follow-up to review of #5510. The prior fix degraded a schema-failed create response to EMPTY_ISSUE, which React Query treats as a successful mutation: the empty issue is written into the list cache, a blank "created" toast shows, the "View issue" link points at an empty id, and with labels the fallback attach runs against an empty issue id. A create that returns an unusable body is a failed mutation, not a safe-empty read. Fall back to null and reject: mutateAsync is already inside the create modal's try/catch, so a controlled rejection preserves the draft and shows the failure toast, and onSettled still refreshes the list so a genuinely-created issue can still surface. - CreateIssueResponseSchema tightens id to non-empty; an id-less body routes to the same reject path. - createIssue throws on parse failure (empty-message Error so the modal renders its localized "failed to create" toast; parseWithFallback already logged the schema issues + raw body). - Dropped the EMPTY_ISSUE fallback constant. - Tests: whole-body malformed and empty-id now assert createIssue rejects; only-labels-malformed still returns the real issue with labels undefined. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f9f41e991a |
fix(ui): drop native title that stacked on custom hover surfaces (MUL-4836) (#5502)
ActorAvatarBase hardcoded `title={name}` on the avatar div, so every one of
the 155 avatar renders emitted a browser tooltip. On the 34 call sites that
also pass `enableHoverCard`, the native title sat on the inner div while the
PreviewCard trigger sat on an ancestor span — different elements, so neither
suppressed the other and both surfaces showed at once.
Same class of bug in two more places: AgentStatusDot added a third `title`
inside the same trigger on the 16 sites that also pass `showStatusDot`, and
RepoUrlText rendered a `title` carrying the exact string its Tooltip already
showed.
Phase 1 scope only — remove the three duplicate titles. PreviewCard trigger
semantics, `profileLink`, ancestor detection, the 159 call sites, and the
remaining native-title cleanup are deliberately untouched.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
310634064f |
fix(transcript): explain unavailable Antigravity live events (#5182)
* docs: design Antigravity live transcript empty state * docs: plan Antigravity live transcript empty state * fix(transcript): explain unavailable Antigravity live events * chore: remove agent planning artifacts --------- Co-authored-by: CAVIN <zzz163519@users.noreply.github.com> |
||
|
|
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> |
||
|
|
e8ada0e89c |
fix(views): stop avatar hover cards opening on brushed-past avatars (MUL-4827) (#5497)
ActorAvatarHoverCardShell deferred mounting the Base UI PreviewCard root until the first pointerenter, then emulated the missing first-dwell open with a manual 600ms timer. Base UI drives hover through native mouseenter/mouseleave listeners on the trigger and installs its close path inside the mouseleave handler, so swapping the node mid-gesture loses both the event that cancels a pending open and the one that closes the popup. Flicking the pointer across a dense list armed one timer per avatar and cancelled none: in real Chromium a single sweep left 5 profile cards open with the pointer nowhere near them, and no hover-close path remained. The same cold span also opened on touch taps (Base UI sets mouseOnly) and opened instantly on focus, with no focus-visible gate and no delay. Drop the cold/warm swap, the manual timer, the manual focus-open and the focus restore, and let Base UI drive hover and focus natively. Appearance, tabIndex and the standalone-ancestor probe are unchanged. Mounting the root eagerly costs ~0.13ms of JS per avatar in real Chromium (+19.5ms at 150 avatars, the widest realistic board) and adds zero DOM while closed -- the popup subtree and its queries stay unmounted until open. Virtualization already caps mounted rows at 30 (10 per board column), and the popup-deferral work that motivated the hack is untouched. Verified: tsc --noEmit, eslint, and @multica/views vitest (212 files / 2102 tests) pass. A real-Chromium harness passes 7/7 hover/touch/keyboard scenarios on this fix and reproduces 3 failures on the pre-fix code. Still owed: real-device Electron trace on the widest/densest board. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
032c7038ee |
fix(editor): drop underline mark so pasted rich text stops emitting ++ (#5471)
StarterKit registers Tiptap's Underline extension by default. It maps both `<u>` and `text-decoration: underline` to an underline mark and serializes that mark as `++text++` — a syntax neither CommonMark nor GFM defines. The display layer (ReadonlyContent: react-markdown + remark-gfm) has no rule for it, so a saved comment rendered the delimiters literally. Disable the extension in the shared StarterKit config rather than filtering `u` out of the rich-HTML paste selector: the mark is also reachable via the extension's own Mod-u shortcut, so a paste-only fix would leave Cmd+U able to produce the same unrenderable Markdown. Removing the extension also removes its `++text++` markdown tokenizer, so existing text containing `++` now round-trips as literal text instead of being silently reinterpreted as an underline mark. This is shared editor config: it applies to every ContentEditor surface (issue comments, issue descriptions, chat, agent prompts), not just the comment box. No toolbar control, command, or stylesheet referenced the mark. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fa17e5c168 |
fix(views): stop server version from stretching the help menu (#5472)
Cap the Help menu width (max-w-56) and let the server version label wrap (break-words) so a long version string no longer widens the whole popup. The menu still keeps its min-w-40 floor when no version is shown. MUL-4828 Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fff23c1fc3 |
MUL-4818: keep Inbox and sidebar unread counts static (#5461)
* fix(sidebar): keep unread counters static Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): keep title unread count static Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
19e52e007c |
MUL-4798: make Inbox notification preference updates atomic (#5451)
* fix(notifications): make preference updates atomic Co-authored-by: multica-agent <github@multica.ai> * fix(notifications): serialize preference mutations Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
449d9887b5 |
fix(help): wrap server-version row in DropdownMenuGroup to stop Help menu crash (#5458)
Base UI's Menu.GroupLabel (rendered by DropdownMenuLabel) calls useMenuGroupRootContext(), which throws when it has no Menu.Group ancestor. The Help launcher rendered the server-version DropdownMenuLabel directly under the popup, so opening the Help menu on any deployment that returns a non-empty server_version threw during render. No error boundary sits above the app sidebar, so the throw unmounted the whole React tree — a black screen with no error, exactly as reported (MUL-4819). Wrap the row in DropdownMenuGroup, matching every other DropdownMenuLabel usage in the codebase. Also harden help-launcher.test.tsx: the old mock flattened the dropdown to plain divs and silently dropped the Group/GroupLabel contract, which is why the bug shipped. The mock now mirrors Base UI's throw, so a bare label fails the test. Co-authored-by: Bohan-J <bohan@devv.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
|
||
|
|
c599f47ba4 |
fix(redact): cover GitHub fine-grained PATs and Google API keys (#4678)
The primary server redactor now covers github_pat_ and standard AIza keys, but the client transcript safety net does not. Add matching client patterns and regression coverage so previously stored or otherwise unredacted transcript values are masked at display time. Also make the fixed-length Google key rule redact a key ending in '-' while preserving its trailing delimiter. Keep the 39-character format exact to avoid broadening false positives. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b31132edf3 |
feat(agents): add access-scope column, filter, and bulk edit to agents list (#5393)
Adds a three-state access column/filter and a bulk "Set access scope" action to the agents list, plus a fix for a state-update loop in the bulk access dialog (parent/child callback + effect cleanup were re-triggering each other). Co-authored-by: chouti <chouti@users.noreply.github.com> |
||
|
|
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> |
||
|
|
bf64bb9214 |
MUL-4781: keep machine CLI status visible
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
56f453fed6 |
feat(agents): add loading skeleton for Recent work list (#5438)
The Recent work section on the agent Activity tab reads a lazily-loaded per-agent task list. On first paint the query is still pending, so the section rendered its 'nothing finished yet' empty state — a wrong answer that flashes before real data arrives. Render a skeleton (bordered, divided placeholder rows matching the real TaskList shell) while the first fetch is in flight, keyed off the query's isLoading. Once the cache is hydrated the tab opens straight into data with no skeleton flash. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
07e2d378bd |
feat(settings): add random color option to label and property color pickers (MUL-4786) (#5437)
* feat(settings): add random color option to label and property color pickers (MUL-4786) Co-authored-by: multica-agent <github@multica.ai> * feat(settings): unified color picker popover with presets and random for labels and property options (MUL-4786) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2c482ab366 |
MUL-4781: move daemon update to machine page (#5434)
* fix(runtimes): move daemon update to machine Co-authored-by: multica-agent <github@multica.ai> * chore(runtimes): remove obsolete update hook Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
06d79c1750 |
feat(agent): add Grok Build CLI as an ACP runtime (#5285)
Add xAI Grok Build (`grok`) as a first-class Multica runtime over ACP (`grok --no-auto-update agent --always-approve stdio`), reusing hermesClient like traecli and kimi. Includes daemon discovery, protocol_family migration 174, model discovery, MCP passthrough, thinking effort, frontend branding, and product docs. Follows xAI's documented headless ACP flow: after `initialize`, read the advertised `authMethods` and send `authenticate` (preferring xai.api_key when XAI_API_KEY is set, else the cached login token) before any session operation — a real, logged-in CLI rejects session/new and session/load without it. Model discovery performs the same handshake so it returns the live catalog instead of the static fallback. `--no-auto-update` is passed as a global flag (and kept daemon-owned in the blocked custom-arg set) so a background update check can't stall an unattended ACP task. Thinking effort uses the current `--effort` flag, and the minimum grok version is 0.2.89 (ACP + authenticate + session/load + session/set_model + MCP + --effort). Closes #2895 |
||
|
|
101f21d55d | fix(ui): render select labels instead of values (#5435) |