mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-16 19:29:26 +02:00
e15df22e98b50b39f5e52ee7ee5087379b3c4a0a
1131 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e15df22e98 |
feat(autopilots): show creator in autopilot detail properties (MUL-3139) (#3983)
* feat(autopilots): show creator in autopilot detail properties (MUL-3139) The autopilot creator was already persisted end-to-end (created_by_type / created_by_id on the autopilot table, exposed via AutopilotResponse and the frontend Autopilot type) but never rendered. Add a "Created by" field to the detail page Properties section, mirroring the existing assignee field and the issue-detail creator row, reusing ActorAvatar + getActorName. Creator may be a member or an agent (the HTTP create path stamps member today, but backend logic also writes created_by_type=agent), so the display resolves both actor types and does not assume member. List rows are intentionally left unchanged, matching the issue convention (creator lives in detail, not lists). Adds the field_created_by label to all four locale bundles (en/zh-Hans/ja/ko); locale parity test enforces full coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(autopilots): show creator in autopilots list (MUL-3139) Add a Created by column to the autopilots list, mirroring the detail page. Secondary columns (creator, mode, last run) are hidden below lg so small screens keep only name, agent, and status. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b1c8eb5f11 |
feat: support Claude Fable 5 pricing (#3982)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
72179d1145 |
refactor(transcript): reuse payload helper + cover coalesce timestamps (MUL-3174) (#3958)
* refactor(transcript): reuse taskMessageToPayload in WS broadcast The ReportTaskMessages WebSocket broadcast hand-built the payload and duplicated the created_at formatting that taskMessageToPayload already does. Reuse the helper with the just-inserted row, which carries the same redacted values and the DB-assigned timestamp. Co-authored-by: multica-agent <github@multica.ai> * test(transcript): cover coalesce created_at behavior Lock in that coalescing streaming fragments carries the latest created_at, and falls back to the previous timestamp when the merged fragment has none. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9f21d0b634 |
feat(transcript): add timestamps to run transcript entries (MUL-3174) (#3951)
Threads the existing task_message.created_at column through the full stack (Go protocol -> REST/WS handlers -> TS types -> transcript dialog) so agent run transcripts show per-entry timestamps, helping users spot stalled runs. Additive, no migration. |
||
|
|
6d646db577 | fix(issues): soften comment sticky mask (#3956) | ||
|
|
b542c40936 |
fix(issues): keep sticky comment highlight consistent (#3955)
* fix(issues): keep sticky comment highlight consistent * fix(issues): remove reply resolve-thread action |
||
|
|
2bdc8344dd | fix(issues): align sticky comment header padding (#3952) | ||
|
|
c983905d5c |
feat(issues): per-comment thread resolution with sticky collapse (#3910)
* feat(issues): per-comment thread resolution with sticky collapse
Allow resolving any comment, not just roots. Resolving a root folds the
whole thread into one bar (existing); resolving a reply marks it as the
thread's resolution ("Resolve thread with comment") and folds the other
replies behind a "N comments" bar, with the resolution kept visible and
badged. Which comment is the resolution is a pure frontend derivation
(root wins, else latest resolved reply), so no write-side bookkeeping is
needed and any resolved_at combination renders one resolution.
- backend: drop the "only root comments can be resolved" guard
- views: deriveThreadResolution + reply-resolution rendering, sticky
collapse/fold bars (overflow-clip on the card so sticky resolves to the
timeline scroll parent), scroll the folded thread back into view on
collapse, ListChevronsDownUp icon, locales (en/ja/ko/zh-Hans)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): sticky comment headers for long comments
Pin each comment's header (root + replies) to the timeline's scroll
parent while reading, so a long comment keeps its author + actions
visible instead of scrolling out of reach. Exactly one header is pinned
at a time:
- Reply headers stick within their own CommentRow box (release at the
reply's end).
- The root header is wrapped in a root-section container so its sticky
containing block spans only the header + root body — without it the
containing block is the whole thread and the root header stays stuck
behind every reply. Replies render outside the wrapper, gated on open.
- Skip the root header sticky whenever a resolution collapse bar already
owns the top-0 slot (root resolved+expanded, or reply-resolution
expanded) to avoid two bars stacking at the same offset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
90b639888d |
fix(issues): confine inbox deep-link scroll to the timeline container (#3929) (#3942)
Opening an inbox comment notification on an issue with a running agent
shoved the whole desktop page — header included — off the top, and no
amount of scrolling brought it back; only toggling the right sidebar
(which reflows the panel group) restored it.
Root cause: the deep-link landing uses native
scrollIntoView({block:"center"}) on the target comment. Native
scrollIntoView is spec'd to scroll EVERY scrollable ancestor. On a cold
mount where the timeline is still streaming (is-working) and the sidebar
panel starts collapsed, the inner timeline scroller can't center the
target on its own, so the scroll propagates up and scrolls the desktop
shell's overflow:hidden wrapper (desktop-layout.tsx). That wrapper has no
scrollbar and doesn't auto-clamp, so the page stays shoved up until a
resize reflows it. Desktop-only: web sits in a document-scroll context
with a real scrollbar that self-corrects.
Fix: drive the timeline container's scrollTop directly and re-center
across rAF frames until async heights settle, instead of leaning on the
ancestor scroll. The scroll never touches an ancestor, so the header can
no longer be pushed off-screen.
Tests assert the user-facing contract (lands on + highlights the target
comment) rather than the scroll mechanism, which jsdom can't lay out.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
13e9485a3b |
MUL-3130: persist stable /api/attachments/<id>/download URL in comment markdown (#3937)
* MUL-3130: persist a stable attachment download URL in comment markdown Comment image attachments rendered as broken placeholders ~30 minutes after upload because the editor was persisting a short-lived HMAC-signed URL into the comment body. After PR #3903 (MUL-3132) hardened /uploads/* with auth, `attachmentToResponse` started signing `attachment.url` as `/uploads/<key>?exp=<unix>&sig=<HMAC>` for LocalStorage so token-auth clients could keep loading inline images. The signature has a 30-min TTL by design — but `useFileUpload` was returning that signed value as `link` and the editor was writing `` straight into the markdown, so the comment permanently captured a URL that stopped working as soon as the signature expired. The fix is to persist a stable per-attachment URL that the server can re-sign on every request: * `useFileUpload` now returns `link = /api/attachments/<id>/download` (avatar uploads without an id still fall back to `att.url` so the pre-attachment-row code paths keep working). * `DownloadAttachment` self-resolves the workspace from the attachment row instead of reading X-Workspace-Slug / X-Workspace-ID headers, and the route is registered under the auth-only group so a native browser <img>/<video> resource load (which cannot attach those headers) succeeds. Membership is checked inside the handler with a 404 deny shape so the route does not act as an IDOR oracle. * A new `GetAttachmentByIDOnly` SQL query supports the workspace- derivation step. * `AttachmentDownloadProvider` now extracts the attachment id from the stable URL when matching markdown refs to attachment records, with a fallback to the existing url-equality check for legacy comments (and S3/CloudFront markdown that points straight at the CDN). * `contentReferencesAttachment` covers both URL shapes for the composer / standalone-list dedup paths so an attachment uploaded before the fix and one uploaded after both deduplicate cleanly. Tests: - New unit tests for the URL helpers (16 tests, packages/core). - Backend regression test: bare `<img src>`-style request without workspace headers now succeeds for a member (200) and 404s for a non-member, replacing the previous "400 without workspace context" contract. - Existing TestDownload*, TestServeLocalUpload*, TestAttachmentTo Response* and the 1220 frontend views tests all pass. Refs: MUL-3130, GitHub issue #3891 Co-authored-by: multica-agent <github@multica.ai> * MUL-3130: address PR review — split markdown link from upload link, swap render src Two follow-ups from GPT-Boy's review on PR #3937. (1) Don't reroute every upload consumer through the workspace-gated download endpoint. The previous change made `useFileUpload`'s `link` field unconditionally return `/api/attachments/<id>/download` whenever the upload had an id. But `useFileUpload` is also used by avatar / logo pickers (account-tab, workspace-tab, agents/avatar-picker, squads/squad-detail-page) that persist `result.link` directly into `avatar_url`. Avatars are referenced cross-workspace (mention chips, member lists, inbox items), so binding their URL to a workspace-membership-gated endpoint would silently break cross-workspace avatar visibility. The fix splits the URL into two semantically distinct fields: - `link` — same as `att.url` (legacy contract). Avatar / logo callers continue to use this and remain on whatever URL semantics the storage backend dictates. - `markdownLink` — the stable per-attachment URL `/api/attachments/<id>/download`. Only the editor's markdown-persisting flow consumes this. Falls back to `link` for the no-workspace upload branch (where there is no attachment-row id to address). `editor/extensions/file-upload.ts` switches `image.src` and `fileCard.href` to `markdownLink ?? link` so comment markdown gets the stable shape while avatar callers stay on `link` unchanged. (2) Make the render-time img src loadable for token-mode clients. Persisting the stable `/api/attachments/<id>/download` URL fixes the expiry problem but the path itself sits behind `middleware.Auth`, which expects either a `multica_auth` cookie or a Bearer token in `Authorization`. Native `<img>`/`<video>` resource loads from token-mode clients (Electron's default mode, the mobile app, legacy-token web sessions) cannot attach the Authorization header, so the bare URL would 401 immediately rather than 30 minutes later. `Attachment.normalize` now runs the resolved record through a new `pickInlineMediaURL` helper that returns: - `record.download_url` when it's an absolute URL with a recognised CDN signature query (CloudFront-signed `Signature` / `Expires` / `Key-Pair-Id`, or `X-Amz-Signature` for raw S3 presigns) — these load as native resource src in any client. - else `record.url`, which on the LocalStorage backend carries a freshly-minted `/uploads/<key>?exp&sig` query whose signature IS the auth (token-mode-loadable). On non-CF S3 backends this is the raw stored URL — same behaviour as today. - else the original input URL (legacy / unresolved markdown keeps its existing path). This gives the same effect for both `kind: "record"` and `kind: "url"` attachment inputs: once a record is in hand, the rendered media src is whichever URL the current backend exposes a working signature on. Tests: - New `file-upload.test.ts` regression pinning that `markdownLink` is what lands in the markdown body when the upload result returns both a short-lived storage URL and a stable download path. - Updated `attachment.test.tsx` to reflect the new render-time swap (the rendered img src now follows the freshly signed URL, not the raw storage URL) and added a record-mode regression pinning the LocalStorage default — when `download_url` is the bare /api/attachments/<id>/download path, the renderer must fall through to the signed `record.url`. - Updated `chat-input.test.tsx` makeUpload helper for the new `markdownLink` UploadResult field. - 1222 frontend views tests + 507 core tests + typecheck across @multica/{core,ui,views} all pass. Refs: MUL-3130, GitHub issue #3891. Builds on |
||
|
|
072404d912 |
fix(issues): header chip shows 'is queued' when no agent is running (#3923)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0c80c33c62 |
feat(issues): add brand border beam to active agent header chip (#3921)
Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
139cd755e2 |
Revert "MUL-3127: reuse submit button in reply input (#3901)" (#3906)
This reverts commit
|
||
|
|
51a214b4c0 |
MUL-3134: restore header agent popover (#3905)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f6999a9dcb |
MUL-3134: simplify issue agent header chip (#3902)
* MUL-3134: simplify issue agent header chip Co-authored-by: multica-agent <github@multica.ai> * MUL-3134: remove execution log event count Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d434f038c9 |
MUL-3127: reuse submit button in reply input (#3901)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1ddf89a8f2 |
feat(daemon): enable Antigravity (agy) per-agent model selection (MUL-3125) (#3894)
* feat(daemon): wire agy --model and model discovery for Antigravity agy 1.0.6 added a --model flag and an `agy models` catalog command, which were the #1 blocker in the earlier agy-backend review (MUL-3125). The antigravity backend already shipped but deliberately dropped opts.Model because agy 1.0.1 had no way to select a model. - buildAntigravityArgs now passes --model <display name> when opts.Model is set; the value is the exact `agy models` display string (spaces + parens), passed as a single exec arg so no shell quoting is needed. - Block --model in custom_args so it can't override the managed value. - ListModels("antigravity") enumerates via `agy models` (no static fallback: agy silently no-ops on unrecognised models, so a stale guess would turn a typo into a successful empty run). - ModelSelectionSupported now returns true for every built-in provider; the hook stays for any future model-less runtime. - Daemon probe reads MULTICA_ANTIGRAVITY_MODEL for the daemon-wide default. Co-authored-by: multica-agent <github@multica.ai> * docs(providers): mark Antigravity model selection as supported Antigravity gained --model in agy 1.0.6 (MUL-3125). Update the provider matrix + prose (en/zh/ja/ko) from "managed internally / no --model" to dynamic discovery via `agy models`, and refresh the now-stale picker comments. Flag the display-string (not slug) shape and agy's silent no-op on unrecognised values. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): reject unknown Antigravity model at spawn (MUL-3125) agy exits 0 with empty output on an unrecognised --model, so a stale/typo'd value would surface as a 'completed' but empty task. Validate opts.Model against the `agy models` catalog in Execute before spawning: a non-empty model the CLI does not advertise fails fast with an actionable error listing the real choices. opts.Model is the single funnel for agent.model and the MULTICA_ANTIGRAVITY_MODEL default, so this one check covers every source (UI free-text, API, persisted value, env) — addressing Elon's review that a UI-only guard is bypassable. Validation is fail-OPEN: if the catalog can't be discovered we pass the value through and let agy resolve it, so a discovery hiccup never blocks a run. Pure antigravityModelError() is unit-tested (valid / unknown / near-miss / empty-model / empty-catalog); verified live against real agy 1.0.6. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1abd0e33a6 | fix(transcript): close dialog on desktop navigation (#2903) | ||
|
|
a02b3dfb4a |
feat(issues): move agent live signal into the issue-detail header (#3879)
* feat(issues): move agent live signal into the issue-detail header Replace the in-body sticky "agent is working" card (AgentLiveCard) with a compact chip in the issue-detail header, so the live signal sits in one fixed place and never competes with sticky banners in the content column. - New IssueAgentHeaderChip: avatar(s) + live-ticking blue elapsed time; click opens a popover listing every active task. - Popover reuses ExecutionLogSection's ActiveTaskRow (now exported) so the popover and the right panel are literally the same row — no duplication. - PopoverContent gains an optional keepMounted so the row's confirm dialog survives the popover closing on Stop. - Running rows in ExecutionLogSection drop the blue spinner for a live-ticking blue elapsed timer (panel + popover share this). - Source the chip from the workspace agent-task snapshot filtered by issue (same source as board/list indicators, zero extra network); delete the old AgentLiveCard + its test and its heavy per-issue WS machinery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): live event count on the agent chip + execution-log rows Show a live "N events (elapsed)" on running agents, consistent across the header chip, its popover rows, and the right-panel execution log. - Read the shared per-task message cache (taskMessagesOptions, kept live by useRealtimeSync's global task:message handler) instead of a bespoke subscription — one source of truth, deduped across chip / popover / panel / transcript, no extra WS wiring. - Extract <RunningStat> (event count in info-blue + elapsed in muted parens) so all surfaces render the running stat identically. - ExecutionLogSection running rows now show the same "N events (elapsed)"; the transcript opened from them streams live from the shared cache. - Chip: single running shows events (elapsed); multiple shows "N working". - i18n: add agent_live.event_count (4 locales). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dfc159e1aa |
feat: skip agent triggering on /note-prefixed comments (MUL-3115, #3649) (#3885)
* feat(comments): skip agent triggering on /note-prefixed comments A comment whose first token is the reserved /note prefix (case-insensitive) is stored like any other comment but never wakes an agent. The guard sits at the top of triggerTasksForComment, the single chokepoint, so it covers all three trigger paths — assignee, squad leader, and @mentioned agents. Gating only shouldEnqueueOnComment (as originally proposed) would still let "/note @agent ..." through the mention path. Lets members leave human-only tips/notes on agent-assigned issues without burning an agent run. MUL-3115, closes #3649. Co-authored-by: multica-agent <github@multica.ai> * feat(editor): add /note built-in slash command to comment composer Enable the `/` menu in the issue comment and reply composers in a new "command" mode that lists fixed built-in commands instead of the chat skill picker. Currently one command, /note, which marks a comment as a human-only note that won't trigger the assigned agent. Selecting it inserts the plain-text "/note " prefix (not a rich node), so a menu pick and a hand-typed command are byte-identical and the backend detects either with a simple prefix match. The command menu renders nothing on a non-matching `/` (hideOnEmpty) so typing a date like 6/8 isn't noisy. The chat skill picker is unchanged. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> * refactor(editor): match /note by label prefix and localize its description Address PR review feedback: - buildBuiltinCommandItems now matches the command label as a prefix only, dropping the description substring match copied from the skill picker. With one command this keeps the menu predictable (/no surfaces note; /deploy or a description word like /agent shows nothing) and avoids Enter selecting note unexpectedly. - The command description is now a localized UI string: added slash_command.commands.note to all four editor locales (en/ja/ko/zh-Hans) and the menu renders it via the typed translator. The /label itself stays literal since it's the typed token the backend matches. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> * fix(editor): shorten /note command description to avoid truncation The slash menu item is single-line (truncate, w-72), so the longer copy was cut off. Shorten to "won't trigger any agents" across all four locales — also more accurate, since /note skips assignee, squad leader, and @mentioned agents, not just the assigned one. MUL-3115. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7b453ff604 |
fix: show assignee avatar in command search (#3889)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f5db77340f |
feat(web): native notification banners for the web app (MUL-3116) (#3883)
The in-app inbox (sidebar badge, real-time WS updates, settings, inbox page) was already shared and worked on web. The only Desktop-only piece was the native OS banner: handleInboxNew called desktopAPI.showNotification, which is undefined on web, so no banner fired for new inbox items while the app was unfocused. Add the browser equivalent, keeping handleInboxNew as the single decision point (focus + source-workspace mute gating stays shared with desktop): - packages/core/platform/system-notification.ts: browser Notification engine (showWebNotification) + permission helpers + a click-handler registry. Lives in core (the caller does) but injects the click-routing decision so core stays headless. - handleInboxNew: branch desktopAPI (unchanged) → else showWebNotification. - apps/web WebNotificationBridge: registers click routing to the source workspace's inbox (?issue=…), mirroring desktop's DesktopInboxBridge. - Settings → Notifications: web-only opt-in to grant browser permission (hidden on desktop / where the API is unavailable); en/zh-Hans/ja/ko. Permission is an explicit settings opt-in (no auto-prompt on load, per browser best practice). Tests cover the engine and the web path in handleInboxNew. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bcc7cd3688 |
fix(projects): add backdrop-blur to compact list header (#3783)
The sticky header in the Projects compact list was missing backdrop-blur, causing underlying content to bleed through the semi-transparent bg-muted/30 background when scrolling. Matches the DataTable header pattern used in the Agents module. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d9e6d7807b |
fix(runtimes): show each agent's own CLI version in the runtime list (#3850)
The per-agent "CLI" column rendered the shared multica daemon `cli_version` from each runtime's metadata. That value is the version of the multica daemon binary and is identical for every agent registered by one daemon, so Claude / Codex / Gemini / Opencode all displayed the same number (e.g. v0.3.17) even though each tool has its own version (#3838). Each runtime already reports its own underlying CLI tool version in `metadata.version` (e.g. "2.1.5 (Claude Code)", "codex-cli 0.118.0"). The column now shows that. The multica daemon CLI version and its update prompt stay where they belong — the machine meta strip and the detail page's UpdateSection — so the per-row multica update arrow (which compared against the latest multica release) and its now-unused i18n strings are removed. MUL-3097 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f8fb3fdcd1 |
fix: swimlane view filters (MUL-3072) (#3645)
* fix: Swinlane view filters * refactor: Comments |
||
|
|
b0246ef18a |
fix(lark): hide only the Lark (international) connect entry; keep Feishu (#3835)
Mainland Feishu binding works; only the newly-added Lark (international, open.larksuite.com) install path is unreliable — some Lark installs complete on Lark's side but never persist a lark_installation row (no WS, no inbound, no task). Hide just the "Bind to Lark" CTA behind a single LARK_INTL_CONNECT_ENABLED flag and leave the "Bind to Feishu" entry, the settings panel, and all existing-installation management untouched. Flip LARK_INTL_CONNECT_ENABLED back to true to restore the Lark CTA; nothing else changes. Temporary measure while the Lark install-landing bug is investigated. - LarkAgentBindButton: the Lark button is gated by the flag; the Feishu button and the Connected badge / Manage / Disconnect are unchanged. - Tests: the CTA tests assert Feishu shown + Lark hidden; the Feishu click-to-begin (region=feishu) test stays; the Lark click test was removed (no button) and noted for restore; the dialog polling-error tests open via the Feishu CTA. MUL-3083 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
05e38e5d37 |
feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083) (#3832)
* feat(lark): split bind CTA into Feishu and Lark entry points (MUL-3083 follow-up)
The single "Bind to Lark" button began the device flow against
accounts.feishu.cn and relied on a mid-poll tenant_brand="lark" to
auto-switch international users over to accounts.larksuite.com. Lark
users had to scan a QR served from a Feishu domain first, which
surfaced as confusing in real use.
Replace with two explicit CTAs side by side — "Bind to Feishu" and
"Bind to Lark" — and route the device-flow begin straight to the
matching accounts host based on the user's choice. The mid-poll
auto-switch is preserved as a safety net for users who pick the wrong
entry.
Backend
- RegistrationClient.Begin(ctx, namePreset, region): POSTs to
c.cfg.LarkDomain when region=lark, c.cfg.Domain otherwise. Empty /
unknown region falls back to Feishu (matches RegionOrDefault).
- BeginInstallParams.Region threads through to the registration session
and onto runPolling's initial region local. SwitchedDomain still
flips it on tenant_brand=lark.
- POST /api/workspaces/{id}/lark/install/begin accepts ?region=feishu|lark
with empty defaulting to feishu for back-compat.
Frontend
- api.beginLarkInstall(wsId, agentId, region) — region now required
so every call site is forced to pick a cloud explicitly.
- LarkAgentBindButton renders two buttons; dialog state collapsed into
a single dialogRegion useState so an "open but with no region picked"
intermediate state can't exist.
- LarkInstallDialog takes region as a required prop and renders
region-aware copy (title, description, scan hint, link fallback,
success toast).
i18n
- Add bind_button_{feishu,lark}, install_dialog_{title,description}_*,
install_scan_hint_*, install_open_link_fallback_*, and
install_success_toast_* keys across en, zh-Hans, ja, ko. Legacy
single-region keys are kept for now; nothing in the tree references
them anymore but a follow-up cleanup can remove them once the dust
settles.
Tests
- Two new lark.RegistrationClient tests pin region routing in both
directions (region=lark hits LarkDomain; region=feishu hits Domain).
- Two new lark-tab.test.tsx cases pin that clicking each CTA calls
beginLarkInstall with the matching region argument. Existing CTA
tests updated to expect both buttons in place of one.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* fix(lark): bidirectional tenant_brand swap + region-aware badge + link context menu
Addresses Elon's review on PR #3832 plus a separate report that the
"Or tap here to open in Lark" link in the install dialog had no
standard right-click affordances on the desktop app.
Backend (must-fix from review)
The PR's stated 'safety net for users who pick the wrong CTA' only
worked one direction: a Feishu-first begin already swapped to Lark on
tenant_brand=lark, but the new Lark-first begin (added by this same PR)
had no reverse path — a user who picked 'Bind to Lark' but actually
authorized with a Feishu account would carry RegionLark all the way
through finishSuccess and either fail at GetBotInfo or commit a
wrong-region row.
- PollResult now carries SwitchedDomain AND SwitchedRegion in
lockstep, so the caller never has to re-derive region from the
domain string.
- Poll() detects tenant_brand=feishu while polling against a non-Feishu
host symmetrically with the existing tenant_brand=lark check, gated
on the current host so we don't loop on a brand we already match.
- runPolling reads region from res.SwitchedRegion instead of the
hardcoded RegionLark — the SwitchedDomain branch now flips both
feishu→lark and lark→feishu cleanly.
- Tests: updated the existing TestRegistrationClient_Poll_DomainSwitchOnLarkTenant
to assert SwitchedRegion, added TestRegistrationClient_Poll_DomainSwitchOnFeishuTenant
for the reverse, and TestRegistrationClient_Poll_NoSwitchWhenAlreadyOnMatchingHost
(table-driven, both directions) to pin that the gate doesn't loop.
Backend (nit from review)
Handler comment on /lark/install/begin claimed unknown region defaults
to Feishu downstream, but the handler already returns 400 on unknown
values. Updated the comment to match the actual behavior and document
why we 400 rather than silently normalize (so a frontend typo can't
land users on the wrong cloud without telling them).
Frontend (nit from review)
The Agent inspector's Connected badge was hardcoded 'Connected to
Lark' / 'Manage in Lark' (en) and 'Connected to Feishu' / 'Manage in
Feishu' (zh-Hans) — both wrong half the time now that the install
flow can land on either cloud per agent. Made the badge text and
Manage tooltip read from installation.region:
- agent_bot_connected_label_{feishu,lark}
- agent_bot_manage_link_{feishu,lark}
- agent_bot_manage_tooltip_{feishu,lark}
across en / zh-Hans / ja / ko. Legacy single-region keys retained for
safety. Existing badge tests updated: fixtures without 'region' now
expect the Feishu copy; the region: 'lark' test was promoted to also
assert the Lark badge text and link target. 21/21 lark-tab tests pass.
Desktop (separate report)
Right-clicking an <a> in the renderer surfaced only Copy / Cut /
Paste / Select All — no 'Open Link in Browser' or 'Copy Link Address'.
The renderer's <a target="_blank"> click path already routes through
setWindowOpenHandler → openExternalSafely, but discoverability via the
context menu was missing.
context-menu.ts now appends two link-specific items when params.linkURL
is an http(s) URL. Open Link routes through openExternalSafely (reuses
the existing scheme allowlist); Copy Link Address writes to Electron's
clipboard. Labels are localized to the OS preferred language for the
four locales the renderer ships (en / zh-Hans / ja / ko); zh-* variants
all route to zh-Hans, anything else falls back to English. New
context-menu.test.ts pins five cases: link items show for http(s),
not for javascript:/mailto:/etc., not when no link is under the cursor,
zh-CN gets Chinese, fr-FR falls back to English. 198/198 desktop tests
pass.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Jiang Bohan <bhjiang@outlook.com>
|
||
|
|
ef8dabd35d |
feat(lark): split agent integration UI into inspector status + tab actions (#3830)
The agent Lark binding surfaced the same connect/disconnect affordance in two places on one page — the left inspector's INTEGRATIONS section and the right pane's Integrations tab both rendered the full LarkAgentBindButton, so the destructive Disconnect lived in two spots. Split by role: - Inspector (left): a compact, read-only status row (green dot + region chip + "Connected to Lark") that deep-links into the Integrations tab. New LarkAgentBotStatusRow, opted into via LarkAgentBindButton's onShowConnectedDetails prop. - Integrations tab (right): keeps the full badge, now the single home for Manage / Disconnect. The badge itself is reworked to a two-row layout — status (left) + soft `destructive`-variant Disconnect (right) on row 1, "Manage in Lark" demoted to a muted secondary link on row 2. Cross-sibling navigation goes through a one-shot navIntent channel on AgentOverviewPane that routes via requestTabChange, so the unsaved-changes guard still fires when jumping from the inspector. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6a9e07f6d6 |
fix(issues): remove comment composer expand control [MUL-3086] (#3818)
* fix(issues): remove comment composer expand control Co-authored-by: multica-agent <github@multica.ai> * feat(issues): auto-grow composers and highlight reply submit when ready - Drop max-height cap on comment + reply composers so they grow with content - Reply send button turns primary when there is submittable text Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5586b5d46a |
feat(lark): unbind affordance on the agent connected badge (MUL-3090) (#3826)
Surface a Disconnect/Unbind action in LarkAgentBotConnectedBadge so owners and admins can remove a Lark Bot binding directly from the agent inspector — no detour to Settings. The button sits next to the existing 'Manage in Lark' link and is intentionally rendered as a quieter muted-foreground control with a hover-destructive accent so it doesn't compete visually but stays discoverable. Confirmation is mandatory: a small AlertDialog reuses the existing disconnect_confirm_* i18n strings. The action calls api.deleteLarkInstallation, invalidates larkKeys.installations(wsId) on success so the parent re-renders the Bind CTA, and toasts success/failure. Cancel is disabled while the request is in-flight to prevent racing the close. Tests cover button visibility, confirm gating, success path (delete called with correct args, cache invalidated, toast), error path (no invalidate, toast.error), and Cancel-disabled behaviour. Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
25104d1855 |
perf(editor): parse large markdown in chunks to fix O(n²) freeze (#3823)
@tiptap/markdown parses via marked, whose tokenizer is O(n²) in document length. Opening a large markdown doc (issue description, agent instructions, …) froze the UI for tens of seconds: a 533KB plain-text doc took 61.8s to parse while the subsequent ProseMirror setContent was only 40ms. Upgrading marked doesn't help — already on 17.0.5, whose fix only covers `_`/`*` delimiter runs, not general prose. Parse large markdown in chunks instead of in one shot: split on blank lines outside fenced code blocks, parse each chunk independently, then concatenate the resulting docs. This drops marked's cost to O(n²/k) while producing a byte-identical document. Applied transparently at ContentEditor's two parse entry points (mount + WS-driven re-parse), gated at 50KB so normal small docs stay on the single-parse fast path. 533KB: parse 61.8s -> 0.95s (65x), open 100s -> 3.2s (31x). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a9a9e93905 |
fix(core): scope inbox notification mute check to the source workspace (#3821)
Follow-up to #3797. The inbox:new handler keys the notification-preference query on item.workspace_id, but the request itself still resolved its workspace from the active-workspace X-Workspace-Slug header. On a cold cache, a user viewing workspace B who received a workspace-A notification read B's mute setting and cached it under A's key — so A's banners could fire while muted (and vice-versa), polluting A's cache. Add an optional workspaceSlug override to getNotificationPreferences and notificationPreferenceOptions, and pass the resolved source slug from the inbox:new handler. When the source slug can't be resolved, read only an already-warm cache instead of fetching with the wrong workspace. Tests cover the cold-cache source-slug fetch, source mute suppression, and the no-fallback guard. MUL-3062 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6ac8314711 |
feat(lark): support both Feishu and Lark from one deployment (MUL-3083) (#3815)
* feat(lark): serve Feishu and Lark from one deployment, per installation
The Lark integration was locked to a single open-platform host chosen
deployment-wide (MULTICA_LARK_HTTP_BASE_URL / _CALLBACK_BASE_URL,
defaulting to open.feishu.cn), so one deployment could talk to only the
mainland Feishu cloud OR Lark international — never both. Teams on the
other tenant could not use the integration at all.
Make the host per-installation. The device-flow installer already
auto-detects the tenant (Lark emits tenant_brand="lark" mid-poll); we now
persist that as lark_installation.region, carry it on
InstallationCredentials.Region, and resolve the open-platform host per
call (REST + WS bootstrap) from the region. An explicit cfg.BaseURL
(env / httptest) still overrides every region, so existing tests and
staging/proxy setups keep working.
- migration 116: lark_installation.region TEXT NOT NULL DEFAULT 'feishu'
CHECK (region IN ('feishu','lark')) — existing rows are all mainland.
- lark.Region enum + OpenPlatformBaseURL/RegionOrDefault helpers.
- registration: thread the detected region into finishSuccess so the
install-time GetBotInfo hits the right cloud AND the row records it.
- every credential-build site (patcher, replier, WS provider, union_id
backfill) copies region off the installation row.
- region is part of the WS supervisor fingerprint so a re-install that
switches cloud restarts the connection.
- API: surface region on the installation listing DTO.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* feat(lark): surface installation region in settings UI
Read the per-installation region off the listings response: build the
"Manage in Lark" dev-console host from it (open.feishu.cn vs
open.larksuite.com instead of a hardcoded mainland host) and render a
Feishu / Lark badge on each connected bot. The field is optional and
defaults to Feishu when an older server omits it (API-compat). Adds the
region_feishu / region_lark labels to all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): document simultaneous Feishu + Lark support
The cloud each bot belongs to is now auto-detected at install and stored
per installation, so one deployment serves both. Replace the old
"point MULTICA_LARK_HTTP_BASE_URL at larksuite for international tenants"
guidance (now just an optional override) in all four locales.
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* fix(lark): repair legacy Lark-international installs on upgrade
Review follow-up (MUL-3083). Migration 116 backfilled every existing
lark_installation to region='feishu', assuming all historical rows were
mainland. But self-host deployments could already run Lark international
via the deployment-wide MULTICA_LARK_HTTP_BASE_URL override, so those
rows are really Lark — clearing the override after upgrade (which the new
docs invite) would route them to open.feishu.cn and break them.
Add a one-shot startup repair, BackfillRegionFromLegacyOverride, fired
off the hot path like BackfillBotUnionIDs: when the deployment's global
base-URL override targets open.larksuite.com, relabel the still-default
'feishu' rows to 'lark'. Gating on the deployment-wide override is what
makes it safe — every pre-existing install on such a deployment was Lark.
Idempotent; no-op on mainland / fresh deployments. Verified end-to-end
against a scratch DB (flip then 0-row idempotent re-run).
Also document that a Lark/飞书 app_id is globally unique across both
clouds, which is what makes the app_id-keyed token cache and the
UNIQUE(app_id) constraint safe across regions (review nit).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
* docs(lark): fix ops guidance to match auto per-installation region
Review follow-up (MUL-3083). .env.example and docker-compose.selfhost.yml
still told operators that international Lark requires pointing both base
URLs at open.larksuite.com — now wrong, and it would push a fresh
deployment back into a single-cloud override. Rewrite them: the base
URLs are optional deployment-wide overrides; normal dual-cloud operation
keeps them empty. Document the first-boot auto-relabel for deployments
migrating off the old single-cloud override, across the integration docs
(en/zh/ja/ko).
MUL-3083
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
93b93f58b5 |
fix(desktop): route inbox notifications to the item's source workspace (#3797)
Resolves the desktop inbox notification slug from the item's own workspace_id, routes the click through the navigation adapter for a real workspace switch, and invalidates the source workspace's inbox cache. Follow-up: mute-preference fetch should also target the source workspace. Closes #3766 |
||
|
|
2e50df9a6a |
perf(analytics): report $pageview at section granularity, drop web query-string churn (#3813)
capturePageview now section-normalizes the path (strip query/hash, collapse UUID and issue-key resource segments) and dedupes consecutive same-section views, so navigating between issues/agents/etc. no longer fires a billed PostHog event per resource. The web tracker keys on pathname only (not searchParams), removing ~17% pure query-string-churn pageviews and keeping OAuth code/state out of $current_url. MUL-3081 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d6540a1869 |
fix(clipboard): support copy over http:// via execCommand fallback (#3810)
navigator.clipboard is only exposed in a secure context (https or localhost). On self-hosted instances served over plain http:// it is undefined, so every copy / "copy all" / export button silently failed and left the clipboard empty (GitHub #3781). Add a shared copyText(text): Promise<boolean> helper in @multica/ui/lib/clipboard that prefers the async Clipboard API and falls back to a hidden <textarea> + document.execCommand('copy') for non-secure contexts. Migrate all direct navigator.clipboard.writeText call sites (code blocks, agent transcript copy-all, token / webhook / issue-link copy, etc.) to it, gating success side-effects on the returned boolean, and remove the now-redundant copyMarkdown wrapper. Secure-context users keep the native path unchanged. MUL-3068 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0dbe9f0a8f |
Move caret after inserted image uploads (#3796)
* fix editor image upload caret placement Co-authored-by: multica-agent <github@multica.ai> * feat(editor): reserve image box via intrinsic dimensions to kill paste layout shift (#3803) Capture an image's intrinsic width/height on upload and render them as <img width height> so the browser reserves the box before the image decodes. Removes the layout shift that pushed the caret out of view after a pasted-image insert, making the post-insert scrollIntoView correct. - Add width/height node attrs to ImageExtension (render-only; not serialized to markdown, so round-trips stay clean). - Measure dimensions off-thread via createImageBitmap and patch the node after insert. Fire-and-forget so the synchronous-insert contract (instant preview) is preserved; degrades to no-box when the API is unavailable (jsdom). The src swap keeps width/height via attr spread. - Thread width/height through ImageView -> Attachment -> <img>. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4779e24816 |
fix editor image markdown roundtrip (#3790)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b0d479c6e7 | fix: use mentions for chat context (#3755) | ||
|
|
0d38288dbd |
MUL-2926 feat(editor): support markdown checkbox task lists (#3593) (#3657)
* feat(editor): support markdown checkbox task lists (#3593) Render `- [ ]` / `- [x]` as interactive checkboxes in the issue content editor, matching GitHub / Notion. - Register TaskList + a patched TaskItem in the shared extension factory. Both ship their own markdown tokenizer / renderMarkdown, input rules, and a checkbox NodeView; the taskList tokenizer is consulted before marked's built-in list tokenizer, so `- [ ]` becomes a task while a plain `- ` still falls through to the bullet list. - Patch TaskItem's keymap to share PatchedListItem's split -> lift Enter chain (double-Enter on an empty item exits the list); nested: true enables sub-tasks and nested round-trips. - Add a "Task list" entry to the bubble-menu list dropdown (+ i18n for en / zh-Hans / ja / ko). - Style task lists in prose.css for both the editor ([data-type="taskList"]) and the readonly remark-gfm output (.contains-task-list); completed items render muted. Readonly already rendered task lists via remark-gfm; this brings the editable view to parity. Adds markdown round-trip and readonly checked-state tests. MUL-2926 Co-authored-by: multica-agent <github@multica.ai> * fix(editor): keep readonly nested task lists block-laid-out (#3593) The shared `display: flex` rule on task-list items broke nested task lists in the readonly view. remark-gfm renders a task item as `<li><input> text <ul>…</ul></li>` — no body wrapper — so a nested list is a direct sibling of the checkbox and text, and flex pulled it onto the same row. The editor's Tiptap NodeView wraps the body in a `<div>`, so it was unaffected. Split the task-list CSS into separate editor and readonly blocks: the editor keeps the flex row; readonly stays a block list item with an inline checkbox so a nested `<ul>` drops below and indents under its parent. Adds a readonly test that pins the nested DOM shape (nested `<ul>` inside the parent `<li>`), so a future remark-gfm change that wraps the body fails loudly. MUL-2926 Co-authored-by: multica-agent <github@multica.ai> * feat(editor): convert `- [ ] ` typing into a task list (#3593) TaskItem's built-in input rule only converts `[ ] ` / `[x] ` typed at the start of a plain paragraph. When the user types the GitHub-style `- [ ] ` the leading `- ` first turns the line into a bullet, and the built-in rule no longer fires — so `[ ]` stayed as literal text and nothing became a checkbox. Add an input rule on PatchedTaskItem that catches the checkbox token when it is the entire content of a freshly-typed list item (bullet or ordered) and converts just that item into a task item (deleteRange → liftListItem → toggleList). The anchored regex means it only fires on an item whose whole content is `[ ] ` / `[x] `, so sibling items in the same list are left untouched. Adds typing-level tests (real input-rule simulation) covering `[ ] `, `[x] `, `- [ ] `, `- [x] `, the mixed-list split case, and the plain-bullet no-op. MUL-2926 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d98fc85088 |
feat(agents): Integrations tab with Lark Bot bind entry + Lark Bot docs (MUL-2988) (#3751)
* feat(agents): add Integrations tab with Lark Bot bind entry The agent detail page now has an Integrations tab alongside the inspector's Integrations section. It reuses the shared LarkAgentBindButton so the scan-to-bind / already-connected logic stays single-sourced, and adds the not-configured / coming-soon / members-only states the sidebar has no room for. The tab only appears once the deployment has Lark configured. MUL-2988 Co-authored-by: multica-agent <github@multica.ai> * docs: add Lark Bot integration guide Covers binding a Multica agent to a Lark Bot (scan-to-install), using it (DM / @-mention / /issue), management, permissions, and self-host setup. Added in all four locales under the Integrations nav section. MUL-2988 Co-authored-by: multica-agent <github@multica.ai> * fix(agents): show bound Lark state when install_supported is false install_supported governs only whether NEW scan-installs can complete; already-installed bots stay manageable when the transport is unwired (server/internal/handler/lark.go). LarkAgentBindButton checked the install_supported gate before the existing-installation check, so a bound agent on such a deployment showed 'coming soon' / nothing instead of 'Connected + Manage in Lark'. Reorder the guard (existing active install → badge, before the install_supported gate) and mirror it in the new Integrations tab. Adds regression tests for both surfaces. MUL-2988 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
569b43136c |
fix(editor): download attachments without blank web tab (#3752)
* fix(editor): download attachments without blank web tab Co-authored-by: multica-agent <github@multica.ai> * fix(attachments): preserve workspace in web download URLs Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ae27058b0a |
fix(attachments): unified download endpoint with mode + presign + proxy (MUL-2976) (#3747)
Fix attachment download for self-hosted deployments using private S3-compatible buckets without CloudFront. Closes #3721. **Server** - New unified `GET /api/attachments/{id}/download` endpoint that picks CloudFront / S3 presign / server proxy at request time. - `ATTACHMENT_DOWNLOAD_MODE=auto|cloudfront|presign|proxy` and `ATTACHMENT_DOWNLOAD_URL_TTL` env knobs; `auto` routes Docker hostnames / localhost / private IPs through the proxy and public S3 endpoints through presign. - `Storage.PresignGet` capability; S3 implementation generates presigned GET URLs. - `attachmentToResponse` returns the unified relative endpoint instead of leaking raw unsigned S3 URLs when CloudFront is not configured. Proxy path streams via `io.Copy` with `Content-Disposition` / `Content-Length` / `Cache-Control: no-store` / `X-Content-Type-Options: nosniff`. **Clients** - CLI / Desktop / Mobile resolve relative `download_url` values against the configured API base. Desktop covers the Electron native download bridge and the media preview modal; Mobile covers `Linking.openURL`, the markdown image RN loader, and the composer's completed non-image file chip. - Mobile gains a minimal Node-environment vitest lane wired into `mobile-verify.yml`. **Docs** - `.env.example`, `docker-compose.selfhost.yml`, `SELF_HOSTING_ADVANCED.md`, and the `environment-variables` doc set updated with the new env keys and the `ATTACHMENT_DOWNLOAD_MODE=proxy` recommendation for Docker / VPC-internal object stores. **Tests** - `internal/storage`, `internal/cli`, `internal/handler` (download endpoint, mode selection, proxy header, `/content` non-regression), `cmd/server` (trusted proxy parser). - `packages/views/editor/use-download-attachment.test.tsx` and `attachment-preview-modal.test.tsx` exercise relative URL resolution + absolute pass-through. - `apps/mobile/lib/attachment-url.test.ts` covers every helper branch plus the composer non-image chip case. |
||
|
|
49c913a4fa |
fix(sidebar): hide stale pinned items immediately on workspace switch (MUL-2985) (#3564)
* fix(sidebar): hide stale pinned items immediately on workspace switch When the user switches workspaces, previously pinned items from the old workspace were briefly visible until the new workspace data loaded. Reset the pinned list to an empty array on workspace-id change so the stale items disappear instantly, eliminating the flicker. * fix: separate pinnedItems and wsId effects to prevent drag-sort loss When wsId changes, the combined effect would trigger even if pinnedItems hadn't changed yet (still old workspace data), overwriting localPinned and losing any pending drag-sort results. Split into two independent effects: - pinnedItems effect: updates localPinned when data changes (respects isDragging) - wsId effect: updates localPinnedWsId immediately on workspace switch This ensures workspace switches don't interfere with drag operations. |
||
|
|
a1a33f91db |
fix(desktop): surface expired login instead of silently stuck "Starting" daemon (MUL-2973) (#3743)
* fix(desktop): surface expired login instead of silent "Starting" daemon (MUL-2973) When the local daemon's cached PAT is expired/revoked, the daemon 401s during startup and exits before it serves /health. The desktop polled /health forever and kept reporting "starting", so the runtime sat at "Starting…" with no hint that re-login was the fix (GitHub #3512). Detect this in the layer that owns the daemon's credential: when a start fails to reach "running", probe the token against GET /api/me. A 401 (or missing token) surfaces a new "auth_expired" daemon state; a 2xx means the token is fine (non-auth failure) and a network error stays inconclusive — so a network blip is never misclassified as expired login. The desktop then shows a "Sign-in expired · Sign in again" prompt on the runtimes card and a banner in Daemon settings. The action drops the stale cached PAT, re-mints a fresh one from the current session, and restarts the daemon; if minting also 401s (the session token is dead) it falls back to the standard re-login flow. No daemon/CLI behavior change. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): only force re-login on a real 401 during daemon reconnect (MUL-2973) Review feedback: the reconnect helper treated any failure from clearToken / syncToken / restart as "session is dead" and logged the user out. A transient failure (mint 5xx, network blip, config write error, restart hiccup) would wrongly sign them out. Move the failure classification into the main process, where the real HTTP status is available: mintPat now tags its error with the response status, and a new daemon:reauthenticate handler returns a structured ReauthResult — `ok`, `session_invalid` (a genuine 401 → the session token itself is dead), or `transient`. The renderer only calls logout() on `session_invalid`; transient failures keep the user signed in and show a retryable toast. An unexpected IPC error is also treated as transient, never as logout. Add tests locking the classifier (401 → auth, 5xx/network/IO → not auth) and the renderer behavior (transient failure and IPC throw do NOT log out). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
26971e7e45 |
fix(views): left-align picker item rows (#3736)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2840ebb308 |
feat(chat): add explicit context picker (#3735)
* feat(chat): add explicit context picker Co-authored-by: multica-agent <github@multica.ai> * fix(chat): address context picker review Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
aaefa53ea7 |
feat(chat): add searchable agent picker (#3709)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6330286c7e |
fix(lark): use named import for react-qr-code to survive electron-vite interop (#3718)
* fix(lark): use named import for react-qr-code to survive electron-vite interop
Clicking Bind on the agent detail page white-screened the desktop app at
the QR step:
Element type is invalid: expected a string or a class/function but got:
object. Check the render method of `LarkInstallDialog`.
react-qr-code is a CJS package. `import QRCode from "react-qr-code"`
relies on the bundler's __esModule default-interop to unwrap `.default`.
Next.js (web) unwraps it correctly; electron-vite's dep-optimizer handed
back the whole module namespace object `{ default, QRCode, __esModule }`
instead of the component, so React got an object where it expected a
component the moment <QRCode> mounted — desktop-only white screen, web
unaffected.
Switch to the named import `{ QRCode }`, which maps straight to
`exports.QRCode` and doesn't depend on the flaky default-interop path.
Resolves correctly under both bundlers; the package's own .d.ts exports
both the named class and the default, so it typechecks unchanged.
Not a backend / Lark-config issue — purely a frontend CJS interop bug.
* test(lark): expose named QRCode export in react-qr-code mock
Follow-up to the named-import switch in lark-tab. The test stubbed
react-qr-code with only a `default` export; now that the component
imports `{ QRCode }`, the named binding resolved to undefined and the
3 QR-rendering tests failed with "No QRCode export is defined on the
react-qr-code mock". Return the stub as both `QRCode` and `default`,
defined inside the factory (vi.mock is hoisted above top-level vars).
|
||
|
|
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 |