mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
agent/j/dfd5d69f
232 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d9d3c0aa5 |
Revert "Unify resize interaction feedback (#5779)" (#5831)
This reverts commit
|
||
|
|
4bf17f5786 |
Revert "fix(resize): keep drag cursor consistent for table & chat resize (MUL…" (#5830)
This reverts commit
|
||
|
|
975cd49c3f |
fix(resize): keep drag cursor consistent for table & chat resize (MUL-5166) (#5824)
* fix(resize): keep the drag cursor consistent for table and chat resize
Table column resize and the chat floating-window resize locked the active
cursor via document.body.style.cursor, which loses the CSS cascade to any
descendant that declares its own cursor (clickable rows, inputs, buttons).
So the resize cursor showed on hover but flipped back to pointer/text/default
once the drag pointer swept over those elements — inconsistent with the
sidebar rail and react-resizable-panels, which force it globally.
Switch both to the same robust contract used by the sidebar/panels: set a
data attribute on <html> during the drag and let a global
`html[...], html[...] * { cursor: ...; user-select: none } !important` rule in
base.css win over every descendant. Chat keys the attribute by direction
(left/top/corner) so col/row/nw-resize stay correct.
MUL-5166
Co-authored-by: multica-agent <github@multica.ai>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(chat): clean up chat resize cursor lock on every drag-end path
The chat window resize started the drag with setPointerCapture but only
removed data-chat-resizing on pointerup. A pointer-capture drag can end via
pointercancel, lostpointercapture, or window blur without ever firing
pointerup; since the attribute now drives a global
`html[...] * { cursor; user-select } !important` rule, a missed cleanup would
strand it and freeze the whole page in the resize cursor with text selection
disabled.
Mirror the sidebar rail: one idempotent finishDrag() wired to pointerup,
pointercancel, lostpointercapture and blur, releasing pointer capture and
removing every listener plus the attribute from a single path. Add a
regression test covering each termination path.
MUL-5166
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(ui): release table resize cursor lock on window blur too
Table column resize wires stopResize to pointerup/pointercancel but not to
window blur. With the resize cursor now held by a global
`html[data-table-resizing] * { cursor; user-select } !important` rule, an
alt-tab mid-drag (button released while the window is blurred, so no pointerup
arrives) would strand the attribute and freeze the page. stopResize is
idempotent, so also wire it to blur — matching the sidebar rail and chat.
MUL-5166
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>
|
||
|
|
7d6a1dfab1 | Unify resize interaction feedback (#5779) | ||
|
|
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> |
||
|
|
c6fb2c5c50 |
feat(agents): assign emoji avatars by default (#5764)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
41315989bd | fix(editor): prevent incorrect comment autolinks (#5665) | ||
|
|
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> |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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
|
||
|
|
101f21d55d | fix(ui): render select labels instead of values (#5435) | ||
|
|
abeee82cd0 | feat: animate numeric metrics with NumberFlow (#5317) | ||
|
|
1427e8abd3 | feat(agents): add conversational creation studio (#5296) | ||
|
|
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> |
||
|
|
5541819f98 |
refactor(ui): unify collection page patterns (#5258)
* refactor(ui): unify collection page patterns * refactor(agents): redesign agent detail workbench (#5263) * refactor(agents): redesign agent detail workbench * refactor(agents): refine capability and settings surfaces * refactor(agents): rebuild general settings form * refactor(agents): refine overview and settings * refactor(agents): redesign custom args editor * docs(ui): record consistency audit |
||
|
|
ca46fdb483 |
fix(ui): lighten menu shadows with dedicated --menu-shadow token (#5256)
Dropdown, context menu, select, and popover surfaces used --floating-shadow, which is sized for window-level overlays and reads too heavy on trigger-anchored menus. Introduce a lighter --menu-shadow tier (surface < menu < floating) and drop the shadow-lg override on ContextMenuSubContent so submenus match their parent menu. Dialogs and sheets keep --floating-shadow. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4efcfb96e3 | feat(ui): establish surface system (#5248) | ||
|
|
595f785ac9 |
fix(desktop): make overflowing tab additions visible (#5215)
* fix(desktop): animate overflowing tab additions
* fix(desktop): recalculate tabs after pin layout changes
* docs: document daemon log rotation settings
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* Revert "docs: document daemon log rotation settings"
This reverts commit
|
||
|
|
f4de0948a2 |
refactor(ui): unify ActorAvatar size tiers + round all avatars & cropper (MUL-4277, MUL-4184) (#5133)
* refactor(ui): converge ActorAvatar size to semantic tiers (MUL-4277) Replace the free-form numeric `size` on ActorAvatar with a constrained `AvatarSize` union (xs/sm/md/lg/xl/2xl) so avatar dimensions are chosen by role instead of ad-hoc pixels. This eliminates the magic-number drift where the same role rendered at different sizes across pages. - Add `@multica/ui/lib/avatar-size` (AvatarSize union + AVATAR_SIZE_PX map + default tier). - Base `ActorAvatar` (packages/ui) and business `ActorAvatar`/`AgentStatusDot` (packages/views) now take `AvatarSize`; internal font/icon math and the presence-dot threshold read px from the map. - Migrate all web/desktop call sites (packages/ui + packages/views) from numeric sizes to tiers using the role table (12,14->xs 16,18,20->sm 22,24,28->md 30,32,34->lg 40,44->xl 56,64->2xl). - Token-ise the derived consumers `AgentAvatarStack` and `IssueAgentActivityIndicator` (px looked up internally for overlap/+N math). Out of scope (per plan): ui/avatar.tsx primitive, account-tab/AvatarPicker, mobile, and component-name disambiguation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): unify all avatars and the upload cropper to round (MUL-4277, MUL-4184) main's avatar-shape decision rendered non-human actors (agent, squad, system) and the workspace logo as rounded squares, and the upload cropper mirrored that with a square crop window. Per the updated decision (avatars_and_cropper_round_required), every avatar and the crop UI are now circular; the square path is removed rather than left as dead config. - Base ActorAvatar: always rounded-full (drop the isHuman/rounded-md split). - avatar-crop-dialog: remove the AvatarCropShape/square path; crop window is always cropShape="round". - avatar-upload-control: drop VARIANT_SHAPE; the control is always round and no longer threads a shape to the dialog (variant still drives the fallback). - Strip rounded-md/rounded-none square overrides from agent/squad/member ActorAvatar call sites; round the read-only agent/squad static wrappers. - WorkspaceAvatar: round the org logo so it matches the (now round) workspace upload/crop and the shared avatar shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): make round avatar shape a hard invariant (MUL-4277) Close the two remaining square squad-avatar paths flagged in review and prevent call sites from re-squaring the avatar: - base ActorAvatar: keep `rounded-full` as the last class in cn() so a call-site `className` can no longer override the circle. - SquadHeaderAvatar: drop `className="rounded"` (was overriding the base circle into a small rounded square). - SquadsPage no-avatar fallback: route through the shared ActorAvatarBase (`isSquad size="lg"`) instead of a hand-written rounded-md tile, so the fallback matches the image path — one shape source of truth. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): round the agent/squad avatar loading skeletons (MUL-4277) The avatar placeholder skeletons on the agent/squad list, detail, and profile-card loading states were still rounded squares (rounded-md/lg) from the pre-round era, so the avatar visibly popped from square to circle on load — inconsistent with the round avatars and with the member/inbox/issue skeletons that already use rounded-full. Round all five: agents-page, agent-detail-page, squads-page, squad-detail-page, squad-profile-card. 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> |
||
|
|
3f02083fec |
feat(editor): Linear-style issue identifier autolink (MUL-4241) (#5090)
* feat(editor): Linear-style issue identifier autolink (MUL-4241) Bare issue identifiers (e.g. MUL-123, TES-1) now render as navigable issue chips and can be typed/pasted into a real mention, instead of staying inert text. Covers Phase 1 (readonly render) and Phase 2 (editable editor); the Phase 3 batch resolve API is intentionally deferred. Phase 1 — readonly render autolink - Pure, markdown-aware detector `preprocessIssueIdentifiers` in @multica/ui/markdown rewrites bare identifiers to `[MUL-123](mention://issue/MUL-123)`, skipping code, existing links, URLs, and file/path tokens. Runs before linkify/file-card. - `isIssueIdentifier` distinguishes a bare identifier from a real mention UUID at render time (a UUID never matches the identifier pattern). - Chat markdown and comment/description readonly both resolve identifiers to a real issue via a workspace-scoped, exact-match TanStack Query (`issueIdentifierOptions`), rendering a chip on a hit and plain text on a miss / cross-workspace / while loading. The exact `identifier ===` filter enforces the workspace prefix, since the backend search matches by number. - Autolink is opt-in per surface; the shared editable preprocess pipeline is untouched so editable content is never rewritten with fake mentions. Phase 2 — editable editor input/paste - Async ProseMirror plugin resolves a completed identifier (boundary typed after it, or found in pasted text) and swaps it for an issue mention node, serialising to canonical `[MUL-123](mention://issue/<uuid>)`. Only genuine user edits seed candidates (programmatic setContent is gated), so opening existing content never rewrites it. Resolver injected from the setup layer; no React hooks inside the extension. Tests: detector (code/link/url/path skips, dedupe), core resolver (exact match, wrong-prefix miss, empty response, key shape), chat + readonly render (hit/miss/code/canonical), and the editable extension (type/paste/miss/ inline-code/mount-safe/incomplete-token). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): scope Phase 2 autolink to the captured candidate range (MUL-4241) Howard final-review blocker: the async resolve rescanned the whole document for every occurrence of the resolved identifier, so completing a new `MUL-1` also rewrote a pre-existing `MUL-1` the user never touched (persisted-content rewrite; violated "opening existing content is not rewritten"). Fix: capture the specific candidate range(s) a user transaction introduces — the token before the caret when typing, the tokens inside the pasted slice on paste — into plugin state, mapping each range forward on every subsequent transaction. After async resolve, replace ONLY those mapped ranges, verifying each still holds exactly that identifier with intact boundaries and no code/link mark. No document-wide scan by identifier. Also skip link-marked text at capture so an existing link label is never converted. Regression tests: (1) typing a new MUL-1 converts only the new occurrence, not a pre-existing identical one; (2) paste converts identifiers inside the paste range but leaves an identical one outside it untouched; (3) an identifier already carrying an explicit link mark is not replaced. 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> |
||
|
|
c6783efd88 |
feat(views): unify avatar upload with crop editing (#5074)
* feat(views): unify avatar upload with crop editing across web/desktop Add a shared AvatarUploadControl + AvatarCropDialog used by the user, workspace, agent, and squad avatar entry points. Cropping (pan/zoom, fixed 1:1) and compression run client-side on canvas; the existing /api/upload-file + avatar_url chain is reused unchanged (no backend/API/DB changes). This collapses four hand-rolled upload buttons into one control and removes AvatarPicker. Also make the shared display avatar treat all non-human actors (agent, squad, system) as rounded squares — completing the "circles are for humans" convention the editors already assumed, so display and editors agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(views): rebuild avatar cropper to the "Edit avatar" reference form Replace the hand-rolled canvas cropper with react-easy-crop to match the requested design: full-bleed image with a dimmed overlay outside a bright crop window, a rotate control, a zoom slider flanked by −/+, and a Reset / Cancel / Save footer. Round window for people, rounded-square for non-human actors; output stays a 512px square (webp, jpeg fallback) through the same upload/avatar_url chain. avatar-crop.ts keeps the encode pipeline and gains rotation-aware getCroppedAvatarBlob; the interactive geometry now lives in react-easy-crop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): round square avatar crop frame --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0ffb5f6863 |
fix(markdown): drop trailing markdown delimiters from linkified URLs (MUL-4242) (#5139)
Supersedes the read-only gfm-autolink approach (#5091), which split URL linkification across two engines: the editor kept the string preprocessor (urls:true) while the read-only renderer let remark-gfm autolink (urls:false) plus a remark-cjk-autolink plugin. gfm autolink still swallowed the closing `**` into the href whenever a CJK punctuation immediately followed (`**url**(MUL)`), so bold-wrapped URLs stayed broken in Chinese prose. Fix it once, at the shared string layer: collectLinkifyMatches now drops a trailing run of markdown delimiters (`*`, `~`) from each URL match, so `**url**` yields a clean `**[url](url)**` and the emphasis closes. Editor and read-only share preprocessMarkdown / preprocessLinks again — one linkify logic, no renderer-specific machinery. - linkify.ts: trailing-delimiter strip in collectLinkifyMatches; CJK rescan is keyed off the terminator index, independent of the trim. - Remove the urls:false split (detectLinks / preprocessLinks / preprocessMarkdown) and delete the remark-cjk-autolink plugin. - Tests: **url**, **url**(CJK, CJK multi-URL, explicit link untouched, and the trailing-* tradeoff. Known tradeoff: a bare URL that genuinely ends in `*` (e.g. a glob) has the `*` dropped from the link — identical to GitHub's autolink, locked by test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cac2965ddb |
fix(markdown): autolink read-only URLs in the parse tree, not raw text (MUL-4242) (#5091)
* fix(markdown): autolink read-only URLs in the parse tree, not raw text Read-only markdown surfaces (comments, descriptions, chat) pre-linkified bare URLs by rewriting the raw source to [url](url) before parsing. Because linkify-it treats `*` as a valid URL character, a bare URL followed by a bold close — `**PR:https://…/5081**` — had the trailing `**` swallowed into the match and rewritten as [url**](url**). That consumed the emphasis closer (the bold never closed; the leading `**` rendered as literal asterisks) and corrupted the href with a trailing `**` (MUL-4242). Let remark-gfm autolink URLs in the parse tree instead, where emphasis is already resolved so an adjacent delimiter can never be absorbed. The custom string pass now runs in a `urls: false` mode on read-only surfaces and only linkifies file paths (which gfm never does). A small remark plugin (remark-cjk-autolink) re-applies the existing CJK URL boundary to gfm's autolink literals so `https://x/a。后面` still stops at 。. The Tiptap editor path is unchanged (`urls: true`): @tiptap/markdown does not autolink bare URLs, so it still needs the string pass. Note: read-only URL autolinking now follows GFM semantics (scheme, www., or email required); bare fuzzy domains like `NBA.com` render as plain text on read-only surfaces, matching CommonMark/GFM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(markdown): keep every URL in a CJK-separated run linked in readonly Follow-up to the read-only autolink fix. remark-gfm glues `url1、url2` into a single autolink literal because it treats CJK punctuation as a URL character; remark-cjk-autolink trimmed only at the first terminator and dropped the tail to plain text, so the second URL stopped being a link — a same-class regression of MUL-4242 for CJK-punctuation-separated URLs (flagged in review). Re-derive the segments with detectLinks (which reuses collectLinkifyMatches' truncate-and-rescan) and rebuild the [link, text, link, …] sequence, so every URL in the run stays linked. Adds a read-only test for `两个地址 https://a.com/x、https://b.com/y`. 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> |
||
|
|
a51ab4d551 |
feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) (#5076)
* feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) Replace the floating chat FAB/window with a first-class Chat tab under Inbox, laid out as an IM-style two-pane surface (thread list + conversation). Highlights: - New Chat page (packages/views/chat/chat-page.tsx) with URL-addressable session selection; web + desktop routing wired up. Removes the old chat-fab / chat-window / resize-handles / context-items paths. - IM thread list: agent avatar + last-message preview + IM timestamp, red unread *count* badge (read-cursor model), presence-gated typing vs waiting. Rename lives only in the conversation header ⋯ menu (not the list hover). - Per-session conversation header (rename / view agent / delete), agent-aware empty state (avatar + name + description + starter prompts), and a deterministic clean-title derivation from the first message. - Server: read-cursor unread model (migration 145) and per-user pinned agents (migration 146, dedicated chat_pinned_agent table + handler/queries). New-agent welcome chat auto-enqueues a real agent run (LLM intro, no static template). - Design: fade the global --border token; borderless list headers on Chat/Inbox, kept (faded) on the conversation header. Verified: pnpm typecheck (all packages), go build ./..., go vet, gofmt. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): make new-agent welcome read as an agent-initiated intro (MUL-4230) The "meet your new agent" chat used to insert a fake user message ("👋 Hi! Please introduce yourself …") and have the agent reply to it, so the thread looked like the creator prompting the agent. Drop the persisted user message. Flag the auto-created session is_agent_intro (migration 147) and drive the intro run server-side: the daemon builds a proactive self-introduction prompt for such sessions (buildChatPrompt) instead of a "reply to their message" prompt. The intro stays LLM-generated; the thread now opens with the agent's own message, as if it reached out first. - migration 147: chat_session.is_agent_intro - CreateChatSession carries the flag; sendAgentWelcomeChat no longer persists/publishes a user message - daemon: ChatIntro threaded from session flag → intro prompt Co-authored-by: multica-agent <github@multica.ai> * feat(chat): Settings toggle for the floating chat window (MUL-4235) (#5080) * feat(chat): Settings toggle for the floating chat window (MUL-4235) Re-introduce the floating chat overlay on top of Chat V2 as an optional, Settings-gated surface instead of deleting it outright. - Settings → Preferences → Chat: a switch (floatingChatEnabled, persisted client preference, default ON) to show/hide the floating window. - FloatingChat wrapper owns the two gates: the preference, and the /chat route (hidden on the tab so the same activeSessionId isn't shown twice). - ChatFab + a compact ChatWindow that reuse the shared useChatController and conversation components, so activeSessionId stays in lockstep with the tab. - Restore use-chat-context-items so the overlay's @ surfaces the current issue/project (the 'current context' affordance) — the tab stays manual. - i18n (en/zh-Hans/ja/ko), store unit tests. typecheck: core/views/web/desktop green. tests: chat store 9, settings 82, chat 39 pass. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): dedicated Chat settings tab, floating window opt-in (MUL-4235) Address review: give Chat its own Settings tab instead of a section inside Preferences, and default the floating window OFF (opt-in). - New Settings → Chat tab (chat-tab.tsx) under My Account; moves the floating-window toggle out of the Preferences tab. - floatingChatEnabled now defaults OFF — only an explicit enable from the Chat tab mounts the FAB/overlay. - i18n: page.tabs.chat + a top-level chat block (en/zh-Hans/ja/ko); revert the Preferences chat section and its test mock; store tests updated for the opt-in default. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop starter prompts from chat empty state (MUL-4237) (#5081) The three starter prompts (List my open tasks by priority / Summarize what I did today / Plan what to work on next) read as filler more than help, so remove them along with the now-unused returning_subtitle ("Try asking"). The empty state keeps its agent-aware header — avatar + "Chat with {name}" + optional description — and the composer stays the entry point. Locale keys dropped across en/zh-Hans/ja/ko (parity preserved). Based on the Chat V2 branch (parent MUL-4171, #5076), not main. Co-authored-by: Lambda <lambda@multica.ai> * feat(chat): pin a chat to the top of the Chat list (MUL-4240) (#5082) Builds on Chat V2 (#5076). Adds a per-conversation pin so a user can keep important chats at the top of the IM-style thread list, above the activity-sorted rest. Backend: - migration 148: chat_session.pinned_at (nullable) + partial index; the timestamp doubles as the pinned-group sort key and the boolean flag. - list queries order pinned-first, then by most-recent activity. - SetChatSessionPinned query + PATCH /api/chat/sessions/{id}/pin handler; pinning never bumps updated_at, so an unpinned chat won't jump the list. - ChatSessionResponse.pinned + chat:session_updated carries the new state. Frontend: - ChatSession.pinned; setChatSessionPinned API + useSetChatSessionPinned with optimistic re-sort; shared sortChatSessions comparator. - thread list: pin indicator on pinned rows + pin/unpin hover action; list sorted pinned-first so it stays ordered after cache patches. - realtime patch re-sorts on pin change; en/ja/ko/zh-Hans strings. Tests: SetChatSessionPinned handler test, sortChatSessions unit tests. * feat(chat): round send button, move file upload into a + menu (MUL-4250) (#5088) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): match chat list selected-item style (inset padding + rounded) (#5093) Wrap the inbox list in p-1 and give each row rounded-md/px-3 so the selected bg-accent reads as an inset rounded card — same treatment the chat thread list already uses — instead of a full-bleed, sharp-cornered highlight. Content stays 16px-inset (p-1 + px-3 == old px-4). MUL-4253 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): rename + menu upload item to "Image or files" (MUL-4250) (#5092) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop welcome intro session repeating the same introduction (MUL-4259) The is_agent_intro flag on chat_session is persistent, so every follow-up turn on a welcome session re-selected the self-introduction prompt in buildChatPrompt and the agent kept replying with the same intro instead of answering the user. Gate resp.ChatIntro at claim time on the session still having zero human (role='user') messages via a new ChatSessionHasUserMessage query: the first, message-less server-driven turn introduces the agent; once the creator replies, later turns fall back to the normal reply prompt. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review findings + unbreak CI (MUL-4171) - task:failed now refreshes the sessions list (invalidateSessionLists), so the thread-list preview / unread / sort stays correct after an agent failure — FailTask persists a failure chat_message but only broadcasts task:failed, mirroring the chat:done success path. - Self-heal stale chat deep links: once the sessions list has loaded and a ?session= id isn't in it (deleted / no access / never existed) with nothing in flight, clear the selection instead of rendering an editable empty chat that would POST into a nonexistent session. Freshly-created sessions are exempt (they carry optimistic messages + a pending task). - CI: add the new parameterless `chat` route to link-handler's WORKSPACE_ROUTE_SEGMENTS and to paths/consistency.test.ts (route set + expectedSegments) — keeps the two in sync, fixes the failing @multica/core test. - Fix a MUL-4235/MUL-4237 merge collision that broke @multica/views typecheck: chat-window.tsx still passed the removed `onPickPrompt` prop to EmptyState. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): self-heal dangling session in shared controller, not just ChatPage (MUL-4171) Re-review follow-up: the stale-session self-heal only lived in ChatPage, so the floating ChatWindow still entered from a persisted activeSessionId and would render an editable empty chat (then POST into a nonexistent session) when the selected session was deleted / lost access off the /chat route. - Move the self-heal into the shared useChatController so every surface (tab and floating window) drops a dangling activeSessionId once the sessions list has loaded and doesn't contain it. - Harden ensureSession: trust the current id only when it's in the loaded list or is a just-created session still awaiting the refetch; a dangling id falls through to create a fresh session instead of POSTing into a 404. - Exempt just-created sessions via an OPTIMISTIC-write signal (hasOptimisticInFlight: pending task or optimistic- message), not hasMessages — a session deleted elsewhere with real cached history stays eligible for self-heal. Add a unit test for the discriminator. Co-authored-by: multica-agent <github@multica.ai> * test(views): fix app-sidebar useWorkspacePaths mock for the new chat nav (MUL-4171) The AppSidebar personal nav gained a `chat` item, so it calls `useWorkspacePaths().chat()` at render. The app-sidebar.test.tsx mock hadn't been updated, so `p.chat` was undefined and every render threw `TypeError: p[item.key] is not a function`, failing @multica/views#test in CI. - Add `chat: () => "/acme/chat"` to the mocked useWorkspacePaths. - Route the chat-sessions query key through a mutable `chatSessions` fixture. - Add coverage for the Chat nav: renders the link, badges the summed unread_count, and hides the badge when all sessions are read — so this drift is caught next time. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): use the original floating window, not the rewritten one (MUL-4235) (#5102) Follow-up to the merged #5080, which shipped a hand-written, simplified ChatWindow and lost the original's animations / drag-resize / expand-minimize. The floating window is just a quick entry point — it should be the original UI, not a rewrite. - Restore chat-window.tsx, chat-fab.tsx, chat-resize-handles.tsx and use-chat-resize.ts verbatim from main (0-diff): motion animations, drag resize, expand/minimize and the session dropdown are back. - Restore the empty_state.returning_subtitle + starter_prompts i18n keys the original window renders (V2 had dropped them); drop the now-unused window.open_full_tooltip key the rewrite added. - Settings gating is unchanged: FloatingChat still wraps the original FAB + window, gated by floatingChatEnabled (default off) and hidden on /chat. typecheck: core/views/web/desktop green. tests: chat + settings views 126 pass. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): archive chats from the list, delete only from Archived (MUL-4263) (#5098) Restore an archive flow as the reversible sibling of delete: - Chat list hover now offers Archive (not Delete); pin/stop unchanged. - A footer entry ('Archived · N') opens an Archived view listing archived chats; hard delete lives only there (hover -> unarchive + delete, with the existing inline confirm). - Conversation header ⋯ menu mirrors this: active chats archive, archived chats unarchive/delete. Backend: PATCH /api/chat/sessions/{id}/archive flips status active<->archived (SetChatSessionArchived), broadcasts status on chat:session_updated so other tabs re-sort into the right list. SendChatMessage already refuses archived sessions, so archived chats stay read-only until unarchived. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) (#5100) * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop chat-list archive marker, keep conversation read-only (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * feat(chat): apply archived-agent read-only to the floating chat window (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address floating-window + archived-agent review blockers (MUL-4171) Re-review follow-up on the restored floating ChatWindow + archive flow: 1. Floating stale-session self-heal. The restored ChatWindow doesn't use the shared controller, so its ensureSession trusted any non-empty activeSessionId and there was no dangling-session cleanup — a deleted / no-access persisted session could send into a nonexistent session. Ported the same guard used for the tab: a self-heal effect that clears a dangling activeSessionId once the sessions list has loaded, and ensureSession only trusts an id that's in the list or has an in-flight optimistic write (hasOptimisticInFlight, reused from use-chat-controller). handleSend seeds the optimistic message + pending task before setActiveSession, so a freshly-created session is never mis-cleared. 2. Floating dropdown bypassed archive-first safety. Its active rows offered a hard-delete, letting the floating window destroy active chats and skip the "archive first, delete only from Archived" model. Active rows now ARCHIVE (reversible, one-click) like ChatThreadList; the floating window offers no hard-delete — unarchive/delete live only in the full Chat page's Archived view (reachable via expand). Removed the now-dead delete-confirm machinery. 3. Orphan user message on archived-agent send. SendChatMessage created the chat_message before EnqueueChatTask, which rejects an archived / runtime-less agent — a stale client would land a user message then get a 500, orphaning it. Added a preflight that checks the session agent's archived / runtime state and returns 409 before any mutation, plus a handler test asserting the send is rejected with no message persisted. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bcad2edc9e |
feat(issues): add Cmd+F in-page find to issue detail (MUL-4126) (#4989)
* feat(issues): add Cmd+F in-page find to issue detail Replace the stopgap "find-in-page is virtualized" toast with a real find bar (MUL-4126). Cmd/Ctrl+F opens a floating bar with keyword input, live match count, and prev/next navigation that scrolls to and highlights each match. - Opening find force-renders the comment timeline flat (reusing the existing highlightCommentId escape hatch) so off-screen comments become searchable — the root cause of the original complaint. - Matches are painted with the CSS Custom Highlight API (ranges only, no DOM mutation), so highlighting layers cleanly over React-rendered markdown and the contenteditable title/description editors. - Scroll-to-match drives container.scrollTop directly (never native scrollIntoView; #3929). Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep in-page find usable without CSS Custom Highlight API On browsers lacking the CSS Custom Highlight API, `!supported` was folded into the match-collection path, so Cmd/Ctrl+F opened the bar and swallowed native find but reported 0 matches and could not navigate — strictly worse than the native find it replaced (MUL-4126 review). Feature-guard only the paint calls now: match collection, count, active index, and scroll-to-match run regardless of support, while `CSS.highlights.set/delete` / `new Highlight` stay behind the guard. The MutationObserver re-derives ranges even when unsupported so fallback counting/navigation track live DOM churn. Adds a hook test that drives the degraded path (jsdom has no highlight API) and asserts counting + prev/next still work. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ebd0248be2 |
MUL-4016: fix mention tokenizer stacktrace backtracking (#4889)
* MUL-4016: fix mention tokenizer stacktrace backtracking Co-authored-by: multica-agent <github@multica.ai> * fix(editor): de-ambiguate escaped-label regexes to kill ReDoS (MUL-4016) The mention/slash/file-card label regexes used `(?:\\.|[^\]])` where both alternatives can consume a backslash. On an unterminated match, each `\x` run is enumerated 2^n ways — pasting a Java stacktrace (`\~\[...\]`) or a crafted ~50-char string freezes the main thread for seconds (GitHub #4881). Exclude backslash from the char class (`[^\]\\]`) so a backslash can only be consumed by `\\.`. The alternatives become disjoint and matching is linear; legal escaped-bracket labels like `David\[TF\]` still parse unchanged. Fixed in all four sites that shared the pattern: - mention-extension.ts tokenize() - slash-command-extension.ts start() + tokenize() - file-card.tsx FILE_CARD_MARKDOWN_RE - packages/ui/markdown/file-cards.ts NEW_FILE_CARD_RE (runs on every read-only comment/description render, not just the editor) Adds adversarial regression tests (repeated `\a` + missing closing bracket) that fail in ~10-40s against the old regexes and pass in <1ms after the fix. Builds on #4889's marker-first mention start(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): escape backslash in mention/slash labels for round-trip (MUL-4016) Follow-up to the de-ambiguation fix, addressing Howard's PR review. The linear tokenizer now treats "\" as an escape lead (\\.), so a label whose serialized form contains a bare "\" adjacent to the closing "]" no longer parses back — the "\]" is consumed as an escaped bracket and swallows the boundary. The old ambiguous regex tolerated this by chance; the de-ambiguation exposes it. mention/slash renderMarkdown escaped only [ and ], not \. Switch both to the shared escapeMarkdownLabel() (escapes [ ] \ ( )) and mirror it on parse with replace(/\\([[\]\\()])/g, "$1"), matching what file-card already does. This also converges the three tokenizers on one escape contract. file-card was already correct and is unchanged. Adds parameterized round-trip tests for labels containing "\" / "\]" / parens (e.g. "A\\", "ends\\", "a\\]b"); these fail on the old serializer and pass now. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c3a33fff49 |
fix(markdown): render inline data-URI images (MUL-3961) (#4832)
* fix(markdown): render inline data-URI images (MUL-3961) Inline data:image/* URIs (QR codes, charts, base64 screenshots) were stripped and rendered as broken images. Two gates dropped the src: - rehype-sanitize's protocols.src only allowed http/https - react-markdown's defaultUrlTransform blanks any data: URL to '' Allow data:image/* through both gates, narrowed to image subtypes only (non-image data URIs stay rejected) and leaving every other src form unchanged. file-cards.ts data: rejection is intentional and untouched. Co-authored-by: multica-agent <github@multica.ai> * fix(markdown): allow data:image/* in ReadonlyContent too (MUL-3961) Issue comments and other read-only surfaces render through ReadonlyContent, which keeps its own sanitize schema + urlTransform separate from the base Markdown component. Both still stripped data: URIs, so an agent inlining an auth QR code in an issue comment still saw a broken image. Apply the same image/*-narrowed data: allowance (protocols.src + attributes.img + urlTransform) and add a regression test covering the comment / readonly path. file-cards.ts data: rejection stays untouched. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <agent-j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c0c41fa0b4 | fix(views): gate right sidebar motion to toggles (#4335) | ||
|
|
27fcbb015f |
Polish desktop sidebar motion
Polish desktop chrome/sidebar alignment and add motion-based transitions for left and right sidebars. |
||
|
|
0f36c88855 |
fix(markdown): don't auto-link bare filenames as external URLs (#4245)
* fix(markdown): don't auto-link bare filenames as external URLs Agent comments that mention a project file like `plan.md` were turned into clickable links to https://plan.md (dead external site). linkify-it fuzzy detection matches `plan.md` as a domain because its extension is also a valid TLD (md = Moldova; likewise io, sh, rs, py). Suppress schemeless (fuzzy) linkify matches whose token is a bare filename (single segment ending in a known source/config extension). Explicit schemes (`https://plan.md`) and real domains (`example.com`) are unaffected. The file extension list is now shared between the file-path and bare-filename detectors so they can't drift. Fixes #4222 Co-authored-by: multica-agent <github@multica.ai> * docs(markdown): drop inaccurate .io example from bare-filename comment io is not in the FILE_EXTENSIONS list, so .io domains are never suppressed. Listing it as an example was misleading. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7c71007e6e |
refactor(comments): trim trigger preview copy and unify composer buttons (#4174)
Two related cleanups to the issue comment/reply/edit composer: - Drop the trigger-preview "context" copy added in #4147 (chip prefix `trigger_context_*` and per-context popover titles `trigger_preview_title_*`). The actual "align context" fix in #4147 was the backend/hook work; the copy was redundant decoration. Removes the `context` prop, the dead i18n keys across en/zh/ja/ko, and the corresponding test assertions; the popover title falls back to the original single `trigger_preview_title`. - Edit-comment footer: lay the trigger chip on a single row with the action cluster (📎 Cancel Save) on the right, attachments on their own full-width row above. The 📎 now sits with the action buttons, matching the new-comment and reply composers. - Unify composer buttons on shadcn `Button`: `FileUploadButton` renders a ghost icon button instead of a hand-rolled circle, and the reply submit button uses `Button` (icon-xs, ghost-when-empty / primary-when-typed) instead of a hand-rolled element. Sizes: 📎 and reply submit are both icon-xs (24px). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3ce4cf6f2f |
fix(lists): navigate rows via onClick, not a nested row anchor (#4146)
Clicking a row's ⋯ kebab (or any in-row control) full-page reloaded the app. The row was a whole-row <AppLink>, so a child's stopPropagation stopped the event before AppLink's onClick (which calls preventDefault to cancel native anchor navigation and do an SPA push) could run — leaving the browser to perform the native <a> navigation, i.e. a full reload. It was also invalid HTML: interactive content (button/menu) nested in an <a>. Rework all five ListGrid row surfaces (agents, runtimes, skills, autopilots, squads) to a plain <div> row whose whole-row navigation is a mouse onClick (new useRowLink hook): left-click pushes, cmd/ctrl/middle opens a background tab. Interactive cells (checkbox, kebab) stopPropagation so they never trigger row nav — and with no <a> ancestor there is no native navigation to cancel, so the reload class of bug is gone. Names are plain text since the row itself is the click target. projects is unchanged — its inline-editable cells make it a deliberate name-link exception. Also fixes two adjacent defects found in the same menus: - agents/runtimes kebab triggers reused the shared <Button>, which lacks the data-popup-open styling the other surfaces have, so the trigger vanished and lost its background while its menu was open. Switch them to the bare-button trigger with data-popup-open: visible + highlighted. - agents archive menu items used className="text-destructive" instead of variant="destructive", so the base focus style overrode the red on hover. Switch to variant (list row + detail page). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
76c687d39a |
fix(markdown): allow attachment download file-card hrefs (#4145)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
63cf0ed308 |
feat(lists): rebuild all six list surfaces on a shared Linear-style list grid (#4038)
* fix(issues): render thread replies in chronological order (#3691)
collectThreadReplies walked the parent_id tree depth-first, so an agent
reply forced to nest under its trigger comment rendered before earlier
sibling replies (A-D-B-C instead of A-B-C-D) whenever the agent returned
late. Sort the collected subtree by created_at (id tie-break) so the
thread reads in arrival order — the same order the server already feeds
agents via `comment list --thread` (ListThreadCommentsForIssue).
All other consumers of the array (resolution derivation, fold bars,
counts, deep-link) are order-independent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rebuild skills list on shared Linear-style list grid
- new ListGrid primitives (subgrid: single source of truth for column tracks)
- skills list: sortable columns, used-by avatar stack, source/creator columns,
row kebab + batch toolbar with add-to-agent and delete
- skill view store in core; addAgentSkills client method; HoverCheck extracted
to views/common (issues header now imports the shared copy)
- locale keys for list actions/filters and the reworked detail page
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): rework detail page into overview/files tabs
- tabs directly under the breadcrumb header: overview (default) and files
- overview: identity block + rendered SKILL.md as the main column, right
rail with metadata card (source/creator/updated, inline name+description
edit toggle) and used-by panel with bind/unbind
- files: file tree + viewer/editor unchanged; SKILL.md "edit" jumps here
- header kebab menu (copy skill ID, delete); page-level save bar shared by
both tabs; tab state persisted in ?tab=
- file tree: ARIA tree roles + roving-tabindex keyboard navigation
- drop the old right sidebar (metadata dl, permissions paragraph)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* revert(skills): restore detail page to main, keep branch list-only
Drop the overview/files tabs rework from this branch so the PR scope is
the list rebuild only. skill-detail-page.tsx and file-tree.tsx are back
to the main versions; the locale detail/file_tree sections are restored
to match. The detail rework is preserved on stash/skills-detail-tabs
for a follow-up PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drop description column from skills list
Description is agent-facing routing metadata, not a scannable list
property — Linear's display options expose no description column for
the same reason. Removes the cell, column key, display toggle, lg grid
track, skeleton cells, and the now-dead table.description /
table.no_description locale keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): drive list column hiding by container width, drop by priority
Replace viewport sm:/lg: breakpoints with Tailwind v4 container query
variants (@2xl/@4xl) on the list wrapper, so an open sidebar or split
pane narrows the column set instead of squashing tracks. Remove the
min-w-fit + overflow-x-auto horizontal-scroll fallback: when space runs
out, low-priority columns (created/source/creator, then updated) drop
and return as the container widens; name and usedBy never drop. ListGrid
conventions comment updated — this is the template for all list pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): virtualize list rows with @tanstack/react-virtual
Linear-style headless virtualization: the virtualizer computes the
visible index range and offsets; offsets land as padding on the
scrolling ListGridBody so mounted rows stay direct subgrid children and
column alignment is untouched. Fixed 48px rows skip per-row measurement.
Hideable column tracks move from max-content to deterministic widths
(CSS vars) — with only the visible slice mounted, content-driven tracks
would resize during scroll. A user-hidden column zeroes its var so the
track still collapses; per-cell max-w caps move into the tracks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(skills): list tiers must fit their container trigger width
The @4xl tier's track sum (~1080px with gaps) exceeded its 896px
trigger; with the horizontal-scroll fallback gone, the right-side
columns were clipped unreachably between 896-1080px. Move tier 3 to
@5xl (1024px), trim usedBy/source/creator tracks, and document the
fit invariant with its arithmetic next to the template and in the
ListGrid conventions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(skills): show description as subtext under the skill name
Lives in the name track as a second truncated line (max-w 36rem,
title attr for the full text) — no track, no header, no slot in the
responsive arithmetic. Both lines fit the fixed 48px row, so the
virtualizer contract is untouched; rows without a description center
the name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Revert "feat(skills): show description as subtext under the skill name"
This reverts commit
|
||
|
|
70b90d287c |
MUL-3267: fix(markdown): disable single-dollar inline math in web renderer
remark-math defaults to singleDollarTextMath: true, so any paragraph containing two dollar amounts (e.g. "costs $120/mo (~$85 net)") has the text between them parsed as inline TeX and rendered by KaTeX in an italic math font, with ~ treated as a non-breaking space. Disable single-dollar parsing in both web render paths, matching GitHub's behavior; explicit $$...$$ math still renders. Co-authored-by: Matt Voska <voska@users.noreply.github.com> |
||
|
|
7d28b5a040 |
fix(issues): remove duplicate emoji reaction entry from comment header (#4068)
The comment card exposed two identical add-reaction affordances: a QuickEmojiPicker in the header's top-right actions and the add button inside the bottom ReactionBar. Keep only the bottom one. - Drop QuickEmojiPicker from the root header and reply-row headers - Always show the ReactionBar add button (it is the only entry point now), removing the isLongContent gating - Remove the now-unused hideAddButton prop from ReactionBar MUL-3262 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a02b3dfb4a |
feat(issues): move agent live signal into the issue-detail header (#3879)
* feat(issues): move agent live signal into the issue-detail header Replace the in-body sticky "agent is working" card (AgentLiveCard) with a compact chip in the issue-detail header, so the live signal sits in one fixed place and never competes with sticky banners in the content column. - New IssueAgentHeaderChip: avatar(s) + live-ticking blue elapsed time; click opens a popover listing every active task. - Popover reuses ExecutionLogSection's ActiveTaskRow (now exported) so the popover and the right panel are literally the same row — no duplication. - PopoverContent gains an optional keepMounted so the row's confirm dialog survives the popover closing on Stop. - Running rows in ExecutionLogSection drop the blue spinner for a live-ticking blue elapsed timer (panel + popover share this). - Source the chip from the workspace agent-task snapshot filtered by issue (same source as board/list indicators, zero extra network); delete the old AgentLiveCard + its test and its heavy per-issue WS machinery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): live event count on the agent chip + execution-log rows Show a live "N events (elapsed)" on running agents, consistent across the header chip, its popover rows, and the right-panel execution log. - Read the shared per-task message cache (taskMessagesOptions, kept live by useRealtimeSync's global task:message handler) instead of a bespoke subscription — one source of truth, deduped across chip / popover / panel / transcript, no extra WS wiring. - Extract <RunningStat> (event count in info-blue + elapsed in muted parens) so all surfaces render the running stat identically. - ExecutionLogSection running rows now show the same "N events (elapsed)"; the transcript opened from them streams live from the shared cache. - Chip: single running shows events (elapsed); multiple shows "N working". - i18n: add agent_live.event_count (4 locales). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d6540a1869 |
fix(clipboard): support copy over http:// via execCommand fallback (#3810)
navigator.clipboard is only exposed in a secure context (https or localhost). On self-hosted instances served over plain http:// it is undefined, so every copy / "copy all" / export button silently failed and left the clipboard empty (GitHub #3781). Add a shared copyText(text): Promise<boolean> helper in @multica/ui/lib/clipboard that prefers the async Clipboard API and falls back to a hidden <textarea> + document.execCommand('copy') for non-secure contexts. Migrate all direct navigator.clipboard.writeText call sites (code blocks, agent transcript copy-all, token / webhook / issue-link copy, etc.) to it, gating success side-effects on the returned boolean, and remove the now-redundant copyMarkdown wrapper. Secure-context users keep the native path unchanged. MUL-3068 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b0d479c6e7 | fix: use mentions for chat context (#3755) | ||
|
|
91c1e51411 |
feat(editor): add / slash-command palette for invoking agent skills (#3159)
* feat(editor): add / slash-command palette for invoking agent skills Adds a `/` trigger in the chat box that opens a popover listing the active agent's skills. Selecting an item inserts a `[/label](slash://skill/<id>)` token; the daemon extracts those IDs in `buildChatPrompt` and emits an "Explicitly selected skills:" block using the canonical names from the agent's skill registry — labels are display-only and never trusted. Built on Tiptap's `Mention` extension so the suggestion lifecycle, keyboard routing, and IME handling mirror the existing `@` mention UX. Item list is sourced from the React Query workspace cache (no per-keystroke fetch). Gated behind a new `enableSlashCommands` prop so only `chat-input` opts in; other `ContentEditor` consumers (issue editor, comments) are unaffected. Read-only markdown surfaces render the token as a `.slash-command` pill via a custom link renderer + sanitize-schema/url-transform allowlists. Closes #3108 * fix(i18n): add slash_command editor copy for ko/ja The PR added slash_command popover empty-state keys to en + zh-Hans only; locales/parity.test.ts requires every locale to cover every EN key, so ko and ja failed CI. Add the two keys (no_skills_configured, no_results) matching existing skill terminology (스킬 / スキル). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d013a31db9 |
fix: escape special chars in image alt and file-card filename (MUL-2899) (#3644)
* fix: escape special chars in image alt and file-card filename during Markdown serialization Filenames containing Markdown label characters ([, ], \, (, )) broke the  and !file[name](url) syntax, causing raw Markdown to render instead of the image/file card. - Add shared escapeMarkdownLabel utility - Apply escaping in file-card renderMarkdown - Add renderMarkdown to ImageExtension for alt text escaping - Add regression tests Closes #3616 Co-authored-by: multica-agent <github@multica.ai> * fix: address review — fix tokenizer regex, unescape labels, add regression tests - Remove unused tokenizeFn (TS6133) - Change file-card regex to (?:\\.|[^\]])* to handle escaped brackets - Unescape labels in tokenize() and preprocessFileCards() - Export ImageExtension for testability - Rewrite tests: 3 describe blocks covering ImageExtension.renderMarkdown, file-card tokenizer round-trip, and preprocessFileCards (6 tests total) - typecheck and vitest both pass Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1aa742053b |
i18n: add japanese locale (MUL-2893) (#3538)
* i18n: add japanese locale * fix: spacing issues * refactor * fix(desktop): set <html lang> before paint to avoid JA Kanji font flash Switch the documentElement.lang sync from useEffect to useLayoutEffect so lang is committed before the first paint. Otherwise Japanese desktop users saw one frame of Kanji rendered with the Chinese-first fallback stack before the html[lang|="ja"] CJK override applied. Also fix the stale selector in the HTML_LANG comment (html[lang^="ja"] -> html[lang|="ja"]). Addresses review nits on MUL-2893. Co-authored-by: multica-agent <github@multica.ai> * fix(docs): tokenize the ideographic iteration mark in JA search Add U+3005 (々) to the Japanese search tokenizer character class. It sits just below the kana blocks, so words like 様々 / 日々 / 個々 previously dropped the mark and split awkwardly, hurting recall. Addresses a review nit on MUL-2893. Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): restore ja locale parity after merging main Merging main brought new EN strings into agents/chat/onboarding/settings/ squads that the ja bundle (authored against an older snapshot) lacked, breaking the locales parity test. Add the Japanese translations for the new keys (workspace logo upload, agents runtime filter, chat session-history stop dialog, onboarding social_github, squad archived status) and drop the two renamed chat window keys (active_group / archived_group) that EN removed in favour of history_group. Fixes the failing @multica/views parity.test.ts on the FE CI for MUL-2893. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5aa4fb7487 |
MUL-2760: feat(i18n): add Korean locale support (#3369)
* feat: add korean locale support * feat(i18n): localize Korean landing page * fix(i18n): refine Korean landing copy * fix(i18n): refine Korean translations * fix(i18n): translate Korean landing subpages * fix(i18n): route Korean landing docs links * fix(i18n): add Korean use case content * fix(i18n): polish Korean locale copy * fix(i18n): improve Korean landing copy * fix(onboarding): persist Korean helper artifacts Co-authored-by: multica-agent <github@multica.ai> * fix(web): add use case locale fallback Co-authored-by: multica-agent <github@multica.ai> * Align Korean pull requests wording Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): dedupe docs href helper Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): localize changelog dates Co-authored-by: multica-agent <github@multica.ai> * fix(docs): prerender Korean fallback pages Co-authored-by: multica-agent <github@multica.ai> * fix(docs): align fallback hreflang metadata Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): preserve Chinese CJK font fallback order Co-authored-by: multica-agent <github@multica.ai> * chore(onboarding): update localized comment wording Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden CJK font fallback assertions Co-authored-by: multica-agent <github@multica.ai> * fix(docs): keep Chinese font fallbacks first Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden locale fallback coverage Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
27b473151b |
refactor(ui): move CODE_LIGATURE_CLASS to zero-dep code-style module (#3463)
Extracts CODE_LIGATURE_CLASS and CODE_LIGATURE_DESCENDANT_CLASS into packages/ui/lib/code-style.ts. Non-markdown CLI command surfaces (onboarding/cli-install-instructions, runtimes/connect-remote-dialog) can now import the class strings without pulling in the shiki + react-markdown + katex dependency graph via the markdown barrel. CodeBlock and Markdown continue to consume the constants from the new module; the markdown barrel no longer re-exports CODE_LIGATURE_CLASS. MUL-2793 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2662291cf2 | fix(runtimes): disable ligatures in CLI command snippets (#3357) | ||
|
|
bd1fb10afa |
chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute - Replace useContext() with React 19 use() across 16 context consumers - Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files (mobile excluded — Hermes lacks toSorted support) - Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check - Fix nested component definition: useMemo wrapping HeaderRight → useCallback - Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad React Doctor score: 23 → 30. No behavioral changes, no business logic modified. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |