* docs(changelog): add v0.3.35 entry for the 2026-07-02 release (MUL-3984)
Summarize the changes on main since v0.3.34: the Issue-surface refactor and its
per-list cache/count accuracy, the chat live-timeline remount fix, four new
features (Show sub-issues toggle, add-label picker in the manual create dialog,
bulk 'Add to agent' on the skill detail page, and S3 path-style addressing
for self-host), plus this release's bug fixes.
Bullets stay in product language across en / zh / ja / ko. Only the Issue
concept is preserved untranslated in the Chinese entry; agent/Squad/sub-issue
follow the translated glossary.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.35 bullets to one clause each (MUL-3984)
Per review, cut each bullet to a single short sentence and shorten the release
title. Same coverage across en / zh / ja / ko; content unchanged.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Connecting the same GitHub App installation in a second workspace silently
overwrote the first workspace's binding: github_installation was
UNIQUE(installation_id) and CreateGitHubInstallation's upsert overwrote
workspace_id on conflict (#4823).
Widen the uniqueness key to (workspace_id, installation_id) so each workspace
keeps its own binding row, and teach the webhook/lifecycle paths to handle N
bindings per installation_id:
- CreateGitHubInstallation upserts per (workspace_id, installation_id).
- Webhook lookup lists all bindings; PR/check_suite routing uses the oldest
binding as the deterministic fallback and still routes per-repo via the
existing workspace.repos registry.
- installation.deleted/suspend drops every workspace binding and broadcasts to
each affected workspace.
- installation.created/unsuspend refreshes account metadata across all bindings.
- Add a standalone index on installation_id (the dropped unique constraint was
the only index behind the webhook lookup).
MUL-3950
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
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
* 📝 [需求] 有序列表光标切换缺陷提案 / docs: requirements for 26-editor-ordered-list-cursor
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): keep the caret in the list item when the comment box remounts
Typing `1.` makes an ordered list, then switching issues and back yanked the
caret to the next line and re-yanked it there whenever the user moved it. The
comment box remounts on issue switch (key={id}) and feeds the persisted draft
back as defaultValue; the content-sync effect treated the redrawn draft as an
external change because @tiptap/markdown reserializes an ordered list slightly
differently from its source, re-parsed it, and restored the caret with a bare
Math.min offset clamp that resolved onto the list's structural (non-text) gap.
- Short-circuit the sync effect when the incoming parser INPUT matches what was
last applied (seeded at mount), so an identical draft is never re-parsed.
- Replace the Math.min clamp with clampSelectionToText, which snaps the prior
offsets to the nearest valid text position via TextSelection.between.
Tests: restore-selection unit tests (real editor) + a content-editor remount
regression test.
Co-authored-by: multica-agent <github@multica.ai>
* fix(editor): seed the caret guard synchronously and keep it in sync with the doc
Addresses cross-review findings on the content-sync guard:
- Seed appliedIncomingRef at render instead of in onCreate. Tiptap v3's
onCreate is deferred past the first sync-effect run, so the guard was null on
that run and a remount could still re-parse the ordered-list draft (the mock's
synchronous onCreate had hidden this).
- Advance appliedIncomingRef on the Guard 3 short-circuit too, so a later
external change back to an older value (e.g. a collaborator reverts the
description) is no longer mistaken for an already-applied input and swallowed.
Tests: add a suppressed-onCreate remount case and an A->B->A external-revert
regression; 23 passed.
Co-authored-by: multica-agent <github@multica.ai>
* docs: remove internal requirements proposal from the PR
Keep the PR to code and comments only, per reviewer request. The requirement
context lives in the tracking issue, not in this open-source code change.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): stop remounting the live timeline on every streamed task message (MUL-3960)
The Virtuoso components prop was built inline, so every render produced new
Header/Footer component types and React unmounted + remounted the entire live
timeline subtree. During task streaming that meant every task:message event
tore down and rebuilt thousands of rows and re-parsed all Markdown, freezing
the renderer for seconds at a time on long agent runs.
- Hoist Header/Footer to module scope and pass per-render data through
Virtuoso's context prop (re-render instead of remount).
- Memoize buildTimeline on the task-messages cache array identity (live
timeline and persisted AssistantMessage).
- Render message text through MemoizedMarkdown so unchanged content skips
the markdown re-parse; only the streaming tail re-renders.
Regression tests assert the footer DOM node survives a streamed message and
that a user-collapsed process fold stays closed (both failed before the fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* perf(chat): memoize MessageBubble so streamed messages skip untouched rows
Follow-up to the MUL-3960 remount fix: each task:message still re-rendered
every visible history row through itemContent. Message objects are
referentially stable and isPending is a boolean, so a shallow memo makes
the persisted history inert during streaming — only the live footer
reconciles per message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
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>
* fix(issues): wake parent squad leader on same-squad/shared-leader child-done (MUL-3969)
The child-done stage-barrier wrote the 'Stage N complete / wrap up the
parent' system comment on the parent but suppressed the parent squad
leader's wake whenever the finished child was owned by the same squad
(childAssigneeIsSquad) or a squad sharing the leader (effectiveChildAgentOwner).
That stranded the common 'a squad decomposes its parent into sub-issues
it works itself' pattern: the parent silently stalled in in_progress
because the leader was never woken to advance the next stage or wrap up.
The prior guards assumed the leader had already observed the work via
its own coordination cycle on the child, but that wake lands on the
CHILD and never carries the parent-level stage-barrier instruction.
Remove both self-trigger guards so the squad path mirrors the agent path
(MUL-2808): always dispatch, bounded only by HasPendingTaskForIssueAndAgent.
The private-leader access gate is unchanged; member/unassigned parents
still never wake. Drop the now-dead effectiveChildAgentOwner /
childAssigneeIsSquad helpers and the unused child param.
Fixes#4838
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): fix stale child-done squad-guard comment (MUL-3969)
The TestChildDoneTriggersParentAgentWhenChildSquadSharesLeader comment
still claimed the both-sides-squads-sharing-a-leader case was guarded on
the squad path and referenced the pre-rename test. That guard was removed
in MUL-3969; point at the renamed test and state the current behavior.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Treat MULTICA_DAEMON_PORT and a workdir daemon-task marker as daemon-managed signals so a task subprocess that loses MULTICA_TOKEN / MULTICA_AGENT_ID / MULTICA_TASK_ID fails closed instead of silently falling back to the user config-file PAT (which made agent writes land as the workspace owner). Adds an actionable error naming a leftover marker for local_directory recovery. Fixes#4204.
* 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>
* fix(markdown): render inline data-URI images (MUL-3961)
Inline data:image/* URIs (QR codes, charts, base64 screenshots) were
stripped and rendered as broken images. Two gates dropped the src:
- rehype-sanitize's protocols.src only allowed http/https
- react-markdown's defaultUrlTransform blanks any data: URL to ''
Allow data:image/* through both gates, narrowed to image subtypes only
(non-image data URIs stay rejected) and leaving every other src form
unchanged. file-cards.ts data: rejection is intentional and untouched.
Co-authored-by: multica-agent <github@multica.ai>
* fix(markdown): allow data:image/* in ReadonlyContent too (MUL-3961)
Issue comments and other read-only surfaces render through ReadonlyContent,
which keeps its own sanitize schema + urlTransform separate from the base
Markdown component. Both still stripped data: URIs, so an agent inlining an
auth QR code in an issue comment still saw a broken image.
Apply the same image/*-narrowed data: allowance (protocols.src +
attributes.img + urlTransform) and add a regression test covering the
comment / readonly path. file-cards.ts data: rejection stays untouched.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Emit the recovered Antigravity transcript reply as a MessageText event so the
execution transcript catches up with Result.Output on empty-stdout recovery.
Closes#4779
MUL-3941
* fix(slack): recover attachment/blocks text for empty-Text chat messages (MUL-3931)
`multica chat thread`/`chat history` dropped any message with an empty
top-level Text via normalizePage(), intended to skip join/system markers
but also discarding alerting/webhook bot roots (Grafana cards, incoming
webhooks) whose body lives in Attachments/Blocks. This regressed the
#4717 fix for its most common trigger: @mentioning the agent under an
alert card.
Add flattenSlackText(): fall back to Attachments[].Fallback, then
title/text/fields, then a best-effort Block Kit flatten (section/header/
context/markdown) before discarding. Derived text is capped so a verbose
card cannot flood the agent context; genuine content-less markers are
still dropped. Both read paths share normalizePage, so one fix covers
chat thread and chat history.
Closes#4803
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): flatten rich_text blocks in chat history fallback (MUL-3931)
Addresses the review nit on #4805: flattenBlocks() previously handled
section/header/context/markdown but skipped rich_text, so a message
whose only body is a rich_text block (the shape Slack rich-text input
produces) could still be dropped. Walk rich_text sections/lists/quotes/
preformatted runs and concatenate their text and link runs.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): preflight mermaid syntax before rendering
* refactor(views): use suppressErrorRendering instead of a pre-render parse
Enable Mermaid's suppressErrorRendering so render() throws on invalid
syntax without drawing its built-in error graphic into the DOM, letting
the existing catch fall back to the compact error state. This drops the
redundant mermaid.parse() pass over valid charts while still fixing the
orphaned error-SVG artifact from #3653.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Surface the existing multi-select Add-to-agent dialog (previously only on
the skills list row/bulk actions) on each skill's own detail page, so one
skill can be attached to several agents at once. The trigger lives on the
"Used by N agents" sidebar panel header — the section that already lists
which agents have the skill — so adding more agents happens right there.
Agents that already have the skill render checked + disabled in the dialog
(the shared dialog's existing hasAll behavior). Also add an agent search
box to the shared AddToAgentDialog (benefits the list page too); groups
auto-expand while searching.
MUL-3922
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): add 'Show sub-issues' display toggle (MUL-3923)
Add a 'Show sub-issues' switch to the issue display-settings menu. When
turned off it hides sub-issues (issues with a parent) from the board,
list, swimlane and gantt views so users can focus on top-level / parent
issues. It is a pure display filter and never changes parent/child
relationships. Defaults to on and persists per view store, so /issues,
/my-issues, project detail and the actor tasks panel each remember it.
- view-store: new showSubIssues state + toggleShowSubIssues action,
persisted; propagates to my-issues and actor view stores via the shared
slice.
- filter: optional showSubIssues on IssueFilters; drop issues with a
parent when explicitly false (undefined keeps show-all, so existing
callers and mobile's positional variant are unaffected).
- wire the toggle into every surface that renders the display menu.
- i18n for en / zh-Hans / ja / ko.
- filter unit tests for the new toggle.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): apply Show sub-issues to assignee board & swimlane extra path (MUL-3923)
Address review of #4801 — two paths bypassed the new toggle:
1. Assignee-grouped board rendered straight from the server 'groups'
array, skipping the flat filterIssues() output, so sub-issues stayed
visible in assignee grouping on /issues, /my-issues and project detail.
Added filterAssigneeGroups() which re-applies the client-only display
filters (Show sub-issues + the agents-working quick filter) to each
group, recomputes total and drops emptied groups. Wired into all three
surfaces. Generalizes and replaces the old filterRunningAssigneeGroups.
2. Parent swimlane's batch/per-parent extra-children merge rebuilt its
internal activeFilters without showSubIssues, so lazily-loaded
sub-issues reappeared even with the toggle off. Carry showSubIssues
through.
Tests: filterAssigneeGroups unit tests (sub-issue hide, running filter,
AND composition, empty-group drop, by-reference passthrough) and a
swimlane test covering the batch-children path with the toggle off.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The Slack `/issue` slash command used to directly create a raw issue: the
typed line became the title verbatim and a `todo` issue was assigned to the
agent to work on immediately. That files a rough, unstructured issue and starts
the agent on it before it is well-formed.
Switch the slash command to the quick-create pipeline instead
(TaskService.EnqueueQuickCreateTask, the same path as the web "quick create"
modal): the invoker's natural-language description is handed to the
installation's agent as a prompt, and the agent authors a well-formed issue
(proper title + structured description) in the background, attributed to the
bound member. Because creation is now asynchronous, the ephemeral reply is an
acknowledgement ("On it…") rather than a created-confirmation with a number;
the agent's completion surfaces to the invoker as a Multica inbox notification
through the shared quick-create completion path.
Installation routing and identity/membership checks are unchanged, so the same
workspace boundary and account-binding rules apply. Scope is the slash command
only — the message-based `@bot /issue` still runs through the shared
cross-platform engine (which also serves Lark) and keeps its direct-create
behavior.
- slash_command.go: swap IssueService.Create for EnqueueQuickCreateTask via a
narrow quickCreateEnqueuer interface; prompt is the full text (no title/body
split); drop the now-unused splitIssueText / issueCreatedText / GetWorkspace.
- router.go: wire h.TaskService instead of h.IssueService.
- tests: cover enqueue + ack, multiline prompt pass-through, empty prompt,
unbound, non-member, inactive, team mismatch, and enqueue-failure.
- docs (4 locales): describe the quick-create behavior.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
A Slack user who linked their Multica identity to one bot was re-prompted
to link again when messaging a second bot (a different Slack app) in the
SAME Slack workspace, because bindings are keyed per-installation
(installation_id, channel_user_id).
On an unbound inbound, the identity resolver now looks up an existing
binding for the same (Multica workspace, Slack team, Slack user) via the
new FindReusableChannelUserBinding query and, if that user is still a
workspace member, materializes a binding for the new installation instead
of returning ErrSenderUnbound — so the second bot resolves the user
silently. Reuse is fenced to one Multica workspace AND one Slack team, so
it never crosses either boundary; legacy installs with no recorded team
never reuse.
Adds resolver unit tests for every decision path and updates the Slack
integration docs (en/zh/ja/ko).
MUL-3911
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Follow-up to #4780 (the /issue slash command), which merged the code + docs. The DB migration was pushed to that branch after it had already been squash-merged, so it never landed on main — the /issue command still fails at issue creation.
The issue.origin_type CHECK constraint (migration 111) only allowed autopilot / quick_create / lark_chat. Slack stamps origin_type='slack_chat' on every /issue create, so the INSERT trips SQLSTATE 23514 and IssueService.Create fails ("Something went wrong creating the issue"). This also silently broke the pre-existing message-based Slack /issue. Extends the constraint to include 'slack_chat', mirroring 111 (lark_chat). Numbered 131 because 129/130 were taken on main since #4780 branched.
Verified against a real Postgres: full chain up to 131 applies cleanly and an issue insert with origin_type='slack_chat' passes the CHECK after 131 (fails with the pre-131 constraint).
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): native /issue slash command over Socket Mode (MUL-3908)
A message beginning with `/issue` is intercepted by the Slack client as a slash command and never delivered to the app, so the message-prefix /issue never worked on Slack (no event, no 👀, no issue).
Register /issue as a real slash command in the app manifest and handle EventTypeSlashCommand over the existing per-installation Socket Mode connection. It is a one-shot issue creation (no chat session / agent run) that reuses the shared IssueService and the same installation-routing + identity/membership checks as the message path, replying privately via the command's response_url (ephemeral) since a slash command has no message to react to.
Docs: register the command in the manifest and describe the slash-command behavior across all four locales.
Co-authored-by: multica-agent <github@multica.ai>
* docs(slack): add commands scope to /issue manifest; fix chat-run wording (MUL-3908)
Review follow-up: the manifest examples registered features.slash_commands but omitted the commands bot scope, so updating + reinstalling could still fail to grant the /issue command. Add - commands to oauth_config.scopes.bot in all four locales and document it in the permissions table.
Also correct the misleading "no agent run" wording in the slash-command header and router comment: a todo issue assigned to the agent still triggers it via maybeEnqueueOnAssign (issue-assignment), like the message /issue — the slash command only skips the chat session / chat run.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Self-hosted local-disk deployments serve document previews straight from
the public /uploads/* static route. That route inherited the global
`frame-ancestors 'none'` CSP from the middleware, so iframe-based previews
(PDF/HTML) were blocked by the browser — only the /api/attachments/*
download endpoint had been exempted (#4635 / #4679).
Serve /uploads/* through a new Handler.ServeLocalUpload that applies the
same preview security headers as the download endpoint
(setAttachmentPreviewSecurityHeaders), so the relaxed, config-aware
`frame-ancestors 'self' <configured origins>` policy applies to both
same-origin and split frontend/backend origin setups. Inline <img>
rendering is unaffected (frame-ancestors does not gate images); cloud
storage (S3/CloudFront) never hits this route.
Adds regression tests covering the relaxed CSP on /uploads and the
non-local-storage 404 guard.
Refs #4477
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Follow-up to #4724, which added the Trae CLI (traecli) ACP backend but
left the surrounding docs behind.
- install-agent-runtime: add a Trae CLI section (install, ACP transport,
enterprise login, inline runtime brief, MULTICA_TRAECLI_MODEL)
- providers: fix the MCP paragraph — Trae also receives ACP mcpServers
- daemon-runtimes: add Qoder + Trae CLI to the built-in detection list
- README: add Trae CLI to the architecture diagram and runtime row
- bump stale English tool counts (12/13 -> 14) across cross-references;
the '12' lists were already missing Qoder before this change
Scope: English docs only. The ja/zh localizations are separately behind
(they predate Qoder too) and need their own translation-sync pass.
Co-authored-by: J <agent-j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Adds the official ByteDance TRAE CLI (the `traecli` binary documented at
https://docs.trae.cn/cli — the product paired with the Trae IDE, not the
open-source bytedance/trae-agent) as a built-in agent backend. traecli is
ACP-native, so it is driven over the standard ACP JSON-RPC transport via
`traecli acp serve --yolo`, reusing the shared hermesClient exactly like the
Kiro and Qoder backends.
Validated end-to-end against the real traecli v0.120.42 with a logged-in
account: initialize advertises loadSession:true + mcpCapabilities{http,sse};
session/new returns result.sessionId + models.availableModels (18 models
discovered); session/prompt streams session/update notifications with
sessionUpdate=agent_message_chunk (hermesClient already normalizes this Zed-ACP
wire shape); a real board task ran 14 tool calls and completed in ~47s.
Implementation:
- server/pkg/agent/traecli.go: ACP backend; session/load resume
(loadSession:true), session/set_model, MCP via ACP mcpServers, --yolo
bypass-permissions for headless runs, blocked-arg filtering (acp, serve,
--yolo, --print, --output-format, --permission-mode)
- agent.go: New() + launch header "traecli acp serve"
- models.go: discoverTraecliModels via the shared discoverACPModels
- daemon/config.go: auto-detect the `traecli` binary
(MULTICA_TRAECLI_PATH / MULTICA_TRAECLI_MODEL)
- daemon.go: inline the runtime brief (traecli reads .trae/rules/, not
AGENTS.md) and surface the runtime as "Trae" (providerDisplayName)
- execenv: AGENTS.md + .traecli/skills wiring; ~/.traecli/skills local root
- packages/core mcp-support: traecli consumes mcp_config
- frontend: official Trae provider logo
- docs: providers.mdx matrix + section, CLI_AND_DAEMON.md, README
Tests: fake-ACP unit tests matching the real wire format (streaming,
blocked-arg filtering, session/set_model failure, session/load resume) plus a
gated real-binary smoke test (TestTraecliRealACPSmoke) that skips when traecli
is absent or not logged in. Built-in provider only (mirrors qoder): not in
SupportedTypes / RUNTIME_PROFILE_PROTOCOL_FAMILIES, so no migration is needed.
Resolves#4376.
Every Slack reply was prefixed with process narration like '我先读取 Slack 频道概览,
再打开相关线程…' before the actual answer — the model announcing the history reads
the channel-awareness prompt tells it to do. That narration is internal
context-gathering, not part of the answer.
Add an instruction to the channel-awareness block: do the reads silently and
reply with the answer only, no preamble about what it is about to read or just
read.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Replaces the single scoped `multica chat history --scope` read with two clean
noun-commands so the agent can navigate a channel with many threads (e.g. read
the specific thread a user referred to):
- `multica chat history` — the channel OVERVIEW: recent top-level messages, each
thread tagged with thread_id + reply_count + latest_reply (it does NOT expand
thread contents). Backed by GET /api/chat/history + slack.History.ChannelOverview
(conversations.history).
- `multica chat thread [id]` — read one thread: no id = the thread you're in,
an id = a specific thread IN THE SAME channel. Backed by GET /api/chat/thread +
slack.History.Thread (conversations.replies; DM falls back to history).
The channel stays server-pinned to the session; a thread id is only a
within-channel locator, so the security boundary (no cross-channel reads) is
unchanged. `--scope` is removed.
The prompt now teaches both commands and, via a new chat_in_thread signal
(derived from the binding: last_thread_id != last_message_id), tells the agent
which to start with — `chat history` for a top-level @mention, `chat thread` for
an in-thread one.
Tests: slack ChannelOverview/Thread (current/by-id/DM-fallback/no-binding/clamp),
handler both endpoints + auth, prompt top-level vs in-thread guidance.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The backend service environment block in docker-compose.selfhost.yml
forwarded MULTICA_LARK_SECRET_KEY but omitted MULTICA_SLACK_SECRET_KEY,
so the variable set in .env never reached the container and Slack
integration stayed disabled ("slack integration disabled
(MULTICA_SLACK_SECRET_KEY not set)"). Add the missing passthrough.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The standalone 'Manage access' button on the autopilot detail header was
redundant — anyone who cannot open Edit also cannot manage access. The
first attempt folded it into the edit dialog's sidebar, which read as
cluttered. This instead surfaces it as a compact 'Manage access' button in
the edit modal header that opens a popover with the grant/revoke list.
- Extract the access UI into a reusable AutopilotAccessManager (no Dialog)
- Render it inside a header Popover in edit mode, gated on canManageAccess
- Drop the detail-page button, ManageAccessDialog, and the now-dead
detail.manage_access i18n key (access.* keys are reused by the popover)
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Remove the standalone 'Manage access' button from the autopilot detail
header and surface the grant/revoke list as an 'Access' section inside
the Edit dialog's configuration sidebar. Anyone who cannot open Edit
already cannot manage access, so the separate affordance was redundant.
- Extract the dialog body into a reusable AutopilotAccessManager
- Render it in edit mode only, gated on canManageAccess
- Drop ManageAccessDialog and its now-dead i18n keys
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The Issues board header 'x working' chip derived its count from the set
of distinct running agent_ids, so two agents on the same issue read as
'2 working'. Count distinct issue_ids instead so the number reflects how
many issues agents are working on — matching the filter the chip toggles,
which already narrows the list to those issues. The avatar stack still
shows the distinct agents behind that work.
Adds workspace-agent-working-chip.test.tsx covering the multi-agent /
single-issue case, multi-issue counting, scopedIssueIds filtering, and
the empty state.
Fixes MUL-3875
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): add v0.3.33 entry for the 2026-06-30 release (MUL-3889)
Adds the v0.3.33 release entry to the /changelog page in all four
landing locales (en, zh-Hans, ja, ko), covering the 16 user-visible
changes that landed on main since v0.3.32 (2026-06-29 release).
The entry groups changes into Features / Improvements / Bug Fixes,
using product-language phrasing per the team convention (no
"modified function X" style notes). The Chinese version follows the
team's localization convention: `agent` → `智能体`, `Squad` → `小队`,
while `Issue` stays as-is as the canonical product term.
Release highlights:
- feat(autopilot): View/Write permission layer + Manage Access (MUL-3807)
- feat(slack): unified chat history backfill (MUL-3871) and typing
reaction on inbound messages (MUL-3874)
- feat(skills): import skills from a .skill/.zip archive (MUL-3865)
- feat(cli)!: drop short UUID prefix resolution for `multica issue`
(MUL-3838)
- feat(views): Agents page mobile friendly (MUL-3873)
- improvement: rewrite of the comment routing cascade
(MUL-3794 + MUL-3879 follow-up)
- improvement: docs swap removed Gemini for CodeBuddy (MUL-3861) and
remove 117 dead _one i18n keys (MUL-3877)
- improvement: self-host preflight allows newer Docker Compose
- fix(daemon): reconcile in-flight task and workspace state on WS
reconnect (community contribution, closes#4665)
- fix(agent): recover Antigravity reply from transcript when stdout
is empty (MUL-3726)
- fix(server): skip CLIENT SETNAME for managed Redis compatibility
(MUL-3848, community contribution)
- fix(views): count tasks, not agents, in activity hover header
(MUL-3872)
Verified via the existing `apps/web typecheck`, vitest landing
suite (changelog-page-client.test.ts among them), and eslint on the
i18n directory; all green.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.3.33 entry wording (MUL-3889)
Per feedback that the first draft read too verbose for the public
changelog, trim every bullet of the v0.3.33 entry to one short
sentence and drop the supporting clauses that were rehashing
implementation detail (contributor handles, issue numbers, "30s
ticker" specifics, byline of what the rewrite incidentally fixed).
The net effect is a tighter list that matches the cadence of the
v0.3.32 / v0.3.31 entries already on the page.
Applied identically across en.ts / zh.ts / ja.ts / ko.ts.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Before this, the chat prompt only carried a generic, always-on hint ('if this
came from a chat channel...'), and the task carried no channel signal — so the
agent never definitively knew it was inside Slack. For an ambiguous ask like
'what did you just talk about', it could read Multica instead of the Slack
conversation.
- Thread a chat_channel_type ('slack') signal: the server sets it on the chat
task response when the session has a Slack binding
(GetChannelChatSessionBindingBySession); the daemon Task carries it.
- buildChatPrompt now emits an EXPLICIT block only when channel-backed: 'You are
operating inside a Slack conversation … this conversation and its history live
in Slack, NOT in Multica … read it with multica chat history, do NOT look in
Multica.' Web-only chat sessions get no such block (their history is the
Multica chat_session the agent already resumes).
Tests: slack-backed prompt asserts the explicit Slack/“NOT in Multica”/command
copy; web-only prompt asserts the block is absent.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(slack): add unified `multica chat history` pull for channel backfill (MUL-3871)
Agents @mentioned in a Slack thread/channel only saw the triggering message,
never the prior conversation (GitHub #4717). Instead of force-assembling a
recent-context block on every inbound (the Feishu approach), expose a single
channel-agnostic pull command the agent runs on demand.
- channel: normalized HistoryMessage/HistoryPage/HistoryOptions vocab so the
agent sees one shape regardless of platform.
- slack.History: resolves session -> binding -> installation -> bot token and
reads conversations.replies (real thread) or conversations.history (DM /
top-level channel, capturing sibling messages). thread_ts is recorded on the
binding config at session creation to pick the right call.
- handler GET /api/chat/history: authorized purely by the task-scoped token
(stamped X-Task-ID -> the task's own chat session), so an agent can only read
the conversation it is currently running for.
- multica chat history CLI command (no args; same for every channel).
- buildChatPrompt nudge so the agent discovers the command.
Feishu is intentionally untouched. Adding a platform = implement the reader.
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): require task-token actor source on chat history endpoint
Niko's review caught a privilege-boundary hole: the endpoint trusted
X-Task-ID, but it is mounted under the general Auth group where a normal
JWT / mul_ PAT request does NOT strip a client-forged X-Task-ID — only the
mat_ task-token branch stamps it. A workspace member who knew a chat task id
could forge the header and read that task's Slack channel/DM/thread history.
Gate on the server-set X-Actor-Source == "task_token" (the Auth middleware
deletes any client-supplied value and re-stamps it only on the mat_ branch),
then trust X-Task-ID. Adds a regression test: a forged X-Task-ID without the
task-token actor source is rejected with 403 and never reaches the reader.
Co-authored-by: multica-agent <github@multica.ai>
* fix(slack): thread-first history for follow-ups, channel for first turn (MUL-3871)
A Slack conversation has two nested histories: the surrounding channel and the
agent's own thread (the bot's first reply opens a thread on the @mention). The
first version picked replies-vs-history from a thread_ts fixed at session
creation, so a session started by a top-level @mention always read CHANNEL
history — even on follow-ups inside the bot's thread, which should read THREAD
history first.
- Add a HistoryScope (auto|thread|channel). The handler resolves auto:
first turn (no prior bot reply) -> channel; follow-up -> thread. The agent can
override with --scope channel|thread, and the response reports the scope read.
- The thread root is derived from the binding (last_thread_id / composite-key
suffix), available for every engaged group session, instead of the
creation-time thread_ts (now removed from the binding config).
- A DM degrades a thread request to channel history (DMs have no threads).
- Prompt guidance + CLI help updated to explain the policy.
Tests: scope selection (thread/channel/DM-fallback/no-root), root derivation,
and handler auto-resolution (first->channel, follow-up->thread, explicit
override).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>