mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-05 09:30:05 +02:00
fix/table-interaction
1535 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5fa94e59ab |
fix(ui): let group rows report their height, and re-measure the frozen edge on resize
Self-review of the branch turned up two places where a change did not reach as far as it was supposed to. Group rows never took part in the row measurement. DataTable clones the virtualizer's ref and data-index onto whatever renderRow returns, but IssueTableGroupRow declared only its own three props and absorbed them — and a group header, being shorter than a data row, is exactly the row the measurement was added for. The frozen-column shadow measured its boundary from the scroll handler alone. Resizing a frozen column while the surface is scrolled sideways moves that boundary with no scroll event to follow, leaving the shadow behind at the old position. Also drops a dependency left behind when the load-more rows stopped building their own labels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e6be57ffb6 |
fix(issues): scope the title cell's actions to the title cell
The sub-issue and rename buttons keyed off the row's hover state, so pointing anywhere along a row — a status chip, a date, empty space — put controls that act on the title under a pointer that was somewhere else. They follow the title cell's own hover now. The gradient behind them still tracks the row colour, since that is what the cell is painted with while they are showing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b207a93117 |
fix(ui): measure table rows instead of assuming they are all one height
Virtualisation sized every row at the same 41px estimate, but the table does not have one kind of row: group headers are 37px, an end-of-column footer shorter still, and placeholders different again. Each one put the estimate a few pixels out, and the error accumulates — with grouping on, the rendered window drifts off the rows it should be showing and the scrollbar overshoots the bottom. Rows report their own height through the virtualizer's measureElement now, and the estimate only covers rows that have never been mounted. Rows built by renderRow are cloned to carry data-index and the measuring ref, so callers keep returning a plain <tr> and still take part. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
efb9bdadf4 |
feat(issues): show the table's own grid while its first page loads, and end its columns like every other surface
Two loading-state gaps, both in how the table's non-issue rows are modelled. A cold load rendered a single "Loading…" line under a full header, which reads as an empty table rather than a loading one. The surface-level skeleton meant for it was unreachable: use-issue-surface-data hardcodes isLoading to false for table, so the `mode === "table"` branch never ran — and it would not have fitted, drawing rounded bars at p-2 where the table draws an edge-to-edge grid, so switching it on would have traded one wrong state for a layout jump. Placeholders are rows now, rendered through the ordinary cell renderer so they inherit the real column widths, pinning and borders. Everything the table knows before its data — header, toolbar, column layout — is up immediately, and the rows swap in without moving anything. The unreachable surface branch is gone. The end of a column was hand-rolled too, leaving the table the one surface where a failed page read as muted body text rather than an error, where the prompt sat left-aligned against three centred ones, and where reaching the end of a paginated branch said nothing at all. The row carries its state rather than a finished label and renders through ListLoadMoreFooter, the footer Board, List and Swimlane already share — which exists, per its own comment, so those states and their wording stay consistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
37c77e8a0b |
fix(issues): give the table's title column back the width its hover actions held
The sub-issue and rename buttons sat inline in the title cell at opacity 0. Hidden is not absent: their boxes reserved around 40px of the column at all times, in the one column with the least room to spare, for controls that only appear on hover. They are positioned over the cell's trailing edge now, the way SidebarMenuAction is, with a gradient fading the title running underneath rather than icons sitting on top of it — the sidebar needs no gradient because its labels are short, a title runs to the cell's edge. focus-within keeps them reachable from the keyboard, where hover never fires. Reordering a column also scrolled the table vertically. Modifiers constrain a drag's movement but not its auto-scrolling, which reads raw pointer coordinates, so a few pixels of vertical drift sent the rows moving under a gesture that cannot act on them. Auto-scroll's y threshold is zero now; x keeps it, since a table wider than its viewport needs it to reach a distant slot, but at 0.05 rather than the default 0.2, which arms it a fifth of the way in from either edge — most of a wide header. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
90e8d7871c |
fix(ui): stop the table header flickering black during scroll
The sticky header carried backdrop-blur over a translucent fill. A backdrop-filter on a sticky element with content scrolling beneath it is a known Chromium compositing fault — the blur layer recomputes its backdrop every frame, and virtualisation is adding and removing the very rows it reads from, so the strip flickers black under fast scrolling. Chromium's fix for the same symptom was reverted, so upgrading does not carry it (electron#12906, electron#45854, chromium#339841685). The fill is now the opaque colour that bg-muted/30 was compositing to, which is the mix the pinned header cells already use. A header the content scrolls behind has no reason to show it through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
37e4ed5330 |
feat(ui): size a table column to its content on double-click
Double-clicking a column's resize handle called column.resetSize, which cleared the stored width and let TanStack fall back to its generic default of 150 — a number unrelated to any width this table was designed with. "Reset" therefore widened priority from 130, collapsed labels from 220, and clamped title to its 260 minimum. It restored nothing. It now sizes the column to its widest rendered cell, the convention Excel, Sheets, AG Grid and Notion all share for that gesture. Fixed table-layout ignores content and the cells truncate their own text, so nothing on screen reports the width the content wants. The measurement lifts both constraints across the column's cells, reads them, and restores everything within the same task, so the browser paints once — after the restore — and the intermediate layout is never seen. Only the rows inside the virtual window are measured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bd7098a078 |
fix(ui): mark the frozen column boundary while the table is scrolled sideways
The edge where the frozen columns end had a shadow drawn inside the pinned cell with `--border`. That token is tuned for hairlines between adjacent surfaces — oklch .945 in light mode — and all but disappears once spread into a shadow, so the boundary read as unmarked while content slid underneath it. Darkening it in place only made the frozen column look outlined: an inset shadow can shade that cell's own edge and nothing else, while the depth being described belongs to the content passing beneath. Split into the two things MUI X's data grid separates, a permanent border for where the boundary is and a shadow for something crossing it right now: - the rule between the frozen columns and the rest stays put, as before; - a 12px gradient is cast past the frozen block, mounted outside the scroll container and shown only while scrolled sideways. Its position is measured off the DOM — the trailing frozen header carries a `data-pinned-edge` marker — rather than summed from column sizes. Fixed table-layout stretches columns past their configured widths whenever the container is wider than the table's min-width, so a sum lands the marker in the middle of visible content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
078f9b824d |
fix(issues): stop a dragged table column stretching into its neighbour's width
Dragging a column header applied dnd-kit's full transform, which carries scaleX/scaleY alongside the translation. Those come from its layout animation: old rect over new rect, tweening an item into the shape of the slot it lands in. Between two tabs of equal width the ratio is 1 and never shows. Between two columns it is not — swapping a 174px column with a 96px one stretched the header to 1.8x on the way across. Only the horizontal translation is applied now; reordering changes no column's width, so there is no shape for a tween to describe. The travel and the settle stay animated through `transition`. The travelling wrapper was also fixed at h-8 while the header strip measures 39px once row borders are counted, leaving the block short at both edges and misaligned by 3px — which read as the same flattening. Its height is derived from the cell now. The reorder grip returns as the drag handle, appearing on hover as before and staying visible for the length of a drag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b3460114e4 |
feat(issues): drag a table column by its header, not by a hidden grip
Reordering columns was advertised by a grip that only appeared once the pointer was already on the header, and dragging it moved a wrapper the height of its own line of text while the <th> around it clipped anything that travelled outside the cell — so the column being moved looked like it had vanished rather than slid. The header is now the handle. There is no grip: the cursor carries the affordance (grab, then grabbing), the whole cell responds, and only the sort/hide button opts out, since a press there is always the menu. The wrapper spans the cell's box at all times — the negative margins undo the <th>'s padding and put it back inside — so what travels is a header-sized block and going translucent is the entire drag state, matching how a desktop tab behaves. Overflow is lifted for the length of a reorder and the column in hand is raised over its neighbours. Columns are restricted to the horizontal axis, the same constraint the desktop tab bar puts on tab reordering, which is why packages/views now declares @dnd-kit/modifiers directly. Transforming the <th> itself would carry the header's height along for free, but `transform` on a table cell is a corner of the spec browsers take liberties with — Chromium lifts the cell out of the table's box model and its geometry stops matching the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
fa7cb897e8 |
fix(ui): make table column resizing land where the pointer is (MUL-5166)
Pressing the resize handle committed a width before any drag: it wrote the rendered width, which fixed table-layout had stretched past the configured one, so a stray click on a column edge silently rewrote and persisted that column and stopped it adapting to the window. Dragging then ran ahead of the pointer, because committing one width changes how much leftover space the layout has to share out and rescales every other column mid-gesture. And the gesture never ended if the pointer came up outside the window or the user switched apps — the column kept tracking the pointer on return, with the page stuck unselectable under a col-resize cursor. - Require 4px of travel before anything is committed, matching the column-reorder sensor's activation distance on the same header. - Pin every column to its rendered width on the frame the drag starts, so there is no leftover left to redistribute and the drag maps 1:1. - Capture the pointer and end on blur / pointercancel / lostpointercapture, the same four-part contract the sidebar rail already uses. - Own the cursor from a portaled full-viewport layer for the duration of the drag. document.body.style.cursor loses to any descendant that declares its own, which is why the cursor flickered over rows and text. Resizing also re-rendered every cell each frame, and each cell carries a popover, so a frame cost ~143ms. Widths now travel as custom properties published once on the <table>, and the body is swapped for a memoized copy while a drag is live — the browser applies each new width with no React work. Visually the table had no column rules at all, which left the pinned columns' trailing shadow as the only vertical line and made it read as a stray border on one column. Every column now carries a rule, the pinned shadow appears only once the surface is actually scrolled sideways, and the resize handle brightens its rule to brand rather than matching the faint resting colour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
995ec8fcf2 |
feat(editor): 粘贴超长文本自动转为文本附件 (#6019)
* feat(editor): convert over-threshold pastes into a text attachment Pasting more than PASTE_AS_FILE_THRESHOLD (4000) characters into a turn-based composer now uploads a `pasted-text.txt` attachment instead of writing thousands of characters into the body. Opt-in per editor: chat, new comment, reply and comment edit pass the threshold; issue and project descriptions deliberately do not, because there a long paste IS the content. The synthesised File goes through the existing upload pipeline unchanged (nothing in it inspects a File's origin), so draft persistence, status chips and .txt preview all come for free. Recovery is the part that needed real design. A dropped file survives a failed upload on disk; this text exists nowhere else — it was never written into the document and its source tab may be closed. Since uploads outlive their mount (MUL-5181), the editor cannot own that recovery, so useCoordinatedUploads does: deliverPastedTextBack mirrors deliverFinishedUpload (live editor, else the persisted body) and is the single responder, so the live and dead cases can neither both fire nor both be skipped. The text is restored as markdown because markdown-paste is what would have handled it had it never been converted. A paste inside a code block stays inline — opening a fence is the request to show the thing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(editor): pin the paste handler order against the real extension array Both markdownPaste and fileUpload register catch-all handlePaste handlers, so which one ProseMirror consults first decides whether paste-as-file works at all. That ordering is not visible in either file: Tiptap reverses the extension array before collecting plugins (@tiptap/core `get plugins()`), and neither extension sets a priority, so the array position in createEditorExtensions is the whole contract. The existing tests mounted the fileUpload extension alone and could not see it. This one builds the production array via createEditorExtensions; swapping the two entries makes it fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2274f521dc |
feat(issues): agents-working chip on the sub-issues header (#5825) (#5834)
* feat(issues): aggregate agents-working chip on the sub-issues header (#5825) Add a live "N agents working" chip next to the sub-issues progress ring in issue detail. The per-row IssueAgentActivityIndicator shows which sub-issue is being worked; this chip shows how many agents are on the parent's children at a glance — and keeps that signal visible while the list is collapsed. Derives from the shared workspace agent-task snapshot narrowed by a new selectIssuesTasks select (structural sharing keeps unrelated snapshot churn from re-rendering the header). Counts unique agents to match the workspace chip, whose chip_agents_working / hover_header_queued strings it reuses — already translated in every locale. Hover opens the shared AgentActivityHoverContent task list. Fixes #5825 Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): read the sub-issues chip from the working-agents projection (#5825) The chip landed deriving its own count from the workspace agent-task snapshot, which put a second definition of "an agent is working" in the client. It showed up immediately: the number came from the running tasks only while the hover body listed running plus queued, so a parent with 2 running and 3 queued agents read "2 agents working" over a five-row card. A header count is a claim about a scope, so let the server own both the scope and the arithmetic, exactly as the Issues list header already does. ListWorkspaceWorkingAgents grows an optional parent_issue_id narrowing and the chip reads /api/working-agents?type=issue&parent=<id>. The number, the avatars and the hover body are now one list rather than three derivations, so they cannot disagree. Row indicators keep reading the snapshot. One shared query sliced per row is the right shape for a per-row cue and a stale row decoration costs nothing; a header number is the opposite, it has to be authoritative. The new parameter is additive: omitted, the query and the response are byte-for-byte what they were, so an installed client that never sends it keeps the workspace-wide behaviour. A regression test pins that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
50c89c7094 |
feat(autopilots): hide webhook URL token by default (MUL-5374) (#6015)
* feat(autopilots): hide webhook URL token by default (MUL-5374) The webhook URL is a bearer credential — anyone who reads it off a screen share or screenshot can fire the autopilot. GH #6004 reports exactly that leak during a live demo. The trigger row and the post-create panel now render the URL through a shared WebhookUrlField that masks the token segment by default. Clicking the value (or the eye toggle) reveals it; Copy keeps working while hidden, so the common case never needs a reveal. The plaintext token is not in the DOM until the user asks for it — this is a real display boundary, not a CSS blur. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilots): scope webhook URL reveal to the URL it was granted for (MUL-5374) The reveal was a bare boolean, so a token rotation under a mounted trigger row swapped in the new URL while `revealed` was still true — exposing the new credential in the clear at the exact moment the user rotated to contain a leak. Track which URL the reveal was granted for and derive `revealed` during render. Deriving it (rather than resetting in an effect) means the new token never reaches the DOM, not even for the pre-effect frame. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c271f80999 |
MUL-5370 fix: label stalled skill-bundle downloads, align failure-reason copy with the backend taxonomy (#6001)
* fix(daemon): label stalled skill-bundle downloads and make them retryable A skill bundle that could not be downloaded during task preparation surfaced as the bare string "resolve skill bundles: context deadline exceeded". taskfailure.Classify has no rule for a Go context deadline, so it landed in agent_error.unknown — a bucket that is NOT on the server's retry allowlist. A transient stall therefore became a terminal chat failure carrying a label nobody could act on, and the failure was invisible on the Usage page's Errors breakdown. (MUL-5370) - Add the platform-side reason skill_bundle_unavailable and put it on retryableReasons. Retrying is cheap and safe: the agent process never started, and bundles that did arrive are already cached on disk, so successive attempts converge. - Carry a sentinel error from the resolve loop so the reason is derived structurally rather than by matching the wrapped transport error's text, and name the skill, its declared size and the elapsed wait in the wrap — enough to tell "this bundle is too big for the link" from "the link is dead" without reading daemon logs. - Normalise the wire shape an OLD daemon produces (a non-empty catchall plus the previous "resolve skill bundles:" wrapper) on the server side. Installed daemons upgrade on their own cadence, and FailTask only classifies when the caller supplied nothing, so without this the fix would reach only hosts that happened to update — while the un-upgraded hosts most likely to be hitting the bug kept failing terminally. - Teach Classify about "deadline exceeded" and net/http's "Client.Timeout exceeded while awaiting" so any other Go-side deadline that reaches it as text stops falling into the unknown bucket too. - Backfill historical rows in both agent_task_queue and chat_message. Scoped to agent_error.unknown alone — the old wrapper string postdates the in-flight classifier by three weeks, so no row carrying it can hold the legacy coarse value — which keeps the down migration an exact inverse. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): give chat its own failure copy for the refined reasons #5991 rebuilt the operator-facing failure labels around an open wire string with a raw-value fallback, but the chat bubble kept its own exact-key lookup against the six coarse values from migration 055. So all 14 agent_error.* values still missed and rendered the generic "Something went wrong and the agent couldn't finish replying" — the classification the backend had already computed was discarded at the last step, and that is the message the MUL-5370 reporter saw. - Add resolveFailureReasonKey in packages/core: exact match, else degrade an `agent_error.*` value to its family, else undefined. A reason newer than the shipped client now lands on the family line instead of the fallback. - Rekey the chat copy map by wire value and route it through the helper. Chat deliberately degrades to friendly copy rather than adopting the operator surfaces' raw-value fallback: it is read by the person who just sent a message, and the raw error is one click away under the collapsible. - Add refined chat copy (en / zh-Hans / ja / ko) only where it can say something the family line can't — a different next step: network, auth, quota, rate limit, context overflow, missing/outdated CLI, skill download. - Give skill_bundle_unavailable a label on the web and mobile surfaces and a class on the Usage page's Errors breakdown (runtime — the operator response is "check the daemon's link to Multica", the provider is not involved). - Mobile's two label maps were still coarse-only for the same reason; rekey them by wire value and fill in the refined taxonomy. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b5f19adbab |
feat(composer): post-send caret policy per surface (afterAccepted) (#6014)
* feat(composer): post-send caret policy per surface (afterAccepted)
Where the caret goes after a send was hand-rolled per composer: chat blurred,
quick-create hand-wrote its own requestAnimationFrame focus, and comment/reply
did nothing at all. The mechanics are identical everywhere and easy to get
wrong (must run after the clear, must survive a dialog focus trap, must not
steal focus the user moved elsewhere mid-flight), so they now live once in the
shared send contract.
`useComposerSubmit` gains `afterAccepted` ("refocus" | "blur" | "none",
default "none") plus an optional `containerRef` that bounds focus reclaim to
the composer that sent. The mode may be a function so a surface can decide at
accept time — chat only reclaims focus when the commit actually scrubbed the
shared editor, never when a fire-and-forget send left another session's draft
on screen.
Per-surface policy:
- Chat refocuses (one file, three mount points: chat page, floating window,
agent creation studio). Replaces the deliberate blur.
- Thread replies refocus — the user is mid-conversation.
- A top-level comment blurs, and IssueDetail reveals what was posted instead:
submitComment now returns the created id, and the page scrolls to that row
and flashes it with the same jumpToThread the inbox deep-link uses.
- Quick create's keep-open mode drops its hand-written rAF for the option.
- Inline comment edit and Create Issue keep "none": both close on save.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(composer): drop the scroll-to-posted-comment reveal
The top-level composer is sticky at the bottom of the timeline, so a comment
posted from it lands directly above the box and is almost always already on
screen — the scroll was a no-op and the flash re-announced something the user
had just deliberately done. The flash earns its keep for inbox deep-links,
where the user did not choose the landing spot.
`submitComment` goes back to returning a boolean; it only carried the created
id to feed the reveal. The composer still blurs after posting: the turn is
over, so the caret is dropped rather than kept.
The shared contract is untouched — `afterAccepted` never knew about comments,
which is why removing this costs nothing outside IssueDetail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(composer): drop stale references to the removed comment reveal
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(composer): bind the post-send caret policy to an actually-cleared editor
Review blocker: CommentInput passed a literal `afterAccepted: "blur"`, but its
stale-submit guard declines to clear when the user typed during the request.
Posting comment A on a slow connection while typing comment B therefore
dropped the caret out of B mid-sentence — the guard kept the text, and the
blur fired anyway.
Both issue composers now resolve the mode from a ref set only on the branch
that really wipes the editor, matching what ChatInput and quick-create already
do. ReplyInput gets the same treatment: refocusing a box the user is typing in
happens to be harmless, but leaving one surface on the unsafe shape invites the
next one to copy it.
The rule is now stated on the option itself: a surface whose `onAccepted` can
decline to clear must pass a function and resolve to "none" on those paths.
Both regressions are mutation-verified — reverting either binding fails the new
assertions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
77b309a5ac |
feat(drafts): unified draft lifecycle + upload ownership inversion (MUL-5181) (#5900)
* feat(drafts): unified draft lifecycle + upload ownership inversion (MUL-5181) Unify how every composer preserves unsent work, sends, and handles uploads. L1 foundation (packages/core/drafts): - createDraftStore factory + self-registering cleanup-registry replacing the hand-maintained WORKSPACE_SCOPED_KEYS list; register-all-drafts guarantees registration completeness. Fixes the confirmed cross-user draft leak (persistence + in-memory) on logout / workspace delete. L3 send paradigm: - useComposerSubmit: one await-then-render contract (lock/spin, keep-on-fail, clear-on-success, single-flight, submit-time upload-gate), adopted by comment/reply/edit, create-issue, quick-create, and chat. Per-surface: - Comment/Reply/Edit: attachments moved into the persisted draft. - Create Issue: draft split into shared/manual/agent/activeMode with non-destructive mode switching + migration for old flat drafts. - Chat: optimistic send converted to await-then-render (kept server-driven cancel restore_to_input); chat draft keys registered for cleanup. L2 upload coordinator (ownership inversion, Linear-validated shape): - upload-coordinator + DraftUpload placeholder: uploads owned by a module coordinator that outlives the component, state persisted in the draft; AbortController + abort-on-logout; interrupted-on-reload. Comment surface fully wired. Create-issue/chat upload wiring is a documented residual. Verified: core + views typecheck clean; core 1064 + views 2928 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(drafts): close three review gaps in the unified draft lifecycle (MUL-5181) 1. Logout resurrection: reset in-memory draft stores BEFORE removing their persisted keys — each reset is a setState and persist writes it straight back under the still-active slug, so the old order re-created the deleted keys. The issue draft store's reset is now a full reset including lastAssignee, which clearDraft deliberately re-seeds and would otherwise hand the previous user's last-picked assignee to the next login. 2. Submit gate blind spot: the composer gate now also reads the draft's coordinator-owned upload placeholders (hasUploadingDraft). A composer reopened over a still-in-flight upload could previously send past the editor-only gate, clearing the draft out from under the settling upload. 3. Attachment binding returns to reference-filtering: a submit binds only uploads the body references, so deleting an inline image really unbinds it. An upload that settles after its mount died gets its markdown link written back into the body instead — via the reopened composer's live editor (new ContentEditorRef.insertMarkdownAtEnd) or appended to the persisted draft (new appendToDraftContent) — so close-surviving files stay visible, deletable, and honestly bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): harden upload write-back delivery after independent review Review of the previous commit (fresh-context reviewer + probe against real @tiptap/react) found the write-back could still lose a file: - insertMarkdownAtEnd now returns a boolean: the imperative handle exists from first commit but the Tiptap instance arrives in a passive effect, so an insert in that window (or after destroy) no-ops. Callers previously assumed it landed. - Write-back is now confirmed delivery (deliverFinishedUpload): insert into the live editor and, on success, persist the same body as insurance against the debounced emit being dropped by a quick unmount; append to the store only when NO composer is mounted (a mounted editor's first emit would erase a store-only append); retry while a mounted composer's instance is still warming up. Every attempt re-checks the generation guard and the body reference. - mountedRef flips in a layout effect: React nulls the child editor ref in the unmount commit, and a settle in the gap before passive cleanup saw "mounted" with no editor left to swap. - uploadAndInsertFile guards editor.isDestroyed after the await: now that uploads outlive mounts, the swap/remove paths could dispatch against a destroyed EditorView and escape as an unhandled rejection. - Tests: the reopened-composer test now asserts the editor actually received the insert (it previously passed with liveEditors disabled), plus a warming-up retry case; the mock editor mirrors isDestroyed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(drafts): roll coordinated uploads out to issue-create and chat (MUL-5181 L2) Completes the upload-ownership layer for every composer surface. The generic engine is extracted from the comment implementation into editor/use-coordinated-uploads (UploadDraftBinding adapter: store-backed accessors + registry key + body append), and use-comment-uploads becomes a thin binding over it — behavior unchanged, all comment tests green. Issue-create (manual + agent panels): - shared.attachments migrates Attachment[] -> DraftUpload[]; load normalizes legacy bare rows to `uploaded` and coerces stale `uploading` to `interrupted`. - Uploads are coordinator-owned: placeholder at pick time, survives dialog close, aborts on logout, chips for uploading/failed/interrupted, combined gate on Create and both mode-switch actions. - Write-back targets the body of the MODE that started the upload (manual description vs agent prompt); mount-time prune keeps placeholders and drops only unreferenced `uploaded` entries. Chat (tab + floating window): - inputDraftAttachments migrates to DraftUpload[] with load-time normalization; new store ops (add/settle/fail/remove upload, append-to- draft) mirror the comment store. - ChatInput adopts the engine; the upload target is snapshotted at pick time via resolveUploadTarget so a file dropped while the editor is pinned to a previous session's document files under THAT draft. - uploadMapRef is gone — the draft's uploads are the single binding source, reference-filtered at send. Hosts no longer own transport: onUploadFile prop becomes uploadEnabled, and the controller/window drop uploadWithToast. - commitDraft prunes only `uploaded` entries the body no longer references; placeholders survive keystrokes (chips are their only UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): harden L2 rollout after independent review - attachmentToDraftUpload now strips the response-scoped signed download_url before the row is persisted (draft uploads survive restarts; a stale signature 403s the preview on reopen). Covers comments, issue-create, and chat in one place; issue-create's settle reuses the helper, and the Signature assertion the rollout had dropped is restored. - chat's live-editor registry follows the LOADED draft key (reactive mirror of editorDraftKeyRef): a settle for draft B must not insert into an editor still pinned to draft A's document. - removeUpload aborts an in-flight request before dropping its placeholder. - issue-create hasDraft counts only uploaded/uploading entries so a failed remnant can't pin the sidebar draft dot forever. - Tests: mutation-proof coverage for the two placeholder-preservation rules (create-issue mount prune, chat commitDraft prune) — both previously survived rule inversion; direct core tests for the five new chat store upload ops incl. persistence and signed-URL stripping; quick-create test gets the editor i18n namespace; dead uploadWithToast scaffolding removed from both modal tests; chat-input mock aligned with the real append semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): close third-round review gaps in the upload engine - The live-editor registry registers in a layout effect: chat's adopt swaps the editor's document and loaded key synchronously during commit, and a passive re-registration one task later left a settle window where the old key mapped to an editor already holding another draft's document. The registry key is also built only when a binding exists. - removeUpload aborts only a request THIS surface tracks as `uploading` (guarded before the abort), with the comment now honest about the path being defensive — no current chip exposes ✕ mid-upload. - Mutation-proof test for the loaded-key registry rule: a dead mount's settle for a pinned draft must insert into the editor HOLDING it, not the selected one (verified to fail with the registry keyed by selection). - hasDraft upload semantics pinned by tests (uploaded/uploading count; failed/interrupted remnants don't pin the sidebar dot). - Dead scaffolding dropped: identity use-file-upload mocks and a redundant assertion in the modal tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): stale-submit draft guard + registry layout timing (review BLOCKED items) Blocker 1 — a submit that outlives its composer may only consume the draft it submitted (MUL-5181 P0). Every accepted-submit clear is now guarded: - create-issue / quick-create snapshot the singleton draft's object identity at submit; a dead panel clears (and records last-assignee/mode) only if the draft is untouched, and never runs close/reset effects. A replaced draft B typed after close survives a late success of draft A. - comment / reply / edit snapshot the per-key draft entry; a dead composer clears only the exact entry it submitted. - chat snapshots the sent slot's value; a dead mount's commitInput clears only an unreplaced draft. Mutation-verified tests for the create panels and comments (guard inverted => tests fail), plus untouched-draft control cases. Blocker 2 — the live-editor registry is now genuinely registered in a layout effect. The prior commit claimed this fix but a test-time `git checkout --` discarded the unstaged engine edits before committing; re-applied: layout registration, binding-gated registry key, and the tracked-only abort in removeUpload. New registry timing test captures the registry from a parent layout effect across a key switch — verified to fail with passive registration. Also: `multica:chat:selectedProjectId` joins the workspace-scoped cleanup list (was leaking across logout; flagged as a pre-existing risk). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): mounted submits also clear only the draft they submitted The stale-submit snapshot guard previously protected only dead composers; a mounted one cleared unconditionally on success. But the editor stays interactive during a request (Tiptap cannot toggle editable post-mount), so text typed while draft A was in flight was wiped by A's success. The guard is now unconditional across every surface: success consumes exactly the submitted snapshot, and any later edit survives. - create-issue / quick-create: the editor's pending debounce is flushed into the store BEFORE snapshotting (a late flush of pre-submit typing must not read as a mid-flight edit); a touched draft skips clear AND close/reset — the dialog stays open on the newer work. Untouched behavior unchanged. - comment / reply / edit: same flush + snapshot; a touched entry keeps both the store draft and the editor content (edit mode stays open on it). - chat: commitInput's value compare now applies while mounted too, and the editor is scrubbed only for an untouched draft. - use-composer-submit docs no longer claim "editor locked": they state the real contract — send affordance locks, edits after submit survive. Regression tests: mounted mid-flight-edit cases for manual create (incl. "dialog must not close over draft B"), quick create, comment, and chat, plus mounted-untouched controls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(drafts): idempotent draft writes so a tab switch cannot resurrect a posted comment Final-review blocker: the comment/reply visibilitychange/pagehide flush re-writes IDENTICAL content on every tab switch, and writeDraft minted a new entry object each call — the stale-submit guard's identity compare then read a mid-flight tab switch as "edited during the request", kept the posted comment's draft alive, and left Send enabled for a duplicate. - writeDraft is now a no-op when content and uploads are unchanged (also kills a spurious persist write per tab switch). Regression tests: entry identity preserved on identical setDraft (core), and the reproduced tab-switch-mid-send scenario clears the posted draft (views) — verified to fail with the idempotence removed. - onAccepted now flushes the editor's pending debounce before judging `untouched` on every surface, so typing still inside the debounce window counts as a mid-flight edit instead of being scrubbed. - create-issue records last-assignee/mode from the SUBMITTED values, outside the untouched gate — a created issue updates the preference even when the dialog stays open on newer edits. - Stale guard comments corrected in both create panels; the use-composer-submit docstring no longer claims project/feedback were migrated (they still hand-roll await-then-clear; registered debt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f1cb726d7c |
fix(usage): move the Errors card to the page bottom and rebalance its layout (#6003)
Two problems with the card as shipped, both visible on real data (283 failures across 28 agents): 1. It sat between the trend chart and the leaderboard. Spend is the headline of this page; failures are the follow-up question you ask after seeing who is spending. Moved below the leaderboard. 2. The two-column split put a 7-row list next to an unbounded one. In practice that was 7 rows of classes beside 28 rows of agents — roughly 400px of content next to 1500px, so the left half was mostly whitespace and the card was taller than the rest of the page combined. Rebalanced by stacking two full-width sections instead: - Class breakdown is now a single 100%-stacked bar plus a legend, replacing seven individual progress bars. The question is "what is the mix", and one bar answers it directly instead of making the reader compare seven lengths and total them mentally. ~340px becomes ~70px. - Offender rows fold onto one line, borrowing the leaderboard's grid shape (identity, bar, numbers) instead of stacking a full-width bar under each name. Halves the per-row height and lines the numbers into a scannable column. - The list caps at 8 with a "Show all N" toggle. It is ranked by absolute failure count, so the tail is agents that failed once or twice — real, but not what anyone opens this card to see. The toggle label carries the full count, so the cap is never silent. - The raw error-code list gets two columns on wide viewports now that the section has the full page width. Card height on the screenshot's data drops from ~1500px to ~420px. Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4d0475ce89 |
feat(usage): error/failure charts on the Usage page (MUL-5352) (#5991)
* feat(usage): add error/failure visibility to the Usage dashboard The Usage page could only answer "how much did we spend"; nothing on it showed how often agents fail, what kind of failure it was, or which agent is responsible. Operators had to open failed tasks one at a time to spot a pattern. `agent_task_queue.failure_reason` already carries the refined 21-value taxonomy from server/pkg/taskfailure, so this is a read path over data that already exists. Backend — two rollups, both scoped by workspace/project/window like the existing dashboard endpoints: GET /api/dashboard/failures/daily per-(date, failure_reason) GET /api/dashboard/failures/by-agent per-(agent, failure_reason) They return every terminal task, not just failures: the `failure_reason: ""` row carries the succeeded count. That is what makes the error rate's denominator share filters with its numerator. The run-time rollups can't serve as that denominator — they require `started_at IS NOT NULL`, so a task that expired in the queue (the signature of a runtime outage) contributes nothing to their failed_count. A failed row with an empty reason column lands in an `unclassified` bucket rather than being mistaken for a success. Frontend: - "Errors" joins the trend toggle, daily and weekly, stacked by failure class with the bucket's error rate in the tooltip. - An Errors card breaks the window down by class and by agent, with the raw failure_reason strings behind a disclosure (unlocalised — an operator pastes them into a log search). Each agent row links to its Work tab, which lists the actual failed runs. - The 21 backend reasons fold into 7 display classes in @multica/core/dashboard. Unknown reasons — including ones from a backend newer than the client — land in "other" instead of being dropped, so the class totals always reconcile with the failure count. The Tasks KPI tile is deliberately left alone: its value counts started tasks only, so quoting the failure rollup's larger count there would put two denominators in one tile. The Errors card states its rate with the denominator spelled out instead. Migration 225 adds a partial index on agent_task_queue(completed_at) for terminal statuses. The table had no completed_at index at all, so the two pre-existing run-time rollups were already scanning it; these two new queries would have doubled that. Closes #4429 (MUL-5352) Co-authored-by: multica-agent <github@multica.ai> * fix(usage): correct the Errors drill-down, window and agent exposure Review findings on PR #5991. 1. The drill-down pointed at the wrong page. `?view=work` renders ActorIssuesPanel — the issues assigned to the agent — while its runs live in the Overview pane's ActivityTab. Link to Overview. That page also could not show why a run failed: `failureReasonLabel` was a `Record<TaskFailureReason, string>` indexed with a cast to the old 6-value coarse enum, so every refined reason the backend has written since MUL-1949 resolved to `undefined`. It is now a function over the full 21-value taxonomy plus the legacy coarse values, falling back to the raw wire string for anything newer than the client. Fixes the issue execution log too, which had the same cast. 2. The Errors card covered one more calendar day than the chart above it. `parseSinceParamInTZ` returns N+1 days of headroom on purpose and the dashboard trims the surplus client-side — but only a series carrying a date can be trimmed that way. Totals / classes / reasons now derive from the date-bucketed rollup after that trim, and the per-agent rollup (which has no date to trim on) closes its window server-side via a new `parseExactSinceParamInTZ`. At days=1 the card previously reported yesterday's failures beside a chart showing none. 3. The top-offenders list leaked agents the viewer cannot see. The failure rollups are workspace-scoped and deliberately skip per-agent visibility, but the agent list they are joined against does not — members only see a private agent when they own it or are owner/admin. `name ?? row.agentId` therefore rendered a bare UUID along with that agent's failure count, rate and dominant error class. Unresolvable agents now fold into one anonymous row, and the renderer never falls back to an id. Stricter than `bucketUnknownAgentRows` while the agent list loads: a transient flash of UUIDs is the leak, not a cosmetic glitch. Also from the review: the Errors tooltip echoed the raw Recharts dataKey ("rate_limit") instead of the translated label the legend already carries. Not changed — the schema's `failure_reason` default stays `""`. Defaulting a missing field to a failure bucket guards against a deflated rate, but the realistic drift is `omitempty` on the Go struct tag, which would strip the field from exactly the SUCCESS rows and read as a 100% error rate. Added TestDashboardFailureWireContractKeepsEmptyReason to pin that the server always emits the field, which is the assumption the default rests on. Co-authored-by: multica-agent <github@multica.ai> * fix(usage): renumber migration and fix the anonymous bucket's failure class Review findings on PR #5991, round 2. 1. Migration prefix 225 collided with `225_chat_message_channel_media_pending`, which landed on main while this branch was open — backend CI failed on TestMigrationNumericPrefixesStayUniqueAfterLegacySet. Merged main and renumbered to 231; main now carries 225 through 230, so 226 is taken too. 2. The anonymous "Other agents" bucket could announce the wrong failure class. It merged rows that had ALREADY collapsed to one dominant class per agent, then credited each agent's entire failure count to that class. An agent failing auth 6 / timeout 5 contributed 11 to auth and 0 to timeout, so a bucket whose real composition was timeout 15 / auth 6 rendered as Auth. Fixed by anonymizing the raw per-(agent, reason) rows instead: the sentinel becomes just another agent_id and `aggregateAgentFailures` computes its classes from real counts. That also deletes the parallel bucketing pass — one identity rewrite replaces it. `knownAgentIds` moves up to where both consumers can see it. Also from the review: - The wire-contract test decoded both payloads into one map. json.Unmarshal merges into a non-nil map rather than resetting it, so a residual failure_reason from the first case could have masked an omitempty regression in the second — exactly what the test is meant to catch. Now table-driven with a fresh map per case. - A test comment still described the drill-down as pointing at the Work tab. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ceba14a227 |
fix(issues): MUL-5362 return to the source list after deleting an issue (#5997)
* fix(issues): return to the source list after deleting an issue Deleting an issue from its detail page always pushed the workspace Issues list, so opening an issue from My Issues (or a project list, search, a pin, an agent panel) and deleting it dropped the user's navigation context — GH #5995. Go back instead. `useBackOrReplace` steps back when the platform reports in-app history and replaces with a fallback path when there is none, so a shared link opened cold or a new tab never steps off the app. Web answers via the Navigation API, falling back to counting its own pushes; desktop reads the active tab's virtual history. `replace`, not `push`: the deleted issue's URL must not stay in history for the back button to land on a 404. The not-found "Back to Issues" button loses the same context, so it moves to the same helper and its label becomes a plain "Back". Co-authored-by: multica-agent <github@multica.ai> * fix(web): track history position, not push count, for canGoBack The Navigation API fallback only ever counted pushes, so `pushes > 0` did not mean the current entry still had an in-app page behind it. Cold-open an issue, click Issues, press the browser's Back button, then delete: the tracker still claimed history and `back()` would step off Multica — the exact case the fallback exists to prevent. Count depth instead: a push adds one, any traversal takes one away (clamped at zero). Reaching the document's first entry requires traversing back at least as many times as we pushed, so a positive depth can never be claimed while sitting on it. A browser Forward is now conservative — it reports no history where a step back would have been fine — which costs a fallback navigation rather than an exit. Also corrects the useBackOrReplace contract comment: stepping back leaves the dead URL in forward history, so the guarantee is that it never lands on the back stack, not that it leaves history entirely. Co-authored-by: multica-agent <github@multica.ai> * fix(web): answer canGoBack from the browser alone, never from push counts Review found the counter still lied, one level deeper: it counted `router.push` calls, and a call is not a committed history entry. Next drops a push to `replaceState` when the canonical URL is unchanged (app-router.js, the `pendingPush && href !== canonicalUrl` branch), so clicking a self-link — the breadcrumb on an issue detail page, a pin to the issue you are on — incremented depth with no entry behind it, and a delete from there could still step off the app. Fixing the count needs per-entry markers, which means depending on both React effect ordering (our provider's effect runs before app-router's history commit) and Next's own preserveCustomHistoryState behaviour. Two rounds of review have now found holes in hand-rolled history tracking; a third layer of it is not the way to buy this guarantee. So stop deriving. `canGoBack` is the Navigation API's answer or `false`. Browsers without it take the fallback path, which is exactly what they did before any of this existed — nobody regresses, and the "wrong true walks the user out of the app" failure is now unreachable by construction. Drops the tracker, the popstate wiring and the push wrapper: the `multica:navigate` bridge returns to its original shape. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
38b08acf00 |
feat(diagnostics): report which page a desktop hang happened on (MUL-5345) (#5989)
* feat(diagnostics): attribute desktop hangs to route, function and stack (MUL-5345) A desktop hang currently reports "froze for 8s" and nothing else, so MUL-5345 could not be diagnosed at all. Three gaps, all fixed here. Route attribution was silently dead. The main window's route reporting lived in the PostHog pageview tracker and was deleted with it (MUL-4127), leaving `getDiagnosticContext` in main reading a WeakMap nothing ever wrote — every field report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push, and now feeds the in-renderer watchdog too: the renderer runs a memory router, so `location.pathname` could never identify the page either. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing. Function attribution did not exist. The watchdog now prefers `long-animation-frame` over `longtask` where supported, which carries per-script `sourceFunctionName` / `sourceURL` / `sourceCharPosition`. That covers hangs the thread survives. For hangs it does not, main captures the JS call stack over CDP — which requires the Debugger channel to be warmed at window creation, because a command sent after the thread is stuck never gets dispatched. Commands go through a four-verb allowlist, only scalar code locations are copied out of the paused frames (never `scopeChain`, whose handles dereference into user data), and resume is unconditional so a capture can never turn a recoverable hang into a permanent one. Reports could also be lost before delivery. `freeze:get-last` no longer deletes; the renderer sends with `send_instantly` and acks by exact timestamp, so a report killed by a second hang is retried next boot instead of vanishing with the file (the MUL-4115 failure mode: three deterministic hangs, zero events). A 7-day TTL keeps an undeliverable breadcrumb from becoming permanent boot noise. Operations name themselves: `parseMarkdownChunked` marks the diagnostic context before it runs, and the mark travels to main over the async IPC channel that still lands after the main thread stops responding. The event carries how stale the mark is, so it reads as context rather than as a cause. Verified on Electron 39.8.7 / Chromium 142 (throwaway spike, not committed): Debugger.pause during a 12s synchronous block returned the stack in 2ms with the blocking function on top; holding the channel open all session showed no cost beyond run-to-run noise (A/B/A, ordering drift larger than the effect); DevTools and the channel coexist in both open orders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): close the freeze report ack race and stop shipping raw ids (MUL-5345) Two review findings on #5989. Acking on hand-off was not acking on delivery. `onCaptured` fires when posthog.capture() returns; the request is still in flight, and posthog-js exposes no delivery callback to wait on (`CaptureOptions` has `send_instantly` and `transport`, nothing else). Deleting the breadcrumb there loses the report whenever the app freezes again or is killed in that gap — the same MUL-4115 failure the ack protocol was added to prevent. The flush now waits out a grace window before acking: if the process dies inside it the timer never fires, the file survives, and the next boot retries. Duplicates are the accepted trade and are trivially deduped on `breadcrumb_ts`; a lost report is not. Raw identifiers were reaching telemetry. The breadcrumb context was spread wholesale into the event props, so `workspaceSlug`, `tabId` and the absolute `windowUrl` shipped with every report despite the stated "bucketed path only" constraint. Fixed at both ends: the slug and tab id are no longer put into the route context at all (nothing else read them), the sanitizer constructs its result explicitly so a stale renderer's payload can't reintroduce them, `windowUrl` is dropped since it is an install path that can carry the OS username and the bucketed route already says which page it was, and the event props are now assembled field by field so a future context key cannot ship itself. The flush moved into `freeze-flush.ts` to make both behaviours testable: `onCaptured` does not ack, the grace window does, a cancelled window keeps the breadcrumb, and props built from a context still carrying slug/tabId/windowUrl contain none of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(diagnostics): reduce MUL-5345 to route attribution only Scope call from product review: the next hang should answer "which page", and nothing more. Removes the CDP stack capture, the long-animation-frame observer, the operation breadcrumb, and the read/ack delivery protocol added earlier on this branch, along with the spike-derived build note. What stays is the smallest change that gives the two existing hang events a real route. The route reporting had been dead since MUL-4127 (#4996) deleted it along with the PostHog pageview tracker: `getDiagnosticContext` in main kept reading a WeakMap nothing wrote, so a hang report carried only the asar index.html URL. `DiagnosticRouteReporter` restores the push to main — the only party alive during a true hang, which cannot ask a blocked renderer anything — and also publishes to the in-renderer watchdog, whose `location.pathname` is that same useless packaged path because the shell runs a memory router. Paths are bucketed to templates (`/:slug/issues/:id`) before publishing, and the workspace slug and tab id are not sent at all; nothing outside diagnostics read them. The sanitizer constructs its result explicitly so a renderer older than this build cannot reintroduce them, `windowUrl` is dropped because it is an install path that can carry the OS username, and the breadcrumb event props are assembled by whitelist rather than by spreading the context. Both hang events now report `path` under the same name, so they group in one query. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(diagnostics): bucket hang routes by known structure, not id shape (MUL-5345) The bucketer guessed which segments were ids by looking at them — UUID, issue key, or all digits. Every id that does not look like one therefore travelled to telemetry intact, and most of ours do not: project, autopilot, agent, member, squad, runtime, skill and attachment ids are arbitrary strings from `paths.ts`. `/acme/projects/p1` bucketed to `/:slug/projects/p1`, and `/acme/runtimes/machine%2Fruntime/runtime/runtime%20one` came through completely unchanged. It now matches structurally against the known route shapes, so a `:param` slot is whatever occupies that position regardless of how it is spelled or encoded. Where several patterns fit, the most literal one wins, which keeps `agents/new` the create page rather than an agent whose id happens to be "new". An unmatched path is masked (`/:slug/issues/*`, `/:slug/*`) rather than passed through: a route we do not know is exactly the case where an id cannot be told from a page name, so nothing from it travels. That makes a route added to paths.ts without being added here a loss of detail instead of a leak. To stop the list falling behind quietly, a parity test walks the real path builders — not a copy of them — and asserts that no builder leaks its slug or ids and that none falls back to the mask. Removing a single route from the table fails it with the builder named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c7fe10e549 |
fix(issues): move the thread quick-jump rail to the right edge (MUL-4522) (#5990)
The rail looks and behaves like a scroll affordance, but sat on the far left: on a wide screen it was hundreds of pixels from both the body text and the scrollbar the pointer was already on, so every jump cost a full sweep across the page. Move it to the right edge, just inside the scrollbar. right-3 plus a 20px strip is the inset that holds in both scrollbar modes: it clears a classic scrollbar's ~11px gutter, and lands exactly on the content column's 32px padding when the gutter is 0 (overlay scrollbars), so it covers neither the scrollbar nor body text. Ticks flush right and the hover wave grows them inward; the preview card opens leftward. The find bar steps inside the rail on desktop so it can't cover the top ticks. No left/right preference setting: the evidence is one-sided, and a toggle would mean maintaining and testing two layouts for a preference nobody has argued for yet. Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4581e9ee76 |
fix(server): surface real reason for failed quick-create (MUL-5268) (#5898)
* fix(server): surface real reason for failed quick-create (MUL-5268, #5885) When an agent's quick-create run finishes without producing an issue, the completion path wrote a fixed "agent finished without creating an issue" inbox, discarding the real reason — most often the active-duplicate guard rejecting the create. Users saw no actionable detail. notifyQuickCreateCompleted now: - distinguishes pgx.ErrNoRows (a confirmed no-issue → real failure) from a genuine lookup fault (DB/timeout), so a transient error no longer mislabels a run that may actually have created the issue; - on the real-failure branch, surfaces the agent's final output as the failure reason. The quick-create prompt already requires the agent to exit with the CLI error as its only output, so this carries the concrete cause (e.g. the existing issue's identifier + status), unescaped, bounded, and redacted. Empty output falls back to the existing generic message. No API/CLI contract or migration change. Co-authored-by: multica-agent <github@multica.ai> * fix(server): never end quick-create with no notification on lookup fault Review follow-up. The previous commit returned silently when the completion lookup failed with a non-ErrNoRows error, to avoid misreporting a failure that was never observed. But the task is already completed and nothing retries this reconciliation, so a single transient DB fault permanently stranded the requester with no inbox result at all. The indeterminate branch now writes a neutral, terminal notification: it does not assert failure (the agent may have created the issue), does not reuse the agent output as if it were the confirmed reason, and points at the one safe next step — check recent issues before retrying, so a retry cannot silently produce the duplicate the guard exists to prevent. notifyQuickCreateFailed / notifyQuickCreateUnconfirmed are now thin wrappers over a shared writer so both outcomes keep the identical row shape and the frontend's 'Edit as advanced form' recovery affordance. Tests: - TestQuickCreateLookupFault_WritesUnconfirmedInbox: fails against the previous commit with 'no rows in result set' (the exact silent-drop), passes now. Uses a DBTX wrapper that faults only GetIssueByOrigin so the inbox write still reaches the real DB. - TestQuickCreateFailure_RedactsAgentOutput: locks in that the newly-surfaced agent output is scrubbed before storage. Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): render unverified quick-create outcome as neutral, not failed Review round 2. Three fixes. 1. Rebased onto main and updated the three CompleteTask call sites for the new sessionRolloutMissing parameter; the branch no longer compiles-fails on the merge ref. 2. The unverified outcome reused the quick_create_failed inbox type, so every client framed it as a failure regardless of the neutral title/body: web list rendered 'Failed: {detail}', web detail showed 'Create with agent failed', mobile rendered 'Failed: ...', and getInboxDisplayTitle replaced the neutral title with the original prompt. Users saw 'Failed: Couldn't confirm...' — asserting a failure never observed. Added a distinct quick_create_unconfirmed type end to end: core type union, web list label (no failure framing), web detail pane, the original-prompt box and 'Edit as advanced form' recovery affordance, mobile label + display title, and en / zh-Hans / ja / ko strings. Older clients hit their existing default branch and render the already-neutral title. 3. The terminal notification reused the caller's context, so a lookup that failed with context.Canceled / DeadlineExceeded failed the write for the same reason and still dropped the notification. The write is now detached via context.WithoutCancel with a bounded timeout. Tests (each verified to fail without its fix): - TestQuickCreateLookupCancelled_StillWritesUnconfirmedInbox: cancels the ctx at the lookup; without the detach, 'no rows in result set'. - inbox-detail-label.test.tsx: resolves accessors against the real en locale; pointing the unconfirmed case back at failed_with_detail reproduces 'Failed: Couldn't confirm...'. - inbox-display.test.ts: both outcomes stay recoverable rows. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a1bd3ca9b2 |
fix(editor): allow spaces in mention search (#5980)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ef69c3d4a3 |
feat(projects): add search and max-height to the project picker (MUL-5344) (#5979)
* feat(projects): add search and max-height to the project picker (MUL-5344) Migrate ProjectPicker off the bare DropdownMenu onto the shared PropertyPicker (the same primitive assignee/label/status pickers use), so the project dropdown now caps its height with a scrollable list and gains a client-side search box. Search matches on title substring and pinyin, so Chinese project names are reachable by latin input. Preserves the full existing contract: controlled/uncontrolled open with the Base UI open-latch normalization, the disabled read-only lock, the inline hover/keyboard clear button, and every caller's custom trigger. Co-authored-by: multica-agent <github@multica.ai> * fix(pickers): reset picker search state on programmatic close (MUL-5344) PropertyPicker cleared its search query inside the popover's open-change handler. Every picker closes itself after a selection by calling its own setOpen(false), which flips the `open` prop directly and never routes through that handler — so the stale query survived into the next open and kept the rest of the list filtered out. Move the reset onto the open -> closed transition so it covers programmatic closes too. This also fixes the same latent staleness in the assignee and label pickers, which close on selection the same way. Adds a regression test: search -> select -> reopen must show an empty input and the full list. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6da0407b03 |
fix(issues): keep issue row pages in sync on observer reattach (MUL-5341) (#5978)
* fix(issues): refetch invalidated row pages on observer reattach (MUL-5341) issueTableRowPageOptions used `refetchOnMount: false`, which blocks the mount refetch of a *successful-but-invalidated* row page as well as an errored one. When a row page is WS-invalidated while its dynamic useQueries observer is detached and the observer later reattaches, the page stays `invalidated: true / fetchStatus: idle` under the global `staleTime: Infinity` default — the status count (facet query, always active) updates but the list keeps a stale snapshot missing the moved issue, until a full page refresh. Switch to `retryOnMount: false`, which expresses the intended "don't auto-retry an errored page" behavior without blocking stale (invalidated) successful pages from refetching. Fresh cached pages still don't refetch (staleTime: Infinity), so re-expanding a collapsed section reuses settled cursor pages. Add core regression tests for both the invalidated-refetch and the errored-stays-errored paths, and align the status-branches test fixture with the production client's `staleTime: Infinity`. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep errored row pages stable on reattach (MUL-5341 review) Address Elon's review: `retryOnMount: false` alone only guards a no-data first-load error. When a page has loaded, then an invalidation-triggered background refetch fails, TanStack's "error" reducer flags the page `isInvalidated: true` while retaining its data. Under the default `refetchOnMount: true` that page is both stale and errored, so it re-fires the failing request on every dynamic-observer reattach — bypassing the page's explicit Retry. Add `refetchOnMount: (query) => query.state.status !== "error"` alongside `retryOnMount: false`: a successful-but-invalidated page still refetches on reattach (the original bug), a fresh page stays put under `staleTime: Infinity`, and both first-load and background-refetch errors now wait for an explicit Retry. Add a regression test covering the has-data background-refetch-error path (load → invalidate → refetch fails → detach/reattach asserts no extra request until an explicit refetch). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a964c5229d |
feat(editor): pan/zoom the image attachment preview (MUL-5316) (#5971)
* feat(editor): pan/zoom the image attachment preview (MUL-5316) Image preview could only fit-to-window, so a screenshot of text opened unreadably small with no way in. It now runs on the same pan/zoom canvas the Mermaid viewer uses: fit on open, then wheel (cursor-anchored), drag, pinch, double-click fit<->100%, +/-/0 and arrow keys, plus a toolbar with zoom out / % / zoom in / fit / actual size / reset. The canvas is generalised out of Mermaid rather than duplicated: diagram-transform -> zoom-transform, use-diagram-canvas -> use-zoom-canvas, the canvas CSS out of mermaid.css into zoom-canvas.css, and a shared ZoomCanvas + ZoomControls that MermaidViewer now composes too (dropping its hand-rolled toolbar and canvas markup). The five zoom labels move from editor.mermaid.* to a shared editor.canvas.* in all four locales. Two fixes the generalisation forced, both of which also improve Mermaid: - MIN_SCALE floored computeFitScale at 25%, so content more than 4x the canvas could not be fitted at all. A 1600x8000 full-page screenshot needs ~10% and would have opened cropped — worse than the fit-only behaviour it replaces. The lower bound is now min(0.25, fitScale), threaded through clampTransform / zoomToAt / canZoomOut. - The canvas measured its viewport with getBoundingClientRect while its container was mid scale-in animation, fitting against a viewport a few percent too small; ResizeObserver reports the untransformed layout box so it never fired to correct it. Measures offsetWidth/offsetHeight now. Image-specific handling: natural size is read from the ref as well as onLoad (a cached image is already complete before onLoad attaches), and an image with no intrinsic size — an SVG with only a viewBox — keeps the old letterboxed render with the zoom controls hidden instead of a blank canvas. The canvas is focused on open so the keyboard controls work without a click first, native image drag is disabled so it can't hijack the pan, and the backdrop only closes on a click that actually lands on it. Verified: pnpm typecheck, pnpm lint, pnpm test (3003 views tests, 50 in attachment-preview-modal). The flex chain and the long-screenshot fit were also checked in headless Chromium, which jsdom cannot measure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): keep kbd keycaps readable inside tooltips Upstream shadcn inverts its tooltip surface, so Kbd forced near-white text inside tooltip-content. Our TooltipContent keeps the popover surface, which made keycaps render white-on-white. Drop the inverted-surface overrides so keycaps keep their regular muted colors, which read correctly on popover in both themes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(editor): drop the redundant reset zoom control, add shortcut tooltips The reset toolbar button was a literal alias of fit (reset: fit) — it could never do anything fit doesn't, so remove it along with the reset API, the isFitted state that only served its disabled look, and the reset_view copy in all four locales. Replace the native title attribute on the remaining zoom controls with the shared Base UI Tooltip + ShortcutKeycaps pattern (same as the editor bubble menu), showing the matching keyboard hints: zoom out (-), zoom in (+), fit to view (0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
67d3952ee2 |
fix(transcript): auto-follow live task output in transcript dialog (#5932)
* fix(transcript): auto-follow live task output in transcript dialog (#5921) Since the transcript event list moved to Virtuoso (#5733), a live task's new events required manual scrolling to see — the dialog opened at the top and never followed appended output. Wire live-follow through Virtuoso's own primitives, per sort direction: - Chronological (default): `followOutput` returns "smooth" while the task is live and the reader is at the bottom (Virtuoso's own atBottom tracking, with a forgiving 120px threshold). Scrolling up suspends the follow until the reader returns. - Newest-first: growth is PREPENDS, which the firstItemIndex anchoring deliberately holds in place — so a reader parked at the top would silently stop seeing new rows. Track the top edge via `atTopStateChange` into a ref and snap back to index 0 when new events arrive while at the top. Readers who scrolled away are left in place. - Opening a live chronological transcript now lands on the newest event (`initialTopMostItemIndex: LAST/end`, same pattern as the chat list); the per-listEpoch remount re-applies it after task/sort/filter changes. Completed tasks keep opening at the top. Fixes #5921. * fix(transcript): rework newest-first live follow as a user-intent latch (#5921) The previous approach gated the newest-first follow on "is the viewport at the top right now" — but firstItemIndex prepend anchoring moves the viewport away from the top on every flush, so the signal broke itself: after the first prepend the follow silently disengaged and never recovered. Replace it with a pure latch controller (transcript-follow.ts): - Disengage only on accumulated USER displacement (wheel/touch/key deltas, scrollbar drag) beyond the 120px edge zone; system displacement never counts, no matter how far it pushes the viewport. - While following, non-user displacement is pinned back to the live end on the scroll event itself (Virtuoso's prepend compensation lands after React effects, so an effect-timed snap alone stays one flush behind). - Enforcement is suppressed while the mouse is held (text-selection autoscroll) or mid-gesture; returning within the edge zone re-engages. - Timeline segment clicks explicitly unlatch so navigation isn't pinned back. Chronological followOutput switches "smooth" -> "auto": a still-animating smooth scroll reads as "not at bottom" on the next flush and drops the follow under rapid appends. The latch decision table is unit-tested (transcript-follow.test.ts), and the mechanism was validated end-to-end against react-virtuoso 4.18.7 in a browser harness: follow-at-top, in-zone nudge + flush (no false disengage), scroll-away with stable anchor, return-to-top re-engage, rapid flushes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1869b1b5e0 |
test(issues): give table-view editor test 60s to survive CI CPU starvation (MUL-5326) (#5964)
Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e30776dd9b |
feat(agent): add Claude Opus 5 to the Claude runtime catalog (MUL-5282) (#5910)
Opus 5 is the current flagship in Claude Code's bundled catalog (verified against claude-code 2.1.219: id `claude-opus-5`, display name "Opus 5", pricing tier_5_25, capabilities include xhigh_effort/max_effort). Without a catalog entry the model picker never offered it, and — more damaging — ModelKnownIncompatibleWithProvider treats any unlisted `claude-*` id as a known mismatch, so an agent manually pinned to `claude-opus-5` had the value erased on save. - Add `claude-opus-5` to claudeStaticModels(). Sonnet 4.6 stays the sole badged default; Opus remains a deliberate opt-in. - Allow the full low/medium/high/xhigh/max effort range in claudeModelEffortAllow, matching the rest of the Opus family. - Price it on the standard 5/25 Opus tier in both the server table and the frontend estimator, so Opus 5 usage lands in cost totals instead of the unmapped-model diagnostic. The existing `[1m]` and `<provider>/` tolerances cover the other spellings runtimes report. Verified: go test ./pkg/agent/... ./internal/metrics/..., vitest runtimes/utils.test.ts, tsc --noEmit on packages/views. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ecce589867 |
MUL-5265: GitHub API-snapshot PR cards — CI status + mergeability (#5889)
* feat(github): API-snapshot PR cards — CI status + mergeability (MUL-5265) Fetch each linked PR's CI checks and mergeability from the GitHub GraphQL API as the single source of truth (Plan C). Webhooks, page visits and a bounded TTL sweep are refresh triggers only; nothing is inferred from webhook payloads anymore. Backend (server/internal/integrations/ghsnapshot): - installation-token cache + GraphQL client (private key / tokens never logged) - one paginated pullRequest query -> normalized per-check snapshot - outbound queue: (installation,repo,PR) dedup + single in-flight per PR, bounded worker pool, Retry-After / rate-limit backoff, jitter - head-SHA-guarded atomic batch replace (a slow response for an old head can never overwrite a newer head's snapshot) - bounded chase window (30s->5m, stops on terminal/closed) + page-visit + TTL refresh; clean degradation when no App private key is configured Removes the old suite-level webhook aggregation display path (query + handlers + tests). check_suite / check_run / status are now pure triggers. Frontend: PR card shows two independent tri-state elements (CI status + mergeability). "Ready to merge" only when merge state is clean; no-checks and unknown-mergeable never assert a positive verdict; progress strip removed; four locales; stale marker. Docs: github-integration + environment-variables (four languages) — now required App private key, read-only Checks/Commit-statuses permissions, new event subscriptions, capability boundaries and troubleshooting. Co-authored-by: multica-agent <github@multica.ai> * fix(github): address PR snapshot review blockers Co-authored-by: multica-agent <github@multica.ai> * fix(github): bound snapshot refresh scheduling Co-authored-by: multica-agent <github@multica.ai> * fix(github): concurrent check-run index migration + singleflight token mint Address Elon's third-round review on the MUL-5265 PR snapshot pipeline. Must-fix — migration built a non-concurrent index. The github_pull_request_check_run table declared PRIMARY KEY (pr_id, ordinal) inside CREATE TABLE, which builds a unique index synchronously and violates the repo rule that every migration-created index (including on a new table) use CREATE UNIQUE INDEX CONCURRENTLY in its own single-statement file. Split: 222 now creates the table without a primary key; new 223 adds the (pr_id, ordinal) unique index CONCURRENTLY. The atomic delete-all/insert write path already guarantees ordinal uniqueness, so a plain unique index is sufficient; the index also serves the pr_id-prefix list aggregation and the workspace/PR cleanup deletes. Nit — token mint now singleflights per installation. installationToken released the lock before minting, so the N workers of one installation could mint N tokens on a cold cache or a simultaneous renew. Concurrent callers for the same installation are now collapsed via singleflight into one HTTP mint; added a -race concurrent-mint test asserting a single mint under 16 callers. Verified: fresh DB migrates through 223 (table has no PK, concurrent unique index present); ghsnapshot suite + new test pass under -race; migration lint and handler github/workspace-delete tests pass; sqlc produced no diff; go build / vet / gofmt / git diff --check clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f599fe3b85 |
fix(views): board/list load-more no longer flashes the skeleton + friendlier footer (#5893)
* fix(views): board/list load-more stops flashing the full skeleton Tail (load-more) page fetches were folded into the surface-level isLoading through the per-branch aggregate, so scrolling to load more in List and status-grouped Board replaced the already-rendered rows with the full-surface skeleton. Track head-page pending separately and gate the surface isLoading/isRefreshing on heads only; per-branch pagination state that drives the load-more sentinel is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(views): friendlier infinite-scroll footer for board/list/swimlane Collapse four copies of the error/has-more/reached-end ternary into a shared ListLoadMoreFooter: the loading spinner now carries a "Loading…" label so a slow fetch no longer reads as stuck, and a column that actually paginated shows a muted "No more" marker at the end instead of stopping silently. InfiniteScrollSentinel gains an optional label; adds the table.no_more string in en/zh-Hans/ja/ko. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
485f0f3124 |
fix(search): stop Home/End propagation in command palette inputs (#5870)
* fix(search): stop Home/End propagation in command palette inputs cmdk's root CommandPrimitive captures Home/End at the container level for list navigation (jumping to first/last item). When the search input is focused those keys never reach the native text-input handler, so the text caret does not move to the beginning/end of the query. Fix: add an onKeyDown handler on CommandPrimitive.Input that calls stopPropagation() for Home and End before the event reaches cmdk's root listener. The existing caller-supplied onKeyDown is still called so callers can layer their own handlers. Applied in two places: - packages/ui/components/ui/command.tsx (CommandInput wrapper — generic command palette used across the app) - packages/views/search/search-command.tsx (SearchCommand — Ctrl+K global search palette; uses CommandPrimitive.Input directly) Fixes #5655 * test(search): cover Home/End caret handling in command palette inputs Add regression coverage for the Home/End fix (MUL-5260, PR #5870): - search-command.test.tsx: Home/End move the query caret and do NOT hijack cmdk result selection, while ArrowUp/ArrowDown still navigate. - command-input.test.tsx: the shared CommandInput stops Home/End from bubbling to cmdk's root handler while still forwarding every key to a caller-provided onKeyDown; other keys keep bubbling. Lives in @multica/views because @multica/ui has no test runner. Verified as a genuine guard: reverting the fix hunks fails exactly these new assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: NevilleQingNY <nevilleqing@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
00e658401b |
feat(views): revamp execution log — virtualized, readable, two-tier header (#5890)
Combines the transcript work (previously split across #5860 virtual scroll and #5871 reading hierarchy) into one branch on current main, folding in the runtime-alias display from #5881. - Virtualize the event list (react-virtuoso) so a multi-thousand-event run mounts a bounded number of DOM rows (#5733), with firstItemIndex anchoring for newest-first live prepends. - Reading hierarchy via a pure trace-event-presenter: agent text and errors render expanded in place through RichContent (compact, log-scale markdown); thinking/tool rows fold to one line; tool detail expands into a quiet surface with "show all". - Persisted 3-way expand density (smart/expanded/collapsed) with per-row overrides; legacy defaultExpanded boolean migrated. Filters always persist (dropped the preserve-filters toggle). - Two-tier header: identity row (status, agent, trigger source, triggered-by) + list toolbar (created/duration/events facts left, shared Button controls right); runtime/provider/workdir/timestamps move to an ⓘ popover, runtime shown via runtimeDisplayName (#5881). - Copy-all exports full event bodies (redacted) with RFC 3339 timestamps (#5873), not the truncated summary. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
73b0015475 |
feat(vcs): make self-hosted Git providers self-host-only (MUL-3772, MUL-5138) (#5888)
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772) The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where Multica can reach a Git instance on the operator's own network. On the managed multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied instance URL from the server) and would store third-party Git tokens for all tenants under one key, while only serving the small subset of users whose instance is publicly reachable. Product decision: offer it on self-host only. - Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default off). Connect, rotate, and webhook now require BOTH the switch on AND a valid MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence alone. When off, connect/rotate return 404 and the webhook returns a bare 404 (no config leak), independent of the frontend. - /api/config exposes vcs_integration_available (mirrors the switch, omitted when false) so the Settings UI hides the whole "Git providers" section on cloud instead of surfacing an operator-only "missing key" hint. - docker-compose.selfhost.yml defaults the switch on; .env.example documents it. - Docs (en/zh) lead with a callout: available on self-hosted Multica only, not Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git. #5006 / #5883 stay in place — the schema and backend capability are retained; this only gates availability. No cloud VCS connection can exist (connect always required the key, which the cloud never set), so nothing needs migrating. Verified: go build/vet + VCS/config handler tests on a fresh migrated DB (incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and the integrations-tab + core schema/config vitest suites pass. Co-authored-by: multica-agent <github@multica.ai> * fix(vcs): complete self-host integration gating (MUL-5138) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
999f73e61f |
fix(projects): show owner/repo label for GitHub resource URLs (MUL-5221) (#5795)
* fix(projects): show owner/repo label for GitHub resource URLs instead of truncated full URL Long GitHub URLs in the project-resources sidebar are end-truncated by CSS `truncate`, making similar repos like org/very-long-repo-a and org/very-long-repo-b indistinguishable from the visible prefix alone. Extract just the `owner/repo` segment from the URL for display; keep the full URL in the tooltip so it remains accessible on hover. Applied to both the resource row and the repository picker popover. Custom labels are unaffected. Fixes #5771 Co-authored-by: multica-agent <github@multica.ai> * fix(projects): strip .git suffix and handle scp SSH URLs in githubShortLabel; add middle-truncation for long repo names Addresses #5795 review feedback: - Strip .git suffix (example-org/very-long-repo-name.git → without .git) - Handle git@github.com:owner/repo.git (scp shorthand) which new URL() rejects - Add midTruncate() so repos sharing a long common prefix are distinguishable by their tail (customer-alpha vs customer-beta) rather than being end-truncated identically by CSS Fixes #5771. Co-authored-by: multica-agent <github@multica.ai> * refactor(projects): share githubShortLabel across project resource UIs Move githubShortLabel/midTruncate out of project-resources-section into packages/views/common/github-url.ts so the create-project repository picker reuses the same owner/repo shortening (it was still rendering full URLs) and recognize the www.github.com host. Add github-url.test.ts covering the https / ssh / scp / enterprise / malformed cases plus middle-truncation. Addresses #5795 review feedback: shared helper, create-project coverage, tests. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> |
||
|
|
7cc763bbfb |
fix(runtimes): show runtime aliases across user-facing surfaces (MUL-5248) (#5881)
Several UIs rendered the raw daemon name (runtime.name) and dropped the user's custom_name alias: the Skills "Copy from runtime" selector, the Skills list/detail source, the agent creation runtime chip, the agent runtime filter, the agent overview, the skills/MCP runtime hints, the runtime delete confirmations, the desktop runtime window title, the onboarding runtime cards/hints, and the task transcript chip. Route every user-visible runtime label through the shared display contract: - runtimeDisplayLabel (alias + provider) for standalone labels - runtimeDisplayName (alias only) where a provider icon/text sits beside it - machine.title + runtimeRowLabel(runtime, machine.title) inside machine groups The Skills "Copy from runtime" selector is now machine-grouped via the shared buildRuntimeMachines/runtimeRowLabel helpers instead of a flat raw-name list; runtime.name stays the source of identity for hostname parsing, grouping, search, and protocol payloads only. Add a conventions rule (en + zh) forbidding raw runtime.name in user-visible JSX/i18n/Select labels/document titles. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2aafa41a5f |
MUL-5258: feat(issues): show agent-working indicator in Table view (#5875)
* feat(issues): show agent-working indicator in Table view Reuse the self-contained IssueAgentActivityIndicator (already in List and Board views) in the Table view's Issue column, rendered right after the identifier and before the title — matching the List view placement so the 'agent working' cue sits in the same spot across all three views. The indicator returns null when no agent is active, so inactive rows are unchanged. MUL-5258 Co-authored-by: multica-agent <github@multica.ai> * test(issues): cover Table agent-working badge insertion The previous stub returned null with no assertion, so deleting the badge insertion in table-view.tsx would still pass every test here. Render a marker carrying the issue id and assert the Table cell mounts it for the row's issue, positioned between the identifier and the title (identifier → activity → title, matching List/Board). Verified the new test fails when the insertion is removed. MUL-5258 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
581d9527ba |
feat(vcs): self-hosted Git providers (Forgejo, Gitea, GitLab) alongside GitHub (MUL-3772) (#5006)
Adds self-hosted Git provider support (Forgejo, Gitea, GitLab) alongside GitHub: per-workspace token connection, a provider-dispatched webhook, PR/MR and CI mirroring, and the shared issue auto-link / auto-close machinery. Off until MULTICA_VCS_SECRET_KEY is set, so existing deployments are unaffected. Co-authored-by: Bohan <bohan@devv.ai> |
||
|
|
a767c5b361 |
feat(transcript): include timestamps in copied events (#5873)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7d2f20f2ee |
feat(chat): warn when agent daemon is too old for project context (#5867)
Chat project context (MUL-5150, #5765) is rendered into the run brief by the daemon, so daemons older than the release that ships it silently drop the project description while still honoring the server-extracted repos. Mirror the handoff-note soft gate: resolve the active agent's runtime cli_version from the warm runtime cache and, when it is a stale release build, surface a warning next to the composer chip and inside the project submenu. Selection is never blocked — dev-describe builds and unknown runtimes stay silent. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a3fe6d91dd |
MUL-5150: add project context to Chat (#5765)
* feat(chat): add project context Co-authored-by: multica-agent <github@multica.ai> * fix(chat): resolve MUL-5150 review blockers - Renumber project-context migrations to unique prefixes after current main: 206_chat_session_project -> 212 (column), 207_chat_session_project_index -> 213 (concurrent index). 206/207 collided with 206_agent_disabled_runtime_skills and main's 207-211 client_usage_daily set. - Add the 4 missing chat input.project_context keys to ja/ko locales so the locale parity test passes (en/zh-Hans already had them). - Lock the project-context control while a send is in flight (isSubmitting), not just while the agent is running. A brand-new chat creates its session lazily during send bound to the project at click time; switching project mid-send would create the session against the stale project and clear the editor as if the send landed on the new selection. Add a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): complete project context handling * fix(chat): pin fresh chat to open session's agent on project switch Switching an existing session to a different project opens a fresh chat but only cleared the active session, dropping selection back to the stored `selectedAgentId`. When that preference was stale (open session belongs to agent B while the persisted pick is still agent A), the lazily-created session and its first send bound to the wrong agent (agent A). Extract the project-switch decision into a shared `planProjectContextChange` pure helper in use-chat-controller.ts and route both chat surfaces (the chat tab controller and the floating ChatWindow) through it, so the fresh chat is pinned to the open session's agent and the rule cannot drift between the two copies. Add a dual-entry regression test (pure-fn guard + controller integration) covering the stale selectedAgentId case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(ci): re-trigger required checks on latest head The prior push updated the branch ref but GitHub did not emit a pull_request synchronize for it (PR head-sync lag), so CI/Mobile Verify never ran on the commit carrying the stale-agent project-switch fix. Empty commit to force a fresh synchronize on a head that includes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): renumber project migrations to 213/214 after main added 212 Current main added 212_agent_service_tier; the PR's 212/213 chat migrations collided with it on the merge ref, failing TestMigrationNumericPrefixesStay UniqueAfterLegacySet. Merge current main and move the chat column migration to 213 and the concurrent index migration to 214 (column before index preserved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): lock ProjectPicker clear control during send (keyboard path) The send-pending lock only put pointer-events-none on the wrapper, which blocks the mouse but leaves ProjectPicker's inline clear button in the tab order — a keyboard user could Tab to "Remove from project" and press Enter mid-send, detaching the project after the lazily-created session already went out with the old one (reopens the mid-send retarget path via keyboard). Add an explicit `disabled` capability to the shared ProjectPicker that locks the trigger, the menu (forced closed), and the inline clear button (disabled + out of the tab order). Defaults to false, so issue/create/autopilot callers keep their hover/keyboard clear. ChatInput passes disabled while the project selection is locked. Tests: real-ProjectPicker regression (keyboard activation of the clear control is inert when disabled; still works when enabled) + ChatInput wiring assertion that the picker is disabled mid-send. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> 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: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Co-authored-by: NevilleQingNY <nevilleqing@gmail.com> |
||
|
|
8d18d3a9ec |
Revert "MUL-5180: fix(github): surface CI status on PR cards (#5811)" (#5855)
This reverts commit
|
||
|
|
ffa8e16369 |
MUL-5228 fix(usage): bill Grok at xAI's reported cost, fix $0 resumed sessions (#5841)
* fix(agent): attribute Grok usage from the turn's own model id A resumed Grok session with no configured model recorded its entire spend under the model id "unknown", which matches no pricing row — so the task reported $0 cost instead of its real spend. grok.go only learned the model from the session handshake, and ACP's `session/load` carries no model id (only `session/new` does). When neither the agent nor MULTICA_GROK_MODEL pins a model, `daemon.go` legitimately passes an empty model, leaving nothing to attribute the usage to. Every Grok turn stamps `result._meta.modelId` with what it actually billed against. Parse it in the shared ACP result parser and use it as the fallback in grok.go. Other ACP backends are untouched — they keep whatever the handshake gave them. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(metrics): price the Grok catalog in server-side cost metrics server/internal/metrics/pricing.go carried no Grok rows at all, so RecordLLMUsage took the unpriced branch for every Grok turn: llm_cost_usd reported zero Grok spend while the tokens accumulated in llm_unpriced_tokens. Internal cost monitoring simply could not see Grok. Add the six SKUs xAI publishes rates for, mirroring the frontend table in packages/views/runtimes/utils.ts. Aliases are anchored exact matches like the gpt-5.6 rows, so `grok-composer-*` (in the catalog, absent from the price sheet) stays unmapped instead of inheriting a guessed rate. Short-context tier on purpose: xAI bills a request at 2x once its prompt reaches 200K tokens, but a usage record aggregates every model call in a turn and cannot say which tier an individual request hit. A regression test re-derives the cost of a real grok 0.2.106 turn from the table and checks it against the costUsdTicks xAI returned for that turn. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): scope the Grok cost claim to what was actually fixed The v0.4.9 entry promised "accurate cost" in all four languages, but the fix corrected catalog pricing and cached-input double-counting — it did not implement xAI's 2x long-context tier, so a turn whose requests reach 200K prompt tokens still under-reports by up to 50%. Say what was fixed instead. Also correct two stale claims in the pricing comment: the daemon tags usage rows with the runtime provider `grok`, not `xai` (the bare `grok-*` keys are what make them resolve), and record why thresholding the long-context tier on an aggregated row would be worse than not pricing it at all. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(usage): carry the provider's own cost through to the usage record Cost has always been derived client-side as tokens x a static rate, which cannot express request-level pricing rules. xAI bills a Grok request at 2x once its prompt reaches 200K tokens, and a task_usage row aggregates every model call in a turn — so the stored token counts genuinely cannot say which tier any individual request hit. Thresholding on the aggregate would be worse than the status quo: it turns a bounded 50% under-estimate into an unbounded over-estimate for turns made of many short requests. Grok already reports what it charged, per turn, in `_meta.usage.costUsdTicks`. Parse it, carry it through agent -> daemon -> API, and store it on task_usage as a nullable BIGINT of 1e-10 USD ticks (integer, so sub-cent turns stay exact end to end). NULL means the provider reported no cost — every pre-existing row and every provider that doesn't return one. No backfill: there is no authoritative figure to recover for those, and inventing one is the guess this removes. A single hourly bucket can mix rows that carry a cost with rows that don't, so task_usage_hourly gains both halves: `cost_usd_ticks` sums the authoritative side, and `uncosted_*_tokens` carry exactly the tokens that still need a rate-table estimate. Consumers report authoritative + estimate(uncosted), which degrades to today's behaviour when nothing in the bucket is authoritative. The existing token columns keep covering every row, so token displays are untouched. The new columns are additive with defaults, so the unique key, the dirty-queue shape, and migration 102's triggers are unaffected. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(usage): prefer the provider's own cost over the rate table With the authoritative figure now stored, both cost consumers use it: the usage dashboard (estimateCost / estimateCostBreakdown) and the server-side llm_cost_usd metric. Each reports `authoritative + estimate(uncosted tokens)`, so a row or bucket that mixes priced and unpriced sources stays whole. The static rate tables remain, but for Grok they are now a fallback — they still price usage recorded by a daemon too old to report cost, and every provider that reports none. Custom pricing overrides likewise apply only to the estimated half: they are a user's guess at a rate, and the authoritative half is not a guess. A model with no rate-table row but a provider-reported cost now also drops out of the "unmapped models" banner, since asking the user to supply a rate for it would invite overriding a real bill. llm_cost_usd is labelled by token_type and the provider reports one number per turn, so the charge is distributed across the buckets in the rate table's own proportions. Only the total is authoritative; the split stays an estimate, which is why this scales the existing buckets rather than inventing a label. estimateCostBreakdown does the same, keeping the stacked chart summing to the headline figure instead of silently under-drawing every Grok row. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): say Grok cost now follows xAI's actual charge The earlier wording scoped the claim down to catalog pricing and cached input because the long-context tier was still unhandled. It is handled now — the cost comes from what xAI charged for the turn — so the entry can say so. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(usage): keep the provider's cost when the model has no rate row Both cost consumers bailed out before reading the authoritative figure when the rate table had no row for the model. A `grok-composer-*` turn — in the Grok Build catalog, absent from xAI's price sheet — was therefore reported as $0 spend even though xAI told us exactly what it charged. Worse on the client: estimateCost returned the real cost while estimateCostBreakdown returned zeros, so the headline and the stacked chart disagreed on precisely the rows whose cost is exact — and the unmapped-models banner was (correctly) hidden, so nothing explained the discrepancy. Handle the charge before the rate lookup in both places. Without rates there is nothing to split a total by, so it lands whole in the `input` bucket, the same fallback distributeAuthoritativeCost already uses when it has no shape to scale. Tokens with no rate keep going to llm_unpriced_tokens: "unpriced" describes the rate table, not the money. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * perf(usage): drop the historical rewrite from the cost-split migration Migration 213 rewrote every existing task_usage_hourly row to seed the uncosted counters. That is a full-table UPDATE inside a schema migration — lock time, WAL and bloat all scaling with table size — for rows this issue explicitly does not care about. Deleting the UPDATE alone would have zeroed historical cost: with `NOT NULL DEFAULT 0`, an untouched row asserts "nothing here needs estimating", so every pre-split bucket would report $0 until the rollup happened to touch it. Make the uncosted columns nullable with no default instead. NULL means "never recomputed since the split existed", readers COALESCE it to the row's own token total ("estimate all of it"), and the pre-split behaviour is preserved exactly — with nothing to seed, so no rewrite. A bare ADD COLUMN is metadata-only, so this is now fast DDL. Rows heal into the split naturally as the rollup recomputes their buckets. Verified on a fresh database: a legacy-shaped row reads back as its full tokens to estimate, and a group mixing legacy and post-split buckets sums to the authoritative cost plus both rows' estimable tokens. Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
139cc89200 |
MUL-5180: fix(github): surface CI status on PR cards (#5811)
* fix(github): surface CI status on PR cards (MUL-5180) The CI mirroring pipeline (MUL-2228, MUL-2392) has never received a single event in production. The GitHub App setup docs only ever asked operators to grant `pull_requests: read` and subscribe to `pull_request`, so GitHub never delivered `check_suite` — `handleCheckSuiteEvent` sat dead behind a subscription nobody was told to enable. Every linked PR reports checks_passed/failed/pending = 0 and the sidebar row falls through to "Checks haven't reported yet" forever. Docs (the root cause), all four locales: - add `Checks: Read-only` permission + `Check suite` event to the App setup table - drop the stale "CI check states are not modeled" claim, which predates MUL-2228 and is what let the setup table stay incomplete - add a "PR rows show no CI status" troubleshooting entry with the public `/apps/<slug>` probe to confirm what an App is actually subscribed to, and a warning that existing installations must accept the new permission before any `check_suite` is delivered UI: - give the actionable status kinds (checks failed/pending/passed, conflicts, ready) their own icon + color. CI outcome previously rendered as plain muted 11px text, visually identical to the diff stats beside it — a failing build read the same as "+437 −6 · 6 files". Terminal and unknown kinds stay muted; the row's state icon already carries that meaning. Co-authored-by: multica-agent <github@multica.ai> * fix(github): unbreak docs build, stop overclaiming CI completeness (MUL-5180) Both must-fixes from review. 1. docs production build failed. `<your App>` in prose was parsed as a JSX tag, so `pnpm --filter @multica/docs build` died with `Expected a closing tag for <your>`. Dropped the angle brackets. Repo CI never caught this because no workflow runs the docs production build — only Vercel does, which is why the PR's GitHub checks were green while the deployment errored. 2. `Checks: Read-only` cannot support the pending status the docs promised. GitHub's webhook contract delivers `check_suite.requested` / `.rerequested` only to Apps holding Checks *write*; read-level access receives `completed` only. Verified against GitHub's published docs. Direction chosen: keep read-only, degrade honestly to final-results-only. Checks *write* is a repo-write capability (create/update check runs), not a wider read — escalating every installation to it just to render an in-flight spinner is not a trade to make on the operator's behalf, and it contradicts the integration's read-only posture. The concrete bug this leaves is premature green: with two reporting apps, the first to complete makes total=1/passed=1 and the row claimed "All checks passed" while the second was still running and might fail. Copy is now "Checks passed" in all four locales — it reports what reported and never asserts completeness. `derivePullRequestStatusKind` documents why. Docs gain a "what CI status can and cannot tell you" section (all four locales) with the read-vs-write delivery table, both consequences stated plainly, and the opt-in path for teams that do want in-flight status: set Checks to Read and write on their own App and the existing pending code lights up with no code change. The pending promise is removed from the read-only setup path. Co-authored-by: multica-agent <github@multica.ai> * fix(github): ignore non-completed check_suite actions (MUL-5180) Review was right: the `Read and write` opt-in the previous commit documented does not produce reliable pending, and following it would break the card. `check_suite.requested` / `.rerequested` are not observations that some CI provider started. GitHub sends them only to Apps holding Checks write, and per the CI-checks App docs they mean "GitHub has created a check suite for YOUR app on this commit; now add your check runs to it". Multica observes other apps' results and never creates check runs. Recording such a suite parks a `queued` row nothing can ever complete, and since `checks_pending` outranks `checks_passed` in derivePullRequestStatusKind, one stuck row freezes every PR on that installation at "checks running" and hides the real pass/fail result. Any self-hoster who already grants Checks write hits this on every push, so the gate is on the action, not the permission. - handleCheckSuiteEvent drops every action except `completed`, with the reasoning and the "don't resurrect requested as a running signal" warning recorded at the gate. - TestWebhook_CheckSuite_QueuedCountsAsPending encoded the wrong delivery semantics (two external apps sending `requested`, which GitHub never does). Replaced by TestWebhook_CheckSuite_NonCompletedActionsIgnored, which pins the drop and checks a later `completed` suite still lands. - The two out-of-order stash tests used `requested` payloads to exercise paths that are really about completed suites; both now use `completed` and assert the same guarantees. - Docs (four locales): the write opt-in is gone. In-flight CI is documented as unsupported at any permission level, with the actual reason and the note that real running status needs polling or a check_run model instead. Co-authored-by: multica-agent <github@multica.ai> * fix(github): make legacy non-completed check suites inert (MUL-5180) Review was right again: the previous commit gated the webhook entry point but left the pre-upgrade state — and the people it was meant to protect (self- hosters who already granted Checks write) are exactly the ones holding it. Two leftovers, both now closed: 1. Rows already in github_pull_request_check_suite. The old handler stored GitHub's `requested` suites as `queued`; nothing will ever complete them. ListPullRequestsByIssue still counted them, so `checks_pending` kept outranking `checks_passed` and the PR stayed pinned to "checks running" for as long as its head SHA stood. The aggregation now selects only `completed` suites. Filtering beats deleting here: recovery is automatic on deploy, needs no migration over a table that can be large, and holds for any writer that misses a gate — not just for today's legacy rows. DISTINCT ON runs after the filter, so an app whose newest suite is a stuck `queued` still reports its most recent completed verdict instead of disappearing. 2. Rows already in github_pending_check_suite. replayPendingCheckSuitesForPR is a second write path into the live table that never passes through handleCheckSuiteEvent, so the next `pull_request` event would re-inject a permanently-queued suite after the fix shipped. It now skips non-completed rows; the drain is DELETE ... RETURNING, so skipping discards them. Both are covered by regression tests that seed the legacy row directly — the fixed handler can no longer produce one — and both were confirmed to fail with their respective fix reverted. The stash test additionally asserts its fixture landed under the repo address the drain keys on; the first draft used the wrong owner and passed vacuously. Also corrects the aggregateChecksConclusion doc comment, which still described "pending" as a not-yet-completed suite. It is now reachable only for a completed suite carrying a null conclusion, and is explicitly not a "CI is running" signal. Co-authored-by: multica-agent <github@multica.ai> * test(github): assert the legacy stash row is consumed by the drain (MUL-5180) Review nits. The stash test proved its fixture existed before the webhook but never that the drain consumed it, so a future change to firePullRequestWebhookWithHead's repo address would make the assertions pass for the wrong reason again — the same way the first draft of this test did. Asserting the stash is empty afterwards closes that gap from the other side. Also fixes two comment typos: `an "CI is running"` -> `a`, and drops the "merged-but-open PR" state, which cannot exist. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e17e04b312 |
MUL-5215: fix(agents): keep creation errors visible (#5746)
* fix(agents): keep creation errors visible * fix(agents): localize duplicate-name errors |
||
|
|
98072e2e56 |
fix(issues): filter working agents by active task issues (#5839)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fb9e1a8c72 |
MUL-5191 fix(usage): price the Grok catalog + keep custom pricing editable (#5827)
* feat(usage): price the published xAI Grok catalog Grok usage resolved to no pricing row, so every Grok token contributed $0.00 to cost totals and the models sat permanently in the unmapped diagnostic. Add the six SKUs xAI publishes rates for. Short-context rates on purpose: xAI bills a request at 2x once its prompt reaches 200K tokens, but aggregated usage rows carry no per-request prompt sizes — the same trade-off the Anthropic `[1m]` context tag already takes. `grok-composer-*` ships in the Grok Build catalog but is absent from the price sheet, so it stays unmapped rather than inheriting a guessed rate. Source: https://docs.x.ai/developers/pricing Co-authored-by: multica-agent <github@multica.ai> * fix(usage): keep custom pricing editable after it is saved The custom-pricing dialog had exactly one entry point: the button inside the unmapped-models banner. That banner only rendered while something in the window was unpriced, and `resolvePricing` falls back to the custom store — so saving a rate for the last unmapped model made it resolve, the banner disappeared, and the saved rate could no longer be edited or removed. Clearing localStorage was the only way out, which also wiped every other override. Keep the bar mounted whenever there is something to manage: warning state while models are unpriced, a quiet row with an edit button once they all resolve but overrides exist. Hidden entirely otherwise. The existing store mock hard-coded `getCustomPricing` to undefined, so no test could ever observe a saved override; make it stateful and add the regression case. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan-J <bohan@devv.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
423a5c59cb |
MUL-5200: unify working-agent filters across issue views (#5819)
* fix(issues): query workspace working agents independently Co-authored-by: multica-agent <github@multica.ai> * feat(agents): filter working agents by source type Co-authored-by: multica-agent <github@multica.ai> * feat(issues): scope working agents to My Issues Co-authored-by: multica-agent <github@multica.ai> * test(agents): cover My Issues squad relations Co-authored-by: multica-agent <github@multica.ai> * fix(issues): unify working-agent filters across views Co-authored-by: multica-agent <github@multica.ai> * fix(issues): preserve empty working-agent filters Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1fef98c24f |
fix(desktop): open in-app links in a tab instead of the browser (MUL-5208) (#5826)
* fix(desktop): open in-app links in a tab instead of the browser (MUL-5208) A link written as an absolute URL on this deployment's own origin (`https://<app-host>/acme/issues/1` — what "copy link" produces, and what agents paste into chat) fell through openLink's external branch to window.open, which Electron routes to shell.openExternal. Clicking an issue link in Desktop chat therefore opened a browser window instead of a tab. openLink now resolves such a URL back to its in-app path and takes the same route a relative path does. Backend-served prefixes (/api/, /_next/) stay external so attachment downloads keep working. Two supporting fixes the change depends on: - The `multica:navigate` event had no listener on web, so in-app paths in content were dead links there; normalizing app URLs would have extended that to every pasted app URL. The web platform layer now answers the event with a router push. - The desktop handler opened every path inside the active workspace's tab group. A cross-workspace link now goes through switchWorkspace, matching what the navigation adapter already does for pushes. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): scope in-app link conversion to workspace pages, answer it in issue windows Review follow-ups on MUL-5208. 1. The non-page exclusion list only covered /api/ and /_next/, so a same-origin /uploads/* link — local-storage attachments, served by the backend and proxied by web — was routed as an app page, opening a dead tab instead of the file. Replaced the deny-list with the app's own routing model: an absolute URL converts to an in-app path only when its first segment is a slug a workspace could own, which the existing reserved-slug list (shared with the backend) already answers. /api, /uploads, /_next, /favicon.ico and the pre-workspace routes all stay external without a second list to keep in sync. 2. A dedicated issue window derives the same app origin from its adapter but had no multica:navigate listener, so a same-origin link there became a silent no-op (it used to reach the browser). The window now answers the event: another issue opens in place — matching what its adapter push and mention chips already do — and any other app page, which this single-route window cannot host, goes to the browser. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <agent@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |