mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
2e0b0bb77646ea00def985dcaca92e7f664c7708
1121 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9f21d0b634 |
feat(transcript): add timestamps to run transcript entries (MUL-3174) (#3951)
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration. |
||
|
|
d9347f0715 |
MUL-3175: add June 9 changelog entry (#3954)
* docs(changelog): add June 9 release notes Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): polish zh release copy Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): clarify zh release 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> Co-authored-by: J <j@multica.ai> |
||
|
|
42251b42fc |
fix(cli): honor MULTICA_SERVER_URL in setup self-host (#3912) (#3938)
* fix(cli): honor MULTICA_SERVER_URL in setup self-host `multica setup self-host` resolved the backend URL only from the --server-url flag, falling back to http://localhost:8080 when the flag was absent. It never consulted MULTICA_SERVER_URL, even though that env var is documented on the root --server-url flag and in `multica --help`, and is honored by every other command via resolveServerURL. A self-host user who set the env var instead of the flag still hit localhost and got "Server at http://localhost:8080 is not reachable". Route server-url and app-url through cli.FlagOrEnv so the documented env vars (MULTICA_SERVER_URL / MULTICA_APP_URL) are honored when the matching flag is not set, with the flag still taking precedence. userProvided now reflects flag-or-env, so an env-sourced remote URL still triggers the explicit app_url prompt. Not platform-specific despite the report. Fixes GitHub #3912. Co-authored-by: multica-agent <github@multica.ai> * fix(cli): normalize MULTICA_SERVER_URL in setup self-host MULTICA_SERVER_URL is documented as a ws:// daemon address (ws://localhost:8080/ws) and every other command normalizes it via NormalizeServerBaseURL before use. setup self-host consumed the resolved value raw and probed <url>/health, so a self-hoster who set the documented ws:// form would still fail the reachability check. Run the flag/env value through normalizeAPIBaseURL (ws->http, wss->https, strip /ws) so the documented form works and the stored server_url stays a clean http(s) base. Add a normalization test case and a focused test for the MULTICA_APP_URL env path (review nit). Co-authored-by: multica-agent <github@multica.ai> * docs(self-host): note setup self-host honors MULTICA_SERVER_URL / MULTICA_APP_URL Document that `setup self-host` reads the env vars when the matching flag is omitted (flag wins), and that MULTICA_SERVER_URL accepts the ws://…/ws daemon form. Added to en/zh/ja/ko quickstart for parity. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
54dbb57aef |
MUL-3138: add June 8 changelog entry (#3904)
* docs: add June 8 changelog entry Co-authored-by: multica-agent <github@multica.ai> * docs: simplify June 8 changelog titles Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ddf89a8f2 |
feat(daemon): enable Antigravity (agy) per-agent model selection (MUL-3125) (#3894)
* feat(daemon): wire agy --model and model discovery for Antigravity agy 1.0.6 added a --model flag and an `agy models` catalog command, which were the #1 blocker in the earlier agy-backend review (MUL-3125). The antigravity backend already shipped but deliberately dropped opts.Model because agy 1.0.1 had no way to select a model. - buildAntigravityArgs now passes --model <display name> when opts.Model is set; the value is the exact `agy models` display string (spaces + parens), passed as a single exec arg so no shell quoting is needed. - Block --model in custom_args so it can't override the managed value. - ListModels("antigravity") enumerates via `agy models` (no static fallback: agy silently no-ops on unrecognised models, so a stale guess would turn a typo into a successful empty run). - ModelSelectionSupported now returns true for every built-in provider; the hook stays for any future model-less runtime. - Daemon probe reads MULTICA_ANTIGRAVITY_MODEL for the daemon-wide default. Co-authored-by: multica-agent <github@multica.ai> * docs(providers): mark Antigravity model selection as supported Antigravity gained --model in agy 1.0.6 (MUL-3125). Update the provider matrix + prose (en/zh/ja/ko) from "managed internally / no --model" to dynamic discovery via `agy models`, and refresh the now-stale picker comments. Flag the display-string (not slug) shape and agy's silent no-op on unrecognised values. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): reject unknown Antigravity model at spawn (MUL-3125) agy exits 0 with empty output on an unrecognised --model, so a stale/typo'd value would surface as a 'completed' but empty task. Validate opts.Model against the `agy models` catalog in Execute before spawning: a non-empty model the CLI does not advertise fails fast with an actionable error listing the real choices. opts.Model is the single funnel for agent.model and the MULTICA_ANTIGRAVITY_MODEL default, so this one check covers every source (UI free-text, API, persisted value, env) — addressing Elon's review that a UI-only guard is bypassable. Validation is fail-OPEN: if the catalog can't be discovered we pass the value through and let agy resolve it, so a discovery hiccup never blocks a run. Pure antigravityModelError() is unit-tested (valid / unknown / near-miss / empty-model / empty-catalog); verified live against real agy 1.0.6. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3389e887e0 |
MUL-2614: randomize self-host Postgres password (#3893)
* fix: randomize self-host postgres password Co-authored-by: multica-agent <github@multica.ai> * docs: sync self-host password guidance Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f5db77340f |
feat(web): native notification banners for the web app (MUL-3116) (#3883)
The in-app inbox (sidebar badge, real-time WS updates, settings, inbox page) was already shared and worked on web. The only Desktop-only piece was the native OS banner: handleInboxNew called desktopAPI.showNotification, which is undefined on web, so no banner fired for new inbox items while the app was unfocused. Add the browser equivalent, keeping handleInboxNew as the single decision point (focus + source-workspace mute gating stays shared with desktop): - packages/core/platform/system-notification.ts: browser Notification engine (showWebNotification) + permission helpers + a click-handler registry. Lives in core (the caller does) but injects the click-routing decision so core stays headless. - handleInboxNew: branch desktopAPI (unchanged) → else showWebNotification. - apps/web WebNotificationBridge: registers click routing to the source workspace's inbox (?issue=…), mirroring desktop's DesktopInboxBridge. - Settings → Notifications: web-only opt-in to grant browser permission (hidden on desktop / where the API is unavailable); en/zh-Hans/ja/ko. Permission is an explicit settings opt-in (no auto-prompt on load, per browser best practice). Tests cover the engine and the web path in handleInboxNew. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f98ada547 |
fix(desktop): guard updater events after window destroy (#3871)
* fix(desktop): guard updater events after window destroy Co-authored-by: multica-agent <github@multica.ai> * test(desktop): cover updater send destroy race Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
83ac61e2a1 |
fix(mobile): border + className passthrough on WorkspaceAvatar logo branch (#3849)
The logo (resolved avatar_url) branch was missing the border the fallback tile and web's <img> carry, and didn't thread the className prop. NativeWind has no cssInterop for expo-image, so className/border on <ExpoImage> is silently dropped — wrap the logo in an overflow-hidden View that carries border border-border + className (the same pattern lib/markdown/markdown-image.tsx uses to border/round an expo-image). Both branches now match web parity. Follow-up to #3839. MUL-3096 Co-authored-by: J <agent-j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
96dbe88774 |
fix(mobile): render workspace logos and use English copy in workspace switcher (#3839)
The workspace switcher showed a generic `sf:building.2` glyph for every workspace and never used `workspace.avatar_url`, and the switch sheet, confirm dialog, and More-tab entry row shipped hardcoded Chinese strings (mobile is English-only — no i18n infra yet). - Add `components/workspace/workspace-avatar.tsx`, mirroring web's `packages/views/workspace/workspace-avatar.tsx`: a resolved `avatar_url` renders as a rounded-square logo, otherwise the workspace's initial letter sits in a muted tile. URL resolution reuses the existing `resolveAttachmentUrl` helper (the mobile mirror of core's `resolvePublicFileUrl`). - Use `WorkspaceAvatar` in the switcher list and the More-tab entry row. - Replace the hardcoded Chinese strings with English. Co-authored-by: Matt Voska <voska@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
631fa015be | fix(docs): stabilize localized usage rollup anchor [MUL-2957] | ||
|
|
3caba86b09 | feat(scheduler): DB-backed execution-record scheduler [MUL-2957] | ||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
8c98940b79 |
Lark Bot integration MVP: migration + service boundary (MUL-2671) (#3277)
* feat(db): add Lark integration migration (MUL-2671) Introduces seven tables for the 飞书 Bot integration MVP — per-agent PersonalAgent installations, user/chat bindings, inbound dedup + non-content drop audit, outbound card mapping, and short-lived single-use member binding tokens. Schema notes: - chat_session schema unchanged; Lark routes through a separate binding table rather than adding a metadata JSONB column. - Outbound card mapping is task/message scoped so multiple runs on the same session can't stomp each other's cards. - lark_inbound_audit stores routing / identity / drop_reason ONLY, never message body — the audit channel for unbound users and group messages that don't address the Bot. - app_secret stores ciphertext (encryption helper lands in a follow-up commit on this branch); DB never sees plaintext. Co-authored-by: multica-agent <github@multica.ai> * feat(util): add secretbox AES-256-GCM helper for at-rest secrets First consumer is lark_installation.app_secret (MUL-2671 §4.4), but the helper is intentionally generic — future per-tenant secrets that must not appear in a DB dump can reuse it. Construction: AES-256-GCM with a per-message random nonce, providing authenticated encryption. Tampered ciphertext fails Open instead of silently decrypting to garbage. Master key loaded from a base64 env var via LoadKey; key rotation is not in scope yet. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): extract IssueService.Create as single create entry (MUL-2671) Establishes the service-layer boundary mandated by Elon's 二审 of MUL-2671 §4.8: issue creation no longer lives inside the HTTP handler. Both the HTTP POST /issues handler and the future Lark /issue command call into service.IssueService.Create, so duplicate guard, issue numbering, attachment linking, broadcast, analytics, and agent/squad enqueue stay aligned. Handler responsibilities shrink to parsing the HTTP request, doing actor resolution / validation (transport-specific), and converting service results into the IssueResponse + 201. The transaction-wrapped core, attachment link, event publish, analytics capture, and agent/squad enqueue all move into service.IssueService.Create. A BroadcastPayload callback on the service keeps the WS broadcast shape (the full IssueResponse) without forcing the service to depend on handler-layer response types. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations): add Lark package skeleton (MUL-2671) Establishes the architectural boundaries Elon's 二审 mandated as first-PR blockers without dragging in OAuth, WebSocket, or card-patching code (those land in follow-up PRs): - ChatSessionService interface — channel-aware chat-session entry point for Lark, deliberately separate from the HTTP SendChatMessage handler. The HTTP handler's single-creator guard (creator_id == request user_id) is correct for the browser client but rejects group chat_sessions by construction; Lark needs its own service. - AuditLogger interface — the only path for recording dropped events. Its signature deliberately omits message body, enforcing the drop-audit policy (MUL-2671 §4.7) at the type level: unbound users and non-addressed group messages can't accidentally end up in chat_session. - Typed IDs (OpenID, ChatID) prevent UUIDs from being conflated with Lark-side identifiers at compile time. - DropReason constants align dashboard/audit queries across callers. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): move parent/project workspace check into IssueService (MUL-2671) Parent existence and project workspace membership now live inside IssueService.Create, inside the same transaction as the duplicate guard and counter increment. The HTTP handler stops re-implementing the lookup; every future create entry (Lark /issue, MCP, API keys) inherits the same boundary without copy-pasting the SQL. Adds two error sentinels (ErrParentIssueNotFound, ErrProjectNotFound) so transports can translate to their own error shapes. Handler-level cross-workspace tests guard the boundary against future regressions. Co-authored-by: multica-agent <github@multica.ai> * fix(db): harden Lark migration safety底座 — TTL cap + workspace FK (MUL-2671) Two storage-layer hardenings that move the must-fix line off "the app layer enforces it" and onto the schema itself, so future write paths or hand-inserted rows cannot regress the invariants. 1) lark_binding_token TTL cap. The DB CHECK was 1 hour as defense-in-depth while the app constant was 15 minutes; the CHECK now matches the product cap (15 minutes). Application constant docstring updated to reflect that storage enforces the same bound. 2) lark_user_binding workspace membership. The table previously only FK'd to workspace / user / installation independently, so a binding could exist for a user no longer in the workspace, or claim a workspace different from its installation's. Two composite FKs close the gap structurally: * (installation_id, workspace_id) → lark_installation(id, workspace_id) — guarantees a binding's workspace_id always matches its installation's workspace_id. A new UNIQUE (id, workspace_id) on lark_installation is added as the FK target. * (workspace_id, multica_user_id) → member(workspace_id, user_id) with ON DELETE CASCADE — when a user is removed from the workspace, the binding cascades away in the same transaction. There is no longer a path where lark_user_binding outlives workspace membership. These two FKs are the schema-level proof for §4.3's "unbound or non-workspace members cannot leak content into chat_session" invariant. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations/lark): inbound services + /issue dispatcher (MUL-2671) Lands the inbound service layer for the Lark Bot MVP, sitting on top of the migration + service-boundary scaffold from the previous commits. What ships: - sqlc queries for all seven lark_* tables (idempotent dedup insert, CAS WS-lease, single-use binding-token consume, etc.) plus GetMostRecentUserChatMessage for the /issue fallback. - AuditLogger backed by lark_inbound_audit; signature deliberately body-free so callers cannot leak content into the drop log. - ChatSessionService: find-or-create chat_session via the binding table (winner-takes-all on the UNIQUE race), append-with-dedup, /issue parser, "previous user message" fallback for bare `/issue` invocation. - Dispatcher orchestrates the inbound pipeline in one place: installation routing → group-mention filter → identity check → ensure session → append+dedup → /issue → enqueue chat task. Group sessions use the installer as creator (stable workspace identity); p2p uses the sender. Agent-offline path falls through with OutcomeAgentOffline so the WS adapter can reply with the offline notice from §4.6. - BindingTokenService: random URL-safe token, SHA-256 stored hash, 15-min TTL pinned at the application AND the DB CHECK; Redeem returns the same opaque error for all rejection cases (no timing oracle on replay). - Unit tests for the parser (13 cases), dispatcher (8 cases via fake Queries/Chat/Audit/IssueCreator/Enqueuer), and binding-token hash/entropy. Real-DB integration tests for OAuth + token redeem land alongside the HTTP handlers in the next commit. Out of scope for this commit (next ones on the same feature branch): OAuth callback, HTTP routes, WebSocket hub, outbound card patcher, frontend. Co-authored-by: multica-agent <github@multica.ai> * feat(integrations/lark): installation HTTP surface + secretbox-gated wiring (MUL-2671) Lands the HTTP boundary on top of the inbound services from the previous commit. What ships: - InstallationService.Upsert: the only path that writes lark_installation. Encrypts app_secret with the secretbox passed in at construction time; refuses to fall back to plaintext storage (returns an error from the constructor if no Box is supplied), so a misconfigured dev environment cannot accidentally land a row with cleartext credentials. Revoke flips status without DELETE so audit trail survives. - HTTP handlers under /api/workspaces/{id}/lark/: * GET /installations — member-visible (Integrations tab renders for non-admins). Soft 200 with empty list + configured:false when MULTICA_LARK_SECRET_KEY is unset, so the tab does not error on self-host that has not opted in. * POST /installations — admin-only; 503 when not configured. Re-validates agent_id ∈ workspace before accepting credentials so a cross-workspace agent UUID is rejected. * DELETE /installations/{id} — admin-only; workspace-scoped lookup so one workspace cannot revoke another's installation by UUID guess. - POST /api/lark/binding/redeem (user-scoped, no workspace context): the only path that mints a lark_user_binding row from user action. Redeemer identity comes from the session, not the token, so a stolen link cannot bind an open_id to an attacker's Multica user. The composite FK on lark_user_binding cascades the binding away if the user is not (or no longer) a workspace member, so a non-member who steals the link gets 403 at the DB layer. - Two new event-bus types in protocol.events: EventLarkInstallationCreated, EventLarkInstallationRevoked. - Router wiring: MULTICA_LARK_SECRET_KEY drives a conditional initialization of h.LarkInstallations + h.LarkBindingTokens. When unset, the integration disables itself with an INFO log and the rest of the server boots normally. - Handler tests cover all four not-configured short-circuits. Happy-path integration tests (real DB, full create→list→revoke cycle and token mint→redeem) ship alongside the WS hub PR. Co-authored-by: multica-agent <github@multica.ai> * fix(integrations/lark): close binding-token rebind & typed task errors (MUL-2671) Two must-fixes from PR review on HEAD |
||
|
|
2b4fed9144 |
docs: add June 3 changelog entry (#3708)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5900d8b637 |
fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
48044cc918 |
docs: add June 2 changelog entry (#3656)
* docs: add June 2 changelog entry Co-authored-by: multica-agent <github@multica.ai> * docs: simplify June changelog feature 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> |
||
|
|
f539fdba83 |
feat(onboarding): backfill prompt for missing source attribution (MUL-2796) (#3550)
* feat(onboarding): backfill prompt for users missing source attribution
Adds a one-shot popup shown after login to already-onboarded users
whose `onboarding_questionnaire.source` was never recorded — either
they completed onboarding before the source step shipped, or they
clicked Skip on it. Reuses the existing 12-option StepSource UI and
the existing `PATCH /api/me/onboarding` endpoint, so no schema or
backend changes.
Web renders it as a route at /onboarding/source (sibling of the
reserved /onboarding); desktop dispatches it as a WindowOverlay per
the Route categories rule. Submit and explicit Skip are terminal;
the close X bumps a per-user localStorage counter and stops appearing
after 3 dismissals.
Emits source_backfill_shown / submitted / skipped / dismissed PostHog
events so the funnel can be tracked separately from first-time
onboarding.
For MUL-2796.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): preserve role/use_case and respect dismiss cap in source backfill
Round-2 fixes from Emacs's review of #3550:
1. PATCH wipe: `PATCH /api/me/onboarding` replaces the JSONB column
wholesale (server/internal/handler/onboarding.go), so sending only
the source slots was wiping role/use_case/version for exactly the
historical users this targets. Read user.onboarding_questionnaire,
overlay the source fields client-side via mergedQuestionnairePatch,
and send the full shape. 7 unit cases cover the merge semantics.
2. Legacy single-string source: pre-multi-select rows wrote
`source: "search"` as a bare string. needsSourceBackfill now treats
that as already answered, matching mergeQuestionnaire (views) and
stringOrSlice.UnmarshalJSON (server). Flipped the existing test and
added empty-string + null coverage.
3. Dismiss cap honored in callback: the web auth callback was passing
dismissCount=0, which would force-route capped users through
/onboarding/source on every login (the route page would bounce them
onward, but only after a blank detour and a re-fired
`source_backfill_shown` event). Added readSourceBackfillDismissCount
so the callback reads the same per-user localStorage bucket the
prompt writes to. Test asserts a count of 3 bypasses the detour.
Co-authored-by: multica-agent <github@multica.ai>
* test(onboarding): clear source-backfill dismiss counter in callback test beforeEach
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): footer hint text matches the Submit button on the backfill prompt
The Source step's hint reads "Hit Continue when you're ready" because
its commit button is "Continue". The backfill view ships a "Submit"
button instead, so the inherited hint was misleading. Add a dedicated
`source_backfill.hint_ready` key across en / zh / ko and use it here.
Caught during browser E2E in the round-2 verification stack.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): magic-code login also detours through source backfill
The round-2 fix in PR #3550 only wired the source-backfill detour
into the OAuth `/auth/callback` post-success path. Magic-code login
goes through `/login` → `handleSuccess()` which calls
`resolveLoggedInDestination()` and pushes directly to the workspace,
so those users never reach `/onboarding/source`. Caught during the
local-env demo for Jiayuan.
Add `maybeSourceBackfillDetour` to the login page and apply it in
both the already-authenticated useEffect and the post-verify-code
handler. Predicate consults the same per-user localStorage bucket
the prompt writes to, so a user who hit the close-X cap on this
browser flows straight through.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): source backfill is a workspace-mounted modal, not a route detour
Per UAT, the prompt should overlay the workspace as a Dialog with the
workspace visible behind a dimmed backdrop — the original brief and
reference screenshot both showed a modal. PR #3550 shipped a full-window
takeover (web /onboarding/source + desktop WindowOverlay) which Jiayuan
rejected.
This commit replaces the full-window view with a Dialog-based
`<SourceBackfillModal />` mounted once inside the shared `DashboardLayout`
(packages/views/layout). The modal self-mounts: it reads
`needsSourceBackfill(user, dismissCount)` and opens itself when the
predicate flips to true; X / ESC / outside-click all bump the per-user
localStorage cap and close.
Removed:
- apps/web/app/(auth)/onboarding/source/page.tsx (route)
- paths.sourceBackfill (no longer needed)
- callback page detour
- login page maybeSourceBackfillDetour
- desktop WindowOverlay type "source-backfill"
- desktop navigation interception of /onboarding/source
- desktop App.tsx dispatch effect
- pageview-tracker case
- views/onboarding `SourceBackfillView` + `readSourceBackfillDismissCount` exports
Preserved (semantics unchanged):
- `needsSourceBackfill` predicate (incl. legacy single-string source coercion)
- `mergedQuestionnairePatch` so role / use_case survive Submit / Skip
- PostHog events: source_backfill_shown / submitted / skipped / dismissed
- Per-user dismiss-count cap (3) in localStorage
- en / zh / ko i18n strings
Tests:
- 7 new tests for the modal in packages/views/onboarding/source-backfill-modal.test.tsx
- Adjusted apps/web/app/auth/callback/page.test.tsx: detour tests dropped,
one assertion remains that onboarded users with missing source land in
the workspace (the modal handles the rest)
- Full suite: 965 tests pass, typecheck + lint clean
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): mount source-backfill modal on the desktop workspace too
Desktop's WorkspaceRouteLayout never wraps DashboardLayout, so the
previous commit's modal mount only fired for web. Regression: desktop
users were not seeing the prompt at all.
Wire the same `<SourceBackfillModal />` next to `<WelcomeAfterOnboarding />`
inside `workspace-route-layout.tsx`, with the matching
`!overlayActive` suppression so the Dialog doesn't portal-jump above
an active pre-workspace WindowOverlay (onboarding / accept-invite /
new-workspace). Same component on both platforms — single source of
truth lives in packages/views/onboarding/source-backfill-modal.tsx.
Also drop the now-stale `source-backfill detour` comment in the web
callback test fixture (Emacs nit, non-blocking).
Co-authored-by: multica-agent <github@multica.ai>
* test(desktop): assert workspace-route-layout mounts source-backfill modal
Two structural tests pinning the round-4 fix:
- `mounts SourceBackfillModal when no WindowOverlay is active` —
guards against the regression Emacs caught (modal silently absent
on desktop because the previous round only wired DashboardLayout).
- `suppresses SourceBackfillModal while a WindowOverlay is active` —
mirrors the existing `!overlayActive` rule that WelcomeAfterOnboarding
already relies on so a portal-rendered Dialog can't visually outrank
an active pre-workspace overlay.
Mocks the SourceBackfillModal with a marker component so the test
asserts mount/unmount without depending on the modal's own predicate
gate.
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): backfill modal Other toggles off; entrance settles after 700ms
UAT round-3 follow-ups from Jiayuan:
1. **Other can't be deselected**: the modal kept a parallel
`pendingOther` flag set to true on every Other click, and
`IconOtherOptionCard`'s row click was guarded with
`if (!selected) onSelect()` — so a second click neither flipped
pendingOther nor reached the parent toggle. Drop `pendingOther`
(the `source.includes("other")` derivation is already authoritative)
AND add an opt-in `allowToggleOff` prop to `IconOtherOptionCard`
that lets the row toggle when already selected. The text input
stops click propagation so typing never deselects.
2. **Rebase + absorb GitHub channel**: rebased onto origin/main which
added `social_github` (PR #3612). Modal's option list now mirrors
StepSource — GitHub slotted between YouTube and Other social,
reusing the existing `GitHubIcon`.
3. **Soft entrance**: defer the dialog open by 700ms after the user
lands on a workspace so the underlying view paints first and the
modal feels like an inviting prompt rather than a hard block.
Honour `prefers-reduced-motion: reduce` (open immediately for
users who have opted out of incidental motion).
Tests:
- New `Other toggles off on the second click instead of getting stuck`
- New `renders the GitHub channel rebased from origin/main`
- New `defers the entrance by ~700ms when the user has not opted into
reduced motion`
- Existing tests stamp `prefers-reduced-motion: reduce` in beforeEach
so the dialog opens synchronously and they don't need to drive
fake timers.
Full suite passes (969 tests).
Co-authored-by: multica-agent <github@multica.ai>
* fix(onboarding): backfill modal opens reliably + Other deselects via icon area
Three follow-up fixes after live UAT:
1. Strict-mode regression on entrance delay: the gate ref was being
stamped when the effect *scheduled* the timer, so React Strict
Mode's double-invoke cleared the first timer and then bailed on
the second pass because the ref was already set, leaving the
dialog forever closed. Stamp the ref only inside the timer
callback (or synchronously when reduced-motion is on) so the
second strict pass starts a fresh timer.
2. Other deselect: dropping `pendingOther` wasn't enough — the input
that replaces the label when Other is selected was previously
stopping click propagation, so a re-click on the row never
reached the toggle. Remove `e.stopPropagation()` and instead let
the row's onClick ignore clicks whose target IS the input
(typing / focusing the input still doesn't deselect; clicks on
the icon, padding, or border do).
3. Tests: drive the Other re-click via Playwright `click({position:
{x:24,y:24}})` so the click lands on the icon area instead of the
center of the input, matching real-user behaviour.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(onboarding): source picker is single-select primary source
Per Jiayuan's call after the survey of HDYHAU UX in PLG SaaS (Linear /
Vercel / Loom / Notion / Webflow / Stripe / Figma / Cursor / PostHog
mostly skip the question entirely; where it's asked the documented
default — Fairing / Recast / HockeyStack / Ruler Analytics — is to
capture the primary source so channel weights sum to 100% and ROI
math is defensible).
Modal + StepSource both pivot from multi-select to single-select
radio. Server schema is intentionally untouched: `source` stays
`string[]` for back-compat with v2 multi-select rows; the client
always sends a one-element array. Zero migration, zero data loss.
Frontend:
- `source-backfill-modal.tsx`: state pivots from a multi-element
`source: Source[]` to a single `pickedSlug` derived from
`source[0]`; click handler replaces the array instead of toggling.
Cards switch to `mode="radio"`, the fieldset gets `role="radiogroup"`,
the now-redundant `pendingOther` and `allowToggleOff` opt-in go
away — radio mode means no toggle-off, so the original UAT bug
("Other can't be deselected") is structurally impossible.
- `step-source.tsx`: drop the `multiSelect` prop so it routes
through `step-question.tsx`'s existing radio path (same one
StepRole already uses). Picking a second option replaces the
first; switching away from Other clears `source_other` so a stale
value can't leak.
- `icon-option-card.tsx`: revert the `allowToggleOff` plumbing.
Tests:
- `source-backfill-modal.test.tsx`: drop the multi-select toggle-off
assertion; add "picking a second option replaces the first" with
explicit radio-role queries.
- `step-source.test.tsx`: rewrite multi-select tests as single-select
(no more "stacks several picks" / "toggle off" cases); add
"switching away from Other clears source_other".
Full suite (970 tests) green, typecheck + lint clean.
Co-authored-by: multica-agent <github@multica.ai>
* docs(onboarding): refresh stale multi-select comments around source
Comment-only follow-up to the single-select refactor in
|
||
|
|
d52c4f238f |
fix(desktop): contain renderer crashes (#3643)
* fix(desktop): contain renderer crashes Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): filter renderer exit prompts Co-authored-by: multica-agent <github@multica.ai> * refactor(desktop): drop redundant page-level ErrorBoundary on issue detail The whole-page <ErrorBoundary> wrapper duplicated the new route-level errorElement (DesktopRouteErrorPage). Let render errors bubble to the root route boundary so all detail routes are contained the same way. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(desktop): add Close tab escape to route error page Reload tab recreates the same crashing path and Go to issues is a dead end when the issues route itself crashed. Add a Close tab action that destroys the crashing router entirely and falls back to a sibling tab (or a reseeded default), the only always-safe escape regardless of which route crashed. 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> |
||
|
|
1aa742053b |
i18n: add japanese locale (MUL-2893) (#3538)
* i18n: add japanese locale * fix: spacing issues * refactor * fix(desktop): set <html lang> before paint to avoid JA Kanji font flash Switch the documentElement.lang sync from useEffect to useLayoutEffect so lang is committed before the first paint. Otherwise Japanese desktop users saw one frame of Kanji rendered with the Chinese-first fallback stack before the html[lang|="ja"] CJK override applied. Also fix the stale selector in the HTML_LANG comment (html[lang^="ja"] -> html[lang|="ja"]). Addresses review nits on MUL-2893. Co-authored-by: multica-agent <github@multica.ai> * fix(docs): tokenize the ideographic iteration mark in JA search Add U+3005 (々) to the Japanese search tokenizer character class. It sits just below the kana blocks, so words like 様々 / 日々 / 個々 previously dropped the mark and split awkwardly, hurting recall. Addresses a review nit on MUL-2893. Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): restore ja locale parity after merging main Merging main brought new EN strings into agents/chat/onboarding/settings/ squads that the ja bundle (authored against an older snapshot) lacked, breaking the locales parity test. Add the Japanese translations for the new keys (workspace logo upload, agents runtime filter, chat session-history stop dialog, onboarding social_github, squad archived status) and drop the two renamed chat window keys (active_group / archived_group) that EN removed in favour of history_group. Fixes the failing @multica/views parity.test.ts on the FE CI for MUL-2893. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
baf8b215cb |
Fix workspace recovery for desktop and web (MUL-2894) (#3436)
* fix(workspace): recover from stale workspace state * fix(workspace): apply review nits for recovery flow - no-access-page: navigate via nav.replace so a browser Back doesn't land the user back on NoAccessPage with the dead slug - no-access-page: refresh the stale cookie-clear comment — the recovery button no longer routes through `/`; the clear now guards other `/` entry points (manual nav, Back into `/`, fresh page load) - tab-store: drop the redundant `as string | undefined` cast (the Set value is already string | undefined under TS 5.9) - tab-store.test: cover the route-layout heal path (all stale groups dropped, then seed a fresh tab for a valid slug) and assert the dropped group's router is disposed Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e2720f7d33 |
feat: add opencode thinking variants
Adds OpenCode model variant discovery for thinking controls, passes saved thinking_level through opencode run --variant, and hardens verbose model parsing with fallback coverage. |
||
|
|
a6b83fef41 |
fix(agents): surface archived status for retired agents (#3608)
Retired agents (agent.archived_at set) previously read as offline across the agent dot, hover card, detail badge, and squad member list — a leftover online runtime row could even make them look reachable. Add a dedicated archived presence/status that wins over every runtime/task signal so a retired agent never reads as live or merely offline. - Add archived to AgentAvailability and SquadMemberStatusValue unions - Short-circuit deriveAgentPresenceDetail before runtime/task scan - Backend deriveSquadMemberStatus returns archived instead of offline - Render gray Archive dot/label; skip workload + reassign affordances - en/ko/zh-Hans locale strings |
||
|
|
700cd97407 |
feat(workspace): add per-workspace logo upload (#2760)
Adds avatar_url column to workspace, threads it through the API +
WorkspaceAvatar component, and adds a click-to-upload editor in the
workspace settings tab. Mirrors the squad avatar pattern (migration 086);
UI strings use "logo" while the schema/code uses avatar_url for codebase
consistency with user.avatar_url and squad.avatar_url.
- migration 093: ALTER TABLE workspace ADD COLUMN avatar_url TEXT
- UpdateWorkspace SQL + handler accept avatar_url (auth gated to
owner/admin at the router via RequireWorkspaceRoleFromURL)
- WorkspaceAvatar renders <img> when avatar_url is set, falls back to
the initial-letter span otherwise
- workspace-tab.tsx adds a 16x16 click-to-upload logo editor at the
top of the general settings card, using useFileUpload + accept=
image/png,image/jpeg,image/webp (server stores under workspaces/{id}/)
- en + zh-Hans settings i18n strings added
Co-authored-by: Matt Voska <voska@users.noreply.github.com>
|
||
|
|
03134e11a0 |
docs: add Skill search changelog (#3609)
* docs: add 2026-06-01 changelog Co-authored-by: multica-agent <github@multica.ai> * docs: refine Skill Command changelog copy Co-authored-by: multica-agent <github@multica.ai> * docs: correct Skill search changelog wording Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3c8645e546 |
feat(cli): add squad member set-role (#3583)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ad09baa045 |
feat(agents): add runtime machine filter to Agents tab (MUL-2846) (#3580)
* feat(agents): add runtime machine filter to Agents tab (MUL-2846)
Add a dropdown filter to the Agents tab toolbar that lets the user
narrow the list to agents bound to a specific runtime machine. The
filter reuses `buildRuntimeMachines` from the runtimes package so the
machine grouping (Local / Remote / Cloud) matches the Runtimes page
sidebar, and the per-machine agent counts respect the current scope
(Mine/All) so the numbers reflect what the user would see if they
clicked the row.
Only rendered in the Active view; the Archived view's toolbar is
unchanged. If the selected machine is GC'd while the user is on the
page (daemon stopped, runtime deleted), the filter auto-resets to
'All runtimes' instead of leaving the list empty. The no-matches state
now surfaces 'No agents on <machine>' when the machine filter is the
reason for zero results.
Adds new `runtime_filter` and `no_matches.runtime_filtered` /
`no_matches.search_runtime_filtered` i18n keys in en, zh-Hans, and
ko. 7 new unit tests in
`runtime-machine-filter-dropdown.test.tsx`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): address code review on runtime machine filter
- Plumb localDaemonId / localMachineName / hasLocalMachine / currentUserId
through AgentsPage → buildRuntimeMachines so the Local section and
device-name consolidation match the Runtimes page on both web and
Desktop. Adds a DesktopAgentsPage wrapper that bridges daemonAPI the
same way DesktopRuntimesPage does.
- Make the 'All runtimes' badge use the in-scope total instead of
summing per-machine counts, so an agent bound to a GC'd runtime
doesn't silently vanish from the count.
- Move Date.now() out of the machines useMemo into a useState lazy
init so the snapshot stays stable per mount.
- Drop unused i18n keys (all_description / this_machine / reset) from
runtime_filter in en / zh-Hans / ko.
- Add a regression test for the All-runtimes badge divergence.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): machine-scoped availability counts + Base UI menu items
Follow-up to the previous code-review round (Emacs review at
|
||
|
|
c9c269675c |
fix: align MCP support docs and UI gate (#3553)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2cf8107fc8 |
feat(email): support implicit TLS (SMTPS/465) for SMTP relay (MUL-2768) (#3340)
* feat(email): support implicit TLS (SMTPS/465) for SMTP relay The SMTP relay previously only did opportunistic STARTTLS: it dialed plaintext and upgraded if the server advertised STARTTLS. Providers that only offer implicit TLS on port 465 and do not advertise STARTTLS (e.g. Aliyun enterprise mail) could not be used as a relay at all. Add an SMTP_TLS env var: - unset / starttls (default): unchanged STARTTLS-upgrade behavior. - implicit / smtps / ssl: dial with tls.DialWithDialer (SMTPS). Implicit TLS is auto-enabled when SMTP_PORT=465 and SMTP_TLS is unset, so the common case works with no extra config. The startup log line now reports the negotiated mode (starttls / implicit-tls). Co-authored-by: multica-agent <github@multica.ai> * feat(email): plumb SMTP_TLS through selfhost compose, warn on unknown values The backend reads SMTP_TLS but docker-compose.selfhost.yml never forwarded it, so SMTP_TLS=implicit on a non-standard port (or an explicit starttls override on 465) silently did nothing inside the container. Add it to the backend.environment block. Also log a one-line warning when SMTP_TLS is set to an unrecognized value (e.g. "tls"/"true"/"on"), which would otherwise fall through to STARTTLS and fail to dial a 465 SMTPS port with no startup hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(email): cover SMTP_TLS precedence and alias resolution Table-driven test over NewEmailService asserting the implicit-TLS decision: 465 auto-enables implicit; explicit starttls on 465 overrides auto-detect; implicit/smtps/ssl aliases (case-insensitive, whitespace-trimmed) force SMTPS on any port; unknown values fall back to starttls. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs: document SMTPS / SMTP_TLS support, drop "465 unsupported" Port 465 implicit TLS is now supported, so the five places that said it was unsupported are wrong. Replace those sentences, add an SMTP_TLS row to the environment-variables tables (EN + ZH), and add a copy-pasteable SMTPS env block to the auth-setup pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: guofengchang <guofengchang@cumulon.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9aa8ba0191 |
fix(runtimes): self-host daemon setup URLs (MUL-2804) (#3474)
Expose self-host daemon setup URLs from /api/config at runtime so the Add computer dialog renders the operator's own server/app domains, while Multica Cloud defaults stay unchanged. Fixes #3013. |
||
|
|
382cdd6a0b |
feat(agent): consume OpenCode mcp_config via OPENCODE_CONFIG_CONTENT (#3098)
Closes the runtime-side gap of #2106: previously `agent.mcp_config` was honored only by Claude Code (via `--mcp-config <file>`); for OpenCode the field was accepted by the API but silently ignored at execution time. ## Approach OpenCode has no `--mcp-config` flag. Project the agent's `mcp_config` into OpenCode via OPENCODE_CONFIG_CONTENT — OpenCode's general inline-config injection environment variable, which accepts any subset of OpenCode's config schema (model / agent / mode / plugin / mcp / …) and merges at "local" scope after the project-config loop. MCP is the only field this PR projects through that channel; if a future Multica field needs the same channel it would assemble a combined config slice before the env append. The env-var route was deliberate. An earlier draft of this PR wrote the translated MCP servers into <workdir>/opencode.json and removed the file on cleanup; review (#3098) flagged that the task workdir is reused across turns for the same (agent, issue), and any agent- or user-written model / tools / permission settings in opencode.json must survive across runs. OPENCODE_CONFIG_CONTENT avoids the workdir entirely — nothing is written to disk, no cleanup is needed, and the env entry dies with the spawned process. OPENCODE_CONFIG_CONTENT was added to OpenCode in v1.4.10 (2025-09); the official @opencode-ai/sdk uses the same env var to inject runtime config, so the surface is stable. Verified empirically against OpenCode 1.15.6 in our K8s runtime: `opencode debug config` returns the injected mcp slice deep-merged with the user's global config, and <workdir>/opencode.json is observably untouched. ## Translation surface `agent.mcp_config` accepts two shapes for portability: - Claude-style `{"mcpServers": {name: {url|command, ...}}}` is translated into OpenCode's native form: `type: "local"|"remote"`, `command` coerced to a string array, `env` renamed to `environment`. - Native OpenCode `{"mcp": {name: ...}}` accepts the three shapes OpenCode's schema permits and is strict-decoded against each: - McpLocalConfig: `{type:"local", command:[…], environment?, enabled?, timeout?}` - McpRemoteConfig: `{type:"remote", url:"…", headers?, oauth?, enabled?, timeout?}` - bare override: `{enabled: bool}` (toggle a server inherited from global / project config without redefining it) Decoding uses `json.DisallowUnknownFields` so any field outside the matching schema is rejected — matching OpenCode's `additionalProperties: false`. Without this, a malformed payload (e.g. `command: "node"` instead of `command: ["node"]`) would reach OpenCode verbatim and either silently disable the server or crash the CLI at startup. Field-level checks the strict decoder doesn't catch: - `timeout` must be a positive integer (rejects 0, negative, fractional) - `oauth` must be either an object (validated against McpOAuthConfig) or the literal `false`; primitives and `true` are rejected as ambiguous - `oauth.callbackPort` must be in 1..65535 when set ## Precedence Go's os/exec dedups `cmd.Env` by key keeping the LAST occurrence (Go 1.9+). Appending OPENCODE_CONFIG_CONTENT after `buildEnv(b.cfg.Env)` guarantees the daemon's value wins over any value the user happened to put in `agent.custom_env` — which matches the intended semantics (`mcp_config` is the authoritative daemon-managed field; `custom_env` is the escape hatch). When that override happens we surface a warning log so accidental clobbers are debuggable. ## Limitation (out of scope, accepted in review) OpenCode also deep-merges its **global** config (`~/.config/opencode/opencode.json`) into every session and exposes no flag to disable that. Operators who want strict per-agent isolation from the global layer can set: ```jsonc // agent.custom_env on the platform { "XDG_CONFIG_HOME": "/tmp/opencode-isolated" } ``` …pointing at any directory without an `opencode/` subdir. OpenCode then reads no global config and only honors what the daemon injects via OPENCODE_CONFIG_CONTENT. Verified with `opencode debug config`. ## Changes server/pkg/agent/opencode_mcp.go (new): - buildOpenCodeMCPConfigContent — translates raw mcp_config into the JSON string OpenCode accepts via OPENCODE_CONFIG_CONTENT, returns "" when there's nothing to inject so the caller can skip the env entry (avoids clobbering anything the user put in agent.custom_env.OPENCODE_CONFIG_CONTENT) - translateMCPConfigForOpenCode + helpers — Claude-style → OpenCode native shape - validateOpenCodeNativeMCPEntry + opencodeMCPLocal / opencodeMCPRemote / opencodeMCPEnabledOnly / opencodeMCPOAuth typed structs — strict-decode native-shape entries against the schema (DisallowUnknownFields), plus targeted post-decode assertions for timeout / oauth / callbackPort server/pkg/agent/opencode.go: - 12 lines of env injection in Execute(), placed AFTER buildEnv so the daemon's value wins via os/exec dedup - warning log when agent.custom_env duplicates the same key - no on-disk state, no rollback closure, no post-run cleanup — OPENCODE_CONFIG_CONTENT lives only in the spawned process env server/pkg/agent/opencode_mcp_test.go (new): - TestBuildOpenCodeMCPConfigContent_{Empty,Remote,Local,Native} - TestBuildOpenCodeMCPConfigContent_NativeAcceptsAllSchemaFields — covers each native variant round-tripping every optional field (local with env+timeout+enabled; remote with headers+oauth-object+ timeout+enabled; remote with oauth: false; bare {enabled} override) - TestBuildOpenCodeMCPConfigContent_RejectsMalformedNative — 31-case table covering every constraint on Bohan-J's review: command must be a string array, environment / headers values must be strings, oauth must be an object or false, timeout must be a positive integer, additionalProperties: false (per-shape allow-list checked via DisallowUnknownFields) - TestOpencodeBackendInjectsMCPConfigViaEnv — E2E happy path; fake opencode binary captures $OPENCODE_CONFIG_CONTENT, asserts the translated mcp slice is present AND <workdir>/opencode.json was NOT written - TestOpencodeBackendOmitsMCPEnvWhenEmpty — empty mcp_config does NOT inject the env, preserving any value the user set in agent.custom_env - TestOpencodeBackendOverridesUserOpenCodeConfigContent — daemon value wins via os/exec dedup keep-last apps/docs/content/docs/providers.{en,zh}.mdx: - flip OpenCode's MCP cell from ❌ to ✅ - reword the "MCP configuration: only Claude Code actually reads it" section so OpenCode is included; describe each tool's mechanism (Claude → `--mcp-config`, OpenCode → OPENCODE_CONFIG_CONTENT) apps/docs/content/docs/install-agent-runtime.{en,zh}.mdx: - update the Claude Code blurb (no longer "the only one") - expand the OpenCode blurb to mention mcp_config support - fix the now-broken /providers anchor Refs #2106 (TS types and per-agent UI for mcp_config are separate follow-ups, not in this PR). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c7fdc77908 |
docs: add session resume and Korean support changelog (#3524)
* docs: add 2026-05-29 changelog Co-authored-by: multica-agent <github@multica.ai> * docs: clarify session resume 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> |
||
|
|
9fe7c935a9 |
MUL-2817: docs(i18n): add Korean (ko) documentation translation (#3521)
* docs(i18n): translate documentation corpus to Korean Add Korean (.ko.mdx) translations for all 32 navigable docs pages plus meta.ko.json navigation, mirroring the English source. Product terms (Issue→이슈, Agent→에이전트, Squad→스쿼드, Runtime→런타임, Skill→스킬, Workspace→워크스페이스, etc.) follow the in-app Korean locale at packages/views/locales/ko/. Roles (owner/admin/member) and issue status enums stay lowercase English per the conventions glossary. MUL-2817 Co-authored-by: multica-agent <github@multica.ai> * feat(docs): serve Korean docs content, remove English-fallback stopgap Now that the *.ko.mdx corpus exists, drop the temporary docsContentLang ko→en shim and the static-params fallback-synthesis loop so /docs/ko/* renders real Korean content. Korean is now a first-class locale whose params come straight from source.generateParams(). Also align the docs home hero copy (agent→에이전트) with the app and the translated body. MUL-2817 Co-authored-by: multica-agent <github@multica.ai> * docs(i18n): align residual Korean UI/product terms with the app Address review: sweep the .ko.mdx corpus for product/UI terms left in English and match the in-app Korean locale. - skills page title Skills → 스킬 - UI nav paths localized: Settings → 설정, Runtimes → 런타임, Agents → 에이전트, Projects → 프로젝트, Squads/New squad → 스쿼드/새 스쿼드, Usage → 사용량, Personal Access Tokens → API 토큰, Provider → 제공자, and the agent-create form labels (Name/Provider/Model/Instructions → 이름/제공자/모델/지침) - see-also links Issues/Workspaces/Environment variables and 'Providers Matrix' → Korean - kept as literals (verified): code blocks, the conventions i18n glossary data, 'Anthropic Agent Skills' (standard name), the Squad Operating Protocol/Roster/Instructions prompt-block names, the literal 'Project Context' prompt section, and Xcode's Settings path - add a docsAlternates test asserting ko hreflang is emitted when a real *.ko.mdx page exists MUL-2817 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5aa4fb7487 |
MUL-2760: feat(i18n): add Korean locale support (#3369)
* feat: add korean locale support * feat(i18n): localize Korean landing page * fix(i18n): refine Korean landing copy * fix(i18n): refine Korean translations * fix(i18n): translate Korean landing subpages * fix(i18n): route Korean landing docs links * fix(i18n): add Korean use case content * fix(i18n): polish Korean locale copy * fix(i18n): improve Korean landing copy * fix(onboarding): persist Korean helper artifacts Co-authored-by: multica-agent <github@multica.ai> * fix(web): add use case locale fallback Co-authored-by: multica-agent <github@multica.ai> * Align Korean pull requests wording Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): dedupe docs href helper Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): localize changelog dates Co-authored-by: multica-agent <github@multica.ai> * fix(docs): prerender Korean fallback pages Co-authored-by: multica-agent <github@multica.ai> * fix(docs): align fallback hreflang metadata Co-authored-by: multica-agent <github@multica.ai> * fix(i18n): preserve Chinese CJK font fallback order Co-authored-by: multica-agent <github@multica.ai> * chore(onboarding): update localized comment wording Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden CJK font fallback assertions Co-authored-by: multica-agent <github@multica.ai> * fix(docs): keep Chinese font fallbacks first Co-authored-by: multica-agent <github@multica.ai> * test(i18n): harden locale fallback coverage Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |