mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
fe2c990296bc4dde2cf4d2f4f7f04597ded68f13
1031 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5732b0dae8 |
fix(issues): clear deleted ids from recent issues store (#3420)
cleanupDeletedIssueCaches now also calls a new useRecentIssuesStore.forgetIssue(wsId, issueId) action so the persisted Recent Issues bucket no longer keeps deleted ids around. Both the delete mutation and the WS delete event flow through the same cleanup, so this covers self-delete and cross-client delete. Without this, Cmd+K fires a detail query for every recent id on open and returns a steady stream of 404s for issues the user has deleted (#3413). MUL-2765 Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7722a98a6a | fix(ui): coalesce split task transcript messages (#3097) | ||
|
|
ec5874db96 |
fix(runtimes): consolidate local machine by device name
Fixes #3333 |
||
|
|
bed032f937 |
fix(issues): move local-directory hint out of the comment composer (#3363)
The "Agent will work in-place at …" banner used to render directly above the comment input, which made it read like an input adornment. Move it to the top of the Activity section (below the "Activity" heading, above the live agent card) so it reads as section context instead of composer chrome. Update the component's JSDoc and tweak the margin (mb-2 → mt-3) to match the new placement. MUL-2752 |
||
|
|
171ee842d4 |
fix(editor): preserve raw html-like text on paste (#3355)
* fix(editor): fall back to literal paste when markdown parser drops all content When pasting text like `<T>` or `<MyComponent>`, the CommonMark-compliant markdown parser treats them as inline HTML tags. ProseMirror's schema doesn't recognize unknown HTML elements, so they are silently dropped — producing an empty document from non-empty input. Detect this case (non-empty input → empty parse result) and fall back to literal text insertion so the user sees their text instead of nothing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): escape non-standard HTML tags in paste to prevent content loss When pasting mixed content containing multiple <tag> patterns (e.g. "<t>\n裸 `<tag>` 做转\n<tag>\n<t>"), CommonMark treats bare <word> as inline HTML. ProseMirror silently drops unknown HTML elements, causing partial content loss. The previous empty-result fallback only caught the single-tag case where the entire parse result was empty. Pre-process paste text before markdown parsing: escape <tag> patterns whose tag name is not a standard HTML element, while respecting inline code spans and fenced code blocks. Standard HTML (div, br, img, etc.) passes through normally. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): preserve raw html-like text on paste * fix(editor): prefer rich html paste when semantic * fix(editor): avoid native paste when html drops raw tags --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
85daf7818a |
fix(editor): render code blocks when lowlight highlightAuto returns empty tree (#3358)
lowlight.highlightAuto() returns a Root with zero children (relevance 0) for content it cannot classify into any language. toHtml() of that tree produces an empty string, so dangerouslySetInnerHTML rendered a blank <code> element — the <pre> background was visible but the text was gone. Fall through to plain text render when toHtml produces nothing. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bdb60acae9 |
fix: swimlane empty lanes in due to pagination (MUL-2724) (#3326)
* fix: Swimlane lazy load issues * wip * refactor * fix: Rebase issues * fix: rerender * refactor bactch and chunking |
||
|
|
d16ed2806a |
MUL-2748 docs(autopilots): document webhook event filters + link from UI (#3359)
* MUL-2748 docs(autopilots): document webhook event filters and link from UI Follow-up to PR #3231. The webhook event filters feature shipped without user-facing docs and the UI section gave no hint about how event/action are derived from inbound requests. - Add an "Event filters" subsection to autopilots.mdx and .zh.mdx under the existing "Trigger from a webhook" section: what the filter does (with the `event_filtered` outcome), where the event name and action come from (body envelope / headers / body fallbacks), examples, a non-string-action gotcha, and a curl recipe verifying both the allowed and filtered paths. - Add a small ExternalLink icon next to the "Event filters" label in WebhookEventFilterSection that opens the docs section in a new tab. Locale-aware: zh users land on the Chinese page anchor, en users on the English one. Co-authored-by: multica-agent <github@multica.ai> * docs(autopilots): expand "where event name and action come from" with curl examples Add concrete curl request/inference pairs under each derivation step (body envelope, headers, body fallback, default) and a "common gotcha" explaining why filter `event=trigger, action=true` does not match `{"trigger": true}`. Mirrors the explanation that resolved the on-call confusion about Event filter semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
746c0c4456 |
MUL-2746 fix(avatar): normalize relative avatar urls in desktop/web (#3100)
* fix(avatar): normalize relative avatar urls in desktop/web Co-authored-by: multica-agent <github@multica.ai> * fix: test Co-authored-by: multica-agent <github@multica.ai> * fix(avatar): normalize avatar url in AvatarPicker preview MUL-2746. The picker is used by create-agent and create-squad, and also prefills from a template's `avatar_url` when duplicating an agent. The upload result / template URL is root-relative in local-storage setups, so on Desktop (file:// runtime) the preview <img> resolves against the local filesystem and the avatar fails to render. Route the value through `resolvePublicFileUrl` for rendering only; the stored URL stays raw so the parent's create call still posts what the backend expects. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J (Multica agent) <agents@multica.ai> |
||
|
|
2b5696703f |
MUL-2703: feat(autopilots): webhook event filters per trigger (MUL-2334 follow-up) (#3231)
* feat(autopilots): webhook event filters per trigger (MUL-2334 follow-up) Adds schema-backed event/action filtering to webhook triggers so operators can declare exactly which GitHub (or generic) events should spawn autopilot runs. Events outside the declared scope are recorded as ignored with reason 'event_filtered' — visible in the delivery log but without expensive run/task creation. Closes #3093 (supersedes the description-parsing approach from that PR). Backend: - Migration 108 adds event_filters JSONB to autopilot_trigger - sqlc queries updated for CREATE / UPDATE / LIST / GET - HandleAutopilotWebhook filters against trigger.event_filters before dispatch - Create/Update trigger handlers accept event_filters in the request body - Response shape includes event_filters so the UI can render it Frontend: - New WebhookEventFilterSection component in the autopilot dialog - Inputs for event name + comma-separated actions - i18n strings added (en + zh-Hans) Tests: - Unit tests for splitWebhookEvent and webhookEventAllowedByTriggerScope - Handler-level integration tests for filtered / allowed / no-filter paths co-authored-by: ZephaniaCN <agent/autopilot-webhook-filter> * fix: recognize gitlab/bitbucket/gitea as providers in splitWebhookEvent TestSplitWebhookEvent failed because only 'github' was recognized as a provider prefix. Extract isKnownProvider() to handle gitlab, bitbucket, and gitea as well. * fix(autopilots): address PR #3231 review for webhook event filters Must-fix from PR #3231 review: 1. event_filters now uses typed []WebhookEventFilter at the HTTP boundary instead of []byte. encoding/json was base64-encoding the field on the way out, so the UI could not .map() the response, and a real JSON array on the way in failed to decode. Response field also decodes the stored JSONB into a typed slice before serialising back. 2. UpdateAutopilotTriggerRequest.EventFilters is *[]WebhookEventFilter with tri-state PATCH semantics: nil pointer = leave alone, [] = clear, [...] = replace. The handler marshals an explicit empty slice to the JSONB literal `[]` so COALESCE overwrites instead of preserves. AutopilotDialog now PATCHes the webhook trigger when event_filters change in edit mode (previously the toast said "updated" while the backend was unchanged). 3. webhookEventAllowedByTriggerScope no longer short-circuits to false on the first event-name match whose actions don't line up. Earlier code silently shadowed any later filter that shared the same event name with disjoint actions. Robustness: validateWebhookEventFilters rejects empty event names / actions at write time, and the matcher fails closed on malformed stored bytes instead of widening the allowlist. Tests: handler tests now post real JSON arrays (the prior []byte path masked the contract bug). Adds round-trip / clear-with-[] / preserve- when-omitted / replace / invalid-filter / filters-on-schedule coverage, plus matcher tests for same-event multi-filter and malformed-deny. Migration renamed 108 → 110 to avoid colliding with main's 108_task_token (came in via the merge from main). |
||
|
|
bd1fb10afa |
chore: react-doctor cleanup — button types, useContext→use(), toSorted, error fixes (#3350)
- Add explicit type="button" to 61 <button> elements missing the attribute - Replace useContext() with React 19 use() across 16 context consumers - Replace [...arr].sort() with arr.toSorted() in 12 web/desktop files (mobile excluded — Hermes lacks toSorted support) - Fix rules-of-hooks violation: useSidebar try/catch → useSidebarSafe null check - Fix nested component definition: useMemo wrapping HeaderRight → useCallback - Fix missing ARIA: add aria-expanded + aria-controls to combobox in create-squad React Doctor score: 23 → 30. No behavioral changes, no business logic modified. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c968c13c87 |
feat(auth): support mcn_ Cloud Node PATs verified via Fleet (#3349)
* feat(auth): support mcn_ Cloud Node PATs verified via Fleet
Adds a new token kind, mcn_ (multica cloud node), recognized in both
the regular Auth and DaemonAuth middlewares. mcn_ tokens are minted
and owned by Multica Cloud (not the local personal_access_tokens
table); the server validates them by POSTing to the Fleet's
/api/v1/pat/verify endpoint and uses the returned owner_id as
X-User-ID for downstream handlers.
Cloud is the authoritative owner of token status, so this is a
verifier-only path with no DB fallback:
* Fleet says valid:false -> 401 (token genuinely bad)
* Fleet unreachable / 5xx -> 503 (transient, retry)
* No MULTICA_CLOUD_FLEET_URL configured -> 401 (fail closed)
Verification results are cached in Redis for 60s under
mul:auth:mcn:<sha256> to bound the per-request load on Fleet without
extending the revocation window beyond what the Cloud doc allows.
Negative results are NOT cached, so a freshly minted token doesn't
get locked out by a stale 'token_not_found'.
Reuses MULTICA_CLOUD_FLEET_URL (the same env the cloud-runtime proxy
already uses) so deployments don't need a second config knob.
Tests cover the happy path, every documented invalid reason, 4xx/5xx
mapping, network error, decode error, ctx cancellation, the
fail-closed valid:true-without-owner_id case, trailing-slash URL
normalization, and the Redis cache short-circuit + negative
no-cache contract. Middleware tests pin the four 401/503/200 outcomes
in both Auth and DaemonAuth.
* auth(mcn): require owner_id to map to a real local user; drop X-User-PAT plumbing
Two related changes:
1. Cloud-verified owner_id is now checked against our local users table.
The Cloud owner_id and our users.id share the same UUID space by
contract; a missing local user means either the row was deleted
under an active node or something is forging owner_ids — either
way, fail closed.
CloudPATVerifier.Verify takes a new OwnerLookupFunc:
- returns (true, nil) -> success, cache + return
- returns (false, nil) -> ErrCloudPATInvalid (reason='owner_unknown'),
NOT cached (so a freshly-created user
doesn't get locked out for a TTL window)
- returns (_, error) -> ErrCloudPATUnavailable (transient,
middleware emits 503)
Both Auth and DaemonAuth wire ownerLookupFor(queries), a new shared
helper that wraps queries.GetUser, mapping pgx.ErrNoRows / unparseable
UUIDs to (false, nil) and other errors to a real Go error.
2. Removed all X-User-PAT plumbing. Cloud now mints node-scoped mcn_
PATs itself during /api/v1/nodes (see multica-cloud
docs/api/node-pat.md) and ships them into the EC2 instance via SSM,
so multica-api no longer needs to forward the caller's mul_ PAT.
Propagating a long-lived user PAT into a remote machine widened
the blast radius of any node compromise; that's gone now.
Removed:
- cloud_runtime.go: withUserPAT option, cloudRuntimeUserPAT,
generateCloudRuntimePAT, revokeGeneratedPAT
- cloudruntime/Request.UserPAT field + X-User-PAT header
- X-User-PAT from CORS allowed headers
- obsolete handler tests:
TestCreateCloudRuntimeNodeForwardsValidatedPAT
TestCreateCloudRuntimeNodeRejectsUnownedPAT
TestCreateCloudRuntimeNodeRejectsExpiredPAT
TestCreateCloudRuntimeNodeAutoGeneratesPAT
replaced with TestCreateCloudRuntimeNodeForwardsBody
- X-User-PAT references in packages/core/api/client.test.ts
Tests:
* 3 new verifier-level tests (owner_unknown not cached, lookup error
-> Unavailable, success path is cached for both fleet AND lookup)
* 5 new owner_lookup_test.go tests (nil queries, existing user,
missing user, malformed UUID, DB error)
* 1 new end-to-end DaemonAuth test (cloud says valid, no local user
-> 401)
* Existing X-User-PAT TS assertions removed; full vitest run passes.
* go test ./... and go vet ./... clean on the server module.
|
||
|
|
31b58494cf |
feat(comments): align UpdateComment post-processing with CreateComment (#3337)
* feat(comments): align UpdateComment post-processing with CreateComment (#2965 follow-up) Part 1 — PR #2965 code review follow-ups: - Fix sqlc Column3 naming → AttachmentIds via sqlc.arg(attachment_ids) - Return 500 on ReplaceCommentAttachments failure instead of logging + 200 - Remove optional marker from onEdit attachmentIds (always passed) - Add optimistic update for attachments in useUpdateComment - Extract useEditAttachmentState hook from CommentRow/CommentCardImpl - Add integration tests for attachment replacement scenarios Part 2 — Edit-comment logic alignment: - Add ExpandIssueIdentifiers to UpdateComment (bare identifiers now expand) - Add handleEditMentionDiff: diff old vs new agent/squad mentions on edit, cancel tasks for removed mentions, enqueue tasks for added mentions, cancel + re-trigger when content changes but mentions are unchanged Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(sqlc): regenerate with v1.31.1 + add mention diff integration tests Fixes sqlc version downgrade (v1.31.1 → v1.30.0) that was introduced when the original PR was authored with a local v1.30.0 binary. Regenerated all sqlc output with v1.31.1 to match main. Adds integration tests for handleEditMentionDiff covering: edit adds mention → task enqueued, edit removes mention → task cancelled, edit changes content with same mentions → cancel + re-trigger. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): simplify edit post-processing to cancel-all + re-trigger Replace handleEditMentionDiff (120-line mention diff) with a simpler model: when content changes, cancel all tasks triggered by this comment, then re-run the same three trigger paths as CreateComment (assignee, squad leader, mentions). Fixes gap where assignee/squad-leader tasks were not cancelled or re-triggered on edit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(comments): extract triggerTasksForComment to unify Create/Edit trigger paths Create and Edit duplicated the same three trigger paths (assignee, squad leader, mentioned agents). A fourth path would need changes in two places. Extract into a shared function so the composition is: Create: trigger() + unresolve() Edit: cancel() + trigger() Delete: cancel() Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
17714c3ad1 |
fix(create-issue): preserve parent_issue_id through Create with agent flow (MUL-2534) (#3083)
* fix(create-issue): preserve parent_issue_id through Create with agent flow (MUL-2534) When the create-issue modal was opened from the "Add sub issue" entry on an existing issue and the user switched to "Create with agent", the parent_issue_id was silently dropped: switchToAgent only forwarded prompt + actor + project_id, the AgentCreatePanel had no notion of parent context, and the daemon prompt never instructed the agent to pass --parent <uuid>. The sub-issue intent was lost and the new issue landed as a standalone. This fix threads parent_issue_id through the whole pipeline silently — no new editable form field, the existing carry channel handles it: - Frontend: ManualCreatePanel.switchToAgent + AgentCreatePanel.switchToManual now carry parent_issue_id (and identifier, for display) so the sub-issue intent survives mode flips in either direction. AgentCreatePanel reads parent from `data`, forwards to api.quickCreateIssue, and renders a read-only "Sub-issue of MUL-XX" chip so the user can see the relationship. - API: quickCreateIssue accepts optional parent_issue_id. - Backend: QuickCreateIssueRequest validates parent_issue_id belongs to the same workspace (same path as CreateIssue), persists it in QuickCreateContext, and the daemon claim handler resolves the parent's identifier for prompt context. - Daemon prompt: when ParentIssueID is set, buildQuickCreatePrompt instructs the agent to pass `--parent <uuid>` and treat the modal entry point as authoritative. Tests cover all three hops: switchToAgent carry payload, AgentCreatePanel → api.quickCreateIssue, and the daemon prompt's --parent injection (with both identifier-present and UUID-only fallback branches). Co-authored-by: multica-agent <github@multica.ai> * test(create-issue): cover quick-create parent trust boundary + identifier fallback (MUL-2534) Address review on PR #3083: - Add server-side test for POST /api/issues/quick-create parent_issue_id: same-workspace parent threads through QuickCreateContext.ParentIssueID, foreign-workspace and bogus UUIDs return 400 and never enqueue a task. - Fall back to `data.parent_issue_identifier` in ManualCreatePanel's switchToAgent when the parent detail query hasn't hydrated yet, so the agent chip never renders "Sub-issue of " with an empty tail. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
329fc0139d | fix(create-project): anchor Source popover to top so it doesn't get pushed below the trigger when the modal is expanded | ||
|
|
341ce7bfa5 |
feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)
Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.
Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.
Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.
Co-authored-by: multica-agent <github@multica.ai>
* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)
Addresses the Elon review on PR #3263:
- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
matching handler, CLI `project resource update`, and a new
EventProjectResourceUpdated WS event. resource_type stays immutable;
ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
embedded label differs — the row-level UNIQUE only matches the full
ref JSON, so a label typo would otherwise let the same working
directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
invalid path) and the label-shadow conflict on both create and
update; the in-place rename still succeeds because the conflict
scan ignores the row being edited.
Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.
Co-authored-by: multica-agent <github@multica.ai>
* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)
Two follow-ups from MUL-2662 review round 2:
- CreateProject inline resources path now dedupes local_directory entries on
(daemon_id, local_path) before opening the transaction. The DB-level
UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
full JSON match, so two rows with the same target but different `label`
would otherwise slip past. Standalone POST/PUT already cover this via
findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
row before applying per-type shortcut flags, so `--default-branch-hint x`
on its own no longer constructs a payload missing `url` (which the server
400s on). Local_directory partial edits get the same merge behavior.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)
* feat(desktop): local_directory project_resource UI (MUL-2665)
First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.
What's new for the renderer:
- ProjectResourcesSection grows a desktop-only "Add local directory"
button next to the existing GitHub-repo popover. Clicking it opens
Electron's native folder picker, validates the path through a new
IPC pair (existence + r/w), and submits a project_resource of
resource_type=local_directory with daemon_id pulled live from
daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
greys out when ref.daemon_id != this machine's daemon_id (with a
"only available on the machine that registered this directory"
tooltip). Delete stays enabled so users can drop stale registrations
from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
shows "Agent will work in-place at {label} ({path})" when the issue's
project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
that the daemon will publish when it dequeues a task but can't
acquire the path lock. The render is in place now so the daemon
sibling subtask can wire the status string without an additional UI
PR.
Plumbing:
- @multica/core/types gains LocalDirectoryResourceRef +
UpdateProjectResourceRequest, and the api client gets the matching
PUT method backed by the server endpoint that landed in
|
||
|
|
2af2f7b3e4 |
fix(comments): defer input clearing until submit succeeds (#3344)
clearContent() and setIsEmpty() were called before await onSubmit(), causing permanent content and draft loss on network failure. Move both to the success path, consistent with attachment and draft cleanup. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b343e13ec4 |
fix(settings): remove orphan repo count from GitHub tab shortcut card (#3342)
The bare "N" under the Repositories shortcut had no label and was
adjacent to a sentence ("Repository URLs live in the Repositories
tab") that has no semantic link to a number, so users read it as a
typo. The card is a navigation shortcut, not a status panel — the
actual count is visible after clicking through.
MUL-2725
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
6261e2b6b2 |
fix(comments): clear editor immediately on submit to eliminate WS race glitch (#3331)
* fix(comments): clear editor immediately on submit to eliminate WS race visual glitch The comment editor stayed populated while WebSocket delivered the new comment faster than the HTTP response, causing a "duplicate comment" flash. Move clearContent/setIsEmpty before the await so the editor clears at click time. Also remove dead `submitting` state in useIssueTimeline (redundant with the input components' own guards) and dead `isTemp` logic in comment-card (no code path ever creates temp- prefixed entries). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(comments): preserve attachments on submit failure and fix CommentRow indentation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
963ed5cd0e | feat(comments): allow selecting multiple attachments | ||
|
|
7d24a8594a | fix(comments): support edit-time attachment removal (#2965) | ||
|
|
730fb61f4a |
fix(views): keep sort label centered in viewport during board scroll (#3325)
The "Board ordered by" overlay used absolute positioning inside a scrollable container, causing it to drift with scroll content. Move the overlay outside the scroll area into a non-scrolling wrapper so it stays centered in the visible viewport. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e55f050b84 |
fix(i18n): clean up zh-Hans translation inconsistencies (#3308)
- Normalize nav/page/section titles to plural English (Issues/Skills/Tasks) per conventions.zh.mdx rules for section titles - Lowercase 'Issue' inside UI short phrase '我的 Issue' (UI short-phrase rule) - Translate concept words in GitHub settings (Connection/Features/Repositories/Done) - Translate 'Cloud Runtime' to '云端运行时' to match runtime→运行时 glossary Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fa2a0e57ec |
feat(views): swimlane supports parent / project / assignee grouping (MUL-2711) (#3311)
* feat(views): swimlane supports parent / project / assignee grouping (MUL-2711) The swimlane view was hard-coded to group by parent issue. This adds a display dropdown so users can pick parent (default), project, or assignee — analogous to how the board view exposes its grouping option. - Generalise the lane builder in swimlane-view.tsx behind a `LaneGroup` abstraction (matcher + per-grouping `moveUpdates` payload) so the drag-end handler no longer branches on grouping. Cell ids gain a `<grouping>:<rawId>` prefix and lane sortable ids include the grouping so dnd-kit cannot collide entries from different groupings. - Extend the view store with `swimlaneGrouping`, `swimlaneOrders` (one saved order per grouping), and a grouping-keyed `collapsedSwimlanes`. The persist `merge` defends against the old `string[]` shape so a pre-upgrade snapshot doesn't crash on first read. - Wire `setSwimlaneGrouping` into the issues display popover next to the existing board grouping control. Add en / zh-Hans copy for the three swimlane buckets (Parent issue / Project / Assignee) and the two new pinned lanes (No project / Unassigned). - Expand swimlane tests with parent / project / assignee smoke cases and update existing mocks to the new lane-id format. Add stable `useActorName` / `projectListOptions` mocks to avoid the set-state-in-effect loop that an unstable `getActorName` would trigger via the cells-rebuild memo. Co-authored-by: multica-agent <github@multica.ai> * feat(views): default swimlane grouping to assignee Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
735f18a4ef |
fix(agents): drop the import-hint callout on agent Skills tab (again) (#3301)
#3265 already removed this blue "Importing creates a workspace copy..." banner, but #3286 (the skills_local toggle revert) brought it back as collateral. Re-remove it — this tab isn't where skill imports happen (that lives behind Skills page → Add Skill → From Runtime), so the callout is pure noise here. Also flip the header row back to items-center now that the intro is once again the only thing in it. |
||
|
|
30a7da6d10 |
feat(list): drag-and-drop reordering + loadMore sort fix (#3284)
* feat(list): drag-and-drop reordering + shared drag utils - List view drag-and-drop: reorder within/across status groups, drop into collapsed groups, DraggableListRow with useSortable + checkbox protection - Extract shared drag utilities (computePosition, findColumn, buildColumns, makeKanbanCollision, issueMatchesGroup, getMoveUpdates) to drag-utils.ts - Reuse IssueDisplayControls in MyIssues header (dedup ~380 lines) - Radio check marks for grouping/sort dropdowns - Remove outer p-1 wrapper, unify board view to p-2 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(my-issues): move MyIssuesHeader inside ViewStoreProvider IssueDisplayControls calls useViewStore which requires the provider context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(header): restore swimlane option in view toggle dropdown Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
744b474199 |
revert(agent): remove per-agent local skill toggle (MUL-2603) (#3286)
* Revert "feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)" This reverts commit |
||
|
|
057cb2ff34 |
fix(issues): thread sort through load-more so appended pages render (MUL-2678) (#3279)
The recent server-side-sort change (#3228) keyed the issue-list cache by sort but did not update the load-more hooks: useLoadMoreByStatus used a prefix-match that could pick a stale cache variant, and neither hook forwarded sort to the API request. As a result, scroll-to-load-more fired its request, but the response was either appended to a cache no useQuery was subscribed to, or it appended rows in an unsorted order into a sorted bucket. Pass `sort` explicitly through Board/List/Swimlane and into the hooks. The hook now targets the full sorted key via setQueryData and forwards sort to the listIssues / listGroupedIssues calls so the appended page lines up with the existing items. Also adds focused tests for both load-more hooks: stale-sort cache is untouched, sort is forwarded to the API, and sort-less callers still hit the {} key path used by actor-issues-panel. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0b50c5a209 |
feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)
* feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) Only Claude Code and Codex runtimes actually enforce `skills_local` at exec time today — Claude isolates `~/.claude/skills/` via `CLAUDE_CONFIG_DIR`, Codex isolates `~/.codex/skills/` via per-task `CODEX_HOME`. Every other runtime currently stores the field but treats it as a no-op, which made the toggle in the Create Agent dialog and Skills tab misleading for those runtimes. Gate the toggle on `runtime.provider` so it only renders for the providers the daemon currently isolates. Centralise the supported-provider list as `isSkillsLocalSupportedProvider()` in `packages/core/agents` and reuse it from the create dialog and the Skills tab. The create dialog also drops `skills_local` from the payload when the selected runtime is unsupported, so a runtime swap can't leave a stale `ignore` opt-in pinned where it would never take effect. Docs (EN + ZH) updated to say the toggle is hidden — not just "a no-op" — for the unsupported runtimes. Co-authored-by: multica-agent <github@multica.ai> * docs(agents): align skills_local hint and type comment with claude+codex boundary Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
65ed5744a6 |
feat: add issue view swimlane (MUL-2607) (#3149)
* feat: Add swimlane view * refactor * test: add tests * wip * wip * wip * refactor * refactor and fixes * refactor * fix: parent empty cells * chore: Remove meanless comments * remove meanless comments |
||
|
|
bf8a346cf0 |
feat(runtimes): cascade-archive agents on runtime delete (MUL-2667) (#3266)
* feat(runtimes): cascade-archive agents on runtime delete (MUL-2667) Replace the bare 409 "cannot delete runtime: it has active agents" with a structured response carrying the blocking agent list, and wire a cascade endpoint that archives those agents, cancels their tasks, pauses dangling autopilots and deletes the runtime in a single transaction. The unified DeleteRuntimeDialog opens directly in cascade mode when the runtime has bound agents, pivots from light to cascade if the strict DELETE refuses with runtime_has_active_agents, and re-prompts when the cascade refuses with runtime_delete_plan_changed (live agent set drifted while the dialog was open). The online-local self-healing rule is preserved at the affordance level (kebab hidden, Diagnostics button disabled with tooltip) and re-checked at confirm time as defence in depth. Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): close cascade race + i18n delete dialog (PR #3266 review) - Acquire FOR UPDATE on the runtime row at the top of the cascade tx so FK-validated agent INSERTs/UPDATEs that would point at this runtime block until commit, and lock each currently-active agent row via ListActiveAgentsByRuntimeForUpdate so a concurrent archive/move of an existing active row also blocks. - Switch the bulk archive from runtime-keyed (ArchiveAgentsByRuntime) to ID-keyed (ArchiveAgentsByIDs), narrowed to the user-confirmed expected_active_agent_ids set. Combined with the runtime row lock, this guarantees no agent outside the confirmed plan can be silently archived between plan-compare and archive even at read-committed. - Wire delete-runtime-dialog.tsx to runtimes locale via useT(); add detail.delete_dialog.{light,cascade} keys (EN with _one/_other plurals, zh-Hans _other) covering titles, descriptions, warning, notices, checkbox, buttons, table headers, presence labels, and toasts. Resolves the i18next/no-literal-string CI failure. - Locale parity test passes (51 tests). All 4 dialog test cases pass unmodified (EN copy preserves original wording). Full views vitest: 91 files / 792 tests green; full server go test: green. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d8075a5775 |
fix(agents): tighten skills-tab intro and drop redundant import hint (#3265)
Three small UI cleanups on the agent Skills tab: - The blue "Importing creates a workspace copy that your team can edit and reuse" callout was visual clutter — drop it (and the Info icon import that it relied on). - The intro paragraph conflated two things: the workspace-skills concept (applies to every runtime) and the Allow-locally-installed-skills toggle (only honoured by Claude Code and Codex; verified — none of copilot/cursor/gemini/opencode/openclaw/hermes/pi/kimi/kiro read agent.SkillsLocal). Rewrite the intro to only describe the main concept; the toggle's own local_hint_on/off strings still carry the Claude/Codex caveat where it belongs. - The trimmed intro now fits one line, so flip the header row from items-start to items-center so the text sits on the same baseline as the "Add skill" button instead of clinging to its top edge. |
||
|
|
612ac8f28e |
feat(issues): server-side sort + fix drag position corruption (#3228)
* refactor(editor): split rich text styles * feat(issues): server-side sort + fix drag position corruption in non-manual sort Backend: ListIssues and ListGroupedIssues now accept `sort` and `direction` query params (position/priority/title/created_at/start_date/due_date). ListIssues converted from sqlc to hand-written SQL for dynamic ORDER BY. Priority sort uses CASE expression for semantic ordering. Frontend: query keys include sort so changing sort triggers server refetch. Client-side sortIssues() removed from board-view and list-view. Drag-and-drop: non-manual sort disables within-column reorder (prevents silent position corruption). Cross-column drag only updates status/assignee, preserves original position. Column overlay shows current sort during drag. Cache: query key split into prefix (list) for invalidation and full key (listSorted) for queryOptions. All optimistic update paths use prefix matching via getQueriesData to work with any active sort. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(board): prevent drag flicker by settling columns until mutation refetch After drag-and-drop, the optimistic cache patch updates position values without reordering the bucket array. The useEffect that rebuilds columns from TQ data would overwrite the correct local drag order, causing cards to snap back then forward. Fix: isSettlingRef blocks column rebuilds between drag end and mutation onSettled. Also invalidate issueKeys.list on WS position changes so other windows refetch correctly sorted data instead of showing stale bucket order. Includes debug logs (to be removed after verification). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(board): stabilize drag-and-drop for non-manual sort modes Three behavioral fixes for board drag when sort != position: 1. Settling: isSettlingRef + settleVersion blocks column rebuilds between drag-end and mutation settle, preventing the optimistic cache patch (which updates position values without reordering the bucket array) from overwriting the correct local column state. 2. Non-manual cross-column: handleDragOver returns prev (no visual card movement — column highlight + sort label is sufficient). handleDragEnd uses overCol directly instead of findColumn on the card's current position (which would be the source column). Cards use useSortable({ disabled: { droppable: true } }) to suppress within-column insertion indicators. 3. Collision detection: when no card droppables exist (disabled in non-manual sort), return column droppables from pointerWithin instead of falling through to closestCenter, so isOver reflects the column the pointer is actually inside. Also: WS position changes now invalidate issueKeys.list so other windows refetch correctly sorted data. Insertion-position prediction intentionally omitted — PostgreSQL's en_US.utf8 collation (glibc) cannot be faithfully replicated in JavaScript (ICU/V8), and an inaccurate indicator is worse than none. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sort): manual sort ignores direction param on both ends Manual sort (position) is user-defined order via drag-and-drop — reversing it has no product meaning. Backend: sort=position now skips the direction query param and always uses ASC. Both ListIssues and ListGroupedIssues handlers. Frontend: sort object omits sort_direction when sortBy is position. Direction toggle hidden in the display popover for manual mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * perf(board): memo columns + stabilize references to reduce re-renders - BoardColumn, PaginatedBoardColumn, PaginatedAssigneeBoardColumn wrapped in memo() — only columns with changed props re-render - IssueAgentActivityIndicator wrapped in memo() — 111 snapshot subscribers no longer trigger full re-render on every WS task event - buildColumns rewritten from O(groups × issues) to single-pass O(n) - EMPTY_IDS constant replaces ?? [] fallbacks (stable reference) - EMPTY_CHILD_PROGRESS constant replaces new Map() default - BOARD_COL_WIDTH / BOARD_CARD_WIDTH constants shared between column and DragOverlay for consistent card dimensions - issueListOptions + issueAssigneeGroupsOptions use placeholderData: keepPreviousData so sort/filter changes don't flash a full-page skeleton - Loading skeleton scoped to content area only — header stays rendered during data transitions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: remove outdated server-side sort implementation plan Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
960befa56f |
feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603) (#3200)
* feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603)
Adds an agent-scoped `skills_local` switch ("ignore" default / "merge") so
shared agents stop inheriting the operator's user-global Claude skill
directory. A single broken local skill on one operator's machine was
crashing the Claude CLI before it ever read stdin — the daemon saw a
"broken pipe" with no recoverable signal (GitHub #3052).
- DB: migration 108 adds `agent.skills_local` (NOT NULL DEFAULT 'ignore'),
with sqlc CreateAgent/UpdateAgent updates and handler validation.
- Claude runtime: when the agent is in "ignore" mode the backend points
CLAUDE_CONFIG_DIR at an empty per-task scratch dir under the task cwd
(fallback: OS temp), strips any inherited override, and cleans up after
the run. Workspace skills under `{cwd}/.claude/skills/` still load.
"merge" preserves the legacy inherit-from-machine behavior; Codex and
other isolated backends are no-ops.
- UI: new Skills toggle in the Create Agent dialog and the Agent → Skills
tab, with EN/zh-Hans copy and SkillsLocalToggle shared between the two.
- Tests: unit coverage for the new env helper, isolation dir lifecycle,
full Claude execute paths (ignore + merge), and the handler tristate
contract. Existing skills-tab test updated for the new copy.
- Docs: updated `/skills` docs (EN + ZH) and added a 0.3.7 changelog entry
in the landing-page i18n.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve claude login + validate skills_local input (MUL-2603)
Address Elon's review on PR #3200:
1. Skill isolation no longer drops the operator's Claude login. The
per-task scratch dir now mirrors every entry under `~/.claude/`
as symlinks except `skills/`, so `.credentials.json`, settings,
plugins, etc. reach the CLI exactly as on the host while the
user-global skills directory stays hidden. Without this, default
`ignore` would have broken every Claude agent on a non-API-key
host the moment migration 108 landed.
2. Internal CreateAgent callers (agent_template, onboarding_shim)
now set `SkillsLocal: "ignore"`. The Go zero value was about to
trip the migration-108 CHECK constraint and 500 template /
onboarding agent creation.
3. Create / update handler validation no longer normalizes garbage
to "ignore". The strict 400 path is now reachable on bad client
input; the drift-safe `normalizeSkillsLocal` stays on the read
side only.
UI copy + docs clarified that the toggle is Claude-only; other
runtimes ignore the setting.
Verification:
- `go test ./...` green (full suite locally).
- `pnpm --filter @multica/views exec vitest run agents/components/tabs/skills-tab.test.tsx` green.
- Handler DB-backed tests still skip locally without docker (same
as Elon's run) — CI will validate the create / update paths
against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): mirror effective claude config dir with windows fallback (MUL-2603)
Address Elon's second-round review on PR #3200:
1. The per-task scratch dir now mirrors the *effective* host Claude
config dir, not unconditionally `~/.claude/`. Precedence: agent
`custom_env` CLAUDE_CONFIG_DIR > parent process env > `~/.claude/`.
Without this, an operator who pinned Claude at a managed install
(custom env CLAUDE_CONFIG_DIR) would get the wrong credentials in
the scratch dir, because `buildClaudeEnv` strips that env before
handing it to the child. We resolve the source up front and feed
it to the mirror, so the override env still points at the right
bytes.
2. Mirror entries now go through platform-aware linkers. On Windows
without Developer Mode / admin, `os.Symlink` is denied, which
previously left the scratch dir empty and broke Claude Code auth
on default `ignore`. The new helpers try symlink first, then fall
back to a directory junction (`mklink /J`) for dirs or a hardlink
(same-volume content share) / copy for files. Mirrors the
execenv/codex_home_link_windows.go pattern.
3. Tests:
- `TestResolveHostClaudeConfigDir` locks in the custom_env >
parent_env > `~/.claude` precedence.
- `TestNewIsolatedClaudeConfigDirMirrorsCustomHostDir` confirms
the scratch dir picks up `.credentials.json` from a synthetic
custom host dir, proving the source resolution actually
propagates into the mirror.
- `TestNewIsolatedClaudeConfigDirEmptyHostIsNoop` documents the
env-var-auth-only case (no host source ⇒ empty scratch dir).
- `TestMirrorHostClaudeExceptSkillsWith_FallbackWhenSymlinkFails`
exercises the Windows-no-Developer-Mode path via the new
`mirrorHostClaudeExceptSkillsWith` seam, asserting credentials
and sub-dir children still reach the scratch dir after the
symlink stand-in fails.
- `TestMirrorHostClaudeExceptSkillsWith_PropagatesFirstLinkError`
confirms callers see the per-entry error when even fallback
fails (so the warn-log fires on broken Windows installs).
- `TestCopyFileRoundTrip` covers the last-resort copy fallback
and its EXCL no-overwrite contract.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` is the
end-to-end check: an agent with custom_env CLAUDE_CONFIG_DIR
reads its credentials from the pinned dir, not `~/.claude/`.
4. Docs: `apps/docs/content/docs/skills.{mdx,zh.mdx}` updated to
describe the effective-source resolution and the Windows
fallback chain so the docs match the runtime behaviour.
Verification:
- `go test ./...` green (full server suite locally, including
`pkg/agent` 23 cases covering the new + existing isolation
paths).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean, confirming the
Windows-tagged linker file builds.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): default skills_local to merge to preserve legacy behavior (MUL-2603)
Per Bohan's product decision on PR #3200, the per-agent host-skill toggle
defaults to "merge" — the pre-MUL-2603 inherit-from-machine behavior —
so existing personal workflows that rely on locally installed Claude
Skills keep working unchanged. Agent owners explicitly opt into "ignore"
when they need to harden a shared agent against a broken local skill on
one operator's machine (GitHub #3052).
Also audited all 11 runtimes for user-global skill discovery paths and
documented the scope of the toggle. Only Claude reads a user-global
`~/.claude/skills/`; Codex isolates via `CODEX_HOME`, the ACP backends
(Hermes / Kimi / Kiro) and the JSON-stream backends (Copilot / Cursor /
Gemini / Pi / OpenCode / OpenClaw) anchor discovery to the task workdir
and never read a user-global skill directory. UI copy and docs now say
"for runtimes that support it (currently Claude Code)" everywhere so
the scope is explicit.
Changes:
- Migration 108: column default flipped to 'merge'.
- Handler CreateAgent: missing field → "merge"; explicit "ignore" /
"merge" still validated, garbage still 400.
- normalizeSkillsLocal: drift-safe coercion now lands on "merge" for
anything that isn't the exact literal "ignore".
- agent_template.go / onboarding_shim.go: internal CreateAgent callers
send "merge" instead of "ignore" to match the new default.
- Claude runtime (`claude.go`): isolate-mode gate flipped from
`SkillsLocal != "merge"` to `SkillsLocal == "ignore"`, so "" (legacy
daemons / older clients) and "merge" both walk `~/.claude/` directly.
- Create Agent dialog + Skills tab: toggle defaults to on (merge); only
duplicate of an explicit "ignore" agent carries through. The
isolation opt-in is now `skills_local: "ignore"` when the user flips
off; "merge" is omitted from the request body.
- i18n (EN + zh-Hans): copy reframed — "On (default) — merged"; "Off —
ignored. Recommended for shared agents".
- Docs (`/skills`, `/guides/agents.zh`): describe new default and
enumerate which runtimes act on the toggle.
- Landing changelog 0.3.7: retitled "Per-Agent Local-Skill Toggle"; note
the on-by-default behavior + off-to-isolate framing.
- Tests:
- `TestClaudeExecuteIsolatesHostSkillsWhenIgnoreOptedIn` replaces the
old by-default isolation case (now requires explicit "ignore").
- New `TestClaudeExecuteDefaultModeKeepsHostConfigDir` locks in that
default ExecOptions preserve the host CLAUDE_CONFIG_DIR.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` now explicitly opts
into "ignore" mode.
- Handler tests: omitted → "merge"; explicit "ignore" round-trips;
preserve-existing test seeds "ignore" and asserts "merge" flip-back.
- `TestNormalizeSkillsLocal_DriftStaysSafe`: only literal "ignore"
maps to ignore; everything else → "merge".
- `skills-tab.test.tsx`: toggle ON by default; flip OFF when agent
opted into "ignore". Intro-text matcher anchored to a more specific
phrase so it no longer collides with the toggle hint copy.
Verification:
- `go test ./...` green (full server suite locally).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean (windows-tagged linker
file still builds).
- `pnpm typecheck` green across all packages and apps.
- `pnpm --filter @multica/views test` 88 files / 771 tests green.
- `pnpm --filter @multica/core test` 43 files / 390 tests green.
- Handler DB-backed tests still skip locally without docker; CI will
validate the create / update paths against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* chore(landing): drop 0.3.7 changelog entry from this PR (MUL-2603)
The landing-page release notes belong in a separate release-prep PR, not in the feature PR.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): propagate skills_local=ignore to codex user-skill seed (MUL-2603)
Make the per-agent skills_local toggle real for Codex too, not just Claude.
Previously the toggle was only consumed by the Claude backend, while the
daemon's execenv layer always seeded Codex's per-task CODEX_HOME with the
host machine's user-installed skills from ~/.codex/skills/. A shared Codex
agent with skills_local=ignore could still inherit a broken local skill
from one operator's machine.
Now: PrepareParams/ReuseParams carry SkillsLocal; hydrateCodexSkills
skips seedUserCodexSkills when SkillsLocal == "ignore" so the per-task
CODEX_HOME exposes only workspace skills to the codex CLI. Default
("merge", or empty from older servers/clients) preserves existing
inherit-from-machine behavior. UI / docs are updated to reflect the
contract honestly: Claude Code and Codex honor the toggle; other
runtimes (Hermes / Kimi / Kiro / Copilot / Cursor / Gemini / Pi /
OpenCode / OpenClaw) leave $HOME untouched and discover user-level
skills natively, so the toggle is a no-op for them today.
New tests: TestPrepareCodexSkillsLocalIgnoreSkipsUserSeed,
TestPrepareCodexSkillsLocalMergeSeedsUserSkills, and
TestReuseCodexSkillsLocalIgnoreSkipsUserSeed cover Prepare(ignore),
Prepare(merge), and the toggle-flip-on-reuse path.
Co-authored-by: multica-agent <github@multica.ai>
* docs(skills): scope skills_local toggle copy to Claude Code + Codex (MUL-2603)
Off-state hint and Skills tab intro now explicitly call out Claude Code +
Codex as the only runtimes that honor the toggle, with "other runtimes
ignore this setting" wired into both states (en + zh-Hans), so users on
non-Claude/Codex agents don't read "Off" as runtime-wide isolation.
Docs (skills.mdx, skills.zh.mdx, guides/agents.zh.mdx) stop describing
Hermes / Kimi / Gemini / Copilot / Cursor / Pi / OpenCode / OpenClaw / Kiro
as having native user-level skill discovery; the daemon simply does not
manage user-level skill discovery for those runtimes today, and the toggle
is a no-op regardless of where it is set.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
312ee29cb6 |
fix(views): always show assignee meta row on board cards when assignee property is enabled (#3250)
When no assignee was set, the entire meta row (assignee + dates + child progress) could disappear because showAssignee required both storeProperties.assignee AND issue.assignee_id. Now the row visibility depends only on storeProperties.assignee, and unassigned cards show "Unassigned" text with a clickable picker to assign. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ec1589f7b6 |
fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) (#3249)
* fix(deps): add eslint phantom dep detection + fix existing violations (MUL-2654) Introduce eslint-plugin-import-x/no-extraneous-dependencies rule to prevent phantom deps from causing production build splits when pnpm creates peer-dep variants. Fix all existing phantom deps across the monorepo, unify catalog references, and enable desktop smoke CI on PRs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * revert(ci): remove desktop smoke PR trigger per user feedback The existing smoke workflow only verifies packaging completes — it does not actually start the app or check rendering. This means it wouldn't have caught the white-screen bug (which was a runtime issue, not a build failure). Adding it to PRs would slow CI without providing meaningful protection. The ESLint no-extraneous-dependencies rule is the actual prevention mechanism. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(deps): sync pnpm-lock.yaml for rehype-sanitize dep classification Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(ui): move rehype-sanitize to deps + declare eslint-config (MUL-2654) - Move rehype-sanitize from devDependencies to dependencies (used in production Markdown.tsx) - Add @multica/eslint-config to devDependencies (imported by eslint.config.mjs but previously undeclared) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e33b893c3f |
feat(views): sticky group headers in list view (#3221)
* feat(views): add sticky positioning to list-view group headers Group headers now stay pinned at the top of the scroll viewport so users always know which status group they are looking at. Background changed from semi-transparent to opaque to prevent content bleeding through. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): remove top padding from list-view scroll container for sticky headers The `p-2` padding on the scroll container caused an 8px gap above sticky group headers. Replace with `px-2 pb-2` to keep horizontal and bottom padding while allowing headers to stick flush to the top. Sync skeleton containers in issues-page and my-issues-page to match. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): use p-2 pt-0 instead of px-2 pb-2 for list-view scroll container Reporter preferred adding pt-0 to override the top padding from p-2, keeping the original p-2 shorthand intact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(views): opaque sticky header hover + cursor-pointer on trigger Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c5b4c45e41 |
fix(chat): unify expand and drag-to-max rendering (MUL-2653) (#3235)
* fix(chat): unify expand and drag-to-max rendering so both produce same dimensions (MUL-2653) Expand button used CSS `inset-3` (parent minus 24px each side) while drag-to-max used explicit 90%-of-parent pixel dimensions — different sizes for the same conceptual state. Expand also hid resize handles, preventing drag-back. Now both paths render with explicit width/height at bottom-right and resize handles stay visible in all states. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(chat): animate width/height via framer-motion for smooth expand toggle (MUL-2653) Move width/height from style prop into animate prop so framer-motion interpolates size changes. Remove layout="position" which only tracked position. Drag uses duration:0 for instant feedback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
636fa1adb4 |
fix(issues): hide activity-block header while only recent entries are shown (#3226)
In the trailing activity block's default truncated state ("last 8 shown,
N older hidden"), we were rendering two stacked chevron rows: a "v N
activities" collapse header and a "> Show N more activities" reveal link.
Visually that looked like nested folds even though they're parallel
controls, and the header is redundant when the user just wants a glance
at recent activity.
Drop the header in the truncated default state. It reappears the moment
the user clicks "Show N more" — at that point they're seeing the full
block and a fold-back affordance becomes useful again. Blocks that fit
within the 8-entry limit (and non-trailing blocks, which never truncate)
keep their header as before.
|
||
|
|
441fa18db4 |
feat(issues): truncate trailing activity block to most recent 8 (MUL-2628) (#3219)
* feat(issues): truncate trailing activity block to most recent 6 (MUL-2628) The trailing activity block defaults to expanded, but a block with dozens of entries still drowns the comment area. Show only the most recent 6 by default; older entries fold behind an in-place "Show N more activities" toggle. Non-trailing blocks are unchanged — they still collapse whole. The "show older" choice is tracked per block id in a separate Set so it survives the block losing its trailing position (when a new comment lands after it) and survives a collapse/re-expand cycle. Co-authored-by: multica-agent <github@multica.ai> * refactor(issues): bump trailing activity block visible limit from 6 to 8 (MUL-2628) User feedback on the original PR: 6 felt slightly too tight. Bumped the trailing-block truncation threshold to 8 entries to give the "most recent activity" view a bit more headroom before older entries fold behind the "Show N more activities" toggle. Test count is unchanged; the existing trailing-block / non-trailing-block truncation cases were adjusted to exercise the new 8-entry boundary (10-entry trailing block → 2 hidden; 8-entry trailing block → none hidden; 10-entry non-trailing block → all visible after expand). Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
13f74e651a |
feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600) (#3209)
* feat(agents): remove custom_env from agent resources, add audited env endpoint (MUL-2600)
The agent resource shape (list / get / create / update / archive /
restore responses + WebSocket events) no longer carries `custom_env`
values. Reads/writes of env now flow exclusively through a dedicated
`/api/agents/{id}/env` endpoint that is owner/admin-only, rejects
agent-actor sessions, applies a "****" sentinel preserve guard on
PUT, and writes a persistent audit row per reveal/update.
Why
- `multica agent list --output json` historically returned plaintext
`custom_env` for owner/admin callers (the redaction gate gave only
members the masked map). Any agent token running on the workspace
inherits its owner's role and could read every other agent's
secrets just by listing.
- Patching list/get redaction alone (PR #3175 direction) left
symmetric leaks via mutation responses, WS events, the "reveal"
path itself (no actor-aware auth), and a `****` overwrite footgun
on UpdateAgent.
What changed
- Backend: drop `custom_env` from AgentResponse; add coarse
`has_custom_env` + `custom_env_key_count`. Strip env handling from
UpdateAgent (silently ignored if sent). Keep CreateAgent's
custom_env acceptance.
- Backend: new GET/PUT `/api/agents/{id}/env` handlers in
`internal/handler/agent_env.go`:
- resolveActor → 403 for agent actors (closes the lateral-movement
path).
- Owner/admin role gate via existing helper.
- PUT honours value == "****" as "preserve existing value".
- Both write to `activity_log` with `agent_env_revealed` /
`agent_env_updated` actions. Audit details record key names only,
never values.
- Daemon claim path (`ClaimAgentTask`) unchanged — `TaskAgentData`
still carries plaintext env for runtime injection.
- SQL: new `UpdateAgentCustomEnv` query; sqlc regenerated (v1.31.1).
- CLI: new `multica agent env get|set` subcommands. `--custom-env*`
flags removed from `multica agent update`; the no-fields error
now points to the new path.
- Frontend: drop env fields from `Agent` + `UpdateAgentRequest`; add
`getAgentEnv` / `updateAgentEnv` client methods; rewrite env-tab
to show "N variables configured" + explicit "Reveal & edit"
button, fetching values only on intentional reveal.
- Locales: parity-safe additions to en + zh-Hans.
- Docs: agents-create.{mdx,zh.mdx} reflect the new threat model and
endpoint.
- Mobile: schema drops `custom_env` / `custom_env_redacted`, adds
metadata fields.
Tests
- Handler tests pinned the new invariants: no env in list/get
responses, owner reveal happy-path + audit row, agent-actor 403,
`****` sentinel preserves real values, UpdateAgent silently
ignores `custom_env`, pure `mergeAgentEnv` cases.
- CLI tests pivot to the new flag surface: `agent update` MUST NOT
expose the env flags; `agent env set` MUST expose
--custom-env-stdin/--custom-env-file.
- Frontend test fixtures updated; pnpm typecheck / test / lint
pass cleanly.
This is a breaking API change. Scripts that read `custom_env` from
`/api/agents` must migrate to `GET /api/agents/{id}/env`.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close actor-spoofing + audit fail-closed in env endpoints (MUL-2600)
Addresses Elon's review of #3209:
* Mint a task-scoped `mat_` token per claim, bound to (agent, task,
workspace, owner). Daemon injects it into the agent process in place
of its own credential. Auth middleware authoritatively rebuilds
X-User-ID / X-Agent-ID / X-Task-ID from the token row and sets
X-Actor-Source=task_token; that header is server-set only — incoming
values are stripped before any auth branch runs. resolveActor honors
the header so an agent that strips X-Agent-ID / X-Task-ID still
resolves as actor=agent.
* GetAgentEnv / UpdateAgentEnv are now fail-closed on audit-log
failures: GET refuses to return plaintext, PUT persists inside the
same tx as the audit row so they commit/roll back together.
* PUT /api/agents/{id} returns 400 when the body carries custom_env
instead of silently dropping it — directs callers to the audited env
endpoint.
* Agent actors never see mcp_config, even when the underlying member
is owner/admin; mutation broadcasts go through a redaction shim so
WS subscribers don't pick it up either.
* Fix backend test that asserted dense JSON (jsonb::text renders
whitespace) and frontend test that assumed a unique "Test User"
match.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): close residual MUL-2600 gaps from review (MUL-2600)
Migration 108 FK now correctly references agent_task_queue(id) instead
of the non-existent agent_task table; the previous name blocked CI
backend migrations.
Task-token-authenticated requests can no longer be re-routed at a
different workspace by passing workspace_slug / workspace_id /
?workspace_id / a URL workspace param. ResolveWorkspaceIDFromRequest
and resolveWorkspaceUUID both short-circuit on X-Actor-Source=task_token
and return only the token-bound X-Workspace-ID; buildMiddleware adds a
defence-in-depth 403 if any URL-resolved workspace disagrees with the
token binding.
mcp_config no longer leaks back to agent actors through UpdateAgent /
CreateAgent / ArchiveAgent / RestoreAgent HTTP responses — the same
redactAgentResponseForActor helper that GetAgent/ListAgents use is now
applied to mutation responses too. WS broadcasts were already redacted
via broadcastAgentResponse.
FailTask and every TaskService cancel path (CancelTask /
CancelTasksForIssue / CancelTasksForAgent / CancelTasksByTriggerComment
/ BroadcastCancelledTasks) now eagerly DeleteTaskTokensByTask so the
mat_ token's 24h window doesn't outlive a terminated task. Failure is
non-fatal — the FK cascade and expiry remain durable guards.
Doc-only: clarify that PUT /api/agents/{id} now hard-rejects bodies
that carry custom_env (was previously "silently ignores").
Tests:
- middleware: TestResolveWorkspaceIDFromRequest gains a task_token
case asserting client-supplied slug/id/query cannot override the
bound workspace.
- handler: TestUpdateAgent_RedactsMcpConfigForAgentActor and
TestUpdateAgent_KeepsMcpConfigForMemberActor pin the mutation-
response redaction contract per actor type.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): match redacted mcp_config as JSON null, not Go nil (MUL-2600)
`AgentResponse.McpConfig` is `json.RawMessage` without `omitempty`, so
the redacted response serialises as `"mcp_config": null`. On decode,
`json.RawMessage` keeps the literal bytes `null` rather than collapsing
to Go nil, which made the assertion fire on a non-leak.
The product contract (field always present, distinguished from "no
config" via `mcp_config_redacted`) is intentional, so adjust the test
to check for "no secret-bearing content" instead of weakening the
contract via `omitempty`.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
5c1fad4508 | refactor(editor): split rich text styles (#3211) | ||
|
|
90455abd8d |
fix(desktop): preserve tab scroll position across Activity visibility cycles (MUL-2602) (#3196)
Closes #3183. Tabs render under `<Activity mode="visible|hidden">`, which keeps React state but drops DOM scrollTop when the subtree leaves layout. Switching to another tab and back sent users to the top of long discussions. `useTabScrollRestore` records the scrollTop of every element marked with `data-tab-scroll-root` while the tab is visible (capture-phase scroll listener) and restores them in a useLayoutEffect on the next visible transition, before paint. Saved offsets are dropped when the tab's path changes so intra-tab navigation lands at scroll=0 instead of inheriting the previous route's position. Mark scroll containers in views with `data-tab-scroll-root` (issue detail + chat message list ship with the marker; other views can adopt the convention as needed). `useAutoScroll` previously called `scrollToBottom()` on every effect mount, which would have overwritten the restored offset every time a chat tab cycled back to visible. Guard it with a once-per-instance ref. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9d5c023145 | fix(markdown): disable code ligatures (#3038) | ||
|
|
1c5e483b1c |
feat(pricing): add DeepSeek, Kimi K2.6, and Zhipu GLM cost tracking (MUL-2606) (#3204)
Adds rows to MODEL_PRICING for the Chinese-model SKUs listed on each provider's official pricing page, so opencode / OpenRouter-routed runtimes stop showing $0.00 in the dashboard for these models. Sources (now cited inline above the table): - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing - Moonshot: https://www.kimi.com/resources/kimi-k2-6-pricing - Zhipu z.ai: https://docs.z.ai/guides/overview/pricing Notes vs the closed PR #3170: - Only SKUs that exist on the official pages are added. glm-z1*, deepseek-v4-pro at $0.55/$2.19, kimi-k2.6 at K2's tier were all hallucinated and are NOT included. - deepseek-chat / deepseek-reasoner are routed by DeepSeek to deepseek-v4-flash, so they share the v4-flash rate. - deepseek-v4-pro is priced at the post-promo standard rate ($1.74 / $3.48), not the 75%-off promo that ends 2026-05-31. Brief over-estimate beats a sudden 4x jump on June 1. - glm-*-flash are priced at $0 because z.ai's free tiers are the literal published price. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a8cda1bd96 |
feat: add description field to workspace repository settings (#3198)
- Add optional description field to WorkspaceRepo type - Show description input below URL in edit mode - Display description text in view mode - Update isDirty to compare descriptions - Update tests for new field Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6261ea45fd | Improve board and squad hover cards (#3188) | ||
|
|
077bc055f7 |
fix(views): sort timeline entries by created_at on WebSocket append (MUL-2582) (#3139)
* fix: sort timeline entries by created_at on WebSocket append When multiple agents post comments concurrently, WebSocket events may arrive out of chronological order. The handlers blindly appended new entries to the end of the cached timeline array, causing display misordering. This fix sorts the array by created_at (with id as tie-breaker) after each insert. Changes: - use-issue-timeline.ts: sort after comment:created and activity:created - issue-ws-updaters.ts: sort in appendTimelineEntry Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(views): extract sortTimelineEntriesAsc helper, cover mutation onSuccess Review feedback from @Bohan-J: useCreateComment.onSuccess also appends unsorted (mutations.ts:558). When the local user posts a comment whose HTTP response returns after a concurrent WS event, the unsorted append leaves the cache misordered and the subsequent WS dedup skips re-sort. Extract sortTimelineEntriesAsc helper and reuse it in all three web cache writers: - comment:created WS handler - activity:created WS handler - useCreateComment.onSuccess Mobile keeps its own inline sort (apps/mobile/CLAUDE.md boundary). Add regression tests for sort position (mid-insert and oldest-insert). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5f1f08e466 |
feat(web): add use-cases content pipeline with welcome page (MUL-2349) (#2795)
* feat(web): add use-cases content pipeline with welcome page (MUL-2349) Wire fumadocs-mdx into apps/web with an independent collection rooted at content/use-cases/. Add the first page at /use-cases/welcome (header + H1 + prose + screenshot + footer) using the about-page visual shell. - source.config.ts + lib/use-cases-source.ts (separate from apps/docs) - features/landing/components/mdx/screenshot.tsx wraps next/image - public/use-cases/welcome/screenshot-1.png placeholder (55KB) - next.config.ts wraps NextConfig with createMDX() - .gitignore + eslint ignore .source/ Co-authored-by: multica-agent <github@multica.ai> * feat(web): bilingual db-boy use case with cookie locale (MUL-2349) Extends the use-cases pipeline into the first real article. - ZH + EN MDX (auto-data-analysis.{zh,en}.mdx) sharing three real screenshots; sensitive fields on db-boy-profile.png (RDS host, DB name, password) are blurred in-place. - Cookie-based locale: /use-cases/<slug> reads multica-locale server-side via lib/use-cases-i18n.ts (mirrors LandingLayout's cookie + Accept-Language fallback). Same URL serves either language; no [lang] segment so all other landing routes stay unchanged. - Frontmatter schema (source.config.ts): z.looseObject with declared hero_image / updated_at (required) / category (optional); a preprocess converts YAML-auto-parsed Date back to a YYYY-MM-DD string. - MDX components factory createMdxComponents(locale) routes the secondary CTA to /docs/zh (ZH) or /docs (EN); internal MDX links use <Link> for SPA nav; full-width and half-width colons both trigger [CTA: ...] / [占位图: ...] markers; 副 and Secondary both work as the secondary CTA prefix. - Index page localizes hero / subtitle / card CTA / metadata; sort fallback uses an epoch placeholder so undefined-order disappears. - Landing header + footer surface use-cases entry in both locales. - Detail route: sticky header, right-rail TOC with anchor jumps, scroll-mt-[100px] on H2/H3 so anchor jumps don't slip under the sticky header. - Drop welcome demo page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): resolve code review blockers on use-cases PR - Add `use-cases` to reserved_slugs.json + regenerate TS (P1: prevent future workspace slug collision) - Fix dead links in both MDX files: /features/* → /docs/* (P2) - Remove duplicate brand suffix in page title metadata (nit) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(web): align usecases locale routing * chore: refresh web mdx lockfile * fix(web): type mdx next config adapter * fix(web): wrap settings route page --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f0c32d5728 |
fix(views): move member count from header badge to section label in squad popover (MUL-2586) (#3178)
Remove the standalone member-count badge from the squad profile card header and display the count inline with the Members section label (Members · N). Add max-height + scroll guard on the member list to prevent card overflow with many members. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |