Two fixes for Emacs Round 2 findings:
1. Desktop TerminalPanel now sends `{type:"auth", payload:{token}}` as
the first WS frame in token mode and waits for `auth_ack`. Previously
it transitioned straight to "connected" without authing, so the
server's 10s first-frame deadline closed the connection and the
terminal never opened. Cookie-mode apps still skip the handshake.
Exposes ApiClient.getToken() so the panel can snapshot the bearer
without reaching into auth-store internals.
2. terminalBridge.handleOpen no longer treats `terminal.opened` as a
droppable control frame. sendFrame now returns bool; if the ack
can't be enqueued the bridge rolls the half-open session back
(cancel ctx, drop the route, sess.Close) so the spawned PTY doesn't
orphan until the idle sweep — which also pinned the GC active-env
mark for that window.
Regression coverage:
- TestTerminalBridge_OpenedEnqueueFailureClosesSession: drop-only
sender, spawn must happen exactly once, manager and bridge route map
must drain to zero.
- packages/views/issues/components/terminal-panel.test.tsx: asserts
the auth frame is sent in token mode and NOT sent in cookie mode.
Co-authored-by: multica-agent <github@multica.ai>
Wires the Phase 1 terminal.Manager into the live daemonws transport so a
browser tab on the Multica Desktop app can attach to a PTY running in
the daemon's task workdir. End-to-end frame path:
Desktop (xterm.js) → /ws/issues/{id}/terminal (server proxy, cookie
or first-frame JWT auth, workspace membership
enforced before upgrade)
→ daemonws hub (new SendToRuntime + TerminalRouter routing
terminal.* frames back to the proxy by
request_id, then re-keyed on session_id)
→ daemon (new terminal_bridge.go owns one Manager per WS
connection, drains PtySession.Output() into
terminal.data frames, surfaces ExitC as
terminal.exit; closeAll on WS disconnect)
→ terminal.Manager.OpenWithInfo (new method) — server resolves
task.work_dir/issue_id/prior_session_id from its DB and embeds
them on the protocol; daemon trusts the server payload, no
daemon-local task cache needed.
Auth + ACL:
- Browser proxy enforces workspace membership before the upgrade.
- TerminalOpenPayload carries the resolved task info; cross-workspace
is structurally impossible at the bridge layer because both
OpenParams.WorkspaceID and TaskInfo.WorkspaceID come from the same
server-resolved field.
- terminal_bridge maps terminal.{Manager.OpenWithInfo} errors to the
protocol error codes (workspace_mismatch / task_not_found /
unsupported_os / spawn_failed / internal).
Resume:
- Server passes prior_session_id; daemon injects CLAUDE_SESSION_ID +
MULTICA_{WORKSPACE,ISSUE,TASK,USER}_ID into the PTY env per the RFC.
`claude --resume \$CLAUDE_SESSION_ID` continues the agent's session.
Lifecycle:
- Daemon installs a fresh terminalBridge per daemonws connection and
tears every PtySession down on disconnect; session_ids minted on one
WS cannot be reused on a reconnect because the server-side routing
registration is also gone.
- daemonws client.send buffer raised from 16 to 256 and read limit
from 4KB to 64KB so PTY traffic fits without evicting connections
used for heartbeat / wakeup hints.
Desktop UI:
- packages/views/issues/components/terminal-panel.tsx renders an
xterm.js console with FitAddon, base64 wire encoding, ResizeObserver
→ terminal.resize, reconnect button, and a clear web-only placeholder
when window.desktopAPI is absent.
- TerminalPanelSection wrapper hangs collapsed in the issue-detail
sidebar next to the execution log so bootstrap doesn't run for
every issue view.
Tests:
- terminal/manager_test.go: OpenWithInfo happy path + cross-workspace
reject (16 tests total, all -race clean).
- daemonws/terminal_test.go: TerminalRouter request_id → session_id
re-keying and unknown-session drop.
- daemon/terminal_bridge_test.go: server-supplied workdir round-trips
through OpenWithInfo, missing-workdir surfaces task_not_found and
never spawns, data+exit round trip.
- GOOS=windows go test -c clean for both daemon and daemon/terminal.
Phase 1 / 2 / 3 / 4 mapping unchanged: Phase 3 (CLI) and Phase 4
(execenv GC hook + issue runs entry + audit log) are still untouched.
Co-authored-by: multica-agent <github@multica.ai>
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>
* 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>
* 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>
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>
* 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>
* 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>
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>
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>
* 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>
* 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.
* 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>
* 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>
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>
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>
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>
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>
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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
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>
* 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>
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>
* 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>
* 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>
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>
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>
* 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>
* 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>
* 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.
* 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>
#2505 (Squad MVP) merged with 29 hardcoded English strings in JSX text
nodes — packages/views/squads/components/squads-page.tsx (4) and
squad-detail-page.tsx (25). The package's eslint config enforces
`i18next/no-literal-string` as ERROR for every .tsx file, so
@multica/views#lint has been red on main, which Turbo cascades to
@multica/web#build, @multica/desktop#build, and @multica/views#typecheck
— effectively blocking every open PR's frontend CI (#2538, #2540, etc.).
Rather than disabling the rule for the Squad files (which would just
hide debt in a high-visibility surface), wire up a proper i18n
namespace and replace every flagged literal.
Namespace plumbing
- New `packages/views/locales/en/squads.json` and
`packages/views/locales/zh-Hans/squads.json` covering all 29 flagged
strings, grouped by surface (page / inspector / name_editor /
add_member_dialog / description_dialog / discard_changes_dialog /
members_tab / instructions_tab).
- Registered in `packages/views/locales/index.ts` and
`packages/views/i18n/resources-types.ts` so `t($ => $.squads.*)` is
type-safe.
Component replacements
- `squads-page.tsx`: add `useT("squads")`, replace 4 literals.
- `squad-detail-page.tsx`: add `useT("squads")` to seven inner
components that hold flagged text (`SquadDetailPage` / `InlineEdit
Popover` / `AddMemberDialog` / `RoleEditor` / `SquadDescriptionEditor`
/ `SquadDescriptionEditorBody` / `SquadOverviewPane` / `SquadMembers
Tab` / `SquadInstructionsTab` / `SquadDetailInspector`), replace all
flagged literals.
- Plural members count uses i18next's standard `_one` / `_other`
suffixes via `t(..., { count })` — matches the convention already
used in `runtimes/usage` and `agents`.
Notes
- A few unflagged user-facing strings remain (tab labels in
squadDetailTabs array, ternary alternatives like `"Save"` inside
`{x ? <Loader/> : "Save"}`, the inline `confirm()` archive prompt,
the `toast.success("Leader updated")` message). The eslint rule
uses `mode: "jsx-text-only"` so it only flags string children of
JSX nodes; attribute strings, object-literal values, and ternary
alternatives slip past. Those are real i18n gaps too but expanding
scope here would gold-plate the CI-unblock fix.
Verification
- `pnpm --filter @multica/views lint`: 0 errors (was 29). Remaining 13
warnings are pre-existing in unrelated files and don't fail CI.
- `pnpm typecheck`: 6/6 packages pass — namespace types resolve, all
selector calls infer correctly.