Commit Graph

878 Commits

Author SHA1 Message Date
Jiayuan Zhang
06242c7b77 Redesign runtimes machine layout 2026-05-17 22:07:19 +08:00
Bohan Jiang
3645bdb5b6 feat(issues): add start_date field with progressive disclosure (MUL-2274) (#2696)
* feat(issues): add start_date field with progressive disclosure (MUL-2274)

Mirrors the existing due_date implementation end-to-end so an issue can
express a planned start in addition to a deadline. Surfaces start_date as
an optional sidebar property alongside priority / due_date / labels (added
in MUL-2275), with consistent picker, board/list/sort, activity, and inbox
plumbing.

Backs the Project Gantt work (parent MUL-1881) and keeps the
progressive-disclosure attribute experience consistent.

- DB: migration 091 adds issue.start_date TIMESTAMPTZ.
- sqlc: ListIssues / CreateIssue / UpdateIssue / CreateIssueWithOrigin /
  ListOpenIssues read & write start_date.
- Backend: IssueResponse + create/update/batch-update handlers parse and
  emit start_date with RFC3339 validation; new start_date_changed activity
  event + subscriber notification (with prev_start_date in event payload).
- CLI: --start-date flag on `multica issue create` / `issue update`.
- Frontend: StartDatePicker component, start_date wired into Issue type,
  Zod schema, draft / view stores, sort util, header sort + card-property
  options, list-row / board-card display, create-issue modal, and the
  issue-detail progressive-disclosure "+ Add property" surface (visibility
  rule, picker row, add-property menu icon + label).
- i18n: en + zh-Hans for sort_start_date / card_start_date /
  prop_start_date / activity start_date_set / start_date_removed /
  picker start_date.trigger_label / clear_action / inbox labels.
- Tests: new TestNotification_StartDateChanged; existing Issue / draft /
  modal fixtures extended with start_date.

Co-authored-by: multica-agent <github@multica.ai>

* feat(issues): align start_date with due_date in actions menu and CLI table

- Add Start Date submenu (today / tomorrow / next week / clear) in
  actions menu, mirroring Due Date — parity with the Due Date quick
  setters in list/board context and 3-dot menus.
- Add corresponding en / zh-Hans i18n keys
  (actions.start_date / start_today / start_tomorrow / start_next_week
  / start_clear).
- CLI human table for `multica issue list` and `multica issue get`
  now shows a START DATE column next to DUE DATE; --full-id variant
  too.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 15:01:38 +08:00
Jiayuan Zhang
668cab6022 feat(github): mirror PR CI checks and merge conflict status (MUL-2228) (#2632)
* feat(github): mirror PR CI checks and merge conflict status (MUL-2228)

Surface "checks passed/failed" and "conflicts/no conflicts" badges under
each linked PR on the issue page so users can judge readiness without
flipping over to GitHub. CI state is fed by check_suite webhooks
(GitHub Actions + apps using the Checks API; legacy status events are
out of scope for MVP); conflicts are read from pull_request.mergeable_state.

Data model:
  * github_pull_request: add head_sha + mergeable_state
  * github_pull_request_check_suite: per-suite rows keyed by (pr_id, suite_id)
  * Aggregation done at query time, filtering by current head_sha so
    late-arriving suites for a stale head can't contaminate the new head's
    pending view; per-app latest suite chosen first so a single app firing
    multiple suites isn't counted N times.

Webhook hardening:
  * synchronize/opened/reopened/edited(base) explicitly clear mergeable_state
  * single-row ordering protection on the check_suite upsert prevents a
    late-delivered older event from overwriting a newer one
  * check_suite.pull_requests is iterated; unknown PRs are logged and dropped

UI:
  * PR row shows Checks + Conflicts badges; opaque mergeable values
    (blocked/behind/unstable/...) render as no badge, not as conflicts.
  * Terminal PR states (merged/closed) suppress the status row entirely.

Tests: * Pure unit coverage for derivePRMergeableState + aggregateChecksConclusion
  * Webhook integration tests: multi-app aggregation, old-head ignore,
    late-older-event ignore, synchronize clears mergeable_state
  * Vitest coverage for pull-request-list badge rendering across CI/conflict
    combinations and the legacy (null) fallback.
Co-authored-by: multica-agent <github@multica.ai>

* fix(github): scope check_suite PR lookup; preserve mergeable on metadata

Addresses code review on PR #2632.

1. check_suite handler now resolves the PR through the workspace-scoped
   GetGitHubPullRequest query instead of GetGitHubPullRequestByRepoNumber.
   The (workspace_id, repo_owner, repo_name, pr_number) tuple is the real
   uniqueness key, so a bare (owner, repo, number) lookup could return a
   stale row from another workspace and either land the suite on the wrong
   PR or skip the right one when the installation ids drifted. The old
   unscoped query is removed.

2. derivePRMergeableState now returns (value, clear) and the upsert SQL
   distinguishes three cases: state-changing actions clear the column to
   NULL, non-empty payloads write the value, and metadata events with an
   empty payload preserve the existing column. Previously every empty
   payload became NULL, so a labeled/assigned event silently wiped a
   known clean/dirty verdict in violation of the RFC's "metadata empty
   payload preserves" rule.

3. ListPullRequestsByIssue narrows to the issue's PR ids before running
   the per-app check_suite aggregation, avoiding a full-table scan over
   github_pull_request_check_suite when only a handful of rows belong to
   the requested issue.

New helper test covers labeled+empty preserves; new integration test
verifies a metadata event after a known mergeable_state keeps the value.

Co-authored-by: multica-agent <github@multica.ai>

* feat(github): PR card layout v3 increment — stats + segmented progress bar

Replaces the row + badge layout under "Pull requests" on the issue
detail sidebar with a card that mirrors the GitHub PR summary look:
title, author/avatar, +N −M · K files diff stats, segmented progress
bar (failed → pending → passed, failure leftmost), and a one-line
status caption following an explicit priority pass-through.

Backend
- Migration 092: github_pull_request adds additions / deletions /
  changed_files (INT NOT NULL DEFAULT 0). Zero defaults are what the
  new frontend treats as "legacy backend — hide the stats row" so old
  PR rows that pre-date this migration don't render "+0 −0 · 0 files".
- pull_request webhook handler reads stats off the top-level payload.
- ListPullRequestsByIssue now surfaces per-suite counts
  (checks_passed / failed / pending) alongside the existing aggregate
  conclusion, so the segmented bar reuses the already-computed counts
  with no new aggregation.

Frontend (packages)
- core/github/pull-request-status.{ts,test.ts}: pure-function module
  for the status-kind priority table and the segment derivation; 15
  cases covered, includes the "all-zero → hide stats" guard.
- views/issues/components/pull-request-list.tsx: PullRequestCard plus
  a compact-row fallback used when count > 4 (first 3 as cards, the
  remainder collapsed behind a Show more toggle).
- i18n: new `pull_request_card_*` keys in en + zh-Hans.

Tests
- 12 component tests covering each rule of the priority table, the
  legacy-zero stats fallback, and the collapse threshold.
- Reuse of the v3 webhook handler tests confirmed.

Verification
- pnpm typecheck + pnpm test green (60 test files, 536 tests).
- go build ./... + go vet ./... clean.
- 6 demo issues (DEV-2..DEV-7) screenshotted via Playwright; see the
  PR comments for the visual check matrix.

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): collapse PR cards at N>=4, not N>4

The card-vs-collapse threshold used `>` so 4 PRs slipped past it and
all rendered as full cards, contrary to RFC v3 (N >= 4 collapses to
3 cards + compact tail). Switch to `>=` and update the threshold-
boundary test to expect "Show 1 more".

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): align PR sidebar rows with existing list style

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): hide terminal PR status badges

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-16 21:26:30 +02:00
Jiayuan Zhang
e00b94b0f9 fix(realtime): invalidate per-issue token usage on task events (MUL-2298) (#2723)
The issue-detail right-rail Token usage card is fed by useQuery(issueUsageOptions(id)),
but the realtime task: handler only invalidated ["issues","tasks"]. As a result the
card only refreshed on remount, so consecutive runs on the same issue left the
numbers stuck until the user navigated away and back. Mirror the existing tasks
invalidation with a prefix invalidation of ["issues","usage"] so any task
lifecycle event refreshes the aggregated usage numbers.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-16 12:45:27 +02:00
Jiayuan Zhang
380c6b5122 feat(usage): add Time and Tasks to daily-trend toggle (MUL-2283) (#2709)
Extends the workspace /usage page Daily tokens chart toggle from
Tokens | Cost to Tokens | Cost | Time | Tasks, so users see daily
run-time and task-count trends alongside spend without leaving the page.

- New SQL `ListDashboardRunTimeDaily`: per-date totals from
  agent_task_queue (terminal tasks only), scoped to workspace and
  optionally project. Same time anchor as ListDashboardAgentRunTime
  so day boundaries line up.
- New handler GET /api/dashboard/runtime/daily + TanStack Query option.
- New DailyTimeChart (single-series, smart h/m/s unit) and
  DailyTasksChart (completed + failed stacked).
- Empty-state is per-metric so a workspace with tokens but no terminal
  runs (or vice-versa) doesn't get a false "no data".
- i18n: en + zh-Hans daily.metric_time / metric_tasks + titles.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 18:51:02 +02:00
Naiyuan Qing
0079a73430 fix(views): narrow agent/squad create dialogs to max-w-2xl (#2706)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 23:09:15 +08:00
Jiayuan Zhang
3698fd85d5 feat(views): show Total in daily token/cost chart tooltips (MUL-2282) (#2704)
* feat(views): show Total in daily token/cost chart tooltips (MUL-2282)

Add a Total row at the bottom of the daily-tokens-chart and daily-cost-chart
tooltips so users can see the precise stack sum on hover, in addition to the
per-stack breakdown.

Implemented by extending shared ChartTooltipContent with an optional `footer`
prop (ReactNode | (payload) => ReactNode) that renders below the items with a
top divider; backwards-compatible (no behavior change when footer is omitted).

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): i18n Total label in chart tooltips (MUL-2282)

Lint rule i18next/no-literal-string flagged the hardcoded "Total" string
in daily-cost-chart and daily-tokens-chart tooltips. Move it to
runtimes.charts.tooltip_total and read via useT.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 15:50:20 +02:00
Jiayuan Zhang
8e88156356 Add assignee grouping for issue boards (#2693) 2026-05-15 18:44:08 +08:00
iYuan
d8635ad580 fix(issues): prevent duplicate active issue creation (MUL-2225) (#2602)
* fix: prevent duplicate active issue creation

* fix(issues): address duplicate guard review

* fix(autopilot): skip duplicate issue admissions

* fix(issueguard): tighten duplicate lookup edge cases

* test(issues): cover duplicate guard autopilot skips

* feat(autopilots): group skipped runs in history
2026-05-15 18:27:56 +08:00
Bohan Jiang
57be69517f feat(views): progressive disclosure for issue sidebar properties (MUL-2275) (#2675)
* feat(views): progressive disclosure for issue sidebar properties (MUL-2275)

Split sidebar Properties into a core group that always renders
(status / priority / assignee / labels) and an optional group
(due_date / project / parent) that only appears when the issue has
the value set or the user explicitly added it via a new
"+ Add property" picker. A field cleared in-session stays visible
to avoid row flicker; navigating to a different issue reseeds
visibility from that issue's set fields. The standalone "Parent
issue" card is folded into Properties as one of those optional
rows. Adds `defaultOpen` to DueDatePicker / ProjectPicker so a
newly-added row drops the user straight into edit state.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(views): swap sidebar optional set to due_date + labels

Per design feedback: status / priority / assignee / project / parent
are all required and should always render in the sidebar; only
due_date and labels are progressive-disclosure optionals. Move project
and parent rows out of the optional block (drop their +Add property
menu entries and the parent special-case in addOptionalProp). Move
labels into the optional block, gated on the issue's actual attached-
label count (queried via issueLabelsOptions), with defaultOpen wired
through LabelPicker so picking "Labels" from +Add property drops the
user straight into the picker. Tests updated for the new split.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(views): restore standalone parent card, move priority to optional

Parent goes back to its own collapsible section, rendered only when the
issue actually has a parent — matching the pre-MUL-2275 behavior. It is
no longer interleaved with Properties rows.

Priority joins the progressive-disclosure set (priority / due_date /
labels). New issues default to priority "none", so the row is hidden
until set or added via "+ Add property", and PriorityPicker gains
defaultOpen so the field drops straight into edit state when chosen
from the add-property menu.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(issue-detail): tighten Add-property popover visual rhythm

Picked up a small visual inconsistency while reviewing the PR's UI:
the "Add property" dropdown floated above the inspector at a noticeably
larger type scale than the property rows, and each item was bare text
while the rows it sat above all rendered with an icon + value pair.

Tweaks:
- Items: `text-sm py-1.5` → `text-xs py-1`, matching the inspector
  row typography and trimming row-to-row gap from 12px to 8px.
- Each option leads with the icon the resulting picker uses
  (`PriorityIcon` bars / `CalendarDays` / `Tag`) so the dropdown reads
  as a preview of what will appear in the new PropRow.
- Focus indicator: replace the default thick focus ring with
  `focus-visible:bg-accent + outline-none`, matching the hover state
  language — keyboard focus and mouse hover now look the same.
- Popover width: `w-48` → `w-44` since the labels are short and the
  visual is now denser; still leaves room for translated strings.

* fix(issue-detail): dismiss Add-property popover when an option is picked

Base UI's `Popover` doesn't auto-dismiss when a child is clicked (it's
not a Menu primitive), so picking an option left the "+ Add property"
popover sitting behind the picker that auto-opens for the newly added
row — two popovers visibly stacked.

Make the Popover controlled with a local `addPropPopoverOpen` state and
close it inside `addOptionalProp` right after enqueuing the row's
auto-open. The picker still pops on mount via `defaultOpen={autoOpenProp
=== key}`, so the user flow is unchanged from their perspective:

  Click "+ Add property" → menu opens
  Click an option         → menu closes AND target picker opens

(Was the same flow on paper before; just had the orphan popover behind
the picker.)

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 18:04:33 +08:00
Naiyuan Qing
f64d182fd1 fix(views): narrow agent/squad create dialogs from max-w-5xl to max-w-4xl (#2688)
Both create dialogs were too wide at 5xl (1024px). Align with the
codebase convention for full create dialogs (create-project,
create-issue expanded) which use max-w-4xl (896px). Keeps both
modals consistent.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 17:59:45 +08:00
Naiyuan Qing
5ad1641b72 Revert "Squad archive dialog + role editor + transactional DeleteSquad (#2680)" (#2687)
This reverts commit 2980ead4c7.
2026-05-15 17:44:59 +08:00
Naiyuan Qing
1cb926d52d feat(views): refine navigation progress bar with brand color and glow (MUL-2269) (#2681)
* feat(views): refine navigation progress bar with brand color and glow (MUL-2269)

The previous 1px bg-primary bar read as near-black on light theme and
snapped on/off in a single frame, which felt abrupt despite being a small
visual element. Switch to a 2px brand-colored sweep with right-edge glow,
slower 1.4s cubic-bezier easing, and a 200ms fade-out so completion
doesn't pop.

- Container: h-px → h-0.5 (2px); always mounted with opacity-driven fade
- Bar: bg-primary → bg-brand + two-layer box-shadow glow via color-mix
- Keyframe: 1.1s ease-in-out → 1.4s cubic-bezier(0.4, 0, 0.2, 1)

Zero new design tokens (reuses existing --brand) and zero tailwind config
changes. Desktop unaffected — same component, same prefetch=no-op path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(views): unmount nav progress sweep when hidden (MUL-2269)

Hiding the bar with opacity-0 left the inner element's `infinite` keyframe
animation running on every dashboard page, defeating the perceived-perf goal.
Mount the sweep only while navigating, plus the 200ms fade tail (unmount on
opacity transitionend), so nothing animates while hidden.

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>
2026-05-15 17:39:28 +08:00
Naiyuan Qing
2980ead4c7 Squad archive dialog + role editor + transactional DeleteSquad (#2680)
* docs(squad): address plan-review feedback for archive + role plan

Resolve the 4 items the reviewer raised on MUL-2265:

1. TS schema: declare `active_issue_count` as optional (`number | null | undefined`)
   so list/create/update Squad responses don't lie about their shape; only
   `getSquad` parses through SquadSchema.
2. Archive semantics: restrict TransferSquadAssignees to active issues
   (status NOT IN done, cancelled) so dialog count and SQL operate on one set
   and terminal-state issues keep their historical assignee.
3. Index assumption: corrected — `idx_issue_assignee (assignee_type,
   assignee_id)` exists and is sufficient at realistic squad cardinality;
   no new index needed.
4. Fixed `*int64` test comparison and added `.loose()` to SquadSchema per
   the local schemas.ts convention.

Co-authored-by: multica-agent <github@multica.ai>

* docs(squad): plan v3 — revert to count-all/transfer-all on archive

Reviewer round 2 surfaced two structural problems with plan v2's
active-only carve-out:

1. useActorName resolves squad names via ListSquads, which filters
   archived_at IS NULL. A closed issue with an archived-squad assignee
   would render as "Unknown Squad".

2. The status-only update path in UpdateIssue skips validateAssigneePair,
   so a done/cancelled issue with an archived-squad assignee could be
   reopened to in_progress, violating the "no active issue on an archived
   squad" invariant enforced elsewhere.

Both problems disappear by reverting to count-all + transfer-all: after
ArchiveSquad runs, no issue points at the archived squad, so neither
case can occur. The product trade-off is that closed historical issues
now show the leader agent instead of the archived squad in their
"Assigned to" badge — consistent with existing agent-level reassignment
behavior elsewhere in the product.

Field rename: active_issue_count -> issue_count.
TransferSquadAssignees SQL is unchanged (already transfers all).

Co-authored-by: multica-agent <github@multica.ai>

* docs(squad): add Task 2b — wrap DeleteSquad transfer + archive in one tx

Reviewer round-3 flagged that the v3 invariant ("after archive no
issue points to the squad") was asserted on the happy path only.
DeleteSquad's current best-effort impl breaks it two ways:
- transfer failure → slog.Warn but archive proceeds (Unknown Squad,
  reopen-into-archived-squad bugs reappear)
- archive failure after a committed transfer → 500 with squad still
  active but emptied

Task 2b rewrites DeleteSquad to run TransferSquadAssignees +
ArchiveSquad inside one pgx tx, mirroring the project.go:266-314
pattern. Publish moves below Commit. Adds two regression tests that
lock both partial-write failure modes.

Co-authored-by: multica-agent <github@multica.ai>

* feat(squad): replace native confirm() with AlertDialog and rewrite role editor as combobox

Backend:
- Add CountIssuesForSquad sqlc query (counts every issue assigned to a squad,
  no status filter — matches the existing transfer-all archive semantics).
- Extend SquadResponse with optional `issue_count` (`*int64` + omitempty,
  populated only by GetSquad to avoid an N+1 in the list endpoint).
- Wrap DeleteSquad's transfer + archive in a single pgx transaction so the
  v3 invariant ("after archive, no issue points to the squad") is durable
  rather than best-effort. Promote slog.Warn to slog.Error and check the
  parseUUIDOrBadRequest ok flag (silent zero-UUID was a #1661-class latent
  bug). Publish only after Commit so realtime never sees rolled-back state.
- Tests cover happy path (count, transfer-all including terminal statuses)
  and both rollback directions (transfer fail / archive fail) via a
  fault-injecting tx wrapper.

Frontend:
- Extend Squad TS type with `issue_count?: number | null` (optional —
  list/create/update legitimately omit it). Add SquadSchema with `.loose()`
  and wrap getSquad with parseWithFallback so older servers and count-error
  responses degrade to the dialog's "no count" copy variant.
- Replace `window.confirm()` with shadcn `ArchiveSquadConfirmDialog`
  (destructive variant, leader name + count + closed-issue caveat in the
  copy, Loader2 while pending). i18n keys added under squads.archive_dialog.
- Rewrite RoleEditor as a Popover + Command combobox: Pencil affordance is
  always visible, suggestions aggregate other members' roles, commit only
  on Enter or selecting a suggestion (blur discards), per-member savingId
  drives Loader2 so the spinner only renders on the row being saved.

Co-authored-by: multica-agent <github@multica.ai>

* fix(squad): discard RoleEditor draft on close and no-op blank Enter

Two reviewer findings on e0d754bf:

1. Closing the Popover (outside click, Esc, trigger re-click) left `query`
   in state, so reopening + Enter would commit the stale draft. Clear
   `query` on every non-saving close path.
2. With an existing role, opening the editor and pressing Enter on an
   empty input committed "" — `commit` only no-op'd when trimmed matched
   value. Treat blank Enter as a no-op; clearing a role would need an
   explicit clear action that doesn't exist yet.

Add two regression tests:
- close (via outside click) → reopen surfaces a clean input; Enter does
  not commit the stale draft
- blank Enter on an existing role does not call onSave

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(squad): add explicit Clear button to RoleEditor

Role is optional, but the previous fix turned blank Enter into a no-op
without exposing any other way to clear an existing role — that broke a
valid terminal state. Keep blank Enter as no-op; add a "Clear role"
button at the bottom of the popover that only renders when value is
non-empty and routes through onSave("").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 17:29:37 +08:00
Naiyuan Qing
e8d6c912c4 feat(views): prefetch + transition + skeleton for snappy web navigation (MUL-2269) (#2677)
Internal navigation on web feels laggy because clicking a sidebar link blocks
0.2–0.6s with zero visual feedback — no prefetch, no Suspense fallback in the
dashboard segment, and no React transition to mark the route commit as pending.

This change adds the three pieces App Router needs to make the click→commit
window feel instant, scoped to the (dashboard) segment so auth/landing keep
their existing chrome:

- NavigationAdapter gains an optional prefetch(path). The web adapter wires
  it to router.prefetch; desktop leaves it undefined (react-router has no
  equivalent and doesn't need one). AppLink prefetches on hover/focus and
  preserves caller-supplied onMouseEnter/onFocus/onClick.
- NavigationProvider wraps push/replace in useTransition and exposes the
  pending flag via useIsNavigating(). Every useNavigation().push caller —
  sidebar AppLink, command palette, post-create modal jumps — picks this up
  automatically.
- New apps/web/app/[workspaceSlug]/(dashboard)/loading.tsx renders a minimal
  skeleton during cold transitions inside the dashboard segment only.
- DashboardLayout renders a 1px top progress bar driven by useIsNavigating.

packages/views remains free of next/* imports; desktop is unaffected by
construction (no prefetch, transition flips quickly, no loading.tsx).

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 17:01:42 +08:00
Bohan Jiang
461be83970 feat(views): collapse activity blocks in issue timeline (#2585)
Each consecutive run of activities renders as a single "N activities"
summary by default. Clicking expands the block in place. Comments are
unaffected; the most recent activity block stays expanded so users see
"what just happened" without a click.

Refs MUL-2188

Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
2026-05-15 14:39:59 +08:00
LinYushen
75dc70686b fix(realtime): include actor_type in WS broadcast messages (#2668)
* fix(realtime): include actor_type in WebSocket broadcast messages

The WS broadcast message format was {type, payload, actor_id} but missing
actor_type. This meant the web UI could not distinguish agent from human
operations in real-time events at the top level.

While payload data for comments (author_type) and activities (entry.actor_type)
already included the type, the top-level message did not — causing the web UI
to display agent CLI operations as human operations when relying on the
broadcast actor identity.

Changes:
- server/cmd/server/listeners.go: add actor_type to all broadcast messages
- packages/core/types/events.ts: add actor_type to WSMessage interface
- packages/core/api/ws-client.ts: pass actor_type to event handlers
- packages/core/realtime/hooks.ts: update EventHandler type signature
- packages/core/realtime/provider.tsx: update EventHandler type signature

Fixes MUL-2260

Co-authored-by: multica-agent <github@multica.ai>

* test: add frame-shape unit test asserting actor_type in WS frames

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 14:10:24 +08:00
Bohan Jiang
9b6b8f5877 fix(ci): refresh pnpm-lock.yaml + name test wrapper after #2665 (#2667)
* fix(deps): refresh pnpm-lock.yaml after #2665 added test deps to core

#2665 (MUL-2256, fix(realtime)) added `@testing-library/react` and
`react-dom` to `packages/core/package.json` devDependencies, plus moved
`react` from dependencies → devDependencies, but didn't commit the
regenerated lockfile. CI runs `pnpm install` with --frozen-lockfile
(implicit in CI envs), which bails immediately:

  ERR_PNPM_OUTDATED_LOCKFILE: pnpm-lock.yaml is not up to date with
  packages/core/package.json
  * 2 dependencies were added: @testing-library/react@catalog:,
    react-dom@catalog:

Frontend CI has been red on main since 7c8cf929. Backend is fine
because Go doesn't share the lockfile.

Lockfile delta is small (+9 / -3): the only changes are the three
specifier blocks for the deps already declared in package.json. No
version upgrades, no transitive churn — `pnpm install` produced an
identical resolved tree minus the missing entries.

* fix(core): name the test wrapper component to satisfy react/display-name

Same source of CI red as the lockfile bump in this PR — #2665 also
introduced packages/core/realtime/use-realtime-sync-ws-instance.test.tsx
where `createWrapper` returned an anonymous arrow component. The
`react/display-name` lint rule (enforced as error in core) flagged it,
and once `pnpm install` was unblocked the next CI step fell through to
this lint failure.

Convert the inline arrow into a named `function Wrapper(...)` —
identical render output, satisfies the rule.

Verified: `pnpm --filter @multica/core lint` → 0 errors (was 1).
The 4 tests in this file still pass.
2026-05-15 13:51:35 +08:00
LinYushen
7c8cf929d1 MUL-2256 fix(realtime): invalidate workspace queries on WSClient instance change (#2665)
* fix(realtime): invalidate workspace queries on WSClient instance change

When switching workspaces, the old WSClient is torn down and a new one
is created. Events emitted during the transition are lost because
onReconnect only fires for reconnections within the same instance.

Add an effect that tracks the WSClient instance via useRef and, on
detecting a non-initial new instance, invalidates all workspace-scoped
queries (same set as onReconnect). The first assignment is skipped to
avoid redundant refetches on initial mount.

Closes multica-ai/multica#2562

Co-authored-by: multica-agent <github@multica.ai>

* refactor(realtime): extract shared invalidation helper + add ws instance test

- Extract invalidateWorkspaceScopedQueries() to deduplicate the
  invalidation key list shared by onReconnect and ws-instance-change effects
- Add hook test covering: first ws skip, null gap no-op, new instance
  invalidates exactly once, same instance no re-invalidation

Addresses review nits from PR #2665.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 13:37:48 +08:00
Jiayuan Zhang
2f0e5b589e [codex] Add member and agent task views 2026-05-15 07:23:00 +02:00
Naiyuan Qing
f29bd93444 feat(squads): rework Create Squad modal (MUL-2233) (#2645)
* feat(squad): accept avatar_url on CreateSquad

Threads avatar_url through the SQL query, sqlc-generated code, and the Go
handler so the create-squad flow can persist an avatar at creation time
instead of forcing a follow-up PATCH.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(squad): add avatar_url to CreateSquadRequest

Extends the TS contract for the new backend field so the frontend can pass
an uploaded avatar URL through api.createSquad.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* feat(squads): rework Create Squad modal to match CreateAgentDialog (MUL-2233)

Replaces the cramped small-dialog flow with the same large-dialog shape used
by Create Agent: identity row (AvatarPicker + name + description with char
counter), grouped Leader picker (My Agents first, then Workspace Agents),
and a new multi-select Additional Members picker covering agents and
workspace members. The members trigger collapses to "+N" once more than
three are selected; promoting an agent to leader auto-drops it from the
additional-members list.

After createSquad, additional members are attached via Promise.allSettled
so a single failure surfaces a warning toast without blocking navigation —
the squad still exists and the user can retry from the Members tab.

Adds packages/views/modals/create-squad.test.tsx covering identity binding,
leader-group ordering, leader/member conflict sanitization, the empty- and
partial-failure success paths, and the create-failure recovery path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(squads): valid trigger HTML + drop conflicted leader from members

Two issues from PR #2645 review:

1. AdditionalMembersPicker's PopoverTrigger was a <button> containing
   MemberChip's remove <button>, which React/HTML flags as nested
   interactive content (hydration + a11y warning). Render the trigger as
   a <div role="combobox"> via Base UI's render prop so the chip's
   remove button is valid.

2. sanitizedMembers only hid the leader from rendered/submitted output,
   so promoting an additional member to leader then switching leader
   away resurrected the hidden pick. Drop it from selectedMembers at
   the moment of promotion via handleLeaderChange; sanitizedMembers is
   no longer needed.

Adds a test that promotes → switches leader and asserts the member is
not resubmitted.

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>
2026-05-15 13:11:08 +08:00
Bohan Jiang
2acc454ea5 fix(repos): accept scp shorthand in repo URL inputs (MUL-2250) (#2661)
Backend now validates http/https/ssh/git scheme plus scp-like
`git@host:owner/repo.git` shorthand, but three repo URL inputs were
still `type="url"`. The browser's native URL validation rejected scp
shorthand with "Please enter a URL" before the value could reach the
backend.

- Switch the three inputs to `type="text"` so submission isn't blocked
  client-side (project resources picker, workspace repositories tab,
  create-project repo picker).
- Extend the en/zh placeholders to show a scp shorthand example
  alongside the existing https one.
- Add a repositories-tab test that types `git@github.com:...` and
  asserts the input is text-type, passes native validity, and reaches
  the update mutation.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 13:09:06 +08:00
Bohan Jiang
968ef1ca84 test(runtimes): pin combined provider+dotted+dated Claude normalization (#2657)
Adds a regression test for `anthropic/claude-opus-4.7-20251001` that
exercises all three resolvePricing tolerances at once (provider strip,
Claude dot→dash, date trim). Each step was already covered pairwise;
this nails down their composition so a future change to candidate
ordering can't silently drop a step.

Follow-up to #2654 (MUL-2243); raised in second review.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 12:35:31 +08:00
Bohan Jiang
833032ed9c fix(runtimes): price Claude IDs reported as dotted / provider-prefixed (MUL-2243) (#2654)
Copilot's `meta.agentMeta.model` reports Claude SKUs with dots
(`claude-opus-4.7`, `claude-sonnet-4.6`, ...), and openclaw / opencode
emit the `<provider>/<model>` form (`anthropic/claude-opus-4.7`). The
maintained MODEL_PRICING table only keys on Anthropic's canonical
dashed form (`claude-opus-4-7`), so every Copilot-routed turn was
falling through to the "Custom model pricing" dialog and silently
contributing $0 to cost totals.

Teach `resolvePricing` two new tolerances, in order before date stripping:

  1. Strip a leading `<provider>/` segment — that's routing metadata,
     not part of the SKU.
  2. For `claude-*` IDs only, normalize dots to dashes. Scoped to
     Anthropic because for OpenAI the separator is semantic (`gpt-5.4`
     is a distinct SKU from a hypothetical `gpt-5-4`).

Custom pricing still wins over nothing, but the maintained catalog
still wins over a stale custom override (existing invariant preserved
by the test suite).

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 12:29:25 +08:00
Jiayuan Zhang
e7db644563 fix(chat): make session dropdown width track its trigger (MUL-2223) (#2630)
The chat header dropdown was capped at max-w-80 while the trigger
could grow unbounded with the current chat title, so the popup
appeared narrower than the trigger and titles inside were truncated
early. Cap the trigger at max-w-96 and let the popup inherit the
trigger width via --anchor-width with the same upper bound, so the
two stay visually consistent and only truncate at extreme lengths.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 06:25:24 +02:00
Naiyuan Qing
cc3a510952 fix(issues): respect create-mode preference at generic entry points (#2640)
Sidebar "新建 issue" button, command palette "New Issue", and the `c`
shortcut all hard-coded which create modal to open, ignoring the
persisted lastMode in useCreateModeStore. Pressing `c` after switching
from agent → manual reverted to agent on the next open.

Add `openCreateIssueWithPreference(data?)` helper next to the store.
Generic entries call it; entries that pre-seed manual-only fields
(status, project_id, parent_issue_id from board / list / project /
sub-issue actions) keep opening "create-issue" directly because agent
mode does not honour those seeds.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-15 10:00:53 +08:00
Jiayuan Zhang
9517536d49 fix(runtimes): keep base name visible, truncate hostname first (#2629)
The RUNTIME cell rendered base name + (hostname) with both spans using
flex: 0 1 auto, so the longer hostname dominated and squashed the name
to a single letter. Give the base name shrink priority and let the
hostname own the flex slot with basis-0, so hostname truncates first
while the name stays readable.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 15:47:28 +02:00
Jiayuan Zhang
675ed02aa6 MUL-2216: persist Mine/All tab selection on Agents and Squads pages (#2624)
* MUL-2216: feat(agents,squads): persist Mine/All tab selection per workspace

Tab selection on the Agents and Squads list pages was held in
component-local state, so navigating into a detail page and back
remounted the list and reset the tab to the default "Mine". Move
`scope` into Zustand stores backed by `persist` +
`createWorkspaceAwareStorage`, matching the pattern used by the
Issues view store. Selection now survives list → detail → back
navigation and page reloads, scoped per workspace.

Only `scope` is persisted; `search`, `sort`, and other ephemeral
filters intentionally still reset on remount.

Co-authored-by: multica-agent <github@multica.ai>

* fix(views): reset scope to mine when switching to a workspace with no persisted value

zustand persist.rehydrate() is a no-op when storage returns null, so
workspaces with no entry kept the previous workspace's in-memory scope
("all" leaked from one workspace into the next). Provide a custom merge
that resets to the default "mine" when no persisted state is present.

Add coverage for the missing-storage workspace-switch case for both
Agents and Squads.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 14:11:22 +02:00
Jiayuan Zhang
9da52add15 feat(settings): view/edit toggle for repositories tab (MUL-2217) (#2625)
* feat(settings): view/edit toggle for repositories tab

Saved repos render as static rows (truncated, monospace) with hover/focus-revealed
Edit + Delete affordances. Clicking Edit flips to the existing Input; on
successful Save the row returns to display mode. Save button is gated on a
dirty check (URL arrays in order) so a clean state reads as "All changes
saved". Resolves user feedback that the always-visible input made saved
state ambiguous (MUL-2217).

- Track editingIndices with a Set; new rows auto-enter edit mode; deleting
  a row remaps indices so the wrong row never opens.
- Touch devices and focus-within keep the action buttons reachable.
- New i18n keys in en + zh-Hans (saved_hint, empty, edit/delete_aria, url_empty).

Co-authored-by: multica-agent <github@multica.ai>

* fix(settings): add Cancel affordance to exit clean edit mode

Clicking Edit on a clean saved row opened the row in edit mode with
no way back to display mode unless the user changed the URL and saved,
re-introducing the original saved-state ambiguity after an accidental
click. Add a per-row Cancel (X) button visible only in edit mode that:

- reverts the URL to the saved value for existing rows
- removes the row entirely for never-saved (newly added) rows
- exits edit mode without dirtying Save

Action group is always visible (no hover gate) while editing so the
exit is discoverable. Adds en/zh-Hans cancel_aria string and three
regression tests covering clean-cancel, dirty-cancel, and new-row-cancel.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 13:59:26 +02:00
Prateek Bhatnagar
681d720671 fix(issues): file-card render for self-host with local storage (#2349)
* fix(issues): file-card render for self-host with local storage

Fixes #1520. When self-hosting without S3, the upload handler returns
site-relative URLs like /uploads/workspaces/<wsId>/<file>. Four
frontend regexes only matched https?://, so persisted
!file[name](/uploads/...) markdown failed to parse and leaked through
as raw text in the issue view, chat, skill file viewer, and board
card preview.

Narrow allow-list: the relative branch only accepts /uploads/ — not
any /-prefixed href — so protocol-relative //evil.com/x, path-traversal
/../api/x, and other internal /api/... paths are rejected. Without
this, a stored file-card with an attacker-chosen filename and a
//host/x href would turn into a one-click external-site jump via
window.open from inside an issue (per review feedback on #2349).

Single source of truth: packages/ui/markdown/file-cards.ts now exports
isAllowedFileCardHref + FILE_CARD_URL_PATTERN. The four sites use one
of them, so the next regression is cheaper than restoring four parallel
regexes.

- packages/ui/markdown/file-cards.ts: helper + URL pattern.
- packages/views/editor/extensions/file-card.tsx: Tiptap tokenizer
  composes from FILE_CARD_URL_PATTERN.
- packages/views/editor/readonly-content.tsx: sanitiser uses helper.
- packages/ui/markdown/Markdown.tsx: sanitiser uses helper.
- packages/views/issues/components/board-card.tsx: strip markdown
  tokens from the line-clamped board preview so raw !file[...] no
  longer leaks there either.
- packages/ui/markdown/file-cards.test.ts: covers accept (/uploads/ok,
  https://cdn/x) and reject (javascript:, data:, //evil.com/x,
  /../api/x, /api/x, empty, ftp:, bare 'uploads/x') for both the
  helper and the parser composed from the pattern.

javascript:, data:, and other dangerous schemes remain rejected.

* test(markdown): move file-card href allow-list test into @multica/views

Per review feedback on #2349: keep the test where vitest is already
running instead of bootstrapping a new test runner inside @multica/ui.
The test now lives at packages/views/editor/file-card-href.test.ts and
imports isAllowedFileCardHref / FILE_CARD_URL_PATTERN /
preprocessFileCards from the @multica/ui/markdown public surface,
exercising the same 30 cases.

Reverts the @multica/ui package.json test script + vitest devDep + the
local vitest.config.ts that the previous commit added; the package
goes back to typecheck + lint only, matching every other ui-only
package in the monorepo.

---------

Co-authored-by: Lalbadshah <11599756+Lalbadshah@users.noreply.github.com>
2026-05-14 18:32:40 +08:00
Naiyuan Qing
43b9a1173c refactor(agents): drop template chooser from create-agent dialog (#2615)
* refactor(agents): drop template chooser from create-agent dialog

Removes the blank-vs-template chooser, the template picker, and the
template detail step. The "Create agent" entry point now opens directly
on the form. The createAgentFromTemplate API and types remain
untouched — this only removes the UI entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* docs(squads): fix stale comment about createAgentFromTemplate

Squad-scoped create flow no longer goes through the template path;
the dialog now only calls api.createAgent then api.addSquadMember.

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>
2026-05-14 18:05:37 +08:00
Naiyuan Qing
77b929fd3e feat(squads): add agent live peek hover card on member avatars (#2608)
* feat(squads): add agent live peek hover card on member avatars

Squad members tab now opens a live-state peek card on agent avatar
hover/focus — workload, current issue (clickable), and last activity.
Identity (description / runtime / skills / owner) stays on the existing
AgentProfileCard; new AgentLivePeekCard is the second `hoverCardVariant`
on ActorAvatar so the 23+ existing profile-card call sites keep their
behaviour. Reuses the workspace agent-task snapshot already fetched by
the presence dot, so this adds zero new requests per row. Failed
terminal tasks surface as a small ⚠ on the last-activity line without
polluting workload (workload stays current-state only, matching the
deliberate split documented in core/agents/types.ts).

Co-authored-by: multica-agent <github@multica.ai>

* fix(squads): only enable hover card for agent avatars

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 17:30:08 +08:00
Naiyuan Qing
5eb04f73e3 feat(squads): add tooltips and agent detail link to squad member row (#2603)
* feat(squads): add tooltips and agent detail link to squad member row

Replace native title attributes on the make-leader and remove buttons
with proper Tooltip components, and add a new icon button on agent
rows that navigates to the agent detail page. All three tooltips are
localised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(squads): keyboard focus visibility + AppLink for agent detail

- Add group-focus-within:opacity-100 so Tab to the row's hover-only
  action buttons makes the container visible (previously opacity-0
  kept buttons focusable but invisible).
- Replace the agent-detail jump button's onClick+push() with AppLink
  href, restoring middle/Cmd+Click new-tab behavior. Removes the
  now-unused onViewAgent callback chain.

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>
2026-05-14 16:35:15 +08:00
Naiyuan Qing
bc613c08b3 fix(squad): align squad detail tab width with agent detail (#2600)
Drop mx-auto + max-w-2xl wrappers around the Members and Instructions
tab content so the right pane fills the available width like the agent
detail page (TabContent uses flex h-full flex-col p-4 md:p-6).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 16:09:45 +08:00
Naiyuan Qing
2c7738b03a feat(issues): close composer attachment preview loop end-to-end (#2594)
Text/code attachments (markdown, JSON, .ts, .log, …) need an attachment id
to render through `/api/attachments/{id}/content`. The composer pipeline
was dropping that id at the upload-hook boundary, so the Eye preview gate
only fired for media (PDF / video / audio via filename fallback).

- `useFileUpload` now returns the full `Attachment` (with `link` kept as a
  `url` alias) so editor providers can resolve content-type and id.
- New-comment and reply composers hold a `pendingAttachments` state and
  feed it to `ContentEditor`; the active subset (those still referenced in
  the markdown) is sent on submit as before.
- Comment edit modes (CommentRow + CommentCardImpl) merge pending uploads
  with `entry.attachments` for the editor and pipe `attachment_ids` into
  `onEdit` so newly uploaded files actually bind to the comment.
- Issue description editor pushes pending `attachment_ids` on every
  debounced save and invalidates `issueKeys.attachments` so the preview
  Eye survives a refresh.
- `UpdateComment` and `UpdateIssue` handlers accept `attachment_ids` and
  call the existing `linkAttachmentsByIDs` / `linkAttachmentsByIssueIDs`
  helpers; the bind is idempotent so re-sending an existing id is safe.

Closes MUL-2153.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 15:06:21 +08:00
Naiyuan Qing
0c4133ef5b feat(agents): rewrite template catalog as 25 lightweight starters (#2587)
* feat(agents): rewrite template catalog as 25 lightweight starters

Replaces every Phase-1 template with a curated set built around the
"persona + intake + scaffold + hard negatives" instruction shape. Cross-
platform survey (Cursor / Cline / Roo / Continue / Custom GPTs) showed
the industry baseline for starter agents is "few but sharp" — single
intent, no methodology buy-in, mostly prompt-only. The original catalog
went the opposite direction (avg 2.5 skills, six-skill Full-stack
methodology stack) and felt heavy for first-time use.

Catalog shape:

- 25 templates across 7 categories: Engineering (8), Product (4),
  Writing (5), Design (3), Communication (2), Team (1), Productivity (2).
  New Product / Design / Communication / Team domains fill gaps the old
  Eng-heavy catalog ignored.
- 16 / 25 are prompt-only (no skill fan-out). Avg 0.56 skill per template
  vs. 2.5 prior. Heaviest is 2 skills, only for templates whose intent
  cannot be expressed in instructions alone (Playwright runner, single-
  file HTML bundlers, design + UX-guidelines pair).
- Universal top-frequency intents that the old catalog missed are now
  covered: Code Explainer (intent #1 across every platform surveyed),
  Translator (中英), Summarizer, Writing Critic, PRD Drafter/Critic,
  RCA Writer, ADR Writer, PR Description Writer, Commit Message Writer.

Loader allows 0-skill templates:

- server/internal/agenttmpl/loader.go drops the "must declare at least
  one skill" validation; comment explains the picker's "Prompt only"
  rendering path.
- loader_test.go: removed the corresponding negative case, added
  TestLoadFromFS_PromptOnlyTemplate as a regression guard.
- agent_template.go handler is unchanged — every len(tmpl.Skills) call
  site was already 0-safe (empty fan-out short-circuits the fetch phase
  and the in-tx loop both skip cleanly).

Frontend:

- template-picker.tsx: 18 new lucide icons (BookOpen, Bug, GitPullRequest,
  GitCommit, AlertTriangle, Scale, ClipboardList, Microscope, UserRound,
  Target, Highlighter, Languages, AlignLeft, GraduationCap, Lightbulb,
  Type, MessageSquare, Briefcase). Card renders a "Prompt only" badge
  when skills.length === 0 instead of "0 skills".
- template-detail.tsx: skill list section is hidden entirely for prompt-
  only templates — a header reading "Includes 0 skills" above an empty
  list was just visual noise. Instructions section below carries the
  agent's identity for these.
- locales/en + zh-Hans agents.json: new create_dialog.template_card.
  prompt_only key ("Prompt only" / "纯指令").

Verification:

- go test ./internal/agenttmpl/ — 9/9 pass, including
  TestLoad_RealTemplates which fails closed if any new JSON is malformed.
- pnpm typecheck — all 6 packages clean.
- pnpm --filter @multica/views test — 482/482 pass.
- pnpm lint — 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agents): add category filter pills to template picker

25 templates across 7 categories made the picker scroll-heavy on first
open. Add a single-select category filter row above the grid so a PM
can isolate Product templates in one click, an engineer can jump
straight to Engineering, etc.

Visual reuses the IssuesHeader scope-toggle pattern verbatim — Button
variant="outline" + active class swap (bg-accent / text-muted-foreground)
— so the affordance reads the same as the existing filter pills in
issues / squads / runtimes / my-issues. flex-wrap keeps the 8 pills
(All + 7 categories) honest on narrow widths.

Counts are inlined into the label ("Engineering (8)") rather than
shown as a separate badge — single-line-tall pills look right next to
the picker grid, and surfacing the per-category density up front
doubles as a hint at the catalog's "less but sharper" intent.

When a specific category is active, the grid renders flat (no
section headers) — the active pill already names what's on screen,
and a header reading "Engineering" above an only-Engineering grid is
visual duplication. "All" falls back to the prior grouped layout.

State is component-local (no URL sync, no persistence) since the
picker is dialog-internal transient state — closing the dialog
naturally resets the filter, which is the expected behaviour for a
"choose from a catalog" surface.

i18n: new `create_dialog.template_picker.filter_all` key in en + zh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:12:18 +08:00
LinYushen
0cb759b446 fix(squad): suppress no-action leader comments (#2583) 2026-05-14 14:07:26 +08:00
LinYushen
053a37d19c feat: add pinyin search to subscriber popover in issue-detail (#2584)
Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 13:57:46 +08:00
LinYushen
d1c8c213e4 feat: extend pinyin search to all Agent/Member/Squad selectors (#2582)
Integrate matchesPinyin into:
- AssigneePicker (issue assignee selector)
- IssuesHeader (assignee filter bar)
- AgentPicker (autopilot agent selector)
- SquadDetailPage (add member/agent picker)
- QuickCreateIssue (agent/squad picker)
- CreateProject (lead picker)
- ProjectDetail (lead picker)
- ProjectsPage (lead filter)
- AgentsPage (agent search)
- SquadsPage (squad search)

Closes MUL-2179 extended scope.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 13:57:38 +08:00
Bohan Jiang
f15a745182 feat(squads): add Create Agent entry on Squad detail (MUL-2178) (#2579)
Adds a Create Agent button on the Squad detail Members tab, visible
only to workspace owner/admin (matching the AddSquadMember backend
gate). The dialog reuses the existing CreateAgentDialog — both the
manual and template paths now accept an optional squadId; when set,
the dialog runs addSquadMember after createAgent / createAgentFromTemplate
and skips the navigation to the agent detail page so the user lands
back on the Members tab.

Atomicity is best-effort frontend-serial (no new backend transaction):
on partial failure the dialog surfaces a warning toast and the agent
remains addable from the existing Add Member flow.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 13:32:28 +08:00
LinYushen
ca10535bb6 fix: execution log name rendering and squad assignee support (#2575)
* fix: execution log name rendering and squad assignee support

- Strip mention markdown in trigger_summary ([@Name](mention://...) → @Name)
  so execution log rows show clean text instead of raw markdown
- Add squad to ActorFilterValue type so squad assignees are filterable
- Add squad section to assignee filter dropdown in issues-header
- Add i18n keys for squads_group (en/zh-Hans)

Co-authored-by: multica-agent <github@multica.ai>

* fix: address PR #2575 review feedback

1. Extract stripMentionMarkdown as reusable helper with proper regex
   - Handles escaped brackets in names (e.g. David\[TF\])
   - Skips backslash-escaped mentions (\[@...])
   - Handles issue mentions (no @ prefix)
   - Does not touch regular markdown links
   - 10 unit tests added

2. Squad only appears in Assignee filter, not Creator
   - Added showSquads prop to ActorSubContent (default true)
   - Creator filter passes showSquads={false}

3. Squad included in Agents scope
   - issues-page scope filter now includes squad in agents scope
   - 2 regression tests added for scope coverage

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 13:08:05 +08:00
LinYushen
c628958fdd feat: support pinyin search in @mention suggestions (#2572)
* feat: support pinyin search in @mention suggestions

Add pinyin matching for Chinese names in the mention suggestion popup.
Users can now search by:
- Full pinyin: 'liyunlong' matches '李云龙'
- Initial letters: 'lyl' matches '李云龙'
- Partial/hybrid: 'liyu' or 'liyunl' matches '李云龙'

Implementation:
- New pinyin-match.ts utility using pinyin-pro library
- Integrated into member, agent, and squad filters in mention-suggestion.tsx
- 21 tests passing (9 unit + 12 integration)

Co-authored-by: multica-agent <github@multica.ai>

* fix: normalize ü→v in pinyin matching for names like 吕布

Enable pinyin-pro's v:true option so 吕→lv instead of lü.
Add test case for 吕布/lvbu matching.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-14 12:44:43 +08:00
LinYushen
9256743549 fix(mention): prefetch squads so @mention list shows all squads
Closes MUL-2176
2026-05-14 11:52:13 +08:00
Naiyuan Qing
c49c778613 fix(editor): align Preview gate with Download — survive URL-only sources (#2566)
The Eye button required a fully resolved Attachment record (URL-lookup
via `resolveAttachment(href)`) before showing. Download only required
the URL, falling back to `openExternal(href)` when the lookup missed.
Result: any case where the URL in markdown couldn't be reverse-matched
to the entity's `attachments` prop (cross-comment copy-paste, stale
caches) silently hid the Preview button while Download kept working —
edit and readonly surfaces diverged for the same content.

Widen the Preview gate to mirror Download: show the Eye whenever the
filename indicates a previewable type. Introduce a `PreviewSource`
tagged union — `{ kind: "full", attachment }` for the existing path,
`{ kind: "url", url, filename }` for the fallback. Media kinds
(pdf/video/audio) render directly from the URL; text kinds still
require an attachment id because the /content proxy is ID-keyed, so
`tryOpen` rejects URL+text combinations and PreviewContent has a
defensive fallback for direct mounts.

Side effects:
- `getPreviewKind` gains filename-extension fallbacks for video/audio
  (was PDF-only); without these the URL-only path can't infer kind
  when content_type is empty.
- AttachmentList in comment-card.tsx unchanged behaviorally — only the
  tryOpen call site is updated to the new signature.

Pre-existing architectural issues (AttachmentList readonly-only,
URL-based attachment lookup, per-entity ownership) are intentionally
out of scope.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:33:48 +08:00
Naiyuan Qing
52d032335a feat(agents): expose runtime + model on create-from-template (#2565)
Template create used to silently default the runtime to "first usable"
and never collected a model — users had no idea where the new agent
would run or which model it would use until they opened the detail
page. Add a Runtime + Model picker pair above the skill list on the
template-detail step so the choice is visible (and overridable) before
the one-click Use action.

- Extract RuntimePicker out of create-agent-dialog so the form and the
  template-detail step share one popover; selection seeding moves into
  the picker too, since it's the only place that knows the active
  filter (mine/all). Parent keeps just the duplicate-mode pre-fill.
- Mirror RuntimePicker's label-row + trigger DOM in ModelDropdown so
  the two pickers render at identical heights when sat side-by-side
  (fixes a 6-8px misalignment caused by inconsistent label-row sizing).
- Send model in createAgentFromTemplate; server side already accepts
  the field (CreateAgentFromTemplateRequest.Model, omitempty), empty
  string still falls through to the runtime's default model.
- Drop the runtime_register_first fallback hint that made the Runtime
  trigger two-line in the empty state, breaking alignment with Model's
  one-line trigger.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:33:39 +08:00
Bohan Jiang
0345285b86 feat(quick-create): searchable actor picker + squad support (#2552)
* feat(quick-create): searchable actor picker + squad support (MUL-2163)

- Replaces the flat agent dropdown in the "Create with agent" modal with a
  searchable PropertyPicker that lists Agents and Squads in separate
  sections, so users can filter by name and pick a squad as the creator.
- Persists the selection as (lastActorType, lastActorId), removing the
  agent-only lastAgentId field on the quick-create store.
- Adds squad_id to the quick-create API request and stamps it onto the
  task's QuickCreateContext. The handler resolves the squad to its leader
  agent (re-using validateAssigneePair) and the daemon claim path injects
  the squad-leader briefing when the task carries a squad hint, matching
  the behavior of issue-bound squad tasks.

Co-authored-by: multica-agent <github@multica.ai>

* fix(create-issue): forward squad picks across manual→agent switch

Manual mode → agent mode previously only carried `agent_id`, so picking
a squad and then flipping to agent silently fell back to the persisted
actor / first visible agent and lost the user's choice. Carry `squad_id`
on the same branch so the agent panel honors the squad pick.

Adds a sibling test alongside the existing project-carry case.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 22:31:17 +08:00
DimaS
efddb2284b fix(issues): clean caches after issue delete (#2487)
* fix(issues): clean caches after issue delete

* fix(issues): restore partial batch delete snapshots
2026-05-13 22:30:16 +08:00
Jiayuan Zhang
7e20ca27bb fix(issues): unify assignee menu with shared AssigneePicker (MUL-2157) (#2543)
* fix(issues): unify assignee menu with shared AssigneePicker (MUL-2157)

The Assignee submenu inside IssueActionsMenuItems was a parallel
implementation: no search, no squads, no agent permission check, no
archive filter, no frequency sort. The divergence was most visible from
the Inbox (where the issue detail's sidebar starts collapsed, so users
reach for the 3-dot menu).

Replace the submenu with a single menu item that closes the
surrounding dropdown / context menu and hands off to the shared
AssigneePicker popover — same component already used in the issue
detail sidebar, board cards, batch toolbar, and create-issue modal.

The picker is conditionally mounted to avoid every row in list / board
views subscribing to the members / agents / squads / frequency queries
on mount.

Co-authored-by: multica-agent <github@multica.ai>

* test(issues): mock squadListOptions + add Assignee picker handoff test

`AssigneePicker` reads `squadListOptions` and `assigneeFrequencyOptions`
from `@multica/core/workspace/queries`. Tests that render IssueDetail
or IssueActionsDropdown without those mocks throw at the picker's
useQuery call and cascade into unrelated assertion failures — this is
what was leaving the `@multica/views` test job red on the MUL-2157 PR.

Add the missing mocks. Add a regression test that clicks the Assignee
menu item and asserts the shared picker (search input + Members group)
takes over, so a future regression to the parallel-implementation bug
this PR fixes fails loudly instead of silently.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 22:11:18 +08:00
Bohan Jiang
4c1bef2e1f feat(usage): mirror Tokens metric toggle onto Usage page Daily chart (MUL-2148) (#2540)
* feat(usage): mirror Tokens metric toggle onto Usage page Daily chart (MUL-2148)

#2537 added the Cost/Tokens metric toggle to the Daily chart inside the
runtime-detail Usage section (packages/views/runtimes/components/
usage-section.tsx). The workspace-level Usage page at /{slug}/usage
imports the same DailyCostChart primitive but renders it from
dashboard-page.tsx without any toggle wrapper, so #2537 only landed on
half of the surface that says "Daily cost".

This PR mirrors the same pattern to dashboard-page.tsx so users see
the toggle wherever a "Daily" chart appears.

Changes
- `packages/views/dashboard/utils.ts`: new `aggregateDailyTokens` helper
  that folds DashboardUsageDaily[] into the same DailyTokenData[] shape
  the DailyTokensChart consumes (mirrors aggregateByDate's dailyTokens
  branch from the runtimes side, adapted to DashboardUsageDaily field
  names).
- `packages/views/dashboard/components/dashboard-page.tsx`: rename
  `DailyCostBlock` → `DailyTrendBlock`, add a Cost/Tokens Segmented
  next to the section title, switch chart and title based on the
  active metric, per-metric empty-state (so a workspace with unmapped
  pricing but recorded tokens still gets a real Tokens chart while
  the Cost view falls through to the empty-state — same convention as
  DailyTab in usage-section.tsx).
- usage.json (en + zh-Hans): split `daily.title` into `title_cost` +
  `title_tokens`, add `metric_cost` + `metric_tokens` toggle labels.

* feat(usage): default Daily chart to Tokens metric

Most users land on /{slug}/usage to gauge "how much agent work
happened" rather than "how much was spent." Tokens is the more
universally meaningful axis on first read (Cost depends on having
pricing mapped for every model and on whether the workspace has
unmaintained models). Cost stays one click away via the same toggle.

Also reorder the Segmented so Tokens sits first, matching the new
default.
2026-05-13 22:07:47 +08:00
Jiayuan Zhang
291c2c7898 feat(usage): reuse runtime timezone picker on the usage page (#2533) (#2546)
* feat(usage): add timezone picker to usage page (#2533)

Extracts the runtime detail page's timezone dropdown into a shared
TimezoneSelect at packages/views/common/timezone-select.tsx and reuses
it in the usage page header, immediately to the right of the 7d / 30d
/ 90d segmented control. Defaults to the browser-resolved zone with
the same "(browser)" suffix rendering as the runtime page.

The runtime-detail TimezoneEditor still owns the PATCH mutation; only
the dropdown UI moved. UI-only — no API client / handler changes.

Co-authored-by: multica-agent <github@multica.ai>

* fix(usage): make header wrap so timezone picker fits on narrow widths

The h-12 PageHeader is a single non-wrapping flex row. Adding the
timezone picker with a 180px min-width pushed the title + project
filter + range switch + tz select past the viewport on narrow and
medium widths. Drop the picker's hard min-width, let the header grow
vertically (h-auto + min-h-12) and let the right toolbar wrap. Wide
viewports still render the original single row.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 15:58:53 +02:00