mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
e7aecfc574c90ed67ead1fa00a06e08a3a39e7ea
1253 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d2bc85e01a |
refactor(slack): declutter the Slack connect UI (#4700)
* refactor(slack): declutter the Slack connect UI Trim the Slack bring-your-own-app UI to match the leaner Lark card and stop burying the setup behind prose nobody reads: - Drop the "Required bot scopes: …" block from the connect dialog. - Shorten the Slack integration card description to mirror the Lark card; the token/admin details stay in the setup docs. - Remove the dialog intro paragraph and the per-field token hints; replace the small "Read the setup guide" link with a larger, more prominent step-by-step guide link. Removes the now-unused i18n keys (byo_dialog_intro, byo_bot_token_hint, byo_app_token_hint, byo_scopes_hint) across en/zh-Hans/ja/ko. * docs(slack): drop the users:read warning callout The bot manifest already lists users:read as a required scope (with the bots.info rationale in the scopes table), so the standalone warning callout was redundant. Removed across en/zh/ja/ko. |
||
|
|
63eb6f73ad |
feat(analytics): anonymous self-host onboarding source beacon (MUL-3708) (#4691)
* feat(analytics): anonymous self-host onboarding source beacon (MUL-3708) Production self-host servers now report the anonymous onboarding "how did you hear about us" channel to Multica's public write-only ingest, so the self-host source distribution becomes visible alongside official cloud. Official cloud keeps its existing PostHog capture unchanged; this is a submit-time beacon, not a background telemetry pipeline. - server/internal/sourcebeacon: ShouldSend gate (production + non-local + non-*.multica.ai app host, fail-closed — judged by the app/frontend host, not the backend URL, which official often leaves unset), per-instance salted hashing, deterministic event uuid, fire-and-forget sender. - POST /api/telemetry/self-host-source: public, write-only, per-IP rate-limited, 4 KiB body cap, channel allowlist, strict unknown-field rejection. Lands in PostHog as self_host_source_channel with a deterministic uuid (best-effort dedup), $process_person_profile=false, and deployment=self_host — a distinct event name so it never pollutes the official onboarding funnel. - Hook in PatchOnboarding fires once when the source is first set; never blocks onboarding. Only channel enum(s) + two per-instance hashes leave the box — never user_id/email/name/workspace/org/domain/role/use_case/the source_other free-text/IP. - migration 128: system_settings singleton holding instance_salt. - frontend: self-host-only anonymous-collection notice on the source step, gated by a new /api/config self_host_source_notice flag (en/zh-Hans/ko/ja). - analytics.Event gains an optional top-level uuid; docs/analytics.md, SELF_HOSTING.md and .env.example document exactly what is/isn't sent and how to disable it (ANALYTICS_DISABLED). Also fixes the long-standing team_size→source drift in docs/analytics.md. Verified locally: go build/vet, go test (sourcebeacon, analytics, handler), pnpm typecheck (all packages), locale parity (157), step-source (6) + core config/schema (69) vitest, lint (0 errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): wire self-host source beacon through metrics, guard nil pool (MUL-3708) Addresses Howard CI blockers on #4691 (no product-direction change): - loadInstanceSalt returns "" on nil pool; salt is only loaded when ShouldSendFromEnv() is true, via a bounded (5s) context — restores the "router constructible without a DB" invariant (nil-pool routing tests). - Add multica_self_host_source_channel_total counter (by source) + an IncForEvent case, so every analytics event is paired with a Prometheus counter. NormalizeSourceChannel reuses sourcebeacon allowlist (no 3rd copy). - Beacon handler now builds the event via the analytics.SelfHostSourceChannel helper and ships it through obsmetrics.RecordEvent (no naked Capture); not IsMetricsOnly, so it still reaches PostHog. - Prime the new family in the registry-families test. Verified: go build/vet, go test ./internal/metrics ./internal/sourcebeacon ./internal/handler ./cmd/server (incl. the 3 named blockers + registry + record-event-helper lints) all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c2e8892194 |
fix(chat): refresh message caches on reconnect (MUL-3831) (#4677)
Invalidate per-session chat message caches (messages, messages-page, pending-task, task-messages) on websocket reconnect / WS instance change so a chat that missed chat/task events while disconnected recovers without a full reload, matching the existing per-issue recovery pattern. Co-authored-by: Ryan <1141524679@qq.com> |
||
|
|
5206d7c613 |
feat(slack): link the Slack integration guide from the Connect dialog (MUL-3666) (#4697)
The bring-your-own-app Connect Slack dialog only had a (hidden) video CTA, so users had no in-product pointer to the setup instructions. Add an always-visible "Read the setup guide" link that opens the Slack integration docs page, localized to the viewer's language (https://multica.ai/docs[/<lang>]/slack-bot-integration), following the existing doc-link convention in the app. Adds the byo_docs_link string to en / zh-Hans / ja / ko. The doc page it points to ships in the docs PR (#4693). Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
658e63d9be |
fix: prefer local upload attachment URLs (#4686)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
11a3cf206b |
feat(slack): bring-your-own-app install + per-installation Socket Mode (MUL-3666) (#4566)
* feat(slack): single app-level Socket Mode connection routed by team_id (MUL-3666) Reshape the Slack adapter from the stage-3 per-installation Socket Mode model into the multi-tenant B2 connection model: ONE deployment-level Socket Mode connection (app-level xapp- token, env MULTICA_SLACK_APP_TOKEN) receives the Events API stream for every installed workspace and routes each inbound event to its channel_installation by team_id — the existing GetChannelInstallationByAppID routing, unchanged. - AppConnector: the single shared connection (slack/app_connector.go). No leader election — per the design "one (or a few)" connections are fine: each replica opens one, Slack delivers each event to one of them, and the existing (installation, message_id) two-phase dedup guarantees exactly-once processing. Resolves the per-team bot user id (via the same app_id query) to detect/strip @-mentions, since one connection serves many workspaces. - Inbound translation (Events API -> channel.InboundMessage) extracted to slack/inbound.go as free functions parameterized by the per-team bot identity. - channel.go trimmed to the outbound Send-only sender; per-installation config (config.go) no longer carries an app-level token — installs hold only the per-workspace bot token (xoxb-) for outbound, since xapp- can't be OAuth'd. - engine.Supervisor now skips channel types with no registered Factory, so Slack installs (driven by the app-level connector, not per-installation channels) no longer churn the lease/Build loop. - Wiring: router.go builds the connector when MULTICA_SLACK_APP_TOKEN is set; main.go runs it alongside the Supervisor. Feishu untouched; channel_* schema unchanged. Verified: go build ./..., go vet ./..., gofmt, and go test ./internal/integrations/... all pass. Co-authored-by: multica-agent <github@multica.ai> * feat(slack): OAuth self-serve install backend (MUL-3666) Add the in-product OAuth install flow that creates Slack installations, the keystone the B2 connector consumes. - slack.InstallService: Begin (build authorize URL, seal workspace/agent/ initiator into the OAuth state), Complete (verify state, exchange code via oauth.v2.access, upsert channel_type='slack' install with the bot token encrypted at rest, auto-bind the installer's Slack id so their first message is not dropped), plus List/Get/Revoke. State is stateless: sealed with the deployment secretbox + an embedded expiry, no session store. - HTTP handlers (handler/slack.go): member-visible list, admin-only begin + revoke, and the public OAuth callback (recovers context from the sealed state, redirects the browser back to Settings → Integrations with a result flag). - Routes + wiring: workspace-scoped list/begin/revoke mirror the Lark admin/member split; the callback is a public route like GitHub's. Built from MULTICA_SLACK_CLIENT_ID/SECRET (+ redirect derived from MULTICA_PUBLIC_URL, override MULTICA_SLACK_REDIRECT_URL; scopes via MULTICA_SLACK_SCOPES). - Realtime: slack_installation:created / :revoked events. Verified: go build ./..., go vet, gofmt, and go test ./internal/integrations/slack/... all pass (new install_test.go covers state sign/verify/expiry/tamper, authorize URL, code exchange + encrypted upsert + installer bind, and oauth error paths). Co-authored-by: multica-agent <github@multica.ai> * feat(slack): in-product OAuth install UI for web + desktop (MUL-3666) Add the "Connect Slack" self-serve install UI mirroring the Feishu/Lark integration, completing the in-product install half of B2. Slack's OAuth flow is a redirect (not a device-code QR poll), so the UI is simpler than Lark's. - core: SlackInstallation / List / Begin types; api.listSlackInstallations / beginSlackInstall / deleteSlackInstallation; slackKeys + slackInstallationsOptions query; realtime invalidation on slack_installation:* events. - views: slack-tab.tsx (SlackTab settings panel + per-agent SlackAgentBindButton + connected badge + disconnect confirm). Connect calls beginSlackInstall and hands the authorize URL to openExternal (system browser on desktop, new tab on web); Slack bounces to the backend callback which lands the install, and the realtime event refreshes the list. Wired into the Settings → Integrations tab and the agent-detail Integrations tab alongside Lark. - i18n: en + zh-Hans settings.slack.* strings. Verified: pnpm typecheck (full monorepo, 6/6) and pnpm lint (@multica/core, @multica/views — 0 errors) pass. Co-authored-by: multica-agent <github@multica.ai> * feat(slack): outbound Replier + user-binding redeem flow (MUL-3666) Fill the stage-3 Replier=nil tail so non-installer Slack users can onboard and get status feedback — completing B2 end to end. - slack.OutboundReplier (engine.OutboundReplier): on NeedsBinding it mints a single-use binding token and DMs/replies a "link your account" prompt with the redeem URL (wrapped as <url|label> so formatMrkdwn doesn't mangle the base64url token); on AgentOffline/AgentArchived it posts a status notice; on an /issue-created Ingest it confirms the new issue. Plain chat stays silent (the agent's own reply lands via EventChatDone). Reuses the bot-token Send path and reads the installation row from ResolvedInstallation.Platform — no new transport. - slack.BindingTokenService: Mint + transactional RedeemAndBind over the generic channel_binding_token / channel_user_binding queries (channel_type='slack'), mirroring lark.BindingTokenService. 15-min TTL, SHA256-hashed tokens, the three typed failure modes (invalid/expired, already-assigned, not-member). - HTTP: POST /api/slack/binding/redeem (public, session-authed) maps the failures to 410/409/403. NewSlackResolverSet now takes the replier (nil disables it). - Frontend: /slack/bind redeem page (packages/views/slack + apps/web route) + api.redeemSlackBindingToken + en/zh slack_bind copy. Verified: go build ./..., go vet, gofmt, go test ./internal/integrations/... (new replier_test.go covers all outcome branches + the prompt URL), plus full pnpm typecheck (6/6) and pnpm lint (0 errors). Co-authored-by: multica-agent <github@multica.ai> * fix(slack): address review must-fixes — connector leak, team-keyed install, /issue copy (MUL-3666) Three fixes from Niko's review: 1. AppConnector.connectOnce leaked the Socket Mode goroutine/connection on a handler error: it ran sm.RunContext on the long-lived ctx and returned the error without cancelling it, so a transient DB/router error left the old connection alive (consuming events into an unread channel) while Run opened a second one. Each connection now runs under its own cancellable context and a deferred cancel + join tears it down on every exit path before reconnect. 2. Slack re-install collided with the (channel_type, app_id) unique index: connecting the same Slack team to a different agent failed because the upsert conflict key was (workspace_id, agent_id, channel_type). Add a team-keyed UpsertChannelInstallationByAppID (ON CONFLICT on the (channel_type, app_id) index, updating agent_id) and use it for the Slack OAuth install, so re-connecting a workspace moves the bot to the chosen agent instead of erroring. Feishu's per-agent upsert is unchanged. 3. /issue clarified: it is not a registered Slack slash command (no `commands` scope), so Slack never routes one to us. Issue creation runs through the message path — `@bot /issue <title>` in a channel or `/issue <title>` in a DM — which the engine parser handles. Documented in the connector and the user-facing copy (en + zh). Verified: go build ./..., go vet, gofmt, go test ./internal/integrations/..., make sqlc, plus pnpm typecheck (6/6) and pnpm lint (0 errors). Co-authored-by: multica-agent <github@multica.ai> * fix(slack): make OAuth install transactional — agent-move binding consistency + cross-workspace guard (MUL-3666) Address Elon's review: the team-keyed upsert kept the same installation row and only flipped agent_id, but engine session reuse matches purely on (installation_id, channel_chat_id) and each chat_session is permanently tied to the agent it was created under — so after moving a Slack team from Agent A to Agent B, existing DMs/threads kept routing to Agent A; only brand-new channels/threads reached B. Cross-workspace re-install was worse: the SQL also moved workspace_id while the application-layer user/chat-session bindings stayed behind, inheriting the previous workspace's relations. InstallService.Complete now runs one transaction (lookup → upsert → retire → installer-bind), all application-layer per the no-FK rule: - Look up the existing installation by team_id (config->>'app_id'). - Reject a silent cross-workspace ownership change (ErrTeamOwnedByAnotherWorkspace → callback redirects with slack_error=team_in_other_workspace). The owning workspace must disconnect first. - On an agent change within the same workspace, retire the installation's chat-session bindings (new DeleteChannelChatSessionBindingsByInstallation) so the next message creates a fresh session under the new agent. The chat_session rows are preserved for history; user bindings stay valid (same users/workspace). - Installer auto-bind moves into the tx; an already-bound-elsewhere id is a benign skip, a real DB error aborts the whole install. InstallService now takes a TxStarter; the queries seam gains WithTx (dbInstallQueries adapter) so Complete stays unit-testable with a fake tx. Verified: make sqlc, go build ./..., go vet, gofmt, go test ./internal/integrations/... (new tests: agent-move retire, same-agent no-retire, cross-workspace reject, fresh-install no-retire). Co-authored-by: multica-agent <github@multica.ai> * fix(slack): atomic cross-workspace install guard + green up frontend CI (MUL-3666) Two things: address Elon's review and fix the failing frontend CI job. Review (atomic cross-workspace guard): the previous guard was a SELECT before the upsert, which loses the concurrent-OAuth race — two workspaces can both read no rows, one inserts, the other's ON CONFLICT update then silently re-points the team. Move the guard into the upsert itself: ON CONFLICT ... DO UPDATE ... WHERE channel_installation.workspace_id = EXCLUDED.workspace_id, and map the empty RETURNING (pgx.ErrNoRows) to ErrTeamOwnedByAnotherWorkspace. The pre-SELECT now only feeds the agent-change cleanup. Also corrected the error copy: a team stays bound to its first Multica workspace (revoke is soft, keeping the row + unique index), so migration is an operator action, not "disconnect first". CI (frontend vitest, @multica/views#test): - The agent IntegrationsTab now renders the real SlackAgentBindButton, whose connected badge calls useQueryClient — absent from integrations-tab.test.tsx's react-query mock. Hoisted the owner/admin gate above the per-platform sections (one role notice instead of one per platform), made the agents members_note generic (en/zh/ja/ko), and updated the test (mock @multica/core/slack, stub SlackAgentBindButton, assert both platforms). - Added slack-tab.test.tsx covering the real SlackAgentBindButton / SlackTab. - locale parity: added the slack (settings) + slack_bind (common) blocks to ja and ko so every EN key has a translated counterpart. Verified: make sqlc, go build ./..., go vet, gofmt, go test ./internal/integrations/...; pnpm --filter @multica/views test (1478 pass), pnpm typecheck (6/6), pnpm lint (0 errors). Co-authored-by: multica-agent <github@multica.ai> * fix(slack): surface agent-page Slack entry points when Lark is off (MUL-3666) The agent-detail Integrations tab and the inspector's Integrations section only considered Lark, so a Slack-only deployment (Lark disabled) showed neither the Integrations tab nor a Connect-Slack button — the per-agent entry points were unreachable. - agent-overview-pane: gate the Integrations tab on Lark OR Slack configured (new slackInstallationsOptions query), not Lark alone. - agent-detail-inspector: render SlackAgentBindButton alongside LarkAgentBindButton in the Integrations section. - regression test: the Integrations tab appears when only Slack is configured. Verified: pnpm typecheck (6/6), pnpm --filter @multica/views test (1478+ pass), pnpm lint (0 errors). Co-authored-by: multica-agent <github@multica.ai> * feat(slack): BYO-app install backend — paste xoxb+xapp, per-app install keyed by real app id (MUL-3666) Adds the bring-your-own-app install path so multiple agents can each have their own bot identity in the SAME Slack workspace (hosted B2 caps at one agent/workspace). User pastes their app's bot token (xoxb-) + app-level token (xapp-); we validate the bot token via auth.test, parse the real Slack app id from the xapp- token, encrypt both tokens, and persist a per-app installation keyed by that app id (real 'A…' ids never collide with hosted 'T…' team ids in the existing unique index — no schema change). - config.go: add app_token_encrypted (BYO discriminator + per-app socket token) - install.go: extract shared persistInstall (atomic cross-ws guard + agent-move retire) - byo_install.go: RegisterBYO + auth.test + app-id parse - handler + route: POST /api/workspaces/{id}/slack/install/byo (admin-only) - tests: keying, encryption, invalid tokens, auth.test failure, cross-ws, agent move Follow-ups (separate commits): per-app Socket Mode connector that consumes the stored app token; in-product BYO install dialog (video + paste form). Co-authored-by: multica-agent <github@multica.ai> * refactor(slack): drop OAuth, unify on BYO per-installation model (MUL-3666) Per product decision, Slack drops the hosted-app OAuth path entirely and unifies on bring-your-own-app (BYO): every installation carries its OWN app-level token and gets its OWN Socket Mode connection, so multiple agents can each have a distinct bot identity in one Slack workspace. - Remove OAuth install (Begin/Complete/code-exchange/sealed state/OAuthConfig/ default scopes), the OAuth callback + begin handlers + routes, and the MULTICA_SLACK_CLIENT_ID/SECRET/REDIRECT/APP_TOKEN env wiring. - Replace the single deployment-level AppConnector with a per-installation slackChannel (authenticated with its own xapp- token) registered as a channel Factory, so the engine Supervisor drives one Socket Mode connection per installation (exactly like Feishu). inbound/outbound/resolvers reused as-is. - Route inbound by the event's api_app_id (== the installation's real app id), not team_id. - InstallService slims to at-rest encryption + the shared persistInstall + list/get/revoke; install is the BYO paste path only (byo_install.go). - Tests: drop the OAuth tests; slack + handler + engine all green. Follow-up (frontend): replace the OAuth "Connect Slack" button with the BYO paste dialog (the begin endpoint it calls is now gone). Co-authored-by: multica-agent <github@multica.ai> * fix(slack): verify BYO bot + app tokens are from the same app, and the app token is live (MUL-3666) Niko review: RegisterBYO only parsed the app id from the xapp string and auth.test'd the bot token, so pasting app A's bot token with app B's app token would 'connect' but be broken (inbound on B's socket, outbound with A's identity). Now: resolve the bot's owning app id via bots.info (on the bot_id from auth.test) and require it to equal the xapp's app id; and live- validate the app token via apps.connections.open. Reject (no persist) on mismatch or a dead app token. Co-authored-by: multica-agent <github@multica.ai> * feat(slack): in-product BYO install dialog (paste bot + app tokens) (MUL-3666) The OAuth begin endpoint was removed server-side, so the "Connect Slack" button now opens a dialog where the admin pastes the bot token (xoxb-) and app-level token (xapp-) of the Slack app they created, and submits to the BYO install endpoint. Includes an optional setup-video link (URL constant, left empty until the walkthrough is recorded). - core: drop beginSlackInstall / BeginSlackInstallResponse; add registerSlackBYO + RegisterSlackBYORequest. - views: SlackAgentBindButton opens the BYO dialog; refreshed comments and install_supported docs (now means "configured", no OAuth). - i18n: new slack.byo_* keys + refreshed page_description in en/zh-Hans/ja/ko. - tests: dialog submit path; views vitest (1479), typecheck, lint, locale parity all green. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): Elon review — team_id routing guard, per-agent reconnect, users:read hint (MUL-3666) 1. Inbound routing keys on api_app_id (the APP, not the Slack workspace), so additionally require the event's team_id to match the installation's stored team. A distributed BYO app installed into another Slack workspace emits the same app id and would otherwise mis-route to this Multica installation. Extracted installationServesTeam() + unit test. 2. BYO install is now agent-keyed (UpsertChannelInstallation, conflict on workspace_id+agent_id+channel_type): one bot per agent. Disconnect → reconnect a NEW app for the SAME agent now UPDATES that agent's row in place instead of violating the (workspace, agent, channel) unique. A unique violation on the (channel_type, app_id) routing index → ErrTeamOwnedByAnother- Workspace (the app is already connected to another agent/workspace). No chat-session retire is needed: a row's agent_id never changes. 3. UX: bots.info (the same-app check) needs the users:read scope — the connect dialog now lists the required bot scopes including it, and the error text says so. Backend build/vet/gofmt/test + views vitest + typecheck + locale parity green. Co-authored-by: multica-agent <github@multica.ai> * fix(slack): publish slack_installation:created on BYO connect; refresh stale comments (MUL-3666) Niko final review: RegisterSlackBYO wrote the response but never published EventSlackInstallationCreated, so only the installer's own tab refreshed — other open clients (Settings, Agent Integrations, other tabs) did not see the new bot in realtime, inconsistent with the revoke event and Lark. Now publishes it on success via a small publishSlackInstallationCreated helper, with a unit test (Bus.Publish is synchronous). Also refreshed comments that still described the removed hosted-OAuth / single deployment-level AppConnector model (handler SlackInstall field, channel.go / inbound.go / outbound.go / byo_install.go). PR title updated separately to the BYO per-installation Socket Mode model. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6e2d2c003c |
fix(issues): sync sticky comment header background with highlight fade (MUL-3759) (#4690)
The deep-link highlight tint faded out over 700ms on the comment body layers but the sticky header's background switched instantly, and its 4px bottom `after` gradient band recolored by class-switching that `transition-colors` cannot animate. Both desynced from the body during the fade, showing a white header and a pale seam under it. Add `transition-colors duration-700` to the sticky shell so the header background fades with the body, and make the `after` band derive its color from the header via `bg-[inherit]` + a `mask-image` fade instead of a per-state gradient color, so all three layers are driven by the single header background transition. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ff0979008b |
fix(dashboard): hide deleted agents from usage leaderboard (MUL-3771) (#4637)
* fix(dashboard): hide deleted agents from usage leaderboard (MUL-3771) The usage leaderboard fell back to rendering the raw agent UUID when an agent was no longer in the workspace agent list (`agent?.name ?? row.agentId`). Hard-deleted agents only survive as legacy usage rollup rows, so they showed up as a bare UUID. Filter the leaderboard rows down to agents still present in the workspace. The agent list is fetched with `include_archived: true`, so archived agents keep their names and stay; only hard-deleted agents drop out. Filtering is skipped until the agent list has loaded so a slow fetch doesn't transiently blank the board. Top-line KPI totals are unchanged — only the per-agent list is affected. Co-authored-by: multica-agent <github@multica.ai> * fix(dashboard): stabilize empty agent list Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
37d9fafda6 |
feat(issues): add Remove parent issue action (MUL-3764) (#4630)
* feat(issues): add Remove parent issue action to promote a sub-issue to standalone (MUL-3764) Surfaces a discoverable UI affordance for clearing an issue's parent — the backend and CLI (multica issue update --parent "") already support it, but the Official App only exposed Set parent. Adds: - A 'Remove parent issue' item in the issue actions menu (dropdown + right-click), shown only when the issue has a parent. - A hover unlink button on the parent card in the issue detail sidebar. - A removeParent handler that clears parent_issue_id and stage in one write (stage only orders sub-issues under a parent) with a success toast. Closes #4629 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): toast remove-parent on success only, prune old parent's children cache (MUL-3764) Addresses review feedback on #4630: - use-issue-actions.ts: the remove-parent success toast fired eagerly after mutate(), so a request that failed on permission/network/validation would flash "removed" before the error toast and optimistic rollback. Move it to onSuccess so only a server-confirmed detach is announced. - mutations.ts: when a write re-parents an issue away from its current parent, prune it from the old parent's children cache instead of patching it to parent_issue_id: null in place. The parent's sub-issues list renders that array directly, so the orphaned row used to linger until the settle refetch. onError still restores prevChildren, so the prune rolls back on failure. Adds cache-prune coverage (optimistic remove / rollback / non-reparenting no-op) and onSuccess-vs-onError toast coverage. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
714f9b1ab7 |
fix(editor): keep Tab inside lists instead of escaping focus (MUL-3697) (#4605)
In a list item that cannot indent (first child / max depth), Tab was returned unhandled, so the browser's native Tab moved focus out of the editor onto adjacent controls. Decouple "swallow the key" from "did the indent move anything": best-effort indent, then swallow whenever the caret is inside the list (editor.isActive(name)) and only fall through to focus navigation when not in a list. Covers bullet, ordered and task lists via the shared keymap. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
553419f8ef |
fix(editor): indent multi-item list selection on Tab (MUL-3697) (#4587)
Stock prosemirror-schema-list `sinkListItem` returns false without dispatching whenever `range.startIndex === 0`, so selecting a list from the top and pressing Tab did nothing. Bullet, ordered, and task lists all routed through the same command and were equally affected. Wrap the shared Tab keymap (PatchedListItem + PatchedTaskItem) with `sinkListItemRange`: try stock sink first, and when it bails on a multi-item range whose first item is the list's first child, re-run the stock command on a selection narrowed to start inside the second selected item. The first item stays as an anchor and the rest nest under it (Notion/GitHub nested-list behaviour), in a single undoable transaction. Shift-Tab / liftListItem already handles ranges and the first-item case, so it is unchanged. No schema change. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cd6cd9dcd1 |
fix(editor): keep code-block selection stable during background re-renders (MUL-3621) (#4594)
Selecting text in a readonly code block (comment/issue markdown) lost the
selection within seconds, making copy impossible, whenever the surrounding
view re-rendered — most reliably while a sibling agent task streamed over
WebSocket (a re-render roughly every ~100ms).
Root cause: the `code` renderer emits highlighted HTML via
`dangerouslySetInnerHTML={{ __html }}`, a fresh prop object every render. Each
unrelated parent re-render re-ran react-markdown, and React rewrote the
`<code>` innerHTML even though the HTML string was byte-identical, tearing down
and rebuilding all 161 hljs `<span>` nodes. The native selection is anchored to
those nodes, so it collapsed.
Fix: memoize the entire `<ReactMarkdown>` subtree on its only real inputs
(`processed` + `components`). A stable element reference lets React bail out of
the subtree on unrelated re-renders, so the code-block DOM is never rebuilt
while content is unchanged. Confirmed via an instrumentation probe: zero
`<code>` DOM mutations during streaming after the fix.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9e807efc62 |
feat(sidebar): per-workspace switcher dot + count unread per issue (MUL-3695) (#4591)
* feat(sidebar): mark which workspace has unread in the switcher dropdown (MUL-3695) The aggregate avatar dot only says "some other workspace has unread". When the user opens the workspace switcher they couldn't tell which one. Add a per-row brand dot next to each OTHER workspace that has unread inbox items, in the same right-edge slot as the active-workspace check (the active workspace is excluded — its unread is the Inbox nav count — so dot and check never collide on one row). Reuses the existing cross-workspace summary data; no backend change. New pure helper unreadWorkspaceIds() + unit tests, and AppSidebar dropdown tests covering: dot only on the other unread workspace, no dot at count 0, and never on the active workspace. Co-authored-by: multica-agent <github@multica.ai> * fix(inbox): count switcher unread per issue, matching the inbox dedup (MUL-3695) The unread-summary that drives the workspace-switcher dot counted raw unread inbox_item rows, but the inbox UI deduplicates notifications per issue and treats an issue as read when its NEWEST non-archived item is read. Opening an issue marks only that newest item read (markInboxRead is per-item; only archive cascades to siblings), so older siblings stay unread in the DB. Result: a workspace whose inbox the user sees as empty still lit the dot (reported on bohan-personal showing a dot for Multica AI with no unread). Rewrite CountUnreadInboxByWorkspace to pick the newest non-archived item per (workspace, issue-or-id group) via DISTINCT ON and count only groups whose newest item is unread — the exact semantics of deduplicateInboxItems(...).filter(!read) on the client. No schema/handler change; query-only. Adds TestInboxUnreadSummaryDedupesByIssue covering the read-newest / unread-older case and its inverse. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
54145ad72e |
feat(sidebar): dot the workspace switcher when other workspaces have unread inbox (MUL-3695) (#4577)
Adds a cross-workspace unread summary so the workspace switcher shows the existing brand dot when a workspace OTHER than the active one has unread inbox items. The active workspace's own unread stays on the Inbox nav count to avoid a duplicate signal, and the dot is shared with the pending- invitation indicator. Backend: new GET /api/inbox/unread-summary returns per-workspace unread counts for the user, scoped via a member join so a left workspace can't light the dot. One account-level query instead of N per-workspace inbox fetches. Frontend: schema-guarded api.getInboxUnreadSummary, a single account-level TanStack Query, and a derived "other workspace has unread" boolean in AppSidebar (shared by web + desktop). Inbox WS events (new/read/archived/ batch) and reconnect invalidate the summary, so the dot appears and clears in realtime even for events from a non-active workspace. Closes multica-ai/multica#3773 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f1e6c18e3e | fix(issues): add loading state to edit comment save button (MUL-3709) (#4588) | ||
|
|
0d8df7032c |
fix(core): parse search API responses (#4572)
Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f0d6d88069 |
fix(views): resolve handoff-note version gate locally for direct agent assigns (#4585)
The run-confirm interception box gated its handoff-note field on the preview round-trip's `handoff_supported`, so every open showed a "checking…" wait before the note box could even be used — to learn something the client already holds. For a concrete agent assignee the target runtime is exactly that agent's, and its CLI version is already warm in the prefetched agent + runtime caches, so the box can settle synchronously, the same way the quick-create version gate does. Add a frontend `handoffSupported` mirror of the server's MinHandoffCLIVersion gate, resolve the agent → runtime → cli_version locally, and drive the note box from that verdict without waiting on loading. Squad / batch-status / unresolved-agent paths — whose resolved trigger set is only known server-side — keep falling back to the preview's `handoff_supported`, unchanged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8ff312dbe9 |
feat(editor): accept highlighted composer suggestion on Tab (MUL-3685) (#4570)
* feat(editor): accept highlighted composer suggestion on Tab Plain Tab now accepts the highlighted mention / slash-command suggestion, matching Enter, across every composer built on the shared TipTap editor (chat, issue description, comment/reply). A single shared isPickerAcceptKey predicate centralizes the accept-key policy so the two picker lists stay in sync instead of each re-deciding what counts as accept. Shift+Tab and Ctrl/Cmd/Alt+Tab are intentionally NOT accept keys, so reverse focus navigation and OS window switching are preserved. When no picker is open, Tab keeps its existing behavior (list indent / focus traversal). Adds unit coverage for both picker lists plus a plugin-order guard that fires Tab through real ProseMirror dispatch with the caret inside a list item, proving the suggestion layer outranks PatchedListItem's Tab -> sinkListItem. Scope: web/desktop shared composer only; mobile and the generic combobox are untouched. Refs: MUL-3685 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(editor): make Tab/list-item priority guard actually exercise sinkListItem The guard placed the caret in the first (only) bullet item, where sinkListItem is a no-op (no preceding sibling to nest under), so the test passed regardless of whether the suggestion layer intercepted Tab. Rebuild the fixture as a two-item list with the caret in the SECOND item, where Tab -> sinkListItem can fire, and add a sanity control proving a bare Tab (no picker) does sink that item — so the accept-wins assertion is meaningful. Production code unchanged. Refs: MUL-3685 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e7d28bff1 |
fix(issues): emit project_changed so moved issues leave the old project list (MUL-3669) (#4571)
The per-project issue list rides the filtered myAll cache. Changing an issue's project is a membership change, but the surgical patch (patchIssueInBuckets) is filter-blind and never removes a card that no longer matches the list's project filter — so a moved issue stayed visible in the old project's list until a manual refetch (#4548 / MUL-3669). Root cause: project_id was the only membership-affecting field with no server *_changed flag. The WS handler fell back to diffing project_id against its own cache, which breaks once onMutate has optimistically overwritten the cached value on a local move. - server: stamp project_changed on issue:updated (UpdateIssue + Batch), alongside status_changed / assignee_changed. - events.ts: surface project_changed (optional, additive — old clients ignore). - ws-updaters: prefer the server flag, fall back to the cache diff only when absent (older backend) so a new frontend on an old backend does not regress. - mutations: onSettled invalidates myAll when project_id changed — a local safety net that never depends on the WS echo (update + batch). Tests: WS flag wins over a matching cache (local-move repro), explicit false suppresses the legacy diff, the cache-diff fallback still fires, and both mutations invalidate myAll on a project change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
adddfbdf89 |
fix(issues): reconcile board column counts on off-screen status change (#4557)
Board column headers read byStatus[status].total, which #4415 began maintaining purely client-side via patchIssueInBuckets ±1. That patch adjusts the totals only when it can locate the issue in a loaded page, so a status change on an issue outside the loaded window (very common when an agent flips the status of something the viewer never scrolled to) was a silent no-op. #4415 also removed the list invalidation that used to reconcile counts, so the totals drifted until a full reload. Thread the server's status_changed flag through onIssueUpdated and, when a status-changed patch cannot be applied surgically (no-op because the issue is off-screen), refetch just that one list to reconcile its counts. The loaded/echoed fast path is untouched, so #4415's no-flicker drag behavior is preserved. Closes #4554 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
57d1a0a00f |
fix(quick-create): track concurrent uploads with in-flight counter (MUL-3339) (#4562)
`useFileUpload` exposed a single `uploading: boolean` shared across all
concurrent upload calls. When the user drag-dropped N images into the
Quick Create modal, the first upload's `finally` flipped the flag back
to false while N-1 uploads were still in flight. The submit gate (which
only checked `uploading`) re-enabled, and on submit:
- `getMarkdown()` ran `stripBlobUrls()` and erased every still-pending
`` placeholder from the prompt.
- `pendingAttachments` only contained the first-completed upload, so
`activeAttachmentIds` shipped a single ID.
- The remaining attachment rows never got linked to the new issue —
their `issue_id` stayed NULL and the UI showed "Attachment doesn't
exist".
Fix:
1. Replace the boolean with an in-flight counter in `useFileUpload`.
`uploading = inFlight > 0` so the flag stays true until ALL concurrent
uploads resolve (or reject — the `finally` decrements either way).
The public return shape is unchanged; every existing call site keeps
working.
2. Belt-and-suspenders: add `editorRef.current?.hasActiveUploads()` to
the quick-create submit gate. The editor already tracks per-node
`uploading` attrs (the source of truth for "is there a blob preview
still resolving"); checking it on submit guarantees the strip step
can never run against an in-flight image even if some future code
path mis-clears `uploading`.
3. Regression coverage in `use-file-upload.test.ts`:
- Fire two concurrent uploads, resolve them out of order, assert
`uploading` stays true until BOTH resolve. Confirmed to fail
against the pre-fix code.
- Reject one of the concurrent uploads, assert the counter still
decrements correctly (the `finally` runs on rejection).
The mock ContentEditor in `quick-create-issue.test.tsx` gains a
`hasActiveUploads: () => false` stub so the new defense call doesn't
explode in existing tests.
Verified: `pnpm test` on @multica/core (663 tests) and @multica/views
(1471 tests) both green; typecheck + lint clean on both packages.
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
343ace89a7 |
feat(editor): MUL-3557 add one-click task-list toggle to bubble menu (#4538)
Add a dedicated checkbox/task-list toggle button to the editor bubble menu, between the List dropdown and Quote. It turns the current line(s) into a `- [ ]` task item or back to a paragraph in one tap, reusing the existing toggleTaskList() command and the same Toggle/Tooltip pattern as the neighboring controls. Adds bubble_menu.task_list locale keys for en/zh-Hans/ja/ko. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
20eecfb093 | fix(projects): honor repo resource checkout refs (MUL-3593) (#4470) | ||
|
|
76c58a4ee8 |
MUL-3617: remove Gemini CLI runtime (#4503)
* fix: remove gemini cli runtime Co-authored-by: multica-agent <github@multica.ai> * fix: skip unsupported custom runtime profiles Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4a8210912a |
feat(featureflag): framework-level feature flag system (MUL-3615) (#4496)
* feat(featureflag): framework-level feature flag system (MUL-3615) Introduces a reusable feature flag framework so future features can adopt flags without writing infrastructure code. Backend: server/pkg/featureflag (Go) - Service / Provider / Decision separation per Martin Fowler's Toggle Point / Toggle Router / Toggle Configuration pattern. - Providers: StaticProvider (rules in source control), EnvProvider (FF_<KEY> overrides for ops kill switches), ChainProvider (first-hit-wins composition). - EvalContext carried through context.Context with WithEvalContext / EvalContextFrom; supports user_id, workspace_id, free-form attributes. - PercentRollout via deterministic FNV-1a bucketing; same user always lands in the same bucket so experiments do not flap between requests. - Nil-safe Service: a nil *Service or missing flag returns the caller's default so business code never panics on a missing flag. - 100% unit-test coverage with -race; go vet clean. Frontend: packages/core/feature-flags (TypeScript) - Same vocabulary as the Go side (Decision, EvalContext, Rule, PercentRollout). FNV-1a parity ensures cross-tier bucket agreement. - FeatureFlagService + StaticProvider + ChainProvider in pure TS. - React glue: FeatureFlagsProvider, useFlag(key, default), useVariant(key, default). Hooks fall back to the default when no provider is mounted so Storybook / unit tests stay simple. - Vitest tests for service, providers, hash, and React hooks. Docs: docs/feature-flags.md — wiring, EvalContext, toggle points, backend-protection note, and the standard best-practice checklist. The framework intentionally has no third-party Go deps and no API surface beyond what real callers will need. New providers (DB, remote config, LaunchDarkly) plug in by implementing Provider; no existing caller has to change. Co-authored-by: multica-agent <github@multica.ai> * fix(featureflag): cross-tier hash parity + variant only when enabled (MUL-3615) Two must-fix issues from the PR review on #4496: 1. TS hash had a trailing zero separator that Go did not emit, so the same (key, identifier) bucketed differently on the two tiers. The "user lands in the same bucket on server and client" promise was broken. For example billing_new_invoice/user-42 was bucket 97 in Go and bucket 11 in TS. Fix: TS fnv1a now emits the zero separator BETWEEN parts only, never after the last one, matching Go's hash.Write byte stream exactly. Verified by parallel golden tests on both sides that pin five (key, identifier) -> bucket triples; if either side drifts both tests fail and one must be brought back in sync. 2. StaticProvider returned `Rule.Variant` regardless of whether the rule evaluated to enabled=true. A 0%-rollout user, a deny-listed user, or a default-off user would see variant="experiment-v2", so callers branching on Variant() would route control users into the experiment arm. Fix: Rule.Variant is now the ON-variant only. When the rule evaluates to enabled=false the Decision's variant is the canonical "off", regardless of what Rule.Variant says. Documented as a behavior contract in the Rule godoc / JSDoc and covered by regression tests on both sides. Tests: - go test -race ./pkg/featureflag/... : all green (1.58s). - pnpm --filter @multica/core test : 661/661 (3 new). - pnpm --filter @multica/core typecheck: clean. Co-authored-by: multica-agent <github@multica.ai> * fix(featureflag): hash UTF-8 bytes on the TS side for cross-tier parity (MUL-3615) Follow-up review on PR #4496 caught that the previous hash fix was only correct for ASCII input. The TS side used `charCodeAt`, which returns UTF-16 code units, while the Go side hashes the UTF-8 byte representation. Any non-ASCII flag key or identifier — Chinese flag names, accented user IDs, emoji — would bucket differently on backend vs frontend, silently breaking the "same user, same bucket" promise the PR description makes. Concretely: flag/é Go 53 vs TS-old 68 flag/🦄 Go 82 vs TS-old 75 实验/user-1 Go 90 vs TS-old 4 flag/用户-1 Go 95 vs TS-old 2 Fix: replace per-char charCodeAt with a module-level `TextEncoder` ('utf-8') and hash each encoded byte. After the fix all four cases above match Go exactly, and the existing ASCII cases continue to match. The cross-language golden tables on both sides now include the 5 new non-ASCII cases alongside the 5 ASCII cases, so any future regression that swaps UTF-8 for charCodeAt (or vice versa) will fail loudly on both Go and TS simultaneously. TextEncoder is part of WHATWG Encoding and is available in every evergreen browser, in Node 11+, and in Hermes (React Native) >= 0.74, which covers every runtime that imports @multica/core/feature-flags. Tests: - go test -race ./pkg/featureflag/... : all green. - pnpm --filter @multica/core test : 661/661. - pnpm --filter @multica/core typecheck : clean. Co-authored-by: multica-agent <github@multica.ai> * feat(featureflag): wire into main app config — YAML file + env override (MUL-3615) Follow-up requested by Yushen on PR #4496: make the feature flag framework configurable through the existing main-program config system instead of requiring Go code edits. multica's main app is purely env-var driven (see .env.example) with optional MULTICA_*_FILE knobs for richer config; feature flags now follow the same pattern. server/pkg/featureflag/config.go - LoadRulesFromYAMLFile(path) parses a YAML rule set into runtime Rule structs. Empty files are a valid "no flags yet" state; missing or malformed files surface a hard error so operators see misconfig the same way DATABASE_URL parse errors do. - NewServiceFromEnv composes the standard provider chain: 1. EnvProvider("FF_") (runtime kill-switch path) 2. StaticProvider from YAML file (declarative rule set) When MULTICA_FEATURE_FLAGS_FILE is unset, only the env layer is active and every IsEnabled call falls through to the caller's default, so the server can boot before any flag is authored. server/cmd/server/main.go - Construct the Service once at startup right after env-var warnings, fail loudly on malformed YAML, log the loaded rule count via the Service logger. The Service is held in a local `flags` variable ready to be threaded into handler.Handler / service constructors when the first flag user lands. Threading is deferred to the PR that adds the first business consumer so this PR stays a pure framework + config layer. .env.example - New "Feature flags" section documents MULTICA_FEATURE_FLAGS_FILE and the FF_<KEY> override convention, with a minimal YAML schema example inline. docs/feature-flags.md - Replace the "build a provider manually" example with the NewServiceFromEnv pattern that now matches what main.go actually does. Show the YAML schema in one place. Note the on-variant / off semantics from the previous review round. server/pkg/featureflag/doc.go - Update package doc to mention the gopkg.in/yaml.v3 dependency (already a server-level dep) instead of the now-inaccurate "no third-party dependencies" claim. Tests: - go test -race -count=1 ./pkg/featureflag/... all green; new config_test.go covers: simple YAML, full-shape YAML, empty file, missing file, malformed YAML, no env var, file-only, env-beats-file, bad file surfaces error. - go test -race -count=1 -run TestHealth ./cmd/server/... sanity check that the main.go boot path with the new wiring still passes. - go vet ./... clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a7908e6967 |
fix(issues): sync header agent chip with execution log via shared query (#4498)
The header live chip derived its active-task state from the workspace-wide agent-task-snapshot, while the right-panel Execution log read the per-issue task list. Two queries, two endpoints, two independent refetches: the heavier workspace snapshot lands later than the per-issue list, so the log could show a running task while the header chip had not started yet. Point the chip at the same `issueKeys.tasks(issueId)` cache the Execution log uses (identical query options). Both surfaces now observe one cache entry and update atomically. Drop the now-redundant workspace-id lookup and client-side issue_id filter, since the endpoint is already issue-scoped. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8a0934c741 |
fix(editor): track @mention selection by identity, not slot index (MUL-3607) (#4488)
The @mention popup tracked the highlighted row with a positional integer (selectedIndex into displayItems), but rows are rendered in the re-bucketed order produced by groupItems() (current → recent → search → users → issues). An async server "search" result is appended to the END of displayItems yet hoisted near the TOP on render, so the highlighted row and the committed item pointed at different entries — you navigate to one target but mention its neighbour. A separate useEffect also force-reset selectedIndex to 0 on every displayItems change, snapping an active selection back to the first row whenever async results landed. Root cause: a slot index is not a stable target for a list whose order and length change asynchronously. Track selection by item identity instead: - Replace selectedIndex state with selectedKey (the item's `type:id`). - Derive groups/orderedItems (the exact rendered order) and resolve the numeric index from selectedKey against orderedItems; fall back to row 0 when the pinned item is gone or nothing is picked yet. - Keyboard nav, Enter, clicks, highlight, and scroll all index orderedItems, so the highlighted row always equals the committed item. - Drop the force-reset effect; identity-based selection self-heals across reorders and async arrival without resetting an active pick. Adds a regression test asserting the highlighted row equals the committed item when groupItems reorders the list, both initially and after ArrowDown. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d43840f322 |
Revert "fix(editor): index @mention selection by rendered order (MUL-3607) (#…" (#4489)
This reverts commit
|
||
|
|
1890a9fa19 |
fix(editor): index @mention selection by rendered order (MUL-3607) (#4487)
The @mention popup navigated and committed by indexing the flat `displayItems` array, but rendered rows in the re-bucketed order from `groupItems()` (current → recent → search → users → issues). In chat (context mode) the async server-search results are appended to the end of `displayItems` yet tagged `group:"search"`, so `groupItems()` hoists them near the top. The highlighted row and the committed item then point at different entries — you select one target but mention its neighbour (the reported "@bohan picks the next one" off-by-one). Make the flattened grouped order (`orderedItems`) the single index space for `selectedIndex`, arrow keys, Enter, and clicks, so the highlighted row is always the committed item. Plain issue-comment mentions (default mode) were already safe — no group tags means `groupItems()` is order-preserving — and stay unchanged. Adds a regression test asserting highlighted row == committed item when the list is reordered by a hoisted search result. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a898e5317e |
fix(chat): stop cancelled-run drafts resurrecting after deletion (#4486)
Cancelling a chat task restored the message into the input via a
separate `editorRestore` component state that also held a private copy
of the content and forced an editor remount (bumping `editorKey`).
That copy was never cleared and `activeRestore` re-derived on every
return to the draft's session, so `defaultValue={activeRestore?.content
?? inputDraft}` kept re-injecting text the user had already deleted —
the draft (`inputDraft`) cleared, but the stale copy did not.
The editor already self-syncs external `defaultValue` changes into a
live instance (content-editor.tsx defaultValue-sync effect, used for WS
description updates), so the remount mechanism was redundant. Drop the
whole `editorRestore` state and let `inputDraft` be the single source of
truth: restore just writes the draft, the editor's own sync displays it.
Now cancel-restore behaves exactly like normal typing — delete the text
and it stays gone across navigation.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
294953ba37 |
fix: delete custom runtime profiles from runtime rows (#4456)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2857a4c649 |
fix(transcript): live-update issue/agent transcript dialog from shared cache (#4452)
The transcript dialog opened from a running task's row showed only a one-shot snapshot taken at open time: TranscriptButton fetched once via api.listTaskMessages and cached it locally, never subscribing to the shared ["task-messages", taskId] cache that the WS task:message stream already seeds. New tool calls / thinking / text never appeared until the task finished or the page reloaded. Add a live-cache mode to the shared TranscriptButton: when isLive and the parent provides no items and the task id is a persisted UUID, render from the shared task-messages cache so the open dialog grows in real time. On open (and again on the running→terminal transition) force a backfill via api.listTaskMessages and merge it into the cache by seq — taskMessagesOptions is staleTime:Infinity, so a plain subscription never heals a WS reconnect gap. The cache observer is read-only (enabled:false) so React Query never blind-replaces the cache; only the WS handler and the seq-merged backfill write it. The subscription mounts only while the dialog is open, so closed live rows add no baseline requests; terminal tasks keep the lazy one-shot fetch. Covers issue execution-log and agent activity. Autopilot issue-less run_only live log is out of scope: the backend doesn't broadcast task:message for tasks with no issue/chat session, so there's nothing to subscribe to — backend broadcast unchanged. Extract mergeTaskMessagesBySeq into packages/core/chat/queries.ts and route both the realtime task:message handler and the new backfill through it, so there is one seq-merge semantics for that cache instead of two. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3ce97453b3 |
fix(issues): pre-trigger preview + run-confirm + handoff UX polish (MUL-3375) (#4454)
* fix(issues): stop issue-trigger preview flicker The pre-trigger preview re-rendered/refetched on every workspace task event: WS task lifecycle invalidated issueTriggerPreviewAll (staleTime 0), forcing a background refetch whose isFetching was surfaced as isLoading, collapsing and reopening CreateRunHint's reveal band. The assign source (create / assignee change) cancels existing tasks before enqueuing, so its verdict can't shift from a task event at all; the status source's pending dedup could, but the preview is advisory and the write path re-evaluates authoritatively, so a rare stale label is harmless. Drop the WS invalidation so the preview refetches only on input (signature) change. Keep the comment-trigger invalidation — its verdict genuinely changes mid-compose and its chips drive an immediate, unconfirmed send. Align the hook's data handling with the comment-trigger preview: keepPreviousData so an input switch swaps in place instead of collapsing, and treat only the first load (no prior data) as loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm modal for backlog assign Assigning a Backlog issue to an agent/squad never starts a run (the parking lot — server/internal/service/issue_trigger.go), so the pre-trigger confirm modal only rendered an empty "won't start" box with a single Apply button. Apply directly instead: the single path checks issue.status, the batch path skips only when every selected issue is Backlog (mixed selections still confirm — the non-backlog ones trigger). Mirrors the existing backlog short-circuit in handleBatchStatus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(modals): run-confirm loading state + submit spinner The dialog grew in height after open: it rendered the short "won't start" variant while POST /api/issues/preview-trigger was in flight, then the note box appeared when the predicate landed. Keep the note box mounted (disabled) during loading so assign mode opens at its resolved height, and show a Spinner + 'checking' headline while loading. Submit had no feedback — buttons only disabled, which read as frozen for note assigns (the request starts an agent server-side). Track which footer action is in flight and show a Spinner on the clicked button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): show handoff note in execution-log trigger text An assignment-triggered run that carried a handoff note showed the generic "Initial run" label. Surface the note inline (truncated, like comment triggers show their text) so the row reads as the handoff. taskToResponse now populates handoff_note for all callers (dropping the now-redundant explicit set in ClaimTaskByRuntime); the field is added to the AgentTask type + zod schema (optional, additive — old clients ignore it via the loose schema, new clients fall back to "Initial run"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a5636f0ff4 |
feat: add copy button to readonly code blocks (#4448)
Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
12ea1f6a8c |
MUL-3495: support custom runtime args and registration errors (#4408)
* feat: support custom runtime args Co-authored-by: multica-agent <github@multica.ai> * fix: address custom runtime review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ab335b8a5 |
MUL-3416: Issue pre-trigger preview + Handoff Note (#4383)
* feat(issues): unify run-enqueue decision behind WillEnqueueRun + preview endpoint Collapse the issue update/batch enqueue copies into one service predicate service.IssueService.WillEnqueueRun, shared verbatim with a new dry-run endpoint POST /api/issues/preview-trigger so the four entry points stop drifting (squad/self-loop/batch omissions, MUL-3375). The private-agent gate stays at the HTTP boundary: write paths inject allow-all, preview injects the real gate so it never leaks a private agent's readiness. Add suppress_run to issue update/batch: the change applies but no run starts. Remove the now-dead handler mirrors shouldEnqueueSquadLeaderOnAssign / isSquadLeaderReady. service.Create and the comment trigger chain are untouched. Tests: preview behavior, preview<->write-path match, batch aggregation, member no-trigger, suppress_run skip, malformed-body 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): inject handoff note into assigned runs via first-class task field Add an optional handoff_note carried by issue assign/promote into the run's opening prompt and issue_context.md, via a dedicated agent_task_queue column (migration 122) and a daemon assignment-handoff render branch — never a fabricated comment, never trigger_comment_id (MUL-3375 §6.1). Thread the note through enqueueIssueTask/enqueueMentionTask + WithHandoff public variants and dispatchIssueRun; suppress_run or a parked write drops it (no run = nothing to inject). Soft version gate: MinHandoffCLIVersion + HandoffSupported, surfaced per-trigger as handoff_supported in the preview so the UI can gray the note box on old daemons; the assignment never hard-fails. Tests: daemon prompt + issue_context render via the assignment branch (not quick-create/comment), version helper matrix, note persists on the task, suppressed assign enqueues nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): leave a display-only handoff record on the timeline When an assign/promote with a handoff note starts a run, write one type='handoff' timeline record via TaskService.RecordHandoff — a direct Queries.CreateComment + timeline event that bypasses Handler.CreateComment, so it never reaches triggerTasksForComment and cannot start a second run (MUL-3375 §6.2, the must-not-retrigger invariant). Author is the actor who handed off; body is the note. Migration 123 admits the 'handoff' comment type. Recorded only on a real run start: suppress_run or a parked write writes nothing. enqueueSquadLeaderTask now reports whether it enqueued so the trace is gated on an actual dispatch. Test: exactly one handoff record on assign-with-note, exactly one task (no re-trigger), and no record when suppressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): frontend plumbing for issue-trigger preview + handoff (core) Add api.previewIssueTrigger + IssueTriggerPreviewSchema (zod parseWithFallback), the use-issue-trigger-preview hook, issueKeys.issueTriggerPreview(+All) with WS queue-state invalidation, suppress_run/handoff_note on UpdateIssueRequest, the 'handoff' CommentType, and stripping of the control fields from optimistic update/batch cache patches (MUL-3375 §9). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): exclude handoff records from new-comment counting type='handoff' is a display-only timeline record, not conversation. Exclude it from CountNewCommentsSince so a handoff note never inflates the count of "new comments to catch up on" fed to a claiming agent (MUL-3375 §12). Analytics already excludes it (RecordHandoff is a direct write that emits no analytics event), and the comment-trigger path is already bypassed. Test: a handoff record does not bump the new-comment count; a real comment does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): pre-trigger preview UI, handoff note, timeline card (web/desktop) Wire the §9 frontend onto the preview endpoint + handoff fields: - Delete the backlog blocking dialog (backlog-agent-hint*) and its modal type; the over-eager nag is gone. Backlog awareness is now a passive label. - RunConfirmModal: single assign + batch assign/status route here. Shows the backend predicate's verdict ("将启动 @X" / "将启动 N 个" / parked), an optional handoff note (assign only, soft-gated by handoff_supported), and 暂不启动 — then applies via update/batch. No frontend guessing. - create modal: passive CreateRunHint ("将启动 @X" / backlog parked). - single status change stays a direct apply (unchanged). - timeline: render type='handoff' as a distinct, non-interactive handoff card. - i18n run_confirm + handoff_card across en/ja/ko/zh-Hans; drop backlog action keys; locale parity green. Tests: use-issue-actions (assign → run-confirm modal, member → direct), create-issue + comment-card suites updated/green; views typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): use a valid anchor in the handoff count-exclusion test CountNewCommentsSince filters id <> @anchor_id; SQL id <> NULL is NULL and excludes every row, so an empty anchor made the control assertion read 0. The production caller always passes a real anchor — mirror that with a non-matching sentinel uuid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(issues): RunConfirmModal apply logic (start/suppress/note-gate/batch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(core): preview schema malformed/missing/null fallback coverage Cover IssueTriggerPreviewSchema via parseWithFallback (MUL-3375): well-formed parse, top-level + item default fills (empty/older backend), and fallback to { triggers: [], total_count: 0 } for malformed shapes, a dropped required issue_id, a wrong-typed total_count, and null/non-object bodies — so the four entry points degrade to "nothing will start" instead of throwing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): remove display-only handoff timeline record (留痕) The handoff "留痕" timeline record (type='handoff' comment written on run start) was judged superfluous and dropped per product call. This removes only the display-only trace; the handoff NOTE injection into the run's opening prompt + issue_context.md is untouched. - backend: drop RecordHandoff + its call in dispatchIssueRun - db: drop the `type <> 'handoff'` exclusion in CountNewCommentsSince and migration 123 (comment_type_check reverts to the 4-type set from 001); no production data exists for this unreleased feature - frontend: drop the "handoff" CommentType, HandoffCard, and handoff_card i18n (all locales) - tests: drop handoff_count_test.go and the record-write assertions in issue_trigger_preview_test.go (note-injection tests retained) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(issues): dismissable run-confirm modal + team-handoff copy Two fixes to the pre-trigger confirm modal (MUL-3375). 1. Dismissable: switch RunConfirmModal from AlertDialog to the standard shadcn Dialog so it has the close (X) button + Esc + click-outside. Previously the only choices were "start" / "don't start now" with no way to abort the action entirely; dismissing now cancels with no write. 2. Copy: rework the action-surface wording away from the backend term "run" toward team-handoff voice — 指派 / 开始 / 交接 (run stays only on record surfaces). Unifies the note's three names to "交接说明", and parallels the rewrite across en/ja/ko. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * chore(agent): bump handoff note min CLI version to 0.3.28 The daemon release that renders handoff notes ships in 0.3.28 (0.3.27 was the prior tag), so move the soft-gate threshold up. Below this the note is silently dropped and the frontend grays the note box — assignment is never blocked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm when batch-moving issues to backlog A move into backlog never starts a run (service/issue_trigger.go), so the pre-trigger confirm modal degenerated to an empty "won't start" box with a single Apply button — pure friction. Apply directly instead, matching the single-issue status path. Other target statuses still route through the modal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): refine pre-trigger preview hint and copy - Move the create-issue run hint to a reveal band (grid 0fr→1fr) above the property toolbar. It was sharing the footer button row and, lacking a width constraint, reflowed the submit buttons whenever it appeared. Restyle to a borderless, comment-style avatar+caption that is purely a caption (non-interactive avatar). - Distinguish squad from agent in the pre-trigger copy: a squad's leader evaluates and delegates rather than "starting work" itself. Add will_start_named_squad / will_start_squad / create_will_start_squad across en/zh/ja/ko (reusing the squad_leader_* evaluate→arrange vocabulary) and branch run-confirm + the create hint on squad assignees. - Bold the assignee name in the run-confirm headline via a language-safe sentinel split (no per-language prefix/suffix keys). - Align zh "开始处理" → "开始工作" on the single-assign copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(issues): stub ActorAvatar in create-issue suite CreateRunHint now renders an ActorAvatar for agent/squad assignees, which pulls in getActorInitials/getActorAvatarUrl + the workspace/presence/navigation hook tree. This form-focused suite only stubbed getActorName, so the squad-forwarding test crashed with "getActorInitials is not a function". Stub the avatar inert — its own behavior is covered elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Walt <walt@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4679217586 |
feat(cli): STR-208 오토파일럿 구독자 플래그 추가 (#4438)
* feat(cli): STR-208 오토파일럿 구독자 플래그 추가 * test(core): Issue fixture stage 기본값 추가 * test(views): Issue fixture stage 기본값 추가 |
||
|
|
45dae3185f |
fix(issues): eliminate optimistic-update drag flicker (board, list, batch, WS) (#4415)
* fix(issues): stop kanban card snapping back on drag A cross-column drag on a non-position-sorted board left the card in its origin column for the whole request, then jumped to the target only when the mutation settled — the "snaps back, then moves" glitch. Root cause was three coupled choices in the optimistic path: - board-view never updated local columns on drop for sortBy != "position" (onDragOver is a no-op there), so the card relied on the settle refetch to move across. - useUpdateIssue invalidated the whole list on settle, replacing the column and re-landing the card even on success. - patchIssueInBuckets appended a moved card to the column tail instead of its position slot, so any later cache refresh teleported it to the end. Fixes: - board-view: optimistically move the card into the target column on drop for the non-position path (insertIdByPosition), and reconcile local columns from the cache on settle for both paths (revert on error now that the list is no longer refetched). - mutations: reconcile via onSuccess surgical patch of the returned entity; drop the list/detail invalidation from onSettled (aggregates still flush). - cache-helpers: patchIssueInBuckets inserts the moved/reordered card at its position slot; a plain field update still keeps its slot. Adds cache-helpers and drag-utils unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): patch My-Issues / Project board caches on move too The drag fix made the board reconcile local columns from its feeding cache on settle. The workspace board rides issueKeys.list (patched by onMutate), but the My-Issues and Project boards ride the myList cache, which the mutation did not patch — so a successful move snapped back on those boards. useUpdateIssue now patches/snapshots/rolls back every bucketed list cache (workspace list + myList), selected by the ListIssuesCache `byStatus` shape so grouped (assignee) and flat (gantt) caches are skipped. Adds renderHook regression tests covering both-cache optimistic move, both-cache rollback, and no-list-invalidation-on-settle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): drop redundant WS position->list invalidate onIssueUpdated already surgically patches the non-filtered workspace board via patchIssueInBuckets (cross-status move + same-column reorder). The extra `if (position) invalidateQueries(list)` re-pulled the whole board on top of that, re-introducing drag flicker through the echoed-back WS event. Removed. Filtered myAll lists still invalidate (membership can change there) — the client-side membership reconciliation for those is a separate follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): batch update patches myList + stops list refetch on settle - onMutate now patches both issueKeys.list and the filtered issueKeys.myAll bucketed caches, so a batch edit on a My-Issues / Project board is optimistic too. Previously only the workspace board was patched, so batch edits on those boards relied entirely on the settle refetch. - onSettled no longer invalidates issueKeys.list: the optimistic patch is a complete reconcile for these bucketed boards (batch changes status / priority / project, never a server-computed value), so a full-board refetch only re-introduced the flicker the single-issue path removed. Aggregate / grouped caches still refresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): list view optimistically moves row on non-position drag The sortBy != "position" branch called onMoveIssue without moving the row in local columns, so the row sat in its origin group for the whole request and only jumped across on settle -- the same snap-back the board view had before its fix. Now mirrors board-view: setColumns(insertIdByPosition) on drop so the settle rebuild is a visual no-op. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): keep My-Issues/Project boards in place on non-membership WS change onIssueUpdated now surgically patches the filtered myList (myAll) caches and only invalidates them when the change can actually move an issue in/out of the filter: an assignee change (covers My-Issues direct-assignee + the involves leg + actor panels) or a project change (Project board). A pure status / position / priority / label change reconciles in place -- no refetch -- removing the last drag flicker on filtered boards. Uses the assignee_changed flag the server already sends on issue:updated (surfaced on IssueUpdatedPayload + forwarded by the realtime dispatch); project change is diffed client-side against the cached value. No predicate replication, no backend change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(issues): add settle-lock to swimlane drag (no clobber mid-flight) The swimlane drag had no settle window: the resync useEffect (and the issueMap freeze) guarded only isDraggingRef, so a cache change landing after drop but before the move settled could rebuild localCells out from under the optimistic move. Adds isSettlingRef + settleVersion (mirroring board-view / list-view): the lock is held from drop until onMoveIssue settles, then released, forcing a single resync from the reconciled cache. onMoveIssue now accepts the same optional onSettled callback board/list already use; the parent handleMoveIssue supplies it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): extract shared useDragSettle hook for board + list board-view and list-view carried byte-identical drag/settle scaffolding (the local columns mirror, the dragging/settling locks, the post-move animation-frame throttle, and the settle callback). That duplication is exactly what let list-view silently drift earlier (it had lost the optimistic-move half of the fix, and its position-branch settle callback omitted the settleVersion bump). Extract the primitive into useDragSettle so both surfaces share one implementation and can't drift again. Behavior-preserving for board-view. For list-view the one intended alignment: its position-branch failed move now reverts, gaining the settleVersion bump board-view already had. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3a814bd74a |
fix(issues): refine Stage field icon and dropdown font (#4425)
Replace the # (Hash) icon for the Stage property with the Milestone icon across the picker trigger, dropdown option rows, and the Add-property menu. Shrink the Stage dropdown option font to text-xs (scoped to the Stage picker only; the shared PickerItem keeps text-sm so other property dropdowns are unaffected). |
||
|
|
a123dfc2df |
MUL-3508: stage sub-issues so the parent wakes per stage, not per child (#4410)
* feat(issues): stage sub-issues so the parent wakes per stage, not per child Sub-issues under a parent can be grouped into ordered stages (issue.stage). The child-done -> parent notification + assignee wake now fire only when a stage barrier closes: every sub-issue in the lowest unfinished stage has reached a terminal status (done/cancelled). An unstaged sibling set is one implicit stage, so the parent is woken once when the last sub-issue finishes instead of on every child — the default fix for the fire-on-every-child cascade reported in discussion #4320 / MUL-3508. Stage advancement stays agent-driven: the server only detects the closed barrier and wakes the parent assignee, who decides whether to promote the next stage. - DB: nullable issue.stage (CHECK >= 1) + sqlc regen - API: stage on issue create/update/response and batch update - CLI: `issue create`/`issue update` --stage; new `issue children` command that lists sub-issues grouped by stage (table + json) - stageBarrierClosed / stageProgressSummary in issue_child_done.go, with the wake comment now stage-aware, plus unit tests - skill docs (multica-working-on-issues SKILL.md + source map) Web UI (create-form stage picker, sidebar edit, group-by-stage display) is a follow-up; the API already returns stage for it to consume. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(issues): address review on stage barrier (cancel, batch, unstaged) Resolves the three blockers from the PR review: 1. Cancel can close a stage. The child-done barrier now fires on any non-terminal -> terminal transition (done OR cancelled), not just done. isTerminalChildStatus already treats cancelled as terminal (a cancelled sibling never finishes, so it must not hold a stage open), so a cancelled last-open child now closes its stage and wakes the parent. Keying on the transition also makes a later cancelled -> done edit a no-op, avoiding a lagging duplicate wake. 2. Batch update of stage no longer no-ops. `hasMutation` now includes "stage", so `{"updates":{"stage":N}}` persists instead of returning {"updated": 0}. 3. Unstaged children no longer participate in the staged frontier. In a staged sibling set, NULL-stage children neither hold a stage open nor fire on their own completion, and the wake comment no longer renders "Stage 0". This matches migration 123 ("NULL does not participate in staged grouping") and the CLI's separate unstaged group, removing the footgun where an unstaged backlog child silently blocked Stage 1. Tests: cancellation closes a stage (staged + unstaged), unstaged ignored in a staged set, stage summary skips unstaged, and a stage-only batch update persists. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * feat(web): stage UI — create picker, sidebar edit, group sub-issues by stage Frontend for the sub-issue stage feature (web + desktop, shared via packages): - core: `stage` on the Issue type + create/update request types; zod IssueSchema parses it (defaults to null for older backends) with schema tests for the numeric and omitted cases. - StagePicker component (mirrors the other property pickers): "No stage" + Stage 1..N, offering one beyond the current/sibling max. - Create-issue modal: a Stage pill, shown only when a parent is selected, threaded into the create payload. - Issue detail sidebar: an editable Stage row + "add property" entry, gated to sub-issues (issues with a parent). - Sub-issue list grouped by stage with per-stage headers (flat when unstaged). - i18n: stage keys across en / zh-Hans / ja / ko (parity test passes). Verified: full typecheck (6/6), core (591) + views (1433) vitest suites, lint clean (no new findings). Backend/CLI shipped earlier in this PR. MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * test(issues): add stage to Issue fixtures merged from main The merge brought in new Issue fixtures that predate the required `stage` field: core issues/batch.test.ts, views batch-action-toolbar.test.tsx, and the mobile EMPTY_ISSUE_FALLBACK sentinel. Add `stage: null` so they satisfy the Issue type (mobile reuses core's IssueSchema for parsing, so only the sentinel needs it). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> * fix(web): feed StagePicker the sibling max stage so higher stages stay selectable The StagePicker accepts maxStage to extend its option list beyond the floored Stage 1-3, but neither call site passed it, so a parent with an existing Stage 4/5 child could not pick that stage when creating a new sub-issue or editing one in the sidebar. - Compute the sibling max stage at both call sites: the create modal now loads the parent's children (childIssuesOptions) and the detail sidebar reuses the already-loaded parentChildIssues. - Extract maxSiblingStage + stageOptions as pure helpers on stage-picker and unit-test them (the regression: a Stage 5 sibling keeps Stage 5 selectable and offers Stage 6). MUL-3508 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
da72e2fa22 |
feat(daemon): inject project description into the agent brief (MUL-3465) (#4395)
* feat(daemon): inject project description into the agent brief Issues bound to a project only surfaced the project title in the runtime brief; the project description (durable, project-wide context the owner sets) was loaded but dropped. Carry it end-to-end: - claim handler reads proj.Description onto the response (issue-bound and quick-create paths) - new ProjectDescription field on AgentTaskResponse, daemon Task, and TaskContextForEnv - rendered in the brief's `## Project Context` section and written to .multica/project/resources.json as project_description Empty descriptions render nothing (no extra heading). Updated the projects-and-resources built-in skill docs in the same change. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * feat(projects): clarify project description is injected as agent context The project description is now durable context injected into every task's brief, but the UI still presented it as a plain "Description" field, so existing descriptions could silently become agent input. Add a hint under the description editor on the project detail page and in the create-project modal, in all four locales, stating it is shared with agents as context for every task in the project. No data-semantics change. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> * test(handler): assert project description flows through task claim The execenv tests cover brief rendering, but nothing pinned the claim handler boundary where proj.Description is read onto the response. Add two tests — issue-bound and quick-create paths — so a regression in that assignment fails loudly instead of silently dropping the description. Addresses review feedback on PR #4395. MUL-3465 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
78342a39ce |
MUL-3305: feat(agent): add qoder CLI as a choice of agent provider. (#2461)
* feat(agent): Qoder ACP runtime, chat reconnect recovery, and task linkage - Add Qoder CLI backend (ACP transport, model discovery, blocked-args policy) - Wire daemon/runtime config, docs, and UI provider assets - Retry terminal task reports; add backoff unit tests - Chat: SQL attach user message to task; handler + optimistic cache reconcile - Invalidate chat/task-messages caches on WS reconnect; extract helper + tests Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop non-Qoder changes (chat reconnect, task link, terminal report retries) Keep only Qoder runtime, docs, daemon config/execenv, and UI provider assets. Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * fix(agent): harden Qoder ACP drain and wire project skills path - Stop streaming to msgCh after reader wait so grace timeout cannot race close - Resolve injected skills to .qoder/skills per Qoder CLI discovery - Update AGENTS.md skill copy and add execenv tests Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> * feat(qoder): add provider logo and wire MCP config into ACP sessions - Add inline SVG QoderLogo component to provider-logo.tsx, replacing the generic Monitor icon placeholder - Add convertMcpConfigForACP helper to convert Claude-style MCP server config (object map) into ACP array format for session/new and session/resume - Add unit tests for convertMcpConfigForACP covering stdio, SSE, empty/nil, and multi-server cases Co-authored-by: Orca <help@stably.ai> * fix(test): capture both return values from InjectRuntimeConfig in Qoder test Co-authored-by: Orca <help@stably.ai> * fix(qoder): preserve remote MCP headers and promote provider errors Addresses review feedback on #2461 (Bohan-J): two runtime-correctness issues in the Qoder ACP backend. 1. Remote MCP headers were dropped. The bespoke convertMcpConfigForACP only forwarded url/type, so an authenticated remote MCP server looked configured in Multica but failed inside the Qoder session. Replace it with the shared buildACPMcpServers helper (same path Hermes/Kimi/Kiro use), which preserves headers as [{name, value}], sorts for deterministic output, and handles remote transport aliases. Fail closed on malformed mcp_config instead of silently dropping servers. 2. Provider failures could report as completed tasks. stderr was wired via io.MultiWriter and the result was only promoted to failed when output was empty, so a terminal upstream error (HTTP 429 / expired token) racing a stopReason=end_turn with text still became "completed". Switch to StderrPipe + an explicit copier, drain it (bounded by the existing grace window, since qodercli can leave a child holding the inherited fds) before the decision, and run the shared promoteACPResultOnProviderError. Tests: replace the convertMcpConfigForACP unit tests with two end-to-end Qoder tests — one asserts the Authorization header reaches the session/new payload as {name, value}, the other asserts a terminal stderr error with non-empty output reports failed. Co-authored-by: Orca <help@stably.ai> * fix(qoder): align ACP session handling Co-authored-by: Orca <help@stably.ai> * fix(agent): guard qoder late output after drain Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
149cc9bd0a |
fix(issues): reflect real common value in batch toolbar pickers (#4403)
The batch action toolbar hardcoded status="todo", priority="none", and a null assignee, so the status/priority/assignee pickers always checked a fixed row regardless of the selected issues. The batch write itself worked, but the picker mis-reported the current value, surfacing as "status always defaults to todo" (MUL-3510). The same defect applied to priority and assignee, across all five toolbar mount points. Derive the shared status/priority/assignee of the selected issues via a new commonIssueFields helper and feed it to the pickers; when the selection is mixed, pass an empty value so no row is checked. Pickers now accept a nullable current value, and AssigneePicker gains a `mixed` flag to distinguish an all-unassigned selection (check "No assignee") from a mixed one (check nothing). Each call site passes its issue universe, mirroring the skill list's selected-rows approach. Adds unit tests for commonIssueFields and a toolbar picker-wiring test. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
916cee5c5d |
feat(issues): open agent activity chip on hover (#4405)
The header 'agent is working' chip previously required a click to reveal the activity card. Open it on hover instead so the live signal reads as a glanceable status surface. The hover config lives on Base UI's Popover.Trigger (openOnHover + delay/closeDelay), and the trigger stays a real button so click/keyboard access is retained for touch and a11y. Add a regression test asserting openOnHover is wired on the trigger so a click-only implementation can no longer pass. MUL-3507 Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cf30991f91 |
feat(sidebar): add dismissible Join Discord card (#4400)
Add a Join Discord promo card pinned to the bottom of the left sidebar (above the help launcher). Dismiss state persists per-user in localStorage so it stays hidden once closed. Extract the shared DiscordIcon + invite URL into layout/discord.tsx so the help launcher and the card reuse one source. i18n copy added for en / zh-Hans / ja / ko. MUL-3505 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9d053c57f9 | MUL-3420: fix(runtimes): clarify custom runtime deletion | ||
|
|
8a9f15dbc9 |
feat: add Discord community entry points (#4388)
Add a Discord invite (https://discord.gg/W8gYBn226t) in three places: - Website footer: social icon + link in the Resources group (en/zh/ja/ko) - In-app help menu: Discord item in the help launcher (all 4 locales) - GitHub repo README: badge + link (README.md and README.zh-CN.md) MUL-3492 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9d7060caf1 |
fix(auth): autofocus OTP input on verification step (#4344)
* fix(auth): autofocus OTP input on verification step The email-verification step renders the OTP input without focus, so users must click the field before typing the code. This is friction on every login, especially when switching accounts. Add `autoFocus` to the InputOTP so the cursor lands in the field as soon as the step mounts. Mirrors the existing email-step input and the mobile OTP component, both of which already autofocus. * test(web): polyfill document.elementFromPoint for input-otp in jsdom Autofocusing the OTP input makes input-otp run its focus-time DOM measurement, which calls document.elementFromPoint. jsdom doesn't implement it, so the web login test threw an unhandled error. packages/views/test/setup.ts already stubs this for the same reason; mirror the stub in the web test setup (which already stubs ResizeObserver for input-otp). * test(auth): assert OTP input autofocuses on verification step Guards the autofocus behavior: the test fails if the autoFocus prop is removed from the verification-step InputOTP. Lives in packages/views since it covers shared component behavior, mocking @multica/core. |
||
|
|
31d942d010 |
MUL-3438: fix(projects): require admin for project deletion (#4327)
* fix(projects): require admin for project deletion * test(projects): clean up orphaned member rows in delete-permission helper The schema uses no foreign keys or cascades, so deleting the test user left its member row behind in the shared test workspace, polluting later tests in the package. Delete the member row before the user in both the pre-seed cleanup and t.Cleanup. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c0c41fa0b4 | fix(views): gate right sidebar motion to toggles (#4335) |