mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
fix/pi-empty-model-list
1092 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 d14f9d09f. Five docblocks still described `source` as multi-select; they now correctly say single-select and explain the array shape is kept purely for v2 back-compat with the JSONB column. - packages/core/onboarding/types.ts — QuestionnaireAnswers docblock - packages/core/onboarding/store.ts — PostHog mirror comment - packages/views/onboarding/steps/step-question.tsx — header docblock, canContinue branch, and footer-hint comment (Source moves from the multi-select side to the single-select side; Use case stays as the remaining multi-select consumer) - server/internal/handler/onboarding.go — questionnaireAnswers docblock and the stringOrSlice fall-back comment (the column "going multi- select" is no longer the current state; rename to "pre-array shape") - server/internal/analytics/events.go — OnboardingQuestionnaireSubmitted docblock No behaviour changes. Tests + Go build still green. Co-authored-by: multica-agent <github@multica.ai> * i18n(onboarding): add ja translations for source-backfill keys The Japanese locale landed on main (PR #3538) after this branch started, so my source-backfill round-2 keys (`common.close`, `source_backfill.eyebrow / lede / submit / hint_ready`) never made it into ja and the parity test fails in CI. Add them now with translations that match the en/zh-Hans/ko wording and tone. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
d63d2328ab |
fix(runtimes): consolidate out-of-band (WSL2) local daemon under LOCAL (MUL-2799) (#3497)
* fix(runtimes): consolidate out-of-band local daemon by host name The desktop derived `localMachineName`/`localDaemonId` only from the daemon it manages itself. When the real daemon runs out-of-band (e.g. in WSL2 on a Windows host), the app never gets a device name, so the #3336 device-name consolidation short-circuits on `!!localMachineName`: the local-mode runtime falls into REMOTE and an empty "This machine" placeholder is synthesized. Fix in two parts: - Desktop exposes the host OS hostname (`os.hostname()`) via a new `daemon:get-host-name` IPC and uses it as the final fallback for `localMachineName`, independent of daemon state. - Scope device-name consolidation to the current user's own local runtimes (`owner_id === currentUserId`). The runtime list is workspace-wide, so a host-name match alone could otherwise claim another member's identically named machine as "this machine". Once the real runtime classifies as current it moves to LOCAL and the empty placeholder is no longer synthesized. MUL-2799 Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): use OS-neutral "This device" label for the current machine The current-machine badge was hard-coded to "This Mac" (en), so it rendered incorrectly on Windows/Linux hosts. zh-Hans already used the neutral "本机". MUL-2799 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
16336ad33c | docs: ITT-236 clarify issue mention Markdown (#3365) | ||
|
|
063e9711df |
docs(changelog): add v0.3.11 release notes (#3449)
* docs(changelog): add v0.3.11 release notes Co-authored-by: multica-agent <github@multica.ai> * docs(changelog): refine v0.3.11 release notes Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7bbca54e92 |
feat(billing): test page consuming /api/cloud-billing/* (#3442)
* feat(billing): test page consuming /api/cloud-billing/*
Stuffs every cloud-billing endpoint onto a single dev page so we can
verify the proxy + Stripe end-to-end flow without a designed UI.
Reachable at /<workspaceSlug>/billing — the page is account-level
data but lives under the workspace dashboard layout because that's
where the authenticated shell sits. No sidebar entry on purpose;
this is test-quality and meant to be deleted when the real billing
UI ships.
What's there:
* Balance card (GET /balance)
* Stripe-success polling banner — visible only when ?session_id=
is in the URL (Stripe substitutes it into checkout_success_url
on its way back). React Query refetchInterval polls every 2s
until the topup status reaches credited / failed / canceled,
then a 'Clear from URL' button calls navigation.replace(pathname).
* Buy section: server-authoritative price tier buttons (GET
/price-tiers) → POST /checkout-sessions → window.location to the
Stripe URL. We do NOT hard-code amounts on the frontend; tier
config lives in cloud's billing.price_tiers.
* Stripe Billing Portal button (POST /portal-sessions). Opens in a
new tab so the originating page stays put for easy verification.
Documented behaviour: 400 is expected for users with no Stripe
customer record yet.
* Three lists: transactions / batches / topups.
Plumbing:
* packages/core/types/billing.ts — interfaces mirroring the cloud
response shapes. Status / source / tx_type fields are typed
'string' rather than enum unions to match the schemas' z.string()
parsing (same convention as CloudRuntimeNode); the canonical
enum values are exported as separate type aliases for callers
that want to switch on them.
* packages/core/api/schemas.ts — 9 zod schemas + 7 EMPTY_ fallbacks,
all .loose() so a non-breaking cloud-side field addition doesn't
crash the parser.
* packages/core/api/client.ts — 8 methods using parseWithFallback,
matching the existing cloud-runtime shape.
* packages/core/billing/{queries,mutations,index}.ts — React Query
queryOptions + mutations. Notable choices: balance / lists are
NOT keyed on workspace (account-level data), and the
checkout-session polling stops automatically when status is
terminal so we don't poll forever after a user closes the tab.
* packages/core/package.json + packages/views/package.json — exports
map updated for @multica/core/billing and @multica/views/billing.
Verification:
* pnpm --filter @multica/core typecheck clean
* pnpm --filter @multica/views typecheck — only pre-existing
hast-util-to-html error in editor code (exists on main)
* pnpm --filter @multica/core test — 412 passing
* pnpm --filter @multica/views test — 877 passing, 1 failure
(editor/readonly-content) is also pre-existing on main, not
caused by this change
Out of scope: real production-quality billing UI; sidebar entry; i18n
strings; mobile app. This is a single test page; it gets replaced
when the real UI ships.
* fix(billing): refetch balance/lists when checkout polling reaches terminal
Closes the second-half of the Stripe-return race the previous commit
left dangling.
Symptom:
After Stripe redirects back with ?session_id=..., the banner polls
/checkout-sessions/{id} every 2s and the rest of the page (balance,
transactions, batches, topups) is fetched once on mount. The
webhook race means those four queries usually see pre-credit state
— but the banner is the only thing that keeps polling, so once it
reads 'credited' nothing else on the page knows. The user would
see 'Final status: credited' next to a stale balance card until
they manually refresh.
Fix:
Add useInvalidateBillingDataAfterCredit() in @multica/core/billing —
a hook returning a callback that flushes balance / transactions /
batches / topups (NOT the checkout-session itself; its
refetchInterval already terminated, refetching would just confirm
the same value). The Stripe-success banner runs this callback in
a useEffect keyed on terminal-status transition, so it fires
exactly once when the polling lands.
Strict scope is documented in the hook's JSDoc:
- balance/transactions/batches: only change at the 'credited'
transition (cloud writes ledger + batch + wallet in one DB tx)
- topups: changes on every terminal transition
- For 'failed' / 'canceled' we technically over-fetch the first
three; three cheap round-trips, simplifies the call site, fine
on a test page.
Effect dep is . terminal flips false→true at most once
per session id (the polling stops when terminal is true so the
data won't change again). If the user lands here with a session
that is already terminal (re-opened tab on a credited URL), the
effect still fires on first data load and we still re-fetch —
correct, the cached snapshot is just as stale in that case.
go build / pnpm typecheck / pnpm test clean (core 412 passing; only
pre-existing hast-util-to-html error in unrelated editor code on
views, same as on main).
|
||
|
|
90ddfb04e2 |
feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777) (#3441)
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433) When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces returns 403 for every caller and the UI hides every "Create workspace" affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This closes the gap where ALLOW_SIGNUP=false still let any signed-in user open an isolated workspace the platform admin couldn't see. - server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace, workspace_creation_disabled in /api/config, Go tests. - frontend: new workspaceCreationDisabled in configStore, hide sidebar entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding StepWorkspace to a "creation disabled, ask for invite" state when the flag is on, EN + zh-Hans locale strings. - ops: .env.example, docker-compose.selfhost, helm values + configmap, SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs (EN + zh). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): drive create path off workspaceCreationAllowed (#3433) PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already has a workspace, StepWorkspace still walked the resume copy (`headline_resume` / `lede_resume` mentioning "or start another") and `creatingActive` ignored the flag, leaving a stale clickable create CTA possible if /api/config arrived late. Refactor StepWorkspace to derive a single `workspaceCreationAllowed` boolean from the config store. It now drives: - Initial `mode` state (defaults to "existing" when disabled + reusing so the CTA is pre-armed for the only valid action). - `creatingActive` so the footer CTA cannot fall back into the create branch even mid-render. - Eyebrow / headline / lede strings — adds `creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for the disabled + reusing variant. Tests: cover the three reachable shapes — flag off + no existing, flag on + no existing, flag on + existing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bae8a84abd |
MUL-2767 feat(agent): add Antigravity runtime backend (#3427)
* feat(agent): add Antigravity runtime backend Adds Google's Antigravity CLI (`agy`) as the 12th supported coding-tool runtime, alongside Claude / Codex / Cursor / Copilot / Gemini / Hermes / Kimi / Kiro / OpenCode / OpenClaw / Pi. The CLI emits plain assistant text on stdout (no structured event stream), so the backend streams stdout line-by-line as `MessageText` events and accumulates the same text as the final `Result.Output`. Session resumption uses `--conversation <id>`; because the conversation UUID is not echoed on stdout, the daemon routes `--log-file` to a temp file and recovers the id from the glog-formatted log lines. MUL-2767 Co-authored-by: multica-agent <github@multica.ai> * fix(agent): correct Antigravity capability contract from Elon review - ModelSelectionSupported now returns false for antigravity. `agy` has no --model flag and antigravityBackend deliberately drops opts.Model, so the UI must render a disabled "Managed by runtime" picker instead of an empty dropdown plus a silently-ignored manual-entry field. Also stop seeding AgentEntry.Model from MULTICA_ANTIGRAVITY_MODEL — the backend would silently ignore it. - Antigravity skills now write to {workDir}/.agents/skills/, the CLI's native workspace path (inherits Gemini CLI's layout per https://antigravity.google/docs/gcli-migration). Previously they went to the .agent_context/skills/ fallback that the CLI doesn't scan. Runtime brief moves antigravity into the native-discovery branch and local_skills.go points the user-level skill root at ~/.gemini/antigravity-cli/skills for Runtime → local skill import. - Doc + UI comment sync: providers matrix / install-agent-runtime / cloud-quickstart / agents-create / tasks (session-resume support) / skills / README all now list Antigravity in the right buckets, and the model-picker / model-dropdown comments cite antigravity (not the stale hermes reference) as the supported=false example. New tests: TestAntigravityModelSelectionUnsupported, TestInjectRuntimeConfigAntigravity (native discovery wording), TestWriteContextFilesAntigravityNativeSkills (.agents/skills/ landing, .agent_context/skills/ NOT written). Co-authored-by: multica-agent <github@multica.ai> * feat(provider-logo): swap inline placeholder for real Antigravity PNG Replaces the hand-drawn planet+arc placeholder with the official asset shipped from Downloads. Stored next to the component; bundlers (Next.js / electron-vite) resolve the PNG import to a URL string at build time. Added a small assets.d.ts so packages/views' tsc accepts PNG / SVG module imports — there was no prior asset usage in this package to register the declaration. --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fe2c990296 |
docs(self-host): document Microsoft Exchange / SMTP relay modes and failure diagnostics (#3426)
GH#3405 / MUL-2768. Self-host docs already point at the SMTP path, but on-prem operators ran into two gaps: - The Option B env block in auth-setup and self-host-quickstart only showed a 587 authenticated example, with no copy-pasteable block for the most common Exchange "anonymous internal relay on port 25" pattern, and no explicit mapping between port / auth / TLS / supported-or-not. - troubleshooting "Emails not received" only covered Resend; SMTP failures (smtp dial / starttls / auth / MAIL FROM / RCPT TO / DATA) surface as wrapped errors in the backend logs, but operators had no doc telling them which Exchange-side fix maps to each. Adds: - A relay-mode table (anonymous 25 / authenticated 587 / 465 still unsupported) and two copy-pasteable env blocks in both auth-setup.mdx and self-host-quickstart.mdx (EN + ZH). - Explicit note on the EmailService startup log line so operators can confirm SMTP is the active provider after restart, without leaking credentials. - An SMTP failure-mode table in troubleshooting.mdx (EN + ZH) keyed on the exact wrapped error string, with the Exchange-side fix for each. No code changes; env variable surface unchanged (still SMTP_HOST / SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORD / SMTP_TLS_INSECURE). Port 465 stays "not supported" pending #3340. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1947830d4b |
chore(mobile): re-ignore .DS_Store under apps/mobile/data/
The `!data/**` negation that rescues apps/mobile/data/ from the root .gitignore's `data/` rule was inadvertently pulling .DS_Store back in too — Finder metadata kept showing up in git status. Restate .DS_Store after the negation so last-match-wins re-ignores it. |
||
|
|
be32e5af00 |
docs(changelog): add v0.3.10 release notes (#3362)
* docs(changelog): add 2026-05-27 release notes Co-authored-by: multica-agent <github@multica.ai> * docs: refine v0.3.10 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> |
||
|
|
d16ed2806a |
MUL-2748 docs(autopilots): document webhook event filters + link from UI (#3359)
* MUL-2748 docs(autopilots): document webhook event filters and link from UI Follow-up to PR #3231. The webhook event filters feature shipped without user-facing docs and the UI section gave no hint about how event/action are derived from inbound requests. - Add an "Event filters" subsection to autopilots.mdx and .zh.mdx under the existing "Trigger from a webhook" section: what the filter does (with the `event_filtered` outcome), where the event name and action come from (body envelope / headers / body fallbacks), examples, a non-string-action gotcha, and a curl recipe verifying both the allowed and filtered paths. - Add a small ExternalLink icon next to the "Event filters" label in WebhookEventFilterSection that opens the docs section in a new tab. Locale-aware: zh users land on the Chinese page anchor, en users on the English one. Co-authored-by: multica-agent <github@multica.ai> * docs(autopilots): expand "where event name and action come from" with curl examples Add concrete curl request/inference pairs under each derivation step (body envelope, headers, body fallback, default) and a "common gotcha" explaining why filter `event=trigger, action=true` does not match `{"trigger": true}`. Mirrors the explanation that resolved the on-call confusion about Event filter semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bd1fb10afa |
chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute - Replace useContext() with React 19 use() across 16 context consumers - Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files (mobile excluded — Hermes lacks toSorted support) - Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check - Fix nested component definition: useMemo wrapping HeaderRight → useCallback - Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad React Doctor score: 23 → 30. No behavioral changes, no business logic modified. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
963c33f030 |
MUL-2618 docs(project-resources): document local_directory resource type (#3347)
* docs(project-resources): document local_directory resource type Add user-facing guidance for the local_directory project resource introduced in #3283 — when to pick it over github_repo, the Desktop / CLI attach flow, path validation rules, the daemon-scoped one-per-(project, daemon) limit, serial task execution + waiting_local_directory status, what the daemon will and won't touch in the user's folder, and the v1 limits to call out (no auto branch switch / commit / PR; dirty tree carried through). Also ship the missing Chinese counterpart of project-resources and wire it into meta.zh.json. MUL-2618 Co-authored-by: multica-agent <github@multica.ai> * docs(project-resources): cover write-conflict trade-off + mixed-resource behavior Expand the local_directory docs in response to review feedback: - Restate "when to pick local_directory" as two distinct use cases (clone cost; fine-grained changes needing frequent local review) instead of a one-liner, and make the trade-off explicit: v1 ships no file-level write lock, so the per-directory serial gate is the only protection against cross-issue agents touching the same files. - Add a new "Mixing resource types, and multiple local_directory resources" section that answers: github_repo + local_directory on the same project (local takes precedence on the bound daemon, github_repo falls back everywhere else), and two local_directory resources (only possible across two daemons, routed by the agent's runtime assignment, no load-balancing). Mirrored into the Chinese translation. typecheck + tests still pass. Co-authored-by: multica-agent <github@multica.ai> * docs(project-resources): tighten local_directory wording per review - Soften "only the bound daemon can take tasks" to "only the bound daemon uses this local directory" (zh) — aligns with the mixed-resource fallback section where other daemons still run. - Clarify that local_directory does not create/use a github_repo worktree for that task (en + zh); the per-workspace repo cache may still sync as a background behaviour. - Match implementation for the Desktop "Add local directory" button: it stays visible but is disabled with a hint when daemon is offline or the per-daemon limit is reached; only the web app hides it outright (en + zh). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
341ce7bfa5 |
feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)
Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.
Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.
Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.
Co-authored-by: multica-agent <github@multica.ai>
* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)
Addresses the Elon review on PR #3263:
- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
matching handler, CLI `project resource update`, and a new
EventProjectResourceUpdated WS event. resource_type stays immutable;
ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
embedded label differs — the row-level UNIQUE only matches the full
ref JSON, so a label typo would otherwise let the same working
directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
invalid path) and the label-shadow conflict on both create and
update; the in-place rename still succeeds because the conflict
scan ignores the row being edited.
Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.
Co-authored-by: multica-agent <github@multica.ai>
* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)
Two follow-ups from MUL-2662 review round 2:
- CreateProject inline resources path now dedupes local_directory entries on
(daemon_id, local_path) before opening the transaction. The DB-level
UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
full JSON match, so two rows with the same target but different `label`
would otherwise slip past. Standalone POST/PUT already cover this via
findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
row before applying per-type shortcut flags, so `--default-branch-hint x`
on its own no longer constructs a payload missing `url` (which the server
400s on). Local_directory partial edits get the same merge behavior.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)
* feat(desktop): local_directory project_resource UI (MUL-2665)
First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.
What's new for the renderer:
- ProjectResourcesSection grows a desktop-only "Add local directory"
button next to the existing GitHub-repo popover. Clicking it opens
Electron's native folder picker, validates the path through a new
IPC pair (existence + r/w), and submits a project_resource of
resource_type=local_directory with daemon_id pulled live from
daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
greys out when ref.daemon_id != this machine's daemon_id (with a
"only available on the machine that registered this directory"
tooltip). Delete stays enabled so users can drop stale registrations
from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
shows "Agent will work in-place at {label} ({path})" when the issue's
project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
that the daemon will publish when it dequeues a task but can't
acquire the path lock. The render is in place now so the daemon
sibling subtask can wire the status string without an additional UI
PR.
Plumbing:
- @multica/core/types gains LocalDirectoryResourceRef +
UpdateProjectResourceRequest, and the api client gets the matching
PUT method backed by the server endpoint that landed in
|
||
|
|
7e37461075 |
docs: add 2026-05-26 changelog entry (#3293)
* docs: add 2026-05-26 changelog entry Co-authored-by: multica-agent <github@multica.ai> * docs: refine 2026-05-26 changelog entry Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
744b474199 |
revert(agent): remove per-agent local skill toggle (MUL-2603) (#3286)
* Revert "feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)" This reverts commit |
||
|
|
0b50c5a209 |
feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)
* feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) Only Claude Code and Codex runtimes actually enforce `skills_local` at exec time today — Claude isolates `~/.claude/skills/` via `CLAUDE_CONFIG_DIR`, Codex isolates `~/.codex/skills/` via per-task `CODEX_HOME`. Every other runtime currently stores the field but treats it as a no-op, which made the toggle in the Create Agent dialog and Skills tab misleading for those runtimes. Gate the toggle on `runtime.provider` so it only renders for the providers the daemon currently isolates. Centralise the supported-provider list as `isSkillsLocalSupportedProvider()` in `packages/core/agents` and reuse it from the create dialog and the Skills tab. The create dialog also drops `skills_local` from the payload when the selected runtime is unsupported, so a runtime swap can't leave a stale `ignore` opt-in pinned where it would never take effect. Docs (EN + ZH) updated to say the toggle is hidden — not just "a no-op" — for the unsupported runtimes. Co-authored-by: multica-agent <github@multica.ai> * docs(agents): align skills_local hint and type comment with claude+codex boundary Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
46f7ac6bac |
docs(self-host): document task_usage_hourly rollup requirement (MUL-2682) (#3280)
The Usage / Runtime dashboards read from `task_usage_hourly`, but the default self-host stack does not schedule `rollup_task_usage_hourly()` anywhere — the bundled pgvector/pgvector:pg17 image ships without pg_cron, and the backend does not run the rollup in-process. Fresh installs see the dashboard stay at zero forever (#3244), and upgrades from v0.3.4 → v0.3.5+ are blocked by migration 103's fail-closed guard (#3015). Document the three supported paths (external cron / systemd-timer / CronJob, Postgres with pg_cron, or backfill_task_usage_hourly for upgrades) across SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, the quickstart pages on the docs site, and add troubleshooting entries for both the silent-zero and the migration-guard failure modes. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
960befa56f |
feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603) (#3200)
* feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603)
Adds an agent-scoped `skills_local` switch ("ignore" default / "merge") so
shared agents stop inheriting the operator's user-global Claude skill
directory. A single broken local skill on one operator's machine was
crashing the Claude CLI before it ever read stdin — the daemon saw a
"broken pipe" with no recoverable signal (GitHub #3052).
- DB: migration 108 adds `agent.skills_local` (NOT NULL DEFAULT 'ignore'),
with sqlc CreateAgent/UpdateAgent updates and handler validation.
- Claude runtime: when the agent is in "ignore" mode the backend points
CLAUDE_CONFIG_DIR at an empty per-task scratch dir under the task cwd
(fallback: OS temp), strips any inherited override, and cleans up after
the run. Workspace skills under `{cwd}/.claude/skills/` still load.
"merge" preserves the legacy inherit-from-machine behavior; Codex and
other isolated backends are no-ops.
- UI: new Skills toggle in the Create Agent dialog and the Agent → Skills
tab, with EN/zh-Hans copy and SkillsLocalToggle shared between the two.
- Tests: unit coverage for the new env helper, isolation dir lifecycle,
full Claude execute paths (ignore + merge), and the handler tristate
contract. Existing skills-tab test updated for the new copy.
- Docs: updated `/skills` docs (EN + ZH) and added a 0.3.7 changelog entry
in the landing-page i18n.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve claude login + validate skills_local input (MUL-2603)
Address Elon's review on PR #3200:
1. Skill isolation no longer drops the operator's Claude login. The
per-task scratch dir now mirrors every entry under `~/.claude/`
as symlinks except `skills/`, so `.credentials.json`, settings,
plugins, etc. reach the CLI exactly as on the host while the
user-global skills directory stays hidden. Without this, default
`ignore` would have broken every Claude agent on a non-API-key
host the moment migration 108 landed.
2. Internal CreateAgent callers (agent_template, onboarding_shim)
now set `SkillsLocal: "ignore"`. The Go zero value was about to
trip the migration-108 CHECK constraint and 500 template /
onboarding agent creation.
3. Create / update handler validation no longer normalizes garbage
to "ignore". The strict 400 path is now reachable on bad client
input; the drift-safe `normalizeSkillsLocal` stays on the read
side only.
UI copy + docs clarified that the toggle is Claude-only; other
runtimes ignore the setting.
Verification:
- `go test ./...` green (full suite locally).
- `pnpm --filter @multica/views exec vitest run agents/components/tabs/skills-tab.test.tsx` green.
- Handler DB-backed tests still skip locally without docker (same
as Elon's run) — CI will validate the create / update paths
against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): mirror effective claude config dir with windows fallback (MUL-2603)
Address Elon's second-round review on PR #3200:
1. The per-task scratch dir now mirrors the *effective* host Claude
config dir, not unconditionally `~/.claude/`. Precedence: agent
`custom_env` CLAUDE_CONFIG_DIR > parent process env > `~/.claude/`.
Without this, an operator who pinned Claude at a managed install
(custom env CLAUDE_CONFIG_DIR) would get the wrong credentials in
the scratch dir, because `buildClaudeEnv` strips that env before
handing it to the child. We resolve the source up front and feed
it to the mirror, so the override env still points at the right
bytes.
2. Mirror entries now go through platform-aware linkers. On Windows
without Developer Mode / admin, `os.Symlink` is denied, which
previously left the scratch dir empty and broke Claude Code auth
on default `ignore`. The new helpers try symlink first, then fall
back to a directory junction (`mklink /J`) for dirs or a hardlink
(same-volume content share) / copy for files. Mirrors the
execenv/codex_home_link_windows.go pattern.
3. Tests:
- `TestResolveHostClaudeConfigDir` locks in the custom_env >
parent_env > `~/.claude` precedence.
- `TestNewIsolatedClaudeConfigDirMirrorsCustomHostDir` confirms
the scratch dir picks up `.credentials.json` from a synthetic
custom host dir, proving the source resolution actually
propagates into the mirror.
- `TestNewIsolatedClaudeConfigDirEmptyHostIsNoop` documents the
env-var-auth-only case (no host source ⇒ empty scratch dir).
- `TestMirrorHostClaudeExceptSkillsWith_FallbackWhenSymlinkFails`
exercises the Windows-no-Developer-Mode path via the new
`mirrorHostClaudeExceptSkillsWith` seam, asserting credentials
and sub-dir children still reach the scratch dir after the
symlink stand-in fails.
- `TestMirrorHostClaudeExceptSkillsWith_PropagatesFirstLinkError`
confirms callers see the per-entry error when even fallback
fails (so the warn-log fires on broken Windows installs).
- `TestCopyFileRoundTrip` covers the last-resort copy fallback
and its EXCL no-overwrite contract.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` is the
end-to-end check: an agent with custom_env CLAUDE_CONFIG_DIR
reads its credentials from the pinned dir, not `~/.claude/`.
4. Docs: `apps/docs/content/docs/skills.{mdx,zh.mdx}` updated to
describe the effective-source resolution and the Windows
fallback chain so the docs match the runtime behaviour.
Verification:
- `go test ./...` green (full server suite locally, including
`pkg/agent` 23 cases covering the new + existing isolation
paths).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean, confirming the
Windows-tagged linker file builds.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): default skills_local to merge to preserve legacy behavior (MUL-2603)
Per Bohan's product decision on PR #3200, the per-agent host-skill toggle
defaults to "merge" — the pre-MUL-2603 inherit-from-machine behavior —
so existing personal workflows that rely on locally installed Claude
Skills keep working unchanged. Agent owners explicitly opt into "ignore"
when they need to harden a shared agent against a broken local skill on
one operator's machine (GitHub #3052).
Also audited all 11 runtimes for user-global skill discovery paths and
documented the scope of the toggle. Only Claude reads a user-global
`~/.claude/skills/`; Codex isolates via `CODEX_HOME`, the ACP backends
(Hermes / Kimi / Kiro) and the JSON-stream backends (Copilot / Cursor /
Gemini / Pi / OpenCode / OpenClaw) anchor discovery to the task workdir
and never read a user-global skill directory. UI copy and docs now say
"for runtimes that support it (currently Claude Code)" everywhere so
the scope is explicit.
Changes:
- Migration 108: column default flipped to 'merge'.
- Handler CreateAgent: missing field → "merge"; explicit "ignore" /
"merge" still validated, garbage still 400.
- normalizeSkillsLocal: drift-safe coercion now lands on "merge" for
anything that isn't the exact literal "ignore".
- agent_template.go / onboarding_shim.go: internal CreateAgent callers
send "merge" instead of "ignore" to match the new default.
- Claude runtime (`claude.go`): isolate-mode gate flipped from
`SkillsLocal != "merge"` to `SkillsLocal == "ignore"`, so "" (legacy
daemons / older clients) and "merge" both walk `~/.claude/` directly.
- Create Agent dialog + Skills tab: toggle defaults to on (merge); only
duplicate of an explicit "ignore" agent carries through. The
isolation opt-in is now `skills_local: "ignore"` when the user flips
off; "merge" is omitted from the request body.
- i18n (EN + zh-Hans): copy reframed — "On (default) — merged"; "Off —
ignored. Recommended for shared agents".
- Docs (`/skills`, `/guides/agents.zh`): describe new default and
enumerate which runtimes act on the toggle.
- Landing changelog 0.3.7: retitled "Per-Agent Local-Skill Toggle"; note
the on-by-default behavior + off-to-isolate framing.
- Tests:
- `TestClaudeExecuteIsolatesHostSkillsWhenIgnoreOptedIn` replaces the
old by-default isolation case (now requires explicit "ignore").
- New `TestClaudeExecuteDefaultModeKeepsHostConfigDir` locks in that
default ExecOptions preserve the host CLAUDE_CONFIG_DIR.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` now explicitly opts
into "ignore" mode.
- Handler tests: omitted → "merge"; explicit "ignore" round-trips;
preserve-existing test seeds "ignore" and asserts "merge" flip-back.
- `TestNormalizeSkillsLocal_DriftStaysSafe`: only literal "ignore"
maps to ignore; everything else → "merge".
- `skills-tab.test.tsx`: toggle ON by default; flip OFF when agent
opted into "ignore". Intro-text matcher anchored to a more specific
phrase so it no longer collides with the toggle hint copy.
Verification:
- `go test ./...` green (full server suite locally).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean (windows-tagged linker
file still builds).
- `pnpm typecheck` green across all packages and apps.
- `pnpm --filter @multica/views test` 88 files / 771 tests green.
- `pnpm --filter @multica/core test` 43 files / 390 tests green.
- Handler DB-backed tests still skip locally without docker; CI will
validate the create / update paths against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* chore(landing): drop 0.3.7 changelog entry from this PR (MUL-2603)
The landing-page release notes belong in a separate release-prep PR, not in the feature PR.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): propagate skills_local=ignore to codex user-skill seed (MUL-2603)
Make the per-agent skills_local toggle real for Codex too, not just Claude.
Previously the toggle was only consumed by the Claude backend, while the
daemon's execenv layer always seeded Codex's per-task CODEX_HOME with the
host machine's user-installed skills from ~/.codex/skills/. A shared Codex
agent with skills_local=ignore could still inherit a broken local skill
from one operator's machine.
Now: PrepareParams/ReuseParams carry SkillsLocal; hydrateCodexSkills
skips seedUserCodexSkills when SkillsLocal == "ignore" so the per-task
CODEX_HOME exposes only workspace skills to the codex CLI. Default
("merge", or empty from older servers/clients) preserves existing
inherit-from-machine behavior. UI / docs are updated to reflect the
contract honestly: Claude Code and Codex honor the toggle; other
runtimes (Hermes / Kimi / Kiro / Copilot / Cursor / Gemini / Pi /
OpenCode / OpenClaw) leave $HOME untouched and discover user-level
skills natively, so the toggle is a no-op for them today.
New tests: TestPrepareCodexSkillsLocalIgnoreSkipsUserSeed,
TestPrepareCodexSkillsLocalMergeSeedsUserSkills, and
TestReuseCodexSkillsLocalIgnoreSkipsUserSeed cover Prepare(ignore),
Prepare(merge), and the toggle-flip-on-reuse path.
Co-authored-by: multica-agent <github@multica.ai>
* docs(skills): scope skills_local toggle copy to Claude Code + Codex (MUL-2603)
Off-state hint and Skills tab intro now explicitly call out Claude Code +
Codex as the only runtimes that honor the toggle, with "other runtimes
ignore this setting" wired into both states (en + zh-Hans), so users on
non-Claude/Codex agents don't read "Off" as runtime-wide isolation.
Docs (skills.mdx, skills.zh.mdx, guides/agents.zh.mdx) stop describing
Hermes / Kimi / Gemini / Copilot / Cursor / Pi / OpenCode / OpenClaw / Kiro
as having native user-level skill discovery; the daemon simply does not
manage user-level skill discovery for those runtimes today, and the toggle
is a no-op regardless of where it is set.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
ec1589f7b6 |
fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) (#3249)
* fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) Introduce eslint-plugin-import-x/no-extraneous-dependencies rule to prevent phantom deps from causing production build splits when pnpm creates peer-dep variants. Fix all existing phantom deps across the monorepo, unify catalog references, and enable desktop smoke CI on PRs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * revert(ci): remove desktop smoke PR trigger per user feedback The existing smoke workflow only verifies packaging completes — it does not actually start the app or check rendering. This means it wouldn't have caught the white-screen bug (which was a runtime issue, not a build failure). Adding it to PRs would slow CI without providing meaningful protection. The ESLint no-extraneous-dependencies rule is the actual prevention mechanism. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(deps): sync pnpm-lock.yaml for rehype-sanitize dep classification Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): move rehype-sanitize to deps + declare eslint-config (MUL-2654) - Move rehype-sanitize from devDependencies to dependencies (used in production Markdown.tsx) - Add @multica/eslint-config to devDependencies (imported by eslint.config.mjs but previously undeclared) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e351f89843 |
fix(desktop): declare phantom deps + dedupe react-query in renderer (MUL-2651) (#3232)
Packaged renderer was bundling two copies of @tanstack/react-query because apps/desktop imported it without declaring the dep, so Node resolution fell through to the hoisted root variant (react@19.2.0, pulled in by apps/mobile), while packages/core resolved to the catalog variant (react@19.2.3). Two physical paths → two QueryClientContexts → "No QueryClient set" white screen on launch. - Declare @tanstack/react-query, lucide-react, zustand as direct deps via catalog: so apps/desktop resolves to the same peer variant as packages/core/views. - Add @tanstack/react-query to renderer dedupe as a defense-in-depth bound against future peer drift. Verified: realpaths under apps/desktop, packages/core, packages/views all point to @tanstack+react-query@5.96.2_react@19.2.3; production renderer bundle now contains exactly one "use QueryClientProvider to set one" string (was 2) and no useQueryClient\$1 suffix. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
13f74e651a |
feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600) (#3209)
* feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600)
The agent resource shape (list / get / create / update / archive /
restore responses + WebSocket events) no longer carries `custom_env`
values. Reads/writes of env now flow exclusively through a dedicated
`/api/agents/{id}/env` endpoint that is owner/admin-only, rejects
agent-actor sessions, applies a "****" sentinel preserve guard on
PUT, and writes a persistent audit row per reveal/update.
Why
- `multica agent list --output json` historically returned plaintext
`custom_env` for owner/admin callers (the redaction gate gave only
members the masked map). Any agent token running on the workspace
inherits its owner's role and could read every other agent's
secrets just by listing.
- Patching list/get redaction alone (PR #3175 direction) left
symmetric leaks via mutation responses, WS events, the "reveal"
path itself (no actor-aware auth), and a `****` overwrite footgun
on UpdateAgent.
What changed
- Backend: drop `custom_env` from AgentResponse; add coarse
`has_custom_env` + `custom_env_key_count`. Strip env handling from
UpdateAgent (silently ignored if sent). Keep CreateAgent's
custom_env acceptance.
- Backend: new GET/PUT `/api/agents/{id}/env` handlers in
`internal/handler/agent_env.go`:
- resolveActor → 403 for agent actors (closes the lateral-movement
path).
- Owner/admin role gate via existing helper.
- PUT honours value == "****" as "preserve existing value".
- Both write to `activity_log` with `agent_env_revealed` /
`agent_env_updated` actions. Audit details record key names only,
never values.
- Daemon claim path (`ClaimAgentTask`) unchanged — `TaskAgentData`
still carries plaintext env for runtime injection.
- SQL: new `UpdateAgentCustomEnv` query; sqlc regenerated (v1.31.1).
- CLI: new `multica agent env get|set` subcommands. `--custom-env*`
flags removed from `multica agent update`; the no-fields error
now points to the new path.
- Frontend: drop env fields from `Agent` + `UpdateAgentRequest`; add
`getAgentEnv` / `updateAgentEnv` client methods; rewrite env-tab
to show "N variables configured" + explicit "Reveal & edit"
button, fetching values only on intentional reveal.
- Locales: parity-safe additions to en + zh-Hans.
- Docs: agents-create.{mdx,zh.mdx} reflect the new threat model and
endpoint.
- Mobile: schema drops `custom_env` / `custom_env_redacted`, adds
metadata fields.
Tests
- Handler tests pinned the new invariants: no env in list/get
responses, owner reveal happy-path + audit row, agent-actor 403,
`****` sentinel preserves real values, UpdateAgent silently
ignores `custom_env`, pure `mergeAgentEnv` cases.
- CLI tests pivot to the new flag surface: `agent update` MUST NOT
expose the env flags; `agent env set` MUST expose
--custom-env-stdin/--custom-env-file.
- Frontend test fixtures updated; pnpm typecheck / test / lint
pass cleanly.
This is a breaking API change. Scripts that read `custom_env` from
`/api/agents` must migrate to `GET /api/agents/{id}/env`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close actor-spoofing + audit fail-closed in env endpoints (MUL-2600)
Addresses Elon's review of #3209:
* Mint a task-scoped `mat_` token per claim, bound to (agent, task,
workspace, owner). Daemon injects it into the agent process in place
of its own credential. Auth middleware authoritatively rebuilds
X-User-ID / X-Agent-ID / X-Task-ID from the token row and sets
X-Actor-Source=task_token; that header is server-set only — incoming
values are stripped before any auth branch runs. resolveActor honors
the header so an agent that strips X-Agent-ID / X-Task-ID still
resolves as actor=agent.
* GetAgentEnv / UpdateAgentEnv are now fail-closed on audit-log
failures: GET refuses to return plaintext, PUT persists inside the
same tx as the audit row so they commit/roll back together.
* PUT /api/agents/{id} returns 400 when the body carries custom_env
instead of silently dropping it — directs callers to the audited env
endpoint.
* Agent actors never see mcp_config, even when the underlying member
is owner/admin; mutation broadcasts go through a redaction shim so
WS subscribers don't pick it up either.
* Fix backend test that asserted dense JSON (jsonb::text renders
whitespace) and frontend test that assumed a unique "Test User"
match.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close residual MUL-2600 gaps from review (MUL-2600)
Migration 108 FK now correctly references agent_task_queue(id) instead
of the non-existent agent_task table; the previous name blocked CI
backend migrations.
Task-token-authenticated requests can no longer be re-routed at a
different workspace by passing workspace_slug / workspace_id /
?workspace_id / a URL workspace param. ResolveWorkspaceIDFromRequest
and resolveWorkspaceUUID both short-circuit on X-Actor-Source=task_token
and return only the token-bound X-Workspace-ID; buildMiddleware adds a
defence-in-depth 403 if any URL-resolved workspace disagrees with the
token binding.
mcp_config no longer leaks back to agent actors through UpdateAgent /
CreateAgent / ArchiveAgent / RestoreAgent HTTP responses — the same
redactAgentResponseForActor helper that GetAgent/ListAgents use is now
applied to mutation responses too. WS broadcasts were already redacted
via broadcastAgentResponse.
FailTask and every TaskService cancel path (CancelTask /
CancelTasksForIssue / CancelTasksForAgent / CancelTasksByTriggerComment
/ BroadcastCancelledTasks) now eagerly DeleteTaskTokensByTask so the
mat_ token's 24h window doesn't outlive a terminated task. Failure is
non-fatal — the FK cascade and expiry remain durable guards.
Doc-only: clarify that PUT /api/agents/{id} now hard-rejects bodies
that carry custom_env (was previously "silently ignores").
Tests:
- middleware: TestResolveWorkspaceIDFromRequest gains a task_token
case asserting client-supplied slug/id/query cannot override the
bound workspace.
- handler: TestUpdateAgent_RedactsMcpConfigForAgentActor and
TestUpdateAgent_KeepsMcpConfigForMemberActor pin the mutation-
response redaction contract per actor type.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): match redacted mcp_config as JSON null, not Go nil (MUL-2600)
`AgentResponse.McpConfig` is `json.RawMessage` without `omitempty`, so
the redacted response serialises as `"mcp_config": null`. On decode,
`json.RawMessage` keeps the literal bytes `null` rather than collapsing
to Go nil, which made the assertion fire on a non-leak.
The product contract (field always present, distinguished from "no
config" via `mcp_config_redacted`) is intentional, so adjust the test
to check for "no secret-bearing content" instead of weakening the
contract via `omitempty`.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
5877cff9de |
docs: add 2026-05-25 changelog entry (#3218)
* docs: add 2026-05-25 changelog entry Co-authored-by: multica-agent <github@multica.ai> * docs: clarify iOS changelog availability Co-authored-by: multica-agent <github@multica.ai> * docs: remove reverted squad fix from 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> |
||
|
|
f9dfb3b9fc | Fix duplicate desktop back navigation (#3210) | ||
|
|
b9bf2653be |
fix(desktop): retry tab scroll restore until virtualized content rehydrates (#3214)
Follow-up to #3196. Switching tabs and back on a long issue still landed at scrollTop=0 because issue-detail uses Virtuoso with customScrollParent — Virtuoso wires its scroll/resize observers in a passive useEffect, which fires *after* useLayoutEffect. So at the moment the restore hook ran, the spacer that gives the scroll container its tall scrollHeight hadn't been re-established yet (scrollHeight === clientHeight), and the browser silently clamped `scrollTop = saved` down to 0. Diagnostic console output confirmed this: marker key=true saved=10356.5 currentScrollTop=0 scrollHeight=750 clientHeight=750 → set scrollTop to 10356.5 actually now 0 Fix: keep the synchronous set as the fast path, then if the assignment was clamped, retry across rAF frames for up to ~500ms (30 frames at 60fps). That gives Virtuoso's passive effect time to re-establish the spacer, after which the next tick succeeds. Cancel any in-flight retry when the effect tears down (Activity hidden again or component unmount). Existing 4 tests in use-tab-scroll-restore.test.tsx still pass — the synchronous fast path covers the simple-content case they exercise. A jsdom regression for the Virtuoso scenario didn't reproduce reliably (the clamp + rAF interplay needs a real browser), so this relies on manual verification: open issue-detail, scroll deep into comments, switch tabs, switch back — scroll position now holds. |
||
|
|
90455abd8d |
fix(desktop): preserve tab scroll position across Activity visibility cycles (MUL-2602) (#3196)
Closes #3183. Tabs render under `<Activity mode="visible|hidden">`, which keeps React state but drops DOM scrollTop when the subtree leaves layout. Switching to another tab and back sent users to the top of long discussions. `useTabScrollRestore` records the scrollTop of every element marked with `data-tab-scroll-root` while the tab is visible (capture-phase scroll listener) and restores them in a useLayoutEffect on the next visible transition, before paint. Saved offsets are dropped when the tab's path changes so intra-tab navigation lands at scroll=0 instead of inheriting the previous route's position. Mark scroll containers in views with `data-tab-scroll-root` (issue detail + chat message list ship with the marker; other views can adopt the convention as needed). `useAutoScroll` previously called `scrollToBottom()` on every effect mount, which would have overwritten the restored offset every time a chat tab cycled back to visible. Guard it with a once-per-instance ref. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6703072241 |
Improve landing header CTA hierarchy (#3197)
* Improve landing header CTA hierarchy * Remove duplicate landing login action |
||
|
|
077bc055f7 |
fix(views): sort timeline entries by created_at on WebSocket append (MUL-2582) (#3139)
* fix: sort timeline entries by created_at on WebSocket append When multiple agents post comments concurrently, WebSocket events may arrive out of chronological order. The handlers blindly appended new entries to the end of the cached timeline array, causing display misordering. This fix sorts the array by created_at (with id as tie-breaker) after each insert. Changes: - use-issue-timeline.ts: sort after comment:created and activity:created - issue-ws-updaters.ts: sort in appendTimelineEntry Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(views): extract sortTimelineEntriesAsc helper, cover mutation onSuccess Review feedback from @Bohan-J: useCreateComment.onSuccess also appends unsorted (mutations.ts:558). When the local user posts a comment whose HTTP response returns after a concurrent WS event, the unsorted append leaves the cache misordered and the subsequent WS dedup skips re-sort. Extract sortTimelineEntriesAsc helper and reuse it in all three web cache writers: - comment:created WS handler - activity:created WS handler - useCreateComment.onSuccess Mobile keeps its own inline sort (apps/mobile/CLAUDE.md boundary). Add regression tests for sort position (mid-insert and oldest-insert). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3df26ddd28 |
feat(self-host): add Helm chart for Kubernetes deployment (#2377)
* Include k8s deployment instructions * Use helm for deployment * docs(self-host): add Helm / Kubernetes deployment to quickstart (en + zh) * fix(helm): gate backend ExternalName alias behind a value The unprefixed Service/backend in the chart is load-bearing, but as written it limits the chart to one release per namespace and fails helm install whenever a Service/backend already exists in the namespace (without --take-ownership). Gate the alias behind frontend.compatibility.backendAlias (default true, so existing installs are unchanged). Operators running a web image with a patched REMOTE_API_URL can set it to false to drop the Service entirely. Document the one-release-per-namespace constraint and the opt-out in values.yaml and the SELF_HOSTING.md Kubernetes section. Addresses review item #1 on PR #2377. * fix(helm): add backend startupProbe so cold installs survive migrations The entrypoint runs `./migrate up` before serving traffic. On a cold cluster (Postgres still coming up) this can take minutes, during which the livenessProbe (initialDelaySeconds 30 / periodSeconds 30) trips and restarts the pod 1-2 times. Add a startupProbe on /healthz (failureThreshold 30, periodSeconds 10, ~5 min budget). Kubernetes disables liveness/readiness until it passes, so migrations finish without the pod being killed, and the aggressive livenessProbe is untouched for steady-state. Update the SELF_HOSTING.md install step, which no longer expects 1-2 restarts. Addresses review item #2 on PR #2377. * fix(helm): roll backend pods on config/secret change via checksum annotations envFrom does not watch the referenced ConfigMap/Secret, and helm upgrade alone does not change the pod template hash, so editing values.yaml + `helm upgrade` left the old backend pods running stale config. Add checksum/config (hash of the rendered configmap.yaml) and checksum/secret (hash of the live existingSecret via lookup, since it is created out-of-band and has no chart template) to the backend pod template. Config edits now actually re-roll the backend on upgrade, and Secret rotations do too. lookup is empty under `helm template`/`--dry-run`; that placeholder is harmless and documented inline. Addresses review item #3 on PR #2377. * docs(self-host): sync quickstart with new startupProbe behavior SELF_HOSTING.md was updated to reflect that the backend now stays Running but not Ready while Postgres comes up (startupProbe absorbs it, so no restart), but the EN/ZH quickstart docs still described the pre-startupProbe behavior of "may restart 1-2 times". Bring them in line. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5f1f08e466 |
feat(web): add use-cases content pipeline with welcome page (MUL-2349) (#2795)
* feat(web): add use-cases content pipeline with welcome page (MUL-2349) Wire fumadocs-mdx into apps/web with an independent collection rooted at content/use-cases/. Add the first page at /use-cases/welcome (header + H1 + prose + screenshot + footer) using the about-page visual shell. - source.config.ts + lib/use-cases-source.ts (separate from apps/docs) - features/landing/components/mdx/screenshot.tsx wraps next/image - public/use-cases/welcome/screenshot-1.png placeholder (55KB) - next.config.ts wraps NextConfig with createMDX() - .gitignore + eslint ignore .source/ Co-authored-by: multica-agent <github@multica.ai> * feat(web): bilingual db-boy use case with cookie locale (MUL-2349) Extends the use-cases pipeline into the first real article. - ZH + EN MDX (auto-data-analysis.{zh,en}.mdx) sharing three real screenshots; sensitive fields on db-boy-profile.png (RDS host, DB name, password) are blurred in-place. - Cookie-based locale: /use-cases/<slug> reads multica-locale server-side via lib/use-cases-i18n.ts (mirrors LandingLayout's cookie + Accept-Language fallback). Same URL serves either language; no [lang] segment so all other landing routes stay unchanged. - Frontmatter schema (source.config.ts): z.looseObject with declared hero_image / updated_at (required) / category (optional); a preprocess converts YAML-auto-parsed Date back to a YYYY-MM-DD string. - MDX components factory createMdxComponents(locale) routes the secondary CTA to /docs/zh (ZH) or /docs (EN); internal MDX links use <Link> for SPA nav; full-width and half-width colons both trigger [CTA: ...] / [占位图: ...] markers; 副 and Secondary both work as the secondary CTA prefix. - Index page localizes hero / subtitle / card CTA / metadata; sort fallback uses an epoch placeholder so undefined-order disappears. - Landing header + footer surface use-cases entry in both locales. - Detail route: sticky header, right-rail TOC with anchor jumps, scroll-mt-[100px] on H2/H3 so anchor jumps don't slip under the sticky header. - Drop welcome demo page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): resolve code review blockers on use-cases PR - Add `use-cases` to reserved_slugs.json + regenerate TS (P1: prevent future workspace slug collision) - Fix dead links in both MDX files: /features/* → /docs/* (P2) - Remove duplicate brand suffix in page title metadata (nit) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(web): align usecases locale routing * chore: refresh web mdx lockfile * fix(web): type mdx next config adapter * fix(web): wrap settings route page --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |