mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
fix/transcript-virtual-scroll
652 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6c6143e8fc |
fix(agents): always enable skill toggles (MUL-4520) (#5381)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ac62f72c2a |
MUL-4480: make daemon workspace sync event-driven (#5354)
* feat(daemon): make workspace sync event-driven Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): preserve trailing workspace changes Co-authored-by: multica-agent <github@multica.ai> * fix(workspace): reconcile failed creates Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47f9c5813f |
fix(chat): reland archived-session unread + mount-race auto-read (MUL-4360) (#5333)
* fix(chat): correct unread — archived sessions + mount-race auto-read (MUL-4360) Reland of #5315, which was reverted by #5332 as collateral in an unrelated release-wide revert (to unwind 162/163 migration BLOCK from other PRs), not for any defect in this code — Howard/Preflight assessed these changes WARN / non-blocking. Restores all three fixes verbatim off current main: - backend: ListAllChatSessionsByCreator derives unread_count=0 / has_unread=false for status='archived' rows via a CASE gate. last_read_at is untouched, so unarchiving restores the true unread state. Single source of truth for every unread surface (quick-chat FAB, sidebar Chat tab, chat-window header, mobile); installed desktop clients benefit with no app update. - frontend: the archive mutation onMutate optimistically zeroes the row's unread so no badge counts a just-archived session in the frame before the refetch lands. Unarchive does not fabricate a count — the true state returns from the server refetch. - frontend: auto-mark-read is deferred a tick and cancelled on cleanup, so a session that is only momentarily active on mount (persisted activeSessionId restored for one frame, then cleared by the URL->store effect) is not marked read; only a session that stays active past the tick is. Verification: sqlc regenerate produces no drift; go test ./internal/handler -run 'TestListChatSessions_ArchivedSessionReportsZeroUnread|TestSetChatSessionArchived_ClearsChannelBinding' passes against a real Postgres; vitest mutations.test.tsx (3) and use-chat-controller.test.tsx (8) pass; core + views typecheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): converge archived unread on chat:session_updated realtime event (MUL-4360) Howard's #5333 review found a real cross-tab gap: the chat:session_updated handler patched status but never unread, and chatSessionsOptions is staleTime: Infinity, so a session archived in one tab kept its unread badge lit in another tab/device forever — the same stuck-badge bug, one surface over. Extract the inline handler into applyChatSessionUpdatedToCache and force unread_count=0 / has_unread=false when payload.status === "archived", mirroring the archive mutation's optimistic patch and the backend deriving unread=0 for archived rows. Unarchive does NOT fabricate a count — the true unread returns from the server refetch (last_read_at untouched). No sessions-list invalidation; minimal field patch as reviewed. Adds use-realtime-sync.test.ts coverage: an archived event zeroes a cached unread row; an active event does not resurrect unread; a rename-only event leaves unread untouched. 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> |
||
|
|
411a160b99 |
fix(release): harden v0.3.44 migrations (#5345)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
47e3fdedd0 |
feat(projects): start_date / due_date UI + create-project footer aligned with create-issue (MUL-4388) (#5331)
* feat(projects): add start_date / due_date pickers to project create modal and sidebar #5313 landed the backend start_date/due_date fields + types but deliberately shipped no UI. Wire up the two editor surfaces users expect: - ProjectStartDatePicker / ProjectDueDatePicker mirror the issue pickers (same calendar-day contract, clear idiom, shared @multica/core/issues/date helpers) but are typed to UpdateProjectRequest and scoped to the "projects" i18n namespace. One component serves both surfaces via a custom trigger. - Create-project modal: two date pills; values flow into the create payload and the persisted draft (draft-store gains startDate/dueDate). - Project sidebar (project-detail): two PropRows after Lead, wired to the update mutation, with clear support (send null). - i18n: prop_start_date / prop_due_date / clear_date across en/zh-Hans/ja/ko, reusing the existing issue date wording. Tests: picker display + clear behavior (real popover), and the create modal renders both pills. typecheck + lint + i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(projects): align create-project footer with create-issue Restructure the create-project modal footer to match the create-issue pattern (per design feedback): the primary action moves out of the cramped single justify-between row into its own border-t action strip, and the property pills sit in a dedicated wrapping toolbar above it. Low-frequency fields (start/due date) collapse into a ⋯ overflow via progressive disclosure — a pill only renders inline once its date is set or the user opens it from the menu — so the default toolbar stays a clean single row (Status · Priority · Lead · Repos · ⋯). - Use the shared PillButton (../common/pill-button) instead of the modal-local copy, gaining the data-popup-open styling create-issue uses. - ProjectStartDatePicker / ProjectDueDatePicker gain controlled open props so the overflow menu can reveal + open them (mirrors the issue pickers). - i18n: create_project.set_start_date / set_due_date / more_options_aria across en / zh-Hans / ja / ko, reusing the create-issue wording. Test updated to assert the dates are revealed from the overflow rather than shown inline by default. typecheck / lint / i18n parity pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> * refactor(views): extract shared DateOnlyPicker base for date pills (Elon nit2) The issue and project start/due-date pickers were near-complete copies of the same Popover + Calendar + clear wiring, so they could drift in behaviour or formatting. Extract that into one entity-agnostic DateOnlyPicker (packages/views/common/date-only-picker.tsx); each of the four pills is now a thin wrapper supplying only its field name (via onChange), icon, overdue flag, and localized copy. -264 lines of duplication, single source of truth. Behaviour is unchanged: the issue pickers keep their full API (trigger / triggerRender / open / onOpenChange / align / defaultOpen — all still used by board-card, issue-detail, create-issue) and the calendar-day contract stays in @multica/core/issues/date. The en-US display format now lives in one place rather than being duplicated per entity. Full views test suite (1857 tests) + typecheck + lint pass. Part of #5227 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9a071b827f |
Revert "fix(chat): correct unread — archived sessions + mount-race auto-read …" (#5332)
This reverts commit
|
||
|
|
b72bd55d16 |
feat(shortcuts): make browser-reserved accelerators recordable on desktop (#5327)
* feat(shortcuts): make browser-reserved accelerators recordable on desktop Cmd/Ctrl+P (and L/T/N/D/U) were rejected by the shortcut recorder on every platform because a browser tab cannot reliably own them. The Electron renderer receives these as plain keydowns — neither Electron's default menu nor the desktop shell binds any of them — so the reservation now only applies to the web runtime. Adds a ShortcutRuntime dimension (configured by CoreProvider from the client identity, with a preload-global fallback that is already correct at module-eval time so store hydration sanitizes with the right runtime). App-owned accelerators (W/R/Q, editing keys, zoom row) stay reserved everywhere. Closes MUL-4457 Co-authored-by: multica-agent <github@multica.ai> * fix(shortcuts): limit desktop unlock to bare primary browser accelerators Review follow-up (MUL-4457): the desktop skip matched primary+key with any extra modifiers, which would also unreserve OS-owned combos such as Option+Cmd+D (macOS Dock toggle) and Ctrl+Alt+T (Linux terminal). Only the bare primary chord is now recordable on desktop; every extra-modifier variant keeps the historical reservation on both runtimes. Adds regression tests for the OS combos. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cec071dc06 |
fix(chat): correct unread — archived sessions + mount-race auto-read (MUL-4360) (#5315)
* feat(skills): search runtime local skills * feat(skills): highlight matched substrings in runtime local skill search Reuse the shared HighlightText (the same component the global search command uses) to highlight matched substrings in a result's name, provider, description, and path, so styling stays consistent across the app. Narrow the search to the fields the row actually renders and drop `key`, so every match maps to something visible. While a query is active, lift the description's 2-line clamp so a match past the first two lines stays on screen instead of being clipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): zero unread for archived chat sessions across all badges (MUL-4360) Archiving a chat session flips status but deliberately does not advance last_read_at, and ListAllChatSessionsByCreator counted unread unconditionally. So an archived session that had unread replies kept reporting has_unread=true / unread_count>0 — a stuck badge the user can never clear (archived sessions are read-only and hidden from history, so there is no mark-as-read entry). MUL-4372 fixed only the quick-chat FAB surface; the sidebar Chat tab badge and the chat-window "other unread" header still counted it. Fix at the source: derive unread_count = 0 for status='archived' rows in ListAllChatSessionsByCreator. Because has_unread is server-derived as unread_count > 0, and all surfaces (FAB, sidebar via countUnreadChatMessages, chat-window header, and mobile) read this one payload, every badge drops archived sessions with no per-surface filter. last_read_at is left untouched so unarchiving restores the true unread state. Installed desktop clients benefit without an app update. Also zero unread optimistically in the archive mutation so no badge counts a just-archived session in the frame before the refetch lands (FAB already filtered archived; this keeps sidebar/header consistent). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(chat): stop auto-mark-read from clearing a transiently-active session on mount (MUL-4360) The chat page persists `activeSessionId`, so on a bare `/chat` navigation it restores the last-open session as active for one frame before its URL→store effect (which runs AFTER useChatController's effects, since the hook is called first) clears it back to null. The auto-mark-read effect fired in that gap and marked the restored-but-never-opened session read — its unread badge vanished though the right pane still showed "select a chat" and the user never opened it. This is why the sidebar count dropped (e.g. 2 → 1) just by entering the tab. Defer the read by a tick and cancel it on cleanup: a session that is only momentarily active (restored on mount, then cleared) has its pending read cancelled when activeSessionId changes; only a session that stays active past the tick — a real select, deep link, or refresh — is marked read. A live-store re-check in the timer is a belt-and-suspenders guard. Adds the previously-missing auto-mark-read coverage: a stable-active session is read after the tick; a momentarily-active one is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: coderbaozi <YHbaozi1988@163.com> Co-authored-by: abun <103836393+coderbaozi@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
abeee82cd0 | feat: animate numeric metrics with NumberFlow (#5317) | ||
|
|
bf288349f6 |
feat(project): add start_date and due_date fields (MUL-4388) (#5313)
Projects become schedulable planning objects alongside their issues: add optional start_date / due_date, mirroring issue.start_date / issue.due_date. This is only the first slice of #5227 — labels, metadata, and the editable metadata UI are still out of scope. - migration 166: two nullable DATE columns on `project` (calendar days, no FK/index — matches the issue end-state after migration 112) - sqlc CreateProject / UpdateProject carry the dates; UpdateProject uses narg so an explicit null clears - handler: parse YYYY-MM-DD (400 on bad format), rawFields-presence clear on update, and the hand-scanned SearchProjects query returns the columns - CLI: `project create/update --start-date/--due-date` (empty clears on update) - frontend + mobile types/zod schemas: the two new schema fields are nullable().default(null) so a project from an older backend (frontend deploys before backend) parses to null instead of degrading the batch to the empty fallback; added a search schema drift test - projects skill / CLI docs Part of #5227 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0b054b5c8b |
fix(chat): clear external channel binding on archive, drop archived from FAB unread (#5214)
Archiving a channel-bound chat session now severs its channel_chat_session_binding in the same tx as the status flip. The web send path already treats status='archived' as read-only, but the channel engine (Feishu/Slack) resolves inbound traffic through the binding without checking session status, so an archived-but-still-bound session kept receiving agent replies and a stuck, uncleared unread badge. Dropping the binding makes the next inbound message fork a fresh session; unarchive does NOT recreate it (a later session may already own the channel). The FAB unread badge counted status=all sessions without excluding archived, so residual unread on an archived session (archive does not mark-read) held the badge even though the session is hidden and read-only. Extract countUnreadChatSessions() and exclude archived. This matches what mobile already computes (active-only list), restoring count parity. Tests: backend archive-clears-binding + unarchive-does-not-recreate; frontend countUnreadChatSessions archived-exclusion cases. MUL-4372 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b47e835d7d | refactor(runtimes): organize runtime management by machine (#5297) | ||
|
|
1427e8abd3 | feat(agents): add conversational creation studio (#5296) | ||
|
|
6aed9b40ee |
fix: confirm shortcut reset and refresh translations (#5295)
* fix: confirm shortcut defaults reset * fix: refresh i18n resources during hot updates |
||
|
|
80e54094a8 |
feat(issues): sticky setting for the issue comment bar (MUL-4435) (#5293)
* feat(issues): add sticky setting for the issue comment bar (MUL-4435) The bottom comment composer can now pin itself to the scroll viewport so it stays reachable while reading a long timeline. A pin toggle on the bar itself persists the preference (default: on) via a new localStorage-backed useCommentComposerStore. While pinned, the editor area is height-capped so long drafts scroll internally instead of covering the timeline. Co-authored-by: multica-agent <github@multica.ai> * refactor(settings): move the sticky comment bar toggle to Settings > Preferences (MUL-4435) Per review feedback: pinning the comment bar is a low-frequency choice, so the toggle moves off the bar itself into Settings > Preferences as a Switch row. The composer keeps reading the store for the sticky behavior and the 40vh editor cap; the pin button and its issues-locale tooltips are removed. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b64ebd60b5 | feat: add customizable keyboard shortcuts (#5294) | ||
|
|
3bd69bdaa2 |
MUL-4432: add DM button on agent detail page (#5289)
* feat(agents): add DM button on agent detail page The header gains a DM action next to Assign work. It shares the MUL-3963 invocation gate with assignment: allowed users land on the Chat tab with a fresh compose bound to the agent via a new one-shot ?agent=<id> deep link (consumed once the permission-filtered agent list resolves, then stripped); denied users get an explanatory toast instead of a hidden affordance. Closes MUL-4432 Co-authored-by: multica-agent <github@multica.ai> * fix(chat): harden the ?agent= deep link against races and undetermined permissions Review follow-up for the DM button (PR #5289): - An explicit user action (thread select, archive, manual new chat) now supersedes a still-pending ?agent= intent, so an intent deferred by slow agent/member queries can no longer fire late and clobber that choice. - The composingNew reset reads the live store value; under StrictMode's effect replay the render-captured snapshot re-closed the compose pane the intent had just opened (one-shot guard rightly refuses to re-fire). - A settled miss (revoked access, archived agent, bad id) now toasts and strips the param instead of silently keeping a re-fireable intent; still- loading queries keep the intent pending. Query errors never settle. - useAgentPermissions exposes isLoading: the detail page disables DM while membership resolves instead of toasting a false not_member deny at a legitimate member. The chat store test mock is now reactive (useSyncExternalStore) — with a plain mutable ref React bails out of committing post-effect re-renders and the StrictMode regression cannot reproduce. Both new regression tests fail against the pre-fix code. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c377d7fb4f |
feat(labels): add scoped label management (#5279)
* feat(labels): add scoped label management * fix(labels): address review feedback * fix(migrations): use unique label migration prefix |
||
|
|
a14098288b | feat: redesign agent Skills and MCP capabilities (#5277) | ||
|
|
e10bb3fff0 |
fix(chat): unify unread badge counting across sidebar, mobile, and thread list (MUL-4286) (#5239)
The aggregate chat unread badge used two different definitions: web/ desktop's sidebar summed unread messages while mobile's tab badge (and the since-removed ChatFab badge it mirrored) counted sessions — the same account state showed e.g. 5 on web and 2 on iOS. The sidebar also summed ALL sessions while the thread list zeroes the row being viewed, so a reply landing in the open conversation flashed a sidebar count with no matching row. - packages/core/chat/unread.ts: countUnreadChatMessages() as the single shared definition (IM-style message total, optional viewed-session exclusion); pure so mobile can import it. - app-sidebar: use the helper and exclude the actively-viewed session, but only while a chat surface is actually showing it (chat route or floating window open) — a remembered selection with both surfaces closed still counts, since nothing will auto mark-read there. - mobile: tab badge switches to the shared message count (99+ cap like the sidebar), ChatSessionSchema parses unread_count, and markRead's optimistic patch zeroes unread_count alongside has_unread. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
06df00f414 |
refactor(views): remove Runtimes tab CLI-update red dot (#5228)
The sidebar Runtimes nav item showed a red dot (introduced in #533) whenever the current user owned a local, CLI-launched runtime whose reported cli_version was older than the latest GitHub release. Remove the dot and the now-unused useMyRuntimesNeedUpdate hook. The per-runtime update indicator on the Runtimes page (useUpdatableRuntimeIds) is unchanged. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb87dd106b |
feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) (#5195)
* feat(chat): task-owned direct-chat input batches + explicit no_response outcome (MUL-4351) Direct (web/mobile) chat no longer uses the last-assistant-row as an implicit input cursor. Each direct send now owns an immutable input batch: - agent_task_queue.chat_input_task_id makes a task the owner of the user messages it must consume; the send path creates the task + user message + attachment bindings + session touch in one transaction, and the daemon is notified only after commit. A claim reads exactly that batch, so a message that arrives mid-run belongs to the next task and is never absorbed. - Auto-retry inherits the root input owner and is queued at a bumped priority, created inside FailTask's transaction so no newer chat task can jump ahead. - CompleteTask writes exactly one assistant outcome inside the completion transaction: a normal message, or a visible no_response outcome (with a non-empty English fallback) when the final output is empty. The write failing rolls the completion back and the handler returns 5xx so the daemon retries; the status CAS keeps it idempotent. chat:done carries message_kind. - Web/desktop/mobile render no_response as a localized 'no text reply' state (keeping the tool timeline), suppress Copy, keep it unread, and keep the session-list preview non-blank. - Legacy/channel tasks (chat_input_task_id NULL) keep the trailing-message selector, so a rolling deploy never replays Slack/Lark history. Co-authored-by: multica-agent <github@multica.ai> * fix(chat): scope no_response to direct tasks; don't cancel task on input read error (MUL-4351) Addresses PR review (Niko): - writeChatCompletionOutcome only writes a no_response row for task-owned direct tasks (chat_input_task_id set). Legacy/channel (Slack/Lark) tasks keep the prior behavior: empty output writes no assistant row, so chat:done carries empty content and the channel outbound silently drops it — the no_response fallback body never reaches an external channel. - The daemon claim distinguishes a genuine zero-input batch from a failed input read: on ListChatInputMessages / ListChatMessages error it returns 5xx and preserves the dispatched task for redelivery instead of cancelling a valid task on a transient DB error. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f7ca045fb1 |
feat(daemon): discover Codex model and reasoning catalog dynamically (#5198)
Discover the Codex model list and per-model reasoning efforts from the installed CLI (codex debug models --bundled), with a verified static fallback for old/offline installs. Server gates token syntax; the daemon validates the exact (model, effort) pair. Closes #5197 MUL-4354 |
||
|
|
bf161f2f9c |
fix(tasks): preserve merged comment delivery (#5192)
Track actual claim-time delivery, support legacy daemons, and repair comment batches across claim, retry, edit, and delete races. MUL-4348 Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f02083fec |
feat(editor): Linear-style issue identifier autolink (MUL-4241) (#5090)
* feat(editor): Linear-style issue identifier autolink (MUL-4241) Bare issue identifiers (e.g. MUL-123, TES-1) now render as navigable issue chips and can be typed/pasted into a real mention, instead of staying inert text. Covers Phase 1 (readonly render) and Phase 2 (editable editor); the Phase 3 batch resolve API is intentionally deferred. Phase 1 — readonly render autolink - Pure, markdown-aware detector `preprocessIssueIdentifiers` in @multica/ui/markdown rewrites bare identifiers to `[MUL-123](mention://issue/MUL-123)`, skipping code, existing links, URLs, and file/path tokens. Runs before linkify/file-card. - `isIssueIdentifier` distinguishes a bare identifier from a real mention UUID at render time (a UUID never matches the identifier pattern). - Chat markdown and comment/description readonly both resolve identifiers to a real issue via a workspace-scoped, exact-match TanStack Query (`issueIdentifierOptions`), rendering a chip on a hit and plain text on a miss / cross-workspace / while loading. The exact `identifier ===` filter enforces the workspace prefix, since the backend search matches by number. - Autolink is opt-in per surface; the shared editable preprocess pipeline is untouched so editable content is never rewritten with fake mentions. Phase 2 — editable editor input/paste - Async ProseMirror plugin resolves a completed identifier (boundary typed after it, or found in pasted text) and swaps it for an issue mention node, serialising to canonical `[MUL-123](mention://issue/<uuid>)`. Only genuine user edits seed candidates (programmatic setContent is gated), so opening existing content never rewrites it. Resolver injected from the setup layer; no React hooks inside the extension. Tests: detector (code/link/url/path skips, dedupe), core resolver (exact match, wrong-prefix miss, empty response, key shape), chat + readonly render (hit/miss/code/canonical), and the editable extension (type/paste/miss/ inline-code/mount-safe/incomplete-token). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): scope Phase 2 autolink to the captured candidate range (MUL-4241) Howard final-review blocker: the async resolve rescanned the whole document for every occurrence of the resolved identifier, so completing a new `MUL-1` also rewrote a pre-existing `MUL-1` the user never touched (persisted-content rewrite; violated "opening existing content is not rewritten"). Fix: capture the specific candidate range(s) a user transaction introduces — the token before the caret when typing, the tokens inside the pasted slice on paste — into plugin state, mapping each range forward on every subsequent transaction. After async resolve, replace ONLY those mapped ranges, verifying each still holds exactly that identifier with intact boundaries and no code/link mark. No document-wide scan by identifier. Also skip link-marked text at capture so an existing link label is never converted. Regression tests: (1) typing a new MUL-1 converts only the new occurrence, not a pre-existing identical one; (2) paste converts identifiers inside the paste range but leaves an identical one outside it untouched; (3) an identifier already carrying an explicit link mark is not replaced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
152edab8d0 |
Revert "fix(workspace): drop workspace from list cache while delete is pendin…" (#4982)
This reverts commit
|
||
|
|
f9b7e5035a |
fix(workspace): drop workspace from list cache while delete is pending (MUL-4129) (#4980)
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> |
||
|
|
346f818206 |
fix(runtime): add traecli to custom runtime profile whitelist (#4972)
Trae (traecli) already has a New() backend, launch header (traecli acp serve) and provider branding, but was missing from every protocol_family whitelist, so custom runtime profiles based on Trae were rejected and it never appeared in the family picker. Add traecli to SupportedTypes (Go), RUNTIME_PROFILE_PROTOCOL_FAMILIES (TS), the lockstep test's want map, and a new migration 136 widening the runtime_profile_protocol_family_check constraint. MUL-4094, #4945 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
129efb7688 |
feat(runtime): allow qoder as a custom runtime profile base (#4883) (#4912)
Qoder CN (`qoderclicn`) users could only reach a working custom runtime by misrouting through the Kiro backend, which launches `<cmd> acp --trust-all-tools`. That is incompatible with Qoder's global `--acp` / `--yolo` argv, so the task failed immediately with `kiro initialize failed` and no run messages. Expose `qoder` in the custom-profile protocol_family whitelist across every lockstep layer: - server/pkg/agent SupportedTypes (+ whitelist pin test) - migration 134 runtime_profile protocol_family CHECK (NOT VALID, mirroring 126) - packages/core RUNTIME_PROFILE_PROTOCOL_FAMILIES The existing qoderBackend already honors an ExecutablePath override and launches `<cmd> --yolo --acp`, so a profile with protocol_family=qoder and command_name=qoderclicn now launches with the correct argv instead of the Kiro shape. Provider branding/logo for qoder already exists. MUL-4018 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ae0a1f5bf |
fix(agents): defend AccessPicker against undefined invocation_targets (GH #4915) (#4924)
Opening the agent detail page in v0.3.37 crashed the whole route with "Cannot read properties of undefined (reading 'some')" whenever the cached agent record's `invocation_targets` was missing at runtime — even though the TypeScript type declares it a required `AgentInvocationTarget[]`. Root cause: `AccessPicker`'s `hasWorkspaceTarget` / `selectedMemberIds` / `selectedTeamIds` helpers called `.some()` / `.filter()` directly on the prop, and the same unguarded pattern was mirrored in `AgentMcpTab` and the `canAssignAgentToIssue` permission gate. The field can legitimately be undefined at runtime because: - `packages/core/api/schemas.ts` declares `invocation_targets` as `.optional()`, and `MinimalAgentSchema` (used for the create-from- template response) also marks it optional. - `api.listAgents` / `api.getAgent` return raw JSON without running through the schema, so an older self-host backend that predates MUL-3963 (permission_mode + invocation_targets) — the exact scenario in GH #4915 — yields an Agent with the field missing. Fix (`?? []`) at every site that indexes into the list, matching the already-established pattern in `create-agent-dialog.tsx`: - `access-picker.tsx`: coerce inside the three helpers and widen the prop type to `AgentInvocationTarget[] | undefined` so the accepted runtime shape is documented. - `agent-mcp-tab.tsx`: `(agent.invocation_targets ?? []).some(...)`. - `permissions/rules.ts`: `agent.invocation_targets ?? []` before the workspace / member membership checks. Adds three regression tests (AccessPicker owner + read-only paths, AgentMcpTab shared-warning path, canAssignAgentToIssue) that pass `undefined` for invocation_targets and assert the UI degrades to the private / empty-allowlist state instead of throwing. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
dd9996d0aa |
MUL-4014: persist transcript filters and expansion (#4884)
* feat(transcript): persist log view preferences Co-authored-by: multica-agent <github@multica.ai> * fix(transcript): wrap modal header controls Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4d968ba875 |
fix(agents): gate create-agent access picker on composio_mcp_apps flag (MUL-4010) (#4888)
Reuse the existing `composio_mcp_apps` feature flag instead of the separate `agent_access_picker` flag introduced in #4879. The MUL-3963 permission_mode + invocation_targets model exists to gate Composio sharing, so the create-flow access picker ships on the same switch as the rest of the Composio rollout — environments that already enable Composio (`FF_COMPOSIO_MCP_APPS=true`) now also see the aligned Private / Public-to picker in Create / Duplicate. - Drop `AGENT_ACCESS_PICKER_FLAG` (frontend keys.ts + index re-export). - Drop `AgentAccessPicker` (server featureflags list). - `CreateAgentDialog` reads `COMPOSIO_MCP_APPS_FLAG` instead. - Tests updated to set the composio flag. All 10 create-agent-dialog tests + Composio-related tabs tests pass. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
77ba0fdddb |
MUL-4009: hide not-configured Composio toolkits at Service layer (#4880)
* MUL-4009: hide not-configured Composio toolkits at Service layer Filter out toolkits with no enabled auth config in Service.ListToolkits so the Settings UI only shows connectable apps. A dead 'Not configured' card is noise for end users, so drop the entry entirely instead of showing a greyed label (which existed only to avoid a dead Connect button, MUL-3720). - service.go: only append connectable toolkits; drop the connectable-first sort (all entries are connectable now); a resolver error now returns an error (502) instead of masking to an empty catalog, so the UI shows its honest load-failed state rather than a misleading 'no apps configured'. - Keep the wire 'connectable' field (always true) for backward compat with older desktop clients that branch on it. - composio-tab.tsx: remove the 'Not configured' branch; keep the toolkit.connectable guard as a client-side backstop. - i18n: drop composio.not_connectable / not_connectable_hint from en/ja/ko/zh-Hans. - Update service + handler tests to assert filtering and the resolver-error path. Co-authored-by: multica-agent <github@multica.ai> * MUL-4009: address review — empty-state copy, 502 handler test, stale comments Review follow-up on #4880: - i18n: rewrite composio.page_description / empty_title / empty_description in all 4 locales. The empty state now means 'no toolkit with an enabled auth config in the project', not 'Composio returned no catalog' — the old copy misled users after filtering landed. - Add TestComposio_ListToolkits_ResolverErrorIs502: fakes an auth-config resolver error and asserts ListComposioToolkits returns 502, pinning the no-silent-empty-catalog behavior (composioFakeSDK gains listAuthErr). - Refresh stale 'full catalog / false connectable' docs in packages/core/types/composio.ts, composio/queries.ts, api/client.ts to the connectable-only model. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b24b63c513 |
feat(agents): align create-agent visibility with MUL-3963 access model (MUL-4010) (#4879)
Adopt the private / public_to invocation-permission model in the Create Agent (and Duplicate) dialog to match the AccessPicker on the agent detail page. When the new `agent_access_picker` feature flag is ON, the visibility section is replaced with an inline access editor that: - Toggles between Private (owner-only) and Public - Under Public, offers "Everyone in workspace" plus a member allow-list (multi-select, current user excluded, mirroring AccessPicker semantics) - Preserves team targets on the source agent when duplicating - Collapses an empty public_to (no workspace, no members) back to private on submit — same normalisation the AccessPicker emits The dialog submits `permission_mode` + `invocation_targets` in that mode, matching the authoritative gate; when the flag is OFF (default) it keeps sending the legacy `visibility` field so production is unaffected until the rollout is greenlit. Backend: register the flag in the frontend-public list so it flows to the web config bootstrap. Tests: legacy toggle still submits `visibility`; flag-on submits `permission_mode`/`invocation_targets`, collapses empty public_to to private, and preserves ticked member grants. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cb68669c73 |
feat(composio): gate MCP apps behind feature flag (#4876)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720) (#4608)
* feat(composio): server-side connect flow + connections REST (Notion MVP) (MUL-3720)
Compose the merged server/pkg/composio SDK into a user-facing connection
manager: signed-state connect handshake, local user_composio_connection
mirror, idempotent disconnect, and a per-user MCP session helper (not yet
wired into task dispatch).
- migration 127_user_composio_connection (no FK/cascade, per DB rules)
- sqlc queries: upsert (idempotent on user_id+connected_account_id), list
active, owner-scoped get, mark revoked
- internal/integrations/composio: signed HMAC-SHA256 state, BeginConnect,
CompleteCallback (idempotent upsert), ListConnections, Disconnect
(upstream 404 = idempotent success), CreateMCPSession (no-op when empty,
pins connected_accounts per toolkit), CallbackRedirect
- REST handlers under /api/integrations/composio (user-scoped, 503 when
COMPOSIO_API_KEY unset): connect/init, callback (302), connections list,
delete
- router wiring gated by COMPOSIO_API_KEY; COMPOSIO_AUTH_CONFIGS_JSON maps
toolkit->auth_config (MVP: notion); state secret from COMPOSIO_STATE_SECRET
or derived from JWT_SECRET; callback base from COMPOSIO_CALLBACK_BASE_URL
or MULTICA_PUBLIC_URL
- tests: state (expire/tamper/wrong-secret), service (mapping, callback
idempotency, non-success, disconnect owner/404 idempotency, MCP pin),
handlers (httptest), redact regression for Bearer mcp_ tokens
MVP scope: Notion only; no task-dispatch overlay, sharing, or webhook
event handling (later stages).
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): bind callback account to user + idempotent revoked disconnect (MUL-3720)
Address PR 4608 review (CHANGES_REQUESTED):
- callback: verify connected_account_id with Composio before mirroring it.
The signed state only proved user/toolkit/exp, so a valid state paired with
a tampered connected_account_id would be written verbatim. CompleteCallback
now calls ListConnectedAccounts and fails closed (ErrAccountVerification)
unless the account belongs to the state's user (composio_user_id == multica
user id) and was created under the toolkit's auth config. No row is written
on mismatch / unknown account / upstream error.
- disconnect: short-circuit to a no-op when the local row is already revoked,
before touching upstream. Previously a second DELETE re-hit Composio and a
non-404 upstream error surfaced as a 502, breaking the 204-idempotent
contract.
- CreateMCPSession: document the v1 single-active-connection-per-(user,toolkit)
constraint and make duplicate selection deterministic (newest-wins, rows are
connected_at DESC) instead of order-dependent map overwrite. Stage 3 owns the
real single-account-enforcement vs multi-account-shape decision.
Tests: tampered/wrong-auth-config/unknown-account callback rejection, revoked-row
disconnect no-op (asserts upstream not re-hit). composio pkg 85% coverage; all
green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): list all toolkits + dynamic auth-config resolution (MUL-3720)
Yushen's follow-up to the Notion MVP: surface the full Composio toolkit
catalog, render it in Settings, and drop the static env mapping in favor of
dynamic auth-config discovery.
Config correctness (per Composio docs):
- Remove COMPOSIO_AUTH_CONFIGS_JSON entirely. The toolkit→auth_config mapping
is now resolved at request time from the project's /auth_configs (cached,
5-min TTL), so enabling a toolkit is a dashboard action, not a redeploy.
- Do NOT add COMPOSIO_PROJECT_ID. The project API key (x-api-key) authenticates
to exactly one project; the project is resolved from the key. Only org-level
endpoints use x-org-api-key, which this integration never calls.
Backend:
- SDK: server/pkg/composio/auth_configs.go — ListAuthConfigs (toolkit_slug,
is_composio_managed, show_disabled, limit, cursor).
- service: dynamic resolver (authConfigMap cache; betterAuthConfig prefers a
custom/white-label config over Composio-managed, newest wins); BeginConnect
and CompleteCallback resolve via it; ListToolkits fetches the full catalog
(paginated, capped) annotated with connectable = has an enabled auth config,
connectable-first ordering.
- handler + route: GET /api/integrations/composio/toolkits (user-scoped, 503
when COMPOSIO_API_KEY unset) returning slug/name/logo/category/connectable.
Frontend:
- core: ComposioToolkit/ComposioConnection types, api client methods, and
composio query options (@multica/core/composio).
- views: Settings → Integrations now has a Composio section rendering every
toolkit as a card with search. Connect is gated on `connectable`;
non-connectable toolkits show a muted "not configured" hint instead of a
dead button. Connected toolkits show a badge + Disconnect (with confirm).
- i18n: composio block added to en/zh-Hans/ja/ko settings.
Tests: SDK + service (dynamic resolution, custom-over-managed preference,
connectable flag, resolver-error soft-degrade) and handler toolkits endpoint;
composio pkg 85.7% coverage. go build/vet/gofmt clean; core+views typecheck,
core+views lint, and core tests (691) all green.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): close cross-toolkit callback fail-open by signing auth_config_id into state (MUL-3720)
Re-review blocker: CompleteCallback resolved the toolkit's auth config at
callback time and ignored a resolve error/empty result, while
verifyAccountOwnership skipped the auth-config comparison when the expected
value was empty. A user could then pass another toolkit's connected_account_id
into this toolkit's callback — the owner check passed and it was written under
the wrong toolkit_slug/account binding.
Fix: the auth_config_id is already resolved in BeginConnect (before the state
is signed), so sign it into the state and compare it exactly at callback. No
re-resolve, no fail-open. verifyAccountOwnership now fails closed when the
expected auth config is empty (rejects instead of skipping) and requires an
exact match — closing the cross-toolkit binding gap.
Tests: state round-trips auth_config_id; BeginConnect signs it; callback
rejects wrong/cross-toolkit auth config and an empty (no-mapping) auth config
fails closed. composio pkg 85.2% coverage, all green.
Frontend (non-blocking): the Composio settings tab now surfaces an error when
the connections query fails instead of silently rendering everything as
unconnected.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): hide Settings section entirely when integration unconfigured (MUL-3720)
Decision (option 2, hide-then-merge): don't show a card that leaks the internal
COMPOSIO_API_KEY env-var name to every end user. IntegrationsTab now gates the
whole Composio section (heading + body) on the toolkits query — a 503 means the
key is unset, so the section is withheld instead of rendering the not-configured
card. Admin-only setup guidance is a later, role-gated affordance.
Removed the notConfigured card (and now-unused ApiError import) from
ComposioTab; it only mounts when configured. views typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): Stage 2 frontend polish — callback toast, last_used & expired UI, e2e (MUL-3718) (#4688)
* feat(composio): callback toast + refresh, last_used & expired UI, e2e (MUL-3718)
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): real callback redirect route + StrictMode-safe toast dedup (MUL-3718 review)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): callback endpoint should not require Multica auth (MUL-3843) (#4709)
* fix(composio): move OAuth callback out of the Auth group (MUL-3843)
Composio 302-redirects the browser to /api/integrations/composio/callback
at the end of the OAuth flow, but PR #4608 mounted it inside the cookie-auth
middleware group. When the session cookie is absent (expired session,
SameSite=Strict / Safari ITP, private window, self-hosted callback subdomain)
the Auth middleware returned a hard 401 and a JSON blob instead of the
settings redirect, breaking the flow.
Identity never came from the cookie anyway: it is carried by the HMAC-signed
state param that CompleteCallback verifies (signature, expiry, replay) and
cross-checked by verifyAccountOwnership; h.Composio == nil still 503s. So the
callback is registered alongside the other public OAuth/webhook routes; the
other four composio endpoints stay session-gated.
Refs MUL-3843, MUL-3715.
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): correct stale callback routing comments (MUL-3843)
The package header and ComposioCallback doc comments still described the
callback as sitting under the Auth middleware group. After the route was
moved out (this PR), update both to state it is a public route whose identity
comes from the signed state — addressing review nit from 张大彪.
Refs MUL-3843.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): inject MCP overlay into agent runtime at task dispatch (MUL-3721) (#4704)
Stage 3 of the Composio epic. Wires the per-user Composio MCP session into
every agent task so the agent process sees the initiator's connected tools
without any prompt-time plumbing.
Server side
- Migration 128 adds agent_task_queue.runtime_mcp_overlay JSONB plus a
BEFORE-UPDATE trigger that wipes the column on any transition into a
terminal status (completed / failed / cancelled). A trigger is the single
source of truth — future queries that flip status cannot bypass it.
- composio.Service.BuildTaskOverlay(userID) reuses CreateMCPSession and
emits the Claude-style { mcpServers: { composio: { type: http, url,
headers } } } shape the daemon's existing sidecar generators consume.
Returns (nil, nil) on zero active connections so we never burn a
Composio session for a user with nothing to call.
- TaskService grows a Composio ComposioOverlayBuilder seam, wired in
router.go after composiointeg.NewService succeeds. Five enqueue paths
(issue / mention / quick-create / chat / auto-retry) attach the overlay
after CreateAgentTask returns and before the daemon is notified — so
every claim reads a settled row, with no second daemon hop. Best-effort:
a builder failure logs and proceeds with no overlay.
- resolveInitiatorFromTriggerComment derives the initiator user from the
trigger comment when it was authored by a member. Agent-authored
triggers are not treated as initiators (their connected-apps view is
empty by construction).
Daemon side
- handler/daemon.go claim path merges task.runtime_mcp_overlay onto
agent.mcp_config via mergeMCPOverlay before populating
TaskAgentData.McpConfig. Overlay wins on server-name collisions
because it carries the live user-scoped session URL. Errors fall back
to the agent config unchanged — a bad overlay must not surprise-disable
saved MCP tools. The existing execenv sidecar generators (cursor /
codex / openclaw / opencode / hermes-kiro) need no changes: they keep
consuming the merged result through TaskAgentData.McpConfig.
Tests
- 9 merge cases (mcp_overlay_test): both-nil short-circuit, agent-only
pass-through, overlay-only canonicalization, two-side merge, name
collision (overlay wins), top-level key preservation, malformed agent
fallback, malformed overlay fallback, non-object server rejection.
- 4 dispatch cases (composio): zero-connections returns nil without
CreateSession, happy-path emits the right shape with the right user
id, empty-URL defensive branch, SDK error surfacing.
- 4 TaskService helper cases: nil Composio is a no-op (Queries-safe),
invalid initiator does not call the builder, nil overlay skips the
UPDATE, builder error swallowed without panic.
- Migration 128 verified to roll up + down + up cleanly against the test
database.
Out of scope (deferred): assignment-triggered enqueue paths with no
trigger comment get no overlay attached today (no initiator UUID flows
through enqueueIssueTask in that case). Retry paths recompute the overlay
fresh from the parent's initiator_user_id instead of inheriting the bearer
from the parent row, so a stale token can never resurface on a retry.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869) (#4736)
* feat(composio): per-agent allowlist + originator-scoped MCP overlay (MUL-3869)
Stage 3.1 of the Composio epic (MUL-3721 parent). PR #4704 wired in the
runtime_mcp_overlay column and a per-task dispatch hook; this change
inverts the default from "all-on" to opt-in and locks the overlay to the
agent owner's own connected apps:
- Agents carry composio_toolkit_allowlist TEXT[]. NULL or [] => no MCP.
Owner-only read/write; non-owner GET/PUT silently redacts/drops the
field (same shape as mcp_config).
- agent_task_queue carries originator_user_id UUID. Set from the
top-of-chain HUMAN at every enqueue path:
* issue/mention comment by member -> author_id
* issue/mention comment by agent -> inherit via comment.source_task_id
-> parent task originator_user_id
* quick-create -> requester_id
* chat -> initiator_user_id
* retry -> SQL-inherited from parent row
* autopilot -> NULL (system-driven)
- BuildTaskOverlay (composio dispatch) now takes (ctx, originatorUserID,
agent) and short-circuits on five gates: invalid originator,
originator != agent.owner_id, empty allowlist, empty intersection of
allowlist ∩ active connections, defensive empty session URL. Composio
CreateSession is called with BOTH `toolkits.slugs` (the intersection)
AND `connected_accounts` (the pinned account ids), narrowing the
tool-router twice.
- The originator-vs-owner gate closes the agent-fanout privacy hole: any
workspace member who can @-mention a public agent used to project the
owner's connected apps into their run. Now the overlay only mounts
when the human at the top of the chain IS the agent owner.
Tests:
- dispatch_test.go covers all 5 gates plus uppercase/whitespace slug
normalisation.
- task_runtime_mcp_overlay_test.go covers the no-op gates of the new
applyRuntimeMCPOverlay signature.
- agent_composio_allowlist_test.go (handler): owner roundtrip
(list/empty/null), workspace-admin silent-drop, owner-only GET
visibility, pure normaliseComposioToolkitAllowlist.
- resolve_originator_test.go (service, DB-backed): member-authored,
agent-authored inherits via comment.source_task_id, invalid id.
Migration 129 up/down/up verified against docker postgres.
Co-authored-by: multica-agent <github@multica.ai>
* chore(composio): gofmt + regenerate sqlc with v1.31.1 (MUL-3869 review nits)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
* fix(composio): accept nested connected account auth config
* feat(views): creator-only MCP tab for per-agent Composio allowlist (MUL-3870) (#4743)
Stage 3.2 frontend on top of the Stage 3.1 backend (MUL-3869,
|
||
|
|
03828015ca |
fix(feedback): validate response and pass error kind (#4633)
Wire structured feedback kind through the frontend/core feedback path so desktop route-renderer errors submit as bug feedback, and bring the feedback client onto parseWithFallback. MUL-3768 |
||
|
|
c4b116ec3a |
feat(views): expose add-label entry in create-issue dialog (#4846)
Adjust the manual create-issue dialog toolbar so the add-label entry is surfaced directly, and move the lower-frequency due date into the ⋯ menu: - Add a Labels picker in the slot Due date used to occupy. LabelPicker gains a draft mode (no issueId): selection is held via selectedIds / onSelectedIdsChange and attached to the issue right after it's created (the create endpoint takes no labels), mirroring the sub-issue linking pattern already in this dialog. - Collapse Due date into the ⋯ overflow menu with the same reveal rule as Start date (inline only when it has a value or was just opened). Give DueDatePicker the controlled open/onOpenChange props StartDatePicker already had. - Persist chosen labels in the issue draft store (labelIds) like every other draft field. - Add useAttachLabelToIssue (variables-based attach) so labels can be attached to a just-created issue. - i18n: add create_issue.set_due_date and toast_link_labels_failed across en / zh-Hans / ja / ko. MUL-3971 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ade6b34e5f |
MUL-3903: Extract shared issue surfaces (#4774)
* MUL-3903 refactor project issue surface state Co-authored-by: multica-agent <github@multica.ai> * Refactor project issue surface ownership Co-authored-by: multica-agent <github@multica.ai> * Extract shared issue surface entrypoints Co-authored-by: multica-agent <github@multica.ai> * Fix issue surface create defaults and selection reset Co-authored-by: multica-agent <github@multica.ai> * test(editor): add missing AbortSignal to suggestion items() calls The suggestion items() contract gained a required signal param; the mention/slash test call sites were never updated, breaking pnpm typecheck for @multica/views. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): server-side assignee_types filter on ListIssues ListGroupedIssues has taken assignee_types since squads shipped, but ListIssues never did — so the workspace Members/Agents tabs had to fetch the unfiltered workspace list and post-filter loaded pages client-side, which made column totals and load-more pagination reflect the unfiltered counts. Add the same parse + WHERE clause to ListIssues (count query shares the WHERE, so totals agree), thread the param through the TS client, and widen MyIssuesFilter so scoped list caches can carry it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(issues): route issue cache writes through a membership-aware coordinator useUpdateIssue, useBatchUpdateIssues, and the WS issue:updated handler each maintained their own similar-but-diverging patch/invalidate rules. Consolidate them into cache-coordinator.ts (applyIssueChange / rollbackIssueChange / invalidateIssueDerivatives) so local writes and remote echoes follow one rules table by construction. The coordinator is membership-aware via surface/membership.ts (true | false | unknown against each list cache's own filter contract): - a change that moves an issue off a filtered surface removes the card surgically (bucket total decremented) — fixes assignee changes leaving stale cards on My Assigned with no local safety net (previously only the WS echo recovered it), and replaces the blanket invalidate-myAll net for project moves (MUL-3669) with per-key precision - possible entry into a loaded list marks that key stale — never hard-insert; page/slot is server knowledge - stale keys flush on settle for mutations (a mid-flight refetch would stomp the optimistic state) and immediately for WS - batch updates now patch detail + inbox like single updates; the off-screen bucket-count recovery previously exclusive to the WS path now covers local mutations too Preserved invariants: synchronous optimistic patches (dnd-kit), MUL-3375 control-field stripping, and no refetch of surgically reconciled lists (the drag-flicker fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(issues): resolve surfaces via core query plan/repository with window-keyed remount Read-path convergence and the loading/empty semantics that fall out of it: - scope -> API params moves from scope.ts helpers into surface/query-plan.ts; workspace members/agents become server-filtered scoped plans (assignee_types) and the client postFilter machinery is deleted — tab counts and load-more are now exact - query selection moves behind surface/repository.ts; the views data hook no longer branches on workspace-vs-scoped plumbing - IssueSurfaceContent remounts on data-window change (wsId + scope): keepPreviousData placeholders keep sort/filter changes flicker-free within one window but must never let project A's (or workspace A's) cards impersonate B's with no loading state — cold window shows the skeleton, warm window hits cache instantly - isEmpty is only asserted from full-window data; the gantt scheduled-only projection can't prove the window is empty, so GanttView's own "no scheduled issues" empty state renders instead of the generic create-issue one - per-card project lookups hoist into a surface-level projectMap (drops a per-card useQuery), create-defaults typing tightens to IssueCreateDefaults Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(issues): count-only arithmetic for off-window status/membership changes An issue beyond a list's loaded page window used to force a full first-page refetch just to fix two column counts. When the change is CERTAIN (base entity known, membership definitive) the coordinator now does the arithmetic locally: - stayed a member + status changed: move one unit of total between the two buckets (loaded arrays untouched; hasMore stays consistent) - left the list (reassigned / re-projected): old status bucket total -1 - member-to-member reassignment: counts unaffected, not even a stale key Entering a list and any uncertainty (no base, unknown membership) still refetch — the right page/slot is server knowledge. Branches on membership OUTCOMES, not on which field changed, so future dimensions (team) join automatically. Biggest win is the WS path: agents flipping off-screen statuses no longer trigger refetch storms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): deferred view-refresh indicator during placeholder revalidation Sort/date changes (and any grouped-board filter change) revalidate behind the previous snapshot — correct, but on a slow network the click felt dead: content stays put and isLoading never fires. Surface the state as isRefreshing (isPlaceholderData of the active query) and render a shared ViewRefreshIndicator in every issues header: a fixed-width slot (zero layout shift) whose spinner fades in after 300ms, so sub-second responses show nothing (NN/g) while slow ones get a working signal. Bound to the revalidation STATE, not to any particular control — any current or future server-side view change lights it automatically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |