mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
0582db356e730a8649dffbb6b38794f162cfa31c
3972 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0582db356e |
feat(issues): make cancelled a default status, not filter-gated (MUL-4290) (#5135)
MUL-4261 surfaced cancelled issues only when the status filter explicitly selected "cancelled": a separate BOARD_STATUSES (six statuses, cancelled excluded) plus a runtime showCancelled gate hid cancelled from the default list/board/swimlane. That is the wrong product model — cancelled is a lifecycle state in the same category as todo/in_progress/done/blocked and should be a first-class default column. - Remove BOARD_STATUSES. Its only purpose was to exclude cancelled, which this change reverses. PAGINATED_STATUSES is now ALL_STATUSES; the surface's default visible/hidden status derivation, the assignee-grouped board's default status set, and the swimlane column fallback all use ALL_STATUSES. - Remove the `bucketedIssues.filter(status !== "cancelled")` gate in the surface data layer. Cancelled flows through to list/board/swimlane columns, header facet counts, batch selection, and isEmpty like every other status. - hiddenStatuses derives from ALL_STATUSES, so cancelled participates in the board show/hide controls consistently (hideStatus already used ALL_STATUSES). The status filter now narrows the visible set instead of unlocking an otherwise-hidden bucket. Cancelled renders last (its canonical ALL_STATUSES position). Mobile keeps its own status mirror and is out of scope. Regression tests updated: controller now asserts cancelled is a default visible status, the filter narrows (and can hide cancelled), swimlane renders the Cancelled column by default and drops it only when the filter narrows past it, and the assignee board fetches cancelled by default. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6077f1a9b0 |
feat(issues): surface cancelled issues via status filter (MUL-4261) (#5099)
* feat(issues): surface cancelled issues via status filter (MUL-4261) Cancelled issues were never visible in the web/desktop issue surface: `PAGINATED_STATUSES`/`BOARD_STATUSES` excluded `cancelled`, so the list/ board/swimlane never fetched or rendered it, and the status filter offered a "Cancelled" checkbox that resolved to an empty list. Implement plan A (fetch-always, hide-by-default): - `PAGINATED_STATUSES` now includes `cancelled`, so it is always fetched into the byStatus cache and rebuckets correctly when an issue is cancelled (previously the card was dropped). `BOARD_STATUSES` stays the default *visible* column set. - The surface gates the flattened list on the status filter: cancelled issues are excluded from `surfaceIssues` (and therefore list/board/ swimlane columns, header facet counts, batch selection, and isEmpty) unless the filter explicitly selects "cancelled". Then a Cancelled section appears, sorted last. - `hiddenStatuses` stays board-only, so cancelled is never offered as a hideable/persistent board column. Dragging a card into the Cancelled column (visible only when filtered) sets status=cancelled through the existing generic column DnD — no new entry point or copy added. Non-goals (unchanged): mobile, member/agent archive surfaces, an always-on cancelled column. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): swimlane must keep the cancelled column when filtered (MUL-4261) The swimlane derived its status columns as `BOARD_STATUSES.filter(s => visibleStatuses.includes(s))`, re-imposing canonical order by intersecting with BOARD_STATUSES. Since BOARD_STATUSES omits `cancelled`, a filter-selected Cancelled column was silently dropped even though the controller's `visibleStatuses` included it — the surface fetched and gated cancelled correctly, but swimlane never rendered it. Filter against ALL_STATUSES instead: same canonical ordering, but a selected `cancelled` column now survives. `hiddenStatuses` stays board-only, so cancelled is still never a hideable/persistent column. Regression tests: - swimlane renders a Cancelled column + its cards when cancelled is in visibleStatuses, and omits it otherwise (verified failing pre-fix); - controller asserts hiddenStatuses never contains cancelled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cac2965ddb |
fix(markdown): autolink read-only URLs in the parse tree, not raw text (MUL-4242) (#5091)
* fix(markdown): autolink read-only URLs in the parse tree, not raw text Read-only markdown surfaces (comments, descriptions, chat) pre-linkified bare URLs by rewriting the raw source to [url](url) before parsing. Because linkify-it treats `*` as a valid URL character, a bare URL followed by a bold close — `**PR:https://…/5081**` — had the trailing `**` swallowed into the match and rewritten as [url**](url**). That consumed the emphasis closer (the bold never closed; the leading `**` rendered as literal asterisks) and corrupted the href with a trailing `**` (MUL-4242). Let remark-gfm autolink URLs in the parse tree instead, where emphasis is already resolved so an adjacent delimiter can never be absorbed. The custom string pass now runs in a `urls: false` mode on read-only surfaces and only linkifies file paths (which gfm never does). A small remark plugin (remark-cjk-autolink) re-applies the existing CJK URL boundary to gfm's autolink literals so `https://x/a。后面` still stops at 。. The Tiptap editor path is unchanged (`urls: true`): @tiptap/markdown does not autolink bare URLs, so it still needs the string pass. Note: read-only URL autolinking now follows GFM semantics (scheme, www., or email required); bare fuzzy domains like `NBA.com` render as plain text on read-only surfaces, matching CommonMark/GFM. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(markdown): keep every URL in a CJK-separated run linked in readonly Follow-up to the read-only autolink fix. remark-gfm glues `url1、url2` into a single autolink literal because it treats CJK punctuation as a URL character; remark-cjk-autolink trimmed only at the first terminator and dropped the tail to plain text, so the second URL stopped being a link — a same-class regression of MUL-4242 for CJK-punctuation-separated URLs (flagged in review). Re-derive the segments with detectLinks (which reuses collectLinkifyMatches' truncate-and-rescan) and rebuild the [link, text, link, …] sequence, so every URL in the run stays linked. Adds a read-only test for `两个地址 https://a.com/x、https://b.com/y`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
67cd1e645a |
fix(core): add exponential backoff with jitter to WSClient reconnect (#5036)
* fix(core): add exponential backoff with jitter to WSClient reconnect The WebSocket client used a flat 3-second reconnect delay with no backoff, jitter, or attempt limit. When the server restarts, every connected client (web + desktop) reconnects at exactly T+3s, creating a thundering-herd connection spike. Replace the fixed delay with exponential backoff: - Base delay 1 s, doubling each attempt (1 → 2 → 4 → 8 → …) - Cap at 30 s to keep recovery time reasonable - ±20 % jitter to decorrelate clients that disconnect simultaneously - Give up after 20 consecutive failures (log error, allow manual retry) - Reset the counter on successful authentication Add 7 unit tests covering the backoff curve, cap, jitter range, counter reset, max-attempt cutoff, and disconnect cancellation. Closes #5035 * fix(core): clamp jittered delay to max and make jitter test deterministic Address Copilot review feedback: 1. Clamp the final delay to RECONNECT_MAX_DELAY_MS after jitter is applied. Previously, when base was already at the 30s cap, +20% jitter could push the delay to 36s, violating the configured max. 2. Replace the nondeterministic jitter test (which relied on real Math.random() producing ≥2 distinct values in 20 samples) with a deterministic stub that alternates between 0 and 1, asserting exact min/max delays (800ms and 1200ms). * fix(core): remove reconnect attempt limit, retry indefinitely with capped backoff Address maintainer feedback (NevilleQingNY): The web/desktop UI does not currently expose a visible disconnected state or manual retry action, so the 20-attempt give-up limit would leave an open tab silently stale after a long outage. Remove the limit and let the client retry indefinitely with the 30s capped jittered delay. - Drop RECONNECT_MAX_ATTEMPTS constant and the give-up early-return - Update JSDoc to document the indefinite-retry contract - Replace "stops after max attempts" test with "keeps retrying indefinitely with capped delay" that verifies 25+ attempts still schedule reconnects at 30s |
||
|
|
3a3159a14a |
fix(ui): unify chat list & inbox list avatar size to 32px [MUL-4283] (#5121)
Chat list avatars read too large at 36px; inbox list was 28px, so the two surfaces were inconsistent. Standardize both to 32px (the common list-row avatar size) — chat list 36->32 (fallback placeholder size-9->size-8), inbox list 28->32. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3efe5bac17 |
fix(views): restore chat and inbox list gutters (#5120)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f8c4c88129 |
feat(agents): show runtime model + effort on agent hover card [MUL-4280] (#5117)
The agent profile hover card now surfaces the runtime-native model id (mono, e.g. `claude-opus-4-8`) with the reasoning/effort token as a badge, so a quick hover answers "which model is this agent running?" without opening the detail page. Empty model renders a "Runtime default" placeholder. Also fixes the Skills row: chips wrap to multiple lines, so the vertically centered label drifted to the middle chip — it now pins to the first chip row (items-start + pt), and each chip truncates so a long skill name can't blow out the card width. The effort badge is gated on `effort` alone (not `hasModel`): an agent with no pinned model can still persist a thinking_level override that applies at run time, and hiding it would misreport the agent's real config. Adds agent-profile-card.test.tsx covering the model/effort render states, including the `model:"" / thinking_level:"high"` regression. New i18n: profile_card.model_label / model_unset (en/zh-Hans/ja/ko). Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
23e4f125b2 |
feat(chat): default floating chat window to on (#5113)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4a93f4ce41 |
fix(chat): advance selection to next chat when archiving the open session (#5110)
* fix(chat): advance selection to next chat when archiving the open session Archiving the chat currently open in the two-pane Chat tab left the conversation pane showing a now read-only, dangling session. Mirror the Inbox list's handleArchive: move selection to the next chat in the sorted, non-archived history list, fall back to the previous one, and clear only when nothing is left. Archiving a non-open row is unchanged. Closes MUL-4278 Co-authored-by: multica-agent <github@multica.ai> * fix(chat): route archive-advance through the shared controller Address review of #5110: - Advance now routes through handleSelectSession so selectedAgentId stays in sync when the next chat belongs to a different agent (a follow-up "new chat" no longer defaults to the archived chat's agent). - Move the advance/next-prev/clear logic into use-chat-controller (advanceSelectionAfterArchive + archiveSession) and drive both Chat-tab entry points from a single ChatPage.handleArchive: the thread-list row AND the conversation header ⋯ menu (the header previously only flipped status, stranding the user on the archived read-only conversation). - Mobile: archiving the open fullscreen conversation returns to the list. - Floating window: archiving the open chat now advances to the next chat (with cross-agent sync) instead of clearing, matching the Chat tab. Tests: controller test covers advance/fallback/clear/no-op + cross-agent sync; thread-list test now asserts it delegates to onArchive. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
956f74e1d2 |
fix(chat): refresh Chat V2 input placeholder without remount (MUL-4276) (#5111)
Tiptap's Placeholder only reads its text at mount, and ContentEditor had a defaultValue-sync effect but no placeholder-sync effect. Switching between sessions of the same agent doesn't remount the editor, so the placeholder froze on the previous value — e.g. stuck on "This session is archived" after visiting an archived session, even on an active, usable input. Mutating the extension's string option at runtime does not repaint (Tiptap snapshots a string placeholder at mount). A function placeholder, however, is re-invoked on every decoration pass, so: - extensions: `placeholder` option now accepts `string | (() => string)`. - content-editor: pass a getter over a live `placeholderRef`; a new sync effect updates the ref and dispatches an empty transaction (docChanged=false, no onUpdate loop) to force a decoration recompute — no remount required. This also fixes placeholder staleness when the session/agent archive state changes. - chat-input: update the editorKey comment (placeholder no longer relies on the agent-switch remount). - tests: getter reads the live value + one repaint on change; no repaint when the placeholder prop is unchanged. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a51ab4d551 |
feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) (#5076)
* feat(chat): Chat V2 — first-class IM-style Chat tab (MUL-4171) Replace the floating chat FAB/window with a first-class Chat tab under Inbox, laid out as an IM-style two-pane surface (thread list + conversation). Highlights: - New Chat page (packages/views/chat/chat-page.tsx) with URL-addressable session selection; web + desktop routing wired up. Removes the old chat-fab / chat-window / resize-handles / context-items paths. - IM thread list: agent avatar + last-message preview + IM timestamp, red unread *count* badge (read-cursor model), presence-gated typing vs waiting. Rename lives only in the conversation header ⋯ menu (not the list hover). - Per-session conversation header (rename / view agent / delete), agent-aware empty state (avatar + name + description + starter prompts), and a deterministic clean-title derivation from the first message. - Server: read-cursor unread model (migration 145) and per-user pinned agents (migration 146, dedicated chat_pinned_agent table + handler/queries). New-agent welcome chat auto-enqueues a real agent run (LLM intro, no static template). - Design: fade the global --border token; borderless list headers on Chat/Inbox, kept (faded) on the conversation header. Verified: pnpm typecheck (all packages), go build ./..., go vet, gofmt. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): make new-agent welcome read as an agent-initiated intro (MUL-4230) The "meet your new agent" chat used to insert a fake user message ("👋 Hi! Please introduce yourself …") and have the agent reply to it, so the thread looked like the creator prompting the agent. Drop the persisted user message. Flag the auto-created session is_agent_intro (migration 147) and drive the intro run server-side: the daemon builds a proactive self-introduction prompt for such sessions (buildChatPrompt) instead of a "reply to their message" prompt. The intro stays LLM-generated; the thread now opens with the agent's own message, as if it reached out first. - migration 147: chat_session.is_agent_intro - CreateChatSession carries the flag; sendAgentWelcomeChat no longer persists/publishes a user message - daemon: ChatIntro threaded from session flag → intro prompt Co-authored-by: multica-agent <github@multica.ai> * feat(chat): Settings toggle for the floating chat window (MUL-4235) (#5080) * feat(chat): Settings toggle for the floating chat window (MUL-4235) Re-introduce the floating chat overlay on top of Chat V2 as an optional, Settings-gated surface instead of deleting it outright. - Settings → Preferences → Chat: a switch (floatingChatEnabled, persisted client preference, default ON) to show/hide the floating window. - FloatingChat wrapper owns the two gates: the preference, and the /chat route (hidden on the tab so the same activeSessionId isn't shown twice). - ChatFab + a compact ChatWindow that reuse the shared useChatController and conversation components, so activeSessionId stays in lockstep with the tab. - Restore use-chat-context-items so the overlay's @ surfaces the current issue/project (the 'current context' affordance) — the tab stays manual. - i18n (en/zh-Hans/ja/ko), store unit tests. typecheck: core/views/web/desktop green. tests: chat store 9, settings 82, chat 39 pass. Co-authored-by: multica-agent <github@multica.ai> * feat(chat): dedicated Chat settings tab, floating window opt-in (MUL-4235) Address review: give Chat its own Settings tab instead of a section inside Preferences, and default the floating window OFF (opt-in). - New Settings → Chat tab (chat-tab.tsx) under My Account; moves the floating-window toggle out of the Preferences tab. - floatingChatEnabled now defaults OFF — only an explicit enable from the Chat tab mounts the FAB/overlay. - i18n: page.tabs.chat + a top-level chat block (en/zh-Hans/ja/ko); revert the Preferences chat section and its test mock; store tests updated for the opt-in default. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop starter prompts from chat empty state (MUL-4237) (#5081) The three starter prompts (List my open tasks by priority / Summarize what I did today / Plan what to work on next) read as filler more than help, so remove them along with the now-unused returning_subtitle ("Try asking"). The empty state keeps its agent-aware header — avatar + "Chat with {name}" + optional description — and the composer stays the entry point. Locale keys dropped across en/zh-Hans/ja/ko (parity preserved). Based on the Chat V2 branch (parent MUL-4171, #5076), not main. Co-authored-by: Lambda <lambda@multica.ai> * feat(chat): pin a chat to the top of the Chat list (MUL-4240) (#5082) Builds on Chat V2 (#5076). Adds a per-conversation pin so a user can keep important chats at the top of the IM-style thread list, above the activity-sorted rest. Backend: - migration 148: chat_session.pinned_at (nullable) + partial index; the timestamp doubles as the pinned-group sort key and the boolean flag. - list queries order pinned-first, then by most-recent activity. - SetChatSessionPinned query + PATCH /api/chat/sessions/{id}/pin handler; pinning never bumps updated_at, so an unpinned chat won't jump the list. - ChatSessionResponse.pinned + chat:session_updated carries the new state. Frontend: - ChatSession.pinned; setChatSessionPinned API + useSetChatSessionPinned with optimistic re-sort; shared sortChatSessions comparator. - thread list: pin indicator on pinned rows + pin/unpin hover action; list sorted pinned-first so it stays ordered after cache patches. - realtime patch re-sorts on pin change; en/ja/ko/zh-Hans strings. Tests: SetChatSessionPinned handler test, sortChatSessions unit tests. * feat(chat): round send button, move file upload into a + menu (MUL-4250) (#5088) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): match chat list selected-item style (inset padding + rounded) (#5093) Wrap the inbox list in p-1 and give each row rounded-md/px-3 so the selected bg-accent reads as an inset rounded card — same treatment the chat thread list already uses — instead of a full-bleed, sharp-cornered highlight. Content stays 16px-inset (p-1 + px-3 == old px-4). MUL-4253 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): rename + menu upload item to "Image or files" (MUL-4250) (#5092) Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop welcome intro session repeating the same introduction (MUL-4259) The is_agent_intro flag on chat_session is persistent, so every follow-up turn on a welcome session re-selected the self-introduction prompt in buildChatPrompt and the agent kept replying with the same intro instead of answering the user. Gate resp.ChatIntro at claim time on the session still having zero human (role='user') messages via a new ChatSessionHasUserMessage query: the first, message-less server-driven turn introduces the agent; once the creator replies, later turns fall back to the normal reply prompt. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address review findings + unbreak CI (MUL-4171) - task:failed now refreshes the sessions list (invalidateSessionLists), so the thread-list preview / unread / sort stays correct after an agent failure — FailTask persists a failure chat_message but only broadcasts task:failed, mirroring the chat:done success path. - Self-heal stale chat deep links: once the sessions list has loaded and a ?session= id isn't in it (deleted / no access / never existed) with nothing in flight, clear the selection instead of rendering an editable empty chat that would POST into a nonexistent session. Freshly-created sessions are exempt (they carry optimistic messages + a pending task). - CI: add the new parameterless `chat` route to link-handler's WORKSPACE_ROUTE_SEGMENTS and to paths/consistency.test.ts (route set + expectedSegments) — keeps the two in sync, fixes the failing @multica/core test. - Fix a MUL-4235/MUL-4237 merge collision that broke @multica/views typecheck: chat-window.tsx still passed the removed `onPickPrompt` prop to EmptyState. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): self-heal dangling session in shared controller, not just ChatPage (MUL-4171) Re-review follow-up: the stale-session self-heal only lived in ChatPage, so the floating ChatWindow still entered from a persisted activeSessionId and would render an editable empty chat (then POST into a nonexistent session) when the selected session was deleted / lost access off the /chat route. - Move the self-heal into the shared useChatController so every surface (tab and floating window) drops a dangling activeSessionId once the sessions list has loaded and doesn't contain it. - Harden ensureSession: trust the current id only when it's in the loaded list or is a just-created session still awaiting the refetch; a dangling id falls through to create a fresh session instead of POSTing into a 404. - Exempt just-created sessions via an OPTIMISTIC-write signal (hasOptimisticInFlight: pending task or optimistic- message), not hasMessages — a session deleted elsewhere with real cached history stays eligible for self-heal. Add a unit test for the discriminator. Co-authored-by: multica-agent <github@multica.ai> * test(views): fix app-sidebar useWorkspacePaths mock for the new chat nav (MUL-4171) The AppSidebar personal nav gained a `chat` item, so it calls `useWorkspacePaths().chat()` at render. The app-sidebar.test.tsx mock hadn't been updated, so `p.chat` was undefined and every render threw `TypeError: p[item.key] is not a function`, failing @multica/views#test in CI. - Add `chat: () => "/acme/chat"` to the mocked useWorkspacePaths. - Route the chat-sessions query key through a mutable `chatSessions` fixture. - Add coverage for the Chat nav: renders the link, badges the summed unread_count, and hides the badge when all sessions are read — so this drift is caught next time. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): use the original floating window, not the rewritten one (MUL-4235) (#5102) Follow-up to the merged #5080, which shipped a hand-written, simplified ChatWindow and lost the original's animations / drag-resize / expand-minimize. The floating window is just a quick entry point — it should be the original UI, not a rewrite. - Restore chat-window.tsx, chat-fab.tsx, chat-resize-handles.tsx and use-chat-resize.ts verbatim from main (0-diff): motion animations, drag resize, expand/minimize and the session dropdown are back. - Restore the empty_state.returning_subtitle + starter_prompts i18n keys the original window renders (V2 had dropped them); drop the now-unused window.open_full_tooltip key the rewrite added. - Settings gating is unchanged: FloatingChat still wraps the original FAB + window, gated by floatingChatEnabled (default off) and hidden on /chat. typecheck: core/views/web/desktop green. tests: chat + settings views 126 pass. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): archive chats from the list, delete only from Archived (MUL-4263) (#5098) Restore an archive flow as the reversible sibling of delete: - Chat list hover now offers Archive (not Delete); pin/stop unchanged. - A footer entry ('Archived · N') opens an Archived view listing archived chats; hard delete lives only there (hover -> unarchive + delete, with the existing inline confirm). - Conversation header ⋯ menu mirrors this: active chats archive, archived chats unarchive/delete. Backend: PATCH /api/chat/sessions/{id}/archive flips status active<->archived (SetChatSessionArchived), broadcasts status on chat:session_updated so other tabs re-sort into the right list. SendChatMessage already refuses archived sessions, so archived chats stay read-only until unarchived. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) (#5100) * feat(chat): handle archived agents in Chat V2 list & conversation (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * refactor(chat): drop chat-list archive marker, keep conversation read-only (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> * feat(chat): apply archived-agent read-only to the floating chat window (MUL-4265) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address floating-window + archived-agent review blockers (MUL-4171) Re-review follow-up on the restored floating ChatWindow + archive flow: 1. Floating stale-session self-heal. The restored ChatWindow doesn't use the shared controller, so its ensureSession trusted any non-empty activeSessionId and there was no dangling-session cleanup — a deleted / no-access persisted session could send into a nonexistent session. Ported the same guard used for the tab: a self-heal effect that clears a dangling activeSessionId once the sessions list has loaded, and ensureSession only trusts an id that's in the list or has an in-flight optimistic write (hasOptimisticInFlight, reused from use-chat-controller). handleSend seeds the optimistic message + pending task before setActiveSession, so a freshly-created session is never mis-cleared. 2. Floating dropdown bypassed archive-first safety. Its active rows offered a hard-delete, letting the floating window destroy active chats and skip the "archive first, delete only from Archived" model. Active rows now ARCHIVE (reversible, one-click) like ChatThreadList; the floating window offers no hard-delete — unarchive/delete live only in the full Chat page's Archived view (reachable via expand). Removed the now-dead delete-confirm machinery. 3. Orphan user message on archived-agent send. SendChatMessage created the chat_message before EnqueueChatTask, which rejects an archived / runtime-less agent — a stale client would land a user message then get a 500, orphaning it. Added a preflight that checks the session agent's archived / runtime state and returns 409 before any mutation, plus a handler test asserting the send is rejected with no message persisted. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6be496c652 |
refactor(cli): make issue reorder require exactly one target flag via a cobra flag group [MUL-4222] (#5095)
`issue reorder` takes exactly one target: --top, --bottom, --before, or --after. That rule was a hand-rolled runtime count in runIssueReorder; declare it with cobra's MarkFlagsMutuallyExclusive + MarkFlagsOneRequired (extracted into registerIssueReorderFlags, shared with the tests) so cobra validates it before RunE with canonical messages and shell completion drops the sibling target flags once one is set. Keep an explicit guard for no-op target values that cobra's presence check cannot see: empty --before/--after, and --top=false / --bottom=false. Follow-up to #4110; addresses the second review item from the merge comment (the first was handled in #5072). Co-authored-by: Nick Webster <nick@nitrad.co.uk> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3790ca78e7 |
fix(desktop): run git describe without a shell so version derivation works on Windows (#5097)
#5057 restricted version derivation to `git describe --tags --match 'v[0-9]*'`, but the command was passed to `execSync` as a shell string. On Windows the shell is cmd.exe, which does not strip the POSIX single quotes around 'v[0-9]*', so git received the quotes literally, matched no tag, fell through to `--always`, and the version degraded to the `0.0.0-g<hash>` fallback. That is what shipped a `0.0.0-gc05b67ae4` Windows Desktop build (electron-builder `--publish always` then auto-created a bogus release) during the v0.3.41 release, even though the tag was sitting exactly on HEAD. Linux/macOS were unaffected because /bin/sh strips the quotes. Fix: invoke git with an argv array via execFileSync in every version-derivation path, so the match pattern reaches git as one literal argument regardless of platform: - apps/desktop/scripts/package.mjs (Desktop version → electron-builder) - apps/desktop/scripts/bundle-cli.mjs (bundled CLI ldflags version) - apps/desktop/src/main/app-version.ts (dev-mode version fallback) The Makefile is intentionally left as-is: make's `$(shell ...)` always runs via /bin/sh (even on Windows) and the CLI release runs on Linux, so its single quotes are stripped correctly. Tests: export `deriveVersion` and `DESCRIBE_ARGS` and add coverage that runs the real `git describe` against throwaway repos (clean semver tag, semver tag chosen over a nearer non-semver tag, and the no-tag fallback), plus a structural check that the match pattern is a bare argv token with no embedded quotes. The prior suite only unit-tested the `normalizeGitVersion` string transform, which is why this slipped through. MUL-4256 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c05b67ae4a |
docs(changelog): add v0.3.41 release notes (en/zh/ja/ko) (#5094)
Co-authored-by: multica-agent <github@multica.ai>v0.3.41 |
||
|
|
bcb111dbd9 |
fix(runtimes): machine-level rename + fix picker search copy (MUL-4217) (#5087)
* fix(runtimes): make rename a machine action + fix picker search copy (MUL-4217)
Follow-up to the runtime-naming feature based on testing feedback:
1. The create-agent runtime picker filters by MACHINE (its search matches the
machine title / host / provider names), but the placeholder said "Search
runtimes", which misled users into typing a runtime name. Change the
placeholder and the empty-state to "Search machines" / "No matching
machines" (all 4 locales).
2. Rename was framed as "rename this runtime" with an opt-in "apply to whole
machine" checkbox, but the intent is naming the machine (the computer), not
an individual runtime. Rework it into a machine-level action:
- New RenameMachineDialog always names the whole machine (apply_to_machine);
the per-runtime dialog + checkbox are gone.
- The entry now lives on the Runtimes page, on the selected machine's header
(a pencil next to the machine title), owner/admin gated — that's where the
user looked for it. Removed the per-runtime pencil from the runtime detail
page.
No backend change — apply_to_machine and machine-name inheritance already exist.
Verified: pnpm typecheck (all apps), full views suite (1650) + locale parity,
lint (0 errors).
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): always show machine header in picker; pre-fill rename with shared name only
Testing + review follow-ups on #5087:
1. The create-agent picker hid the machine group header when there was only
one machine (e.g. after a search narrowed to one), collapsing to a flat
list. Always render the machine header so grouping stays consistent.
2. (Elon) RenameMachineDialog pre-filled from the first non-empty custom_name
on the machine, but the machine title only uses a name when ALL runtimes
share one (sharedCustomName). A lone per-runtime name would thus pre-fill as
if it were the machine name — the same runtime-vs-machine confusion this PR
set out to remove. Export sharedCustomName and use it for the pre-fill;
otherwise pre-fill empty. Added direct unit tests.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
875eb55109 |
fix(chat): friendlier failure message when agent runtime errors (MUL-4249) (#5085)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fd3216fd6b |
feat(lark): let an agent's owner bind/manage its Lark bot (MUL-4213) (#5079)
* feat(lark): let an agent's owner bind/manage its Lark bot (MUL-4213) Scan-to-bind was authorized by workspace role only, so a non-admin member could not bind a Lark bot even to an agent they own. Authorize the device-flow install, status poll, and revoke by the same rule that governs every other agent-management op — canManageAgent: the agent's owner OR a workspace owner/admin. Backend: - router: begin/status/revoke drop to workspace-member level; the per-agent check moves into the handlers (agent_id is a query param / installation id, which the role middleware can't see). - BeginLarkInstall + RevokeLarkInstallation load the target agent and run canManageAgent. - GetLarkInstallStatus scopes the read to the session initiator or a workspace owner/admin; others get 404 (no existence leak). Session state now carries InitiatorID for this. Frontend: - LarkAgentBindButton takes agentOwnerId and lets the agent owner through (mirrors canEditAgent). - Agent Integrations tab gates Lark per-agent (owner or admin) while Slack stays workspace-admin-only, since its routes are unchanged. Tests: begin/status/revoke authorization (owner, agent owner, unrelated member) on the backend; agent-owner bind visibility on the frontend. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): keep orphan installation revoke available to workspace admins (MUL-4213) RevokeLarkInstallation loaded the bound agent and ran canManageAgent unconditionally, so once the agent was hard-deleted the load 404'd and a workspace owner/admin could no longer disconnect the orphan Lark installation — a documented cleanup path (ListByWorkspace lists orphans; the active-connection query filters them; Settings surfaces "Unknown Agent" Disconnect). Fall back to workspace owner/admin-only revoke when GetAgentInWorkspace finds no agent; agents that still exist keep the owner-OR-admin canManageAgent check. A plain member gains no orphan-row cleanup rights. No FK/cascade — resolved in the application layer. Adds a backend regression test: orphan installation is revocable by a workspace owner but not a plain member. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
947a54b674 |
docs(agent): align state guidance with sweep (#5083)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fd813f2569 |
fix(lark): isolate topic-group sessions by thread (#5061)
* fix(lark): isolate topic-group sessions by thread A Feishu topic group (话题群) collapsed every topic into one chat_session: the session binder passed the raw group chat id as the engine BindingKey, violating the engine.EnsureSessionInput contract that a threaded platform must never key sessions by raw chat id. Multiple users @-mentioning the bot in different topics shared one transcript, and replies all landed in whichever topic wrote last_thread_id last. Adopt the Slack channel:threadRoot model: a message inside a topic (thread_id present) keys the session by "chat:thread" and persists the real chat id in the binding config (larkBindingConfig); outbound paths (chat reply, error card) resolve the send target via outboundChatID — config first, falling back to the key for pre-topic rows, which keeps legacy bindings routing unchanged. P2p and plain (non-topic) group chats keep the raw chat id key and existing behavior. No migration: existing topic-group sessions stay as-is; new topic messages create per-topic sessions from the first @-mention onward. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(lark): remove unused CreateLarkChatSessionBinding helper The helper and its CreateChatSessionBindingParams had no callers; all chat-session bindings are created through the shared engine.EnsureSession path. Dropping the dead code removes a way to bypass the engine and create topic bindings with a hardcoded empty config. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c8cfd0a214 |
fix(desktop): restrict git describe to semver tags for version derivation (#5057)
Non-semver tags (e.g. release-train tags) could become the nearest match for `git describe --tags`, producing a version string that is not a valid semver prefix. Restrict describe to `v[0-9]*` tags across the CLI ldflags, desktop bundling, and app-version paths so the resolved version always has a `major.minor.patch` shape. |
||
|
|
fd58e13bec |
feat(runtimes): custom runtime names + searchable machine-grouped picker (MUL-4217) (#5070)
* feat(runtimes): custom runtime names + searchable machine-grouped picker
MUL-4217. Runtime names were daemon-generated ("Claude (host)") and
uneditable, so picking one at agent-create time was painful once a
workspace had many machines.
Phase 1 — create-agent RuntimePicker: add a search box (>6 runtimes) and
group options by machine (Local/Remote/Cloud, online-first, current
machine first) reusing buildRuntimeMachines/filterRuntimeMachines. Rows
show the provider under a machine header instead of a flat repeated list.
Phase 2 — custom names: new nullable agent_runtime.custom_name column,
never written by the registration/heartbeat upsert so the daemon can't
clobber it; display is coalesce(custom_name, name) via runtimeDisplayName.
PATCH /api/runtimes/:id gains custom_name (+ apply_to_machine to name every
runtime sharing a daemon_id in one action, owner-scoped for non-admins).
Rename UI on the runtime detail page; `multica runtime rename` CLI command.
Verified: go build/vet, sqlc, handler tests (incl. new custom-name single
+ machine-fanout), 1650 views + 764 core TS tests, typecheck, locale parity.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): address review — persist machine name on new registrations, keep custom_name in register response
Elon's review on #5070 (MUL-4217):
1. Machine name looked "lost" when a new provider registered on an
already-named machine — the new row landed with custom_name=null and
broke sharedCustomName. Now a fresh runtime inherits the machine's shared
custom name at register time (ListDaemonCustomNames + sharedDaemonCustomName),
so the machine title stays stable as providers come and go.
2. DaemonRegister rebuilt the response row by hand and dropped custom_name,
so register returned custom_name:null — inconsistent with list/get/update.
Both branches now carry CustomName.
Also: tighten the updateRuntime patch type to custom_name?: string (drop the
misleading `| null`, since the server treats null as "unchanged", not "clear").
Tests: register response preserves custom_name; new runtime inherits machine name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtimes): inherit machine name for failed-profile registrations too
Elon's re-review of #5070 (MUL-4217): the machine-name inheritance added
last round only covered the normal req.Runtimes path. The req.FailedProfiles
branch also upserts a daemon_id-scoped agent_runtime row (offline, profile
registration error), which shows up in the runtime list / machine grouping —
so on a named machine a failed custom-profile row landed with custom_name=NULL
and dragged the machine title back to the hostname.
Extract the inheritance into h.inheritMachineCustomName and call it from both
the normal runtime path and the failed-profile path. Add a test: named daemon
+ failed profile upsert -> the failed row's persisted custom_name is inherited.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
405d88c1dc |
feat(squad): allow members to create and manage their own squads (MUL-4223) (#5071)
Squad create/manage was gated behind workspace owner/admin, inconsistent with agents and projects which any member can create. Move squads to a creator-scoped model: any member can create a squad and becomes its creator, and manages only the squads they created; owner/admin continue to manage every squad. Backend (server/internal/handler/squad.go): - Add canManageSquad (admin/owner OR creator) and gate UpdateSquad, DeleteSquad, AddSquadMember, RemoveSquadMember, UpdateSquadMemberRole on it (member load + squad load + per-squad check, replacing requireWorkspaceRole). - CreateSquad is now member-creatable. - Add memberCanWireAgent: a non-admin may only wire agents they can @-trigger (canInvokeAgent as themselves) as squad leader (create/update) or worker (add member); admins may wire any workspace agent. Prevents a creator from smuggling an agent they cannot invoke into a squad. Frontend: - squad-detail-page: compute per-squad canManage (admin || creator) and render the inspector, members tab, instructions and archive read-only otherwise, mirroring the agent detail canEdit pattern. - squads-page: per-row actions and the actions column now key off per-squad canManage instead of workspace-admin. Squads stay visible workspace-wide (ListSquads unfiltered); creator transfer is out of scope for this iteration. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ed582e3bb |
fix(cli): reject issue list --direction when sort is position or omitted (#5072)
position (the manual board order) is always sorted ascending server-side, so --direction was silently dropped for the default/position sort. A passed -but-ignored flag is a footgun, especially in scripts. Reject the combination up front with a message that names the directional sort columns instead. MUL-4222 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
519d2aeff0 |
feat(cli): enable issue ordering via cli (issue reorder, --position, --sort/--direction) (#4110)
Closes #4109 MUL-4222 |
||
|
|
324ebf6560 |
fix(core): mark list stale after off-window status count move (#5038)
The coordinator's absent-card status-move branch shifted one unit of server total between the two status buckets and stopped there — no stale key. Under the app's staleTime: Infinity + no focus-refetch setup, explicit invalidation is the only channel that reconciles a loaded list, so an open board would show the moved count with the row permanently missing from the destination bucket's visible window (e.g. "done 61" with 60 visible rows) until an unrelated event happened to invalidate the list. Push the list key onto staleKeys after a successful moveBucketTotal. The count still moves instantly (optimistic UX unchanged); the flush timing follows the existing contract — mutations invalidate on onSettled, the WS path immediately. No mutation/WS fork, no new coordinator parameters (MUL-4182). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c56f081660 | fix(skills): preserve runtime import files (#5066) | ||
|
|
c4997af4d1 |
fix: archive autopilots on delete (#5042)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9d4283ae7a |
docs(changelog): add v0.3.40 release entry (2026-07-07) (#5033)
* docs(changelog): add v0.3.40 release entry (2026-07-07) Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): drop reverted worktree_pool feature from v0.3.40 (#5037 reverted #4986) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>v0.3.40 |
||
|
|
77a05fb731 |
Revert "feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)" (#5037)
This reverts commit
|
||
|
|
e002ee5a6b | fix(daemon): isolate agent temp dirs (#5005) | ||
|
|
4f371c5c9c |
fix: expose mcp config for supported providers (#5024)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
33bd8aeaa9 |
MUL-4134: fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot (#4997)
* fix(lark): allow rebinding a revoked Feishu bot to a different agent When a Feishu/Lark Bot is disconnected from agent A (status → revoked), the row is preserved for audit but still holds the (channel_type, config->>app_id) unique index slot. Binding the same Bot to agent B would fail with: duplicate key value violates unique constraint "idx_channel_installation_type_appid" (SQLSTATE 23505) because UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type) — a different agent_id means no conflict match, so it tries INSERT and hits the app_id unique index. Fix: before the upsert, inside the same transaction, hard-delete any revoked installation with the same app_id in the same workspace. The delete is fenced to status=revoked so an active installation can never be silently removed. If no revoked row exists the delete is a no-op (deletes zero rows, returns nil error) and the upsert proceeds normally. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(lark): preserve same-agent bindings when reconnecting a revoked Feishu bot The cleanup added in the previous commit hard-deletes every revoked channel_installation sharing the app_id in the workspace before the upsert — including the row belonging to the agent currently being (re)installed. That regresses the common "disconnect then reconnect the same bot to the same agent" flow: disconnect only flips status to 'revoked' (bindings are preserved), and UpsertChannelInstallation conflicts on (workspace_id, agent_id, channel_type), so before this the same agent's row was reactivated in place — installation_id and every channel_user_binding / channel_chat_session_binding kept. Deleting it first forces an INSERT with a fresh installation_id, orphaning every member's account link (they must re-link) and all chat-session continuity; only the installer is re-bound. Fence the delete with `agent_id <> $agent_id` so it only clears a DIFFERENT agent's revoked row (the genuine app_id-slot blocker). The same agent's revoked row is left for the upsert to reactivate losslessly. Since idx_channel_installation_type_appid is globally unique on (channel_type, app_id), at most one row ever holds a given app_id, so the excluded row is exactly the one the upsert will reuse. Adds DB-backed regression tests: same-agent revoked row preserved, different-agent revoked row deleted, active row never deleted, other workspace fenced, plus end-to-end reactivation semantics (same agent keeps installation_id + bindings; different agent gets a fresh id). Co-authored-by: multica-agent <github@multica.ai> * fix(lark): clean dependent rows when hard-deleting a rebound Feishu installation Addresses review on #4997 (MUL-4134). channel_* has no FK/cascade (MUL-3515 §4), so hard-deleting a different-agent revoked installation left application-owned rows dangling at a removed installation_id: - channel_chat_session_binding: the outbound patcher would resolve a binding, then fail loading the deleted installation — turning a clean no-op into error logs. - channel_binding_token: a still-unexpired bind link (15 min TTL) could be redeemed into the deleted installation, reporting "bound" against a bot that no longer reaches the user. - channel_inbound_audit: dangling installation_id, where migration 124 models the old ON DELETE SET NULL as an app-layer NULL. - channel_user_binding: dead member links (a different agent is a distinct connection; links do not follow and can never be reused). Rework RemoveRevokedInstallationByAppID to resolve the single row holding the app_id and act only when it is revoked, in this workspace, and owned by another agent; then, on the caller's transaction, clear chat-session bindings, pending binding tokens and member links, NULL the audit references, and finally delete the row via the fenced query (defense in depth). Same-agent reconnect and active/other-workspace rows are no-ops. Adds DeleteChannelUserBindingsByInstallation, DeleteChannelBindingTokensByInstallation, and NullChannelInboundAuditInstallationID queries, plus a DB-backed test (TestChannelStore_RebindCleansDependentRows) asserting every dependent is cleaned and the audit row survives detached. Verified the test fails when the cleanup is skipped. Co-authored-by: multica-agent <github@multica.ai> * fix(lark): make the rebind cleanup race-safe with a guarded delete gate Addresses the concurrency must-fix on #4997 (MUL-4134). The prior shape read the candidate installation, checked revoked/workspace/agent in Go, cleaned the dependent rows, then ran the fenced delete. That read-then- clean-then-delete order has a TOCTOU: while B is rebinding the bot to a different agent, A can reconnect to the SAME agent and reactivate the row to 'active' in between. B still wipes A's user/chat/token bindings and NULLs its audit based on the stale "it was revoked" read, then the fenced delete no-ops (status is no longer revoked) — so A's installation survives active but its bindings are gone. Concurrent same-agent data loss, reintroduced. Make the guarded DELETE the atomic gate. DeleteChannelInstallationByAppID becomes DeleteRevokedChannelInstallationByAppID `:one ... RETURNING id`, and RemoveRevokedInstallationByAppID keys all dependent cleanup off the id the delete actually claimed. No separate read. Under READ COMMITTED a concurrent reactivation makes the DELETE re-check status='revoked' against the live row (EvalPlanQual): it claims nothing, returns pgx.ErrNoRows, and no dependents are touched. With no FK the cleanup can follow the claiming delete in the same transaction; any failure rolls the whole thing back. Adds TestChannelStore_RebindGuardedDeleteRaceWithReactivation: two real transactions race on one revoked installation — one reactivates and holds the row lock, the other runs the rebind cleanup and blocks on the guarded delete — asserting the installation and every binding stay intact. Verified this test fails on the old read-then-clean-then-delete shape and passes (also under -race) on the gated version. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: jiangliangyou <jiangliangyou@xiaomi.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <j@multica.ai> |
||
|
|
b6adf23f91 |
feat(api): emit Content-Length header on JSON responses (#5021)
The core writeJSON helpers streamed the body via json.NewEncoder(w).Encode after WriteHeader, which forces net/http into chunked transfer encoding and omits Content-Length. Buffer the marshaled body first, set an accurate Content-Length, then write — so API (and health) JSON responses advertise their exact size. writeMeasuredJSON gets the same header. Adds a test asserting the header matches the on-wire body length. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
074efeec57 |
fix(web): stop login arrival effect from racing fresh form logins (#5009) (#5019)
The /login page's already-authenticated effect fired on any user change, including the one verifyCode writes mid form-login while handleVerify is still fetching the workspace list. It then read the cold list cache (getQueryData ?? []) and raced handleSuccess with a replace to /workspaces/new — users with existing workspaces could land on the create-workspace page after login. Two changes, both in the arrival effect: - Ownership: latch once auth settles as logged-out on this page; any user appearing afterwards came from the login form, whose handleSuccess owns post-login navigation. The effect now only serves visitors who arrived authenticated. The desktop-handoff branch stays above the latch so form logins with platform=desktop still mint the deep-link token. - Correct data: for genuine arrived-authenticated visitors, fetch the list via ensureQueryData instead of reading a possibly-cold cache, which misrouted workspace owners to /workspaces/new on fresh page loads. Regression tests verified red against the previous implementation. Fixes #5009 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7bb8076ed0 |
feat(daemon): worktree_pool mode for local_directory (MUL-3483) (#4986)
* feat(daemon): add worktree_pool mode for local_directory (MUL-3483) ## What changed Squad workflows bound to the same `local_directory` resource used to serialise on a single path mutex — a documented pain point from GitHub issue #4377. This introduces an opt-in `worktree_pool` mode on the `local_directory` project resource. When enabled, each task gets its own `git worktree add` under a daemon-managed pool root, so sibling tasks on the same base repo now run truly in parallel while `git worktree add/remove/prune` stays serialised behind a per-repo mutex. ## Shape - `local_directory.resource_ref` gains three optional fields: `mode` ("in_place" default / "worktree_pool"), `pool_root` (defaults to `<parent>/.multica-worktrees/<base>`), `max_parallel` (defaults to 4). Legacy rows are byte-identical after round-trip: the server validator strips the pool fields on the default in_place path so older clients keep behaving exactly as before. - New `WorktreePoolManager` (`server/internal/daemon/worktree_pool.go`) owns pool allocation, per-repo git-metadata mutex, and cleanup. - `acquireLocalDirectoryLockIfNeeded` now branches on the ref's mode. in_place stays on `LocalPathLocker` and the shared tree; worktree_pool routes through the pool manager, publishes a lease keyed by task ID, and pins the agent to the freshly allocated worktree in `execenv.PrepareParams.LocalWorkDir`. - Pool saturation is a structured wait_reason (`worktree_pool saturated (N/M) on <path> (holders: ...)`), retrying on the existing cancel-poll interval — same UX as the historical path-mutex wait. ## Safety guardrails (also known footguns from prior art) - Repos with initialised submodules are refused up front. Multi-checkout of a superproject is explicitly unsupported by `git worktree(1)` BUGS and the per-worktree `modules/` directories bloat disk by pool size ×. - Dirty worktrees are NEVER `--force` removed on release. If the agent left uncommitted changes behind we keep the directory (and free the slot) so users can inspect. This is the failure mode claude-code#55724 documented and the pool must not regress into. - The per-repo mutex covers every `git worktree add/remove/prune` and `submodule status` invocation for a given base, matching the in-process-queue fix Anthropic settled on for claude-code#34645 (`.git/config.lock` races on concurrent add). - Task UUID is the source of truth for both branch (`multica/<uuid>`) and worktree path (`<pool_root>/<uuid>`) so a single agent running multiple worker tasks in parallel can never collide. - Non-empty leftover directories at the target path abort the allocation instead of silently starting the agent in an unknown state. ## Explicit MVP non-goals (deferred, tracked as follow-up work) - Windows worktree-remove retry (permission-denied on locked handles). - Detached-HEAD fast path for read-only exploration tasks. - `post-checkout` hook opt-out / serialisation. - Automatic `git lfs install`. - UI surfacing of the pool state / dirty worktree list. ## Tests - `worktree_pool_test.go` (new): full acquire→release lifecycle, parallel allocation, saturation with holder list, slot re-use after release, dirty-worktree preservation, concurrent-acquire serialisation (the config.lock guard), submodule refusal, missing base rejection, pool root auto-mkdir, non-empty leftover refusal, ctx cancel. - Handler validator gains three rejection cases (unknown mode, relative pool_root, negative max_parallel) and a round-trip test that pins the normalised JSON shape for both modes. - Daemon `localDirectoryRef` helpers get a defaults test and the pool root path derivation is pinned. ## Wire-compat and rollout - Default off. Existing rows keep the historical shape (no `mode`, `pool_root`, or `max_parallel` in the JSON) and behave exactly as before. - Opt-in via `--ref '{"local_path":"...","daemon_id":"...","mode":"worktree_pool"}'` today. CLI flag shortcuts (`--mode`, `--pool-root`, `--max-parallel`) can follow in a small tail PR — not blocking. - No DB migration. No UI change required. Co-authored-by: multica-agent <github@multica.ai> * feat(daemon): address worktree_pool review nits (MUL-3483) Follow-up to #4986. Three non-blocking review points from GPT-Boy: 1. **Daemon integration test for lease → runTask plumbing.** `TestAcquireLocalDirectory_WorktreePoolPublishesLease` (and its in_place counterpart) pin the exact contract runTask relies on when it reads `d.localLeases.Load(task.ID)` and feeds `lease.WorkDir` into `execenv.PrepareParams.LocalWorkDir`. A future refactor that drops the Store, mistypes the key, or swaps back to `assignment.AbsPath` on the pool branch will now fail here rather than silently defeat the whole point of worktree_pool mode. 2. **Untracked-only dirty case now classifies as dirty.** `worktreeIsDirty` used `--untracked-files=no`, which meant a worktree with only untracked files was reported "clean" and hit the `git worktree remove` branch — git itself would then refuse the removal because the file exists (so no data was lost), but the log path lied about what happened on disk. Switching to `--untracked-files=normal` routes agents' fresh drafts directly through the "leaving on disk for user inspection" branch, and `TestWorktreePool_UntrackedOnlyIsKept` pins the guarantee so nobody quietly reverts the flag later. 3. **Skill doc note on default `pool_root` location.** `multica-projects-and-resources/SKILL.md` now spells out the three new ref fields (`mode`, `pool_root`, `max_parallel`), the default `<parent>/.multica-worktrees/<repo>` location (next to the repo, not inside it), the write-permission requirement on the parent directory, and the submodule restriction — so agents advising self-host users hit the right doc line rather than reading source. Existing test suite still green: - `go vet ./...` clean - `go test ./internal/daemon/... ./internal/handler/... ./internal/service/...` all pass Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
93aa959239 |
fix(workspace): await delete before navigating; suppress self-initiated realtime relocate (MUL-4129) (#4983)
* fix(workspace): drop workspace from list cache while delete is pending (MUL-4129) The delete-workspace flow navigates away before awaiting the DELETE (required ordering — see navigateAwayFromCurrentWorkspace's CancelledError notes), but useDeleteWorkspace left the workspace in the list cache until onSettled. During the pending window any list refetch re-presented the deleting workspace as a selectable/current option. Optimistically remove it in onMutate (after cancelling in-flight list fetches), roll the snapshot back in onError so a failed delete restores the workspace alongside the existing error toast, and keep the onSettled invalidate as the server-truth reconcile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(workspace): own storage cleanup on delete success and tombstone pending deletes (MUL-4129) Address final-review blockers on #4980: 1. The realtime workspace:deleted handler reverse-looks-up the slug from the list cache to clear the ${key}:${slug} persisted namespace; the optimistic removal empties that row on the initiating client, so the lookup misses and cleanup was silently skipped. Capture the slug in onMutate before removal and clear storage in onSuccess only — a failed DELETE rolls back and must not touch persisted state. 2. cancelQueries only covered fetches already in flight at onMutate. Add a pending-delete tombstone (marked onMutate, lifted onSettled before the reconcile invalidate) filtered inside workspaceListOptions' queryFn, so invalidation/reconnect/fetchQuery refetches that land mid-pending cannot write the not-yet-committed row back into cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs(claude-md): scope optimistic updates to same-screen field patches Replace the blanket "mutations optimistic by default" state rule with three scoped rules: optimistic only for predictable same-screen field patches; navigating/confirming flows (create/delete/leave) await the server first; chat send uses the pending-message pattern. Aligned with TanStack Query maintainer guidance and React Router's pending-UI criteria; the old blanket rule is what steered the original MUL-4129 fix toward optimistic entity removal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(workspace): await delete before navigating; drop optimistic removal (MUL-4129) Rework of the previous approach on this branch. The optimistic removal emptied the workspace list cache at click time, while the settings page was still mounted on the old slug — useWorkspaceId (URL slug + list lookup) then threw 'no workspace selected'. Root cause of the original navigate-first ordering was dual ownership of delete handling between the initiating flow and the realtime workspace:deleted handler. - useDeleteWorkspace: no optimistic removal, no rollback; onMutate only marks the delete self-initiated and captures the slug; onSuccess owns storage cleanup; onSettled invalidates. - pending-delete.ts: repurposed from tombstone filter to self-initiated marker; kept on success (suppresses the WS echo), lifted on failure. - use-realtime-sync: workspace:deleted no-ops for self-initiated deletes; it now only serves deletes initiated elsewhere. - workspace-tab: confirm dialog stays open in loading state, navigate only after the DELETE succeeds; failure leaves the user in place with nothing to roll back. Replace throwing useWorkspaceId with workspace?.id + enabled gating (independent crash on external deletes of the current workspace). Known debt: useLeaveWorkspace still navigates before awaiting (member:removed has no self-initiated marker yet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
566d51f1c0 |
perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) (#5018)
* perf(chat): fix pending-task index mismatch + cut aggregate request storm (MUL-4159) ListPendingChatTasksByCreator was a top DB hotspot. Root cause: the partial index idx_agent_task_queue_chat_pending (migration 040) only covers status IN (queued, dispatched, running), but migration 109 added a fourth in-flight status (waiting_local_directory) that both pending chat queries now filter on. Postgres can only use a partial index when the query predicate is a subset of the index predicate, so the 4-status query stopped using it and degraded to a Seq Scan over the whole agent_task_queue. Implements the reviewed P0-P3 plan in one PR: P0 index fix (split single-statement CONCURRENTLY migrations per repo convention) - 143: CREATE INDEX CONCURRENTLY idx_agent_task_queue_chat_pending_v2 covering all four in-flight statuses (same column list, so GetPendingChatTask still benefits). - 144: DROP the superseded 3-status index, in its own migration. P1 SQL + handler hot path - ListPendingChatTasksByCreator now returns cs.agent_id and states chat_session_id IS NOT NULL so the planner can prove the partial-index subset. - ListPendingChatTasks filters private-agent access against the already-loaded accessible-agent set using the returned agent_id, dropping the extra ListAllChatSessionsByCreator scan on the hot path. - Regenerated sqlc. P2 frontend request amplification - FAB uses the new boolean has-any query gated on enabled:!isOpen, so the minimised button never holds the full aggregate. - use-realtime-sync maintains the pending aggregate (list + has-any) in place from task lifecycle events (queued/dispatch/running/waiting_local_directory -> upsert; completed/failed/cancelled -> remove) instead of invalidating on every chat:message/chat:done, with a debounced fallback invalidate for reconnect / unknown payloads. P3 boolean endpoint - GET /api/chat/pending-tasks/has-any backed by HasPendingChatTasksByCreator (EXISTS). Permission filtering is baked in via agent_id = ANY($3); an empty accessible-agent set short-circuits to false. The detailed list stays for the ChatWindow history / stop-task flows. Tests: new handler tests cover the private-agent gate on both endpoints (hidden from a creator who lost access, visible to the agent owner) plus the boolean status/terminal semantics. EXPLAIN (ANALYZE, BUFFERS) on a 300k-row reproduction: - before (3-status index): Parallel Seq Scan, ~300k rows filtered, shared hit=3012, 12.1 ms. - after (v2 index): Index Scan on idx_agent_task_queue_chat_pending_v2, shared hit=131, 0.07 ms. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): stop optimistic cross-session pending aggregate writes from workspace-fanout task events (MUL-4159) Review on PR #5018 flagged a real privilege-escalation bug in the P2 change: use-realtime-sync optimistically upserted the cross-session pending aggregate (pendingTasks / pendingTasksHasAny) from chat task:* events. Those events are a workspace fanout delivered to every member (server still BroadcastToWorkspace, see cmd/server/listeners.go), and the payload carries no creator / agent visibility. So member B starting a chat task could flip member A's FAB to has_pending=true, bypassing the server-side permission filter on /api/chat/pending-tasks[/has-any]. Fix (option 1 from the review — the self-contained one): never optimistically write the aggregate from task:* events. On every task lifecycle transition, debounced-invalidate the aggregate so it is refetched through the permission-filtering endpoint, which only returns the caller's own creator-owned, accessible-agent tasks. The per-session pendingTask cache is still written directly — it is keyed by chat_session_id and only rendered for sessions the user can open (server-gated), so it is not a cross-user leak. chat:message is still excluded from aggregate refresh, so the MUL-4159 request storm stays fixed; task transitions are per-task and coalesced by the debounce. - Removed upsertPendingAggregate / removePendingAggregate. - Added exported refetchPendingChatAggregate(qc, wsId) — an invalidate, never a setQueryData — used by the debounced handler. - Regression tests: refetchPendingChatAggregate leaves the cached has_pending/list untouched (no optimistic write) and only invalidates for an authoritative server-filtered refetch; no-ops without a workspace id. Verified: @multica/core + @multica/views typecheck; full core vitest suite (752 tests) green including the 2 new guard tests. Co-authored-by: multica-agent <github@multica.ai> * chore(chat): address review nits on pending-tasks endpoints (MUL-4159) - Restore the GetPendingChatTask godoc first line that was clipped when the has-any handler was inserted (nit#1). - ListPendingChatTasks short-circuits to an empty list when the caller has no accessible agents, mirroring HasPendingChatTasks — skips the DB round-trip (nit#2). - Add a cross-creator negative test: user A's in-flight task on a workspace-visible agent returns has_pending=false / empty list for user B, locking the cs.creator_id tenant gate that the agent-visibility filter does not cover (nit#3). Verified: go build ./... and go test ./internal/handler -run PendingChatTasks (7 tests) green against live Postgres. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2747416380 |
MUL-4117: feat(cli): add workspace member invite command (#5017)
Closes #4967 |
||
|
|
30d3aca600 |
feat(attachments): support HTTP Range resume on proxy download (MUL-3962)
Add HTTP Range support to the attachment proxy-download path so an interrupted download can resume from where it left off instead of restarting at byte 0. - Seekable backends (local disk) delegate to http.ServeContent for full Range / If-Range / 206 / Content-Range / 416 handling. - Forward-only backends (S3/MinIO streaming) get a single-range fallback that advertises Accept-Ranges and serves 206 + Content-Range, with a rangeParseOutcome that returns 416 only for genuinely unsatisfiable byte ranges and otherwise ignores unsupported/empty-object ranges (full 200), matching the seekable path. Closes #4831. MUL-3962. |
||
|
|
e36c0cd404 |
fix: preflight Claude root/sudo launches with an actionable error (#4944)
Detect the root/sudo + bypassPermissions launch condition before starting Claude Code and fail fast with an actionable error (run as non-root, or set IS_SANDBOX=1 in a genuine container/sandbox). Closes #3278 MUL-4095 |
||
|
|
1de0c7d14c |
MUL-4158: allow deleting orphaned profile runtimes
Fixes MUL-4158 |
||
|
|
bcad2edc9e |
feat(issues): add Cmd+F in-page find to issue detail (MUL-4126) (#4989)
* feat(issues): add Cmd+F in-page find to issue detail Replace the stopgap "find-in-page is virtualized" toast with a real find bar (MUL-4126). Cmd/Ctrl+F opens a floating bar with keyword input, live match count, and prev/next navigation that scrolls to and highlights each match. - Opening find force-renders the comment timeline flat (reusing the existing highlightCommentId escape hatch) so off-screen comments become searchable — the root cause of the original complaint. - Matches are painted with the CSS Custom Highlight API (ranges only, no DOM mutation), so highlighting layers cleanly over React-rendered markdown and the contenteditable title/description editors. - Scroll-to-match drives container.scrollTop directly (never native scrollIntoView; #3929). Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep in-page find usable without CSS Custom Highlight API On browsers lacking the CSS Custom Highlight API, `!supported` was folded into the match-collection path, so Cmd/Ctrl+F opened the bar and swallowed native find but reported 0 matches and could not navigate — strictly worse than the native find it replaced (MUL-4126 review). Feature-guard only the paint calls now: match collection, count, active index, and scroll-to-match run regardless of support, while `CSS.highlights.set/delete` / `new Highlight` stay behind the guard. The MutationObserver re-derives ranges even when unsupported so fallback counting/navigation track live DOM churn. Adds a hook test that drives the degraded path (jsdom has no highlight API) and asserts counting + prev/next still work. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3cb5dc3ad6 |
chore(analytics): retire redundant PostHog tracking (MUL-4127) (#4996)
* chore(analytics): retire redundant PostHog tracking (MUL-4127) PostHog had become a chaotic, largely-unused second copy of data we already query from the DB and Grafana. Remove the redundant instrumentation. Server: every product event (signup, workspace_created, issue_created, issue_executed, chat_message_sent, team_invite_*, onboarding_*, agent_created, cloud_waitlist_joined, feedback_submitted, contact_sales_submitted, squad_created, autopilot_created) is now in metricsOnlyEvents, so metrics.RecordEvent still increments the Prometheus/Grafana counter but no longer ships to PostHog. DB rows remain the source of truth. Runtime/autopilot/ agent_task lifecycle were already Prometheus-only. Frontend: delete the PostHog-only funnel instrumentation — $pageview (+ web and desktop trackers), download_intent_expressed/page_viewed/initiated, the onboarding_started mirror, onboarding_runtime_path_selected/detected, feedback_opened, and source_backfill_*. The source-backfill modal itself stays (it PATCHes the questionnaire to the DB). Kept on PostHog (frontend only): $exception autocapture and the client_crash / client_unresponsive stability telemetry (no DB equivalent), plus $identify/$set. captureSignupSource (attribution cookie) stays — it still feeds the signup_source Prometheus label. Verified: pnpm typecheck, pnpm lint (0 errors), vitest (core/views/web/desktop), go test ./internal/analytics/... ./internal/metrics/... Co-authored-by: multica-agent <github@multica.ai> * docs(analytics): fix stale PostHog references after MUL-4127 (review follow-up) Addresses review of #4996 — three spots still described server events as active PostHog signals after they became metrics-only: - docs/analytics.md: issue_executed is no longer a PostHog success signal; it is Prometheus-only (multica_issue_executed_total) + issue.first_executed_at, in both the event contract and the Reconciliation section. - docs/analytics.md: the signup $set_once person properties (email, signup_source) are no longer emitted — signup is Prometheus-only; only the bucketed signup_source survives as the multica_signup_total label. - server/internal/metrics/business_events.go: RecordEvent doc comment no longer claims it ships product events to PostHog / "PostHog is reserved for user/product-behaviour events" — every server event is now metrics-only. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
380a2b8cb8 |
fix(chat): default chat window to closed on workspace open (#5003)
Opening a workspace no longer pops the chat window for users who have never toggled it — the FAB keeps chat discoverable. An explicit stored open/closed preference is still honoured on reload. Closes MUL-4149 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
466fe2ca0b |
docs(readme): sync runtime list, drop retired Gemini (#4990)
* docs(readme): sync runtime list, drop retired Gemini Gemini CLI was removed from the runtime whitelist (agent.SupportedTypes) back in v0.3.29; Google also officially retired Gemini CLI on 2026-06-18 in favor of Antigravity CLI. Both README.md and README.zh-CN.md still advertised Gemini and had drifted from the canonical list. Sync every runtime/provider list in both READMEs to the 14 entries in server/pkg/agent/agent.go: remove Gemini, add the missing CodeBuddy, Antigravity and Trae CLI, and align ordering. * docs(readme): use consistent "Trae CLI" naming Align the intro and "Create an agent" provider lists with the architecture diagram and runtime table, which already say "Trae CLI". MUL-4145 --------- Co-authored-by: Bohan Jiang <bhjiang@outlook.com>v0.3.39 |
||
|
|
4c510dfef6 |
fix(daemon): harden background-task-safety brief against background-and-yield (MUL-4140) (#4998)
A Multica-managed run goes terminal the moment the top-level turn exits; there is no "background work finishes later and wakes you up" step. When an agent starts background work (a run_in_background shell, a Monitor, an async subagent) and ends its turn to "wait for a completion notification", the work is orphaned and the result comment it meant to post is never sent (MUL-4091 / PR #4970). The existing claude-only protocol guard forces run_in_background tool inputs to foreground and fails loud on async_launched tool results, but it cannot catch the actual MUL-4091 mechanism: a turn that ends cleanly with a "Standing by, I'll report when CI finishes" message. That shape is only addressable behaviorally, and it is harness-agnostic. Harden the Background Task Safety brief (both the legacy/verbose production path and the slim staging path) with explicit hard pins: - never background-and-yield / expect a future wakeup that does not exist here; - do every wait synchronously in a single foreground call (e.g. gh run watch); - the standalone-harness "running in the background, keep working" hint does not apply in Multica-managed runs; - never end a turn with a "standing by" / "I'll report back" sign-off. Add verbose- and slim-path test coverage for the new pins so a future brief trim cannot silently drop them. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
39ccb7d342 |
fix(server): do not cancel issue tasks on assignee change (MUL-4113, #4963) (#4975)
* fix(server): don't cancel issue tasks on assignee change (#4963) Changing an issue's assignee previously called CancelTasksForIssue, which cancels every active task on the issue by issue_id alone — regardless of which agent owns the task or how it was triggered. In a multi-agent workspace this silently dropped unrelated in-flight work (a mention-triggered run for another agent, a squad task) with no requeue, and it self-cancelled a run that reassigned the issue from inside its own turn (the daemon then interrupted the live run before its post-handoff cleanup could finish). Reassignment now cancels nothing: ownership handoff no longer implies interruption. The new assignee's run, if any, is still enqueued by WillEnqueueRun and runs alongside whatever was already in flight. Explicit terminal actions — issue -> cancelled and delete issue — still cancel active tasks, unchanged. Applies to both UpdateIssue and BatchUpdateIssues. Adds handler tests that fail against the old behavior (both the previous assignee's own run and an unrelated agent's run got cancelled) and pass now. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> * test(server): cover agent→agent reassign; fix stale WillEnqueueRun comment Addresses review nits on #4975 (MUL-4113, #4963): - Rewrite the outdated WillEnqueueRun doc comment. The assign source no longer cancels existing tasks, so the old "assign cancels existing tasks before enqueuing, pending task moot" premise is wrong. Describe the real invariant instead: the write is guarded by the (issue_id, agent_id) partial unique index, only the status source needs the pending-task dedup, and the assign source safely skips it. - Add a handler test for the core agent→agent handoff path. The existing no-cancel tests only reassigned to a member; this one reassigns from one agent to another and asserts both effects independently: the previous agent's running task survives (no collateral cancel) and the new assignee still gets exactly one run enqueued. MUL-4113 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
75e8bd5b64 |
fix: make release index migrations concurrent (#4995)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
12d901638e |
fix(desktop): resolve electron-vite bin via PATH in dev script (#4992)
Under the hoisted linker (node-linker=hoisted) electron-vite's bin only lands in the repo-root node_modules/.bin, so the hardcoded apps/desktop/node_modules/.bin path fails. Use envWithLocalBins to put both .bin directories on PATH and invoke electron-vite by name. |
||
|
|
7183ec9b6d |
feat: add agent list search (#4988)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |