* 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>
Adds a dedicated bilingual /docs/squads page covering the squad model
(leader + members), assignment, comment trigger rules, archive
semantics, and the squad CLI surface. Wires the new page into
meta.json and meta.zh.json under the Agents section, and adds
short cross-references from agents, assigning-issues,
mentioning-agents, and the CLI reference so users can discover
squads from the pages they're already on.
MUL-2206
Co-authored-by: multica-agent <github@multica.ai>
When the user opens quick-create with a squad selected, the task is
enqueued against the squad's leader agent — but the squad, not the
leader, is the expected owner. The prompt previously instructed the
leader to "default to YOURSELF" using its own agent UUID, hiding new
issues from the squad's delegation flow.
Surface the squad's id + name on the claim response and branch the
default-assignee instruction in buildQuickCreatePrompt: when SquadID is
present, point --assignee-id at the squad UUID and explicitly forbid
self-assignment.
MUL-2203
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>
Exposes the existing /api/tasks/{id}/cancel backend endpoint as a CLI
command. Combined with upstream #2107 (cancel running agent on
server-side task delete), this gives operators a way to interrupt a
runaway agent push-storm without resorting to admin-bypass on the
downstream PR.
Use cases:
- Titan / DevBot iterating beyond its boundary (e.g. push-skip loops)
- Codex turn that locked in tool-call spam
- Manual recovery when a long-running task needs to stop NOW
Symmetric with 'issue rerun': accepts the short ID prefix shown by
'issue runs', supports --issue scoping, and reuses resolveTaskRunID
for ambiguity handling.
Refs: PR#19 octo-server post-mortem (2026-05-13)
Co-authored-by: yujiawei <yujiawei@mininglamp.com>
* 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>
* fix: trigger squad leader agent run when squad is @mentioned in comment
Previously, enqueueMentionedAgentTasks only processed m.Type == "agent"
mentions, skipping squad mentions entirely. The shouldEnqueueSquadLeaderOnComment
path only fires when the issue is already assigned to a squad.
This adds handling for m.Type == "squad" in enqueueMentionedAgentTasks:
when a squad is @mentioned, look up the squad's leader agent and enqueue
a task for them (with the same dedup/self-trigger/archived guards as
direct agent mentions).
Co-authored-by: multica-agent <github@multica.ai>
* fix: add canAccessPrivateAgent gate to squad mention branch
Closes the P1 permission vulnerability where a plain workspace member
could trigger a private squad leader by @mentioning the squad, bypassing
the private-agent access check that the direct @agent mention path
enforces.
Adds regression test TestCreateComment_SquadMentionPrivateLeaderBlocksPlainMember.
Co-authored-by: multica-agent <github@multica.ai>
---------
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>
The per-turn prompt in buildCommentPrompt() only injected the squad
leader no_action prohibition inside the 'if TriggerAuthorType == agent'
block. When a member (human) posted a comment like 'LGTM', the squad
leader was triggered but the per-turn prompt did NOT include the
prohibition, causing the model to post noise comments like 'LGTM is a
pure acknowledgment — no reply needed. Exiting silently.'
Fix: move the squad leader no_action rule outside the agent-only block
so it fires for ALL trigger types (agent and member).
Fixes: MUL-2168
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>
PR #2564 only added IsSquadLeader handling to the assignment-triggered
workflow path and the Output section. When a squad leader is triggered by
a comment (the common case for re-evaluation), the comment-triggered
workflow path had NO squad leader special handling, so the model still
posted comments announcing no_action/silence.
Changes:
- runtime_config.go: Add IsSquadLeader check to comment-triggered step 4
with explicit prohibition against posting no_action announcement comments
- runtime_config.go: Strengthen Output section from 'may exit silently' to
'MUST exit without posting any comment' with explicit DO NOT examples
- runtime_config.go: Strengthen assignment-triggered step 5 similarly
- prompt.go: Add squad leader no_action rule to per-turn comment prompt
when trigger author is an agent and agent instructions contain the
Squad Operating Protocol marker
- Add tests for both the per-turn prompt and CLAUDE.md generation
Fixes MUL-2168
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): skip leader on comment when a member @mentions any agent (MUL-2170)
When a human commenter routes an issue directly at a specific agent via
[@Name](mention://agent/<id>), the squad leader was still being woken up
to evaluate the same comment. The leader's only real options were to
re-delegate to the agent the member already named or to record
no_action — both of which produce queue noise without changing the
outcome.
This skips the leader-enqueue path entirely when:
- the assignee is a squad,
- the comment author is a member, AND
- the comment body contains at least one agent mention.
Agent-authored comments are intentionally exempt: when an agent posts
an update that @mentions another agent, the leader still needs to
coordinate the thread. The existing leader-self-trigger guard is
preserved. Only the current comment's body is inspected — parent
(thread root) mentions are not inherited here.
Tests cover the helper (mentions parsing) plus the integration matrix:
member plain / member @member / member @non-leader-agent /
member @leader / agent @agent / leader-self.
Co-authored-by: multica-agent <github@multica.ai>
* test(squad): exercise full CreateComment path for leader-skip rule (MUL-2170)
Adds an integration test that drives the HTTP-layer CreateComment handler
(not just the helper) to lock the call-site wiring: a member top-level
comment with an @agent skips the squad leader, and a subsequent plain
reply in the same thread DOES wake the leader — the parent's @agent
mention must not be inherited into the leader-skip decision.
Picks up a non-blocking review note on PR #2569.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squad): skip leader on any explicit member mention, not only @agent (MUL-2170)
Broaden the leader-skip rule for squad-assigned issues: a member comment
that explicitly @mentions anyone — @agent, @member, @squad, or @all —
counts as deliberate routing and the squad leader stays out. Issue
cross-references (mention://issue/...) are not routing and still trigger
the leader as before.
Per Bohan's follow-up on MUL-2170 — @member should suppress the leader
for the same reason @agent does: the human has already pointed at a
specific recipient, so a leader turn would just be observation noise.
Helper renamed commentMentionsAnyAgent → commentMentionsAnyone with
explicit handling of all four routing mention types. Existing call-site
wiring (current-comment-only, agent-author exemption, leader self-trigger
guard) is unchanged.
Tests updated and extended to cover the full routing matrix:
@member / @squad / @all / @issue (cross-ref) plus the @agent variants
already covered.
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>
The runtime prompt's Output section unconditionally required all tasks to
post a comment via 'multica issue comment add', which conflicted with the
squad leader protocol that says to 'exit silently' on no_action.
Changes:
- Add IsSquadLeader bool to TaskContextForEnv (detected via Squad Operating
Protocol marker in agent instructions)
- Relax the Output section and assignment-triggered workflow step 5 to
allow squad leaders to exit with only a 'multica squad activity' call
when the outcome is no_action
Fixes MUL-2168
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): resolve squad assignees in issue create/update/assign (MUL-2165)
The CLI assignee resolver only searched workspace members and agents, so a
quick-create input like "assign to <SquadName>" silently fell through to
"Unrecognized assignee: <SquadName>" in the issue description — even though
squads are first-class assignees server-side and the prompt's whole point was
to route the work for the user.
Extend resolveAssignee / resolveAssigneeByID to also fetch /api/squads, teach
the actor display lookup to render squad names in table output, update the
quick-create prompt and runtime-config command listing to mention
`multica squad list` alongside members and agents, and lock in the new
behavior with tests.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): gate squad assignee resolution behind an allowed-kinds set (MUL-2165)
The earlier MUL-2165 fix taught resolveAssignee / resolveAssigneeByID to also
return (squad, ...), but those helpers are shared. Project lead and issue
subscriber callers were still using them, and their target schemas reject
squads — project.lead_type has a DB CHECK constraint
(server/migrations/034_projects.up.sql:10) and the subscriber handler's
isWorkspaceEntity switch only knows member/agent
(server/internal/handler/handler.go:414). So
`multica project create --lead "<SquadName>"` and
`multica issue subscriber add --user "<SquadName>"` would resolve to
(squad, ...) and surface as a 500/403 server-side instead of a clean
CLI-side resolution error.
Thread an assigneeKinds set through the resolver and the pickAssigneeFromFlags
helper. Issue create/update/assign/list pass `issueAssigneeKinds` (all three);
project lead and subscriber pass `memberOrAgentKinds`. The squads fetch is
skipped entirely when not allowed, and the not-found / no-match error wording
adapts to the allowed kinds so it never mentions a type the caller cannot use.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* 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.
* feat(sidebar): top/bottom scroll fade mask (MUL-2150)
Apply useScrollFade to SidebarContent so the menu list softly fades
into the header / footer when overflowing, matching the existing
pattern used in chat list and onboarding steps.
Co-authored-by: multica-agent <github@multica.ai>
* fix(ui): useScrollFade re-evaluates on content mutations
ResizeObserver only fires on the observed element's own box. When a
flex / auto-height container's children grow asynchronously (sidebar
pinned items loading from TanStack Query, collapsibles expanding),
scrollHeight changes but clientHeight does not — mask stayed 'none'
until the user scrolled. Add a MutationObserver on childList to
recompute fade when content is inserted or removed.
Co-authored-by: multica-agent <github@multica.ai>
* test(paths): include squads in workspace route consistency check
main added the squads parameterless route to paths.workspace() in #2505
but the C4 consistency assertion wasn't updated, turning frontend CI
red on every PR. Add 'squads' to both the parameterless-method set and
the segment-mapping table.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
#2505 (Squad MVP) added paths.workspace(slug).squads() / squadDetail()
to paths.ts but didn't update paths/consistency.test.ts, whose first
test enumerates ALL parameterless workspace route methods and compares
the actual Set to an explicit expected Set. Squads landed on main, the
test started flagging the unexpected extra entry, and the @multica/core
test job has been red since 29082f7c.
Add "squads" to both:
- the expected-routes Set in `exposes the expected parameterless
workspace route methods` (the test that was failing)
- the expected-segments array in `each parameterless route emits
/{slug}/{segment}` (was silently skipping squads, now covered)
Also extend paths.test.ts with `ws.squads()` / `ws.squadDetail("sq_1")`
expectations so the per-route smoke test mirrors the rest of the
parameterless routes.
No source changes — only test files. The squad routes themselves
already exist on main and match the test's expectations.
The runtime Usage page's Daily timeline only showed daily $ cost, which
hides the underlying usage shape: cost varies wildly by model price, so
a quiet day on Opus can outspend a busy day on Haiku. Add a Cost/Tokens
toggle next to the Daily/Hourly/Heatmap tabs that swaps the chart over
to a four-segment stack of raw token counts (input / output / cache
read / cache write).
No backend changes needed — the existing /api/runtimes/{id}/usage
response already carries the per-day per-model token breakdown; this
just wires up DailyTokensChart on top of the dailyTokens aggregate that
aggregateByDate was already producing.
Co-authored-by: multica-agent <github@multica.ai>
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(agents): three-phase agent quick-create plan
Captures the full design for moving agent creation from manual form +
one-by-one skill attachment to a tiered experience:
- Phase 1 (this PR): one-click curated templates, AI-free.
- Phase 2 (next): AI-recommended skills via the existing quick-create
task mechanism — no new server-side LLM dependency.
- Phase 3 (later): AI creates the whole agent end-to-end, composing
Phase 2 with a new `multica agent create` CLI driver.
Documents the architectural decisions that keep all three phases on
existing infrastructure (no SSE, no server-side LLM SDK, no new WS
channels), the two soft blockers Phase 1 unlocks for later phases
(createSkillWithFiles TX composability + skill same-name dedupe), and
the scope decisions we explicitly opted out of (Anthropic plugin
marketplace, ClawHub UI affordances).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(skills): harden import against invalid UTF-8 and binary files
PG rejects two byte patterns in a TEXT column. Both crashed real skill
imports we hit while assembling the template catalog:
- Embedded NUL (0x00) -> SQLSTATE 22021. Already stripped by
sanitizeNullBytes, kept as-is.
- Other invalid UTF-8 (e.g. 0x91 — Windows-1252 smart quote in a skill
whose author saved prose from Word). sanitizeNullBytes now also runs
strings.ToValidUTF8 over the content so the second class no longer
takes the whole import down.
For non-text payloads (images, fonts, archives, compiled binaries),
sanitization isn't the right fix — agents never read those as text,
and the bytes can't survive a TEXT column at all. addFile now skips
them by extension before the per-bundle cap counters tick, logging
the skip so an unexpected drop leaves a breadcrumb.
Function name kept for compatibility with the many call sites; both
behaviours are strict supersets of the original.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): split createSkillWithFiles for tx composition + add workspace find-or-create query
Two soft blockers cleared so create-from-template (next commit) can
fold N skill creates and the agent + binding writes into one outer
transaction:
1. createSkillWithFiles used to Begin/Commit its own tx. Caller
composition was impossible — N invocations meant N separate
transactions and no atomicity over the whole materialise step.
Pull the body into createSkillWithFilesInTx(ctx, qtx, input); the
original function becomes a thin wrapper that manages its own tx
for standalone callers. Existing call sites: zero behaviour change.
2. Add GetSkillByWorkspaceAndName sqlc query — workspace skill lookup
by name, anchored to UNIQUE(workspace_id, name) from migration
008. Lets the template materialiser implement find-or-create:
reuse the workspace's existing skill row when a template
references the same name, rather than crashing on the unique
constraint or polluting the workspace with `<name>-2` clones.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): agent template catalog + create-from-template endpoint
Server-side foundation for Phase 1 of the quick-create roadmap (see
docs/agent-quick-create-plan.md). Adds:
- server/internal/agenttmpl/ — embed-loaded catalog of curated agent
templates. Each template ships pre-written instructions plus a list
of skill URLs that get materialised into the workspace at create
time. Validation runs at startup (init() panics on a malformed
template) so a bad JSON ships as a deploy-time defect, not a
runtime 500. Slug must equal the filename basename so the URL
router is mirror-symmetric with the file layout.
- 11 starter templates covering Engineering / Writing / Building /
Testing (code-reviewer, frontend-builder, planner, docs-writer,
one-pager, html-slides, full-stack-engineer, …).
- Three new endpoints, all behind RequireWorkspaceMember:
GET /api/agent-templates — picker list (no instructions)
GET /api/agent-templates/:slug — detail with instructions
POST /api/agents/from-template — materialise + create
Create flow:
1. Auth + runtime authorization happen BEFORE the GitHub fan-out
so a 403 never wastes 20s of upstream fetches.
2. Pre-flight dedupe by cached_name reuses workspace skills
without an HTTP fetch — second create-from-the-same-template
drops from 20s to <100ms.
3. Parallel fetch (30s per-URL timeout) for the remaining skills.
4. Single transaction: every skill insert, the agent insert, and
the agent_skill bindings. On any upstream fetch failure the TX
rolls back and the API returns 422 with `failed_urls` so the
UI can name the bad source(s).
5. extra_skill_ids (user-supplied additions) are verified through
GetSkillInWorkspace per id before attach, so a malicious client
can't graft a skill from another workspace via UUID guessing.
- multica agent create --from-template <slug> CLI flag dispatches to
the new endpoint with a 60s ceiling, matching `multica skill import`.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): one-click create-from-template UI
Frontend half of Phase 1. CreateAgentDialog becomes a state machine
spanning four steps:
chooser → Start blank / From template cards
blank-form → existing manual form (post-chooser)
duplicate-form → existing form pre-filled from a duplicated agent
template-picker → grid of templates, click navigates to detail
template-detail → instructions + skill list preview + one-click Use
Picking a template never lands on the form: name auto-deduped against
existingAgentNames, runtime = first usable one, visibility = private.
Refinement happens on the agent detail page if needed. Same rationale
the doc spells out — templates exist precisely to skip configuration.
New components, all collapsible-by-default so quick-create stays fast:
- template-picker.tsx — categorised grid, lucide icons + semantic
accent tokens resolved through static maps so Tailwind's JIT picks
up every variant (dynamic class strings would silently miss).
- template-detail.tsx — instructions preview, skill list with cached
descriptions, Use CTA. Renders the failedURLs banner when a 422
fires — the only step that can trigger that response.
- instructions-editor.tsx — collapsed preview-card / expanded full
ContentEditor.
- skill-multi-select.tsx + skill-picker-list.tsx — shared multi-
select surface, also adopted by the existing skill-add-dialog.
- avatar-picker.tsx — agent avatar upload, mirrors the inspector's
visual language.
Schema-defended client (CLAUDE.md → API Response Compatibility): the
three new endpoints are wired through parseWithFallback with lenient
zod schemas. Desktop builds outlive any given server — a future
field rename / wrapping must not white-screen older installs.
listAgentTemplates accepts both the current bare array and a future
{templates: [...]} envelope. Coverage: 7 new schema-test cases in
schema.test.ts (null body, missing skills/instructions, malformed
create response, envelope migration).
Catalog + detail go through TanStack Query with staleTime: Infinity —
workspace-independent static data, no per-mount refetch.
Other:
- skill-add-dialog becomes a true multi-select (Confirm button +
checkbox list); attached skills are filtered out of the list.
- agents-page hands the freshly-created Agent back to the dialog so a
follow-up setAgentSkills can attach the form-selected skills.
- agent-overview-pane drops the mx-auto/max-w-2xl frame on config-
tab content; the wider dialog visual language reads better with
tabs filling the column.
- Every new UI string lives in both en/agents.json and
zh-Hans/agents.json under create_dialog.* / tab_body.skills.* —
locales/parity.test.ts blocks drift in CI.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): align skill import test + drop next-only lint suppression
- TestFetchFromSkillsSh_ResolvesRootLevelSkillMd now expects assets/logo.png
to be skipped; matches the new addFile binary-extension guard
(6fafd86e). The .png is intentionally dropped so PG TEXT inserts don't
hit SQLSTATE 22021.
- packages/views shares zero next/* deps, so the @next/next/no-img-element
eslint plugin isn't loaded there. The eslint-disable directive
referencing it produced a hard "rule not found" error in CI lint. Raw
<img> is the right primitive in views; remove the disable comment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(agents): wrap CreateAgentDialog tests in workspace/navigation providers
The dialog now calls useNavigation() and useWorkspacePaths(), both of
which throw outside their providers. The existing tests rendered the
dialog bare and tripped both new requirements:
- NavigationProvider — supply a stub adapter so push() works for the
agent-detail redirect.
- WorkspaceSlugProvider — useWorkspacePaths() requires a slug.
The blank-vs-template chooser is now the default first step; the
existing tests target the runtime picker on the manual form, so the
helper auto-clicks "Start blank" when no template is passed
(duplicate-mode tests skip the chooser).
Manual afterEach(cleanup) + document.body wipe. Base UI's Dialog
portal renders into document.body and leaves focus-guard/inert wrapper
divs behind across tests, so the second test in the suite saw two
"All" / "My Runtime" matches and getByText failed. The wipe is local
to this file rather than the shared setup because it isn't a global
issue — only suites that open Base UI dialogs hit it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
The four user-visible strings exposed by packages/ui rendered untranslated
on every page that used them:
- file-upload-button.tsx — "Attach file" aria-label/title
- sidebar.tsx — "Toggle Sidebar" sr-only label/aria-label/title
- pagination.tsx — "Go to previous/next page" aria-labels
- CodeBlock.tsx — "plain text" language fallback + "Copy code" aria-label/tooltip
Root cause: the package had no i18n hookup at all because the package
boundary rule forbids importing @multica/core. Replicating the pattern
five times would have been the same hack five times. Hooking up
react-i18next directly is the structurally clean fix — i18next is a
generic library, not business logic, and the upstream I18nextProvider
already exposes the instance via context.
To let packages/ui typecheck the selector form standalone (i.e. without
the views resource-types augmentation in scope), the augmentation is
split: views declares everything except the `ui` namespace on a new
global `I18nResources` interface, and packages/ui contributes the `ui`
slice via declaration merging in packages/ui/types/i18next.ts. Views'
resources-types side-effect-imports that file so both packages see the
merged shape during downstream typechecks.
Scope intentionally excludes:
- packages/ui/components/common/error-boundary.tsx — keeping its fallback
in English so a render-time crash never depends on i18n being healthy.
- apps/desktop/src/renderer/src/components/update-notification.tsx —
ships with the next desktop release, not via this PR.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(storage): add GetReader to Storage interface
Adds a streaming read method to the Storage abstraction so callers can
pull object bytes without forcing a full in-memory load. S3Storage wraps
GetObject; LocalStorage opens the file with path-traversal and sidecar
guards. Tests cover happy path, traversal rejection, sidecar rejection,
and missing key.
Used in the next commit by the attachment-preview proxy endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(server): add attachment preview proxy endpoint
GET /api/attachments/{id}/content streams the raw bytes of a
text-previewable attachment back to the client. Exists to (a) bypass
CloudFront CORS, which is not configured on the CDN, and (b) bypass
Content-Disposition: attachment which Chromium honors for iframe document
loads. Media types (image/video/audio/pdf) intentionally do NOT go through
this endpoint — clients render them directly from the signed CloudFront
download_url, which is already served with Content-Disposition: inline.
Hard cap: 2 MB. Larger files return 413. Anything outside the text
whitelist returns 415. The whitelist (isTextPreviewable) mirrors the
client-side dispatcher; the cross-reference comment in file.go flags
the manual sync until a JSON SSOT generator lands.
Response always uses Content-Type: text/plain; charset=utf-8 so a
hostile HTML payload can't be re-interpreted as a document. The
original MIME ships via X-Original-Content-Type for client dispatch.
Cache-Control: no-store so revoked attachment access takes effect
immediately on the next request.
Tests cover happy path (md), extension fallback when content_type is
generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and
the isTextPreviewable table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core/api): add getAttachmentTextContent + preview error types
Adds an ApiClient method that fetches the text body of an attachment via
the new /api/attachments/{id}/content proxy. Two typed errors —
PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the
preview modal render specific fallbacks instead of a generic failure.
Refactors the private fetch() into a shared fetchRaw() helper so the
new method inherits the standard infra: auth headers, 401 →
handleUnauthorized recovery, X-Request-ID, error logging, and the
ApiError contract. The previous draft bypassed all of these by calling
window.fetch directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(views/editor): add AttachmentPreviewModal + Eye entry points
In-app preview for non-image attachments. An Eye icon now sits next to
the existing Download button on file cards / readonly file cards / the
standalone AttachmentList. Clicking it opens a full-screen modal that
dispatches by content_type:
pdf: <iframe src={download_url}> — Chromium PDFium
video/*: <video controls src={download_url}> — native controls
audio/*: <audio controls src={download_url}> — native controls
md: <ReadonlyContent> — full markdown pipeline
html: <iframe srcdoc sandbox=""> — fully restricted
text: <code class="hljs"> — lowlight highlight
Media types render directly from the signed CloudFront download_url
(server marks them inline-disposition). Text types fetch through the
new /api/attachments/{id}/content proxy via TanStack Query, wrapped
in useAttachmentPreview() so each entry point owns its own modal
state without depending on a global Provider mount.
Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger
than create-issue's max-w-4xl since PDF / video need room, but capped
to viewport on small screens. Sub-renderers use h-full to follow the
fixed modal height instead of viewport-relative units.
Images are intentionally NOT touched — the existing ImageLightbox
(extensions/image-view.tsx) already handles them correctly. The new
modal would be churn without user-visible benefit.
Adds i18n keys under attachment.* (en + zh-Hans) and registers
Preview/Download/Upload in the conventions glossary so future
translations stay consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(desktop): enable Chromium PDF viewer for attachment preview
Adds webPreferences.plugins: true to the main BrowserWindow so the
bundled Chromium PDFium plugin activates inside iframes — required for
the attachment preview modal's PDF dispatch. Default is false in Electron;
without it <iframe src=*.pdf> renders blank.
Security trade-off, accepted intentionally and documented inline:
1. This window already runs with webSecurity: false + sandbox: false,
so plugins: true does NOT meaningfully widen the renderer's attack
surface beyond what is already accepted.
2. The only PDFs that reach an iframe here are signed CloudFront URLs
we ourselves issued; user-supplied URLs are routed through
setWindowOpenHandler → openExternalSafely and cannot land in this
renderer.
3. Chromium's PDFium plugin is itself sandboxed and only handles
application/pdf — no Flash/Java/other historical plugin surfaces.
If we ever tighten webSecurity / sandbox, the follow-up is to host the
PDF viewer in a dedicated BrowserView with plugins scoped to that view,
keeping the main renderer plugin-free.
Old desktop builds ship without the preview modal, so the Eye button
never appears and PDF preview is gated by the same release — zero
regression risk for users on stale clients.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the regression reported in https://github.com/multica-ai/multica/issues/2515 that
PR #2437 only half-fixed in v0.2.31.
Two gaps remained on Ubuntu/GNOME:
1. The .deb shipped only the source 1024×1024 PNG under
/usr/share/icons/hicolor/, with no usable smaller sizes. GNOME's hicolor
lookup walks 16…512 and falls back to the theme default when none
match, so the launcher had no icon. The auto-generation pass in
electron-builder silently produced only the source size for us. Drop
pre-rendered 16/24/32/48/64/128/256/512 PNGs into build/icons/ and
point `linux.icon` at the directory so packaging stops depending on
the toolchain re-running that generation correctly.
2. WM_CLASS at runtime was `@multica/desktop`, while the .desktop file
declared `StartupWMClass=Multica`. PR #2437 assumed Electron derives
WM_CLASS from electron-builder.yml's `productName`, but Electron
reads `app.getName()`, which reads the *packaged ASAR's* package.json
— productName if present, otherwise name. Our source
apps/desktop/package.json had no top-level productName, so the ASAR
carried only `name: "@multica/desktop"` and Chromium emitted that as
WM_CLASS, breaking the .desktop association and the dock icon.
Fixed in two anchors for belt-and-braces: add
`"productName": "Multica"` to apps/desktop/package.json (so the ASAR
carries it and app.getName() resolves correctly by default), and call
`app.setName("Multica")` in the production branch alongside the
existing dev-only setName so a future regression in package.json or
the build pipeline cannot silently re-break WM_CLASS.
The `StartupWMClass: Multica` declaration in electron-builder.yml stays
pinned and the surrounding comment has been rewritten to record the
correct WM_CLASS derivation.
Verification on a real Ubuntu install:
- `dpkg-deb -c multica-desktop-*-linux-amd64.deb | grep hicolor` lists
≥8 sizes.
- `xprop WM_CLASS` on the running window prints `"multica", "Multica"`.
- Launcher and dock both show the Multica logo with no manual
~/.local/share/icons workaround.
Co-authored-by: multica-agent <github@multica.ai>
Base UI's Menu uses focus-follows-cursor — hovering a sibling row drags
DOM focus to that row, which made the rename input's onBlur=save fire
just from moving the mouse. The result: clicking the pencil and then
nudging the cursor would silently commit a half-typed title.
Replace the blur handler with a document-level pointerdown listener
(capture phase, so it runs before Base UI's outside-click close handler
unmounts the input). The listener only commits when the user actually
clicks somewhere outside the input. Enter still commits, Escape still
cancels, mouse hover is now a no-op.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
Gemini CLI's folder-trust feature throws FatalUntrustedWorkspaceError
(exit code 55) when the current workspace isn't in
`~/.gemini/trustedFolders.json` and the process is headless — no
interactive trust prompt is available. The daemon spawns gemini with
`-p` + `--yolo` in a freshly checked-out worktree that the user has
never trusted interactively, so every run with `security.folderTrust`
enabled fails after ~10s with exit status 55 and no useful output.
Default `GEMINI_CLI_TRUST_WORKSPACE=true` on the child env to short-
circuit `checkPathTrust` in gemini-core. This mirrors gemini-cli's
documented `--skip-trust` flag; the env var has been gemini's
documented headless escape hatch for the entire folder-trust feature
lifetime so the fix works on every gemini version that can produce
the crash. Callers that explicitly set the same key in cfg.Env win,
preserving the ability to opt back into the gate.
Co-authored-by: multica-agent <github@multica.ai>
The gemini CLI's Windows shim emits `Active code page: 65001` (from
`chcp`) to stdout before the real version reaches `--version` output.
The daemon stored the raw concatenation as the runtime version, so the
runtime detail page rendered `Active code page: 65001 0.42.0` instead
of `0.42.0`.
Scan `<cli> --version` line by line and return the first line carrying
a semver-shaped token. Full strings like `2.1.5 (Claude Code)` or
`codex-cli 0.118.0` survive unchanged; unparseable output falls back to
the trimmed raw value.
Co-authored-by: multica-agent <github@multica.ai>
Adds a pencil icon next to the trash icon on each session row in the chat
dropdown. Clicking it turns the title into an inline editable input:
Enter / blur saves, Escape cancels.
Server: new PATCH /api/chat/sessions/{id} handler that updates the title
via the existing `UpdateChatSessionTitle` sqlc query, broadcasts a new
`chat:session_updated` WS event so other tabs / devices stay in sync, and
rejects blank titles. Frontend mutation is optimistic with rollback,
matching the existing delete-session pattern.
MUL-2110
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): seed user-installed Codex skills into per-task CODEX_HOME
Codex is the only daemon runtime whose HOME is redirected — the daemon
sets CODEX_HOME to a per-task isolated directory so each task gets a
clean config slate without polluting ~/.codex/. Side effect: the codex
CLI never sees the user's `~/.codex/skills/` and tells the user no skill
was found.
Other runtimes (claude / copilot / opencode / pi / cursor / kimi / kiro)
don't have this issue: they leave HOME untouched and discover both
user-level skills (from ~/.<runtime>/skills) and workspace-assigned
skills (written to a workdir-local dotfile dir) natively. Codex is the
outlier.
Fix: in execenv.Prepare and execenv.Reuse, copy each subdirectory under
`~/.codex/skills/` into the per-task `codex-home/skills/` before writing
workspace-assigned skills. Workspace skills still win on sanitized-name
conflict; user-level installer symlinks (lark-cli style) are followed so
the per-task home gets real content rather than dangling links.
Closes#1922
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): wipe per-task codex skills dir before each hydration
Without this, the Reuse path leaves two classes of stale state behind:
1. Round 1 seeded user skill `writing/drafts/stale.md`. Round 2 reuses
the same workdir with workspace skill `Writing` assigned: seed
stage skips user `writing` (reserved), workspace stage writes
`SKILL.md` via MkdirAll + WriteFile but never clears the directory,
so the round-1 user support files surface under the workspace
skill — violating "workspace fully wins on name conflict" and
potentially leaking user-level files into a workspace skill view.
2. User uninstalls a skill from ~/.codex/skills between two runs. The
prior copy in codex-home/skills/<name>/ lingers, so the codex CLI
keeps seeing the removed skill.
Fix: RemoveAll(codex-home/skills) at the start of hydrateCodexSkills,
then re-seed user skills and re-write workspace skills. On Prepare
this is a no-op (envRoot was already wiped); on Reuse it resets the
slate.
Added two regression tests covering both scenarios.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>