3493 Commits

Author SHA1 Message Date
Bohan Jiang
b0246ef18a fix(lark): hide only the Lark (international) connect entry; keep Feishu (#3835)
Mainland Feishu binding works; only the newly-added Lark (international,
open.larksuite.com) install path is unreliable — some Lark installs
complete on Lark's side but never persist a lark_installation row (no WS,
no inbound, no task). Hide just the "Bind to Lark" CTA behind a single
LARK_INTL_CONNECT_ENABLED flag and leave the "Bind to Feishu" entry, the
settings panel, and all existing-installation management untouched.

Flip LARK_INTL_CONNECT_ENABLED back to true to restore the Lark CTA;
nothing else changes. Temporary measure while the Lark install-landing
bug is investigated.

- LarkAgentBindButton: the Lark button is gated by the flag; the Feishu
  button and the Connected badge / Manage / Disconnect are unchanged.
- Tests: the CTA tests assert Feishu shown + Lark hidden; the Feishu
  click-to-begin (region=feishu) test stays; the Lark click test was
  removed (no button) and noted for restore; the dialog polling-error
  tests open via the Feishu CTA.

MUL-3083

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.17
2026-06-05 19:01:13 +08:00
Multica Eve
a3203e9628 docs: clarify Feishu changelog copy (#3834)
* docs: remove Lark notes from June 5 changelog

Co-authored-by: multica-agent <github@multica.ai>

* docs: clarify Feishu changelog copy

Co-authored-by: multica-agent <github@multica.ai>

* docs: clarify June 5 changelog title

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 18:58:08 +08:00
Bohan Jiang
f8bd1d8fc2 feat(lark): show real speaker names in Feishu group context (MUL-3084) (#3828)
* feat(lark): resolve real speaker names in group context (MUL-3084)

The recent-context block (and quoted/forwarded blocks) labeled senders
positionally as "User 1 / User 2", and the agent had no idea who had
@-mentioned it. Add APIClient.BatchGetUsers (contact/v3/users/batch) and,
on the group prefetch path, resolve the surrounding speakers AND the
trigger sender to display names in one batch call. Speakers now render as
"[Alice]: ..." and the user's own message as "[Charlie]: ..." so the
agent knows who addressed it. Unresolved senders (restricted contact
scope, deactivated user) fall back to positional "User N"; resolution is
best-effort and never blocks ingestion. Closes the standing speaker-name
TODO in the enricher.

Co-authored-by: multica-agent <github@multica.ai>

* fix(lark): resolve names for quoted/forwarded senders too (review)

Address the #3828 review: BatchGetUsers only included the recent-window
and trigger senders, so a quoted parent / merge_forward child whose
sender was NOT in the recent window still rendered as "User N".

Restructure Enrich into fetch (Phase 1) -> resolve names (Phase 2) ->
render (Phase 3): quote/forward items are now fetched up front and their
senders folded into the single Contact batch, so every block (recent +
quoted + forwarded) shows real names in group chats. p2p keeps positional
labels. Replaces the fetch+render renderQuoted/renderForwarded with a
render-only renderQuotedBlock plus an inline forward fetch.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 18:32:31 +08:00
Multica Eve
05e38e5d37 feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083) (#3832)
* feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083 follow-up)

The single "Bind to Lark" button began the device flow against
accounts.feishu.cn and relied on a mid-poll tenant_brand="lark" to
auto-switch international users over to accounts.larksuite.com. Lark
users had to scan a QR served from a Feishu domain first, which
surfaced as confusing in real use.

Replace with two explicit CTAs side by side — "Bind to Feishu" and
"Bind to Lark" — and route the device-flow begin straight to the
matching accounts host based on the user's choice. The mid-poll
auto-switch is preserved as a safety net for users who pick the wrong
entry.

Backend
- RegistrationClient.Begin(ctx, namePreset, region): POSTs to
  c.cfg.LarkDomain when region=lark, c.cfg.Domain otherwise. Empty /
  unknown region falls back to Feishu (matches RegionOrDefault).
- BeginInstallParams.Region threads through to the registration session
  and onto runPolling's initial region local. SwitchedDomain still
  flips it on tenant_brand=lark.
- POST /api/workspaces/{id}/lark/install/begin accepts ?region=feishu|lark
  with empty defaulting to feishu for back-compat.

Frontend
- api.beginLarkInstall(wsId, agentId, region) — region now required
  so every call site is forced to pick a cloud explicitly.
- LarkAgentBindButton renders two buttons; dialog state collapsed into
  a single dialogRegion useState so an "open but with no region picked"
  intermediate state can't exist.
- LarkInstallDialog takes region as a required prop and renders
  region-aware copy (title, description, scan hint, link fallback,
  success toast).

i18n
- Add bind_button_{feishu,lark}, install_dialog_{title,description}_*,
  install_scan_hint_*, install_open_link_fallback_*, and
  install_success_toast_* keys across en, zh-Hans, ja, ko. Legacy
  single-region keys are kept for now; nothing in the tree references
  them anymore but a follow-up cleanup can remove them once the dust
  settles.

Tests
- Two new lark.RegistrationClient tests pin region routing in both
  directions (region=lark hits LarkDomain; region=feishu hits Domain).
- Two new lark-tab.test.tsx cases pin that clicking each CTA calls
  beginLarkInstall with the matching region argument. Existing CTA
  tests updated to expect both buttons in place of one.

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

* fix(lark): bidirectional tenant_brand swap + region-aware badge + link context menu

Addresses Elon's review on PR #3832 plus a separate report that the
"Or tap here to open in Lark" link in the install dialog had no
standard right-click affordances on the desktop app.

Backend (must-fix from review)

The PR's stated 'safety net for users who pick the wrong CTA' only
worked one direction: a Feishu-first begin already swapped to Lark on
tenant_brand=lark, but the new Lark-first begin (added by this same PR)
had no reverse path — a user who picked 'Bind to Lark' but actually
authorized with a Feishu account would carry RegionLark all the way
through finishSuccess and either fail at GetBotInfo or commit a
wrong-region row.

- PollResult now carries SwitchedDomain AND SwitchedRegion in
  lockstep, so the caller never has to re-derive region from the
  domain string.
- Poll() detects tenant_brand=feishu while polling against a non-Feishu
  host symmetrically with the existing tenant_brand=lark check, gated
  on the current host so we don't loop on a brand we already match.
- runPolling reads region from res.SwitchedRegion instead of the
  hardcoded RegionLark — the SwitchedDomain branch now flips both
  feishu→lark and lark→feishu cleanly.
- Tests: updated the existing TestRegistrationClient_Poll_DomainSwitchOnLarkTenant
  to assert SwitchedRegion, added TestRegistrationClient_Poll_DomainSwitchOnFeishuTenant
  for the reverse, and TestRegistrationClient_Poll_NoSwitchWhenAlreadyOnMatchingHost
  (table-driven, both directions) to pin that the gate doesn't loop.

Backend (nit from review)

Handler comment on /lark/install/begin claimed unknown region defaults
to Feishu downstream, but the handler already returns 400 on unknown
values. Updated the comment to match the actual behavior and document
why we 400 rather than silently normalize (so a frontend typo can't
land users on the wrong cloud without telling them).

Frontend (nit from review)

The Agent inspector's Connected badge was hardcoded 'Connected to
Lark' / 'Manage in Lark' (en) and 'Connected to Feishu' / 'Manage in
Feishu' (zh-Hans) — both wrong half the time now that the install
flow can land on either cloud per agent. Made the badge text and
Manage tooltip read from installation.region:

- agent_bot_connected_label_{feishu,lark}
- agent_bot_manage_link_{feishu,lark}
- agent_bot_manage_tooltip_{feishu,lark}

across en / zh-Hans / ja / ko. Legacy single-region keys retained for
safety. Existing badge tests updated: fixtures without 'region' now
expect the Feishu copy; the region: 'lark' test was promoted to also
assert the Lark badge text and link target. 21/21 lark-tab tests pass.

Desktop (separate report)

Right-clicking an <a> in the renderer surfaced only Copy / Cut /
Paste / Select All — no 'Open Link in Browser' or 'Copy Link Address'.
The renderer's <a target="_blank"> click path already routes through
setWindowOpenHandler → openExternalSafely, but discoverability via the
context menu was missing.

context-menu.ts now appends two link-specific items when params.linkURL
is an http(s) URL. Open Link routes through openExternalSafely (reuses
the existing scheme allowlist); Copy Link Address writes to Electron's
clipboard. Labels are localized to the OS preferred language for the
four locales the renderer ships (en / zh-Hans / ja / ko); zh-* variants
all route to zh-Hans, anything else falls back to English. New
context-menu.test.ts pins five cases: link items show for http(s),
not for javascript:/mailto:/etc., not when no link is under the cursor,
zh-CN gets Chinese, fr-FR falls back to English. 198/198 desktop tests
pass.

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Jiang Bohan <bhjiang@outlook.com>
2026-06-05 18:30:19 +08:00
Multica Eve
887d7c2a5e docs: add June 5 changelog entry (#3829)
* docs: add June 5 changelog entry

Co-authored-by: multica-agent <github@multica.ai>

* docs: update June 5 changelog title

Co-authored-by: multica-agent <github@multica.ai>

* docs: mention comment composer cleanup in changelog

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 17:52:54 +08:00
Naiyuan Qing
ef8dabd35d feat(lark): split agent integration UI into inspector status + tab actions (#3830)
The agent Lark binding surfaced the same connect/disconnect affordance in
two places on one page — the left inspector's INTEGRATIONS section and the
right pane's Integrations tab both rendered the full LarkAgentBindButton,
so the destructive Disconnect lived in two spots.

Split by role:
- Inspector (left): a compact, read-only status row (green dot + region
  chip + "Connected to Lark") that deep-links into the Integrations tab.
  New LarkAgentBotStatusRow, opted into via LarkAgentBindButton's
  onShowConnectedDetails prop.
- Integrations tab (right): keeps the full badge, now the single home for
  Manage / Disconnect. The badge itself is reworked to a two-row layout —
  status (left) + soft `destructive`-variant Disconnect (right) on row 1,
  "Manage in Lark" demoted to a muted secondary link on row 2.

Cross-sibling navigation goes through a one-shot navIntent channel on
AgentOverviewPane that routes via requestTabChange, so the unsaved-changes
guard still fires when jumping from the inspector.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 17:35:52 +08:00
Bohan Jiang
659ac2d99a Revert "docs: add db-backed scheduler RFC (#3701)" (#3831)
This reverts commit 23a69f7682.
2026-06-05 17:33:22 +08:00
Naiyuan Qing
6a9e07f6d6 fix(issues): remove comment composer expand control [MUL-3086] (#3818)
* fix(issues): remove comment composer expand control

Co-authored-by: multica-agent <github@multica.ai>

* feat(issues): auto-grow composers and highlight reply submit when ready

- Drop max-height cap on comment + reply composers so they grow with content
- Reply send button turns primary when there is submittable text

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>
2026-06-05 17:24:27 +08:00
Multica Eve
14f89bc08a Fix Claude control request handling (#3827)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 17:14:33 +08:00
Multica Eve
5586b5d46a feat(lark): unbind affordance on the agent connected badge (MUL-3090) (#3826)
Surface a Disconnect/Unbind action in LarkAgentBotConnectedBadge so
owners and admins can remove a Lark Bot binding directly from the
agent inspector — no detour to Settings. The button sits next to the
existing 'Manage in Lark' link and is intentionally rendered as a
quieter muted-foreground control with a hover-destructive accent so
it doesn't compete visually but stays discoverable.

Confirmation is mandatory: a small AlertDialog reuses the existing
disconnect_confirm_* i18n strings. The action calls
api.deleteLarkInstallation, invalidates larkKeys.installations(wsId)
on success so the parent re-renders the Bind CTA, and toasts
success/failure. Cancel is disabled while the request is in-flight
to prevent racing the close.

Tests cover button visibility, confirm gating, success path (delete
called with correct args, cache invalidated, toast), error path (no
invalidate, toast.error), and Cancel-disabled behaviour.

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 17:02:58 +08:00
Xinmin Zeng
270d177475 fix: broken "Add a computer" command on Multica Cloud + two CLI amplifiers (MUL-3087) (#3817)
* fix(server): recognize official cloud by frontend host in daemon setup config

The 'Add a computer' dialog builds its command from /api/config's
daemon_server_url/daemon_app_url, falling back to 'multica setup' when
both are empty. The official cloud is meant to omit them, but the
omission only fired when MULTICA_PUBLIC_URL=https://api.multica.ai. When
that env is unset the server URL defaults to the frontend origin and the
old guard (which required serverURL host == api.multica.ai) didn't match,
so the dialog emitted 'multica setup self-host --server-url
https://multica.ai' — pointing the daemon backend at the frontend (no
/health, no WebSocket proxy).

Identify the official cloud by its frontend host alone (multica.ai /
app.multica.ai) so a missing or misconfigured MULTICA_PUBLIC_URL can no
longer leak the broken self-host command. Regression from #3474.

* fix(cli): probe before persisting self-host config to preserve auth on failure

setup self-host wrote a fresh CLIConfig{ServerURL, AppURL} (a full
overwrite that drops the saved token) and only then probed the server,
returning early on failure. A failed probe therefore logged the user out
and left them unconnected, with no recovery in the same command.

Probe first via persistSelfHostConfigIfReachable: an unreachable server
leaves the existing config — and its token — untouched (failed setup =
no-op). The prober is injected so both branches are unit-tested.

* fix(daemon): serve health before preflight so daemon start readiness is accurate

The CLI's 'daemon start' polls the health endpoint for 15s expecting
status=running, but the daemon only began serving health after
preflightAuth, whose initial workspace sync detects every configured
agent's version by exec'ing it (~20s cold with 8 agents). Health served
too late, so a perfectly healthy daemon printed 'may not have started
successfully'.

Start the health server right after resolveAuth (which still fails fast
on a missing token) and before the slow preflight, so readiness reflects
the daemon core being up rather than agent-version detection finishing.

* fix(daemon): gate /health readiness so daemon start can't report a false start

Serving health before preflightAuth fixed the false-negative (a healthy
daemon printed "may not have started"), but health still returned
status:"running" unconditionally — before preflight (PAT renew + workspace
sync + runtime registration) had completed. `daemon start` and the desktop
treat "running" as ready, so a slow or *failing* preflight could be
misreported as a started daemon: setup prints "connected", then the process
exits or hangs in agent-version detection with no runtime registered. That
is harder to diagnose than the original false-negative.

Split liveness from readiness: bind/serve the health port early (so callers
see a live "starting" daemon instead of connection-refused), but report
status:"starting" until d.ready is set after preflight, then "running".

- daemon.go: add d.ready (atomic.Bool); set it true after the background
  loops launch, before pollLoop.
- health.go: healthHandler reports "starting" until ready, else "running".
- cmd_daemon.go: `daemon start` waits for "running" with a deadline raised
  to 45s (covers cold-start agent detection) and a clearer "still starting"
  message; new daemonAlive() helper treats both "running" and "starting" as
  a live daemon, so the already-running guard, restart, and stop act on a
  starting daemon and don't double-spawn or race its listener; `daemon
  status` shows "starting" distinctly.

Older CLIs/desktop that only know "running" safely treat "starting" as
not-ready (status != "running"), so no boundary break.

Tests: health reports starting-then-running; daemonAlive truth table.
Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): handle daemon "starting" health status in lifecycle

The daemon now reports /health status:"starting" until preflight completes
(liveness/readiness split). That made "starting" a new external contract of
/health, but the Desktop daemon-manager only knew "running", so the readiness
fix would have moved the CLI's false-negative into a Desktop start regression:

- `daemon start` now blocks up to 45s waiting for readiness, but the Desktop
  spawned it via execFile({ timeout: 20_000 }). On a cold start (the ~20s agent
  detection this PR targets) Electron killed the CLI supervisor at 20s and
  reported a start failure, even though the detached daemon child kept booting —
  the UI flashed "stopped" then "running". Raise the timeout to 60s (must exceed
  the CLI's 45s startupTimeout).
- The Desktop treated only raw status === "running" as a live daemon, so a
  daemon that was still "starting" (booting on its own or started via the CLI)
  showed as "stopped", and startDaemon() would spawn a second one — which the new
  CLI rejects as "already running", surfacing as a start error.

Add daemonStatusAlive() (shared, pure, unit-tested) mirroring the Go daemonAlive()
and use it for liveness: fetchHealth() surfaces a daemon-reported "starting" as
state "starting" regardless of our own currentState; startDaemon()'s
already-running guard and the restart-on-user-switch guard treat "starting" as an
existing daemon. version-decision stays gated on "running" (readiness, not
liveness) — unchanged.

Verified: desktop typecheck, eslint, full vitest suite (193 tests) all pass.
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 17:01:23 +08:00
Bohan Jiang
6d0b9e3918 feat(lark): prefetch surrounding group context on @-mention (MUL-3084) (#3819)
* feat(lark): prefetch surrounding group context on @-mention (MUL-3084)

In Feishu group chats the Bot only saw the single message that @-mentioned
it — never the surrounding conversation — because the inbound enricher only
inlined context the user explicitly attached (a quoted reply or a
merge_forward), and the API client had no way to list a chat's history.

Add APIClient.ListChatMessages (GET /open-apis/im/v1/messages,
container_id_type=chat, ByCreateTimeDesc, page_size clamped to Lark's 50
cap) and, for a group message addressed to the Bot, prefetch a bounded
window of recent messages and inline them as a <recent_context> block
ahead of the user's own message. The trigger and any quoted parent are
excluded so nothing is duplicated; speakers are labeled positionally
(User 1/2 / Bot); failures degrade to a visible placeholder and never
block ingestion. Window size is configurable via
InboundEnricherConfig.RecentContextSize (<=0 disables); production wires
DefaultRecentContextSize (20). One list call per addressed turn keeps the
fetch within the inbound ACK / EnrichTimeout budget.

Co-authored-by: multica-agent <github@multica.ai>

* feat(lark): anchor group context window to trigger time, default 10

Address review feedback on MUL-3084:
- Anchor the recent-context prefetch to the trigger message's time:
  thread the message create_time through InboundMessage and pass it as
  the list end_time (millis -> seconds), so the window is the
  conversation up to the @-mention rather than whatever is newest when
  the slightly-later prefetch HTTP call runs. end_time is omitted when
  the time is missing/unparseable (falls back to newest N).
- Lower DefaultRecentContextSize from 20 to 10.

Co-authored-by: multica-agent <github@multica.ai>

* docs(lark): clarify recent-context persistence stance and fetch-window semantics

Co-authored-by: multica-agent <github@multica.ai>

* fix(lark): region-aware doJSON for ListChatMessages after rebase

origin/main merged #3815 (Lark dual-region support), which changed
doJSON to take a per-call baseURL resolved via resolveBaseURL(creds).
Adapt the new ListChatMessages call to that signature so the backend
build passes against latest main, and refresh the now-stale
ListMessagesParams comment (EndTime is exposed).

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 16:37:49 +08:00
Naiyuan Qing
25104d1855 perf(editor): parse large markdown in chunks to fix O(n²) freeze (#3823)
@tiptap/markdown parses via marked, whose tokenizer is O(n²) in document
length. Opening a large markdown doc (issue description, agent
instructions, …) froze the UI for tens of seconds: a 533KB plain-text doc
took 61.8s to parse while the subsequent ProseMirror setContent was only
40ms. Upgrading marked doesn't help — already on 17.0.5, whose fix only
covers `_`/`*` delimiter runs, not general prose.

Parse large markdown in chunks instead of in one shot: split on blank
lines outside fenced code blocks, parse each chunk independently, then
concatenate the resulting docs. This drops marked's cost to O(n²/k) while
producing a byte-identical document. Applied transparently at
ContentEditor's two parse entry points (mount + WS-driven re-parse), gated
at 50KB so normal small docs stay on the single-parse fast path.

533KB: parse 61.8s -> 0.95s (65x), open 100s -> 3.2s (31x).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:28:41 +08:00
Bohan Jiang
a9a9e93905 fix(core): scope inbox notification mute check to the source workspace (#3821)
Follow-up to #3797. The inbox:new handler keys the notification-preference
query on item.workspace_id, but the request itself still resolved its
workspace from the active-workspace X-Workspace-Slug header. On a cold
cache, a user viewing workspace B who received a workspace-A notification
read B's mute setting and cached it under A's key — so A's banners could
fire while muted (and vice-versa), polluting A's cache.

Add an optional workspaceSlug override to getNotificationPreferences and
notificationPreferenceOptions, and pass the resolved source slug from the
inbox:new handler. When the source slug can't be resolved, read only an
already-warm cache instead of fetching with the wrong workspace. Tests
cover the cold-cache source-slug fetch, source mute suppression, and the
no-fallback guard.

MUL-3062

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 16:05:49 +08:00
Bohan Jiang
6ac8314711 feat(lark): support both Feishu and Lark from one deployment (MUL-3083) (#3815)
* feat(lark): serve Feishu and Lark from one deployment, per installation

The Lark integration was locked to a single open-platform host chosen
deployment-wide (MULTICA_LARK_HTTP_BASE_URL / _CALLBACK_BASE_URL,
defaulting to open.feishu.cn), so one deployment could talk to only the
mainland Feishu cloud OR Lark international — never both. Teams on the
other tenant could not use the integration at all.

Make the host per-installation. The device-flow installer already
auto-detects the tenant (Lark emits tenant_brand="lark" mid-poll); we now
persist that as lark_installation.region, carry it on
InstallationCredentials.Region, and resolve the open-platform host per
call (REST + WS bootstrap) from the region. An explicit cfg.BaseURL
(env / httptest) still overrides every region, so existing tests and
staging/proxy setups keep working.

- migration 116: lark_installation.region TEXT NOT NULL DEFAULT 'feishu'
  CHECK (region IN ('feishu','lark')) — existing rows are all mainland.
- lark.Region enum + OpenPlatformBaseURL/RegionOrDefault helpers.
- registration: thread the detected region into finishSuccess so the
  install-time GetBotInfo hits the right cloud AND the row records it.
- every credential-build site (patcher, replier, WS provider, union_id
  backfill) copies region off the installation row.
- region is part of the WS supervisor fingerprint so a re-install that
  switches cloud restarts the connection.
- API: surface region on the installation listing DTO.

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

* feat(lark): surface installation region in settings UI

Read the per-installation region off the listings response: build the
"Manage in Lark" dev-console host from it (open.feishu.cn vs
open.larksuite.com instead of a hardcoded mainland host) and render a
Feishu / Lark badge on each connected bot. The field is optional and
defaults to Feishu when an older server omits it (API-compat). Adds the
region_feishu / region_lark labels to all four locales.

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

* docs(lark): document simultaneous Feishu + Lark support

The cloud each bot belongs to is now auto-detected at install and stored
per installation, so one deployment serves both. Replace the old
"point MULTICA_LARK_HTTP_BASE_URL at larksuite for international tenants"
guidance (now just an optional override) in all four locales.

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

* fix(lark): repair legacy Lark-international installs on upgrade

Review follow-up (MUL-3083). Migration 116 backfilled every existing
lark_installation to region='feishu', assuming all historical rows were
mainland. But self-host deployments could already run Lark international
via the deployment-wide MULTICA_LARK_HTTP_BASE_URL override, so those
rows are really Lark — clearing the override after upgrade (which the new
docs invite) would route them to open.feishu.cn and break them.

Add a one-shot startup repair, BackfillRegionFromLegacyOverride, fired
off the hot path like BackfillBotUnionIDs: when the deployment's global
base-URL override targets open.larksuite.com, relabel the still-default
'feishu' rows to 'lark'. Gating on the deployment-wide override is what
makes it safe — every pre-existing install on such a deployment was Lark.
Idempotent; no-op on mainland / fresh deployments. Verified end-to-end
against a scratch DB (flip then 0-row idempotent re-run).

Also document that a Lark/飞书 app_id is globally unique across both
clouds, which is what makes the app_id-keyed token cache and the
UNIQUE(app_id) constraint safe across regions (review nit).

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

* docs(lark): fix ops guidance to match auto per-installation region

Review follow-up (MUL-3083). .env.example and docker-compose.selfhost.yml
still told operators that international Lark requires pointing both base
URLs at open.larksuite.com — now wrong, and it would push a fresh
deployment back into a single-cloud override. Rewrite them: the base
URLs are optional deployment-wide overrides; normal dual-cloud operation
keeps them empty. Document the first-boot auto-relabel for deployments
migrating off the old single-cloud override, across the integration docs
(en/zh/ja/ko).

MUL-3083

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 16:03:13 +08:00
Nguyễn Phúc Lương
93b93f58b5 fix(desktop): route inbox notifications to the item's source workspace (#3797)
Resolves the desktop inbox notification slug from the item's own workspace_id, routes the click through the navigation adapter for a real workspace switch, and invalidates the source workspace's inbox cache. Follow-up: mute-preference fetch should also target the source workspace.

Closes #3766
2026-06-05 15:51:58 +08:00
Bohan Jiang
2e50df9a6a perf(analytics): report $pageview at section granularity, drop web query-string churn (#3813)
capturePageview now section-normalizes the path (strip query/hash, collapse
UUID and issue-key resource segments) and dedupes consecutive same-section
views, so navigating between issues/agents/etc. no longer fires a billed
PostHog event per resource. The web tracker keys on pathname only (not
searchParams), removing ~17% pure query-string-churn pageviews and keeping
OAuth code/state out of $current_url.

MUL-3081

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:33:32 +08:00
Multica Eve
905ebbdde1 fix(github): populate connected account name on install [MUL-3078] (#3811)
* fix(github): populate connected account name on install [MUL-3078]

The Settings → GitHub connection card was rendering 'Connected to
unknown' because:

1. fetchInstallationAccount in the setup callback hit GitHub's
   /app/installations/{id} endpoint unauthenticated. That endpoint
   requires App JWT auth; the call returned 401, and the function
   fell through to the 'unknown' placeholder which was persisted as
   account_login.
2. The installation webhook handler did upsert the row with the real
   login when GitHub later delivered installation.created, but it
   never published a github_installation:created event. The frontend
   query stayed stale, so the UI kept showing 'unknown' even after
   the row had been refreshed.

Fix:

- Add optional GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY env vars. When
  set, signGitHubAppJWT mints a short-lived RS256 JWT (back-dated 60s
  for clock skew, capped at 9m to stay inside GitHub's 10m max) and
  fetchInstallationAccount uses it as a Bearer token. The setup
  callback now writes the real org/user name on install.
- When the new env vars are not configured, the call still falls
  through to 'unknown' as before — but the webhook handler now
  publishes EventGitHubInstallationCreated after the upsert, so the
  realtime listener invalidates the installations query and the UI
  converges to the real value within seconds, no manual refresh.

Tests cover JWT signing (claims, signature, malformed PEM, partial
config), fetchInstallationAccount with a JWT-gated httptest mock,
and the webhook refresh + broadcast on a seeded 'unknown' row.

Docs updated for .env.example and github-integration /
environment-variables in en, zh, ja, ko.

Co-authored-by: multica-agent <github@multica.ai>

* test(github): defuse JWT clock-bomb by injecting parser time [MUL-3078]

PR review caught that TestSignGitHubAppJWT_ClaimsAndSignature signed the
token with a fixed 'now' (2026-06-05T12:00:00Z) but parsed it with a
default jwt.Parse, which uses real time.Now() for exp validation. Once
real wall clock crossed the token's exp (now + 9m = 12:09:00Z), the
test would have flipped to a deterministic failure on every CI run.

Inject the same fixed 'now' into the parser via jwt.WithTimeFunc so
both signing and validation share one clock. Verified independently
that without the fix the parser rejects the token as 'expired', and
with the fix it accepts.

Also clarified the fetchInstallationAccount comment to be unambiguous
about what 'do not block' actually means: the HTTP call IS synchronous
(no independent timeout, pre-existing wart), but a failure here just
falls back to the unknown placeholder rather than aborting the
callback.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 15:21:22 +08:00
Bohan Jiang
3708fb0f07 fix(daemon): inactivity-based agent run timeout, no wall-clock guillotine (MUL-3064)
Active long-running sessions are no longer killed by a fixed wall-clock deadline. Liveness is delegated to the idle watchdog (MULTICA_AGENT_IDLE_WATCHDOG, default 30m) with a larger in-flight-tool budget (MULTICA_AGENT_TOOL_WATCHDOG, default 2h). MULTICA_AGENT_TIMEOUT is an opt-in absolute cap (default 0 = no cap). The server-side 2.5h sweeper is unchanged as a coarse backstop.

Fixes #3745.
2026-06-05 15:06:07 +08:00
Bohan Jiang
d6540a1869 fix(clipboard): support copy over http:// via execCommand fallback (#3810)
navigator.clipboard is only exposed in a secure context (https or
localhost). On self-hosted instances served over plain http:// it is
undefined, so every copy / "copy all" / export button silently failed and
left the clipboard empty (GitHub #3781).

Add a shared copyText(text): Promise<boolean> helper in
@multica/ui/lib/clipboard that prefers the async Clipboard API and falls
back to a hidden <textarea> + document.execCommand('copy') for non-secure
contexts. Migrate all direct navigator.clipboard.writeText call sites
(code blocks, agent transcript copy-all, token / webhook / issue-link
copy, etc.) to it, gating success side-effects on the returned boolean,
and remove the now-redundant copyMarkdown wrapper. Secure-context users
keep the native path unchanged.

MUL-3068

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:55:23 +08:00
Bohan Jiang
76dbb87762 fix(agent): standardize model-discovery timeouts to 15s, stop caching empty results
Raise pi and cursor model-list discovery timeouts 5s->15s to match opencode/ACP; openclaw stays 30s (sequential multi-spawn). Stop caching empty discovery results so a transient timeout doesn't keep the picker blank for the full TTL. Fixes #3729. MUL-2977.
2026-06-05 14:47:59 +08:00
LinYushen
3913bf9152 docs(self-host): default rollup to in-process scheduler, demote pg_cron/cron/systemd/CronJob to compat paths (#3808)
Rewrite the 'Usage Dashboard Rollup (Required)' section in SELF_HOSTING.md
and apps/docs/content/docs/getting-started/self-hosting.zh.mdx so that:

- The DB-backed in-process scheduler (sys_cron_executions, MUL-2957) is
  documented as the default for fresh self-host installs. The bundled
  pgvector/pgvector:pg17 image works as-is and no operator step is required.
- pg_cron, external cron, systemd timer, and Kubernetes CronJob are demoted
  to a 'Compatibility paths (existing deployments only)' subsection. They
  remain supported (advisory lock 4246 prevents double-writes) but are no
  longer the recommended setup.
- A retirement sequence is added for production environments that already
  have a pg_cron job: confirm in-process SUCCESS rows in sys_cron_executions,
  then cron.unschedule the redundant entry; leave the pg_cron extension
  installed unless other workloads stop depending on it.
- The two upgrade callouts that pointed to the removed
  'Usage Dashboard Rollup -> Option C' anchor are repointed to
  SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup, which already documents
  the auto-hook backfill and the recovery flow.

Refs MUL-3077.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:31:14 +08:00
Bohan Jiang
62925b97f1 chore(cli): remove the --from-template flag from agent create (#3805)
* chore(cli): remove the --from-template flag from agent create

The `--from-template` CLI flag was an untaught, immature surface (the
built-in skill's source-map explicitly marked the template path "out of
scope"). It also silently ignored sibling create flags (--custom-env,
--mcp-config, etc.) by short-circuiting before body assembly. Remove the
flag and its runAgentCreateFromTemplate handler from the CLI.

Scope is CLI-only. The agent-template product feature stays intact:
- registry server/internal/agenttmpl/ (embedded curated templates)
- handler server/internal/handler/agent_template.go
- routes GET /api/agent-templates, GET /api/agent-templates/{slug},
  POST /api/agents/from-template
- the onboarding "create from template" flow (packages/views/onboarding)

The onboarding flow calls the API directly and does not depend on the
CLI flag, so removing the flag does not affect it.

Updates the multica-creating-agents source map accordingly.

MUL-3070

Co-authored-by: multica-agent <github@multica.ai>

* fix: correct source-map note on agent-template usage + guard --from-template

Review of #3805 (MUL-3070) flagged a factual error in the source-map note:
it claimed onboarding uses the agent-template backend. It does not.
`packages/views/onboarding/steps/step-agent.tsx` builds four hardcoded
local presets (i18n-resolved) and creates via plain `POST /api/agents`
(`createAgent`), never `POST /api/agents/from-template`. The whole
agent-template stack (registry, handler, routes, `packages/core` client +
query wrappers) is orphaned — the removed CLI flag was its only non-test
caller. Rewrite the note to say so.

Also add a regression test asserting `agent create` exposes no
`--from-template` flag, so it can't be silently re-added.

MUL-3070

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:21:11 +08:00
Multica Eve
23a69f7682 docs: add db-backed scheduler RFC (#3701)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 14:03:15 +08:00
Multica Eve
631fa015be fix(docs): stabilize localized usage rollup anchor [MUL-2957] 2026-06-05 13:55:00 +08:00
Naiyuan Qing
0dbe9f0a8f Move caret after inserted image uploads (#3796)
* fix editor image upload caret placement

Co-authored-by: multica-agent <github@multica.ai>

* feat(editor): reserve image box via intrinsic dimensions to kill paste layout shift (#3803)

Capture an image's intrinsic width/height on upload and render them as
<img width height> so the browser reserves the box before the image
decodes. Removes the layout shift that pushed the caret out of view after
a pasted-image insert, making the post-insert scrollIntoView correct.

- Add width/height node attrs to ImageExtension (render-only; not
  serialized to markdown, so round-trips stay clean).
- Measure dimensions off-thread via createImageBitmap and patch the node
  after insert. Fire-and-forget so the synchronous-insert contract
  (instant preview) is preserved; degrades to no-box when the API is
  unavailable (jsdom). The src swap keeps width/height via attr spread.
- Thread width/height through ImageView -> Attachment -> <img>.

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>
2026-06-05 13:48:36 +08:00
LinYushen
3caba86b09 feat(scheduler): DB-backed execution-record scheduler [MUL-2957] 2026-06-05 13:46:26 +08:00
Multica Eve
63b847ee48 Honor agent identity in assignment workflow (#3802)
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 13:45:43 +08:00
Bohan Jiang
18a5224fe8 feat(cli): add --mcp-config flags to agent create/update (#3799)
Agents already support an mcp_config field (consumed by the daemon →
provider at task time) and the agent-settings UI exposes an MCP tab, but
the CLI had no way to set it. This adds the missing CLI surface, mirroring
the existing custom-env pattern:

- `agent create` and `agent update` gain --mcp-config / --mcp-config-stdin
  / --mcp-config-file. The stdin/file channels keep MCP server tokens out
  of shell history and 'ps'; the three channels are mutually exclusive.
- The value is validated as a JSON object (or the literal `null` to clear,
  on update), matching the agent-settings MCP tab. Empty stdin/file input
  errors instead of silently clearing a secret-bearing field.
- Unlike custom_env, mcp_config IS settable via `agent update` — it is
  persisted through the generic UpdateAgent endpoint (no dedicated audited
  endpoint), so both create and update expose the flags.

Adds parser/resolver unit tests (incl. secret-leak sanitization) and
updates the multica-creating-agents built-in skill + source map.

MUL-3070

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 13:39:38 +08:00
Bohan Jiang
c9ceaee4d9 fix(agent): stop stripping user-facing CLAUDE_CODE_* config from child env (#3690)
* fix(agent): stop stripping user-facing CLAUDE_CODE_* config from child env

isFilteredChildEnvKey blanket-removed every CLAUDE_CODE_* var from the
spawned Claude Code child's environment. The intent was only to keep the
daemon's internal session markers from leaking, but CLAUDE_CODE_* is also
Anthropic's user-facing config namespace. On Windows this stripped the
user-set CLAUDE_CODE_GIT_BASH_PATH, so Claude Code could not locate
bash.exe, exited immediately, and every task failed with
"write claude input: write |1: The pipe has been ended."

Switch from prefixing the whole CLAUDE_CODE_ namespace to an exact-name
denylist of the internal runtime/session markers (CLAUDECODE,
CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID,
CLAUDE_CODE_TMPDIR, CLAUDE_CODE_SSE_PORT), still blanket-stripping the
wholly-internal CLAUDECODE_* namespace. Every other CLAUDE_CODE_* var
(GIT_BASH_PATH, USE_BEDROCK, USE_VERTEX, MAX_OUTPUT_TOKENS, ...) now
reaches the child. The internal-marker set was confirmed against the live
runtime, not guessed.

Fixes the whole class, not just git-bash: Bedrock/Vertex/etc. were
silently dropped the same way.

MUL-2940

Co-authored-by: multica-agent <github@multica.ai>

* fix(agent): keep CLAUDE_CODE_TMPDIR in child env

CLAUDE_CODE_TMPDIR is a documented, user-configurable temp-dir override
(public env-vars reference), not an internal per-session marker. Claude
Code creates its own per-session subdir under it, so inheriting it is
harmless — and stripping it would silently break a user's temp-dir
override the same way the broad prefix filter broke CLAUDE_CODE_GIT_BASH_PATH.

Drop it from the internal denylist (which now holds only the undocumented
per-process runtime markers: CLAUDECODE, CLAUDE_CODE_ENTRYPOINT,
CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_SSE_PORT) and
assert it reaches the child.

MUL-2940

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 12:29:55 +08:00
Naiyuan Qing
4779e24816 fix editor image markdown roundtrip (#3790)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-05 10:40:58 +08:00
Alex
4da43b383f fix: selfhost env does not accept LARK related env (MUL-3060) (#3771)
* fix: selfhost docker compose env does not accept LARK related env

* fix(selfhost): pass through MULTICA_LARK_CALLBACK_BASE_URL for international Lark

The inbound long-conn callback bootstrap reads MULTICA_LARK_CALLBACK_BASE_URL
(server/cmd/server/router.go buildLarkConnectorFactory ->
HTTPConnectionTokenFetcher), which defaults to open.feishu.cn with no
fallback to MULTICA_LARK_HTTP_BASE_URL. Without it forwarded into the
backend container, international Lark tenants can send (outbound HTTP via
MULTICA_LARK_HTTP_BASE_URL) but never receive messages — the bootstrap
still hits the mainland host.

Forward the var in docker-compose.selfhost.yml and document all three
Lark knobs in .env.example so operators can discover them from the
standard 'cp .env.example .env' onboarding path.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 19:49:50 +08:00
Bohan Jiang
f3ab29cdfc fix(lark): publish lark_installation:created at row-commit, not on status poll (#3770)
The agent Integrations tab's "已连接到飞书" connection badge only updated after
a manual page refresh. lark_installation:created had a single emit site — the
status-poll handler GetLarkInstallStatus — so it only fired while a browser was
actively polling the install dialog to success. Every other surface (a second
admin, the inspector sidebar, the Settings panel, or the installer whose dialog
closed before the success poll) never received the invalidation frame, and under
the QueryClient defaults (staleTime: Infinity) the installations cache stayed
stale until a full page refresh.

Publish the event from RegistrationService.finishSuccess at the row-commit point,
mirroring the already-correct revoke path, so every workspace client refreshes
the moment the install lands. Wire the bus via an optional SetEventBus (keeps the
constructor and its validation tests untouched, nil-safe) and remove the now-
redundant poll-handler emit.

MUL-3059

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 19:29:01 +08:00
Multica Eve
5b69331ad2 docs: add June 4 changelog entry (#3762)
* docs: add June 4 changelog entry

Co-authored-by: multica-agent <github@multica.ai>

* docs: refine June 4 changelog copy

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
v0.3.16
2026-06-04 17:51:48 +08:00
Naiyuan Qing
b0d479c6e7 fix: use mentions for chat context (#3755) 2026-06-04 16:45:10 +08:00
Bohan Jiang
0d38288dbd MUL-2926 feat(editor): support markdown checkbox task lists (#3593) (#3657)
* feat(editor): support markdown checkbox task lists (#3593)

Render `- [ ]` / `- [x]` as interactive checkboxes in the issue content
editor, matching GitHub / Notion.

- Register TaskList + a patched TaskItem in the shared extension factory.
  Both ship their own markdown tokenizer / renderMarkdown, input rules, and
  a checkbox NodeView; the taskList tokenizer is consulted before marked's
  built-in list tokenizer, so `- [ ]` becomes a task while a plain `- ` still
  falls through to the bullet list.
- Patch TaskItem's keymap to share PatchedListItem's split -> lift Enter
  chain (double-Enter on an empty item exits the list); nested: true enables
  sub-tasks and nested round-trips.
- Add a "Task list" entry to the bubble-menu list dropdown (+ i18n for en /
  zh-Hans / ja / ko).
- Style task lists in prose.css for both the editor ([data-type="taskList"])
  and the readonly remark-gfm output (.contains-task-list); completed items
  render muted.

Readonly already rendered task lists via remark-gfm; this brings the editable
view to parity. Adds markdown round-trip and readonly checked-state tests.

MUL-2926

Co-authored-by: multica-agent <github@multica.ai>

* fix(editor): keep readonly nested task lists block-laid-out (#3593)

The shared `display: flex` rule on task-list items broke nested task lists in
the readonly view. remark-gfm renders a task item as
`<li><input> text <ul>…</ul></li>` — no body wrapper — so a nested list is a
direct sibling of the checkbox and text, and flex pulled it onto the same row.
The editor's Tiptap NodeView wraps the body in a `<div>`, so it was unaffected.

Split the task-list CSS into separate editor and readonly blocks: the editor
keeps the flex row; readonly stays a block list item with an inline checkbox so
a nested `<ul>` drops below and indents under its parent. Adds a readonly test
that pins the nested DOM shape (nested `<ul>` inside the parent `<li>`), so a
future remark-gfm change that wraps the body fails loudly.

MUL-2926

Co-authored-by: multica-agent <github@multica.ai>

* feat(editor): convert `- [ ] ` typing into a task list (#3593)

TaskItem's built-in input rule only converts `[ ] ` / `[x] ` typed at the start
of a plain paragraph. When the user types the GitHub-style `- [ ] ` the leading
`- ` first turns the line into a bullet, and the built-in rule no longer fires —
so `[ ]` stayed as literal text and nothing became a checkbox.

Add an input rule on PatchedTaskItem that catches the checkbox token when it is
the entire content of a freshly-typed list item (bullet or ordered) and converts
just that item into a task item (deleteRange → liftListItem → toggleList). The
anchored regex means it only fires on an item whose whole content is `[ ] ` /
`[x] `, so sibling items in the same list are left untouched.

Adds typing-level tests (real input-rule simulation) covering `[ ] `, `[x] `,
`- [ ] `, `- [x] `, the mixed-list split case, and the plain-bullet no-op.

MUL-2926

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 16:36:46 +08:00
LinYushen
5e1a6c4853 fix(cli): degrade 'issue metadata list' to {} on /metadata 404 (#3757)
Self-hosted backends without the per-issue metadata route (older builds,
unapplied 105_issue_metadata migration, or proxy/ingress misroutes) reply
404 to GET /api/issues/:id/metadata. The agent runtime bootstrap calls
'multica issue metadata list <issue> --output json' best-effort, but a
non-zero exit was being escalated by Hermes into a failed agent run even
when the rest of the work succeeded.

This makes only the 'list' verb best-effort: a 404 from /metadata now
prints {} (or an empty table) and exits 0. Other status codes (401, 500,
etc.) keep real error semantics, and 'metadata get / set / delete' are
unaffected — those represent explicit caller intent.

To support the status-code check without changing the user-facing error
string, GetJSON now returns *cli.HTTPError on HTTP failures (the format
'GET <path> returned <code>: <body>' is preserved by HTTPError.Error()).

Refs GitHub issue #3711.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 16:13:27 +08:00
Bohan Jiang
d98fc85088 feat(agents): Integrations tab with Lark Bot bind entry + Lark Bot docs (MUL-2988) (#3751)
* feat(agents): add Integrations tab with Lark Bot bind entry

The agent detail page now has an Integrations tab alongside the inspector's
Integrations section. It reuses the shared LarkAgentBindButton so the
scan-to-bind / already-connected logic stays single-sourced, and adds the
not-configured / coming-soon / members-only states the sidebar has no room
for. The tab only appears once the deployment has Lark configured.

MUL-2988

Co-authored-by: multica-agent <github@multica.ai>

* docs: add Lark Bot integration guide

Covers binding a Multica agent to a Lark Bot (scan-to-install), using it
(DM / @-mention / /issue), management, permissions, and self-host setup.
Added in all four locales under the Integrations nav section.

MUL-2988

Co-authored-by: multica-agent <github@multica.ai>

* fix(agents): show bound Lark state when install_supported is false

install_supported governs only whether NEW scan-installs can complete;
already-installed bots stay manageable when the transport is unwired
(server/internal/handler/lark.go). LarkAgentBindButton checked the
install_supported gate before the existing-installation check, so a bound
agent on such a deployment showed 'coming soon' / nothing instead of
'Connected + Manage in Lark'. Reorder the guard (existing active install →
badge, before the install_supported gate) and mirror it in the new
Integrations tab. Adds regression tests for both surfaces.

MUL-2988

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 15:48:22 +08:00
Naiyuan Qing
569b43136c fix(editor): download attachments without blank web tab (#3752)
* fix(editor): download attachments without blank web tab

Co-authored-by: multica-agent <github@multica.ai>

* fix(attachments): preserve workspace in web download URLs

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 15:45:42 +08:00
Naiyuan Qing
7fdec9e6e4 Teach default PR handoff in issue skill (#3753)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 15:27:00 +08:00
Bohan Jiang
c99c2493ae fix(agent): keep resolvable models when CLI discovery exits non-zero
Parse the discovered catalog even when the model-discovery CLI exits non-zero (pi/opencode/cursor/openclaw) instead of discarding it and returning an empty model picker. Filter pi diagnostic lines so stale-pattern warnings don't coin bogus models. Fixes #3729. MUL-2977.
2026-06-04 14:57:30 +08:00
Bohan Jiang
82e9ca401c docs(i18n): backfill SMTP relay TLS docs for ja/ko parity (#3750)
The earlier SMTP_TLS / port-465 work only updated the English (and zh)
docs, leaving the ja and ko translations stale. This brings them to
parity for the SMTP relay section:

- environment-variables.{ja,ko}: correct the SMTP_PORT row (465 is
  supported, not "unsupported") and add the missing SMTP_TLS row.
- self-host-quickstart.{ja,ko}: fix the stale "465 unsupported" intro
  line and add the port-465 implicit-TLS example.
- auth-setup.{ja,ko}: fix the implicit-TLS row in the relay-modes table,
  add the port-465 example, and align the startup-log line.

Docs-only; code blocks kept identical to English. No SMTP_EHLO_NAME
changes (already synced in #3749).

MUL-2984

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 14:53:48 +08:00
Multica Eve
ae27058b0a fix(attachments): unified download endpoint with mode + presign + proxy (MUL-2976) (#3747)
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes #3721.

**Server**

- New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time.
- `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign.
- `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs.
- `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`.

**Clients**

- CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip.
- Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`.

**Docs**

- `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores.

**Tests**

- `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser).
- `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through.
- `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case.
2026-06-04 14:52:57 +08:00
Bohan Jiang
8db619c1cd fix(email): wire SMTP_EHLO_NAME through self-host config + docs [MUL-2984] (#3749)
* fix(email): wire SMTP_EHLO_NAME through self-host config + docs

Follow-up to #3679, which added SMTP_EHLO_NAME in code but never exposed
it to operators.

- docker-compose.selfhost.yml: pass SMTP_EHLO_NAME through to the backend
  container. The compose env block is an explicit allowlist, so without
  this the override set in .env was silently dropped and never reached
  the process — making the escape hatch unusable on the docker path.
- Document the var alongside its SMTP_* siblings: .env.example,
  SELF_HOSTING_ADVANCED.md, environment-variables.mdx, auth-setup.mdx,
  and self-host-quickstart.mdx (the last two with a strict-relay example).
- email.go: log when os.Hostname() fails instead of silently falling back
  to net/smtp's lazy "localhost" — the exact greeting strict relays reject.
- Add TestNewEmailService_EHLOName covering the env override, trimming,
  and the hostname fallback.

MUL-2984

Co-authored-by: multica-agent <github@multica.ai>

* fix(email): gate EHLO resolution to SMTP mode + sync docs to zh/ja/ko

Addresses review nits on this PR:

- email.go: resolve smtpEHLOName only when SMTP_HOST is set, so the
  Resend / DEV-stdout paths never call os.Hostname() or emit its
  failure log. The EHLO name is only ever used on the SMTP send path.
- docs: add SMTP_EHLO_NAME to the zh/ja/ko variants of
  environment-variables, self-host-quickstart, and auth-setup, in sync
  with the English docs updated earlier in this PR.

Note: the ja/ko self-host-quickstart and auth-setup pages were already
missing the port-465 implicit-TLS example (pre-existing i18n drift from
an earlier SMTP_TLS change, unrelated to this PR); the new EHLO block is
inserted at the correct logical anchor regardless. A full ja/ko re-sync
is left as a separate follow-up.

MUL-2984

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 14:44:55 +08:00
Arnaud Dezandee
a9f0739b52 fix(email): set EHLO hostname for SMTP relay compatibility (#3679) 2026-06-04 14:07:47 +08:00
xiaoyue26
49c913a4fa fix(sidebar): hide stale pinned items immediately on workspace switch (MUL-2985) (#3564)
* fix(sidebar): hide stale pinned items immediately on workspace switch

When the user switches workspaces, previously pinned items from the old
workspace were briefly visible until the new workspace data loaded. Reset
the pinned list to an empty array on workspace-id change so the stale
items disappear instantly, eliminating the flicker.

* fix: separate pinnedItems and wsId effects to prevent drag-sort loss

When wsId changes, the combined effect would trigger even if pinnedItems
hadn't changed yet (still old workspace data), overwriting localPinned
and losing any pending drag-sort results.

Split into two independent effects:
- pinnedItems effect: updates localPinned when data changes (respects isDragging)
- wsId effect: updates localPinnedWsId immediately on workspace switch

This ensures workspace switches don't interfere with drag operations.
2026-06-04 14:06:09 +08:00
Naiyuan Qing
b9334dd59f fix: anchor comment triggers to thread roots (#3746)
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 13:47:05 +08:00
Bohan Jiang
9beb45b55b fix(chat): deliver every debounced user message to the agent (MUL-2968) (#3744)
The Lark short-window debounce (MUL-2968, #3742) can land several user
messages in a chat session before a single agent run fires. But the
daemon claim built the agent prompt from only the *single most recent*
user message (walk history backward, take first user message, break).

So 「看上海天气」then「还有青岛」debounced into one run, and the agent
received only 「还有青岛」— it answered Qingdao and never saw Shanghai.
The session itself was correct (both messages persisted); the gap was in
what the run delivered to the agent. Before debouncing this was masked
because each message got its own run.

Build the prompt from the whole unanswered set instead: the trailing run
of user messages after the last assistant reply (every completed/failed
run writes an assistant row, so the anchor advances one turn at a time —
the full burst on the first turn, only the new message(s) after a reply).
Attachments are collected from each included message. Extracted the
selection into a pure trailingUserMessages helper with table-driven unit
tests, plus a DB-backed claim test asserting both messages reach the
agent and that a post-reply message delivers alone.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 13:12:32 +08:00
Bohan Jiang
a1a33f91db fix(desktop): surface expired login instead of silently stuck "Starting" daemon (MUL-2973) (#3743)
* fix(desktop): surface expired login instead of silent "Starting" daemon (MUL-2973)

When the local daemon's cached PAT is expired/revoked, the daemon 401s during
startup and exits before it serves /health. The desktop polled /health forever
and kept reporting "starting", so the runtime sat at "Starting…" with no hint
that re-login was the fix (GitHub #3512).

Detect this in the layer that owns the daemon's credential: when a start fails
to reach "running", probe the token against GET /api/me. A 401 (or missing
token) surfaces a new "auth_expired" daemon state; a 2xx means the token is
fine (non-auth failure) and a network error stays inconclusive — so a network
blip is never misclassified as expired login.

The desktop then shows a "Sign-in expired · Sign in again" prompt on the
runtimes card and a banner in Daemon settings. The action drops the stale
cached PAT, re-mints a fresh one from the current session, and restarts the
daemon; if minting also 401s (the session token is dead) it falls back to the
standard re-login flow. No daemon/CLI behavior change.

Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): only force re-login on a real 401 during daemon reconnect (MUL-2973)

Review feedback: the reconnect helper treated any failure from clearToken /
syncToken / restart as "session is dead" and logged the user out. A transient
failure (mint 5xx, network blip, config write error, restart hiccup) would
wrongly sign them out.

Move the failure classification into the main process, where the real HTTP
status is available: mintPat now tags its error with the response status, and a
new daemon:reauthenticate handler returns a structured ReauthResult — `ok`,
`session_invalid` (a genuine 401 → the session token itself is dead), or
`transient`. The renderer only calls logout() on `session_invalid`; transient
failures keep the user signed in and show a retryable toast. An unexpected IPC
error is also treated as transient, never as logout.

Add tests locking the classifier (401 → auth, 5xx/network/IO → not auth) and the
renderer behavior (transient failure and IPC throw do NOT log out).

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 13:08:12 +08:00
Bohan Jiang
6e004149a8 feat(lark): debounce inbound run trigger per chat session (MUL-2968) (#3742)
A forwarded transcript plus a follow-up note arrive as two separate Lark
messages, each of which synchronously called EnqueueChatTask — so the bot
ran twice (once on the bare forward, before the note arrived). The chat
task already reads the whole session history at run time, so the messages
never needed stitching; only the run TRIGGER did.

Introduce pendingBatcher: a per-chat_session debouncer that collapses a
burst into one agent run on a 3s silence window. Each message is still
appended, deduped, and ACKed synchronously and individually; step 8 of the
dispatcher now schedules a debounced flush instead of enqueuing inline.

Because EnqueueChatTask's agent-offline / agent-archived verdict is now
only known at flush, the dispatcher emits that notice itself via an
injected FlushReply (wired to OutcomeReplier.Reply) rather than returning
it synchronously to the hub. Infra failures are logged, not surfaced — the
inbound frame was ACKed long ago. The hub drains the batcher on graceful
shutdown so a normal restart does not drop a pending window.

Out of scope (owner-aligned): group-chat multi-speaker batching, restart
recovery for the in-process window, and forwarded-sender real-name
resolution.

Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-04 12:48:02 +08:00