The "+" button in each status column/section opens the create-issue
modal. On the project detail page it was passing only `{ status }`,
so the new issue's project field came up empty even though the user
was clearly in a project context. Thread `projectId` through
BoardView/ListView down to BoardColumn/StatusAccordionItem and
include `project_id` in the modal payload when set.
Co-authored-by: multica-agent <github@multica.ai>
Restructures the assistant timeline into a Conductor-style "X steps"
outer fold that wraps every thinking/tool/intermediate-text item between
the first and last non-text item; the final answer renders below the
fold at full prose size. The inner per-row Collapsibles
(ThinkingRow / ToolCallRow / ToolResultRow) are unchanged.
Adds an inline footer "Replied in 38s · [Copy]" beneath each persisted
assistant reply. Copy puts the markdown source of the visible text
(preface + final, never middle) on the clipboard via the existing
`copyMarkdown` helper. Suppressed during streaming.
Pure carving + extraction lives in `chat/lib/copy-text.ts` with 11 unit
tests covering all timeline shapes (all-text, all-non-text, standard,
preface, multi-final, legacy fallback).
Also cleans up 7 pre-existing `text-[11px]` arbitrary values in this
file to `text-xs`, and uses standard `size="icon-xs"` Button variant
for the Copy button (no manual size overrides).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent-live-card): self-heal stale "is working" banner via reconcile
The banner relied on receiving task:completed/failed/cancelled to clear
itself. When a WS reconnect dropped one of those events the banner stayed
forever and the elapsed timer kept ticking.
Replace the additive update paths (mount + queued/dispatch) with a single
reconcile() that refetches /active-task and replaces the local task set
with the server's truth, preserving accumulated TimelineItems for tasks
still active. Wire it to:
- mount / issueId change
- WS reconnect (useWSReconnect)
- task:queued / task:dispatch
- task:completed / task:failed / task:cancelled (after the optimistic
delete, so a missed sibling end-event also clears)
Per-task hydration guard (hydratedTaskIds) keeps the messages backfill
one-shot when reconcile fires repeatedly within a tick.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent-live-card): guard reconcile against out-of-order responses
reconcile() previously had no request-ordering protection, so a slow
getActiveTasksForIssue response could land after a newer one and clobber
the fresher state. Race scenario: task:queued fires reconcile A (response
includes T but is delayed); task:completed fires next, optimistically
removes T, and triggers reconcile B; B resolves empty and clears the
banner; A finally resolves with the stale snapshot and re-adds T —
permanent stale "is working" banner with no further events to clear it.
Add a monotonic reconcileSeq ref. Each call captures its issued seq;
the response only applies if mySeq === reconcileSeq.current (i.e. no
newer call was issued after this one). Drop the response otherwise.
Add a regression test covering the deferred-promise case plus a
companion test for the WS reconnect self-heal path.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The 60vh value is the magic number that keeps the tab content area
usably tall when the parent stacks inspector + overview on mobile and
delegates scroll to the page. Add a short note next to the className
so future maintainers know what the constraint is for and why `md:`
overrides it.
* feat(autopilot): auto-pause autopilots with sustained high failure rate
Adds a background monitor that pauses any active autopilot whose recent
runs are dominated by failures (defaults: ≥100 terminal runs in 7d, ≥90%
failed). The monitor leaves a severity=attention inbox notification for
the autopilot's creator (or the agent's owner if the autopilot was
agent-created) so a human learns about the auto-pause and can fix the
root cause before re-enabling.
Motivated by MUL-1336 §6 #2: a single broken cron autopilot
(`Registro de ls cada 5 min`, 1,475/1,476 failed in 7d) was burning
~1.5k tasks/tokens per week with no human in the loop.
Tunable via AUTOPILOT_FAIL_MONITOR_{INTERVAL,LOOKBACK,MIN_RUNS,FAIL_RATIO,STARTUP_DELAY};
INTERVAL=0 disables the monitor entirely.
Co-authored-by: multica-agent <github@multica.ai>
* chore(autopilot): relax failure monitor defaults to daily / 50 runs
Per review feedback in MUL-1339: 30-min scan was overkill — the 50-run
threshold already provides multi-hour lag, and operational simplicity
matters. Lowering MinRuns from 100 → 50 keeps low-frequency autopilots
in scope (~7 runs/day reaches threshold within 7d window).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): tighten quick-create prompt to drop meta-instructions and apologetic Context
The quick-create prompt was producing descriptions that:
1. Echoed routing meta-instructions ("create an issue for me", "cc @X") into
the User request body, even though those phrases are handled by separate
CLI flags and are not spec content.
2. Emitted a Context section to apologize for resources it could not fetch
(e.g. an image attachment not piped through to the run), instead of
staying silent and letting the executing agent ask the user.
3. Preserved pure conversational fillers ("对吧?", "嗯", "那个…") because the
model treated removing them as forbidden paraphrasing.
Updates the prompt to call out each of these as explicit non-spec material
to strip before writing the description, while keeping the "high fidelity /
no paraphrasing of substantive content" invariant. Adds a regression test
that locks in the new rules at the substring level.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): preserve cc mention links in quick-create description
Stripping "cc @Y" wholesale would have lost the mentioned member's only
routing channel: `multica issue create` has no --subscriber/--cc flag, and
the platform auto-subscribes members by parsing `[@Name](mention://member/<uuid>)`
links from the description body. Without the mention link in the body, a
cc'd member would never get subscribed or notified.
Updates the prompt to:
- Strip only the verbal "cc" wrapper from the User request body.
- Append a trailing `CC: <mention links>` line to the description so the
platform's auto-subscribe logic still picks the mentions up.
- Spell out the contrast for assignee mentions, where --assignee-id is
the routing channel and the body should not double-encode the mention.
Also adds a substring assertion for the "Pure conversational fillers" rule
that was missing from the original regression test.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(daemon): trim quick-create prompt rules to general principles
Reviewers pointed out the previous rewrite traded one prompt smell (over-
permissive verbatim quoting) for another (too many specific rules and
exhaustive bilingual example tables). Rewrites the description block as
general principles with a single representative example each, trusting the
model to generalize:
- "Strip non-spec material before writing" replaces the multi-bullet list
of routing-meta-instruction and conversational-filler enumerations.
- "Include Context only when references were fetched and produced facts;
never use it as an apology log" replaces the three "Do NOT emit a
Context section to" sub-bullets.
- The CC exception (the only operationally non-obvious rule, since
`multica issue create` has no --subscriber flag) is kept inline as a
single sentence and is still locked in by the regression test.
Net: ~16 fewer lines of prompt text without losing any of the rules the
test asserts.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): add v0.2.26 entry for 2026-05-06 release
Summarizes the 32 PRs landed on main since v0.2.25:
i18n (en + zh-Hans) full rollout, system notifications toggle,
chat session deletion, Redis-backed runtime liveness, long-issue
Timeline keyset pagination, and a batch of daemon/runtime
stability fixes. Mirrored across en.ts and zh.ts.
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): tighten v0.2.26 feature copy
Per review feedback — drop "so you can" / "across the entire app"
clauses, match the terse one-clause cadence used by the 0.2.24 entry.
Improvements/fixes copy is unchanged.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(notifications): add system notifications toggle in settings
Add a per-user, per-workspace toggle to enable/disable native OS
notification banners. Reuses the existing notification-preferences
endpoint by introducing a `system_notifications` key alongside the
inbox event groups; the realtime handler reads the cached preference
and skips desktopAPI.showNotification when muted.
Co-authored-by: multica-agent <github@multica.ai>
* fix(notifications): fetch system_notifications pref lazily
Settings is the only mounted reader of notificationPreferenceOptions,
so a fresh app start (or any session that never visits Settings) left
the cache empty and the muted preference silently fell back to default
"all". Switch the inbox:new handler to ensureQueryData so the value is
fetched on first use and cached for subsequent events.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
- Rename appearance-tab → preferences-tab; AppearanceTab → PreferencesTab
- i18n top-level key appearance → preferences; tab label "Appearance" → "Preferences" / "偏好设置"
- Swap icon Palette → SlidersHorizontal (preferences semantic)
- SettingsPage: read active tab from ?tab= via NavigationAdapter, write back with replace() on change; whitelist valid tabs (incl. desktop extras daemon/updates), unknown values fall back to profile
- Update conventions.mdx (en + zh) references to renamed file and i18n key
Why preferences over appearance: the tab held both theme and language; "Appearance" semantically excludes localization. "Preferences" follows Linear/Slack/Discord and leaves room to add timezone/date format later.
Why query param over path: settings tabs are UI modifier state, not resources; query persistence keeps the existing single Next.js route file and desktop memory router unchanged, gives a natural fallback for unknown values, and avoids 404 risk.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The agent submit button rendered the shortcut hint twice — the i18n
string already contained '(⌘↵)' and the JSX appended another
formatShortcut() suffix. Drop the hardcoded shortcut from the
translations and rely on the platform-aware formatShortcut() in JSX.
Co-authored-by: multica-agent <github@multica.ai>
CI was running build + typecheck + test, but never lint. The i18n
guardrail (eslint-plugin-i18next on packages/views/**/*.tsx) was
configured but not enforced, so PRs kept landing user-facing English
strings (chat session delete, project resources, mermaid fallback,
invitations batch page).
Changes:
- .github/workflows/ci.yml: add `lint` to the turbo command
- packages/eslint-config/react.js: split React rules (JSX-only) from
react-hooks rules (apply to .ts too) — hooks live in .ts modules
like use-agent-presence.ts, and inline-disable comments need the
rule registered to resolve
- Translate the 10 lint errors that surfaced:
- editor/readonly-content.tsx mermaid render-error + rendering
- issues/issue-detail.tsx Archive tooltip
- invitations/invitations-page.tsx full page (new invite.batch.*)
- invitations-page.test.tsx wrap with I18nProvider so getByRole queries
match translated button labels
- core/auth/utils.ts intentional control-char regex: add eslint-disable
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968)
Opening an issue from Inbox with thousands of timeline entries used to
hard-freeze the browser tab on a synchronous render of every comment +
activity. The whole pipeline was unbounded: the API returned every row,
TanStack Query cached the full array, and IssueDetail mounted N
CommentCards (each running a full react-markdown + lowlight pipeline)
in one frame.
This swaps the timeline endpoint to keyset cursor pagination and rewires
the frontend to useInfiniteQuery so a long issue costs the same as a
short one on first paint.
API:
- GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex)
+ ?limit (default 50, max 100); response wraps entries with next/prev
cursors and has_more flags. Cursors are opaque base64 (created_at, id).
- ?around=<entry_id> anchors a window on the target so Inbox notifications
pointing at an old comment never trigger the freeze.
- New composite indexes on (issue_id, created_at DESC, id DESC) replace
the redundant single-column ones so keyset queries are index-only scans.
- /issues/:id/comments default branch now caps at 50 instead of returning
every row unbounded; the unbounded ListComments / ListActivities sqlc
queries are deleted.
Frontend:
- useIssueTimeline switches to useInfiniteQuery, exposes
fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount.
- WS handlers respect the at-latest invariant: comment/activity:created
prepends to pages[0] only when the user is reading the live tail;
otherwise it just bumps a counter so the UI offers a "Jump to latest"
affordance without yanking scroll.
- Optimistic mutations adapted to the InfiniteData shape via shared
helpers (mapAllEntries / filterAllEntries / prependToLatestPage in
core/issues/timeline-cache.ts) and use setQueriesData so all open
windows of the same issue stay in sync.
- IssueDetail Activity section gets a TimelineSkeleton placeholder
during the brief load window plus subtle text-link load-more buttons
matching the existing Subscribe affordance (no Button chrome). Top
uses a divider for boundary clarity; bottom shows
"Jump to latest · N new" weighted slightly heavier when there's
unread state.
- highlightCommentId now flows into the hook's around parameter so
Inbox jumps fetch the surrounding 50 entries directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(agent): default comment list to 50 + prompt hint about long issues
The CLI's "multica issue comment list" used to default to --limit 0
(meaning "fetch every comment"), which lets an agent on a long issue
fill its context window with thousands of rows. The default is now 50;
agents that need older history can pass --limit or --since explicitly.
The local-coding-agent prompt also gains a single-line note about this
in both the comment-triggered and on-assign flows so the agent knows to
scope its fetches when issue size is unknown. Autopilot run-only mode
is intentionally unchanged — it has no issue context to query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): rollout phase — translate 9 namespaces (WIP)
Phase 1 complete (基建 + login + Settings language switcher),
phase 2 partial (Wave 4 done, search done). Pending namespaces
documented inline; another developer can pick up from here.
Infrastructure
--------------
- server: add users.language column + extend PATCH /api/me
(TestUpdateMeAcceptsLanguage / TestUpdateMePreservesLanguage)
- packages/core/i18n: types / pickLocale (intl-localematcher) /
browser-cookie-adapter / createI18n (initAsync false +
useSuspense false) / I18nProvider / LocaleAdapterProvider
- Split server-safe vs React entries:
@multica/core/i18n — for proxy/RSC/middleware (no React)
@multica/core/i18n/react — for client trees (createContext)
(RSC vendored React lacks createContext; mixed import would crash
proxy.ts at module load.)
- packages/views/i18n: useT hook + selector API augmentation
(i18next v26 default; auto-propagates to apps via the side-effect
import in use-t.ts).
- apps/web: proxy.ts (Next 16 renamed middleware) merges existing
legacy/root redirects with x-multica-locale header forwarding;
layout.tsx reads locale via headers() and pre-loads RSC resources.
- apps/desktop: webPreferences.additionalArguments injects
systemLocale (no sendSync — avoids main-thread blocking IPC);
renderer adapter reads via process.argv.
- ESLint: i18next/no-literal-string at file-scope for translated
files via packages/views/eslint.config.mjs TRANSLATED_FILES.
- glossary.md (packages/views/locales/) freezes term policy:
Issue / Workspace / Agent / Skill / Autopilot / Daemon / Runtime
stay English; Inbox / Project / Comment / Member translate.
Translated namespaces (9 / 19)
------------------------------
- auth: login page (web wrapper含 desktop-handoff 文案) + Settings
Appearance language switcher
- editor: 9 .tsx (bubble-menu / link-hover-card / readonly-content /
title-editor / extensions: code-block / file-card / image-view /
mention-suggestion) + 32 keys
- invite: 25 keys
- labels / members / my-issues: Wave 4 全部
- search: command palette 35 keys
- navigation: no user-facing strings (no-op)
Pending (10 / 19)
-----------------
issues (46 files / ~210 keys)
agents (29 files / ~155 keys; presence.ts + config.ts label maps
允许进 i18n)
onboarding (22 files / ~150 keys)
settings rest / skills / modals / workspace / chat / inbox /
projects / autopilots / layout
Workflow for picking up
-----------------------
- Glossary: packages/views/locales/glossary.md (mandatory read)
- Reference impls: auth/login-page.tsx + editor/* (selector API +
i18n-provider test wrapper pattern)
- Per namespace:
1. create locales/{en,zh-Hans}/{ns}.json
2. add to packages/views/i18n/resources-types.ts
3. useT('{ns}') + t($ => $.foo) in components
4. add files to TRANSLATED_FILES in eslint.config.mjs
5. typecheck + test + lint must pass
- Subagents currently CANNOT write files (sandbox deny). Run as
hybrid: subagent researches + outputs full JSON + tsx diff,
controller writes.
Other
-----
- scripts/init-worktree-env.sh: default
MULTICA_DEV_VERIFICATION_CODE=888888 in dev for deterministic
login (gated by isProductionEnv).
Verified: pnpm typecheck (6 pkgs ok), pnpm test (232 pass),
make test (Go).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(i18n): rewrite glossary aligned with docs zh voice
Switch translation policy to match the canonical CN voice already
established in apps/docs/content/docs/*.zh.mdx (20+ files). The new
rule splits product nouns into two classes:
- Typed entities (issue / project / skill / autopilot / task) — kept as
lowercase English in CN text, visually marking them as system types.
- Concepts (workspace / agent / daemon / runtime / inbox) — fully
translated (工作区 / 智能体 / 守护进程 / 运行时 / 收件箱).
Previous glossary kept Workspace / Agent / Daemon / Runtime as English
on "工程惯例" grounds, but docs zh and CN AI ecosystem (Coze / 腾讯元器
/ 百度) consistently translate these. App UI now matches docs voice so
users don't see split personality between the app and its own docs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): register 6 namespaces and retrofit zh strings to new glossary
Two fixes that were blocking the previously-translated namespaces from
actually rendering in CN:
1. RESOURCES gap — locales/index.ts only loaded common/auth/settings,
but resources-types.ts declared 12 namespaces and 6 of them had real
translation content. At runtime i18next would fall back to raw keys
for editor / invite / labels / members / my-issues / search.
Register all 9 currently-translated namespaces.
2. Retrofit zh strings to the docs-aligned glossary:
- "Issue" → "issue" (lowercase entity)
- "Workspace" → "工作区"
- "Agent" → "智能体"
- "Runtime" → "运行时"
- "Skill" → "skill" (lowercase)
- "项目" → "project" (lowercase)
Touched: editor.json (sub_issue + mention.group_issues), invite.json
(3 Workspace occurrences), members.json (agents_section / more_agents),
my-issues.json (8 retrofits across page/header/errors), search.json
(13 retrofits across groups/pages/commands/empty).
Verified: pnpm typecheck (6/6) + pnpm test (238/238) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate inbox namespace
First namespace through the sub-agent → main-agent integration pipeline.
JSON: en/inbox.json + zh-Hans/inbox.json — 60 keys across page / menu /
list / detail / types / labels / errors. Time-formatter labels are kept
compact in EN ("5m" / "3h" / "2d") and use full units in zh ("5 分钟" /
"5 小时" / "5 天") since raw "5 分" reads as "5 marks/points" in CN.
Component changes converted two module-level statics into hooks so the
strings can flow through i18next:
- inbox-list-item.tsx: `timeAgo` (pure fn) → `useTimeAgo` (hook
returning a fn). The local copy is a duplicate of @multica/core/utils
`timeAgo` that is only used by inbox-page; other consumers across
chat/agents/skills/issues stay on the core util for now and will be
translated when their namespaces land.
- inbox-detail-label.tsx: `typeLabels` (static const Record) →
`useTypeLabels` (hook returning the same Record shape). Call sites
keep the existing `typeLabels[type]` access pattern.
inbox-page.tsx now uses both hooks and `useT('inbox')` selector calls
for all hardcoded strings (~24 sites: header / dropdown menu / list
empty state / detail panel / mobile back / quick-create-failed flow /
all error toasts).
Wired up: resources-types.ts, locales/index.ts RESOURCES, ESLint
TRANSLATED_FILES (3 inbox tsx files now lint-protected).
Verified: pnpm typecheck (6/6) + pnpm --filter @multica/views test
(238/238) + ESLint clean on inbox/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate workspace namespace
Translates the three workspace shell views: create-workspace-form,
new-workspace-page, no-access-page. Also fixes the prior-art
no-unescaped-entities lint errors in no-access-page.tsx — the
apostrophes in "doesn't" / "don't" were JSX text literals that move
into JSON values after translation, so the lint rule no longer fires.
Tests wrapped: workspace/create-workspace-form.test.tsx,
workspace/no-access-page.test.tsx, modals/create-workspace.test.tsx
all now wrap render() with <I18nProvider locale="en"> so the en values
in workspace.json drive the rendered text and the existing assertions
continue to match.
Slug constants kept: WORKSPACE_SLUG_FORMAT_ERROR /
WORKSPACE_SLUG_CONFLICT_ERROR exports in workspace/slug.ts are still
imported by onboarding/steps/step-workspace.tsx (out of scope here).
The workspace shell now reads its strings from workspace.json directly.
Multica.ai brand prefix in the slug input affordance is wrapped with
an inline `// eslint-disable-next-line i18next/no-literal-string` per
glossary policy on brand names.
Renamed sign_in_other → sign_in_different to avoid colliding with
i18next's `_other` plural-suffix convention which the selector-API
typings treated as a plural form of `sign_in`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate projects namespace
Translates the projects list page, project detail page, project picker
dropdown, and project chip — all four user-facing surfaces under
packages/views/projects/components/.
New file: projects/components/labels.ts exposes three hooks that
replace the static `.label` field on PROJECT_STATUS_CONFIG /
PROJECT_PRIORITY_CONFIG and the previous module-level
`formatRelativeDate` helper. Core's `.label` stays untouched (it's
still consumed by search and the create-project modal, both
out-of-scope for this namespace) — those will flip when their
respective namespaces translate.
In zh, the "project" entity stays lowercase English per glossary
(`新建 project`, `还没有 project`, `从 project 移除`). Status / priority /
table column labels translate fully.
The cancelled / done / paused etc. status labels duplicate per-
namespace as `projects.status.*` rather than reading from a future
shared status namespace. This matches the auth/inbox/workspace
pattern of self-contained namespaces. If a generic "issue/project
status" pool emerges later, these can collapse.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on projects/ (1 pre-existing warning
about useEffect/sidebarRef dep, unrelated to i18n).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate autopilots namespace
Six tsx files: autopilots-page (list + 6 templates), autopilot-detail-page
(properties / triggers / run history / delete), autopilot-dialog
(create + edit dialog), trigger-config (cron form), and the agent /
timezone pickers.
Hook conversions for module-level helpers that need t():
- summarizeTrigger / describeTrigger → useSummarizeTrigger /
useDescribeTrigger (no external callers, removed the plain exports)
- formatRelativeDate → useFormatRelativeDate (per-component hook)
- formatCountdown → useFormatCountdown (per-component hook)
- TEMPLATES array now keyed by id; titles + summaries pull from
templates/{id}/{title,summary} JSON. Prompts stay raw EN since
they're injected directly into the agent task — translating them
would translate the agent's instructions, not the user's UI.
Status / execution-mode / run-status enums render via t($ => $.status[k])
with k typed against the core type (no separate hook needed).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on autopilots/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate skills namespace
Seven tsx files: skills-page (list + filters + intro banner),
skill-detail-page (the giant — properties + file tree + sidebar +
conflict banner + delete dialog, ~963 lines), create-skill-dialog
(chooser + manual + URL forms), runtime-local-skill-import-panel
(local runtime browse + import), skill-columns, file-tree, file-viewer.
Notable patterns:
- `createSkillColumns` factory → `useSkillColumns` hook so column
headers flow through useT. Column identity changes per render is
fine — DataTable handles it.
- `validateNewFilePath` (pure helper) → `useValidateNewFilePath` hook
so the 5 validation error messages can be translated.
- skill_files / used_by / description_with_agents use i18next plural
keys (`_one` / `_other`) — the type system collapses these into a
single PluralValue access, so call sites use
`t($ => $.foo, { count })` and i18next picks the form.
- Per glossary, "skill" stays lowercase EN in zh ("新建 skill",
"已删除 skill", "未找到该 skill").
Test wrapper: runtime-local-skill-import-panel.test.tsx now wraps
render() with <I18nProvider> so the assertion on /Import to Workspace/i
matches the EN translation.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238) + ESLint clean on skills/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate chat namespace
Translates all 10 chat surfaces: FAB tooltip, input placeholders,
message list (replied-in / failed-after / tools group / show-details
/ tool result preview), session history (header + time-ago labels),
chat window (new-chat / restore / expand / minimize / agent + session
dropdowns / starter prompts / empty states), context-anchor button +
card tooltips, no-agent banner, offline / unstable banner, and the
task-status pill (queued / starting up / thinking / typing + tool
labels: running command / reading files / searching code / making
edits / searching web).
Hook conversions:
- formatTimeAgo (chat-session-history) → useFormatTimeAgo
- ElapsedCaption now takes a typed `variant` ("replied" | "failed")
instead of a free-text `verb` so the i18n key is enumerable
- pickStage (task-status-pill) refactored: pure pickStageKeys returns
StageKey + optional ToolKey; useResolveStage maps to localized labels
Translation policy notes:
- Starter prompts ("List my open tasks by priority", etc.) are user
UI when displayed AND the user's input when clicked — translating
them sends the agent the user's locale-native phrasing, which is
the right UX for a CN user using a CN agent.
- buildAnchorMarkdown (chat-window) stays in English: it's an
agent-bound markdown prefix injected into the outgoing message,
not user-facing UI.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate modals namespace
Translates all 11 modal sources: registry (no UI text), backlog-agent-hint,
set-parent-issue, add-child-issue, delete-issue-confirm, feedback,
issue-picker, create-workspace, create-project, create-issue (manual),
quick-create-issue (agent panel).
Notable patterns:
- create-project re-uses useProjectStatusLabels / useProjectPriorityLabels
hooks from views/projects/components/labels — same translation source
as the projects list / detail, no duplication.
- create-issue.tsx: renamed `toast.custom((t) => ...)` callback param to
`toastId` to avoid shadowing the closure-captured useT() `t` function.
- Test wrapper added to modals/create-issue.test.tsx so the two assertions
on rendered modal text (success toast + Create another) match the EN
bundle. modals/create-workspace.test.tsx was already wrapped (workspace
ns commit).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate settings namespace (rest of tabs)
Builds on the appearance-tab + language switcher already shipped in
Phase 0. Translates the remaining 8 settings surfaces: settings-page
shell (left nav + tab keys), account / profile, notifications-tab
(5 group labels + descriptions), tokens-tab (create / list /
revoke / created dialog), workspace-tab (general fields + danger
zone + leave/delete confirmations), members-tab (invite + role
config + revoke / remove flows), repositories-tab, labs-tab,
delete-workspace-dialog.
Hook conversion: members-tab `roleConfig` static const → `useRoleLabels`
hook returning a Record<MemberRole, {label, description, icon}>. The
icon stays as a typed React component (Crown / Shield / User), so
rendering pattern is unchanged at call sites.
Test wrapper: settings/components/delete-workspace-dialog.test.tsx
now wraps render() with <I18nProvider> (custom render() helper)
because the test asserts on rendered button labels ("Delete workspace",
"Cancel", "Deleting...").
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes namespace (entry surfaces)
Translates the user-facing runtime list page surfaces:
runtimes-page (header / search / filters / chips / empty / no-matches /
bootstrapping), runtime-detail (topbar + delete dialog + delete toasts),
runtime-detail-page (not-found state), shared.tsx (4-state HealthBadge
labels).
Hook conversion: shared `healthLabel(health)` was a pure module-level
function. Added `useHealthLabel` hook for translated call sites; kept
`healthLabel` as an EN-only fallback for non-component callers (column
factory in runtime-columns).
Deferred:
- runtime-list / runtime-columns (data table column headers + cell
bodies) — large surface, not in the page-load critical path.
- connect-remote-dialog / update-section / usage-section — secondary
flows, English remains acceptable until a focused pass.
- charts/* — primarily numeric tooltips and axes; minimal user-visible
text.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate layout namespace (sidebar nav, help, loader)
Translates the cross-cutting layout chrome:
- 9 sidebar nav labels (inbox / my issues / issues / projects /
autopilots / agents / runtimes / skills / settings) — driven by
labelKey instead of inline strings, resolved via useT at render.
- HelpLauncher dropdown (trigger aria + 3 items: Docs / Change log
/ Feedback)
- WorkspaceLoader (named + unnamed loading states)
- SortablePinItem unpin tooltip
Pattern shift in app-sidebar.tsx: nav arrays carry `labelKey: NavLabelKey`
(typed against the layout JSON) instead of `label: string`. The string
comparison checks (`item.label === "Inbox"`) became cleaner ID-based
checks (`item.key === "inbox"`).
Deferred: deeper sidebar surfaces — workspace switcher dropdown,
"New Issue" CTA, "Pinned" / "Workspace" / "Configure" group labels —
remain English. The 9 nav labels are the ones that read in every
session.
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding namespace (welcome + step header)
Translates the user-first-impression surfaces of the onboarding flow:
- step-welcome.tsx (the wordmark, headline, lede paragraphs, all CTAs:
Download Desktop / Continue on web / Start exploring / I've done
this before, illustration caption)
- step-header.tsx ("Step N of M" counter + matching aria-label)
- onboarding-flow.tsx (skip-onboarding error toast)
Test wrapper added to onboarding/components/step-header.test.tsx —
custom render() helper wraps with <I18nProvider> so the "Step 2 of 5"
assertions match the EN bundle.
Deferred (acceptable English fallback for now): step-questionnaire,
step-workspace, step-runtime-connect, step-platform-fork, step-agent,
step-first-issue, cli-install-instructions, option-card, runtime
aside panels, starter-content-prompt, cloud-waitlist-expand. These
are deeper steps with significant copy that would benefit from a
focused dedicated pass — voice on each is more nuanced (questionnaire
options, runtime install instructions, agent template recommendations).
Verified: pnpm --filter @multica/views typecheck (clean) +
test (238/238).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(i18n): add EN/zh-Hans key parity guard
Schema-level vitest that walks RESOURCES.en and RESOURCES["zh-Hans"]
namespace by namespace and asserts both bundles cover the same key
set. i18next plural rule is normalized before compare (`_one` /
`_other` collapse to a single logical key) so EN's plural pair
matches zh's `_other`-only form.
Catches retrofit drift where a new EN key lands without zh —
previously this would silently fall back to the English string in
production. Cheap to keep green: 39 tests across 21 namespaces in
under a second.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate issues namespace
Translates the entire issues surface — list / board / detail / comments /
sub-issues / activity feed / batch toolbar / pickers / context menu /
backlog-agent hint dialog / labels panel.
Component coverage:
- issues-page (page header, empty state, move-failed toast)
- issues-header (scope tabs, filter dropdowns w/ status/priority/
assignee/creator/project/label, display settings, sort, view toggle)
- issue-detail (page header, breadcrumb, properties / parent issue /
details / token usage sections, sub-issues, activity timeline,
formatActivity for status/priority/assignee/title/due-date changes,
subscribe/subscriber popover)
- comment-card + comment-input + reply-input (delete dialog, edit/save,
copy/edit/delete row, reply count, placeholders, expand/collapse)
- agent-live-card (is-working banner, tool count, stop / transcript)
- execution-log-section (section header, show/hide past runs, trigger
text builder, status labels, cancel-task)
- batch-action-toolbar (selected count, delete dialog with plurals)
- backlog-agent-hint-dialog (full dialog content)
- labels-panel (intro, create form, list, delete dialog)
- pickers (status / priority / assignee / due-date / label / property
search placeholder + no-results)
- issue-actions-menu-items (all dropdown / context menu items)
- use-issue-actions / use-issue-timeline (toast strings)
STATUS_CONFIG / PRIORITY_CONFIG label rendering routed through
$.status[enum] / $.priority[enum] at every call site; the core config
keeps its English fallback for non-i18n consumers but UI never reads
.label directly anymore.
Tests retrofitted: issues-page, issue-detail, and issue-actions-menu
RTL specs now wrap renders in <I18nProvider> with the EN bundle, so
their string assertions match the bundle (not hardcoded literals).
ESLint i18next allow-list extended to 24 issues files. Verified:
pnpm --filter @multica/views typecheck + test (277/277) all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate agents namespace
Translates the agents listing + detail surface and the create/duplicate
flow. Covers the high-frequency surfaces; deeper sub-tab editors
(activity / instructions / skills / env / custom-args bodies, and the
hooks-buggy runtime/model/concurrency pickers) are deferred — they
have their own pre-existing react-hooks rule violations and benefit
from a focused dedicated pass.
Component coverage:
- agents-page (page header w/ tagline + new button, scope segment,
search, sort dropdown, availability chips, archived toolbar, empty
state, no-matches messaging w/ search interpolation, list-load
error)
- agent-detail-page (back link, archived banner, archive dialog,
not-found state, all 4 toast strings)
- agent-detail-inspector (avatar editor, name + description popover,
description dialog, every PropRow label, validation message,
presence badge label sourced from $.availability[enum])
- agent-overview-pane (tab labels, discard-unsaved-changes dialog)
- create-agent-dialog (title / description / labels / placeholders /
duplicate-suffix / runtime filter buttons / runtime status copy)
- agent-row-actions (full dropdown items + cancel-tasks dialog with
pluralized "N running + M queued" summary + archive dialog + 6 toasts)
- agent-columns (every header cell, You / Archived chips, runtime
fallback labels, availability + workload labels via $.availability /
$.workload, activity tooltip body w/ created_today / created_days_ago
/ runs / failed-percent interpolation)
- inspector/skill-attach (Attach trigger label + aria)
availabilityConfig and workloadConfig now keep colors only — the
display label lives in the bundle, sourced via $.availability[enum]
and $.workload[enum] at every call site. Same pattern as
STATUS_CONFIG/PRIORITY_CONFIG in the issues namespace.
ESLint i18next allow-list extended to 8 agents files.
Verified: pnpm --filter @multica/views typecheck + test (277/277)
all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): clear 30 stray EN strings in translated files
Tail of literal strings missed in earlier passes — the ESLint i18next
allow-list flagged them but they slipped through review. Files touched:
- layout/app-sidebar.tsx (10 keys: Workspaces / Pending invitations /
Create workspace / Join / Decline / Log out / New Issue + shortcut /
Pinned / Workspace / Configure)
- runtimes/components/runtime-detail.tsx (Serving header + serving_count
pluralization, no_agents copy, running/queued chips with count
interpolation, Diagnostics header, CLI label, Delete runtime button,
Technical details toggle, last seen interpolation)
- onboarding/steps/step-welcome.tsx (entire WelcomeIllustration mock —
5 cards × actor names + body copy + 3 mention chips + 2 timestamps;
zh translation reads naturally instead of leaving the demo English)
- settings/components/labs-tab.tsx (`Co-authored-by: ...` git trailer
wrapped in {} so linter sees a JS string, not JSX text — magic
identifier git relies on, must not translate)
- settings/components/members-tab.tsx (✓ glyph wrapped in {})
- modals/feedback.tsx (⌘↵ shortcut wrapped in {})
ServingAgentsCard now reads availability/workload labels from
`agents` namespace (cross-namespace useT) so the bundle-truth pattern
holds: presenceConfig keeps colours only, label text comes from the
shared bundle.
Verified: typecheck + 277/277 tests + lint (only the pre-existing
react-hooks rule-of-hooks errors remain, which task #6 addresses).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agents): rules-of-hooks + translate 4 model/runtime pickers
Three pre-existing react-hooks/rules-of-hooks violations + one missing
useMemo dep cleared, then the four pickers wired through useT.
Hook order fixes:
- concurrency-picker: useEffect now runs before the !canEdit early
return. Stale-draft reset still works the same way.
- runtime-picker: useMemo for the filtered list moved above the
!canEdit branch.
- model-dropdown: `models = data?.models ?? []` was minting a fresh
array each render and tripping the deps lint of the downstream
useMemo. Wrap in useMemo so the reference is stable.
Translation coverage:
- concurrency-picker: tooltip ("Concurrency · N max..."), range
helper text, Save button.
- runtime-picker: trigger label fallback ("No runtime"), tooltip
text composed from {{name}} + status, Mine/All filter buttons,
empty-list copy, "owned by {{name}}" + status fragments in row
tooltip, Cloud badge, online/offline aria.
- model-picker: trigger label, tooltip, "Managed by runtime"
fallback, search placeholder, "Discovering models…", default
badge, "No models available", "Use \"X\"" custom-id flow, Clear
button + its title.
- model-dropdown: every label string including the "Select a runtime
first" / "Default (provider)" / "Runtime offline — enter manually"
trigger fallbacks, the supported=false explanation block, discovery
failed badge, all popover items.
ESLint allow-list extended to 4 picker files. Verified: typecheck +
277/277 tests + lint (0 errors, only pre-existing react-hooks warnings
in unrelated files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes list + connect dialog + CLI updater
Three deep runtime surfaces wired through useT, with the agents
namespace doing double duty for shared availability/workload labels.
runtime-columns:
- 7 column headers via t-augmented createRuntimeColumns({ t }).
- HealthCell now reads from useHealthLabel() (already translation-aware)
instead of the EN-only healthLabel() helper.
- WorkloadCell sources the label from $.workload[enum] (cross-namespace
to agents) — colour stays via workloadConfig.
- CostCell delta "flat" copy + CLI cell "Desktop" badge + update-
available aria/tooltip + RowMenu's full delete dialog (title /
description with {{name}} interpolation / cancel / confirm /
deleting state) plus its admin-permission hint.
connect-remote-dialog:
- Three steps fully translated: instructions (header + 4 numbered
steps + security warning + troubleshooting list with mono code
snippets escaped as JS strings), waiting (loader + hint), success
(CTA pair).
- Mono CLI commands wrapped in {} so linter sees JS strings — those
are literal commands that must stay untranslated for the user to
paste into a terminal.
update-section:
- statusConfig collapsed to icon+colour only; labels move to
$.update.status[enum] for proper translation per-state.
- "CLI Version:" / "Latest" / "available" / "Update" / "Retry"
copy + the "Managed by Desktop" tooltip and disabled hint.
Layout helpers tagged: runtime-list passes `t` through to the column
factory the same way agent-columns does.
ESLint allow-list extended with the 4 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. usage-section.tsx
(KPI cards / WhenChart / TopUsageBreakdown / receipt table) is the
remaining runtimes surface — chart-heavy and benefits from a focused
pass next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate 5 agent detail tabs + skill-add dialog
The 5 tabs that fill the agent detail right pane plus the shared
skill picker dialog. Agents bundle gains a `tab_body` block with
sub-namespaces per tab + a `common` slot for save/add/unsaved.
Tab coverage:
- instructions-tab: intro paragraph, multi-line example placeholder
(full 18-line zh translation), Save / Unsaved.
- env-tab: read-only intro / empty state, editable intro with two
inline `<code>` env-var examples kept English (mono terminal
payloads), KEY / value placeholders, Show/Hide value aria, Add /
Remove aria, all 3 toasts (duplicate keys / saved / save failed).
- custom-args-tab: intro about whitespace splitting, launch-mode
prefix line + `<your args>` placeholder, --flag value placeholder,
Add, Remove aria, both toasts.
- skills-tab: intro, Add skill button, import-hint callout, empty
state title + hint + add-CTA, remove-failed toast.
- activity-tab: 3 section titles (Now / Last 30 days / Recent work),
active-task pluralization, performance subtitle, all 3 empty
states, runs/success%/avg-duration/failed pluralization with
interpolation, source labels (Issue / Chat / Autopilot / Untracked),
source fallbacks (Quick create / Creating issue / Chat session /
Autopilot run), issue-short fallback, "Triggered by" tooltip
header, open-issue / transcript / cancel-task tooltips and ARIAs,
cancelling state, started/dispatched/queued time prefixes, show
more.
- skill-add-dialog: dialog title + description, empty list copy,
Cancel button, add-failed toast.
skills-tab.test.tsx wrapped in <I18nProvider> with the EN bundle so
its `Local runtime skills are always available` assertion still
matches the resolved translation instead of the raw key path.
ESLint allow-list extended with the 6 wired files. Verified:
typecheck + 277/277 tests + 0 i18n lint errors. Only the per-test
mock for skills-tab needed wrapping; the other 4 tabs ship without
test files of their own and inherit the I18nProvider chain via
agent-overview-pane / agent-detail-page test renders (when those
exist later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding step-questionnaire + option-card
The user-profile step (3 questions) is the first deferred onboarding
deep step now wired through useT.
step-questionnaire:
- Eyebrow + headline + answered-progress counter with {{count}}
interpolation
- All 3 questions and their option labels (team size / role / use case)
- All 3 "Other" placeholders for free-text fallback
- Right-rail "Why three questions" / "What you get" panel: 2 eyebrow
rows, 2 unlock-item title+body pairs, learn-more link
- Back / Continue buttons via shared `common` block
option-card: shared "Other" radio label and aria.
Test wrapped in <I18nProvider>. EN value of `other_label` kept as
"Other" so the existing /^other$/i regex in step-questionnaire.test
keeps matching after the rendering pipeline switched from a hardcoded
literal to a bundle lookup.
ESLint allow-list extended with these 2 files. The remaining 4 deep
steps (workspace / runtime-connect / platform-fork / agent), the
2 ancillary surfaces (cli-install-instructions / starter-content-
prompt), and the 3 side panels (runtime-aside-panel / cloud-waitlist-
expand / compact-runtime-row) will be surfaced + swept by the global
ESLint switch (next commit).
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): flip ESLint to glob + drain remaining hardcoded EN
ESLint i18next/no-literal-string now applies to **/*.tsx by default
instead of an explicit allow-list. Files that genuinely still need
hardcoded EN are listed in STILL_HARDCODED — concrete, finite, and
the goal is to drain that list to zero.
Tail strings translated in this commit (surfaced by the global flip):
- common/task-transcript/agent-transcript-dialog.tsx — full dialog:
status badge (Running / Completed / Failed), sr-only DialogTitle,
Filter dropdown trigger + Clear filters, Copy all / Copy filtered /
Copied, tool-calls + events metadata chips with pluralization,
events-filtered "{{shown}} of {{total}}" interpolation, "Waiting
for events..." live state, "No execution data recorded." past
state. New `transcript` block in agents namespace.
- runtimes/components/charts/activity-heatmap.tsx — Less / More
legend labels around the contribution-style heat squares.
- search/search-trigger.tsx — sidebar Search... button label.
⌘ glyph wrapped in {} to satisfy the linter (mono shortcut symbol,
not translatable).
Holdouts (STILL_HARDCODED, ~14 files): the deep onboarding steps
(workspace / runtime-connect / platform-fork / agent / first-issue /
cli-install-instructions, plus 4 ancillary panels), the runtimes
usage-section + KPI cards, and 5 minor agent visual primitives
(sparkline / agent-presence-indicator / agent-profile-card /
visibility-badge / char-counter). Each one gets a dedicated future
pass; the global rule prevents new hardcoded strings from landing
elsewhere.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): drain agent visual primitives + onboarding small components
8 files removed from STILL_HARDCODED:
agents/components/:
- char-counter — over-limit text with {{count}} interpolation
- visibility-badge — uses new agents.visibility.{private,workspace}.
{label,tooltip} block; drops VISIBILITY_LABEL/TOOLTIP imports from
core in favour of bundle-driven copy
- agent-presence-indicator — availability + workload labels via
$.availability[enum] / $.workload[enum] (cross-namespace),
queue-badge "+N queued" with pluralization
- agent-profile-card — Agent unavailable / Detail link / Owner /
Skills / Runtime / Unknown runtime / Archived chip / availability
line via cross-namespace lookup
agents.json: new presence + visibility + profile_card + char_counter
blocks.
onboarding/components/:
- compact-runtime-row — online/offline aria via agents.availability
- runtime-aside-panel — full content (What's a runtime / Good to
know / Swap anytime / Add more later / docs link)
- starter-content-prompt — full dialog (title / description with
inline emphasis / both buttons / 3 toasts)
- cloud-waitlist-expand — intro paragraph + warning span / email
+ reason labels + placeholders + Optional badge / Join + on-list
states / both toasts
onboarding/steps/:
- cli-install-instructions — copy aria + intro + 2 step labels
onboarding.json: new runtime_aside / cli_install / starter_content /
cloud_waitlist blocks.
Tests for step-platform-fork + step-runtime-connect wrapped in
<I18nProvider> with EN bundle so /you're on the list/i etc. still
matches the resolved translations.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate onboarding deep steps
The 5 large onboarding steps that were deferred from earlier passes,
plus their support helpers, all wired through useT.
step-first-issue (final beat — flips onboarded_at):
- error_title / Retry / retry_failed toast / finishing / opening
states.
step-agent (creates the user's first agent):
- Templates moved from a module-level const to a useT-driven
useAgentTemplates() hook. Names + emoji stay constant (visual
identity), labels + blurbs + instructions resolve from the
bundle. coding / planning / writing / assistant — all four
templates ship a full zh translation that reads naturally.
- Recommended badge, eyebrow + headline + lede, footer hint,
Create {{name}} CTA, create_failed toast.
- Right-rail "About agents" panel (4 way-items + headline +
add-more hint + docs link).
step-workspace (create or pick existing):
- 5 footer states (open / creating / creating-pending / name-first
/ pick), all hint + CTA strings via interpolation.
- Name + URL + slug placeholders, issue-prefix preview spans,
Create-new card title + subtitle.
- 8-row WorkspacePreviewCard sidebar (Inbox / Issues / Agents /
Projects / Autopilot / Runtimes / Skills / And more) — every
label + meta strapped to bundle keys.
- 4 perks (assign / chat / invite / switch) + 3 next-steps
(runtime / agent / starter), 2 toasts (slug-conflict / failed).
- `multica.ai/${slug}` mono URL escaped via template-literal
expression so the linter sees a JS string.
step-runtime-connect (desktop scan flow):
- 3 phase headlines + ledes (scanning / found / empty), trust-strip
status (all online / N online / none online) with pluralization,
online/offline labels, Skip / Continue / Selected hint.
- Empty-view 2 cards (skip + waitlist) and the cloud waitlist
dialog wrapper.
step-platform-fork (web fan-out):
- Eyebrow + headline + lede, footer hint with 3 phase variants.
- Primary download card (before/after click) + 2 alt cards (CLI /
cloud) + CLI dialog with 4 elapsed-time stages (normal / midway /
slow / stalled), live-listening header, runtime-connected
pluralization, cloud waitlist dialog.
ESLint: STILL_HARDCODED list shrunk from 14 entries to 1 — only
runtimes/components/usage-section.tsx (chart-heavy KPI panel)
remains.
Verified: typecheck + 277/277 tests + 0 i18n lint errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate runtimes usage panel + drop STILL_HARDCODED
Final i18n holdout: the runtimes usage panel (KPI hero, WHEN chart
tabs, cost-by breakdowns, daily breakdown table) is wired through
useT("runtimes"). With this drained, the eslint scaffolding for
explicit holdouts is removed — every JSX text node in @multica/views
now flows through i18n.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): drain rollout gaps + add cross-device sync
Lands the post-review punch list for the i18n rollout: closes correctness
gaps that would have shipped silently, and adds the missing cross-device
locale sync the rollout's docs already promised.
Coverage:
- Register issues + agents namespaces in RESOURCES (90 useT call sites
were rendering keys-as-text in production)
- Harden parity test to compare RESOURCES keys against on-disk JSON
files, so a future missing namespace registration fails loudly
- Server-side language whitelist in UpdateMe + reject-unsupported test
- Safe SupportedLocale resolution in appearance-tab (no more `as` cast
on a region-tagged BCP-47 string)
- HTML lang attribute uses zh-CN (not zh-Hans) for screen reader / CJK
font-stack compatibility
- Cookie Secure flag on https
- Pulled createBrowserCookieLocaleAdapter out of the server-safe entry
into a new @multica/core/i18n/browser subpath; document.cookie access
can no longer leak into Edge middleware imports
Cross-device sync:
- New UserLocaleSync component mounted in CoreProvider; on login, if
user.language differs from the active i18n.language, persist via the
adapter and reload. Both apps benefit
- Desktop main process tracks system locale and emits IPC on focus when
it changes; renderer reloads only when the user has no explicit
Settings choice (their preference still wins)
Tests:
- pickLocale / matchLocale (11 cases incl. region-tagged BCP-47, malformed
tags, zh-Hant collapse-to-zh-Hans semantics)
- browser-cookie-adapter (6 cases under jsdom)
- Shared renderWithI18n helper at packages/views/test/i18n.tsx that wraps
the real RESOURCES map; future tests opt in instead of inlining a
per-file TEST_RESOURCES slice that goes stale silently
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(conventions): consolidate naming + i18n glossary into docs site
Single source of truth for code naming, i18n translation glossary, and
Chinese voice rules. Previously split between packages/views/locales/glossary.md
and scattered comments — now lives at apps/docs/content/docs/developers/conventions.{mdx,zh.mdx}
with both English and Chinese versions kept in sync.
Three sections per page:
1. Code naming — routes, packages, files, DB, Go, TS, commits
2. i18n translation glossary — entity vs concept rule, what to translate,
word combination, plurals, interpolation, key naming
3. Chinese voice + style — punctuation, principles, where to look in doubt
Side effects:
- packages/views/locales/glossary.md collapses to a stub redirecting to
the docs page; do not edit it
- CLAUDE.md gets a new top-level "Conventions reference" section so any
Claude session sees the pointer before any other rule
- apps/docs/content/docs/developers/ gets a stub English meta.json so the
conventions page is reachable on the EN side (contributing.zh.mdx /
architecture.zh.mdx remain ZH-only — separate work)
- Both root sidebars get a new "Developers" group
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): apply zh voice rules + translate project/autopilot
Two-part cleanup driven by the conventions doc landed last commit:
Voice violations (mechanical sweep across 10 zh-Hans namespaces):
- 「」 (Japanese-style brackets) → \" to match the EN source's straight
double quotes (~13 sites)
- … (single-char ellipsis) → ... three dots (~43 sites)
- Drop translation-ese pronoun "我们" where it's a pure narrator
("我们已发送" → "已发送", "我们替你托管" → "由 Multica 托管"); keep
"告诉我们" where "we" is the legitimate brand recipient
- "作为父级 / 作为子级" → "设为父级 / 设为子级"
- "任务" mistranslated as the task entity → `task` (lowercase EN entity)
- Dialog title "Autopilot" → "autopilot"
Translate project / autopilot per industry consensus:
- `project` → 「项目」 (~42 value sites). Feishu / Tower / Teambition /
PingCode / GitHub Projects all translate; no Chinese product keeps
`project`.
- `autopilot` → 「自动化」 (~34 value sites). Avoids the Tesla-style
「自动驾驶」 association; matches Notion / Feishu's industry term.
- Issue / skill / task remain lowercase EN per dev-team familiarity.
- Sidebar nav-label entities get Title Case ("Issue" / "Skill" / "我的
Issue") so the entry-point label reads as a proper UI signal; body
prose stays lowercase.
Conventions doc (EN + ZH) reflects the decision and adds a "why these
translate but issue/skill/task don't" rationale block.
Verification: parity test 45/45, full monorepo typecheck green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(i18n): translate chat session delete + project resources section
Two features main shipped while this branch was idle never went through
the i18n pass:
- Chat session delete confirmation dialog (#2115) and history toggle
tooltip (#2117): adds session_history.delete_dialog.* and
session_history.row_delete_*, plus window.history_show_tooltip /
history_back_tooltip.
- Project resources sidebar (#1926/#2080/#2111): entire component
including toasts, popover form, attach/remove tooltips. New
projects.resources subtree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(server): return 500 for transient DB errors in daemon task lookup
requireDaemonTaskAccess used to turn any GetAgentTask error into
404 "task not found", including transient DB connection / pool errors.
Combined with PR #2107 — which added 404+"task not found" as a daemon
cancellation trigger — that means a single DB hiccup could kill an
in-flight agent run.
Distinguish pgx.ErrNoRows (real "task gone", 404) from other errors
(transient, 500 + warn log) using the existing isNotFound helper.
Tests cover both paths via the mockDB pattern already used by
TestFindOrCreateUserGating.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): honor task-deleted signal in post-runTask completion guard
The final pre-completion check in handleTask only looked for
status == "cancelled" and ignored errors. After PR #2107 added a 404
task-deleted cancellation path to the in-flight watcher, this trailing
guard fell out of sync — if the task was deleted between the watcher's
last poll and runTask returning, handleTask would still try to call
CompleteTask and only learn about the deletion via the 404 from that
callback.
Reuse shouldInterruptAgent so the same truth table (cancelled OR
404 task-not-found, but NOT transient errors) drives both polling and
the final guard.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
When the server deletes a task while the daemon's agent is still running
(issue removed, agent reassigned, workspace cleanup), GetTaskStatus
starts returning 404 "task not found". The previous polling loop only
checked for status == "cancelled" and silently swallowed the error, so
the local agent kept emitting tool calls against a dead task until its
own timeout fired — minutes of wasted model spend and patch_apply
operations against a workdir nobody would consume.
Changes:
- Add isTaskNotFoundError next to isWorkspaceNotFoundError so the daemon
can distinguish "task gone" 404 from "workspace gone" 404 (already
handled separately) and from generic network errors.
- Extract the cancellation polling goroutine in handleTask into
watchTaskCancellation, plus a pure shouldInterruptAgent decision
helper. The pure helper makes both signals (cancelled status and 404
task) easy to unit-test without spinning up a real backend.
- Trigger interruption on the new 404 path. Transient errors (5xx,
network) intentionally still don't cancel — the next poll will retry
and a flaky link should not kill an in-flight agent.
Tests cover the helper truth table, the existing "status cancelled"
path, the new "task deleted (404)" path, and a negative case ensuring a
running task is not interrupted.
Co-authored-by: “646826” <“646826@gmail.com”>
`ensureSymlink` previously short-circuited whenever `dst` already existed
as a regular file ("Regular file exists — don't overwrite"). On Windows
that branch is reachable via the createFileLink copy fallback that fires
when `os.Symlink` is unavailable, so once a per-task `codex-home/auth.json`
was written as a copy it would never be refreshed by subsequent
Prepare/Reuse calls. If the shared `~/.codex/auth.json` rotated (e.g.
Codex Desktop refreshed the token in the background), the daemon kept
handing Codex a now-revoked refresh_token, which the OAuth server
rejected with `refresh_token_reused` / `token_expired`. Renaming the
workspace directory was the only recovery path.
Treat any non-matching dst — wrong-target symlink, broken symlink, or
stale regular file — as something to delete and re-create via
createFileLink, so each Prepare/Reuse mirrors the current shared source.
Add a `logCodexAuthState` info log (file kind, link target, size, mtime —
never contents) so operators chasing the same symptom can see at a glance
whether the per-task home is tracking the shared auth or has drifted.
Tests cover: stale regular-file dst is replaced, copy-fallback dst is
refreshed when the shared source rotates, and a high-level
prepareCodexHome regression simulating the Windows + token-rotation
scenario from issue #2081.
Co-authored-by: multica-agent <github@multica.ai>
A non-trivial fraction of completed task workdirs (~28% in field reports)
end up with .gc_meta.json files containing issue_id: "". Empty issue_id
defeats the daemon's own GC loop (gc.go:139 calls
GetIssueGCCheck(meta.IssueID)) and external retention scripts that
cross-reference issue status before deleting orphaned workdirs.
Refuse to write the file when issueID is empty, logging a Warn so
operators have a starting point for debugging the upstream race
condition. Skip is preferred over a sentinel-marker file: it keeps the
data invariant clean (a .gc_meta.json file always carries a valid
issue_id) and matches the repo CLAUDE.md preference for not preserving
dual-state behavior.
WriteGCMeta now takes a *slog.Logger so it can emit the warning. The
package already uses log/slog (Prepare/reuseEnv), and daemon.go:884 has
taskLog in scope at the only call site.
Closes#1913
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
When the local state.db of an ACP backend (hermes, kimi, kiro) is wiped
— crash, config change, manual kill, container reset — the backend's
session/resume (or session/load, in kiro's case) silently creates a
brand-new session rather than failing, and returns the new id in the
response. Today the daemon ignores the response and stamps
sessionID = opts.ResumeSessionID across all three backends, so every
subsequent session/prompt is addressed to a session id the backend has
no record of. The task fails with JSON-RPC -32603 (Internal error) on
the very first turn, with no operator-visible signal that the problem
is a session-id mismatch one layer down.
The behavior is invisible: agent shows "started", then "failed" with a
generic Internal error. Reproducing in production took repeated runs
because nothing in the logs pointed at the silent reset.
Fix: route all three ACP backends through a small `resolveResumedSessionID`
helper that:
- prefers the id the backend returned in its response (the canonical
id; the one the backend will accept on the next call)
- falls back to the requested id when the response is malformed,
empty, or omits sessionId — defensive fallback so older / non-
conforming backends (notably kiro's current session/load shape)
behave identically to today
- signals (via a bool) when the id changed, so the caller logs a Warn
with `backend=<hermes|kimi|kiro>` and operators can grep for silent
state resets to correlate them with task failures
Why this is at the backend layer rather than the daemon's existing
session-resume fallback: server/internal/daemon/daemon.go:1554-1566
already retries with a fresh session when resume fails, but it gates
on `result.Status == "failed" && result.SessionID == ""`. The backend
WILL hand back a result.SessionID — just the new one it silently
committed to — so the daemon-level fallback never fires for this
failure mode.
The helper is also what session/new already uses (extractACPSessionID,
documented in code as "Shared by all ACP backends"). session/new
extracts the canonical id from the response; session/resume just
didn't, until now.
Coverage:
- hermes.go: confirmed bug, root cause of -32603 in production
- kimi.go: same code shape, same protocol method, same response
schema as hermes (per extractACPSessionID's comment) — same bug
- kiro.go: same code shape, different method (session/load). Current
observed response doesn't include sessionId, so the defensive
fallback means today's behavior is preserved. Routing through the
same helper means a future kiro release that DOES return a sessionId
on silent reset works the same way as hermes/kimi without another
diff.
Tests (server/pkg/agent/hermes_test.go — helper covers all three
backends, no per-backend duplication):
- TestResolveResumedSessionIDMatching — backend confirms requested id
- TestResolveResumedSessionIDDifferent — backend returned a new id;
caller is told to switch
- TestResolveResumedSessionIDEmptyResponse — older / malformed body;
defensive fallback to requested id (covers kiro's current shape)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(web): rewrite 404 page using design tokens
Replace editorial-style 404 (hardcoded cream/ink/terracotta colors,
Instrument Serif font, fluid clamp() typography) with a minimal version
using semantic tokens and the project's buttonVariants helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workspace): break NoAccessPage redirect loop by clearing stale cookie
The web proxy redirects / to /<lastSlug>/issues based on the
last_workspace_slug cookie alone, with no access check. When a user
gets evicted from a workspace, the cookie still points at it; clicking
"Go to my workspaces" then loops: NoAccessPage -> / -> proxy ->
same bad slug -> NoAccessPage.
Clear the cookie on mount so the proxy falls through to the landing
page, which resolves the correct destination via the workspace list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): mark not-found as client to allow buttonVariants import
buttonVariants is exported from a "use client" module, so calling it
from a server component is rejected by Next 16's directive checks.
Production build of /workspaces/new prerender failed because of this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(daemon): isolate runtime poll & heartbeat schedules per runtime
A daemon serving multiple workspaces ran a single round-robin poll loop
and a single HTTP heartbeat loop across every registered runtime. A 30s
HTTP timeout for any one runtime serialized that delay across all the
others — observed in production as one workspace's runtimes wedging
every other workspace's runtimes on the same daemon.
This change:
- Replaces the shared runtime-set channel with a multi-subscriber
watcher so taskWakeupLoop, heartbeatLoop, and pollLoop can each
react to runtime-set changes independently.
- Splits heartbeatLoop and pollLoop into supervisor + per-runtime
worker goroutines. Each runtime owns its claim cadence and its
heartbeat ticker, so a slow request on one runtime no longer blocks
any other.
- Stagers the per-runtime heartbeat first tick by a jittered delay up
to one full interval to avoid a thundering herd at startup.
- Sizes the WS writer channel to scale with the runtime count
(max(16, 2*N)) so a full per-runtime heartbeat batch always fits;
the previous fixed 8-slot buffer dropped heartbeats whenever a
daemon watched more than ~8 runtimes.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): acquire execution slot only after ClaimTask, drain pollers before taskWG
Two issues from review on the previous commit:
1. Acquiring the shared task slot before ClaimTask reintroduced the very
head-of-line blocking the refactor was meant to remove. With
MaxConcurrentTasks=1, a slow claim on one runtime parked the only slot
for the duration of the HTTP timeout (up to 30s), starving every other
runtime's claim attempts. Slots are now acquired after the claim
returns a task; other runtimes' pollers stay free to claim. The
already-dispatched task waits for a slot under MaxConcurrentTasks
bounds, which is the same backpressure shape we had before.
2. pollLoop's shutdown path called taskWG.Wait immediately after
cancelling pollers, but a poller could still be between ClaimTask
returning a task and taskWG.Add(1). When taskWG's counter is zero
that races with Wait — undefined sync.WaitGroup misuse, sometimes
panic. Added a pollerWG so the supervisor blocks until every poller
goroutine has actually returned before reaching taskWG.Wait.
Tests:
- TestRunRuntimePollerIsolatesSlowRuntime now uses MaxConcurrentTasks=1
(was 4) so it would have failed under the old slot-before-claim path.
- New TestPollLoopShutdownWaitsForPollersBeforeTaskWG drives the exact
race window — claim returns a task at the same moment shutdown fires —
under -race.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): acquire slot before ClaimTask so capacity-waiters never enter dispatched
The previous commit moved slot acquisition AFTER ClaimTask to address a
review concern about head-of-line blocking with MaxConcurrentTasks=1.
That introduced a strictly worse failure mode: server-side ClaimTask
flips the task to `dispatched` immediately (agent.sql:174-176), and the
runtime sweeper fails any task in `dispatched` for >300s with
`failed/timeout` (runtime_sweeper.go:25-28). When local execution
capacity is full and the next claimed task can't acquire a slot within
5 minutes, the user sees the exact failure this issue is fixing —
`dispatched_at` set, `started_at` NULL, `failure_reason=timeout`.
Reverted to slot-before-claim. The trade-off is the original review
concern: with MaxConcurrentTasks=1 and a slow ClaimTask, other
runtimes' claims are delayed by up to client.Timeout=30s. That's a
30s polling delay, not a failure — server-side those tasks remain
`queued` (no timeout in that state) until a slot frees. 30s ≪ 300s,
so other runtimes' tasks cannot get sweeper-failed because of this.
The pollerWG fix from the previous commit (avoiding sync.WaitGroup
misuse on shutdown) is preserved.
Tests:
- TestRunRuntimePollerIsolatesSlowRuntime: MaxConcurrentTasks back to
4 (the pre-issue baseline) — the headroom case where slot-before-
claim still gives full per-runtime isolation.
- New TestRunRuntimePollerSkipsClaimWhenAtCapacity: holds the only
slot and verifies the poller never calls ClaimTask while sem is
empty. The previous "claim first" path would have failed this.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): add resource_count breadcrumb to project responses
Closes#2087
`multica project get` previously returned project metadata with no signal
that resources existed. Agents that fetched a project this way had no way
to discover its attached resources without already knowing about
`/api/projects/{id}/resources` or the on-disk `.multica/project/resources.json`.
Rather than inline the full resource list into the parent payload (which
conflates parent metadata with a child sub-collection and locks the
resource_ref shape into the project endpoint's contract), this adds a
scalar `resource_count` breadcrumb to ProjectResponse. The actual list
stays at the dedicated sub-collection endpoint.
Changes:
- GetProjectResourceCounts :many — new batched sqlc query
- ProjectResponse.ResourceCount populated in GetProject, ListProjects,
SearchProjects, and the with-resources CreateProject echo
- multica project get prints a stderr hint pointing at
multica project resource list <id> when count > 0; the JSON on stdout
stays parseable
- Meta-skill (runtime_config.go) lists multica project get and
multica project resource list in Available Commands so agents that
read CLAUDE.md / AGENTS.md know about both paths
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): wire ResourceCount through Update + Create event payload
Review feedback on #2118.
- UpdateProject now reloads ResourceCount before responding/publishing.
Previously a title- or status-only PUT served (and broadcast over WS)
resource_count: 0 even when resources existed.
- The with-resources CreateProject path sets resp.ResourceCount before
the project:created publish, so the WS event payload matches the HTTP
echo. The hand-rolled response map collapses to an embedded
ProjectResponse + resources array — one source of truth for the
serialized shape.
- packages/core/types/project.ts: Project gains resource_count: number
to keep the TS contract aligned with the server response.
Tests:
- TestProjectResourceCountBreadcrumb extends to assert UpdateProject
preserves the breadcrumb.
- TestCreateProjectWithResourcesEchoesCount asserts the create echo
carries resource_count matching the attached resources.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The runtimes list page renders a CostCell per row that only displays a
7d cost total plus a 7d-vs-prior-7d delta. Until now each cell still
fetched a 180d usage window so the cache key matched the runtime-detail
page (clicking a row would pre-warm detail). The side effect was N
parallel 180d in-line aggregations against task_usage on every list
visit, one per runtime, which dominated DB load for this view.
Switch the cell to a 14d window — exactly the data it actually needs
for cost7d + costPrev7d. Detail still owns its own 180d query; the
worst case after this change is one extra request on first navigation
into detail, in exchange for a large steady-state reduction on the
list page (down to 14d × N instead of 180d × N, ~13× fewer rows
scanned per request).
This is the frontend half of the runtime-usage perf work tracked in
MUL-1748. The backend index + daily rollup changes will land
separately.
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
ChatSessionHistory was already implemented but unreachable: nothing in the
app rendered it and there was no UI to toggle showHistory. The trash icon
on each session row was therefore invisible.
Adds a History icon button to the chat-window header that toggles the
panel; when on, it renders ChatSessionHistory in place of the message
list and input. Per-row delete (hover trash + AlertDialog) works as
designed.
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): support deleting chat sessions
Replaces the unreachable archive endpoint with a real hard delete and
exposes it from the chat history panel.
- DELETE /api/chat/sessions/{id} now hard-deletes the session and its
messages (CASCADE), cancels any in-flight tasks before removal so the
daemon doesn't keep running work whose result has nowhere to land,
and broadcasts chat:session_deleted.
- Frontend adds a per-row delete button with a confirmation dialog,
optimistically drops the session from both list caches, and clears the
active session pointer locally + on other tabs via the WS handler.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): make session delete atomic and keep archived sessions read-only
Address review feedback on #2115.
- DeleteChatSession now runs lock + cancel + delete in a single tx and
only broadcasts events post-commit. The new LockChatSessionForDelete
query takes FOR UPDATE on chat_session, which blocks the FK validation
of any concurrent SendChatMessage trying to enqueue a task for this
session — that insert fails after we commit, so it can no longer
produce an orphaned task whose chat_session_id is nulled by
ON DELETE SET NULL. Cancel failure now aborts the delete instead of
warn-and-continue.
- SendChatMessage refuses non-active sessions again. The archive code
path is gone, but legacy rows with status='archived' may still exist
in the DB; keep the guard until we explicitly migrate them.
- Frontend re-reads allChatSessionsOptions to disable ChatInput on
legacy archived sessions so the UX matches the server-side guard.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add --assignee-id / --to-id / --user-id for unambiguous targeting
`multica issue {create,update,list}`, `issue assign`, and `issue subscriber
{add,remove}` accepted only fuzzy name matching, which fails in workspaces
where one user's name is a substring of another (e.g. agent "J" vs
"Cursor - J" / member "Jiayuan"). #1642 added UUID acceptance through the
existing flags, but there was still no explicit path that signals "this is a
UUID, not a name" — important for scripts that read IDs from
`multica workspace members --output json`.
Adds an `-id`-suffixed counterpart for every assignee-taking flag:
- `issue list` : --assignee-id
- `issue create` : --assignee-id
- `issue update` : --assignee-id
- `issue assign` : --to-id
- `issue subscriber {add,remove}` : --user-id
The new flags route through `resolveAssigneeByID`, a strict resolver that
requires a canonical UUID and fails with a clear error when the entity is
not in the workspace (no name fallback). A shared `pickAssigneeFromFlags`
helper enforces mutual exclusion between the name and id flags so a script
that accidentally sets both never silently applies one over the other.
Refs MUL-1254.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): detect assignee flag presence via Changed, not value-emptiness
`pickAssigneeFromFlags` previously branched on `flag value != ""`, so
explicitly passing an empty UUID silently routed through the "no flag set"
path:
multica issue list --assignee-id "" # listed every issue
multica issue create --assignee-id "" # created an unassigned issue
multica issue subscriber add --user-id "" # subscribed the caller
This is exactly the failure mode the strict-UUID flag was added to prevent —
a script interpolating `--assignee-id "$MAYBE_UUID"` against a missing env
var should fail loudly, not silently degrade to a different operation.
Switch the picker (and the assign-command top-level guard) to use
`Flags().Changed`, so an explicit empty value reaches `resolveAssigneeByID`
/ `resolveAssignee` and surfaces a clear "expected a canonical UUID" /
"no member or agent found matching" error.
Co-authored-by: multica-agent <github@multica.ai>
* docs(cli): cover --assignee-id / --to-id in user docs and quick-create prompt
Follow-up to the --*-id flag rollout: surface the new flags everywhere the
old ones are documented so users (and agents) can discover them.
- assigning-issues.{mdx,zh.mdx}: the page explicitly calls out the
duplicate-name footgun ("first one listed wins, so rename before
assigning") — replace that workaround with a --to-id <uuid> example
- cloud-quickstart.{mdx,zh.mdx}: add a --to-id hint after the substring-
match callout so first-time users learn about the strict path
- internal/daemon/prompt.go (quick-create injected prompt):
- default-to-self: pass --assignee-id <task.Agent.ID> instead of
--assignee <name>; the picker agent's UUID is already in scope and
UUID matching is unambiguous in workspaces with overlapping agent
names (J / Cursor - J / Pi - J etc.)
- user-named: tell the agent to prefer --assignee-id <uuid> using the
user_id/id from the JSON it already fetched; --assignee <name> stays
a fallback for unambiguous workspaces
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(storage): build region-qualified S3 public URLs (#2051)
The uploadedURL fallback (no CloudFront, no custom endpoint) wrote
"https://<bucket>/<key>" — missing the ".s3.<region>.amazonaws.com"
suffix — so any deployment that pointed S3_BUCKET at a real AWS bucket
without a CDN got broken image URLs back to the client. Avatar URLs
were persisted in this broken form on the user/agent rows, so profile
pictures uploaded via the SDK never rendered.
- Track S3_REGION on S3Storage and emit
https://<bucket>.s3.<region>.amazonaws.com/<key> by default;
fall back to path-style https://s3.<region>.amazonaws.com/<bucket>/<key>
when the bucket name contains dots, since the AWS wildcard cert
can't validate dotted virtual-hosted hosts.
- Teach KeyFromURL to recognise the new region-qualified hosts (both
styles) and keep recognising the legacy bucket-only host so historical
records can still be deleted/migrated.
- Document that S3_BUCKET is the bucket name only, not a hostname,
in env-vars docs (en+zh), self-hosting guides, and .env.example.
Co-authored-by: multica-agent <github@multica.ai>
* feat(storage): warn at startup when S3_BUCKET looks like a hostname
Catches the most common misconfiguration shape (S3_BUCKET set to
"<bucket>.s3.<region>.amazonaws.com") with a startup log line so
operators don't silently end up with a config that signs uploads
against an invalid bucket name.
A real bucket name can never legitimately contain "amazonaws.com",
so the check is a single substring match — no false positives
worth carving out.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The repo button in the Add Resource popover used the native `disabled`
attribute when a repo was already attached. Browsers suppress pointer
events on disabled form controls, so the tooltip on the URL text never
fired for attached rows — the issue spec calls out "hovering over any
URL should also show the complete URL in a tooltip".
Switch to `aria-disabled` plus a click guard so the row still announces
as disabled to assistive tech, looks the same visually, and is no
longer click-able, but hover still reaches the tooltip trigger.
Co-authored-by: multica-agent <github@multica.ai>
Replace `scope.replace("project:", "")` with the `projectId` already
held by `ProjectDetail`, so the create-issue handler in the empty
state no longer depends on the `project:<id>` scope-string format.
Co-authored-by: multica-agent <github@multica.ai>
When a project has no issues, show a [+ New Issue] button that opens
the create-issue dialog with the project pre-selected. Previously
users had to navigate to the issues page and manually assign the
project.
Also add tooltips to repository URLs in the Resources section so
truncated URLs can be read in full on hover.
Fixes#2078
Mobile project-detail mounted its <Sheet> with open=true for one render —
useIsMobile() reports false on first render and flips to true on the next,
so the mobile branch briefly mounted Base UI Dialog open, painted its
fixed inset-0 z-50 backdrop and locked scroll. The follow-up useEffect
toggled it closed within the same animation cycle, leaving Dialog's
pointer-events/inert/scroll-lock state stuck on mobile.
Mirror packages/views/issues/components/issue-detail.tsx by keeping
desktopSidebarOpen (default true) and mobileSidebarOpen (default false)
as separate states, binding the mobile <Sheet> to mobileSidebarOpen only.
The single-state pattern dates back to #1087, where issue-detail and
project-detail received mobile-Sheet support together but only
issue-detail used split state.
* refactor(quick-create): remove daemon CLI version gate
Local-source daemons report dev-suffixed versions (e.g.
v0.2.15-235-gdaf0e935) that the picker pre-check and server gate both
treat as too old, blocking quick-create during local testing.
Drops the gate end-to-end: removes MinQuickCreateCLIVersion +
CheckMinCLIVersion in pkg/agent, the checkQuickCreateDaemonVersion
handler and readRuntimeCLIVersion helper in handler/issue.go, and the
mirrored cli-version.ts plus the modal's pre-check, blocked-state UI,
and daemon_version_unsupported error branch.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(quick-create): skip daemon CLI version gate in dev
Restores the gate (reverts the full-removal commit) and bypasses it in
non-production environments instead. The motivation for the original
removal — local source-built daemons report a `git describe` version
like v0.2.15-N-gHASH that parses below 0.2.20 and blocks dev testing —
is now handled by checking APP_ENV on the server and NODE_ENV on the
client. Production keeps the original "needs upgrade" UX.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(quick-create): exempt git-describe daemons instead of env bypass
Replaces the per-environment bypass added in the previous commit with a
shared daemon-version signal. CheckMinCLIVersion / checkQuickCreateCliVersion
now treat any daemon whose CLI version matches the
`vX.Y.Z-N-gHASH[-dirty]` git-describe shape as OK; tagged releases keep
going through the normal min-version comparison.
Why: Emacs flagged that (a) NODE_ENV !== "production" also disables the
gate on staging and other non-prod deployments, undoing the protection
for the case the gate was originally written for, and (b) NODE_ENV (web
client) and APP_ENV (server) are not equivalent, so the modal pre-check
and server gate could disagree on the same request. Both go away when
the signal is intrinsic to the daemon's version string.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Consecutive "completed the task" entries from the same agent now merge
into a single line showing the count (e.g. "completed the task (7 times)")
regardless of time gap. Other activity types keep the existing 2-minute
coalescing window.
Closes MUL-1709
The sidebar search trigger, quick-create-issue modal, and feedback modal
hardcoded the Mac glyphs (⌘, ↵) for their keyboard hints, so Windows and
Linux users always saw Mac shortcuts even though the underlying handlers
already accept metaKey || ctrlKey.
Extract a small platform helper (isMac, modKey, enterKey, formatShortcut)
in packages/core/platform/keyboard.ts and route all four affected sites
(plus the editor bubble menu, which had the same logic inlined) through
it, so non-Mac users see Ctrl+K, Ctrl+Enter, etc.
Closesmultica-ai/multica#2056
The uniqueness check on workspace invitations only filtered by
status='pending', not by expires_at. Combined with the partial unique
index idx_invitation_unique_pending (also keyed only on status), a
past-due pending row permanently blocked re-inviting the same email.
Now, before creating a new invitation, the handler flips any past-due
pending row for the same (workspace_id, invitee_email) to 'expired',
freeing the unique slot. Also tightens GetPendingInvitationByEmail to
require expires_at > now(), matching the existing list queries.
Closesmultica-ai/multica#2055.
* feat(agents): make agent detail page mobile responsive (#1)
Stack the inspector + overview pane vertically below md, switch the
shell to page-level scroll so the inspector flows naturally, give the
overview pane a min-h-[60vh] floor so tabs stay usable, and let the
5-tab nav scroll horizontally on narrow viewports.
* fix(settings): make Repositories tab and Settings shell mobile-responsive (#2)
The Settings shell used a fixed w-52 sidebar with no responsive behavior,
leaving almost no room for tab content on phone-width viewports. Stack the
nav above the content on mobile, scale inner padding, and let the
Repositories tab's input/button rows wrap rather than overflow.
Pasting `line1\n\nline2` while the caret was inside a code block ran the
text through the Markdown parser, which split on the blank line and tore
the code block open, dropping the trailing content into a sibling
paragraph.
Detect the codeBlock parent on `handlePaste` and insert the clipboard
text verbatim instead. Code blocks have `code: true`, so newlines stay
literal — exactly what users expect when pasting code or logs.
Closes#1982
* fix(codex): handle MCP elicitation server requests correctly
Fixes#1942.
handleServerRequest responded with {} to unrecognized Codex server
requests including mcpServer/elicitation/request. Codex 0.125+ expects
{action, content, _meta} for elicitation — the empty object causes a
deserialization error and the MCP tool call is reported as user-rejected.
Changes:
- Add mcpServer/elicitation/request case with correct response schema
- Add respondError helper for JSON-RPC error responses
- Return proper JSON-RPC method-not-found error for unknown server
requests instead of silent empty object
- Add tests for MCP elicitation and unknown method handling
* fix: use cfg.Logger instead of global slog in codex handleServerRequest
Switch the unhandled-server-request warning from global slog.Warn to
c.cfg.Logger.Warn for consistency with all other log calls in codex.go.
This ensures the warning appears in daemon run-logs and per-task
pipelines where operators look during triage.
`onIssueLabelsChanged` patched the embedded `labels` field in the
issue list and detail caches but never touched `labelKeys.byIssue`,
the cache backing the issue-detail Properties LabelPicker. Mutations
already covered all three caches; WS-driven changes (agents, other
tabs) left the picker stale until remount, since `staleTime: Infinity`
plus `refetchOnWindowFocus: false` prevent recovery on focus.
When creating an issue with agent, the input content was lost when
navigating away (e.g., to view a ticket) and returning. Manual create
already persisted its draft - now agent create does too.
Changes:
- Add prompt field to useQuickCreateStore (persisted with workspace)
- AgentCreatePanel reads initial prompt from draft store if no transient
data.prompt is provided
- onUpdate now saves prompt to draft store (not just hasContent)
- clearPrompt() called after successful submit
Fixes: #1957
* feat(chat): support fullscreen mode similar to Linear
When the expand button is clicked, the chat window now fills the entire
content area (inset-0) instead of scaling to 90% of parent. Resize
handles are hidden in fullscreen mode.
* fix(chat): use stacked card layout for fullscreen mode
Fullscreen chat now uses inset-3 with rounded corners, ring, and shadow
to create a stacked card effect on top of the content area — matching
the Linear design — instead of a flush inset-0 fill.
* feat(chat): add motion.dev spring animations for expand/collapse
- Install `motion` in @multica/views
- Replace CSS transitions with motion.div layout animation for
expand/collapse (spring-based FLIP), giving a natural bouncy feel
- Open/close uses spring scale + smooth opacity fade
- Layout animations are disabled during drag-to-resize (instant updates)
* fix(chat): remove spring bounce from expand/collapse animation
Use critically damped springs (bounce: 0) so the animation settles
directly at its target without overshooting.
* fix(chat): fix text distortion during expand/collapse animation
Use layout="position" instead of layout (full FLIP). Full FLIP uses
scale transforms to animate size changes, which distorts text and
child content. Position-only layout animates translate only — size
changes are instant, text stays crisp.
* fix: regenerate lockfile with pnpm@10.28.2
The lockfile was previously generated with pnpm 10.12.4, causing
unrelated churn (lost libc constraints, deprecated metadata). Reset
to main and regenerated with the repo's pinned pnpm@10.28.2 so
the diff is scoped to the new motion dependency only.
* fix(daemon): remove Co-authored-by hook when workspace setting is off
The prepare-commit-msg hook is installed in the bare repo's shared
hooks dir, so once installed it persists across worktrees. CreateWorktree
only installed the hook when the setting was enabled, but never removed
it — so disabling the workspace toggle had no effect on subsequent
commits.
Add removeCoAuthoredByHook and call it in both CreateWorktree branches
when the setting is disabled. Use a marker comment in the hook script so
removal only deletes hooks the daemon owns; user-installed hooks at the
same path are left alone.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): recognize legacy Multica prepare-commit-msg hook on removal
The first cut of removeCoAuthoredByHook only recognized hooks installed
by the new code (containing the multicaHookMarker sentinel). Bare clones
already on disk from previous daemon releases carry the older script
without that line, so toggling the workspace setting off would have
treated them as user hooks and left the trailer in place — exactly the
state reported in MUL-1704.
Match against a list of known daemon signatures (current marker + the
legacy "Installed by the Multica daemon." comment), and add a test that
seeds the verbatim legacy hook before CreateWorktree(... disabled) to
keep recognition aligned with what production hosts actually have on
disk.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(analytics): suppress PostHog $pageview on desktop tab/workspace switches
Desktop tab switches were emitting a $pageview every time the user clicked
between already-open tabs (or workspaces), since the tracker fired on any
change to the resolved active path. Real-data audit showed this was the
single largest source of PostHog quota burn — desktop accounted for 51% of
all $pageviews at ~34 pv/user/30d vs web's ~10 — and the re-emitted paths
add no signal because the original navigation already fired.
Detect "tab switch" as `(workspace, tabId)` identity changing while the
surface stays `tab`, and skip the capture in that case while still updating
the ref so the next in-tab navigation compares against the right baseline.
Login transitions, overlay open/close, and intra-tab navigation continue
to fire as before.
Co-authored-by: multica-agent <github@multica.ai>
* fix(analytics): only suppress $pageview for re-activations of known tabs
Prior commit suppressed every (workspace, tabId) change while the surface
stayed `tab`, which also swallowed the first $pageview for newly opened
tabs (`openInNewTab` / `addTab`) and for cross-workspace `switchWorkspace`
into a not-yet-seen tab.
Track an observed `(workspace, tabId) → path` map seeded from the
persisted tab store on mount. Suppress only when the active key is
already in the map AND its recorded path matches the current path —
i.e. genuine re-activation of an already-known tab. New tabs and
cross-workspace navigation to a fresh tab now correctly emit one
pageview.
Adds a vitest covering the three behaviors GPT-Boy flagged plus the
intra-tab navigation, overlay/login transitions, and persistence-restored
mount paths. Wires the `@/` alias into `vitest.config.ts` so component
tests can resolve renderer-relative imports.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(analytics): reuse tab-store helpers and inline observed-tabs seed
Replace the two ad-hoc tab selectors with the existing
`useActiveTabIdentity()` + `getActiveTab()` helpers from tab-store, which
already provide the (slug, tabId) primitive pair and the active tab
lookup with the same stability guarantees.
Move the observed-tabs Map seeding from a useEffect into a synchronous
first-render initializer. The seed runs once per mount before any
state-driven effect, so the previous useEffect-then-defensive-fallback
pattern in the second effect was unreachable.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix: handle square brackets in agent names for mention parsing (#1991)
The mention regex used [^\]]* to match labels, which broke when agent
names contained square brackets (e.g. David[TF]). The ] inside the name
caused the regex to stop matching prematurely, silently dropping the
mention.
Changes:
- Backend (mention.go): Switch to .+? (non-greedy) anchored on
](mention:// to correctly match labels with brackets
- Frontend (mention-extension.ts): Same regex fix in tokenizer, plus
escape [ and ] in renderMarkdown to prevent creating ambiguous
markdown syntax
- Add comprehensive tests for ParseMentions covering bracket names
Fixes#1991
* fix: add optional chaining for match group access
Fixes TS2532: Object is possibly 'undefined' on match[1] when calling
.replace() in the mention tokenizer.
* fix: tighten mention tokenizer to reject ordinary Markdown links
- Replace .+? with (?:\\.|[^\]])+ in start() and tokenize() regexes
so the label cannot cross a ]( Markdown link boundary
- Escaped brackets (\[ \]) from renderMarkdown() are still accepted
- Add frontend tokenizer/serializer round-trip tests:
- Plain mention
- Escaped brackets (David[TF]) round-trip
- Normal Markdown link + mention on same line (regression)
- Multiple links before mention
- Nested brackets (Bot[v2][beta])
- Issue mentions without @ prefix
Addresses review feedback on #1992.
* fix: add type assertions for tiptap MarkdownTokenizer interface in tests
The tiptap MarkdownTokenizer type allows start to be string | function
and tokenize to accept 3 arguments. Our extension always provides
single-arg functions, so cast them for TypeScript satisfaction.
Fixes CI typecheck failure in @multica/views package.
* fix: cast renderMarkdown to single-arg shape and reset file modes to 0644
Folds together everything that landed since the last public changelog
entry (0.2.21) into one 0.2.24 release note: repo checkout --ref,
agent avatar CLI, Hermes per-turn gate, multi-replica model picker
on Redis, Inbox long-timeline perf, and the rest of the smaller fixes
queued for tonight's release.
en.ts and zh.ts both updated.
Co-authored-by: multica-agent <github@multica.ai>
TestRegisterTaskReposSurvivesWorkspaceRefresh started flaking on CI
after #1988 (`feat: support repo checkout ref selection`) extended the
bare-clone path to run an extra `git fetch` to backfill
refs/remotes/origin/* under the new refspec layout. The race was
already latent: registerTaskRepos kicks off `go syncWorkspaceRepos(...)`
to clone a repo into the cache root, which in tests is `t.TempDir()`.
Once the test waited on `repoCache.Lookup` to return a path it would
proceed and return — but the bg goroutine was still inside
`ensureRemoteTrackingLayout` running git operations on the clone dir.
`t.TempDir`'s cleanup then races with those git commands and surfaces
either as "directory not empty" or "fatal: cannot change to ... No such
file or directory", with no hint that the failure is unrelated to the
test's actual assertion.
Track the background goroutine on the Daemon via a sync.WaitGroup and
expose `waitBackgroundSyncs()` for tests. `newRepoReadyTestDaemon`
registers a t.Cleanup that calls it, so every test that uses the
helper now drains in-flight syncs before t.TempDir cleanup runs. No
production-behavior change — registerTaskRepos still fires-and-forgets
from the caller's perspective.
Verified with `go test ./internal/daemon -run
TestRegisterTaskReposSurvivesWorkspaceRefresh -count=30` (was failing
within ~10 iterations before, 30 green after) and the full
`go test ./internal/daemon/...` suite.
Co-authored-by: multica-agent <github@multica.ai>
* perf(issue-detail): memoize timeline render to fix Inbox long-timeline freeze
On long-timeline issues (thousands of comments), opening from Inbox hard-freezes
the browser tab because every WS-driven parent re-render re-runs the full
react-markdown + rehype-* + lowlight pipeline for every comment. This is the
S3 mitigation for multica#1968:
- Wrap ReadonlyContent in React.memo so equal-content re-renders skip the
markdown pipeline entirely (the dominant cost per comment).
- Wrap CommentCard in React.memo so unrelated parent state updates don't
re-render every card.
- useMemo the timeline grouping in IssueDetail so the allReplies Map and
groups array references are stable across re-renders that don't change
timeline.
- Stabilize toggleReaction via a timelineRef so its identity doesn't change
on every WS event, which previously defeated CommentCard memoization.
Virtualization (S2) is the root fix for first-paint cost and lands separately.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issue-detail): destructure mutate/mutateAsync so CommentCard memo holds
Per review on PR #2025: TanStack Query v5 returns a fresh result wrapper
from useMutation on every render, with only the inner mutate / mutateAsync
functions guaranteed stable. The previous useCallback dependencies listed
the whole mutation object, so on every parent re-render the callbacks
flipped identity — defeating React.memo on CommentCard and leaving the
long-timeline mitigation only half-effective.
Pull just the stable handles into deps. Add a renderHook-based regression
test that re-renders useIssueTimeline twice and asserts the four callbacks
passed to CommentCard keep the same identity.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Hermes ACP can flush queued session updates from the previous turn
before the current turn actually starts — both as session/resume
history replay and as chunks queued before our session/prompt response
streams. Without a gate those updates were appended to output and
re-emitted to the UI, so the previous answer appeared duplicated next
to the new one. Closes#1997.
PR #1789 added the acceptNotification hook field to hermesClient and
the call site in handleNotification, but never assigned it for Hermes,
so the guard short-circuited and every notification was processed.
This change mirrors the working Kiro pattern (kiro.go:87/97/240):
- declare a streamingCurrentTurn atomic.Bool in the backend.
- assign acceptNotification, onMessage, onPromptDone gates that all
return early when the flag is false.
- flip the flag to true immediately before c.request("session/prompt").
Adds TestHermesClientAcceptNotificationGate as a regression test that
exercises the gate directly on hermesClient.
Verified with `go test ./pkg/agent`.
Co-authored-by: multica-agent <github@multica.ai>
Follow-up nits from PR #1988 review:
- Move the comment that documents getRemoteDefaultBranch's resolution
walk into the resolveBaseRef call site description, and rephrase the
"" branch so it's clear that path only fires for the default-branch
case (the requested-ref path returns an explicit error before
reaching it).
- Add TestCreateWorktreeWithRequestedTagRef to lock in the
refs/tags/<ref> candidate. The test tags the initial commit, advances
the default branch past it, then asserts the worktree HEAD matches
the tagged commit (so the tag must have been resolved, not the
default branch).
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): persist ModelListStore across replicas via Redis
The model picker uses a pending-request pattern: the frontend POSTs to
create a request, the daemon pops it on its next heartbeat, runs
agent.ListModels locally, and reports back. Until now the store was a
plain in-memory map per Handler instance.
That works for self-hosted single-instance deploys but fails in any
multi-replica environment (Multica Cloud). Each replica has its own
map, so:
POST /runtimes/:id/models → request stored in replica A
GET /runtimes/:id/models/<requestId> → polls land on B/C → 404
daemon heartbeat → only A sees PendingModelList
POST .../<requestId>/result → daemon's report has to land on A
Success probability ~1/N². The visible symptom is "No models available"
in the picker for every provider, even those (Claude/Codex) whose
catalog is statically populated end-to-end.
Same shape of bug, same Redis-backed fix as multica-ai/multica#1557 did
for LocalSkillListStore / LocalSkillImportStore. Reuse the operational
playbook (namespaced keys, ZSET-backed pending queue, atomic
ZREM+SET-running via the shared Lua script) so we don't introduce a
second concurrency model for the same primitive.
Changes:
- Convert ModelListStore from struct to interface with context-aware
methods. Add HasPending for cheap heartbeat-side probing.
- InMemoryModelListStore — single-node fallback, used when REDIS_URL
is unset (self-hosted dev / tests).
- RedisModelListStore — multi-node implementation using the same key
layout and Lua atomic claim as RedisLocalSkillListStore.
- Use RunStartedAt (not UpdatedAt) as the running-timeout reference
point, matching the local-skill stores so subsequent UpdatedAt
bumps don't reset the running clock.
- Heartbeat now uses the probe-then-pop pattern for the model queue
(matching local-skills) so a slow Redis can't stall every connected
daemon. Extends heartbeatMetrics + slow-log with probe_model_ms /
pop_model_ms / probe_model_timed_out for parity.
- Wire the Redis backend in NewRouterWithOptions when rdb != nil.
- Tests for both backends. Redis tests gate on REDIS_TEST_URL so
laptop runs without Redis still pass; CI provides it.
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): persist RunStartedAt + retry model report on transient failures
Two follow-ups from PR #2022 review:
1. RedisModelListStore was dropping ModelListRequest.RunStartedAt on
persistence — the field is tagged json:"-" so it doesn't leak into
the HTTP response, which made plain json.Marshal(req) silently
discard it. Across-node readers saw RunStartedAt=nil and
applyModelListTimeout's running branch became a no-op, so the 60s
running-timeout escape hatch never fired. CI's
TestRedisModelListStore_RunningTimeout was failing on this exact
case. Fix mirrors RedisLocalSkillImportStore's envelope pattern —
wrap in an internal struct that re-promotes the field. HTTP shape
stays clean. Adds a no-Redis unit test that pins the round trip.
2. Daemon's handleModelList called d.client.ReportModelListResult
directly and swallowed any 5xx, leaving the pending request
stranded in "running" until its 60s server-side timeout — exactly
the failure mode the multi-node store fix was meant to eliminate.
Generalize the existing local-skill retry helper into
reportRuntimeResultWithRetry (kind: model_list / local_skill_list /
local_skill_import) and wire handleModelList through a new
reportModelListResult helper. Renames the test-overridable
var localSkillReportBackoffs → runtimeReportBackoffs to match.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Latest Codex CLI ships with GPT-5.5 / GPT-5.5 mini, but the static
catalog still topped out at GPT-5.4 so users couldn't pick the new
model from the agent picker.
Add gpt-5.5 + gpt-5.5-mini to codexStaticModels and promote 5.5 as
the default badge. Keep the older 5.4 / 5.3-codex / gpt-5 / o3
entries for users on older Codex CLI builds. Add a regression test
mirroring TestGeminiStaticModelsExposesAliasesAndGemini3 so the
next OpenAI release isn't a silent miss.
Co-authored-by: multica-agent <github@multica.ai>
- Expand chat-resume comment in ClaimTaskByRuntime to spell out *why* the
task-row fallback exists (single failed turn must not drop chat memory)
and that it covers more than just legacy NULL rows.
- Replace the sessionRuntimeID := t.RuntimeID; sessionRuntimeID.Valid = ...
pattern in CompleteTask/FailTask with a clearer var-then-assign that makes
the "no session_id, leave runtime_id alone" coupling obvious.
- Add TestClaimTask_ChatLegacyNullRuntimeFallsBackToTaskRow covering the
case the prior PR's tests didn't reach: chat_session.runtime_id IS NULL
(legacy / unbackfilled) plus a matching-runtime task row, fallback
should resume. This is the dominant post-migration shape and was
previously only covered transitively.
No behavior change beyond the new test; runtime-guard semantics stay
identical to PR #1905.
Co-authored-by: multica-agent <github@multica.ai>
Copilot's backend (server/pkg/agent/copilot.go) and the public docs
site (apps/docs/) already treat it as one of the 11 supported agents,
but the root README, CLI guide, and self-host docs still listed only
10. Bring those to parity. Also brings README.zh-CN.md up to current
English content (was missing Copilot, Kimi, and Kiro CLI).
* fix(cli): make `multica login --token` accept the PAT as a value
The flag was registered as a Bool, so `multica login --token <PAT>` parsed
`--token` as `true` and dropped the supplied value as an unused positional
argument, then unconditionally prompted "Enter your personal access token:".
This contradicted the user-facing docs (`cli.mdx`, `CLI_AND_DAEMON.md`,
the in-app `connect-remote-dialog`) which show `--token <mul_...>`.
Switch `--token` to a String flag. Both `--token mul_...` and
`--token=mul_...` now bind the value and skip the prompt. Passing
`--token=` with an empty value (or `multica login --token=""`) still
falls through to the interactive prompt for users who don't want the
token in shell history. Updates the few internal docs that showed the
no-value form.
Fixes#1994
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): preserve `multica login --token` (no value) prompt path and tighten regression test
Addresses review feedback on #2017:
1. Restore the legacy no-value form. After the prior commit, `multica
login --token` (no value) errored with `flag needs an argument:
--token`, which broke the CLI_INSTALL.md / CLI_AND_DAEMON.md flow for
headless users. Set `NoOptDefVal` on the `--token` flag to a sentinel
that runAuthLoginToken treats as "prompt me," so:
- `--token mul_xxx` and `--token=mul_xxx` consume the value (the
#1994 fix is preserved),
- `--token` alone falls through to the interactive prompt,
- `--token=""` (explicit empty) also prompts.
pflag with `NoOptDefVal` won't bind the next positional as the flag's
value, so runAuthLogin recovers `--token mul_xxx` (the form from
#1994) by promoting a single positional arg into the token. loginCmd
gains `Args: cobra.MaximumNArgs(1)` so multi-positional typos still
error fast.
2. Tighten regression coverage. Split into TestLoginTokenFlagWiring
(asserts the production loginCmd.Flags().Lookup("token") is a String
flag with the prompt-mode NoOptDefVal — would fail if anyone reverts
the flag to Bool) and TestLoginTokenFlagParsing (drives all five
documented invocation forms through the same flag wiring + the
runAuthLogin space-form recovery). The synthetic-only test that the
reviewer flagged is gone.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add UploadFileWithURL and AttachmentResponse to APIClient
* feat(cli): add agent avatar command and show avatar_url in agent get output
* fix(server): include id and url in no-workspace file upload response
* fix(cli): remove dead HTTPClient timeout swap, extend ctx to 60s for avatar upload
The 30s context deadline was tighter than the 60s HTTPClient timeout
swap, so the swap was dead code and did nothing for slow connections.
Both Neo and Omni Mentor flagged this in review.
Fix: extend the command context to 60s and remove the HTTPClient
mutation. This is simpler, thread-safe, and actually works for slow
uploads.
* fix: align fallback upload response shape and honor context deadline
- file.go: fallback returns {id, url, filename} instead of {filename, link},
matching the no-workspace path response shape.
- client.go UploadFileWithURL: tolerate empty attachment ID (S3 succeeded
but DB record failed — the file is still usable via its URL).
- client.go UploadFileWithURL: use a context-deadline-aware HTTP client so
that the 60s upload timeout set by the avatar command actually takes
effect instead of being shadowed by the default 15s client timeout.
- client_test.go: update 'missing id' test to verify empty-id success
(fallback tolerance).
* fix(cli): shallow-copy HTTP client to preserve Transport on upload timeout
When the context deadline exceeds the default 15s HTTP client timeout,
UploadFileWithURL was creating a bare &http.Client{Timeout: remaining},
silently dropping any custom Transport, Jar, or CheckRedirect configured
on the original client. This causes obscure connection failures when the
CLI uses an authenticated proxy, custom TLS, or mock transport in tests.
Fix: perform a shallow copy of the original client struct and only
mutate the Timeout field on the copy.
* fix(daemon): add safe.directory=* to gitEnv to fix CI dubious ownership errors
TestRegisterTaskReposAllowsProjectOnlyURL and
TestRegisterTaskReposSurvivesWorkspaceRefresh fail on GitHub Actions CI
because git clone --bare from local temp directories triggers git's
safe.directory ownership check when the runner UID differs from the
directory owner.
Set safe.directory=* via GIT_CONFIG env vars in gitEnv() so all daemon
git subprocesses trust any directory. The daemon manages its own bare
caches and worktrees, so the ownership check provides no security value.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): preserve existing GIT_CONFIG_* entries in gitEnv
Instead of resetting GIT_CONFIG_COUNT to 1, read the existing count
from the environment and append safe.directory at the next available
index. This preserves any env-scoped git config (auth, URL rewrites,
extra headers) injected into the daemon process.
Adds TestGitEnvPreservesExistingConfig to verify the append behavior.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Dragging an issue between kanban columns was forcefully switching the
sort mode to "position" (manual), resetting any user-chosen display
settings like sorting by title. Remove the auto-switch so the sort
preference is preserved across drag operations.
Fixesmultica-ai/multica#1960
Co-authored-by: multica-agent <github@multica.ai>
Remove the "mark as done" hover button from inbox list items since it
duplicates the one in the issue detail header. For done tasks, show an
archive button in the issue detail header instead.
Co-authored-by: multica-agent <github@multica.ai>
Switch Autopilot list rows to a stacked layout below the sm breakpoint,
hide desktop column headers on mobile, and match loading skeletons to
the mobile row shape. Desktop table layout is preserved at sm and above.
Closes MUL-1653
Co-authored-by: multica-agent <github@multica.ai>
The previous description rule ("stay faithful + keep it concise") caused
agents to over-compress user input into vague single-sentence summaries,
losing context that the executing agent needs.
Key changes:
- Replace "keep it concise" with structured two-section format:
User request (faithful restate) + Context (verifiable external facts)
- Add hard rules against information compression and semantic downgrading
- Remove "one-line description" phrasing (UI supports richer input)
- Strip redundant behavioral rules from issue_context.md (already
covered by AGENTS.md guardrails and per-turn prompt)
Co-authored-by: multica-agent <github@multica.ai>
PR #1868 conflated "has workspace" with "completed onboarding" —
restore `onboarded_at` as the single signal, and route invited users
through a dedicated /invitations page before they ever see onboarding.
- Backend: CreateWorkspace + AcceptInvitation atomically set
onboarded_at alongside the member insert, establishing the
invariant "member row exists ↔ onboarded_at != null" at the DB
layer.
- Migration 065: one-shot backfill closes the dirty rows produced
by PR #1868 (users with a workspace but onboarded_at == null).
- Entry points (web callback, login, desktop App): if onboarded_at
is null, look up pending invitations by email and route to the
new batch /invitations page; otherwise the resolver picks
workspace / new-workspace as before.
- OnboardingPage: stops bouncing on hasWorkspaces; only
hasOnboarded bounces. Unblocks the user from completing
Step 3 (workspace creation) → Steps 4 / 5.
- StarterContentPrompt: only shows when the user is the solo
member of the workspace, so invited users never get prompted to
import starter content into someone else's workspace.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(daemon): Redis empty-claim fast path for /tasks/claim polling
Daemons poll /tasks/claim every 30s per runtime; the steady-state
warm-empty case currently runs ListPendingTasksByRuntime against
Postgres on every poll. This collapses that path:
- New ListQueuedClaimCandidatesByRuntime query restricts to status =
'queued' (the old query also returned 'dispatched' rows that can
never be reclaimed) and is backed by a partial index keyed on
(runtime_id, priority DESC, created_at ASC).
- New EmptyClaimCache caches the negative verdict in Redis with a
30s TTL. ClaimTaskForRuntime checks the cache before SELECT and
populates it on confirmed-empty results.
- notifyTaskAvailable now invalidates the runtime's empty key before
kicking the daemon WS, so newly enqueued tasks become claimable
immediately rather than waiting out the TTL.
- AutopilotService.dispatchRunOnly now goes through
TaskService.NotifyTaskEnqueued so run_only tasks get the same
invalidate-then-wakeup contract as every other enqueue path.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): close MarkEmpty/Bump race in empty-claim fast path
GPT-Boy's review on PR #1860 caught a real concurrency bug. Under the
prior implementation it was possible for a slow claim to write an
empty verdict AFTER a concurrent enqueue had already invalidated it:
T1 claim: SELECT -> empty
T2 enqueue: INSERT row, DEL empty key (no-op, key not set yet),
wakeup
T1 claim: SET empty (writes a stale "empty" verdict)
T3 wakeup: IsEmpty -> hit -> returns null
The just-queued task would then sit idle until the empty key's TTL
expired (up to 30s).
Replace the DEL-based invalidation with a per-runtime version
counter:
- CurrentVersion(rt) is a Redis INCR counter at
mul:claim:runtime:version:<rt> with a 24h sliding TTL.
- Claim samples version BEFORE the SELECT and passes it to MarkEmpty,
which stores the verdict's value as the observed-version string.
- IsEmpty MGETs both keys and trusts the verdict only when the
empty-key value equals the current version.
- Enqueue Bumps the version (INCR + EXPIRE) before the wakeup,
causing any verdict written under a prior version to be rejected
on the next read.
Also bound every Redis call from this cache with a 250ms timeout —
notifyTaskAvailable uses a background context so a wedged Redis
must not block enqueue.
Tests against a real Redis (REDIS_TEST_URL) cover:
- MarkEmpty + IsEmpty under matching version returns hit
- Bump invalidates a prior empty verdict (race-fix pin)
- A MarkEmpty written under a stale pre-Bump version is rejected
- TTL clamping, per-runtime isolation, nil-cache safety
- notifyTaskAvailable Bumps before the wakeup fires
Co-authored-by: multica-agent <github@multica.ai>
* chore(daemon): renumber claim-candidate index migration to 067
Slot 064 was taken on main by 064_notification_preference. The
migration runner tracks per-version in schema_migrations and would
silently skip the second 064_*, leaving the index uncreated.
Rename to 067 (next free slot).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The `POST /api/issues/batch-update` handler walked every issue ID and
incremented `updated` regardless of whether the iteration carried any
mutation. When the caller's payload had no recognized field in
`updates` — e.g. status placed at the top level instead of nested,
"update" misspelled as singular, or "updates" missing entirely —
the loop ran N no-op UPDATEs (each if-guard skipped, each COALESCE
preserved the existing value) and the response cheerfully reported
`{"updated": N}` while nothing changed. Reporters mistook the
positive count for success and chased a phantom persistence bug.
Detect at the top of the handler whether any known mutation field is
present in the parsed `updates` payload; if none is, short-circuit
with `{"updated": 0}`. The wire shape stays 200 + `{updated}`
so existing callers don't break — only the count becomes truthful.
Tests cover the three caller shapes that hit this path (status at top
level, empty `updates: {}`, misspelled "update") plus a positive
case that locks in happy-path persistence and counting.
Closes#1660.
* fix(daemon): reclaim disk on long-open issues + correct cancelled-status check
Two related fixes for GitHub #1890 (self-hosted disk space growth):
- The GC's done/cancelled branch compared `status.Status` against `"canceled"`
(single l), but the issue schema and the rest of the daemon use `"cancelled"`
(double l). Cancelled issues therefore never matched and only fell out via the
72h orphan TTL, which itself doesn't fire because cancelled issues are still
reachable. Aligning the spelling lets cancelled-issue task dirs be reclaimed
on the normal TTL path.
- Add a third GC mode, artifact-only cleanup, for the common case the report
flagged: an issue stays open for days while many tasks complete on it, so
per-task `node_modules`, `.next` and `.turbo` directories accumulate without
ever becoming GC-eligible. The new branch fires when `.gc_meta.completed_at`
is older than `MULTICA_GC_ARTIFACT_TTL` (default 12h), the env root is not
currently in use by an active task, and the issue is still alive. It removes
only directories whose basename matches `MULTICA_GC_ARTIFACT_PATTERNS`
(default narrow: `node_modules,.next,.turbo`); source, `.git`, `output/`,
`logs/` and the meta file are preserved so subsequent tasks can still resume
the workdir. Patterns containing path separators are dropped, `.git` subtrees
are never descended into, symlinked matches are not followed, and every
removal target is verified to live inside the task dir.
Bookkeeping: `Daemon` now tracks active env roots with a refcounted set so the
GC loop never reclaims a directory that is mid-execution; `runTask` claims the
predicted root early plus the prior workdir on reuse paths. The cycle log is
extended with bytes reclaimed and per-pattern counts so self-hosted operators
can see what was freed.
Docs: extend the daemon configuration table in CLI_AND_DAEMON.md with the new
GC env vars and add a Workspace garbage collection section explaining the
three modes and the artifact-pattern contract.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): protect active env root from full GC removal too
Address GPT-Boy's PR #1931 review: the active-root guard only fired in the
artifact-cleanup branch, leaving a real race on the full-removal paths. A
follow-up comment on a long-done issue dispatches a task that reuses the prior
workdir, but `CreateComment` does not bump issue.updated_at — so the issue
still satisfies the done+stale GCTTL window and `gcActionClean` would
`RemoveAll` the directory mid-execution. The orphan-404 path is similarly
exposed when a token's workspace access is in flux.
Move the `isActiveEnvRoot` check to the top of `shouldCleanTaskDir` so all
three delete actions (clean, orphan, artifact) skip an in-use env root in one
place, and drop the now-redundant guard from the artifact branch.
Add tests covering the three at-risk paths: active root + done/stale issue,
active root + 404 issue past orphan TTL, active root + no-meta orphan past
TTL.
Also align two stale comments noted in the same review: cleanTaskArtifacts now
documents that symlinks are skipped entirely (the previous note implied the
link itself was removed), and GCOrphanTTL no longer claims that 404s are
cleaned immediately — the implementation gates them on the same TTL.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The toolbar button was previously visible on all issue detail views.
Gate it on the `onDone` prop, which is only passed from InboxPage.
Co-authored-by: multica-agent <github@multica.ai>
Sync the "Why Multica?" content from the landing page About section
into both README.md and README.zh-CN.md, explaining the name's
connection to Multics and the multiplexing philosophy.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(repos): drop unused description + tighten create-project layout
Two related changes that touch the workspace-repos surface together.
1. Remove the per-repo `description` field everywhere it was threaded.
The only place it ever surfaced was a markdown table column the daemon
wrote into the agent runtime config, where most rows just read "—"
anyway. Agents already discover project structure by running
`multica project` / `multica issue` against the CLI, so the human-
readable description string carried no real value while taking up an
extra Settings input row and propagating through six layers (settings
UI → workspace.repos jsonb → handler RepoData → daemon RepoData →
repocache.RepoInfo → execenv.RepoContextForEnv).
- Settings → Repositories drops the description input; the URL field
now spans the whole row.
- WorkspaceRepo TS type loses `description`; backend RepoData /
RepoInfo / RepoContextForEnv all collapse to URL only.
- Daemon's runtime_config Repositories block changes from a
`| URL | Description |` markdown table to a simple bullet list.
- Tests updated; jsonb residue in existing workspaces is dropped at
normalize time, so no migration needed.
2. Tighten the Create Project modal footer: pull the Status / Priority /
Lead / Repos pills onto the same row as the Create Project button
(Linear-style single-row footer) instead of stacking them above it,
and swap the Repos pill icon from `FolderGit` to a real GitHub mark
(lucide-react v1 dropped brand icons, so the mark lives inline as a
small SVG component in this file).
I tried promoting Repos to its own "Resources" strip above the footer
to separate the resources abstraction from project metadata, but with
a single pill it looked too sparse — leaving a TODO comment in the
footer to revisit once we add Linear / Notion / Figma / Slack
resource types.
* fix(daemon test): drop residual Description field on RepoData literals
* fix(repos): drop Description residue surfaced after rebase on #1929
Project-resource github_repo lift path (#1929) and registerTaskRepos
both still constructed RepoData{...Description: ...} after the rebase.
Two test sites in daemon_test.go and execenv_test.go also reintroduced
the field. Strip them so the Description-removal change builds and
tests pass with the latest main.
* feat(projects): project github_repo resources override workspace repos
When an issue's project has at least one github_repo resource, the daemon
claim handler now sends only those as resp.Repos — workspace-level repos
are hidden to avoid mixing two repo lists in the agent prompt. With no
project github_repos (or no project), behavior is unchanged: workspace
repos are surfaced as before.
Lifts each project github_repo's url (and label, when present) into a
RepoData entry so `multica repo checkout` and the meta-skill render the
same URLs. The full structured list still ships at
.multica/project/resources.json for skills that want everything.
Adds TestProjectReposReplaceWorkspaceReposInMetaSkill covering the
rendering side. Docs updated to spell out the new precedence.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): allow project repo URLs through the checkout allowlist
When ClaimTaskByRuntime narrows resp.Repos to project github_repo URLs,
the daemon receives URLs that may not exist in the workspace's
GetWorkspaceRepos response. The existing checkout flow rejected those
with ErrRepoNotConfigured because the allowlist (and cache) was built
only from workspace-bound repos.
Adds registerTaskRepos in daemon.runTask: before agent spawn, merge
task.Repos into a new task-scoped allowlist (separate from the
workspace-scoped one so a workspace refresh doesn't wipe project URLs)
and kick off a background cache sync. ensureRepoReady now treats either
allowlist as valid.
Tests:
- TestRegisterTaskReposAllowsProjectOnlyURL — project-only URL is
checkout-able and does not trigger a workspace-repos refresh
- TestRegisterTaskReposSurvivesWorkspaceRefresh — task URLs persist
across refreshWorkspaceRepos
- TestClaimTask_ProjectGithubReposOverrideWorkspaceRepos — claim
handler returns only project repos when present, no workspace leakage
- TestClaimTask_ProjectWithoutRepos_FallsBackToWorkspaceRepos — fall
back to workspace repos when project has no github_repo resources
Docs updated to spell out the daemon-side allowlist behavior.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
A sandbox="" iframe cannot run scripts, so users had no way to zoom or
pan rendered Mermaid diagrams beyond browser scrolling. Add a hover
toolbar with a fullscreen button that opens a portal-based lightbox
showing the same diagram scaled to 90vw x 90vh, while preserving the
sandbox isolation (the lightbox iframe is also sandbox=""). ESC or
clicking the backdrop closes the lightbox.
Co-authored-by: multica-agent <github@multica.ai>
* fix(task): rerun starts a fresh session, skip poisoned resume
When a task ended in a known agent fallback ("I reached the iteration
limit and couldn't generate a summary.", "Put your final update inside
the content string. Keep it concise.") the (agent_id, issue_id) resume
lookup would still pick that session, so a manual rerun inherited the
poisoned state and reproduced the same bad output.
Two complementary guards:
1. Daemon classifies poisoned terminal output and routes it through the
blocked path with failure_reason set ('iteration_limit' /
'agent_fallback_message'). GetLastTaskSession excludes failed tasks
with those reasons, so even comment-triggered tasks no longer resume
them. Tasks that failed mid-flight (timeout, runtime_recovery, etc.)
are still resumable, preserving MUL-1128's auto-retry contract.
2. Manual rerun marks the new task force_fresh_session=true. The daemon
claim handler skips the resume lookup entirely when the flag is set,
capturing the user-intent signal that "the prior output was bad" even
when poisoned classification misses a future fallback wording.
Auto-retry of orphaned mid-flight failures (MaybeRetryFailedTask →
CreateRetryTask) does not take this path, so it keeps resuming.
Tests: classifyPoisonedOutput unit test; integration tests assert the
SQL filter excludes poisoned classifiers, RerunIssue flips the flag,
and the normal enqueue path leaves it false.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): cap poisoned-output matcher to short trimmed text
GPT-Boy review on MUL-1630: the previous strings.Contains match would
classify any output that quoted the marker substring — including a
review/analysis that simply discussed the marker itself. Real fallback
messages are short single-sentence affairs, so cap the candidate at
~one paragraph and trim whitespace before matching. Adds regression
tests covering a long quoting review and a marker buried in a long
real conclusion; both must stay classified as completed.
Co-authored-by: multica-agent <github@multica.ai>
* fix(migrations): rename 065 force_fresh_session → 066 to clear collision
main introduced 065_project_resources after this branch was cut, so
both files shared the 065_ prefix. The readiness check
(server/cmd/server/health.go → migrations.LatestVersion) takes the
last entry by lexical order, which is 065_project_resources, leaving
this branch's 065_force_fresh_session unguarded — a deploy that
applied project_resources but not force_fresh_session would still
report ready, and the next enqueue / rerun / claim would crash on
"column force_fresh_session does not exist".
Renaming to 066_force_fresh_session puts it strictly after
project_resources so readiness blocks until it's applied.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): typed project resources + agent runtime injection
Adds a `project_resource` table that lets a project carry typed pointers
(github_repo today, more later) and surfaces them at agent runtime.
Server
- migration 065: project_resource (resource_type TEXT + resource_ref JSONB)
- sqlc CRUD + handler at /api/projects/{id}/resources
- claim handler attaches project_id/title + resources to issue tasks
Daemon
- TaskContextForEnv carries project context
- writes .multica/project/resources.json into workdir
- adds "## Project Context" block to CLAUDE.md / AGENTS.md / GEMINI.md
via type-dispatched formatter so new resource types just add a case
CLI
- multica project create --repo <url> attaches repos in one step
- multica project resource add/list/remove
Frontend
- Project create modal: Repos pill (workspace repos + ad-hoc URL)
- Project detail sidebar: collapsible Resources section with attach/remove
Docs
- New "Project Resources" chapter explaining the abstraction and
exactly what code to touch when adding a new resource type
Co-authored-by: multica-agent <github@multica.ai>
* fix(projects): transactional resources[] on create + generic CLI ref + test fix
Addresses review feedback on PR #1926:
1. CI red: TestProjectResourceLifecycle delete step called withURLParam
twice, which replaced the chi route context and dropped the project id.
Switched to the existing withURLParams helper from daemon_test.go.
2. POST /api/projects now accepts resources[] and attaches them in the
same transaction as the project. Invalid refs roll back the whole
create — no more half-attached projects on failure. Web modal + CLI
`project create --repo` both use the new bundled payload.
3. CLI `project resource add` now accepts a generic --ref '<json>' flag
so a new resource_type works without a CLI change. Per-type
shortcuts (--url for github_repo) remain as a convenience but are no
longer the only way in. Docs updated to drop the CLI from the
"files you must touch" list.
Adds two new server handler tests:
- TestCreateProjectAttachesResources (resources[] happy path)
- TestCreateProjectRollsBackOnInvalidResource (transactional rollback)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The agent runs the daemon CLI, so issue.creator_type is `agent` and the
issue:created event listener only auto-subscribes the agent — not the
human requester. Result: the requester gets a single completion inbox
item but never sees follow-up comments or updates on their own issue.
Subscribe the requester (reason=`creator`, the only matching value
allowed by issue_subscriber's CHECK constraint without a migration)
inside notifyQuickCreateCompleted, after the issue lookup succeeds and
before the inbox write. Best-effort: log on failure, don't block the
inbox. On success, publish subscriber:added so the UI stays in sync
with manual subscribe and the listener-driven path.
Adds two integration tests in cmd/server: success path subscribes the
requester; failure path (agent finished without creating an issue)
leaves no subscriber rows.
Co-authored-by: multica-agent <github@multica.ai>
PropRow switched to CSS subgrid in #1919, which requires its parent to
declare grid columns. The Properties section's wrapper was updated, but
Details and Token usage in the same file were missed — their PropRows
collapsed to a single column, stacking label and value vertically.
Add the same `grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5` wrapper used
by Properties so all three sections render consistently.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): refresh agent cache after import and agent creation
Two paths could leave the workspace agent-list query cache stale by the
time the dashboard rendered the welcome issue, causing the issue's
agent assignee to resolve to "Unknown Agent":
1. StarterContentPrompt.onImport invalidated pins/projects/issues but
not agents, and didn't await any of them before navigating — so the
issue-detail page could mount and read the cache before TanStack
Query had marked the relevant queries stale.
2. OnboardingFlow.handleAgentCreated created the agent without
invalidating the agent list, so the dashboard's first mount would
read whatever was already cached from earlier in onboarding.
Both now invalidate workspaceKeys.agents, and the import flow awaits
all invalidations via Promise.all before pushing the navigation, so
the next page mount always refetches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): drop editable prop, ContentEditor is editing-only
ContentEditor's `editable` prop had zero true callsites left in the
codebase — every read-only surface had migrated to ReadonlyContent
(react-markdown), and the prop only invited misuse: Tiptap's
`useEditor` reads `editable` at mount, so callers that toggled it
post-mount (like a chat input that needs to disable on no-agent)
silently got stuck in whichever mode the editor first created.
Changes:
- Remove `editable` prop and default; useEditor and createEditorExtensions
no longer take it.
- Remove the `"readonly"` className branch and the readonly content sync
useEffect (only the editing path remains).
- Remove the BubbleMenu and mouseDown editable guards.
- Drop LinkReadonly; rename LinkEditable to LinkExtension and use it
unconditionally.
- Update the docstring to point readers at ReadonlyContent for display
surfaces.
ReadonlyContent's `.readonly` CSS class stays in content-editor.css —
that file's selectors are still used by react-markdown's wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(chat): empty-state by session history, no-agent disabled state
Three independent improvements to the chat window's pre-conversation
states, sharing a new three-state availability primitive:
1. New `useWorkspaceAgentAvailability()` hook (`"loading" | "none" |
"available"`) so callers don't have to reinvent the loading-vs-empty
distinction. Treating loading as "no agent" — the easy mistake —
caused the chat input to flash a fake disabled state for the few
hundred ms after mount, even when the workspace had agents.
2. EmptyState now branches on session history, not agent presence:
never-chatted users get a short pitch ("They know your workspace —
issues, projects, skills"), returning users get the existing
starter prompts. Missing-agent feedback moved to the banner above
the input, keeping this surface focused on "what is chat for".
3. No-agent disabled state: when availability resolves to "none",
ChatInput dims and stops responding to clicks/keys, with cursor
`not-allowed` on hover. The disable lives at the wrapper level
(`pointer-events-none` on the inner card, `cursor-not-allowed` on
the outer one — splitting layers so hover bubbles to where the
browser reads cursor) — we no longer reach into the editor's
editable mode, which never switched cleanly post-mount anyway.
A `<NoAgentBanner>` (sibling of OfflineBanner, mutually exclusive)
states the prerequisite without linking out — no one should be
pulled out of chat mid-thought to a settings page.
Also: default chat width 420 → 380, since the chat docks at the
bottom-right and 420 was crowding everything else.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): align PropRow labels using CSS subgrid
The fixed `w-16` (64px) label column on PropRow broke whenever a label
rendered wider than 64px (e.g. "Concurrency" in the agent inspector) —
the label would overflow into the gap and collide with the value.
Switch to subgrid: the parent declares `grid grid-cols-[auto_1fr]` and
each PropRow becomes `col-span-2 grid grid-cols-subgrid`. The `auto`
track sizes to the widest label across all rows in that parent, so
labels always fit and value columns stay aligned across rows without
picking a magic pixel width.
Updated parents:
- agent-detail-inspector Section wrapper
- issue-detail Properties group
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(permissions): add core permission module and shared UI primitives
Foundation for permission-aware UI: pure rules that mirror the Go backend
permission gates, lightweight per-resource hooks, and two reusable display
components used across agent/skill/runtime detail pages.
- packages/core/permissions: types, rules, hooks (Decision-shaped — carries
reason + message so UI can render disabled state, tooltip, and banner
copy from one source)
- packages/core/agents/visibility-label: VISIBILITY_LABEL/DESCRIPTION/TOOLTIP
constants ("Personal" / "Workspace") to replace scattered hard-coded copy
- packages/views/agents/visibility-badge: read-only visibility chip used on
hover cards, list rows, and inspector when not editable
- packages/ui/components/common/capability-banner: "View only — only X and
admins can edit Y" banner shown on agent / skill detail when current user
lacks edit permission
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(views): permission-aware UI across agent/comment/runtime/skill surfaces
Apply the new permission rules to every surface where the UI was either
lying about who can do what or letting users hit 403s by clicking buttons
the backend would reject.
Agent detail
- Hide archive/restore actions for non-owner non-admin
- Replace inline editors (avatar, name, description, runtime/model/visibility/
concurrency picker, skill-attach) with read-only display when canEdit is
false — value is information, the editor is the action
- Show CapabilityBanner under the header explaining who can edit
Visibility surfaces
- visibility-picker / create-agent-dialog: replace "only you can assign"
(false) with "Only you and workspace admins can assign" via shared
VISIBILITY_DESCRIPTION constants
- agent-columns: truthful tooltip + "You" badge on agents the current user
owns
Comments
- Restore admin override on comment edit/delete (backend already permits
it via comment.go:507-512; the frontend was incorrectly hiding the menu).
canModerate is computed once in issue-detail and threaded down.
Other
- Members tab: disable "demote" options for the last owner with tooltip
- Assignee picker: tooltip on disabled personal agents the user can't assign
- Runtime delete: tooltip and dialog explain the gate; owner column gains
a name label next to the avatar in All scope
- Skill detail: page-level CapabilityBanner alongside the existing lock chip
- Issue delete (single + batch): note that any workspace member can delete
issues — by-design semantics, made transparent
Backend is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agents): hide personal agents from list and @mention for non-owners
Until now an agent's "Personal" visibility only narrowed the assign-to-issue
gate — every workspace member still saw every personal agent in the list
and the @mention dropdown. Members would see, click, and fail.
This filters those surfaces with the canonical canAssignAgentToIssue rule:
regular members only see workspace-visibility agents and the personal
agents they own; workspace owners and admins continue to see everything
(admin override path is intact).
- agents-page: visibleInView layer between active/archived and Mine/All
scope so segment counts also reflect the filter
- mention-suggestion: filter agentItems before they enter the recency-
ranked list; expand the test mock to cover the auth + visibility paths
and add two assertions (member hides others' personal agents; admin
still sees them)
Backend keeps returning every agent — admin tools and direct API access
are unaffected. This is a UI-only filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three signal axes (color / label tiers / per-tool spinner) collapsed
into one (label only):
- Drop 60s amber warning color and 300s cancel-button threshold. The
cancel button duplicated ChatInput's Stop button (both call the same
handleStop) — single entry point is enough; users can judge from the
elapsed seconds whether to stop.
- Drop tiered thinking labels (Thinking / Reasoning / Working through
it / Taking a closer look) — collapse to a single "Thinking".
- Unify all spinners to `breathe` (was: helix / scan / cascade / orbit
/ breathe / pulse / braille mix). Tool-specific spinner choices were
cosmetic noise; one consistent spinner reads cleaner.
- Remove `onCancel` prop chain through ChatMessageList → TaskStatusPill.
Net: 209 → 152 lines in task-status-pill.tsx; no API/contract changes
beyond removing a now-unused prop.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(daemon): add Co-authored-by trailer for Multica Agent to git commits
Install a prepare-commit-msg hook in worktree bare repos that appends
"Co-authored-by: multica-agent <github@multica.ai>" to every commit
made by agents. Uses git interpret-trailers for proper formatting and
skips duplicates.
* feat(settings): add Co-authored-by toggle in workspace Labs settings
Add a workspace-level toggle to enable/disable the Co-authored-by
trailer for agent commits. Default is enabled (on).
Backend:
- Include workspace settings in daemon register response
- Store settings in daemon workspaceState
- Thread CoAuthoredByEnabled through WorktreeParams to conditionally
install the prepare-commit-msg hook
- Parse co_authored_by_enabled from workspace settings JSONB
Frontend:
- Replace empty Labs tab placeholder with a Git section containing
a Switch toggle for the Co-authored-by trailer setting
- Optimistically update the workspace query cache on toggle
* chore(daemon): skip squash commits in Co-authored-by hook
Test commit to verify the prepare-commit-msg hook appends the
Co-authored-by trailer automatically.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Users can now mute specific notification categories (assignments, status
changes, comments & mentions, priority/due-date updates, agent activity)
from Settings > Notifications. Muted event types are silently filtered at
notification creation time — no inbox items are created for muted groups.
- Add notification_preference table (migration 064)
- Add GET/PUT /api/notification-preferences endpoints
- Filter notifications in notifyIssueSubscribers, notifyDirect, and
notifyMentionedMembers based on user preferences
- Add Notifications tab in Settings with per-group toggle switches
The description rule in buildQuickCreatePrompt() instructed the agent to
"always provide a rich, self-contained description" and "spell out what
needs to be done", which caused the agent to fabricate detailed product
specs, implementation phases, and design decisions from a one-line input.
Replace with a faithfulness-first rule: enrich with factual context
(fetched PR details, linked resources) but never invent requirements,
design decisions, or constraints the user did not express.
Fixes MUL-1605
In run-only mode, autopilot runs don't create issues, so there was no
way to view the agent's execution transcript from the UI. Add a
TranscriptButton to each run row that has a task_id but no linked
issue, allowing users to lazy-load and inspect the full execution log
directly from the autopilot detail page.
After creating an agent from the empty state, the query invalidation
triggered a refetch that re-rendered the agents list page (empty → list)
before navigation to the detail page completed, causing a visible flash.
Move navigation.push() before qc.invalidateQueries() so the user lands
on the detail page immediately; the list refetch happens in the
background after we've already left.
* feat(quick-create): enrich issue title and description with URL context
Update the quick-create agent prompt to fetch context from URLs in user
input (GitHub PRs, issues, web pages) before creating the issue. The
agent now produces semantically rich titles (e.g. "Review PR #123:
Refactor auth to OAuth2" instead of "review PR #123") and includes
summarized link content in the description so issues are self-contained.
* refactor(quick-create): let agent decide when to fetch URL context
Replace prescriptive URL enrichment instructions (hardcoded gh/WebFetch
commands) with goal-oriented guidance. The agent now uses its own
judgment to decide whether fetching referenced URLs would produce a
meaningfully better title/description, rather than being told exactly
which tools to use.
* fix(quick-create): always generate rich description for agent execution
The description was previously optional ("omit if simple request"). Since
quick-create issues are executed by agents, richer context leads to
better execution — update the prompt to always produce a substantive
description with actionable context.
* fix(quick-create): remove Chinese text from prompt, use English only
Replace Chinese examples in priority mapping and assignee matching with
language-agnostic English equivalents, per project coding rules.
* fix(quick-create): remove language-related hints from prompt
Agent doesn't need to be told about language handling — remove
"(in any language)" and "or equivalent in any language" qualifiers.
Keep prompt purely in English with no language-related content.
In a desktop app an accidental page reload destroys in-memory state
(tabs, drafts, WS connections) with no URL bar to navigate back.
Add a before-input-event listener on the main BrowserWindow that
intercepts Cmd+R / Ctrl+R (with or without Shift) and F5, calling
preventDefault() to block the reload. DevTools refresh still works.
Add Zustand persisted draft stores for the create-project and feedback
modals, following the same pattern as the existing issue draft store.
Drafts are saved to localStorage on every field change and restored
when the modal reopens, preventing accidental data loss on close.
Draft is cleared on successful submit.
When viewing an inbox notification's issue detail and clicking the "Mark
as done" toolbar button, the inbox item was not archived — only the issue
status changed. Add an onDone callback to IssueDetail so the inbox page
can archive the notification alongside the status update, matching the
behavior of the list-item Done button.
Closes MUL-1594
* feat(views): add remote machine / AWS EC2 connection wizard to Runtimes page
Add a "Connect remote machine" CTA to the Runtimes page header and
empty state that opens a 3-step wizard dialog guiding users through:
1. Installing the Multica CLI on a remote machine
2. Configuring, logging in with a PAT, and starting the daemon
3. Monitoring for runtime registration via WebSocket
Includes security tips (IAM roles, no root keys), troubleshooting
guidance (daemon status/logs, CLI version check), and post-connection
flow to create an agent on the newly registered runtime.
Closes MUL-1588
* fix(views): improve connect-remote dialog layout and usability
- Widen dialog from sm:max-w-lg to sm:max-w-xl for longer commands
- Add max-h-[85vh] + overflow-y-auto so content scrolls on small screens
- Split monolithic code block into 4 separate labeled steps (install,
configure, login, start daemon) — each with its own copy button
- Make copy buttons always visible instead of hover-only
- Condense security tips into a single compact paragraph
- Tighten vertical spacing throughout
* feat(inbox): add one-click Done button to inbox items
Add a hover-visible "Mark as done" button (CircleCheck icon) to each
inbox item that has an associated issue not yet in done/cancelled status.
Clicking it sets the issue status to "done" and archives the inbox item
in one action, replacing the previous multi-step flow of opening the
issue detail sidebar to change status.
* feat(issues): add Mark Done button to issue detail toolbar
Add a "Mark as done" button (CircleCheck icon) to the issue detail
header toolbar, positioned to the left of the Pin button. The button
is only visible when the issue status is not already done or cancelled.
Clicking it sets the issue status to "done" via the existing
handleUpdateField action.
* fix(auth): route invitees to their workspace instead of forcing /onboarding
Workspace presence now wins over `onboarded_at` across every post-auth
entry point, so a user invited into an existing workspace lands inside
that workspace instead of being trapped in the new-workspace wizard.
The redesigned onboarding flow (#1411) intentionally flipped the
priority during frontend development so every login re-entered
/onboarding; the backend `onboarded_at` field shipped but the flipped
priority was never restored. Closes#1837.
- packages/core/paths/resolve.ts: has-workspace beats !hasOnboarded.
Onboarding is reachable only when the user has zero workspaces.
- apps/web/app/auth/callback/page.tsx: drop the early-return on
!onboarded so a `next=/invite/<id>` survives Google OAuth round-trips.
- apps/web/app/(auth)/login/page.tsx: same removal in both the
already-authenticated effect and the post-login handler.
- packages/views/layout/use-dashboard-guard.ts: stop bouncing in-workspace
users to /onboarding; rely on the resolver for zero-workspace cases.
- apps/desktop/src/renderer/src/App.tsx: window-overlay now opens
onboarding only when wsCount === 0 AND !hasOnboarded.
- apps/web/app/(auth)/onboarding/page.tsx: defense-in-depth — bounce
away if the visitor already has a workspace, even on direct URL access.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(auth): fix URLSearchParams leaking state across callback tests
The previous cleanup `mockSearchParams.forEach((_v, k) => mockSearchParams.delete(k))`
silently skipped entries because forEach advances its index while the
underlying URLSearchParams shrinks, so a `state=next:/invite/...` set
in one test bled into the next. Snapshot keys via Array.from before
deleting. Also rewrites the assertions to match the new policy: an
unonboarded user with a safe `next=` honors it, with a workspace lands
in that workspace, and only with zero workspaces falls back to
/onboarding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When no model is explicitly selected, the model dropdown and inspector
picker no longer show "Default — Claude Sonnet 4.6". Instead they show
"Default (provider)" / "Default", avoiding confusion when the actual
CLI default differs from the hardcoded catalog entry.
* feat(views): keep quick capture open after submit for continuous creation
After successfully sending a prompt to the agent, the dialog now clears
the editor and stays open instead of closing. This lets users create
multiple issues in quick succession without reopening the dialog each
time. The user can still close manually via X or Escape.
* feat(views): add success feedback for quick capture continuous mode
After each successful submit, the Create button briefly flashes green
with a checkmark "✓ Sent" for 1.5s, then reverts. A persistent counter
("N sent") appears in the footer so the user knows how many prompts
they've dispatched in this session. No explicit mode toggle needed —
the counter implicitly signals continuous mode is active.
* feat(views): add "Create another" toggle to quick capture (Linear-style)
Replace always-on continuous mode with an opt-in toggle switch in the
footer, matching Linear's "Create more" pattern. The preference is
persisted per-workspace via the quick-create store so it remembers
across sessions.
- Toggle OFF (default): submit closes the dialog (original behavior)
- Toggle ON: submit clears the editor and stays open; button flashes
green "✓ Sent" and a counter shows how many have been dispatched
* fix(views): remove stale breadcrumb identifier test
PR #1872 removed the issue identifier from the breadcrumb but the
corresponding test was not updated, causing CI to fail.
The inbox notification for quick-create showed "Created MUL-1577: <title>"
which truncated the actual issue title. Now the title field shows just the
issue title (the most useful info), and the detail label shows "Created
MUL-XXXX" as context.
The issue detail page breadcrumb showed both the issue identifier and
title (e.g. "MUL-1567 Title"), making the ID appear twice. Remove the
standalone identifier span so only the title is displayed.
* feat(ui): make New Issue button open Quick Capture instead of manual form
The sidebar "New Issue" button and the search command's "New Issue" action
now open the agent-based Quick Capture dialog directly, matching the
platform's agent-first workflow.
Contextual issue creation (board columns, list view status groups, sub-issues)
still opens the manual form since those pass pre-filled data.
Closes MUL-1558
* test(search): update search-command test to expect quick-create-issue
Aligns the test assertion with the behavior change in the previous
commit where "New Issue" now opens Quick Capture.
The agent-mode Quick Capture dialog already supported image paste and
drag-drop through the ContentEditor, but lacked a visible file
attachment button. This made the feature undiscoverable.
Add a FileUploadButton (paperclip icon) to the footer, matching the
pattern already used by the manual create panel and comment input.
Without this guard, submitting during an active upload causes
stripBlobUrls to silently remove the in-flight blob image from the
markdown, so the agent never sees the pasted screenshot. Now the Create
button disables and shows "Uploading…" until all file uploads resolve.
Packaged builds are unaffected: scripts/package.mjs already injects the
git tag into electron-builder's extraMetadata.version, so the .app users
download from GitHub Release reports the right version through
app.getVersion() and the auto-updater's latest.yml comparison works
correctly.
Dev mode (`pnpm dev:desktop`) didn't go through that path though, so
app.getVersion() returned the static "0.1.0" from package.json — the
new Settings → Updates panel surfaced this and made it look like the
dev build was ancient. Add a tiny getAppVersion() helper that falls
back to `git describe --tags --always --dirty` only when !app.isPackaged,
and use it for the app-info IPC. No change to packaged behavior; if git
is unavailable for any reason, we silently fall back to app.getVersion().
Surface the running app version (from app.getVersion via preload's
appInfo) at the top of Settings → Updates so users have a clear place
to check which build they're on, instead of only seeing it inline after
clicking "Check now".
ProseMirror's default clipboardTextSerializer uses Slice.textBetween,
which flattens every node to its inner text. Copying `## 你好` from the
editor only put `你好` on the clipboard's text/plain channel, so pasting
into VS Code, terminals, or messaging apps lost all Markdown markers.
Add a markdown-copy extension symmetric to the existing markdown-paste:
on copy/cut/drag, route the selected Slice through editor.markdown.serialize
to write the Markdown source. The text/html channel is left at ProseMirror's
default so pasting back into another ProseMirror editor still preserves
exact node structure via data-pm-slice.
Registered for both editable and readonly modes — users frequently copy
from rendered comments/issue descriptions.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(quick-create): bound dialog height + scroll editor when content overflows
Pasting a screenshot into the agent-create prompt expanded the editor
unbounded, which dragged DialogContent past the viewport since the agent
mode className had no max-height. Manual mode was unaffected because
manualDialogContentClass pins `!h-96`.
- Cap agent-mode DialogContent at `!max-h-[80vh]` (width stays
`!max-w-xl`); short prompts still render compact, tall content stops
at 80% of the viewport.
- Switch the editor wrapper to `flex-1 min-h-[140px] overflow-y-auto`
so it absorbs the remaining vertical space inside the now-bounded
DialogContent and scrolls internally instead of pushing the dialog.
* feat(quick-create): gate on daemon CLI version with pre-check + server enforcement
The agent-create flow depends on multica CLI behavior introduced in
v0.2.20 (URL attachment handling, no-retry semantics on
`multica issue create` failure — see PR #1851 / MUL-1496). Older
daemons either double-create issues on partial CLI failures or
mishandle pasted screenshot URLs. Per J's review on MUL-1496, gate
the flow at two layers — frontend pre-check for fast feedback,
server re-check as the trust boundary, both fail-closed on
missing/unparsable versions.
Server:
- New MinQuickCreateCLIVersion + CheckMinCLIVersion helper in
pkg/agent (with sentinel errors for missing vs too-old).
- QuickCreateIssue handler reads runtime metadata.cli_version and
returns a stable 422 { code: "daemon_version_unsupported",
current_version, min_version, runtime_id } before enqueuing.
- The check runs after the existing online + ownership validation,
so all rejections surface uniformly through the modal's existing
error path.
Frontend:
- New @multica/core/runtimes/cli-version with the min version
constant, parser, and runtime-metadata reader (tiny semver, no
new lib dep).
- AgentCreatePanel resolves the selected agent's runtime, runs the
same check, shows an inline amber notice below the agent picker
when missing/too old, and disables the Create button.
- Submit handler also catches the server's 422 (defensive race —
runtime can re-register between pre-check and submit) and
surfaces the same wording in the error row.
Switching to manual create remains a clean escape hatch — manual
mode doesn't talk to a daemon at all, so an outdated CLI doesn't
block the user from filing the issue.
A complete UX upgrade for chat sending → receiving → recovering.
* StatusPill replaces the orphan spinner — stage-aware copy
("Reading files · 12s", "Searching the web · 14s", "Typing · 24s"),
shimmer text, monotonic timer, derived effective status, > 60s
warning tone, > 5min cancel button.
* WS writethrough on task:queued / task:dispatch / task:cancelled so
pendingTask cache stays in sync with the daemon state machine without
invalidate-refetch latency. broadcastTaskDispatch now includes
chat_session_id when the task is for a chat session — the existing
payload only carried it on the generic task: events, leaving the pill
stuck at "Queued" until completion.
* Failure fallback — FailTask writes a chat_message tagged with
failure_reason (mirrors the issue path's system comment, gated on
retried==nil). Front-end renders an inline note ("Connection failed",
with a Show details collapsible) instead of the previous black hole.
* Elapsed timing — chat_message.elapsed_ms persists task.completed_at -
task.created_at on success/failure rows. UI shows "Replied in 38s" /
"Failed after 12s" beneath assistant bubbles. Format helper shared
between StatusPill and the persisted caption so the live timer and
final reading never disagree.
* Optimistic burst rebalanced — pendingTask seed + created_at moved
before the HTTP roundtrip so the pill appears the instant the user
hits send; handleStop is fire-and-forget so cancel feels immediate
(server confirmation arrives via task:cancelled WS).
* Presence integration — chat avatars use ActorAvatar (status dot +
hover card); OfflineBanner above the input on offline/unstable;
SessionDropdown shows per-row in-flight/unread pip plus a
cross-session aggregate pip on the closed trigger.
* Editor blur on send so the caret stops competing with the StatusPill
/ streaming reply for the user's attention.
* Chat panel isOpen now persists globally; defaults to OPEN for new
users (storage key absence) so the feature is discoverable. Existing
users' prior choice is respected.
* DB: migrations 062 (failure_reason) + 063 (elapsed_ms), both
ADD COLUMN NULL — fast, non-blocking, backwards compatible.
* WS: task:failed chat path now invalidates chatKeys.messages — fixes
a pre-existing bug where the failure bubble required a page refresh
to appear.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(execenv): default-disable Codex native multi-agent in per-task config
Recent Codex app-server releases enable features.multi_agent by default,
exposing spawn_agent / wait / close_agent tools that let a parent thread
spawn nested subagents. The daemon currently models only the parent thread,
so the parent's turn/completed is treated as task completion even when
spawned children are still running — leading to premature task completion
and dropped child output.
Disable features.multi_agent by default in the per-task CODEX_HOME/config.toml
so Multica's task lifecycle is the only orchestration layer in play. Strip
both the dotted-key form (features.multi_agent) at TOML root and the
multi_agent key inside a [features] table; siblings and unrelated tables
are preserved. Honor MULTICA_CODEX_MULTI_AGENT=1 as an opt-out for users
who explicitly want Codex native subagents inside a Multica task.
The user's global ~/.codex/config.toml is never modified — only the daemon's
isolated per-task copy.
Also widen managedBlockRe to consume `\n*` rather than `\n?` so reruns
don't accumulate blank lines when both the sandbox and multi-agent managed
blocks coexist.
* fix(execenv): inject managed multi_agent inside existing [features] table
Per PR review (codex_multi_agent.go:77-83 vs :112-115): when the user's
config.toml already has a top-level `[features]` table, writing
`features.multi_agent = false` at the TOML root implicitly redefines the
same `features` table. The strict TOML parser used by Codex (`toml-rs`)
rejects that with `table 'features' already exists`, so Codex would fail
to load the per-task config and refuse to start the thread. Verified the
strict-parser failure with pelletier/go-toml/v2; the previous
BurntSushi/toml-based regression test was permissive enough to miss it.
Detect a root-level `[features]` header and place the managed block
inside that table (`multi_agent = false` with marker comments). When no
such header exists, keep the existing root-level dotted-key form. The
managed-block regex matches both layouts so reruns and layout
transitions stay idempotent. A `[features.experimental]` sub-table
without a bare `[features]` header still uses the root dotted-key form,
which is spec-valid (no explicit redefinition).
Tests now use pelletier/go-toml/v2 to actually parse the output and
assert features.multi_agent decodes to false; the regression case from
the PR review is covered explicitly.
* fix(execenv): recognize feature table header variants
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
* feat(server/auth): cache PAT lookups in Redis with 60s TTL
Personal access tokens used to hit Postgres on every request: a SELECT
to resolve token_hash → user_id, plus a fire-and-forget UPDATE of
last_used_at. For a CLI / daemon making many requests per second this
is wasted DB load — the token is the same and the answer hasn't changed.
Add a Redis-backed cache (auth.PATCache) keyed by token hash, TTL 60s:
- On cache hit, the auth middleware skips both the SELECT and the
last_used_at UPDATE. last_used_at is now refreshed at most once per
TTL window per token, not per request.
- On cache miss the middleware falls back to today's behavior: query
Postgres, populate the cache, async-update last_used_at.
- On revoke, the handler invalidates the cache entry so revocation
takes effect immediately rather than waiting for the TTL to expire.
This required changing RevokePersonalAccessToken from :exec to :one
RETURNING token_hash.
The cache is nil-safe: when REDIS_URL isn't configured, NewPATCache
returns nil and the middleware degrades to today's always-hit-DB
behavior. JWT validation is untouched (already DB-free).
Tested with REDIS_TEST_URL — same gating pattern the rest of the
suite uses for Redis-backed tests. New tests cover nil-safety, set/
get/invalidate, TTL, and the middleware short-circuit on cache hit.
* fix(server/auth): clamp PAT cache TTL to token's remaining lifetime
GPT-Boy review caught: a PAT expiring in <60s would still be cached
for the full PATCacheTTL window, so the token could continue passing
auth on cache hit for up to ~60s after its expires_at. The DB query
filters expired tokens (revoked = FALSE AND expires_at > now()), but
that filter never ran on a cache hit.
Make Set take an explicit ttl, and add TTLForExpiry to compute it:
- no expires_at → full PATCacheTTL
- expires_at far → full PATCacheTTL
- expires_at <60s → time until expiry
- already expired → 0, Set skips caching (TOCTOU defense between
the SELECT and the Set, since the SELECT
already filters expired rows)
Regression test pins the clamp behavior end-to-end against Redis.
* feat(server/auth): cache daemon-token + PAT lookups in DaemonAuth, bump TTL to 10m
Daemon /api/daemon/* requests (heartbeat, claim task) hit DaemonAuth
which previously did its own GetDaemonTokenByHash on every request and
*also* duplicated the PAT lookup on the mul_ fallback — bypassing the
cache added in 1cdd674c. Today's daemons authenticate via mul_ PATs
(mdt_ minting isn't wired up yet), so the duplicate PAT path is the one
that actually matters for hot-path DB load.
Three changes:
1. New auth.DaemonTokenCache mirrors PATCache for the mdt_ path
(key = mul:auth:daemon:<sha256>, JSON value = {workspace_id, daemon_id}).
Forward-looking infrastructure for when daemon tokens get minted; the
middleware short-circuits the DB SELECT on cache hit. TTL clamped to
the token's expires_at via the shared TTLForExpiry helper.
2. DaemonAuth now also consults PATCache on its mul_ fallback, sharing
the same cache as the regular Auth middleware. A daemon making 4 hb/min
collapses from 4 GetPersonalAccessTokenByHash + 4 last_used_at writes
per minute to ~1 of each per AuthCacheTTL window (~10 minutes).
3. Rename PATCacheTTL → AuthCacheTTL and bump from 60s to 10 minutes.
The constant is now shared between PAT and daemon caches; 10m matches
the user-requested longer TTL for further DB write reduction. Revoke
latency on the happy path is still instant via active invalidation;
the worst-case (Redis Del miss / direct-DB revoke) grows from ~60s to
~10m.
Tests cover nil-safety, set/get/invalidate, TTL, clamped TTL on near-
expiry tokens, and the middleware short-circuit for both cache paths
(mdt_ via DaemonTokenCache, mul_ fallback via PATCache).
* feat(server/auth): cache PAT lookups on the WebSocket auth path
The third place a PAT is resolved — patResolver.ResolveToken used by
realtime.HandleWebSocket — was still hitting Postgres on every /ws
auth and firing an unconditional last_used_at UPDATE, bypassing the
cache added in 1cdd674c. Wire it through the same shared PATCache so
revoking a token through any path (Auth middleware, DaemonAuth PAT
fallback, or WS auth) hits all three caches with one Invalidate.
Also leaves a comment on DeleteDaemonTokensByWorkspaceAndDaemon —
the query has no caller today, but a future deregister/rotate flow
must remember to call DaemonTokenCache.Invalidate(hash) for each
deleted row, otherwise deleted daemon tokens stay valid until TTL.
PR #1744 fixed literal `\n\n` rendering for the CLI surfaces (`issue
create / update --description`, `issue comment add --content`) but the
agent-completion path bypasses the CLI entirely: the daemon POSTs the
agent's stdout to `/api/daemon/tasks/:id/complete`, and `TaskService.
CompleteTask` writes `payload.Output` straight into `createAgentComment`
and `CreateChatMessage` without decoding. Models (e.g. Codex) routinely
emit Python/JSON-style `\n` literals in their final output, which then
land in the DB as the 4-char escape sequence and render as one wall of
text in the issue/chat panel — exactly the bug report in #1820.
- Move `unescapeFlagText` from `server/cmd/multica/cmd_issue.go` to
`server/internal/util/text.go` as `UnescapeBackslashEscapes` so the
CLI and the service layer share one implementation. The full
contract-boundary test suite moves with it.
- Apply `UnescapeBackslashEscapes` to `payload.Output` before it
reaches `createAgentComment` and `CreateChatMessage` in
`TaskService.CompleteTask`. Same `\n / \r / \t / \\` decoding as the
CLI; other escape sequences (`\d`, `\w`, `\u`, etc.) pass through
verbatim so regex/format strings in agent output survive.
Closes#1820
Two coordinated fixes for a quick-create case where the agent ended up
creating duplicate issues. Repro: user pasted an image into the
quick-create prompt; the front-end uploaded it and embedded the URL as
markdown in the user input; the agent saw the URL, assumed it was an
attachment, and ran `multica issue create … --attachment "https://…"`.
The CLI POSTed the issue first, then failed to read the URL as a file
(`os.ReadFile("https://…")`) and exited 1. The agent treated exit 1 as
"create failed" and retried — but the first issue already existed, so
the workspace ended up with two of them.
CLI (`server/cmd/multica/cmd_issue.go`):
- `runIssueCreate` pre-validates `--attachment` BEFORE POSTing. URLs are
warned about and skipped (they are never local files); local-path
read errors fail before the issue is created so no half-baked issue
lands. Once the POST succeeds, post-create upload failures only
print a stderr warning and the issue metadata is still emitted —
never a non-zero exit, so callers cannot mistake "attachment upload
hiccup" for "create failed" and retry.
- `runIssueCommentAdd` already uploads attachments BEFORE the comment
is created, so its failure mode is fine; it just gets the same
URL-skip behaviour for consistency.
Quick-create prompt (`buildQuickCreatePrompt`):
- Tells the agent NOT to pass `--attachment` for prompt-embedded image
URLs (they are already part of the description as markdown).
- Hardens the "no retry" rule: even on a non-zero exit, do not retry
`issue create` — the issue may already exist.
Custom Next.js root not-found.tsx with cream/ink/terracotta editorial
palette and Instrument Serif hero. Replaces the bare default 404 on any
unmatched URL. Single CTA back to /, which routes appropriately based
on auth state.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pasting a screenshot into the agent-create prompt expanded the editor
unbounded, which dragged DialogContent past the viewport since the agent
mode className had no max-height. Manual mode was unaffected because
manualDialogContentClass pins `!h-96`.
- Cap agent-mode DialogContent at `!max-h-[80vh]` (width stays
`!max-w-xl`); short prompts still render compact, tall content stops
at 80% of the viewport.
- Switch the editor wrapper to `flex-1 min-h-[140px] overflow-y-auto`
so it absorbs the remaining vertical space inside the now-bounded
DialogContent and scrolls internally instead of pushing the dialog.
GitHub #1839: when an issue is reassigned from agent A to agent B, B
often only reads the issue body and misses context A added in comments
(e.g. which repo to clone). The assignment-triggered workflow injected
into CLAUDE.md / AGENTS.md said "Read comments for additional context
or human instructions" — vague enough that agents routinely skipped
it. The comment-triggered branch already gives an explicit
`multica issue comment list` invocation, so behavior diverged.
Promote step 3 to a concrete CLI call, mark it mandatory, and surface
the most common failure mode (stale instructions on reassignment) so
the agent recognizes when it matters. Reorder so comments are read
*before* flipping status to `in_progress`, matching how a human would
catch up on a thread before claiming work.
* feat(quick-create): default assignee to picker agent when user didn't name one
The quick-create prompt previously told the agent to OMIT --assignee
when the user's input didn't name a person. That left almost every
quick-created issue unassigned, which doesn't match user intent — the
user opened quick-create with a specific agent picked, so that agent
is the obvious owner.
Both prompt surfaces (BuildPrompt for the dispatched message, plus
the workflow block in injected CLAUDE.md / AGENTS.md) now instruct
the agent: if the input doesn't name an assignee, pass
`--assignee "<your name>"`. The picker agent's name is interpolated
into the prompt at task-build time so the agent has a literal value
to use rather than guessing its own name. The "explicitly named
assignee → resolve via members" branch is unchanged.
* refactor(execenv): drop duplicated quick-create field rules from CLAUDE.md/AGENTS.md
The quick-create field rules (title / description / priority / assignee
fallback / project / status) lived in two places — the per-turn user
message built by BuildPrompt, and the workflow block injected into
CLAUDE.md / AGENTS.md by buildMetaSkillContent. Same content, two
sources, easy to update one and forget the other (the assignee-default
change in this PR had to touch both).
Quick-create is one-shot, so the per-turn user message is always
present and is the natural single source of truth. The injected
file's quick-create section now keeps only the hard guardrails:
"do exactly one issue create, no issue get / status / comment add,
exit on CLI error". Those guardrails stay in BOTH surfaces because
they're the safety net for providers that don't propagate the user
message into resumed-session context.
renderQuickCreateContext (issue_context.md) was already
guardrails-only — no change needed there.
* fix(comment): don't inherit parent @mentions when parent author is an agent
When an agent posts a comment that @mentions another agent (typically a
one-shot delegation, e.g. a PR-completion comment that asks a reviewer
agent to review), member follow-up replies in the same thread were
auto-inheriting that mention and re-triggering the reviewer on every
plain question. Same root cause: the inheritance branch only required
the reply to have no mentions, not that the parent was member-authored.
Tighten the guard: only inherit when the parent (thread root) is
authored by a member. Member-rooted threads still inherit so a member
who started by @mentioning an agent can keep replying without re-typing.
Agent-authored roots are treated as one-shot — explicit @mentions in
later comments still trigger normally.
Extracted the decision into shouldInheritParentMentions for direct unit
testing, and added an end-to-end regression
(TestMemberReplyToAgentRootDoesNotInheritParentMentions) that reproduces
MUL-1535: J posts a PR completion @mentioning Reviewer; a member's
plain follow-up must not re-enqueue Reviewer.
* chore(comment): gofmt trigger_test.go
#1831 fixed the Toaster wrapper to follow next-themes' resolvedTheme,
but the desktop renderer was importing `Toaster` directly from `sonner`
and never going through the wrapper. So the success toast still rendered
light on a dark UI. Switch the import to `@multica/ui/components/ui/sonner`
to match the web app and pick up the theme + icon overrides.
* fix: polish quick-create UX (kind labeling, dark toast, placeholder)
Three small fixes shaken out from using the agent-create flow:
- AgentTaskResponse now carries a `kind` discriminator
("comment" | "autopilot" | "chat" | "quick_create" | "direct"), computed
from the existing FK shape with no extra DB access. The Activity row
uses it to label quick-create tasks as "Creating issue" instead of
falling through to the generic "Untracked" — once the agent finishes
and the new issue is linked, the row transitions to the normal
identifier+title display.
- Sonner Toaster reads `resolvedTheme` instead of `theme`, so toasts
follow the actual dark/light state. Forwarding "system" let sonner
pick its own answer from `prefers-color-scheme`, which in the Electron
renderer can disagree with next-themes' `html.dark` class — the toast
rendered light on a dark UI.
- Agent-create placeholder rephrased to a more conversational example
with a project reference: "let Bohan fix the inbox loading slowness
in the Web project". Drops the priority hint (priority isn't widely
used) and matches how people actually instruct the agent.
* fix(quick-create): link new issue back to task on completion
Addresses the review on PR #1831: completed quick-create tasks were
left with issue_id=NULL forever, so the activity row stayed on
"Creating issue" instead of transitioning to the normal MUL-XXX +
title rendering once the agent finished.
- Server: notifyQuickCreateCompleted now writes the resolved issue id
back to agent_task_queue.issue_id via a new LinkTaskToIssue query
(guarded by `issue_id IS NULL` so it only ever fills the unset
quick-create case). Best-effort: a write failure logs but doesn't
block the inbox notification.
- Frontend: defensive wording fallback — kind=quick_create rows in
terminal status (completed/failed/cancelled) now render as
"Quick create" instead of the active "Creating issue" label,
covering rows whose link write failed or whose agent never
produced an issue at all.
The agent-facing CLAUDE.md/AGENTS.md injected by InjectRuntimeConfig was
missing every doorway to non-core issue properties:
- `multica issue label list/add/remove` — the only way to label a newly
created issue from the agent. Without it, agents either give up
("no command for that, please add it manually") or hallucinate flag
names like `multica issue create --label foo` and fail.
- `multica issue subscriber list/add/remove` — same story for the
subscribe-on-behalf flow.
- `multica label list/create` — agents need to discover existing label
ids before they can attach one (we don't auto-create labels here).
- `issue create` flag list dropped `--project`, `--due-date`,
`--attachment` even though the CLI has supported them for a while.
- `issue update` flag list dropped `--status`, `--assignee`,
`--project`, `--due-date`, `--parent`, leaving agents thinking they
could only edit title/description/priority via update.
Also splits `issue status` from `issue update` in the doc so the agent
sees the shortcut, and notes the `issue create` body intentionally
does NOT accept labels/subscribers (use the post-create commands).
These were transitional handoff/design docs that fulfilled their purpose:
- docs/agent-runtime-status-redesign.md (802 lines) — design + plan for
PR #1794 (presence v3, availability + last-task split). Shipped.
- docs/agent-runtime-ui-design-brief.md (530 lines) — paired designer
brief for the same redesign. Shipped.
- HANDOFF_ARCHITECTURE_AUDIT.md (383 lines) — 4-task audit packaged for
the workspace URL refactor (PR #1138/#1141). The URL refactor itself
shipped; the other tasks are either resolved or live in code as the
source of truth. File:line snapshots inside have rotted.
Follows the precedent set by #1504 (chore(docs): remove shipped plan and
proposal docs). Code is the source of truth once the work is in.
Prod slow-log on the deployed v0.2.17 fix shows total_ms=4012,
auth_ms=4010, update_ms=1, all skill stages = 0 — meaning the bottleneck
on /api/daemon/heartbeat is now the auth section, not the Redis claim
path. To pinpoint which sub-stage dominates, decompose auth_ms into:
- decode_ms — JSON body decode
- runtime_lookup_ms — Queries.GetAgentRuntime (PG PK select)
- workspace_check_ms — requireDaemonWorkspaceAccess (string compare for
daemon-token, requireWorkspaceMember for PAT/JWT)
Also add auth_path ("daemon_token" | "pat" | "jwt") set by DaemonAuth
middleware so slow-logs disambiguate which token kind was used. PAT/JWT
takes an extra DB round-trip via requireWorkspaceMember and is a
candidate cause of long auth tails on daemons that haven't migrated to
mdt_ tokens.
The handler keeps the same external behavior; the change inlines and
instruments requireDaemonRuntimeAccess in DaemonHeartbeat only — other
callers of the helper are untouched. logHeartbeatEndpointSlow gains the
new fields.
Existing heartbeat tests pass; the slow-probe test output now shows the
new auth_path / decode_ms / runtime_lookup_ms / workspace_check_ms
fields populated.
Members and agents previously appeared in fixed buckets (members first,
then agents) following raw cache order. Replace that with a single ranked
list driven by the user's most recent mentions on this device, with an
alphabetical fallback for never-mentioned targets. Recency is stored in
localStorage per workspace and lazy-pruned at 200 entries.
Recasts Quick/Advanced as Agent/Manual and lets users flip between modes
in-place from a footer switch button instead of a separate Advanced
shortcut. The two old modal types now route through one CreateIssueDialog
shell that owns the single <Dialog> and <DialogContent> — only the inner
panel body swaps on mode change, so the Portal/Backdrop/Popup stay
mounted and the switch is instant (no close→open animation flash).
Mode preference is persisted globally in localStorage via a small
useCreateModeStore, so the `c` shortcut always opens whichever mode the
user last used (or switched to). Carry payload (description / agent /
prompt) hands off through the shell's local state plus the existing
issue-draft store, so nothing the user typed is lost across switches.
Also drops the Shift+C → manual branch — `c` is now mode-agnostic and
the in-modal switch covers the same intent without users having to
remember a second shortcut.
Visible labels: "Quick create" → "Create with agent",
"New issue" → "Create manually".
Two related bugs that combined to leave every quick-create task in
'queued' from the user's POV:
1. ResolveTaskWorkspaceID returned "" for any task whose
issue_id / chat_session_id / autopilot_run_id were all NULL —
exactly the shape of a quick-create task. That made
requireDaemonTaskAccess 404 on the daemon's /start, /progress,
/complete, /fail endpoints, and silently dropped task:dispatch /
task:completed broadcasts. Even when the claim itself succeeded,
the daemon couldn't drive the task forward, so it stalled and
eventually got swept back. Read the workspace from the
QuickCreateContext JSONB so every downstream lookup works.
2. EnqueueQuickCreateTask never called notifyTaskAvailable, so the
daemon WS wakeup never fired for quick-create. The 30 s poll
fallback would eventually pick the task up, but combined with #1
that meant the task spent the bulk of its life looking like
"queued, never triggered". Match the chat / issue / autopilot
enqueue paths and signal the wakeup.
* refactor(views): migrate agent/runtime/skill lists to TanStack DataTable
Replace the per-page CSS Grid + minmax(min, fr) + sticky-first-col + truncate
implementation with a TanStack Table backend rendered through a Dice UI-style
DataTable shell. Column widths are now px-based via column.size, so cells
no longer shrink or auto-truncate as the viewport narrows; when the sum of
columns exceeds the viewport, the container scrolls horizontally instead.
- Add @tanstack/react-table to the catalog (8.21.3) and wire it into
packages/ui (dep) and packages/views (peerDep).
- packages/ui: new DataTable + DataTableColumnHeader + lib/data-table.ts
(getColumnPinningStyle), adapted from Dice UI's registry. The shell
renders <table> directly (skipping shadcn's <Table> wrapper) so its own
outer overflow controls both axes — no nested overflow conflicts.
- packages/views: each list now declares ColumnDef[] with explicit
cell renderers. Row click navigates to detail via onRowClick (instead of
wrapping <tr> in <a>, which is invalid HTML); kebab dropdowns
stopPropagation so they don't trigger the row navigation.
- Drop the previous AGENT_LIST_GRID / GRID_WITH_OWNER / ROW_GRID
templates and the sticky-first-col / subgrid mechanics that came with
them. agent-list-item.tsx is removed; runtime-list.tsx and
skills-page.tsx are trimmed to thin wrappers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent): cap description at 255 chars (db + api + ui)
Symmetric enforcement across DB, server, and UI:
- Migration 060: pre-flight truncate of any oversize rows, then ADD
CONSTRAINT NOT VALID + VALIDATE CONSTRAINT so the new check doesn't
block writes during validation.
- Server handler validates utf8.RuneCountInString on Create/Update and
rejects over-limit input with 400.
- Front-end gets AGENT_DESCRIPTION_MAX_LENGTH in core/agents/constants
(single source of truth shared by the create dialog + edit modal +
test suite) and a CharCounter component that warns at 90% and errors
past the cap.
- Description editor moves from a 288px popover to a roomy modal.
Editor body is mounted only while the dialog is open, so the local
draft state is locked in at mount time and never reset by an external
WS update — the React-recommended replacement for the
useEffect(reset, [value]) anti-pattern.
Counted in code points everywhere (rune count / spread length /
char_length) so multibyte input agrees across all three layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(views): data-table polish across runtime + skill lists
Builds on the DataTable migration in 2be0f287:
- Add ColumnMeta.grow flag — declared via TanStack module augmentation
in ui/lib/data-table.ts. Columns marked meta.grow skip their inline
width so fixed table-layout assigns them the leftover container space
(no spacer column). The Title-grows / others-fixed pattern from
Linear / GitHub PR rows.
- Authoritative table min-width = sum of column.size, applied to the
<table> itself (fixed-layout ignores cell-level min-width per spec,
so the floor has to live on the table).
- Header tightens to h-8 + uppercase + tracking-wider; pinned cells
switch to opaque bg + group-hover so they cover content scrolling
beneath them and follow row hover state.
- Toolbar slot removed from DataTable (callers wrap the toolbar
themselves now — keeps DataTable single-purpose).
Also: hover-card popup stops contextmenu / auxclick / dblclick from
bubbling out (in addition to click). Stops the popup from triggering
ancestor handlers (e.g. issue list rows) on right-click / middle-click
without breaking Base UI's outside-click dismiss, which listens to
pointerdown — pointerdown is deliberately NOT stopped.
Runtime + skill list pages updated to use the new sizing model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent): drop LastTaskState, introduce 3-state Workload
Continues the presence-model rework started in #1794 / #1798.
The previous LastTaskState union (running / completed / failed /
cancelled / idle) carried historical outcome at the list level — a
runtime-healthy agent whose last task failed showed a sticky red dot
indistinguishable from a daemon-dead agent.
New model: presence is two orthogonal "right-now" dimensions:
AgentAvailability — runtime reachability only (online / unstable /
offline). Drives the dot colour everywhere.
Workload — current load (working / queued / idle). Three
states, never historical. Failure / completion /
cancellation are surfaced via Recent Work + Inbox,
not list-level state.
`queued` (= nothing running, ≥1 queued) is an honest "stuck on offline
runtime" signal. To avoid amber flashes during the brief enqueue→claim
race on healthy runtimes, the queued chip composes with availability:
muted on online, warning amber otherwise.
Activity tab cleanup that follows from the new model:
- failureReasonLabel relocated from agents/presence.ts to
tabs/task-failure.ts (presence no longer owns historical state).
- Recent Work paginates (5 initial, +20 per "Show more"); chat-session
tasks are filtered out of every Agent-scoped surface to keep
"team work" separate from private chat.
- Agents page drops the lastTaskFilter chip group; users find broken
agents via Inbox / Recent Work, not a list-level filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(task): trigger summary snapshot + task:queued lifecycle event
Two task-lifecycle improvements that ship together because they share
the same enqueue/retry hot paths and changes interleave inside task.go:
1. trigger_summary snapshot (migration 061)
New nullable column on agent_task_queue. Comment-triggered tasks
snapshot the comment content; autopilot tasks snapshot the run title.
Truncated to 200 runes via strings.Builder so multibyte input counts
correctly without O(N²) concatenation. Snapshot survives source
edits/deletes — every task row self-describes across surfaces (issue
detail Execution log, agent activity tooltip, inbox) without joining
back to the originating row.
Retry rows inherit the parent's snapshot (CreateRetryTask SELECT) so
the description stays meaningful across attempts. The UI is
responsible for stacking "Retry #N" context on top.
2. task:queued WS event
New protocol event covering the ∅ → queued transition. Front-end
types/events.ts registers it; use-realtime-sync's task: prefix path
already invalidates task caches via onAny, so old clients without
this exact-match subscription still refresh correctly. Specific
subscribers (sticky banner) get sub-second updates instead of
waiting for daemon claim.
Retry path now broadcasts task:queued (not task:dispatch) — same
status transition shape as enqueue, so all "new task created" paths
agree on one event type.
Ordering: broadcastTaskEvent runs *before* notifyTaskAvailable so
the queued event is published into the WS bus before the daemon is
poked. Without this, a fast daemon could claim and emit task:dispatch
over the wire before the in-process queued broadcast fan-out reached
clients — race window is tiny but unsafe-by-construction.
Per-agent task list (agentTasksKeys.all) and per-issue task list
(["issues","tasks"]) added to the task: invalidation set so Activity
tab Recent Work and the Execution log section stay fresh.
Type contracts: AgentTask gains parent_task_id / attempt /
trigger_comment_id (already returned by the API, just missing from TS)
plus the new trigger_summary field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issue): ExecutionLogSection — unified active+past runs panel
Replaces two pieces:
- the click-to-expand timeline that lived inside AgentLiveCard
- the standalone TaskRunHistory below the main content
with a single right-panel section that lists every agent run for the
issue. Active runs sit at the top (always visible when present); past
runs collapse behind a "Show past runs (N)" toggle, sorted failed →
cancelled → completed within group.
Active rows show the trigger summary, status + relative time, and
Cancel / Transcript actions on hover (gradient backdrop fades the
status text rather than hard-clipping). Past rows show the same
shape minus Cancel.
Retry tasks prepend "Retry #N · " to the inherited summary so they're
distinguishable from their parent (which would otherwise share the
exact same trigger text).
Cache key registered as issueKeys.tasks(issueId); the global
useRealtimeSync task: prefix path already invalidates ["issues","tasks"]
on every task lifecycle event, so the section stays fresh without
local WS subscriptions.
AgentLiveCard slims down to a header-only "agent is working" sticky
banner — keeps the at-a-glance "is anyone working on this right now"
signal and the Stop / Transcript actions, drops the inline timeline
that ExecutionLogSection now owns. Subscribes to both task:queued and
task:dispatch so retries (which only emit queued) land in the banner
without waiting for daemon claim.
issue-detail mounts ExecutionLogSection in the right panel and removes
the now-defunct TaskRunHistory call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ActorAvatar prop was renamed in #1794 (split presence into
availability + last-task) — `disableHoverCard` is now `enableHoverCard`
with inverted semantics. The QuickCreate modal landed against the old
API and broke main's frontend typecheck. The two avatars in the modal
already want the default (no hover card), so just drop the prop
instead of opting in.
Codex Desktop writes one [[skills.config]] entry per known skill into
~/.codex/config.toml. File-backed entries get path = "...", but
plugin-backed entries (e.g. name = "superpowers:brainstorming") only get
a name. Codex CLI 0.114's TOML deserializer treats path as required, so
it rejects the plugin entries with "missing field path" and fails
thread/start.
The daemon copies ~/.codex/config.toml verbatim into each task's
isolated codex-home, which propagated those broken entries into the
per-task config and blocked every Codex agent run for affected users.
Strip the whole [[skills.config]] array on copy. Multica writes the
agent's currently assigned skills directly to codex-home/skills/ and
Codex auto-discovers them from there, so the user-level skill registry
is redundant for a per-task run.
Closes#1753
* feat(server): add quick-create issue async task path
Adds POST /api/issues/quick-create which validates the picked agent's
reachability up front (not archived, has runtime, runtime online) then
queues an issue-less agent task whose context JSONB carries the user's
natural-language prompt + requester + workspace. Daemon claim resolves
the workspace from the context, and the prompt builder switches to a
quick-create template instructing the agent to translate the prompt
into a single multica issue create call.
Task completion writes a success inbox item to the requester pointing at
the newly-created issue (located by querying the agent's most recent
issue in the workspace since task start, so we don't depend on agent
stdout shape). Failures write an action_required inbox item carrying the
original prompt + agent id so the frontend can offer "Edit as advanced
form" without losing input.
* feat(views): quick-create issue modal + inbox failure CTA
Adds a streamlined create-issue UI bound to the c shortcut: pick an
agent, type one line, submit. The modal closes immediately and the
agent translates the prompt into a multica issue create call in the
background. Shift+c keeps the legacy advanced form for users who want
every field. The "Advanced" button inside the new modal seeds the
shared issue-draft store with the prompt + picked agent so switching
mid-flow doesn't lose input.
Last-used agent persists per (user, workspace) via a workspace-aware
zustand store so frequent users skip the picker on every open.
Inbox renders quick_create_done items with a status pin to the new
issue and quick_create_failed items with an "Edit as advanced form"
CTA that re-seeds the legacy modal with the original prompt.
ApiError now carries the parsed JSON body so the modal can branch on
the structured agent_unavailable code without parsing the error
message.
* fix(quick-create): execenv injection, claim race, private-agent permission
Addresses GPT-Boy review on #1786:
1. execenv was rendering the assignment-task issue_context.md / runtime
workflow even for quick-create, telling the agent to call
`multica issue get/status/comment add` against an empty IssueID.
Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create
branch in renderIssueContext + the runtime_config workflow that
instructs the agent to run a single `multica issue create` and
exit, with explicit "do NOT call issue get/status/comment add"
guards.
2. ClaimAgentTask serialized only on issue_id / chat_session_id, so
concurrent quick-creates on the same agent (both NULL on those
columns) ran in parallel — making the success-inbox lookup race
over "most recent issue by this agent". Adds a third OR clause
that treats "all four FKs NULL" as a serialization key for the
same agent, so quick-create tasks on a given agent run one at a
time.
3. QuickCreateIssue handler bypassed the private-agent ownership rule
that validateAssigneePair enforces elsewhere — a user could POST a
private agent_id they didn't own and trigger it. Now routes the
picked agent through validateAssigneePair before the runtime
liveness check.
4. Clarifies the quick-create-store namespacing comment to match the
actual workspace-aware StateStorage convention used by the other
issue stores (per-user is browser-profile-local).
* fix(quick-create): branch Output section + deterministic origin lookup
Addresses GPT-Boy's second-pass review on #1786:
1. The runtime_config.go Output section forced "Final results MUST be
delivered via multica issue comment add" for every non-autopilot
task — quick-create still got this conflicting instruction even
though there's no issue to comment on. Switched the Output block
to a three-way switch so quick-create gets a tailored "stdout is
captured automatically; do NOT call comment add" branch matching
the autopilot variant.
2. Completion lookup was "most recent issue created by this agent
since task.started_at", which races against concurrent issue
creates by the same agent (assignment task running alongside
quick-create when max_concurrent_tasks > 1). Replaced with a
deterministic origin link:
- Migration 060 extends issue.origin_type CHECK to allow
'quick_create'.
- Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a
quick-create task.
- multica issue create CLI reads the env var and stamps the new
issue with origin_type=quick_create + origin_id=<task_id>.
- Server CreateIssue handler accepts (origin_type, origin_id)
from trusted callers (only "quick_create" is allowed; the pair
is rejected unless both fields are provided together).
- notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on
(workspace_id, "quick_create", task.ID) — no more time-window
racing against parallel agent activity.
The old GetRecentIssueByCreatorSince query is removed.
On Windows the official cursor-agent installer ships cursor-agent.cmd whose
body is `powershell ... -File cursor-agent.ps1 %*`. CreateProcess for a .cmd
file goes through cmd.exe, and `%*` in a batch file is expanded by
re-tokenising the original command line, which mangles arguments containing
newlines or other whitespace - most notably a long, multi-line `-p <prompt>`.
The agent then only sees a truncated prompt and fails with "Workspace Trust
Required" or exits 1 immediately.
When LookPath resolves cursor-agent to a .cmd/.bat launcher and a sibling
cursor-agent.ps1 exists, invoke PowerShell directly with `-File <ps1>` so
Go's os/exec passes each argv as a discrete token. This is exactly what the
.cmd does internally; we just skip the cmd.exe re-tokenisation step.
PowerShell host resolution prefers pwsh.exe (PS 7) on PATH, then
powershell.exe on PATH, and finally falls back to
%SystemRoot%\System32\WindowsPowerShell\v1.0.
Platform-specific code is split via build tags
(cursor_invocation_windows.go / cursor_invocation_other.go) so non-Windows
builds carry no Windows-only dependencies. The lookup is exposed as a
package variable to make the Windows path fully unit-testable without
spawning real PowerShell. Five unit tests cover: passthrough on non-launcher
targets, successful rewrite with a multi-line prompt, .exe direct launch
(skip), missing .ps1 (skip), and missing PowerShell host (skip).
The change leaves macOS / Linux behaviour entirely untouched and stays on
the official cursor-agent launch chain - no node.exe direct invocation, no
prompt mutation, no extra flags.
Closes#1297
Made-with: Cursor
* fix(popover): stop click bubble + resilient presence loading
Two related bugs surfacing on production after #1794:
* Click-through: clicking a Detail link inside an agent hover card, or
a kebab item in agents/runtimes list rows, also fired the parent row
link's onClick. Base UI portals popovers in the DOM but React's
synthetic events still bubble through the React tree, so the
ancestor <a> wrapping the trigger still received the click. Fix at
the primitive level (HoverCardContent + DropdownMenuContent) so
every existing and future popover gets it for free — stopPropagation
on the popup's onClick, then forward consumer-supplied handlers.
* Presence loading forever: useAgentPresenceDetail returned "loading"
whenever any of its three queries had data === undefined. With prod
backend missing the new agent-task-snapshot endpoint (404), or with
an issue assignee referencing an archived agent (not in ListAgents),
the UI spun forever. Now: query errors degrade to empty arrays, and
a missing agent yields a synthesised offline+idle detail. The dot
still renders gray, hover card still shows "Agent unavailable" —
but no infinite skeleton.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(inbox): enable hover card on notification actor avatar
Originally excluded from the hover-card opt-in pass, but inbox
notifications are exactly the kind of "who sent me this?" surface
where seeing the actor profile on dwell is useful. Click-through to
the wrong target is no longer a concern — the popover stop-bubble
fix in this branch handles it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(autopilot): show agent presence dot on autopilots list rows
Autopilot detail / picker / dialog already render the dot — the list
was the lone holdout. With the autopilot-agent dependency this strong
("autopilot is dead if its agent is offline"), an at-a-glance dot is
the most useful signal in the row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type
Lays the API + type contract for the front-end agent presence cache:
- New `GET /api/active-tasks` returns active (queued/dispatched/running)
tasks plus failed tasks within the last 2 minutes for the current
workspace. The 2-minute window powers a UI-side auto-clearing "Failed"
agent state without back-end pollers.
- `agent_task_queue` has no workspace_id column, so the query JOINs agent;
`SELECT atq.*` keeps `failure_reason` (migration 055) on the wire.
- Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend
classifiers (agent_error / timeout / runtime_offline / runtime_recovery
/ manual) to copy without parsing free-text errors.
- New `api.getActiveTasksForWorkspace()` client method; workspace is
resolved server-side from the X-Workspace-Slug header (no path param,
matching /api/agents and /api/runtimes conventions).
Includes the joint engineering plan and designer brief that scope the
broader Agent / Runtime status redesign — Phase 0 is this contract plus
the front-end derivation layer landing in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge
Adds the front-end derivation layer that turns raw server data into the
user-facing 5-state agent / 4-state runtime enums. UI files are
deliberately untouched in this commit — derivation lives behind hooks
(useAgentPresence, useRuntimeHealth) that any component can call with
zero additional network traffic.
Architecture:
- Derivation is pure functions in packages/core/{agents,runtimes}; the
back-end stays free of UI translation. Agents algorithm: runtime
offline > recent failed (2-min window) > running > queued > available.
Runtimes algorithm: status + last_seen_at -> online / recently_lost /
offline / about_to_gc.
- A single workspace-wide active-tasks query backs all per-agent
presence reads, eliminating N+1 across hover cards, list rows, and
pickers. 30-second tick re-renders the hooks so the failed window
expires even when no underlying data changes.
- WS task lifecycle events (dispatch / completed / failed / cancelled)
invalidate active-tasks via the prefix dispatcher. completed/failed
were removed from specificEvents so they go through both the prefix
invalidate and the existing chat ws.on() handlers. Reconnect refetch
picks up active-tasks too.
- Desktop bridges window.daemonAPI.onStatusChange directly into the
runtimes cache via setQueryData, giving the local daemon sub-second
feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace
switches automatically rebind the subscription; daemon_id matching
covers the same-daemon-multiple-providers case.
24 derivation unit tests cover all branches plus null/empty/boundary
inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing
completed_at). Full core suite: 112 tests passing. Typecheck green
across all 8 workspace packages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(agent-status): redesign agent runtime status as two orthogonal dimensions
Splits the conflated 5-state agent presence into two independent axes:
- AgentAvailability (3-state): online / unstable / offline — drives the
dot indicator everywhere a dot appears. Pure runtime reachability;
never sticky-red because of a past task outcome.
- LastTaskState (5-state): running / completed / failed / cancelled /
idle — surfaced as text + icon on focused surfaces (hover card,
agent detail page, agents list, runtime detail). Never colours the dot.
Major changes:
* Domain layer: AgentPresence union → AgentAvailability + LastTaskState.
derive-presence split into deriveAgentAvailability + deriveLastTaskState
+ deriveAgentPresenceDetail orchestrator. Tests reorganised into three
groups (availability invariants, last-task invariants, composition).
* Visual config: presenceConfig (5 entries) → availabilityConfig (3) +
taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips.
* Workspace-level presence prefetch: new useWorkspacePresencePrefetch
hook + WorkspacePresencePrefetch mount component, wired into
DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards
render synchronously with no skeleton flash on first hover.
* ActorAvatar hover: flipped default — disableHoverCard removed,
enableHoverCard added (default false). Opt-in at ~14 decision-moment
surfaces; pickers / decoration sub-chips stay plain. Status dot
decoupled (showStatusDot prop) so picker rows can show presence
without nesting popovers.
* Hover cards: AgentProfileCard simplified — availability dot only,
Detail link top-right (logs live on the detail page). New
MemberProfileCard mirrors the structure: name + role + email +
top-2 owned agents (sorted by 30d run count) with click-through to
agent detail.
* Agents list: split Status into two columns — availability (3-color
dot + label) and Last run (task icon + label, optional running
counts). Two independent filter chip groups (Status + Last run);
combination acts as intersection ("online + failed" finds broken-
but-alive agents).
* Other UI surfaces (issue list/board/detail, comments, autopilots,
projects, runtimes, mention autocomplete, subscribers picker)
updated to the new dot semantics; status dot now strictly 3-color.
Server changes accompany the client redesign — workspace-wide
agent-task-snapshot endpoint, runtime usage queries, etc. — to feed
the derive layer with the data it needs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent-detail): drop last-task chip from detail header + inspector
The Recent work section on the agent detail page already shows the same
data (with task titles, timestamps, error context) — surfacing
"Completed" / "Failed" / etc. up in the header was redundant chrome.
Detail surfaces now show only the 3-state availability dot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tables): handle narrow viewports across agents / skills / runtimes
Three table layouts were squeezing content into adjacent cells at
intermediate widths. Each fix is small and targeted:
* runtime-list: the Runtime cell's base name had `shrink-0`, so it
refused to truncate when its grid column was narrowed under width
pressure — the name visually overflowed into the Health column
("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health
column was also a fixed 9.5rem reservation for the worst-case
"Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it
competes fairly with Runtime.
* skills-page: had a single grid template with no responsive
breakpoints — all 6 columns were rendered at any width and got
visually jammed below md. Added a <md template that drops Source +
Updated; the row markup hides those cells via `hidden md:block` /
`md:contents`.
* agent-list-item: the new Last run column was reserved at minmax(8rem,
max-content); on narrow md viewports the 8rem floor pushed the row
past available width. Changed to minmax(0,max-content) so the cell
shrinks under pressure (its content already truncates).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(agent-card): hover-only Detail + add Runtime row + breathing room
Three small polish tweaks to the agent hover card:
- Detail link gets `mr-1` + fades in only on card hover (group-hover).
It was visually flush against the popover edge and competing for
attention; now it stays out of the way during a quick glance and
surfaces only when the user is dwelling on the card.
- Runtime row is back, in the meta block (cloud/local icon + runtime
name). The earlier removal was over-aggressive — knowing where an
agent runs is part of "who is this agent". The wifi badge stays
dropped because the availability dot in the header already conveys
reachability.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(runtime): wifi-style health icon (4-state) for runtime list + agent card
Replaces the 6px coloured dot with a wifi-shape icon that carries both
state (Wifi vs WifiOff) and severity (success/warning/muted/destructive).
Mapping:
- online → Wifi (success)
- recently_lost → WifiHigh (warning) — transient hiccup, fewer bars
- offline → WifiOff (muted) — long unreachable
- about_to_gc → WifiOff (destructive) — sweeper coming soon
Used in two places:
- Runtime list: replaces HealthDot in the dedicated leading-icon column.
Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized).
- Agent profile card RuntimeRow: derives runtime health from runtime +
clock (matching the 4-state semantics) and renders HealthIcon next
to the runtime name. Cloud runtimes always read as online. The
duplicate signal with the header availability dot is intentional —
it confirms WHICH runtime is the one currently in the dot's state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ignore Kiro ACP session/load history replay before the active prompt starts; keep task messages, usage, and tool state scoped to the current Kiro turn. Verified with go test ./pkg/agent -run TestKiro, go test ./pkg/agent, and git diff --check origin/main...HEAD.
* docs(changelog): publish v0.2.19 release notes
Today's release covers 23 commits since v0.2.18. Headline items are the
macOS dock unread badge with focus-gated inbox notifications, the daemon
WebSocket task wakeup path that drops task startup latency, and a
client-side label filter on the issue list. Improvements / fixes round
out comment linkify, optimistic label attach, agent-to-agent mention
loop prevention, Codex turn timeouts, Windows daemon survivability, and
the comment-delete task cancellation.
The Kiro CLI runtime addition is intentionally omitted pending a
chat-mode regression flagged before release.
* docs(changelog): include Kiro CLI runtime, drop assignee-default line
Per release sign-off: Kiro CLI ACP runtime ships in v0.2.19 once the
chat-mode regression is fixed, so it goes back into the headline. The
"create-issue remembers last assignee" line is dropped from features
to keep the list to four spotlight items.
* feat(desktop): dock unread badge + focus-gated inbox notifications
Wire two OS-level integrations for inbox activity. Both degrade cleanly on
web and unsupported platforms.
- Unread badge on the macOS dock / Linux Unity launcher. Derived from the
same inbox list the UI renders, deduplicated per issue, capped as "99+"
on macOS via `app.dock.setBadge` (setBadgeCount truncates at 99). New
`useInboxUnreadCount` hook (core/inbox) + `useDesktopUnreadBadge`
(views/platform) keep renderer and main in sync via a `badge:set` IPC.
- Native OS notification on `inbox:new`, fired from the renderer only when
`document.hasFocus()` is false — in-focus feedback is the existing inbox
sidebar's unread styling, so we don't fight macOS's deliberate foreground
suppression. Clicking the banner focuses the main window and navigates
to `/inbox?issue=<key>` via the shared `multica:navigate` bus.
Refactors `inbox-page.tsx` to read the unread count through the new hook
(was a per-render inline filter).
* fix(desktop): pin notification routing to source workspace + mark read on URL select
Two bugs GPT-Boy caught on PR #1445:
1. A notification from workspace A used `getCurrentSlug()` at click time,
so if the user switched to workspace B before clicking the banner (macOS
Notification Center persists banners), routing landed on `/B/inbox?issue=<A key>`
and 404'd. Fix: round-trip the emit-time `slug` through the IPC payload
and use it in the click handler.
2. Notification-click navigation set the URL param but never fired the
mark-read mutation (only InboxPage's click-handler did). The row stayed
unread and the dock badge didn't decrement. Fix: move the mark-read
logic from handleSelect into a useEffect keyed on the selected item —
it now covers both click-to-select and URL-param-select.
IPC payload gains `slug` and `itemId`; preload types + main handler + the
desktop bridge are updated to match.
Adds a Label submenu to the workspace issues filter dropdown, backed by
labelFilters in the shared issue view store. The filter is OR'd within
itself (issue matches if it carries any of the selected labels) and
AND'd with the existing status / priority / assignee / creator /
project dimensions, mirroring the multi-select semantics already in
place. Each label row renders via LabelChip for color parity with the
sidebar picker, and each row's count comes from the same
useIssueCounts pass that drives the other filter chips.
Filtering stays client-side, consistent with all other filters today.
The pagination caveat is a known limitation we'll revisit if real
workspaces start hitting it; this PR intentionally does not change the
fetch path.
Released Desktop builds bake VITE_API_URL/VITE_WS_URL/VITE_APP_URL
at build time and ship pointing at Multica Cloud — there is no
in-app 'Connect to a self-hosted instance' button. Reported in
multica-ai/multica#1768.
- Replace the misleading callout in desktop-app.mdx (and zh) with
the actual self-host path: build from source with custom env, or
use web + CLI. Link to #1371 for the runtime-config feature.
- Soften the corresponding 'Next steps' link in self-host-quickstart
(and zh) so it no longer implies one-click Desktop self-host.
Drop the filled status chip (bg-warning/text-white etc.) from the
list/board column headers — StatusIcon already carries the semantic
color, so the chip duplicated it on the text background, and the
bg-muted variants were nearly invisible against the muted/40 row
background. Wrap the shared icon + label + count in a new StatusHeading
component used by both list-view and board-column.
Remove the now-unused badgeBg/badgeText fields from STATUS_CONFIG.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issues): server-side label + filter querying for issue list
Extends GET /api/issues with label_ids, priorities, creator_ids,
project_ids, include_no_assignee, and include_no_project params, and
moves the existing single-value filters onto array-form. Each filter
becomes part of the SQL WHERE clause so paginated buckets reflect the
user's selection — fixes the bug where client-side filtering hid
matches sitting past the first page (#1491).
CLI gains a repeatable --label flag; legacy --priority/--assignee/
--project keep working via the single-value compatibility paths.
* feat(issues): drive workspace + my-issues filters from the server
issueListOptions and myIssueListOptions now key the React Query cache
on a normalized filter object, so each filter combination has its own
cache entry and a filter change re-fetches with the wire-shape filter
applied server-side. Drops the client-side filterIssues step on the
issues page, my-issues page, and project detail — that step silently
hid matches that lived past the first paginated page (#1491).
Adds a Label submenu to the workspace issues filter dropdown, plus
labelFilters in the view store. Mutations and ws-updaters fan their
optimistic patches across every filter-keyed list cache via
qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's
mention-suggestion reads from any matching list cache for instant
first paint regardless of which filter is active.
* fix(issues): route Members/Agents scope through server-side filter
The Members/Agents scope tabs on the workspace issues page were still
narrowing client-side via `assignee_type === 'member'`. That hits the
exact pagination-blind bug this PR is meant to fix: if the first 50
issues per status don't include the right assignee type, the tab
shows "No issues" while later pages have matches.
Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues /
CountIssues, threads it through the API client, normalizer and view
filter, and maps the scope tab to it. Each scope now keys its own
list cache and refetches with the correct first page.
Also disables the My Issues "My Agents" query when the user owns no
agents — `assignee_ids: []` was getting dropped by both the API client
and the query-key normalizer, so the request went out unfiltered and
surfaced unrelated issues under "My Agents".
Follow-ups to #1765 review nits:
- Tighten the per-turn prompt and AGENTS.md workflow instructions so
that "exit with no output" only applies when the trigger is from
another agent AND no actual work was produced this turn. If the
agent did real work, the standard "post results as a comment" rule
still applies — a result reply is not a noise comment.
- Add TestAgentExplicitMentionStillTriggers as a positive control
documenting the boundary the structural fix preserves: suppressing
implicit parent-mention inheritance for agent authors does NOT
block deliberate handoffs. An agent that explicitly @mentions
another agent in its own content still enqueues a task for the
mentioned agent and does not self-trigger.
When an agent replied in a thread whose root mentioned another agent,
the reply inherited the parent mention and re-triggered the other agent.
This caused 'No reply needed' ping-pong loops between co-assigned agents.
Structural fix:
- In enqueueMentionedAgentTasks, suppress parent-mention inheritance
when authorType == 'agent'. Explicit @mentions in the agent's own
comment still work for deliberate handoffs.
Defense-in-depth (prompt):
- Strengthen per-turn prompt and AGENTS.md workflow instructions to
explicitly forbid posting 'No reply needed' noise comments.
Regression test:
- TestAgentReplyDoesNotInheritParentMentions covers both the fix
(agent reply does not re-trigger) and the positive control
(member reply still inherits mentions).
Also updates TestBuildPromptCommentTriggeredByAgent to match the
new prompt wording.
The create-issue modal now remembers the assignee picked at submit
time and prefills the picker with that value when the modal next
opens. Implemented by tracking lastAssigneeType/Id alongside the
draft and seeding clearDraft's reset with those values.
* refactor(server): make ParseUUID error-returning to prevent silent data loss (MUL-1410)
util.ParseUUID previously swallowed errors and returned a zero pgtype.UUID
on invalid input. When this zero UUID reached a write query (DELETE/UPDATE),
the SQL matched zero rows and the handler returned 2xx success — producing
silent data corruption. #1661 (DeleteIssue with identifier-style ID) was the
visible symptom; PR #1680 patched that one site, this commit closes the
class of bug.
Changes:
- util.ParseUUID now returns (pgtype.UUID, error). Add util.MustParseUUID
for trusted round-trips that should panic on invalid input.
- handler/handler.go: parseUUID wrapper now calls MustParseUUID — any
unguarded user-input string reaching it surfaces as a recovered panic
(chi middleware.Recoverer → 500) instead of silently corrupting data.
Add parseUUIDOrBadRequest(w, s, fieldName) for handler entry points.
- Convert every Queries.Delete*/Update* call site reachable from raw user
input (autopilot, comment, project, skill, skill_file, label, pin,
attachment, feedback, issue assignee, daemon runtime, workspace) to
validate UUIDs explicitly with parseUUIDOrBadRequest, returning 400 on
invalid input. Where a resolved entity.ID is already in scope, write
queries now use it directly instead of re-parsing the URL string.
- Update getWorkspaceMember + loadIssueForUser to handle invalid UUIDs
gracefully (404/400 instead of panic).
- Update util/middleware/cmd-level callers (subscriber_listeners,
notification_listeners, activity_listeners, scope_authorizer,
middleware/workspace) to use the error-returning API.
- Add server/internal/util/pgx_test.go covering valid/invalid input and
the MustParseUUID panic contract.
- Add TestDeleteIssueByIdentifier + TestDeleteIssueRejectsInvalidUUID
regression tests in handler_test.go (the original #1661 bug + the
invalid-input case).
- Document the handler UUID parsing convention in CLAUDE.md so the rule
is enforceable in future PR review.
* fix(server): address GPT-Boy review of #1748
P1 fixes from PR #1748 review:
1. Migrate remaining request-boundary UUIDs to parseUUIDOrBadRequest so
malformed input returns 400 instead of panic/500. Was missing on:
- issue.go: workspace_id in CreateIssue/ChildIssueProgress/ListIssues/
SearchIssues/BatchUpdateIssues/BatchDeleteIssues; project_id /
parent_issue_id / lead_id / assignee_id / assignee_ids / creator_id
filters; batch issue_ids and assignee/parent/project fields in
BatchUpdateIssues (skip on bad input via util.ParseUUID, matching
the existing per-row continue semantics).
- project.go: project id + workspace_id in GetProject/UpdateProject/
DeleteProject; lead_id in CreateProject/UpdateProject;
workspace_id in ListProjects + SearchProjects.
- handler.go: resolveActor now uses util.ParseUUID for X-Agent-ID /
X-Task-ID headers; invalid UUID falls back to "member" (matches
pre-existing semantics) instead of panicking.
- issue.go: validateAssigneePair returns 400 on invalid workspace_id
instead of panicking.
2. Fix issue:deleted WS event payloads to emit uuidToString(issue.ID)
instead of the raw URL string. After an identifier-path delete
("MUL-7"), the previous payload would have leaked the identifier to
subscribers, leaving stale entries in frontend caches that key by
UUID. Updated DeleteIssue (issue.go:1341) and BatchDeleteIssues
(issue.go:1641). The slog "issue deleted" log line also now records
the resolved UUID so logs match the WS payload.
3. Extend TestDeleteIssueByIdentifier to subscribe to the bus and
assert issue:deleted.payload.issue_id is the resolved UUID, not
the identifier.
* fix(server): validate remaining reviewed UUID inputs
* fix(server): validate remaining handler UUID inputs
* fix(server): finish request boundary UUID audit
* fix(server): validate remaining request body UUIDs
* fix(server): validate runtime path UUIDs
* fix(server): validate remaining audit UUID inputs
---------
Co-authored-by: Eve <eve@multica.ai>
* fix(labels): apply attach optimistically so chips render before round-trip
Attach went through onSuccess only, so users waited for the server
before seeing the new chip — out of step with detach (already optimistic)
and with status/assignee/priority via useUpdateIssue. Mirror the detach
pattern: snapshot the byIssue cache, look up the full label from the
workspace list cache, patch byIssue + the issue list/detail caches via
onIssueLabelsChanged in onMutate, and roll back on error. onSuccess and
onSettled keep the existing reconcile behavior.
* fix(labels): only patch attach when prev label set is known
GPT-Boy's review caught a corruption case: when byIssue cache was
unpopulated (user clicked before issueLabelsOptions resolved), the
optimistic patch fell back to an empty prev.labels, then mirrored
[label] into issue list/detail via onIssueLabelsChanged — wiping any
denormalized labels already on the issue. Worse, onError only restored
byIssue when ctx.prev existed, so the wipe persisted on failure.
Match useDetachLabel's invariant: skip the optimistic patch unless prev
is in cache. The chip will wait for the round-trip in the rare race
window, but caches stay consistent and rollback always works.
When a user deletes a comment that triggered an agent task, the agent
would still run with the now-deleted content baked into its prompt
(fetched at task claim time) — manifesting as "the agent still sees the
deleted comment". The FK ON DELETE SET NULL only nullified
trigger_comment_id; the queued task itself was never cancelled.
DeleteComment now cancels any queued/dispatched/running task whose
trigger is the deleted comment, before the comment row is removed.
The release job uses GoReleaser to bump the formula in
multica-ai/homebrew-tap. Forks don't have HOMEBREW_TAP_GITHUB_TOKEN
and should not publish to that tap, so the job currently fails on
every fork tag push (401 Bad credentials against the upstream tap).
This makes the workflow red on downstream forks even though the
actual artifact pipeline (verify → docker-backend-build →
docker-backend-merge) succeeds and produces a usable image.
Gate the release job on `github.repository_owner == 'multica-ai'`.
Upstream behaviour unchanged. Forks now see a clean green run for
docker artifacts only.
* docs(changelog): publish v0.2.18 release notes
Today's release covers 13 PRs since v0.2.17. Spotlight is the full Issue
Labels feature (backend + CLI + Web UI), plus the Labs settings tab,
sidebar invitation indicator, and the sharded Redis realtime relay.
Improvements and fixes round out comment rendering, project-icon usage
across the app, self-host env-var pass-through, and several
Windows-specific agent issues.
* docs(changelog): simplify v0.2.18 entries
Trim each line to a short, user-facing sentence; drop implementation
detail (sharded relay, build-id symlinks, --description-stdin, etc.) per
review feedback that the previous draft was too detailed.
* fix(comments): preserve newlines from agent CLI writes
Agents (e.g. Codex) routinely emit `multica issue comment add --content
"para1\n\npara2"` because Python/JSON-style string literals are their
default. Bash does not expand `\n` inside double quotes, so the literal
4-char sequence flowed through the CLI into the database and rendered
as text in the issue panel — comments came out as one wall of prose.
Three coordinated fixes so the platform behavior no longer depends on
whether a given model has strong bash-quoting intuition:
- CLI: decode `\n / \r / \t / \\` in `--content` and `--description` for
`issue create / update / comment add` (callers needing a literal
backslash still have `--content-stdin`).
- Agent prompt: rewrite the comment-add example in the injected runtime
config to require `--content-stdin` + HEREDOC for any multi-line body,
and call out the same rule for `--description`. The previous wording
flagged stdin only for "backticks, quotes", which models read as
irrelevant to plain paragraphs.
- Renderer: add `remark-breaks` to the shared Markdown plugin chain so a
bare `\n` becomes a visible line break instead of a CommonMark soft
break — protects against models that emit single newlines for
formatting.
Tests: pin the new CLI helper, and pin the runtime-config guidance so
the multi-line wording cannot decay back into a footnote.
* fix(comments): address review feedback on newline-rendering PR
- Cover the issue panel: ReadonlyContent (used by every comment card and
the issue description) has its own react-markdown wiring; add
remark-breaks there too so the renderer fix actually applies to the
surface the bug was reported on, not just the chat panel. Pinned by
ReadonlyContent line-break tests.
- Make the prompt's `--description` guidance executable: add
`--description-stdin` to `issue create` / `issue update`, refactor
comment-add to share a single `resolveTextFlag` helper, and have the
injected runtime config name the real flag instead of an imaginary
"stdin / a tempfile" path. Pinned by the runtime-config guidance test.
- Document the unescape contract on each affected flag's help text and
pin the precise boundary in tests: `\n / \r / \t / \\` are decoded;
`\d / \w / \s / \u / \0` and other unrecognised escapes pass through
verbatim, so regex literals and Windows paths survive intact unless
they embed a literal `\n` / `\r` / `\t`. Callers that need the literal
sequence have `--content-stdin` / `--description-stdin` as the escape
hatch.
- my-issues page lost labels because myIssuesViewStore cherry-picked
name/storage/partialize from viewStorePersistOptions and dropped the
cardProperties-aware merge. Persisted snapshots predating the labels
toggle had cardProperties.labels = undefined, falsy-shorting the chip
render. Extracted mergeViewStatePersisted as a generic and wired it
into both stores.
- list-row chips now render right after the title (with a small left
margin for breathing room) instead of in the right-aligned cluster.
* feat(issues): render labels on list/board with bulk server-side fetch
ListIssues / ListOpenIssues / GetIssue now bulk-fetch labels per response
via a new ListLabelsForIssues query so the client gets labels in a single
round-trip instead of N requests per visible issue. List-row and board-card
read issue.labels directly; an issue_labels:changed WS handler patches the
list and detail caches in place so chips stay live across tabs, and
attach/detach mutations mirror their result into the same caches for
immediate same-tab feedback.
Adds a "Labels" toggle to the card properties dropdown (defaults on).
* fix(issues): preserve cached labels and refresh on label edit/delete
Three fixes from gpt-boy's review of #1741:
1. IssueResponse.Labels was a non-omitempty slice, so paths that didn't
load labels (UpdateIssue, batch updates, the issue:updated WS broadcast)
serialized labels:null. onIssueUpdated then merged that null into the
list/detail caches, wiping chips on every other tab whenever any non-
label field changed. Switched to *[]LabelResponse + omitempty: nil =
field absent (client merge keeps existing labels); non-nil (incl. empty
slice) = authoritative.
2. issue.labels is a denormalized snapshot, but useUpdateLabel /
useDeleteLabel and the WS label:* prefix only touched labelKeys, leaving
stale chips in list/board after rename/recolor/delete. Mutations now
also invalidate issueKeys.all(wsId), and the realtime refreshMap maps
the label prefix to both labels and issues invalidation for cross-tab.
3. Persisted cardProperties from before this branch lacks the new `labels`
key. Render fell back to `?? true` but the dropdown switch read it raw
and showed unchecked. Added a custom Zustand merge that deep-merges
cardProperties so newly added toggles inherit defaults for existing
users; dropped the `?? true` fallbacks now that the store guarantees
the key.
Extract <ProjectIcon> with sm/md/lg sizes and a single 📁 fallback,
replacing 9 inline render sites that had drifted into 6 different
sizes and a mixed FolderKanban/emoji fallback.
Two visible fixes fall out of the centralization:
- ProjectPicker trigger now shows the selected project's icon (most
visibly in the issue detail right Properties panel, where it had
always been a generic FolderKanban).
- Sidebar parent nav (Projects, Issues, Settings, ...) now stays
highlighted on child detail routes via a small isNavActive helper.
Pinned items keep strict equality.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(labels): add issue label CRUD + attach/detach handlers (#1191)
The issue_label and issue_to_label tables were scaffolded in 001_init.up.sql
but never wired to any code path. This commit ships the backend for #1191:
- Migration 048: adds created_at/updated_at timestamps + workspace-scoped
case-insensitive unique index on label names
- sqlc queries for label CRUD + issue<->label attach/detach + batch list
(ListLabelsByIssueIDs for board/list views)
- HTTP handlers: /api/labels CRUD, /api/issues/{id}/labels attach/detach
- Protocol events: label:{created,updated,deleted} + issue_labels:changed
- Handler tests covering CRUD, duplicate-name conflict, invalid-color,
attach/detach idempotency, and cross-workspace isolation
* feat(cli): add label and issue label subcommands (#1191)
- multica label {list,get,create,update,delete}
- multica issue label {list,add,remove}
Both follow existing CLI conventions (JSON/table output, flag shapes)
and exercise the /api/labels endpoints shipped in the previous commit.
* feat(web): add labels UI — picker with inline create + management dialog (#1191)
Exposes the backend label feature to users via the existing issue-detail
sidebar.
- `@multica/core/types/label` — Label, CreateLabelRequest, UpdateLabelRequest,
plus response envelopes
- `@multica/core/api/client` — 8 methods for label CRUD and issue↔label
attach/detach
- `@multica/core/labels` — labelKeys, queryOptions, and mutation hooks with
optimistic updates (matches the project/ module layout)
- WS event type literals extended for label:{created,updated,deleted} and
issue_labels:changed
- `views/labels/label-chip.tsx` — colored pill; uses relative luminance
(ITU-R BT.601) to pick #111827 or #f9fafb text so chips stay readable on
both pastel and saturated backgrounds
- `views/issues/components/pickers/label-picker.tsx`
- Multi-select combobox in the issue sidebar
- When 0 labels: "Add label" trigger
- When 1+ labels: the chips themselves are the trigger; × on each chip
detaches without opening the picker
- Inline create: typing a new name + Enter creates with a hash-derived
color and attaches in one motion (matches Linear/GitHub)
- "Manage labels…" footer opens a dialog containing the full workspace
panel — users never leave the issue context to rename/recolor/delete
- `views/issues/components/labels-panel.tsx` — workspace labels manager.
Single-row create form (color swatch + name + Add button). Each label
row supports inline rename + recolor + delete (with confirm dialog).
Color input uses the browser's native picker for full-gamut access —
no preset palette clutter.
- `PropRow label="Labels"` added to the issue-detail sidebar below Project
Labels are issue metadata everyone uses — not admin configuration.
Putting them in Settings next to destructive workspace actions misframed
them; adding a top-level nav entry or a sibling tab to the Issues page
added surface area that wasn't earning its keep for a feature users
touch occasionally. Keeping management in a dialog launched from the
picker itself keeps users in their issue context and matches how GitHub
handles label editing from the label selector.
Electron apps share an identical upstream Electron binary, so its GNU
build-id is the same across every Electron RPM (Slack, VS Code, Discord,
etc.). The default fpm/rpm behavior owns /usr/lib/.build-id/<hash>
symlinks, which collide between packages and make `dnf install` fail
when any other Electron app is already installed.
Pass `_build_id_links none` to rpmbuild via fpm so the multica-desktop
RPM no longer claims those paths.
Fixesmultica-ai/multica#1723.
* fix(agent/opencode): bypass npm .cmd shim on Windows to preserve multi-line prompts
The npm-generated `opencode.cmd` shim forwards argv via Windows batch `%*`,
which silently truncates positional arguments at the first newline. The
daemon spawns OpenCode with a multi-line prompt (system prompt + user
message), so on Windows the agent only ever sees the first line and
responds generically as if it never received the user's message
(reported in #1717 with native-binary repro confirming the same prompt
arrives intact when cmd.exe is skipped).
When `runtime.GOOS == "windows"` and `exec.LookPath` returns a `.cmd`
shim, walk to the native binary that npm bundles next to the shim:
<prefix>\opencode.cmd
<prefix>\node_modules\opencode-ai\node_modules\opencode-windows-x64\bin\opencode.exe
If the native binary is missing (unusual install layout), keep the
original shim path so PATH lookup still wins. The resolver is a pure
function with an injectable `statFn`, so layout assertions are testable
on Linux:
- shim resolves to the bundled native binary
- missing native returns "" (caller keeps original path)
- non-cmd paths (Linux/Mac binary, opencode.exe direct, empty) skip resolution
- uppercase `.CMD` is accepted (PATHEXT entries can be either case)
Closes the user-facing failure mode without restructuring exec resolution
across the rest of the agent backends — the other shim-aware fixes can
follow the same shape if/when they land in similar repros.
* fix(agent/opencode): cover x64-baseline and arm64 npm package variants
`npm install -g opencode-ai` ships three Windows platform packages
(opencode-windows-x64, opencode-windows-x64-baseline for older CPUs
without AVX2, opencode-windows-arm64 for Surface / Copilot+ PC) and
installs whichever matches the host. The previous resolver only knew
about opencode-windows-x64, so baseline-x64 and arm64 hosts would fall
back to the .cmd shim and hit the multi-line prompt truncation again.
Iterate the three package candidates in GOARCH-preferred order. ARM64
hosts try arm64 first; everything else tries x64, then baseline, then
arm64 as a last resort. Cost is one extra statFn call per miss when
the GOARCH-preferred package isn't installed.
Surfaced by review on #1718.
* test(agent): add Windows counterpart to writeTestExecutable
writeTestExecutable in exec_fixture_unix_test.go is referenced by
claude_test.go / codex_test.go / kimi_test.go, but the //go:build unix
constraint meant `go test ./pkg/agent` failed to build on Windows.
ETXTBSY is a Linux/Unix fork-exec race; Windows doesn't have that
pathology, so a plain os.WriteFile is sufficient.
Lifted from #1719 (Codex) with attribution. Surfaced by review on #1718.
docker-compose.selfhost.yml documents these as load-bearing in .env.example
but the backend service never received them, so allowlist / signup-gating
configs were silently ignored on self-hosted deployments. Wires the three
vars through with defaults matching .env.example.
* docs(changelog): publish v0.2.17 release notes
Covers commits between v0.2.16 (2026-04-24) and the v0.2.17 cut
(2026-04-26): --custom-env flag for agents, agent CLI stderr tail in
failure messages, configurable update download timeout, plus reliability
fixes around daemon cancellation, server heartbeat, Codex execenv, Pi
skills path, Windows console, CJK markdown URLs, attachment downloads
and autopilot run-only context.
Both en.ts and zh.ts updated.
* docs(changelog): trim small/internal items from v0.2.17 entry
Drops items that read as internal polish or were too narrow to belong in
release notes:
- Skills landing intro polish
- Codex execenv plugin-cache cleanup
- CLI exact-name/ShortID assignee resolution
- Settings invite role label rendering
- Skills SKILL.md fast-path
- CJK markdown URL-boundary fix
- Relative attachment download URLs
Keeps the user-facing wins: --custom-env, stderr-tail in failure
messages, configurable update timeout, cancelled-task classification,
heartbeat probe/claim split, plus the higher-impact fixes.
#1674 wired claude's post-handshake error path through withAgentStderr but
left the writeClaudeInput failure branch returning a bare "broken pipe"
error. That branch fires precisely when claude crashes during startup —
exactly when the stderr tail is most useful for root-causing V8 aborts,
Bun panics, or missing native modules. cmd.Wait() before sampling Tail()
flushes os/exec's internal stderr copy goroutine, matching the
Wait→Tail synchronization contract spelled out in stderr_tail.go.
Adds TestClaudeExecuteSurfacesStderrWhenChildExitsEarly mirroring the
codex test: a fake claude binary drains stdin, writes a V8-abort line to
stderr, and exits 3. Locks in the contract that Result.Error carries the
stderr tail in the post-handshake failure path on the claude backend too.
Merge the two symlink removal branches in exposeSharedCodexPluginCache —
they shared the same os.Remove + recreate path with only the error label
differing. The branch is now keyed off Lstat's ModeSymlink bit, with
Readlink reused only to fast-path an already-correct link. Behaviour is
unchanged; just less duplicated code.
CancelTasksForIssue silently dropped the list of affected tasks, so
whenever an issue transitioned to "cancelled" or "done" while a task was
still active (6 call sites in issue.go), the underlying agent was left
stuck at status="working" indefinitely and required a manual
`multica agent update <id> --status idle` to self-correct. This matches
the symptom reported in #1587: task rows move to "cancelled" via a
non-user-initiated path, agent status never reconciles.
Change CancelAgentTasksByIssue from :exec to :many (also tack on
completed_at = now() for consistency with CancelAgentTasksByIssueAndAgent),
then update CancelTasksForIssue to iterate the returned rows and call
ReconcileAgentStatus + broadcast task:cancelled per affected task —
mirroring the pattern already used by CancelTask and RerunIssue.
No test added; the change is small and mirrors well-covered paths.
Happy to add a mock-backed test in a follow-up if reviewers prefer.
Refs #1587
Refs #1149
Expose the shared Codex plugin cache inside each per-task CODEX_HOME before launch so plugin-provided skills are available on the first session.
Refresh agent-assigned workspace skills for both newly prepared and reused Codex environments, and cover plugin cache plus reuse behavior with focused execenv tests.
Hoist the existing stderrTail ring-buffer (previously codex-only) into
a shared pkg/agent helper so every Backend that supervises a child CLI
can include the last ~2 KB of that CLI's stderr in Result.Error. Wire
the claude backend through the same path.
Motivation: claude on Windows occasionally exits with a non-zero status
after ~5–8 minutes of a single long-running tool_use, and right now the
daemon only reports "claude exited with error: exit status 3" /
"exit status 0x80000003" — useless for root-causing V8 aborts, Bun
panics, native-module OOMs, or any other CLI-side crash. With the tail
attached, the failure message carries the real signal (panic line, V8
assertion, stderr-printed HTTP error) all the way into the task row's
error field that users see in the API.
Renames withCodexStderr to withAgentStderr(msg, label, tail) so the
helper is self-documenting across providers.
* fix(server): validate assignee_id existence on issue create/update
POST /api/issues and PUT /api/issues/:id silently accepted any
well-formed UUID as assignee_id (#1662). The new validateAssigneePair
helper consolidates the existing canAssignAgent check and adds:
- existence lookup against workspace members for assignee_type=member
- existence lookup against workspace agents for assignee_type=agent
- pair consistency: type and id must be both set or both null
- whitelist for assignee_type values (member|agent)
UpdateIssue and BatchUpdateIssues now run the same validator on the
post-merge assignee pair whenever the caller touches either field,
closing the parallel gap on the update path.
* fix(server): reject malformed assignee_id at handler entry
parseUUID silently returns an invalid pgtype.UUID for unparseable input
and validateAssigneePair treats (type unset + id invalid) as "no
assignee". Together they let `POST /api/issues` and `PUT /api/issues/:id`
silently drop a malformed assignee_id and return a successful response.
Reject the parse failure inline at every entry point — Create, Update,
and BatchUpdateIssues — so the validator never sees an unparseable id.
Adds two regression tests covering the create and update paths.
* feat(cli): add --custom-env to agent create/update
Adds a JSON-object flag on `multica agent create` and `multica agent
update` that writes the agent's `custom_env` map via the existing
handler API. Needed so runtime bearer tokens (e.g. SECOND_BRAIN_TOKEN)
can be provisioned from the CLI without falling back to curl or
admin-only UI access.
- `--custom-env '{"KEY":"value"}'` → sets the map.
- `--custom-env '{}'` or `--custom-env ''` → clears the map on update
(server treats a non-nil empty map as "clear all entries").
- Omitted flag → no change.
- Help text flags the value as secret material and never logged.
- Table-driven tests cover the parser (valid, clear, invalid JSON,
wrong shape) plus flag discoverability on both commands.
* feat(cli): add --custom-env-{stdin,file}; sanitize parse errors
Security review of the --custom-env flag (PR #1518) surfaced two issues:
1. Secrets on the command line leak via shell history and /proc/<pid>/cmdline
regardless of CLI logging. Add --custom-env-stdin and --custom-env-file
as mutually-exclusive alternatives, and update the --custom-env help
text to warn about shell history / 'ps' exposure so the "never logged"
claim is no longer misleading.
2. parseCustomEnv wrapped json.Unmarshal errors with %w; SyntaxError /
UnmarshalTypeError can surface fragments of the (secret) input. Return
a fixed, content-free message instead.
Refactor the body-assembly blocks in both agentCreateCmd and
agentUpdateCmd to go through a single resolveCustomEnv helper so the
three input channels behave identically. Tests cover every channel,
mutual exclusion, error sanitization, and help-text wording.
* fix(cli): require explicit '{}' to clear custom_env; sanitize --custom-args errors
Address PR #1518 review feedback from @Bohan-J:
1. parseCustomEnv now errors on empty/whitespace input. The clear signal
is the explicit '{}' object only. The previous behavior silently wiped
the secret map when an upstream pipe was empty (cat missing.json |
... --custom-env-stdin without set -o pipefail) or when --custom-env-file
pointed at an empty file. resolveCustomEnv emits channel-specific error
messages (e.g. "--custom-env-stdin: empty input; pass '{}' to clear").
2. Drop the '&& filePath != ""' guard so an explicit --custom-env-file ""
surfaces an error instead of being silently ignored.
3. Rewrite TestAgentUpdateNoFieldsMentionsCustomEnv into
TestAgentUpdateNoFieldsErrorMentionsAllCustomEnvFlags — the body now
actually runs runAgentUpdate with no flags and asserts the resulting
"no fields" error names all three --custom-env channels.
4. Extract parseCustomArgs helper. Replace the '%w'-wrapped json error
with a content-free message, mirroring parseCustomEnv. Although
custom_args is not a dedicated secret channel, callers regularly stuff
sensitive values like "--api-key=..." into it, so json.Unmarshal must
never echo input fragments. Adds TestParseCustomArgsErrorSanitization.
Also adds resolveCustomEnv subtests for stdin/file empty-input, empty
file contents, empty file path, and explicit '{}' positive cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Implementer (Multica Agent) <implementer@multica-agent.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The invite-member role Select rendered the raw value ("member"/"admin")
in the trigger because Base UI's SelectValue defaults to the value, not
the item text. PR #1672 worked around it with `className="capitalize"`,
but this file already owns a roleConfig map with proper labels and the
codebase has an established render-prop pattern for SelectValue (see
trigger-config.tsx and runtime-local-skill-import-panel.tsx).
Use roleConfig[inviteRole].label inside SelectValue and reuse the same
labels for SelectItem children. Single source of truth for role display
names; future role additions or i18n won't depend on CSS capitalize.
Follow-up to #1686. Locks in two nits flagged during review:
1. agent.Result.Status doc comment now lists "cancelled" alongside the
existing values, so the enum surface matches actual usage.
2. New TestExecuteAndDrain_ContextCancelled_ReportsCancelled exercises
the path added in #1686: when the parent context is cancelled before
the backend produces a Result, executeAndDrain must return
Status="cancelled" (not "timeout"). A regression here would silently
restore the misleading log line we just fixed.
DeleteIssue passed the raw URL parameter through parseUUID(), which
returns a zero UUID for human-readable identifiers like "API-123".
This caused DELETE requests with identifier-style IDs to silently
succeed (204) without actually deleting the issue.
Use issue.ID from the already-resolved issue object instead, consistent
with BatchDeleteIssues and all other operations in the same handler.
Fixes#1661
When the server cancels a task (e.g. assignee changes during execution,
explicit user cancel, or workspace_isolation check fail), the daemon's
cancellation poll fires runCancel() on the run context. The drainCtx
derived from runCtx then signals Done(), but executeAndDrain() was
returning Status: "timeout" regardless of *why* the context ended.
The "agent finished status=timeout" log line is then misleading — it
suggests an actual deadline timeout when really the task was cancelled
by upstream. We spent hours misdiagnosing a healthy handoff as a
broken timeout because of this.
Distinguish context.Canceled from context.DeadlineExceeded in
executeAndDrain, and add a "cancelled" case to runTask so the status
propagates through the existing log path.
No behaviour change for genuine timeouts; no behaviour change for
the cancelled-by-poll discard path in handleTask. Only the daemon
log line and TaskResult.Status get the more accurate label.
PR #1632 updated the Pi project-level skill dir from
.pi/agent/skills/ to .pi/skills/, but missed two references:
- server/internal/daemon/execenv/runtime_config.go:20 — the comment
block here lists project-level paths for every other provider, so
using Pi's global path was inconsistent and misleading.
- docs/docs-rewrite-plan.md:88 — planning doc still listed the old
path in the Skills row.
Follow-up to #1632.
Mitigates #1637 and the related model-discovery failure in MUL-1397 by bounding the /api/daemon/heartbeat hot path with an ack-safe probe/claim split, adding structured slow-log attribution, and closing the ModelListStore running-state gap. See PR description for details.
Closes the functional gap the reporter hit on alchaincyf/huashu-design
(skills.sh/alchaincyf/huashu-design/huashu-design) without expanding
candidatePaths unconditionally, which would let an unrelated root
SKILL.md hijack a different skill URL in a multi-skill repo.
Try SKILL.md at the repo root before falling into the recursive tree
fallback added in #1432. Verify the frontmatter name matches the
requested skill so only genuine single-skill repos take the fast path.
For those repos this also shaves the recursive tree API call.
Also clarifies the candidate-path comment so the root case is
explicit.
* fix(daemon): use CREATE_NEW_CONSOLE to stop grandchild console popups on Windows (#1521)
CREATE_NO_WINDOW strips the console entirely. When the agent CLI then
spawns a console-subsystem grandchild (bash, cmd, netstat, findstr,
timeout) without itself passing CREATE_NO_WINDOW, Windows allocates a
brand-new visible console window per invocation — trading one popup per
agent run for N popups per tool call.
Switch to CREATE_NEW_CONSOLE + HideWindow=true so the agent gets a
hidden console that grandchildren inherit. Stdio pipes still work via
STARTF_USESTDHANDLES; no changes needed at the 17 hideAgentWindow call
sites.
Add a Windows-only regression test asserting CREATE_NEW_CONSOLE is set
and CREATE_NO_WINDOW is not, per the #1474 Windows-test follow-up.
Root-cause diagnosis by @matrenitski (verified against the shipped
multica.exe and the Claude Code CLI it spawns) in issue #1521.
* test(agent): use CREATE_NEW_CONSOLE-compatible flag in preservation test
CREATE_NEW_PROCESS_GROUP is silently ignored by Windows when combined
with CREATE_NEW_CONSOLE, so asserting it 'survives' was only bitwise-true,
not semantically meaningful. Switch the example to
CREATE_UNICODE_ENVIRONMENT (documented compatible) and also assert a
non-flag field (NoInheritHandles) survives to exercise full struct
preservation.
`multica issue assign --to <name>` matched agent/member names with a plain
`strings.Contains` check, so an exact match on `reviewer` became ambiguous
whenever a longer agent like `peer-reviewer` also existed. There was also
no way to disambiguate by ID.
Rework `resolveAssignee` to bucket candidates by priority:
1. Full UUID or 8-char ShortID (matches `truncateID` output) — case-insensitive.
2. Case-insensitive exact name (with surrounding whitespace trimmed).
3. Substring fallback — preserves the existing partial-name UX.
The first non-empty bucket wins. Ambiguity inside a higher-priority bucket
still errors and short-circuits lower-priority matching.
All six call sites (`issue assign/update/create/list`, `issue subscriber`,
`project`) are fixed by this single change.
Fixes#1620
linkify-it only recognizes ASCII characters as URL boundaries. In Chinese
or Japanese text a URL followed by "。" (or any other full-width
punctuation) was greedily swallowed into the URL along with everything up
to the next whitespace, producing hrefs like
`https://.../pull/1623。merge` that 404 when clicked.
Truncate the detected URL at the first CJK full-width punctuation
character and re-scan the tail, so adjacent URLs separated only by
full-width punctuation are still each linked individually. The
terminator character set mirrors the fix applied in mattermost/marked#22.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skills): restore page description, link to docs, polish intro layout
The previous card-layout refactor (#1614) dropped the page-top
description entirely; without it the page jumps straight from the
PageHeader to a brand-colored banner that explains *how sharing works*,
with nothing answering "what IS a skill?". Bring the description back,
add a docs entry point, and tighten the visual hierarchy so the intro
block reads as one coherent unit above the table card.
- Restore a one-line description as the page's primary intro:
"Instructions any agent in this workspace can use." — uses "any agent
... can use" (capability, not factual usage) since skills must be
manually attached to take effect.
- Add an inline "Learn more about Skills →" link mirroring the
onboarding docs-link pattern (muted underline, new tab) — opens
https://multica.ai/docs/skills.
- Visual hierarchy: description is text-base + text-foreground (primary),
link is text-xs + text-muted-foreground (auxiliary). Same line, eye
follows weight order.
- Banner padding bumped from px-3 py-2 to px-4 py-3 so it breathes and
its inner text lands at the same x as the table content.
- Wrap description + banner in a shared `pl-4 space-y-3` so they read as
one intro block, indented to align with the table card's content.
- Loading skeleton updated to mirror the new structure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skills): keep docs link underline subtle, only animate text color on hover
The underline was inheriting text-decoration-color from the link's text,
so when hover bumped the text from muted to foreground the underline
got darker too — making the link feel more prominent on hover than at
rest, the opposite of what we want for a tertiary docs link.
Pin decoration-color to muted-foreground/30 explicitly so it stays
faint regardless of hover state. Only the text color transitions; the
underline stays as a constant low-key marker that the element is a link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The skills page rolled its own HeroHeader instead of the shared PageHeader,
which meant no mobile sidebar trigger and visual drift from other list
pages. The table was also edge-to-edge inside the dashboard container, so
it felt "naked" compared to the rest of the product.
- Replace custom HeroHeader with shared PageHeader (gives mobile hamburger
and h-12 chrome for free); move "New skill" into the PageHeader as the
page-level action.
- Keep search + scope filters in a toolbar, but move that toolbar *inside*
a bordered, rounded card together with the table, so the whole unit
reads as a single scrollable surface with internal padding.
- Use the existing useScrollFade hook on the row list so the top/bottom
edges fade while scrolling.
- Drop `divide-y` in favor of `border-b` per row — divide-y leaves the
last row without a bottom rule, which looks unfinished when only a
couple of skills exist and the scroll area is taller than the content.
- Drop the redundant description paragraph from the old hero; keep the
"Shared with your workspace" banner above the card since it carries
non-obvious UX context.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(autopilot): rename Routines → Autopilots to match product UI
Unify naming between docs and product. Sidebar label, URL route,
CLI command, and onboarding copy all call this feature "Autopilot";
the docs were the only surface that diverged. Aligning the docs to
the product (rather than the reverse) because the 830+ code-side
references would be a much larger rename to propagate.
- Rename routines.mdx / routines.zh.mdx → autopilots.mdx / autopilots.zh.mdx
- Update meta.json / meta.zh.json index entries (routines → autopilots)
- Drop the reconciliation note ("docs say Routines, CLI says autopilot")
that shipped in the original routines.mdx and the cli.mdx section header
- Update cross-references in cli, how-multica-works, tasks,
assigning-issues, chat, mentioning-agents, daemon-runtimes (EN + ZH)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): link to docs from key steps and starter tasks
Users who want to dig deeper now have a next hop from inside the flow
instead of having to dig through the help menu. Placed as secondary
links (muted, underline-offset-4) so they don't pull focus from the
primary CTA on each step.
Placement — one link per surface, placed in secondary regions:
- Welcome: "Learn how Multica works" below the subhead
- Questionnaire: "Learn how agents work" in the Why-we-ask aside
- Runtime aside (shared by desktop + web): "Learn about runtimes"
- Agent step: "Creating your first agent" in the About-agents aside
- StarterContentPrompt dialog: "Learn how Multica works"
Starter tasks (content/starter-content-templates.ts): added a single
"Learn about X" tail link per task, only on first occurrence of each
concept within a branch. 8 links on the agent-guided branch + 8 on
the self-serve branch + 1 on the welcome issue header (17 total).
URL scheme: absolute https://multica.ai/docs/{slug} throughout —
absolute so desktop (Electron) opens them in the system browser, and
the /en prefix is omitted because the docs middleware redirects it
away (English is the default, Chinese is /zh/).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(landing): add docs link to footer and how-it-works section
Docs were previously reachable only from the in-app help menu. Landing
now surfaces them in two places, both locale-aware (/docs for English,
/docs/zh for Chinese):
- Footer Resources group: Documentation link was pointing at the
GitHub repo; replaced with the real docs URL
- How-It-Works section CTA row: added "Read the docs" between the
primary CTA and the GitHub link, same ghost styling
Locale resolution: href is picked per-render based on the landing's
current locale (cookie-driven via useLocale). The docs app itself
does not auto-detect language, so we must pick the right path
explicitly when emitting the link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): clean up Autopilot rename leftovers and link formatting
- comments.mdx: "not routine updates" → "not day-to-day updates"
(adjectival holdover now that the feature is renamed Autopilot;
zeroes out remaining "routine" mentions in user-facing docs)
- starter-content-templates.ts: move the arrow inside the markdown
link — "[text →](url)" instead of "→ [text](url)" — so the arrow
is part of the clickable region. 17 occurrences.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): drop docs link from welcome screen and starter-content dialog
"Learn how Multica works" was showing up too often in the first two
screens users see. Keep the link in the post-import welcome issue
header (where users actually have time to explore); remove it from
the two earlier surfaces where it competes with the primary CTA.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Menu primitives (context/dropdown/menubar/select/command) had rules like
`focus:**:text-accent-foreground` and `*:[svg]:text-destructive` that forced
descendant svg colors on focus, overriding icons that set their own color
(e.g. StatusIcon's `text-warning`). Remove them so icon color comes from
inheritance only: colored icons keep their color on hover, uncolored icons
still inherit the item's focus/destructive color as before.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateSkillDialog used a controlled \`open\` prop while staying mounted,
so closing meant a data-open → data-closed flip on the already-mounted
Popup plus a tail re-render from \`useEffect([open])\` resetting \`method\`.
Visible as a double-blink: first the close animation, then a second
fade when the reset effect fired.
Align with the CreateIssue / CreateProject pattern: parent conditionally
renders the dialog and \`<Dialog open>\` is hard-coded. Close now unmounts
the component and Base UI's Portal owns the single exit animation. The
per-open method reset becomes unnecessary — fresh mount, fresh state.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Restrict /health/realtime metrics exposure (MUL-1342)
The realtime metrics endpoint was registered on the public router with
no authentication, exposing per-event/per-scope counters, redis.last_error,
and redis.node_id to anonymous callers. This enables information disclosure
and traffic profiling.
Move the handler behind a token + loopback policy:
- If REALTIME_METRICS_TOKEN is set, require Authorization: Bearer <token>
using a constant-time compare. Reject other callers with 401 plus a
WWW-Authenticate hint.
- If the env var is unset, only serve loopback callers and return 404 to
remote clients so the endpoint is not enumerable. This keeps local dev
workflows working without configuration.
The handler is extracted into health_realtime.go with focused unit tests
covering the token, loopback, and rejection paths. .env.example documents
the new variable.
Refs: https://github.com/multica-ai/multica/issues/1606
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fail closed for proxied /health/realtime requests (MUL-1342)
Addresses review on PR #1608: when the server runs behind a reverse
proxy (Caddy / Nginx -> localhost:8080), public callers reach the Go
handler with RemoteAddr=127.0.0.1, so the previous loopback shortcut
exposed the metrics surface in self-hosted deployments.
The no-token path now treats any forwarding header
(X-Forwarded-For / -Host / -Proto, X-Real-Ip, Forwarded) as a
'this request was proxied, can't attribute, fail closed' signal and
returns 404. Direct loopback callers without those headers still work
for local dev. Token-gated path is unchanged.
Tests cover all listed proxy headers (incl. multi-hop XFF chain and
RFC 7239 Forwarded) over both 127.0.0.1 and ::1, plus a regression
case ensuring an empty/whitespace forwarding header does not break
direct loopback access. .env.example updated to call out that proxied
deployments must configure REALTIME_METRICS_TOKEN.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: CC-Girl <cc-girl@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(core): add skill detail path and query helpers
- paths.workspace(slug).skillDetail(id) → /:slug/skills/:id
- skillDetailOptions(wsId, skillId) for fetching a single skill
- selectSkillAssignments(agents) folds the cached agent list into
Map<skillId, Agent[]>; returns a stable reference so consumers can
memoize against agent-array identity without re-rendering on unrelated
agent updates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(views): add cross-platform openExternal helper
On Electron, route through window.desktopAPI.openExternal so the
http/https-only guard in the main process kicks in — direct window.open
inside Electron opens a new renderer window instead of handing the URL
to the OS shell. On web, fall back to window.open with noopener+noreferrer.
SSR-safe.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): extract edit-permission hook and origin helper
- use-can-edit-skill: mirrors the server's rule (admin/owner ∨ creator)
so the UI can hide/disable actions instead of waiting for a 403. Takes
wsId explicitly per the repo rule for workspace-aware hooks.
- lib/origin: discriminated view over Skill.config.origin (manual /
runtime_local / clawhub / skills_sh) so consumers don't spread JSONB
parsing across the UI tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(skills): rewrite skills list page and collapse import UI
- SkillsPage rewritten: new hero header, single table layout with
columns (Name / Used by / Source · Added by / Updated), agent avatar
stack per skill, filter tabs aligned with Issues/MyIssues header
(Button variant=outline + Tooltip + bg-accent active state).
- CreateSkillDialog: dedicated dialog for the manual/import entry
points, replaces the inline row-triggered dialog.
- runtime-local import: dialog variant deleted; panel is now the single
entry point, embeddable inside CreateSkillDialog. Panel covered by a
new test.
- Deleted runtime-local-skill-row (no longer needed — row rendering
lives in SkillsPage directly) and the old skills-page.test.tsx
(structure diverged beyond salvaging; will be re-added alongside the
detail-page tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skills): add skill detail page and wire routes on web and desktop
- SkillDetailPage: dedicated view for a single skill (name, description,
origin, assignments, file listing). Uses skillDetailOptions and the
new origin / use-can-edit-skill helpers.
- apps/web: /:workspaceSlug/skills/:id Next.js route.
- apps/desktop: /:slug/skills/:id added to the memory router under
WorkspaceRouteLayout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(skills): bump runtime-local-skill-import-panel timeouts for CI
The test chains a five-step async cascade (runtime list → setSelectedRuntimeId
effect → skills query → auto-select effect → row render). Comfortable on
local (~600ms) but tight against RTL's 1 s default on CI where jsdom +
Vitest import takes ~100s. Bump findByText and the two waitFor calls to
5 s each — no production behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Destructive actions in the autopilot detail page fired immediately on
click. Wrap "Delete autopilot" and per-trigger delete with AlertDialog
confirmation, matching the existing issue-delete pattern.
Also fix a latent bug in trigger deletion where the success toast was
shown synchronously after mutate(), so failures still reported success —
switch to mutateAsync + try/catch.
Hover and popup-open states now share the same bg-accent + border-accent
treatment. Drop the shadow-md hover (invisible in dark mode) and the
multi-property transition in favor of a single transition-colors.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace translucent tinted hover (border-accent/50 + bg-accent/20) with
a single-dimension shadow lift. The previous overlay was visually weak
because --accent is nearly identical to --card, so a 20% tint rendered
as almost no change. Active (popup-open) state now uses solid bg-accent
so hover and active are distinguished by different dimensions —
elevation vs color — instead of competing on the same axis.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop priority and project_id from autopilot. project_id was never exposed
in the UI and priority duplicated the agent's own task queue priority.
Redesign the create/edit modal as a Runbook (left) + Configuration (right)
layout. Rework the Schedule section around a single visual shell so every
picker aligns pixel-for-pixel on the same row:
- TimeInput (new): segmented HH:MM control adapted from openstatusHQ/time-picker,
driven by keyboard (ArrowUp/Down to step, ArrowLeft/Right to jump segment,
digit typing with a 2s two-digit window). Replaces <input type="time">,
whose native UI broke the design system. Supports a minuteOnly variant
for hourly schedules.
- TimezonePicker (new): searchable Popover with a fixed-width left check
slot so rows stay aligned and GMT offsets never collide with the selected
indicator.
- Runbook editor now lives in a bordered card, giving the placeholder an
input surface instead of bare document flow.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issues): add right-click context menu on list rows and board cards
Extract the detail page's ⋯ dropdown (~180 lines of inline JSX) into a
shared `useIssueActions` hook plus two thin wrappers so the same action
set (status / priority / assignee / due date / sub-issue ops / pin / copy
link / delete) can be mounted as both a DropdownMenu and a Base UI
ContextMenu. Right-click on any list row or board card now opens the
full action menu without entering the detail page.
Shell-level modals replace the detail-page-local state for set-parent /
add-child / delete-confirm / backlog-agent-hint, so any trigger (detail
page, list, board) can open them through `useModalStore`. Detail page
detects its own deletion via a query-transition effect, avoiding the
need to smuggle callbacks through the store.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(issues): hover and active styling on list rows and board cards
Mirror the sidebar's same-color/different-intensity pattern for the new
right-click context menu states. Base UI adds `data-popup-open` to the
ContextMenuTrigger when the menu is open; `hover:not-data-[popup-open]`
suppresses hover feedback on the already-active item.
List rows apply the pattern directly to background color (`accent/60`
hover, `accent` active). Board cards additionally modulate the card's
border and a lighter background tint (`accent/20` hover, `accent/40`
active) so the card's own bg/border/shadow identity stays intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(modals): show target issue banner in SetParent/AddChild pickers
When triggered from an issue's action menu, the IssuePickerModal now
displays a banner at the top showing "Setting parent of" / "Adding
sub-issue to" followed by the originating issue's status, identifier,
and title. Previously the operation target was only implied by the
modal's sr-only title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(modals): create-issue gains ⋯ overflow menu with parent issue linkage
Add a dropdown-menu with "Set parent issue..." / "Remove parent" at the
end of the property pill row. The ⋯ button is always the last DOM child
of the row so it stays at the tail even when the row wraps to multiple
lines. Menu state reflects current selection — unset shows a single
"Set parent…" entry, set shows the current identifier plus a separate
Remove option.
When a parent is set (either via the new menu or via `data.parent_issue_id`
from a "Create sub-issue" trigger), a chip appears in the pill row
showing "Sub-issue of {identifier}" with the same click-to-change /
click-×-to-clear semantics. This replaces the old header breadcrumb
disclosure that was neither editable nor visible in the form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(issues): group relationship actions under "More" submenu
Nest Create sub-issue / Set parent issue / Add sub-issue inside a
`More >` submenu in the issue actions menu (both Dropdown and
Context variants). Top-level keeps Status/Priority/Assignee/Due date
category submenus plus Pin and Copy link; the relationship ops are
lower-frequency and will grow with future relation types (blocks,
duplicates, related) that fit the same category.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(modals): create-issue adds Add sub-issue with deferred linking
The create modal's ⋯ menu gains an "Add sub-issue..." entry that queues
existing issues as children of the new one. Picked issues appear as
chips in the pill row (downward arrow, distinct from the upward parent
chip), each individually removable.
Linking is deferred because the new issue's ID doesn't exist at pick
time. Once createIssueMutation resolves, we run updateIssueMutation
for every queued child in parallel and surface any partial failures
via toast — the new issue itself is already committed and never rolls
back. Parent and child pickers exclude each other so a single issue
can't occupy both relations simultaneously.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(issues): add MoreHorizontal icon to "More" submenu trigger
The "More" label was visually misaligned because every other top-level
entry has a leading icon. Use MoreHorizontal (same icon as the outer ⋯
trigger — semantically "more options, nested") and drop the `inset`
padding hack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(modals): drop target-issue banner from IssuePickerModal
The banner sat directly above the search input and rendered the target
issue with bolder styling than the "Setting parent of" / "Adding sub-issue
to" caption, which made it read like a pre-selected search result rather
than a context label. Users opening the modal from a menu item already
carry the context, so the extra chrome was redundant.
Remove the contextIssue / contextLabel API from IssuePickerModal and
drop the now-unused issueDetailOptions query in SetParentIssueModal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(modals): exclude current parent from create-issue parent picker
Re-opening the parent picker to change the already-set parent used to
show that parent in the results — picking it was a silent no-op. Mirror
the child picker's exclude-list construction so the current parent is
always filtered out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:48:46 +08:00
837 changed files with 75801 additions and 12204 deletions
- Writing or editing translations (`packages/views/locales/`)
- Naming a new route, package, file, DB column, or TS type
- Writing Chinese product copy (UI strings, error messages, docs)
The legacy `packages/views/locales/glossary.md` is now a stub redirecting to the docs page; do not rely on it.
## Project Context
Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens.
@@ -136,6 +151,17 @@ make start-worktree # Start using .env.worktree
- Avoid broad refactors unless required by the task.
- New global (pre-workspace) routes MUST use a single word (`/login`, `/inbox`) or a `/{noun}/{verb}` pair (`/workspaces/new`). NEVER add hyphenated word-group root routes (`/new-workspace`, `/create-team`) — they collide with common user workspace names and force endless reserved-slug audits. Reserving the noun (`workspaces`) automatically protects the entire `/workspaces/*` subtree.
### Backend Handler UUID Parsing Convention
Every Go handler in `server/internal/handler/` follows these rules. The convention exists because `util.ParseUUID` used to silently return a zero UUID on invalid input, which caused #1661 — a `DELETE` returning 204 success while the SQL `DELETE` matched zero rows.
- **Resource path params that accept either a UUID or a human-readable identifier** (e.g. `chi.URLParam(r, "id")` for an issue, which accepts both `MUL-123` and a UUID) MUST be resolved through the dedicated loader (`loadIssueForUser` / `loadSkillForUser` / `loadAgentForUser` / `requireDaemonRuntimeAccess`). After resolution, all subsequent DB calls — especially `Queries.Delete*` / `Queries.Update*` — MUST use `entity.ID` from the resolved object. Never round-trip the raw URL string through `parseUUID` for a write query.
- **Pure-UUID inputs from request boundaries** (URL params that are always UUIDs, request body fields, query params, headers) MUST be validated with `parseUUIDOrBadRequest(w, s, fieldName)`. On invalid input it writes a 400 and returns `ok=false` — return immediately.
- **Trusted UUID round-trips** (sqlc-returned UUIDs being passed back into queries, test fixtures) use `parseUUID(s)` which calls `util.MustParseUUID` and panics on invalid input. A panic here means an unguarded user-input string slipped in — that is a real bug. `chi`'s `middleware.Recoverer` translates the panic into a 500 so the process keeps running.
- **`util.ParseUUID(s) (pgtype.UUID, error)`** is the only safe variant outside the handler package. Always check the error.
When adding a `Queries.Delete*` or `Queries.Update*` call, ask: "Where did this UUID come from?" If the answer is "raw user input that hasn't been validated," route it through `parseUUIDOrBadRequest` or a loader first.
### Package Boundary Rules
These are hard constraints. Violating them breaks the cross-platform architecture:
@@ -70,10 +70,10 @@ Opens your browser for OAuth authentication, creates a 90-day personal access to
### Token Login
```bash
multica login --token
multica login --token <mul_...>
```
Authenticate by pasting a personal access token directly. Useful for headless environments.
Authenticate using a personal access token directly. Useful for headless environments. Pass `--token=` with an empty value to be prompted interactively (so the token never lands in shell history).
### Check Status
@@ -140,12 +140,15 @@ The daemon auto-detects these AI CLIs on your PATH:
The daemon periodically scans `MULTICA_WORKSPACES_ROOT` and reclaims disk space in three modes:
- **Full task cleanup** — when an issue's status is `done` or `cancelled` and has been idle for `MULTICA_GC_TTL`, the entire task directory is removed.
- **Orphan cleanup** — task directories with no `.gc_meta.json` (e.g. left over from a daemon crash) are removed once they exceed `MULTICA_GC_ORPHAN_TTL`.
- **Artifact-only cleanup** — when a task has been completed for at least `MULTICA_GC_ARTIFACT_TTL` but the issue is still open, regenerable build outputs whose directory basename matches `MULTICA_GC_ARTIFACT_PATTERNS` are removed; the rest of the workdir (source, `.git`, `output/`, `logs/`, `.gc_meta.json`) is preserved so the agent can resume the same workdir on the next task.
Patterns are basename-only — entries containing `/` or `\` are silently dropped — and `.git` subtrees are never descended into. The default list (`node_modules`, `.next`, `.turbo`) is intentionally narrow; extend it per deployment if your repos consistently produce other regenerable directories (for example, `MULTICA_GC_ARTIFACT_PATTERNS=node_modules,.next,.turbo,target,__pycache__`). To disable artifact cleanup entirely, set `MULTICA_GC_ARTIFACT_TTL=0`.
Agent-specific overrides:
@@ -178,8 +198,12 @@ Agent-specific overrides:
|----------|-------------|
| `MULTICA_CLAUDE_PATH` | Custom path to the `claude` binary |
| `MULTICA_CLAUDE_MODEL` | Override the Claude model used |
| `MULTICA_CLAUDE_ARGS` | Default extra arguments for Claude Code runs |
| `MULTICA_CODEX_PATH` | Custom path to the `codex` binary |
| `MULTICA_CODEX_MODEL` | Override the Codex model used |
| `MULTICA_CODEX_ARGS` | Default extra arguments for Codex runs |
| `MULTICA_COPILOT_PATH` | Custom path to the `copilot` binary |
| `MULTICA_COPILOT_MODEL` | Override the Copilot model used (note: GitHub Copilot routes models through your account entitlement, so this may not be honoured) |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
@@ -192,6 +216,12 @@ Agent-specific overrides:
| `MULTICA_PI_MODEL` | Override the Pi model used |
| `MULTICA_CURSOR_PATH` | Custom path to the `cursor-agent` binary |
| `MULTICA_CURSOR_MODEL` | Override the Cursor Agent model used |
| `MULTICA_KIMI_PATH` | Custom path to the `kimi` binary |
| `MULTICA_KIMI_MODEL` | Override the Kimi model used |
| `MULTICA_KIRO_PATH` | Custom path to the `kiro-cli` binary |
| `MULTICA_KIRO_MODEL` | Override the Kiro model used |
`MULTICA_CLAUDE_ARGS` and `MULTICA_CODEX_ARGS` are parsed with POSIX shellword quoting, so values such as `--model "gpt-5.1 codex" --sandbox read-only` are split like a shell command line. Agent arguments are applied in this order: hardcoded Multica defaults, daemon-wide env defaults, then per-agent `custom_args` from the task.
### Self-Hosted Server
@@ -275,10 +305,11 @@ multica workspace members <workspace-id>
multica issue list
multica issue list --status in_progress
multica issue list --priority urgent --assignee "Agent Name"
multica issue list --assignee-id 5fb87ac7-23b5-4a7a-81fa-ed295a54545d
multica issue list --limit 20 --output json
```
Available filters: `--status`, `--priority`, `--assignee`, `--project`, `--limit`.
Available filters: `--status`, `--priority`, `--assignee` / `--assignee-id`, `--project`, `--limit`. Use `--assignee-id <uuid>` for unambiguous filtering when names overlap.
### Get Issue
@@ -291,9 +322,10 @@ multica issue get <id> --output json
Flags: `--title` (required), `--description`, `--status`, `--priority`, `--assignee` / `--assignee-id`, `--parent`, `--project`, `--due-date`. Pass `--assignee-id <uuid>` (mutually exclusive with `--assignee`) when scripting against the IDs returned by `multica workspace members --output json` / `multica agent list --output json`.
Expected output should show the authenticated user and server URL.
**If login fails:**
- If no browser is available (headless environment), the user can generate a Personal Access Token at `https://app.multica.ai/settings` and run: `multica login --token`
- If no browser is available (headless environment), the user can generate a Personal Access Token at `https://app.multica.ai/settings` and run: `multica login --token <mul_...>` (use `--token=` with an empty value to be prompted interactively).
- If the server URL needs to be customized: `multica config set server_url <url>` before logging in.
---
@@ -166,12 +166,12 @@ Wait 3 seconds, then verify:
multica daemon status
```
Expected output should show `running` status with detected agents (e.g. `claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, `cursor-agent`).
Expected output should show `running` status with detected agents (e.g. `claude`, `codex`,`copilot`,`opencode`, `openclaw`, `hermes`, `gemini`, `pi`, `cursor-agent`).
**If daemon fails to start:**
- Check logs: `multica daemon logs`
- If a port conflict occurs, the daemon may already be running under a different profile.
- If no agents are detected, ensure at least one AI CLI (`claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`) is installed and on the `$PATH`.
- If no agents are detected, ensure at least one AI CLI (`claude`, `codex`,`copilot`,`opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`) is installed and on the `$PATH`.
---
@@ -185,12 +185,12 @@ multica daemon status
Confirm:
1. Status is `running`
2. At least one agent is listed (e.g. `claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`)
2. At least one agent is listed (e.g. `claude`, `codex`,`copilot`,`opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`)
3. At least one workspace is being watched
If the agents list is empty, tell the user:
> "The Multica daemon is running but no AI agent CLIs were detected. Please install at least one supported CLI (`claude`, `codex`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`), then restart the daemon with `multica daemon stop && multica daemon start`."
> "The Multica daemon is running but no AI agent CLIs were detected. Please install at least one supported CLI (`claude`, `codex`, `copilot`, `opencode`, `openclaw`, `hermes`, `gemini`, `pi`, or `cursor-agent`), then restart the daemon with `multica daemon stop && multica daemon start`."
@@ -30,12 +30,24 @@ Turn coding agents into real teammates — assign tasks, track progress, compoun
Multica turns coding agents into real teammates. Assign issues to an agent like you'd assign to a colleague — they'll pick up the work, write code, report blockers, and update statuses autonomously.
No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**, **OpenClaw**, **OpenCode**, **Hermes**, **Gemini**, **Pi**, and**Cursor Agent**.
No more copy-pasting prompts. No more babysitting runs. Your agents show up on the board, participate in conversations, and compound reusable skills over time. Think of it as open-source infrastructure for managed agents — vendor-neutral, self-hosted, and designed for human + AI teams. Works with **Claude Code**, **Codex**,**GitHub Copilot CLI**,**OpenClaw**, **OpenCode**, **Hermes**, **Gemini**, **Pi**, **Cursor Agent**, **Kimi**, and **Kiro CLI**.
Multica — **Mul**tiplexed **I**nformation and **C**omputing **A**gent.
The name is a nod to Multics, the pioneering operating system of the 1960s that introduced time-sharing — letting multiple users share a single machine as if each had it to themselves. Unix was born as a deliberate simplification of Multics: one user, one task, one elegant philosophy.
We think the same inflection is happening again. For decades, software teams have been single-threaded — one engineer, one task, one context switch at a time. AI agents change that equation. Multica brings time-sharing back, but for an era where the "users" multiplexing the system are both humans and autonomous agents.
In Multica, agents are first-class teammates. They get assigned issues, report progress, raise blockers, and ship code — just like their human colleagues. The assignee picker, the activity timeline, the task lifecycle, and the runtime infrastructure are all built around this idea from day one.
Like Multics before it, the bet is on multiplexing: a small team shouldn't feel small. With the right system, two engineers and a fleet of agents can move like twenty.
## Features
Multica manages the full agent lifecycle: from task assignment to execution monitoring to skill reuse.
@@ -98,7 +110,7 @@ multica setup # Connect to Multica Cloud, log in, start daemon
multica setup # Configure, authenticate, and start the daemon
```
The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`, `openclaw`, `opencode`, `hermes`, `gemini`, `pi`, `cursor-agent`) on your PATH.
The daemon runs in the background and auto-detects agent CLIs (`claude`, `codex`,`copilot`,`openclaw`, `opencode`, `hermes`, `gemini`, `pi`, `cursor-agent`, `kimi`, `kiro-cli`) on your PATH.
### 2. Verify your runtime
@@ -108,7 +120,7 @@ Open your workspace in the Multica web app. Navigate to **Settings → Runtimes*
### 3. Create an agent
Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, OpenClaw, OpenCode, Hermes, Gemini, Pi, or Cursor Agent). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
Go to **Settings → Agents** and click **New Agent**. Pick the runtime you just connected and choose a provider (Claude Code, Codex, GitHub Copilot CLI, OpenClaw, OpenCode, Hermes, Gemini, Pi, Cursor Agent, Kimi, or Kiro CLI). Give your agent a name — this is how it will appear on the board, in comments, and in assignments.
### 4. Assign your first task
@@ -160,9 +172,9 @@ See the [CLI and Daemon Guide](CLI_AND_DAEMON.md) for the full command reference
This installs the `multica` CLI, checks out the latest self-host assets, pulls the official Multica images from GHCR, and configures everything for localhost.
Open http://localhost:3000. To log in, configure `RESEND_API_KEY` in `.env` for email-based codes (recommended), or set `APP_ENV=development` in `.env` to enable the dev master code **`888888`**. See [Step 2 — Log In](#step-2--log-in) for details.
Open http://localhost:3000. To log in, configure `RESEND_API_KEY` in `.env` for email-based codes (recommended), or leave Resend unset and copy the generated code from the backend logs. See [Step 2 — Log In](#step-2--log-in) for details.
> **Prerequisites:** Docker and Docker Compose must be installed. The script checks for this and provides install links if missing.
>
@@ -67,15 +67,15 @@ Once ready:
### Step 2 — Log In
Open http://localhost:3000 in your browser. The Docker self-host stack defaults to `APP_ENV=production` (set in `docker-compose.selfhost.yml`), so the dev master code is **disabled by default** for safety on public deployments. Pick one of the following to log in:
Open http://localhost:3000 in your browser. The Docker self-host stack defaults to `APP_ENV=production` (set in `docker-compose.selfhost.yml`), and there is no fixed verification code by default. Pick one of the following to log in:
- **Recommended (production):** configure `RESEND_API_KEY` in `.env`, then restart the backend. Real verification codes will be sent to the email address you enter. See [Advanced Configuration → Email](SELF_HOSTING_ADVANCED.md#email-required-for-authentication).
- **Evaluation / private network:** set `APP_ENV=development` in `.env` and restart the backend. Verification code **`888888`** will then work for any email address.
- **Without configuring either:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
- **Without email configured:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
- **Deterministic local/private testing:** set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env`, then restart the backend. This fixed code is ignored when `APP_ENV=production`.
Changes to `ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
> **Warning:** do **not** set `APP_ENV=development` on a publicly reachable instance — anyone who knows an email address can then log in with `888888`.
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
> **Note:** The dev master verification code `888888` is gated by `APP_ENV != "production"`. The Docker self-host stack defaults to `APP_ENV=production` (so `888888` is disabled), which protects publicly reachable instances. For local development without email configured, set `APP_ENV=development` in your `.env` to enable `888888` — never do this on a public instance.
> **Note:** If Resend is not configured, generated verification codes are printed to backend logs. A fixed local testing code is disabled by default; to opt in on a private test instance, set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value. It is ignored when `APP_ENV=production`.
### Google OAuth (Optional)
@@ -56,13 +56,15 @@ Changes take effect after restarting the backend / compose stack. The web UI rea
### File Storage (Optional)
For file uploads and attachments, configure S3 and CloudFront:
For file uploads and attachments, configure S3 and (optionally) CloudFront:
| Variable | Description |
|----------|-------------|
| `S3_BUCKET` | S3 bucket name |
| `S3_REGION` | AWS region (default: `us-west-2`) |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
| `S3_BUCKET` | Bucket name only (e.g. `my-bucket`). Do **not** include the `.s3.<region>.amazonaws.com` suffix — the server constructs the public URL from `S3_BUCKET` + `S3_REGION` |
| `S3_REGION` | AWS region (default: `us-west-2`). Must match the bucket's actual region — used for both SDK signing and public URLs |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Static credentials. When both are unset, the AWS SDK default credential chain is used |
| `AWS_ENDPOINT_URL` | Custom S3-compatible endpoint (e.g. MinIO, R2, B2). Setting this switches the public URL to path-style |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain — when set, public URLs use this host instead of the S3 host |
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
| `MULTICA_CLAUDE_MODEL` | Override the Claude model used |
| `MULTICA_CODEX_PATH` | Custom path to the `codex` binary |
| `MULTICA_CODEX_MODEL` | Override the Codex model used |
| `MULTICA_COPILOT_PATH` | Custom path to the `copilot` (GitHub Copilot CLI) binary |
| `MULTICA_COPILOT_MODEL` | Override the Copilot model used (note: GitHub Copilot routes models through your account entitlement, so this may not be honoured) |
| `MULTICA_OPENCODE_PATH` | Custom path to the `opencode` binary |
| `MULTICA_OPENCODE_MODEL` | Override the OpenCode model used |
| `MULTICA_OPENCLAW_PATH` | Custom path to the `openclaw` binary |
@@ -290,14 +295,45 @@ HTTP requests (issues, comments, uploads) work on LAN out of the box — Next.js
@@ -21,7 +21,7 @@ The form has only two required fields: **name** (unique within the workspace) an
## Pick an AI coding tool
Each runtime is backed by a specific AI coding tool. Multica supports 10 of them. The most common choices:
Each runtime is backed by a specific AI coding tool. Multica supports 11 of them. The most common choices:
| Tool | Good for |
|---|---|
@@ -31,7 +31,7 @@ Each runtime is backed by a specific AI coding tool. Multica supports 10 of them
| **Copilot** | Teams leveraging their GitHub account entitlements |
| **Gemini** | Users in the Google ecosystem |
The other five (Hermes, Kimi, OpenCode, Pi, OpenClaw), along with each tool's full capability matrix (session resume, MCP, skill injection path, model selection), are covered in [AI coding tools comparison](/providers).
The other six (Hermes, Kimi, Kiro CLI, OpenCode, Pi, OpenClaw), along with each tool's full capability matrix (session resume, MCP, skill injection path, model selection), are covered in [AI coding tools comparison](/providers).
## Writing system instructions
@@ -123,5 +123,5 @@ Archived agents can't be assigned new tasks.
## Next steps
- [Skills](/skills) — attach knowledge packs to an agent
- [AI coding tools comparison](/providers) — full capability matrix across all 10 tools
- [AI coding tools comparison](/providers) — full capability matrix across all 11 tools
- [Assigning issues to agents](/assigning-issues) — put your new agent to work
@@ -12,7 +12,7 @@ Assign an [issue](/issues) to an [agent](/agents) and it works as the **official
| **Assign** | Hand an agent ownership | Changes assignee | Issue + all comments | Inherits from issue | ✓ |
| [**@-mention**](/mentioning-agents) | Pull it in to take a look | No changes | Issue + trigger comment | Inherits from issue | ✓ |
| [**Chat**](/chat) | One-to-one conversation outside any issue | No issue involved | Current conversation history | Fixed medium | ✓ |
| [**Routines**](/routines) | Scheduled or manual automation | Depends on mode | Depends on mode | Set by routine | ✗ |
| [**Autopilots**](/autopilots) | Scheduled or manual automation | Depends on mode | Depends on mode | Set by autopilot | ✗ |
"Auto retry" refers to retries after infrastructure failures (runtime offline, timeout). Business errors on the agent side (for example, the model reporting an error) are not retried. See [**Tasks**](/tasks) for details.
`--to` takes a member username or an agent name. Giving agents memorable names makes this step smoother — if multiple agents share a name in the workspace, the first one listed wins, so rename before assigning.
`--to` takes a member username or an agent name (fuzzy match). When names overlap — e.g. an agent `J` alongside `Cursor - J` — pass `--to-id <uuid>` instead, using the `user_id` (member) or `id` (agent) from `multica workspace members --output json` / `multica agent list --output json`. UUID matching is strict and unambiguous, which is what you want from scripts and from agents driving the CLI. `--to` and `--to-id` are mutually exclusive.
Unassign:
@@ -78,4 +79,4 @@ But **different agents can work on the same issue in parallel** — for example,
- [**@-mention an agent in a comment**](/mentioning-agents) — a lighter trigger that leaves assignee and status untouched
- [**Chat**](/chat) — one-to-one conversation outside any issue
- [**Routines**](/routines) — let agents start work automatically on a schedule
- [**Autopilots**](/autopilots) — let agents start work automatically on a schedule
description: Configure email + verification code sign-in, Google OAuth, and signup allowlists. Avoid the 888888 trap.
description: Configure email + verification code sign-in, Google OAuth, signup allowlists, and local test codes.
---
import { Callout } from "fumadocs-ui/components/callout";
@@ -27,17 +27,24 @@ The user enters an email on the sign-in page → the server sends a 6-digit code
**What happens if you don't set `RESEND_API_KEY`**: the server doesn't error, but **every email that should have been sent is written to the server's stdout only**. Handy for local development (copy the code from the logs); in production it's a black hole.
## The 888888 trap
## Fixed local testing codes
<Callout type="warning">
**If `APP_ENV` is not set to `production`, anyone can sign in to any account with the code `888888`.**
**Do not enable a fixed verification code on a publicly reachable instance.**
Multica has a development-only master code, `888888` — a backdoor so local development doesn't depend on Resend. The rule is straightforward: when `APP_ENV != "production"`, **any email** plus `888888` passes verification.
The old behavior where non-production instances accepted `888888` by default has been removed. Unless you explicitly configure it, typing `888888` is treated like any other wrong code.
**Production deployments must set `APP_ENV=production`**. If you deploy via `make selfhost` / `docker-compose.selfhost.yml`, this value is already set to `production` by default; but if you deploy from source yourself, write your own Docker config, or redefine environment variables in Kubernetes — you must add `APP_ENV=production` yourself.
Local development without Resend should use the generated code printed in server logs. If you need deterministic local/private automation, set `MULTICA_DEV_VERIFICATION_CODE` to a 6-digit value such as `888888`, and keep `APP_ENV` non-production:
```bash
APP_ENV=development
MULTICA_DEV_VERIFICATION_CODE=888888
```
This shortcut is ignored when `APP_ENV=production`.
</Callout>
To check whether your deployment has this trap: open the sign-in page, enter **any email** to request a code, then enter `888888`. If you get in, your `APP_ENV` is not set to `production`, and **the entire instance is wide open**.
Production deployments should leave `MULTICA_DEV_VERIFICATION_CODE` empty and set `APP_ENV=production`. If you deploy via `make selfhost` / `docker-compose.selfhost.yml`, `APP_ENV` defaults to `production`.
@@ -38,7 +38,7 @@ In day-to-day use you'll only touch the first two directly. The **[daemon](/daem
2. Enter the code; the server issues a JWT cookie (browser) or exchanges it for a PAT (CLI).
<Callout type="warning">
**Self-hosting operators, take note**: if `APP_ENV` is not set to `production`, the verification code is always `888888` — anyone can sign in as anyone. See [Self-host auth configuration](/auth-setup).
**Self-hosting operators, take note**: keep `MULTICA_DEV_VERIFICATION_CODE` empty on public deployments. If you enable a fixed local test code, anyone who can request a code can sign in with that value while `APP_ENV` is non-production. See [Self-host auth configuration](/auth-setup).
description: Let agents start work on a cron schedule — or trigger once manually via the UI or CLI.
---
import { Callout } from "fumadocs-ui/components/callout";
Autopilots let [agents](/agents) **start work automatically on a schedule** — configure a cron expression and a timezone, and Multica dispatches a [`task`](/tasks) on its own, without you triggering anything. It fits periodic checks, recurring reports, and overnight cleanup jobs — the "standing order" shape of work. Compared to the other three trigger paths ([assigning](/assigning-issues), [@-mention](/mentioning-agents), and [chat](/chat), where you are the one kicking things off), the core difference with Autopilots is that they are **time-driven**.
## Configure an autopilot
Create a new autopilot on the workspace's **Autopilot** page. You set:
- **Name** — display name
- **Agent** — who the run is dispatched to
- **Priority** — inherited by the `task` it produces (same semantics as issue priority)
- **Description / prompt** — the work description the agent receives each run
- **Execution mode** — see below
- **Triggers** — at least one `schedule` (cron + timezone)
## Pick an execution mode
An autopilot has two execution modes. **Start with "create issue" mode.**
- **Create issue mode** (`create_issue`) — default, **recommended**. Each trigger first creates an issue in the workspace (the title supports interpolation like `{{date}}`), then assigns the issue to the agent through the normal assignment flow. All work lands on the issue board with the same history, comments, and status as a manually assigned issue.
- **Run-only mode** (`run_only`) — skips issue creation and enqueues a `task` directly. The run is invisible on the board — you can only see it in the autopilot's run history.
<Callout type="warning">
**Run-only mode is currently unstable.** The CLI labels it "not yet supported end-to-end," and the dispatch path has known issues. New users should stick to create issue mode and wait for run-only mode to ship a stable release before switching.
</Callout>
## Run it on a schedule
Every autopilot needs at least one `schedule` trigger. Cron uses the **standard 5-field format** (minute hour day month weekday), with **1-minute** minimum granularity (no seconds). Timezone is IANA-formatted (for example, `Asia/Shanghai`) and determines which timezone the cron expression is interpreted in.
A few examples:
- `0 9 * * 1-5`, `Asia/Shanghai` — 9 AM Beijing time on weekdays
- `*/30 * * * *`, `UTC` — every 30 minutes
- `0 3 * * *`, `UTC` — every day at 3 AM UTC
The Multica server scans for due triggers every **30 seconds** — **the actual fire time can lag by up to 30 seconds**, not down to the second. If the server is restarted across a fire time, it catches up missed triggers on startup (nothing is lost, but they fire right away).
## Trigger once manually
To avoid waiting for cron while debugging an autopilot, trigger it manually:
- UI: click "Run now" on the autopilot detail page
- CLI:
```bash
multica autopilot trigger <autopilot-id>
```
A manual trigger goes through the exact same execution flow as a `schedule` trigger — only the `source` field on the run record is marked `manual`.
## View run history
Every trigger produces a **run record**, visible on the "History" tab of the autopilot detail page:
- Trigger source (`schedule` / `manual`)
- Start time, completion time
- Status (`issue_created` / `running` / `completed` / `failed`)
- The linked issue (create issue mode) or `task` (run-only mode)
- Failure reason (if failed)
## What happens when an autopilot fails
<Callout type="warning">
**Autopilot failures are not auto-retried and do not send inbox notifications.** A failure leaves a `failed` entry in run history — no system-level re-enqueue like assign or @-mention, and no notification to anyone. If the autopilot is periodic, **the next cron fire will trigger a new run**, but the failed work is not automatically re-run.
If an autopilot is important, design your own monitoring — for example, have the agent post a comment on success, and catch failures by noticing missing comments.
</Callout>
Why no auto-retry: autopilots are already periodic, so adding system-level retries stacks on top of the next scheduled run and creates duplicate executions. Leaving the schedule entirely to cron keeps it clean.
## What's not yet available
**Webhook and API triggers are not available yet.** The autopilot trigger schema reserves `webhook` and `api` types, but **they are not wired up to any ingress route** — the UI can create triggers of either type, but they will not actually fire. Today, **only `schedule` and manual triggers are end-to-end usable.**
## Next
- [**Assign issues to agents**](/assigning-issues) — a one-shot hand-off of an issue to an agent
- [**@-mention agents in comments**](/mentioning-agents) — pull an agent in to take a look from a comment
- [**Chat**](/chat) — one-to-one conversation outside any issue
In the docs this feature is called **Routines**, but the CLI subcommand name is still `autopilot` — a future release will unify the two. If you're searching for "routines" and can't find it, try `multica autopilot --help`.
## Autopilots
| Command | Purpose |
|---|---|
| `multica autopilot list` | List every routine in the workspace |
| `multica autopilot get <id>` | Show a single routine |
| `multica autopilot create ...` | Create a routine |
| `multica autopilot list` | List every autopilot in the workspace |
| `multica autopilot get <id>` | Show a single autopilot |
| `multica autopilot create ...` | Create an autopilot |
| `multica autopilot update <id> ...` | Update |
| `multica autopilot delete <id>` | Delete |
| `multica autopilot runs <id>` | Show run history |
@@ -18,10 +18,10 @@ Opens your browser for OAuth authentication, creates a 90-day personal access to
### Token Login
```bash
multica login --token
multica login --token <mul_...>
```
Authenticate by pasting a personal access token directly. Useful for headless environments.
Authenticate using a personal access token directly. Useful for headless environments. Pass `--token=` with an empty value to be prompted interactively (so the token never lands in shell history).
### Check Status
@@ -92,6 +92,10 @@ The daemon auto-detects these AI CLIs on your PATH:
@@ -7,7 +7,7 @@ import { Callout } from "fumadocs-ui/components/callout";
This page walks you end-to-end through Multica Cloud — **sign up → install the [CLI](/cli) → start the [daemon](/daemon-runtimes) → create an [agent](/agents) → assign your first [task](/tasks)**. Takes about 5 minutes.
One prerequisite: you already have at least one [AI coding tool](/providers) installed locally ([Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), or [Pi](/providers#pi)). The daemon auto-detects them on startup and refuses to start if none are present.
One prerequisite: you already have at least one [AI coding tool](/providers) installed locally ([Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), or [Pi](/providers#pi)). The daemon auto-detects them on startup and refuses to start if none are present.
## 1. Create an account
@@ -99,7 +99,7 @@ Assign the issue to the agent you just created — click its avatar in the web U
multica issue assign MUL-1 --to my-agent-name
```
`--to` takes the **name** of an agent or member. A substring match works — if the agent is called `my-code-reviewer`, `reviewer` resolves to it.
`--to` takes the **name** of an agent or member. A substring match works — if the agent is called `my-code-reviewer`, `reviewer` resolves to it. If your workspace has overlapping names, pass `--to-id <uuid>` instead (mutually exclusive with `--to`); look up the UUID via `multica agent list --output json` or `multica workspace members --output json`.
**What happens next from the daemon**:
@@ -114,6 +114,6 @@ The web UI updates in **real time** (via WebSocket) — no refresh needed.
- [Daemon and runtimes](/daemon-runtimes) — how the daemon operates and what runtimes mean
- [Tasks](/tasks) — task lifecycle and retry rules
- [AI coding tools compared](/providers) — capability differences across the 10 tools
- [AI coding tools compared](/providers) — capability differences across the 11 tools
- [Desktop app](/desktop-app) — if you'd rather not run the daemon yourself
- [Self-host quickstart](/self-host-quickstart) — run your own backend
@@ -34,7 +34,7 @@ Mentioning the same person multiple times in one comment still produces **only o
`@all` is a special target: it pushes a notification to every member of the workspace. Both people and agents can use `@all` — which means an agent reporting progress could also `@all`, so remind agents in their instructions to use it sparingly.
<Callout type="warning">
**Use `@all` carefully.** In a larger workspace, a single `@all` generates that many inbox notifications instantly. Reserve it for things everyone genuinely needs to know — not routine updates.
**Use `@all` carefully.** In a larger workspace, a single `@all` generates that many inbox notifications instantly. Reserve it for things everyone genuinely needs to know — not day-to-day updates.
2. Detects AI coding tools installed on your `PATH` (10 built-in: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
2. Detects AI coding tools installed on your `PATH` (11 built-in: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi))
3. Registers itself with the server, along with a runtime for each detected tool
4. Keeps **polling every 3 seconds** for tasks to pick up, and **sends a heartbeat every 15 seconds**
@@ -75,7 +75,7 @@ Multica uses heartbeats to decide whether a runtime is online. Three key numbers
Missing is not permanent — as soon as the daemon sends another heartbeat it returns to online, and the runtime record is preserved. Restarting the daemon does not lose runtimes.
<Callout type="warning">
**Tasks running on a missing runtime are marked as failed** (failure reason `runtime_offline`). For retryable sources (issues, chat), Multica automatically requeues them; Routines-triggered tasks are not retried automatically. See [Tasks → Which failures retry automatically](/tasks#which-failures-retry-automatically-which-dont).
**Tasks running on a missing runtime are marked as failed** (failure reason `runtime_offline`). For retryable sources (issues, chat), Multica automatically requeues them; Autopilot-triggered tasks are not retried automatically. See [Tasks → Which failures retry automatically](/tasks#which-failures-retry-automatically-which-dont).
</Callout>
## How many tasks can run in parallel
@@ -108,4 +108,4 @@ More scenarios in [Troubleshooting](/troubleshooting).
## Next
- [Tasks](/tasks) — the full lifecycle of a task once the daemon picks it up
- [Providers Matrix](/providers) — capability differences across the 10 AI coding tools
- [Providers Matrix](/providers) — capability differences across the 11 AI coding tools
@@ -5,7 +5,7 @@ description: What Multica Desktop is, how it differs from the web app, and when
import { Callout } from "fumadocs-ui/components/callout";
Multica Desktop is a native desktop app for macOS, Windows, and Linux. It talks to the same backend as the web app and shows the same data, but it adds a few things the browser can't: **independent tab groups per [workspace](/workspaces)**, **automatic [daemon](/daemon-runtimes) startup**, and **one-click upgrades**.
Multica Desktop is a native desktop app for macOS, Windows, and Linux. For the environment it is configured for, it talks to the same backend as the web app and shows the same data. By default Desktop uses Multica Cloud; self-hosted instances can be configured with a local runtime config file. Desktop also adds a few things the browser can't: **independent tab groups per [workspace](/workspaces)**, **automatic [daemon](/daemon-runtimes) startup**, and **one-click upgrades**.
## Desktop or web — which to pick
@@ -67,11 +67,33 @@ Grab the installer for your platform from the [Multica downloads page](https://m
On first launch you'll need to sign in — the same email + verification code flow as the web app. Once you're in, Desktop syncs your workspace list automatically.
<Callout type="info">
**Which backend Desktop connects to** is determined by the address you select at sign-in. It defaults to Multica Cloud; if you're running self-hosted, click "Connect to a self-hosted instance" on the first login screen and fill in your server address.
**Desktop defaults to Multica Cloud, but can be pointed at a self-hosted instance with a local config file.** There is still no in-app "connect to self-host" picker. Desktop reads `~/.multica/desktop.json` before the renderer starts; if the file is missing, it uses the Cloud defaults.
Minimal self-host config:
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain"
}
```
`apiUrl` is required and must use `http` or `https`. Desktop derives `wsUrl` as `/ws` on the same origin (`wss` for `https`, `ws` for `http`) and derives `appUrl` from the API origin. If your deployment uses different origins, set them explicitly:
```json
{
"schemaVersion": 1,
"apiUrl": "https://api.your-domain",
"wsUrl": "wss://api.your-domain/ws",
"appUrl": "https://your-domain"
}
```
If `desktop.json` exists but is invalid, Desktop fails closed and shows a blocking config error instead of silently falling back to Cloud. For development builds, `VITE_API_URL` / `VITE_WS_URL` / `VITE_APP_URL` still take precedence during `electron-vite dev`. Runtime Desktop self-host configuration was implemented for [issue #1371](https://github.com/multica-ai/multica/issues/1371).
</Callout>
## Next steps
- [Cloud Quickstart](/cloud-quickstart) — the Cloud onboarding flow for Desktop
- [Self-Host Quickstart](/self-host-quickstart) — connecting Desktop to a self-hosted backend
- [Self-Host Quickstart](/self-host-quickstart) — running your own backend and connecting with the CLI or Desktop runtime config
- [Daemon and runtimes](/daemon-runtimes) — how the daemon works (Desktop starts it for you, but the behavior is the same)
@@ -5,7 +5,7 @@ description: Multica Desktop 是什么、和 Web 有什么区别、什么时候
import { Callout } from "fumadocs-ui/components/callout";
Multica Desktop 是原生桌面应用——macOS / Windows / Linux 三个平台。它和 Web 版连同一个后端,看到的数据完全一样,但给了几个 Web 做不到的能力:**[工作区](/workspaces) 独立的多标签页**、**自动启动 [守护进程](/daemon-runtimes)**、**一键升级**。
Multica Desktop 是原生桌面应用——macOS / Windows / Linux 三个平台。对它当前配置的环境来说,它和 Web 版连同一个后端、看到的数据完全一样。Desktop 默认使用 Multica Cloud;自部署实例可以通过本地运行时配置文件接入。它还给了几个 Web 做不到的能力:**[工作区](/workspaces) 独立的多标签页**、**自动启动 [守护进程](/daemon-runtimes)**、**一键升级**。
description: Single source of truth for code naming, i18n translation glossary, and Chinese voice guide.
---
This page is the single source of truth for code naming, the i18n translation glossary, and the Chinese voice guide. Anything that used to live in `packages/views/locales/glossary.md` or in scattered comments now lives here.
If you write Multica code, change a translation, or write Chinese product copy, this is the page to reference.
---
## 1. Code naming
### Routes
Pre-workspace routes (the routes that exist before the user is in a workspace) MUST use either a single word or the `/{noun}/{verb}` pattern.
Hyphenated word groups at the root collide with user-chosen workspace slugs and force endless reserved-slug audits. Reserving the noun (`workspaces`) automatically protects the entire `/workspaces/*` subtree.
### Workspace-scoped routes
Always live under `/{slug}/{section}` — `/{slug}/issues`, `/{slug}/agents`, `/{slug}/settings`. Never duplicate workspace routing logic; use `useNavigation().push()` from shared code, never framework-specific link APIs.
- For UUID parsing in handlers, follow the rule in the root `CLAUDE.md` — `parseUUIDOrBadRequest` for boundary input, `parseUUID` (panicking) for trusted round-trips, never `util.ParseUUID` directly without checking the error.
### TypeScript
- API responses on the wire are `snake_case`; the api client converts to `camelCase` at the boundary. Inside TS code, **always camelCase**.
- Types: `PascalCase` (`Issue`, `AgentRuntime`); never `IPrefix`, never `_t` suffix.
- TanStack Query keys: factory functions in `<feature>/queries.ts`, e.g. `issueKeys.detail(id)`.
### Issue keys
Every issue has a human-readable key like `MUL-123`: workspace `issue_prefix` (3 letters, uppercase) + sequence number. The prefix is set at workspace creation and is never changed afterward.
### Comments in code
English only. The repo enforces this for both Go and TypeScript. If you find a Chinese comment in code, it's a bug — replace it.
This is the **mandatory** glossary for every translation PR. It used to live at `packages/views/locales/glossary.md`; that file is now a stub pointing here.
### The core distinction: entity vs concept
Multica's product nouns split into two categories:
- **Entity** — has a URL, a database row, an API type. In Chinese text, render as **lowercase English** so it visually reads like a type name and signals "this is a Multica system entity".
- **Concept** — generic noun, not a database entity. **Translate fully** so Chinese users don't see jagged English embedded in flowing text.
This rule is aligned with `apps/docs/content/docs/*.zh.mdx` — the docs are the de facto Chinese voice standard and have been battle-tested across 20+ pages.
**Why `issue` / `skill` / `task` stay English while `project` / `autopilot` are translated**:
- **`issue` / `task`**: dev teams talk in English. The Chinese candidates ("任务" — too vague, almost synonymous with "工作"; "工单" — IT ticket connotation; "议题" — GitHub-style but doesn't match the product feel) all read worse than `issue`.
- **`skill`**: Multica-specific concept with no established Chinese term.
- **`project` → "项目"**: settled mainstream Chinese word. Feishu / Tower / Teambition / PingCode / GitHub Projects — every Chinese product translates it. No product keeps `project` in Chinese context.
- **`autopilot` → "自动化"**: in Chinese, "autopilot" associates with Tesla's "自动驾驶" and doesn't match what the feature does (run tasks on a schedule). Notion and Feishu both use "自动化"; that's the industry consensus.
@@ -7,20 +7,21 @@ import { Callout } from "fumadocs-ui/components/callout";
A self-hosted Multica [server](/self-host-quickstart) reads its configuration from environment variables at startup — database, sign-in, email, storage, signup allowlists all live here. This page groups every variable by purpose: each section spells out **what happens if you leave it unset** and **which ones you must set in production**. For how to actually configure the auth-related ones, see [Sign-in and signup configuration](/auth-setup).
## The five required at startup
## Core server variables
These are the five you must think about before deploying — some have defaults that let the server start, but in production you should set all of them explicitly.
These are the core variables you must think about before deploying — some have defaults that let the server start, but in production you should set the required ones explicitly.
| `PORT` | `8080` | No (unless you change the port) |
| `JWT_SECRET` | `multica-dev-secret-change-in-production` | **Yes** (the default is unsafe) |
| `APP_ENV` | empty | **Yes** (must be `production` — see the next section for the trap) |
| `APP_ENV` | empty | **Yes** (must be `production`) |
| `FRONTEND_ORIGIN` | empty | **Yes** (self-host must set its own domain) |
| `MULTICA_DEV_VERIFICATION_CODE` | empty | No (must stay empty in production) |
<Callout type="warning">
**If `APP_ENV` is not set to `production`, anyone can sign in to any account using the code `888888`.** Multica has a development-only master code, `888888` — when `APP_ENV != "production"`, **any email** plus `888888` passes verification. The behavior is intentional for local development (no Resend dependency); **in production, failing to set `production` is equivalent to disabling auth entirely**. See [Sign-in and signup configuration → The 888888 trap](/auth-setup#the-888888-trap).
**Keep `MULTICA_DEV_VERIFICATION_CODE` empty in production.** A fixed local test code is disabled by default, but if you opt in with `MULTICA_DEV_VERIFICATION_CODE=888888`, anyone who can request a code can sign in with that fixed value while `APP_ENV` is non-production. The shortcut is ignored when `APP_ENV=production`.
</Callout>
### Database connection pool
@@ -65,13 +66,19 @@ Multica stores user-uploaded attachments (images and files in comments). **S3 is
| `S3_BUCKET` | empty | **Bucket name only** (for example `my-bucket`). Do **not** include the `.s3.<region>.amazonaws.com` suffix — the server constructs the public host from `S3_BUCKET` + `S3_REGION`. Setting this enables S3 storage |
| `S3_REGION` | `us-west-2` | AWS region. Must match the bucket's actual region — it is used both for SDK signing and for building the public URL |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | empty | Static credentials. When both are unset, the AWS SDK default credential chain is used (IAM role / environment credentials) |
| `AWS_ENDPOINT_URL` | empty | Custom S3-compatible endpoint (for example [MinIO](https://min.io/)). Setting this switches to path-style URLs |
**When `S3_BUCKET` is unset**: the server logs `"S3_BUCKET not set, cloud upload disabled"` at startup, and all uploads fall back to local disk.
**Public URLs** are constructed in this order of priority:
1. `https://<CLOUDFRONT_DOMAIN>/<key>` if `CLOUDFRONT_DOMAIN` is set.
2. `<AWS_ENDPOINT_URL>/<S3_BUCKET>/<key>` (path-style) if `AWS_ENDPOINT_URL` is set.
3. `https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key>` (virtual-hosted-style). When `S3_BUCKET` contains dots, the server falls back to `https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key>` (path-style) because the AWS-issued wildcard TLS certificate does not validate dotted bucket hosts.
This installs the CLI, checks out the latest self-host assets, pulls the official Multica images from GHCR, and configures everything for localhost. Then open http://localhost:3000 and pick a login method: configure `RESEND_API_KEY` in `.env` for email-based codes (recommended), or set `APP_ENV=development` in `.env` to enable the dev master code **`888888`**. See [Step 2 — Log In](#step-2--log-in) for details.
This installs the CLI, checks out the latest self-host assets, pulls the official Multica images from GHCR, and configures everything for localhost. Then open http://localhost:3000 and pick a login method: configure `RESEND_API_KEY` in `.env` for email-based codes (recommended), or leave Resend unset and copy the generated code from backend logs. See [Step 2 — Log In](#step-2--log-in) for details.
<Callout>
If the self-host server is already running and you only need the CLI on a macOS/Linux machine, install it with Homebrew: `brew install multica-ai/tap/multica`.
@@ -68,16 +68,16 @@ If you prefer running the Docker Compose steps manually: `cp .env.example .env`,
### Step 2 — Log In
Open http://localhost:3000. The Docker self-host stack defaults to `APP_ENV=production` (set in `docker-compose.selfhost.yml`), so the dev master code is **disabled by default** for safety on public deployments. Pick one of the following to log in:
Open http://localhost:3000. The Docker self-host stack defaults to `APP_ENV=production` (set in `docker-compose.selfhost.yml`), and there is no fixed verification code by default. Pick one of the following to log in:
- **Recommended (production):** configure `RESEND_API_KEY` in `.env`, then restart the backend. Real verification codes will be sent to the email address you enter. See [Configuration](#configuration) below.
- **Evaluation / private network:** set `APP_ENV=development` in `.env` and restart the backend. Verification code **`888888`** will then work for any email address.
- **Without configuring either:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
- **Without email configured:** the verification code is generated server-side and printed to the backend container logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing on a single machine.
- **Deterministic local/private testing:** set `APP_ENV=development` and `MULTICA_DEV_VERIFICATION_CODE=888888` in `.env`, then restart the backend. This fixed code is ignored when `APP_ENV=production`.
Changes to `ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` also take effect after restarting the backend / compose stack. The web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
<Callout>
**Warning:** do **not** set `APP_ENV=development` on a publicly reachable instance — anyone who knows an email address can then log in with `888888`.
**Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
</Callout>
### Step 3 — Install CLI & Start Daemon
@@ -212,13 +212,15 @@ Changes take effect after restarting the backend / compose stack. The web UI rea
### File Storage (Optional)
For file uploads and attachments, configure S3 and CloudFront:
For file uploads and attachments, configure S3 and (optionally) CloudFront:
| Variable | Description |
|----------|-------------|
| `S3_BUCKET` | S3 bucket name |
| `S3_REGION` | AWS region (default: `us-west-2`) |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain |
| `S3_BUCKET` | Bucket name only (e.g. `my-bucket`). Do **not** include the `.s3.<region>.amazonaws.com` suffix — the server constructs the public URL from `S3_BUCKET` + `S3_REGION` |
| `S3_REGION` | AWS region (default: `us-west-2`). Must match the bucket's actual region — used for both SDK signing and public URLs |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Static credentials. When both are unset, the AWS SDK default credential chain is used |
| `AWS_ENDPOINT_URL` | Custom S3-compatible endpoint (e.g. MinIO, R2, B2). Setting this switches the public URL to path-style |
| `CLOUDFRONT_DOMAIN` | CloudFront distribution domain — when set, public URLs use this host instead of the S3 host |
| `CLOUDFRONT_KEY_PAIR_ID` | CloudFront key pair ID for signed URLs |
@@ -40,7 +40,7 @@ The daemon auto-detects which CLIs are available on your PATH and registers them
Multica supports two layers of skills:
- **Local skills** — Skills already installed in your local runtime (e.g., `.claude/skills/`, `.config/opencode/skills/`) are automatically discovered and used by agents. You do **not** need to upload them to Multica.
- **Local skills** — Skills already installed in your local runtime (e.g., `.claude/skills/`, `.opencode/skills/`) are automatically discovered and used by agents. You do **not** need to upload them to Multica.
- **Workspace skills** — Skills created or imported in the Multica Skills page are shared across the workspace. They are automatically injected into agent runs as supplementary context, so every team member's agents benefit from them.
Workspace skills are designed for team-wide sharing and collaboration — codify your team's best practices once, and every agent can leverage them:
@@ -13,7 +13,7 @@ Multica is a **distributed** platform. The web interface you see is just the fro
- **Multica server** — the workspaces, issue lists, and comment threads you see all live in its database. It's also a WebSocket hub that pushes real-time updates between you and your teammates. It does **not** execute any agent tasks.
- **Daemon** — part of the Multica CLI, running on your own machine. On start it detects which AI coding tools are installed locally, registers with the server, and begins polling for tasks every 3 seconds and sending heartbeats every 15 seconds.
- **AI coding tools** — one of the ten (or several in parallel): [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Once the daemon has picked up a task, it uses these tools to actually do the work.
- **AI coding tools** — one of the eleven (or several in parallel): [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Once the daemon has picked up a task, it uses these tools to actually do the work.
Because the toolchain stays local, **your API keys, code directories, and authorized tools** are only ever used on your machine — the Multica server never sees any of them. This holds whether you self-host or use Cloud.
@@ -39,7 +39,7 @@ It's not only "assign an issue" — Multica has 4 triggers, one per collaboratio
| **Assign an issue** | The most common. Assign an issue to an agent and it starts on its own | [Assigning issues](/assigning-issues) |
| **@mention an agent in a comment** | "Take a look at this one for me" — don't change the assignee or status, just fire off a comment | [Mentioning agents](/mentioning-agents) |
| **Direct chat** | Standalone conversation, not tied to an issue — ask questions, have it draft an issue | [Chat](/chat) |
| **Routines (scheduled)** | Standing instructions — "do a standup summary every Monday morning" and the like | [Routines](/routines) |
| **Autopilots (scheduled)** | Standing instructions — "do a standup summary every Monday morning" and the like | [Autopilots](/autopilots) |
@@ -13,7 +13,7 @@ This page explains where agents run and the ways you can start using Multica.
Agents do **not** execute tasks on Multica's servers. Multica currently supports one runtime model:
- **Local [daemon](/daemon-runtimes)** — you run `multica daemon` on your own machine, and it drives the [AI coding tools](/providers) installed locally. Ten are built in today: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Your API keys, toolchain, and code directories stay on your machine.
- **Local [daemon](/daemon-runtimes)** — you run `multica daemon` on your own machine, and it drives the [AI coding tools](/providers) installed locally. Eleven are built in today: [Claude Code](/providers#claude-code), [Codex](/providers#codex), [Cursor](/providers#cursor), [Copilot](/providers#copilot), [Gemini](/providers#gemini), [Hermes](/providers#hermes), [Kimi](/providers#kimi), [Kiro CLI](/providers#kiro-cli), [OpenCode](/providers#opencode), [OpenClaw](/providers#openclaw), [Pi](/providers#pi). Your API keys, toolchain, and code directories stay on your machine.
<Callout type="info">
**Cloud runtimes are coming**, currently waitlist-only. Once live, you won't need a local daemon — agent tasks will execute on Multica Cloud directly. Sign up on the [Downloads](https://multica.ai/download) page to get notified.
description: Attach typed pointers (Git repos today, more later) to a project so agents can pick them up as scoped context.
---
A **Project Resource** is a typed pointer — a Git repo URL today, a Notion page or document link tomorrow — attached to a [project](/workspaces). When an [agent](/agents) runs against an issue inside that project, the daemon automatically writes the project's resource list into the agent's working directory and into its [meta-skill](/skills) prompt.
The result: the agent knows which repo to check out, which docs are the "primary references" for this project, without anyone copy-pasting context into the issue body.
## Mental model
A project is no longer just a label. It is a small **resource container**:
- A project has 0..N **resources**.
- A resource has a `resource_type` (e.g. `github_repo`) and a `resource_ref` (a JSON payload typed by `resource_type`).
- New resource types add a string + a handler. **No schema migration. No frontend rewrite.**
This shape is intentional — it's the same pattern Multica already uses for agent providers: a `type` discriminator and a typed payload. It keeps the schema stable so adding "Notion page", "Google Doc", "uploaded file", or "external URL" later is a small, additive change.
## Today: `github_repo`
The first resource type ships ready to use:
```json
{
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/owner/repo",
"default_branch_hint": "main"
}
}
```
`default_branch_hint` is optional — if present, the daemon surfaces it in the meta-skill so the agent knows which branch to base its work on.
## Attaching repos at project creation
In the **Web** or **Desktop** app, opening *New project* now shows a **Repos** pill alongside Status / Priority / Lead. Selecting workspace-bound repos (or pasting an ad-hoc URL) attaches them as `github_repo` resources the moment the project is created.
From the **CLI**:
```bash
# Create + attach in one shot. The server attaches resources in the same
# transaction as the project create — invalid resources roll back the whole
# operation, so you never end up with a project that has half its resources.
Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
```
The text is intentionally minimal. The full payload is on disk; the prompt only orients the agent so it knows the project exists and what's attached.
### Failure mode
Resource fetch is **best-effort**. If the API call fails, the project section is omitted from the prompt and the file is not written, but the task still starts. Agents never block on missing project context.
## Adding a new resource type
The whole point of the abstraction is that new types are cheap. The full path:
1. **Server validator** (`server/internal/handler/project_resource.go`) — add a case in `validateAndNormalizeResourceRef` that parses and normalizes the new payload.
2. **Daemon meta-skill formatter** (`server/internal/daemon/execenv/runtime_config.go`) — add a case in `formatProjectResource` so the agent prompt renders the new type as a readable bullet.
3. **TypeScript types** (`packages/core/types/project.ts`) — extend `ProjectResourceType` and add the payload interface.
4. **UI renderer** (`packages/views/projects/components/project-resources-section.tsx`) — add a case in `ResourceRow` for the new type.
There is **no schema migration**, no new sqlc query, no new endpoint, **and no CLI change** — the CLI's generic `--ref '<json>'` flag accepts any payload the validator understands, so day-one support for a new type is purely the four steps above. (You may *optionally* add a per-type CLI shortcut later; not required.)
The same `project_resource` table and the same three CRUD calls handle every type.
## Workspace repos vs. project repos
The repo list shown to the agent (`## Repositories` block in `CLAUDE.md` / `AGENTS.md`) is chosen by the daemon claim handler with this precedence:
- **Project has at least one `github_repo` resource** → only those repos are surfaced to the agent. Workspace-bound repos are intentionally hidden so the agent doesn't have to guess which one belongs to this issue.
- **Project has no `github_repo` resources (or the issue isn't in a project)** → fall back to the workspace's repo list as before.
This keeps the agent's working set tight: when a project is explicit about its repos, that's the authoritative answer. The structured resource list at `.multica/project/resources.json` always carries the full set, so a skill that wants to inspect everything still can.
The daemon mirrors this on the checkout side: when a task arrives with project-scoped `github_repo` URLs, those URLs are merged into the per-workspace allowlist *and* synced into the local repo cache before the agent spawns. So a project repo URL that isn't bound at the workspace level is still a valid argument to `multica repo checkout` — the daemon won't reject it as "not configured." The allowlist split is internal: workspace-bound URLs and task-scoped URLs are tracked separately, so a workspace-repos refresh doesn't accidentally revoke a project URL mid-run.
## What's intentionally **not** in scope here
- **Cross-project sharing.** Each resource lives on exactly one project today.
- **Per-skill resource scoping.** All resources are visible to every skill on the agent's run; type-aware filtering is a follow-up.
- **Caching / sync.** `github_repo` is just metadata — checkout still happens via `multica repo checkout` on demand. Cached document text for Notion / Google Docs will arrive with those types.
These are deliberate omissions — the goal of the first cut is to validate the abstraction with the smallest set of moving parts.
description: Multica supports 10 AI coding tools; they implement the same interface, but the capability details diverge significantly.
description: Multica supports 11 AI coding tools; they implement the same interface, but the capability details diverge significantly.
---
import { Callout } from "fumadocs-ui/components/callout";
Multica ships with built-in support for **10 AI coding tools**. They all implement the same interface — queue, dispatch, execute, return results — so you can drive any of them from the same Multica board. **But the capability details diverge significantly**: whether session resumption actually works, whether MCP is supported, where skill files live, how models are selected. This page is the full matrix.
Multica ships with built-in support for **11 AI coding tools**. They all implement the same interface — queue, dispatch, execute, return results — so you can drive any of them from the same Multica board. **But the capability details diverge significantly**: whether session resumption actually works, whether MCP is supported, where skill files live, how models are selected. This page is the full matrix.
For guidance on picking a tool when creating an agent, see [Creating and configuring agents](/agents-create).
@@ -20,15 +20,16 @@ For guidance on picking a tool when creating an agent, see [Creating and configu
| **OpenClaw** | Open source | ✅ | ❌ | `.agent_context/skills/` (fallback) | Bound to the agent, can't be switched per task |
| **Pi** | Inflection AI | ✅ (session is a file path) | ❌ | `.pi/agent/skills/` | Dynamic discovery |
| **Pi** | Inflection AI | ✅ (session is a file path) | ❌ | `.pi/skills/` | Dynamic discovery |
## What each tool is for
### Claude Code
From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it's the **only one of the 10 that truly reads MCP configuration**, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
From Anthropic. **First choice for new users** — the most complete feature set: session resumption actually works, it's the **only one of the 11 that truly reads MCP configuration**, and it supports fine-tuning flags like `--max-turns` and `--append-system-prompt`. Requires an Anthropic API key.
### Codex
@@ -54,6 +55,10 @@ From Nous Research. Uses the ACP protocol (shares a transport with Kimi). Sessio
From Moonshot, aimed at the Chinese market. Shares the ACP protocol with Hermes, but the skill path `.kimi/skills/` is Kimi CLI's native discovery mechanism — different from Hermes's fallback.
### Kiro CLI
From Amazon. Uses ACP over stdio via `kiro-cli acp`. Session resumption works through ACP `session/load`, model selection works through `session/set_model`, and skills are copied into `.kiro/skills/` for native project-level discovery.
### OpenCode
From SST, open source. Dynamically discovers available models (scans the CLI's configuration file). Session resumption works. **Suitable for tinkerers who want to customize their model catalog.**
@@ -72,7 +77,7 @@ The session resumption mechanism is covered in [Tasks](/tasks#can-a-task-continu
| Status | Tools | Meaning |
|---|---|---|
| ✅ Really works | Claude Code, Copilot, Hermes, Kimi, OpenCode, OpenClaw, Pi | Pass the resume id and it continues from the previous context |
| ✅ Really works | Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi | Pass the resume id and it continues from the previous context |
| ⚠️ Code exists but unreachable | Codex, Cursor | Resume paths exist in the code but aren't actually reached (Codex silently falls back; Cursor doesn't return session id) — **treat as unsupported** |
| ❌ None | Gemini | The CLI has no resume mechanism |
@@ -80,7 +85,7 @@ The session resumption mechanism is covered in [Tasks](/tasks#can-a-task-continu
## MCP configuration: only Claude Code actually reads it
**Of the 10 tools, only Claude Code actually consumes `mcp_config`**. The other 9 accept the field but **completely ignore it** — no error, no warning, the config just has no effect.
**Of the 11 tools, only Claude Code actually consumes `mcp_config`**. The other 10 accept the field but **completely ignore it** — no error, no warning, the config just has no effect.
<Callout type="warning">
If you set `mcp_config` in an agent configuration but pick a tool other than Claude Code, your MCP servers have **no effect** on that agent. MCP integration currently covers Claude Code only.
@@ -97,8 +102,9 @@ Each tool uses **its own** skill discovery path. Before a task runs, the Multica
description: Let agents start work on a cron schedule — or trigger once manually via the UI or CLI.
---
import { Callout } from "fumadocs-ui/components/callout";
Routines let [agents](/agents) **start work automatically on a schedule** — configure a cron expression and a timezone, and Multica dispatches a [`task`](/tasks) on its own, without you triggering anything. It fits periodic checks, recurring reports, and overnight cleanup jobs — the "standing order" shape of work. Compared to the other three trigger paths ([assigning](/assigning-issues), [@-mention](/mentioning-agents), and [chat](/chat), where you are the one kicking things off), the core difference with Routines is that they are **time-driven**.
## Configure a routine
Create a new routine on the workspace's **Routines** page. You set:
- **Name** — display name
- **Agent** — who the run is dispatched to
- **Priority** — inherited by the `task` it produces (same semantics as issue priority)
- **Description / prompt** — the work description the agent receives each run
- **Execution mode** — see below
- **Triggers** — at least one `schedule` (cron + timezone)
## Pick an execution mode
A routine has two execution modes. **Start with "create issue" mode.**
- **Create issue mode** (`create_issue`) — default, **recommended**. Each trigger first creates an issue in the workspace (the title supports interpolation like `{{date}}`), then assigns the issue to the agent through the normal assignment flow. All work lands on the issue board with the same history, comments, and status as a manually assigned issue.
- **Run-only mode** (`run_only`) — skips issue creation and enqueues a `task` directly. The run is invisible on the board — you can only see it in the routine's run history.
<Callout type="warning">
**Run-only mode is currently unstable.** The CLI labels it "not yet supported end-to-end," and the dispatch path has known issues. New users should stick to create issue mode and wait for run-only mode to ship a stable release before switching.
</Callout>
## Run it on a schedule
Every routine needs at least one `schedule` trigger. Cron uses the **standard 5-field format** (minute hour day month weekday), with **1-minute** minimum granularity (no seconds). Timezone is IANA-formatted (for example, `Asia/Shanghai`) and determines which timezone the cron expression is interpreted in.
A few examples:
- `0 9 * * 1-5`, `Asia/Shanghai` — 9 AM Beijing time on weekdays
- `*/30 * * * *`, `UTC` — every 30 minutes
- `0 3 * * *`, `UTC` — every day at 3 AM UTC
The Multica server scans for due triggers every **30 seconds** — **the actual fire time can lag by up to 30 seconds**, not down to the second. If the server is restarted across a fire time, it catches up missed triggers on startup (nothing is lost, but they fire right away).
## Trigger once manually
To avoid waiting for cron while debugging a routine, trigger it manually:
- UI: click "Run now" on the routine detail page
- CLI:
```bash
multica autopilot trigger <routine-id>
```
A manual trigger goes through the exact same execution flow as a `schedule` trigger — only the `source` field on the run record is marked `manual`.
## View run history
Every trigger produces a **run record**, visible on the "History" tab of the routine detail page:
- Trigger source (`schedule` / `manual`)
- Start time, completion time
- Status (`issue_created` / `running` / `completed` / `failed`)
- The linked issue (create issue mode) or `task` (run-only mode)
- Failure reason (if failed)
## What happens when a routine fails
<Callout type="warning">
**Routine failures are not auto-retried and do not send inbox notifications.** A failure leaves a `failed` entry in run history — no system-level re-enqueue like assign or @-mention, and no notification to anyone. If the routine is periodic, **the next cron fire will trigger a new run**, but the failed work is not automatically re-run.
If a routine is important, design your own monitoring — for example, have the agent post a comment on success, and catch failures by noticing missing comments.
</Callout>
Why no auto-retry: routines are already periodic, so adding system-level retries stacks on top of the next scheduled run and creates duplicate executions. Leaving the schedule entirely to cron keeps it clean.
## Two naming / capability carryovers
**In the CLI it is called `autopilot`.** The current CLI subcommand is `multica autopilot` rather than `multica routine`:
```bash
multica autopilot list
multica autopilot create
multica autopilot trigger <id>
```
The docs use Routines throughout; a future CLI release will unify the naming. For now, treat any `autopilot` wording as Routines.
**Webhook and API triggers are not available yet.** The routine trigger schema reserves `webhook` and `api` types, but **they are not wired up to any ingress route** — the UI can create triggers of either type, but they will not actually fire. Today, **only `schedule` and manual triggers are end-to-end usable.**
## Next
- [**Assign issues to agents**](/assigning-issues) — a one-shot hand-off of an issue to an agent
- [**@-mention agents in comments**](/mentioning-agents) — pull an agent in to take a look from a comment
- [**Chat**](/chat) — one-to-one conversation outside any issue
3. Bring up every service using `docker-compose.selfhost.yml`
4. Wait until the backend's `/health` endpoint is ready
For ongoing production probes after startup, use `/readyz` when you want the
check to fail on database or migration problems.
The backend container **runs database migrations automatically** on startup (`docker/entrypoint.sh` runs `./migrate up` before the server starts) — you'll see the migration output in the backend logs. Version upgrades are handled the same way.
**`docker-compose.selfhost.yml` sets `APP_ENV` to `production` by default** — this prevents the development "master code `888888`" from being enabled on an instance you've exposed to the public internet.
**`docker-compose.selfhost.yml` sets `APP_ENV` to `production` by default** and leaves `MULTICA_DEV_VERIFICATION_CODE` empty, so there is no fixed code on public instances.
**But if your `.env` leaves `APP_ENV` empty or sets it to another value**, `888888` is enabled — **anyone can log in as any email by typing `888888` as the verification code**. See [Auth setup → The 888888 trap](/auth-setup#the-888888-trap).
Only set `MULTICA_DEV_VERIFICATION_CODE` for local or private test automation. If a fixed code is enabled while `APP_ENV` is non-production, anyone who can request a code can sign in with that fixed value. See [Auth setup → Fixed local testing codes](/auth-setup#fixed-local-testing-codes).
Before any public deployment, make sure `.env` has `APP_ENV=production`.
Before any public deployment, make sure `.env` has `APP_ENV=production` and `MULTICA_DEV_VERIFICATION_CODE` is empty.
</Callout>
## 3. Configure the email service (optional but recommended)
Without email configured, your users can't receive verification codes — **unless `APP_ENV != production`, in which case `888888` works** (see the warning above).
Without email configured, your users can't receive verification codes by email; the server prints generated codes to stdout instead.
To actually send verification emails:
@@ -77,6 +80,7 @@ Open [http://localhost:3000](http://localhost:3000):
- Enter your email
- Grab the verification code from the Resend email (or, if you haven't configured Resend, from the server container stdout — look for the `[DEV] Verification code` line)
- Do not use `888888` unless you explicitly set `MULTICA_DEV_VERIFICATION_CODE=888888` on a non-production private instance
- Log in and create your first workspace
## 5. Point the CLI at your own server
@@ -112,4 +116,4 @@ Same flow as Cloud — see [Cloud quickstart → Steps 5-6](/cloud-quickstart#5-
- [Environment variables](/environment-variables) — full env reference
@@ -6,7 +6,7 @@ description: The unit of work for every agent run, with a clear state machine, t
import { Callout } from "fumadocs-ui/components/callout";
import { Mermaid } from "@/components/mermaid";
A **task** is the unit of every [agent](/agents) run — [assigning an issue to an agent](/assigning-issues), [@-mentioning an agent in a comment](/mentioning-agents), sending a message in [chat](/chat), or a [Routine](/routines) firing on schedule all produce a task. Multica puts it in a queue; a [daemon](/daemon-runtimes) picks it up and hands it off to the corresponding [AI coding tool](/providers), then writes the result back to the server when it finishes.
A **task** is the unit of every [agent](/agents) run — [assigning an issue to an agent](/assigning-issues), [@-mentioning an agent in a comment](/mentioning-agents), sending a message in [chat](/chat), or an [Autopilot](/autopilots) firing on schedule all produce a task. Multica puts it in a queue; a [daemon](/daemon-runtimes) picks it up and hands it off to the corresponding [AI coding tool](/providers), then writes the result back to the server when it finishes.
Tasks and [issues](/issues) are two different objects. A single issue can be assigned, @-mentioned, and manually rerun many times — each produces a **new** task.
@@ -59,10 +59,10 @@ Failures fall into two categories: **retryable** and **non-retryable**.
Automatic retry also has two extra conditions:
1. **At most 2 attempts** — 1 original + 1 retry. If the retry also fails, no further retries, even if the reason is retryable.
2. **Only for issue- and chat-triggered tasks** — Routine-triggered tasks do **not** retry automatically.
2. **Only for issue- and chat-triggered tasks** — Autopilot-triggered tasks do **not** retry automatically.
<Callout type="warning">
**Routine tasks don't retry automatically** by design. A Routine has its own firing cadence (e.g. daily); automatic retries on failure would overlap with the next scheduled run. If you need an immediate re-run after failure, use a manual rerun (next section).
**Autopilot tasks don't retry automatically** by design. An Autopilot has its own firing cadence (e.g. daily); automatic retries on failure would overlap with the next scheduled run. If you need an immediate re-run after failure, use a manual rerun (next section).
</Callout>
## Manual rerun vs. automatic retry
@@ -100,7 +100,7 @@ Multica pins the session ID **twice** during a task: once at the start (when the
But **which AI coding tools actually support this** varies a lot:
- ✅ **Real support** — Claude Code, Copilot, Hermes, Kimi, OpenCode, OpenClaw, Pi
- ✅ **Real support** — Claude Code, Copilot, Hermes, Kimi, Kiro CLI, OpenCode, OpenClaw, Pi
- ⚠️ **Code exists but unusable** — Codex, Cursor
- ❌ **No support** — Gemini
@@ -108,5 +108,5 @@ See [Providers Matrix → Session resumption](/providers#session-resumption-who-
## Next
- [Providers Matrix](/providers) — capability differences across the 10 AI coding tools (including the exact session-resumption status)
- [Assigning issues to agents](/assigning-issues) / [@-mentioning agents in comments](/mentioning-agents) / [Chat](/chat) / [Routines](/routines) — the four ways to trigger a task
- [Providers Matrix](/providers) — capability differences across the 11 AI coding tools (including the exact session-resumption status)
- [Assigning issues to agents](/assigning-issues) / [@-mentioning agents in comments](/mentioning-agents) / [Chat](/chat) / [Autopilots](/autopilots) — the four ways to trigger a task
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.