Two follow-up nits from PR #2211 review:
- Rename the package-local `repoCache` interface to `repoCacheBackend`
so the field declaration `repoCache repoCacheBackend` no longer shadows
its own type name.
- Bump the `/health`-must-respond timeout in
`TestHealthHandlerRespondsWhileTaskRepoLookupWaits` from 200ms to 1s.
The regression case blocks indefinitely on the old code, so a 1s
upper bound still fail-fast detects it while leaving headroom for
loaded CI runners.
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): add disk-usage CLI to surface per-task / per-workspace footprint
Adds `multica daemon disk-usage [--by-workspace] [--by-task] [--top N]
[--output json]`, walking the workspaces root to report task and workspace
disk consumption without requiring a running daemon. Sizing reuses the GC
artifact patternSet (basename-only) so the reported "artifact" footprint
matches what `cleanTaskArtifacts` would actually reclaim, and the walk
honors the same safety contract: never enters .git, never follows symlinks,
counts only regular files.
Refactors WorkspacesRoot resolution into an exported `ResolveWorkspacesRoot`
so the read-only CLI picks the same root the running daemon would have.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): distinguish displayed totals from scan totals; add workspace artifact ratio
- Track scan-wide TotalTaskCount / TotalWorkspaceCount on the report so
`--top N` no longer leaves the table footer claiming the truncated row
count is the full count. The CLI now prints a "Showing top N of M …
Displayed: X. Scan total: Y" line whenever truncation happens, and keeps
the bare "Total: …" footer for the un-truncated case.
- Add ArtifactRatio (0..1) on WorkspaceDiskUsage and TotalArtifactRatio on
the report. The workspace table renders an `ARTIFACT %` column. ratio()
guards size=0 so empty workspaces report 0% instead of NaN%.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Filters available skills by name + description (case-insensitive) as the
user types. Auto-focuses on open and clears the query on close. Shows a
distinct "no match" empty state vs. the existing "all assigned" one.
Closes#2266
Co-authored-by: multica-agent <github@multica.ai>
* feat(daemon): extend GC to chat / autopilot / quick-create tasks
Before this change the daemon's GC was strictly issue-centric: only tasks
with a non-empty issue_id ever wrote .gc_meta.json, and shouldCleanTaskDir
called only the issue gc-check endpoint. Chat / autopilot run / quick-create
tasks fell through to the GCOrphanTTL mtime path, which mis-killed active
chat sessions while leaving deleted ones around far longer than necessary.
Schema:
- GCMeta gains a Kind discriminator and per-kind ID fields
(ChatSessionID / AutopilotRunID / TaskID). WriteGCMeta now takes a
GCMeta struct so the call site classifies the task explicitly.
- ReadGCMeta defaults empty Kind to GCKindIssue, so legacy on-disk meta
files keep flowing through the issue path with no migration required.
Server endpoints (siblings of /api/daemon/issues/{id}/gc-check, all behind
requireDaemonWorkspaceAccess for the same anti-enumeration shape):
- GET /api/daemon/chat-sessions/{id}/gc-check -> {status, updated_at}
- GET /api/daemon/autopilot-runs/{id}/gc-check -> {status, completed_at}
- GET /api/daemon/tasks/{id}/gc-check -> {status, completed_at}
shouldCleanTaskDir dispatches on Kind:
- chat: active is hard-skipped (no mtime fallback) so idle sessions are
never reclaimed; archived + GCTTL cleans; 404 falls back to mtime to
stay safe for cross-workspace tokens.
- autopilot_run: terminal (completed/failed/skipped/issue_created) +
GCTTL cleans; running/pending skips. Uses run.completed_at as the TTL
anchor since autopilot_run has no updated_at column.
- quick_create: terminal task status cleans immediately (workdir is not
reused by the linked issue task, which has its own envRoot); running
skips.
Also drops the "skipping .gc_meta.json: issue_id is empty" warn — with
the new kind dispatch, chat/autopilot/quick-create tasks now write a
proper meta file instead of triggering this log.
Refs: GC follow-up to PR #2077 (symptom fix) and #2115 (chat hard delete).
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): chat gc-check 404 cleans immediately, no mtime gate
PR review caught that the chat 404 path was routing through
orphanByMTime, which deferred reclamation to GCOrphanTTL (72h) when
acceptance #3 calls for cleanup within one GC cycle (≤ 1h) after the
user hard-deletes a session.
Every chat_session_id we ever ask about was written by this same daemon
under its current token, so the cross-workspace probe defense the issue
path needs doesn't apply here. Drop the gate and clean on 404 directly.
Test updates:
- TestShouldCleanTaskDir_KindDispatch/chat_404 flips the locked
expectation from gcActionSkip to gcActionClean.
- Adds TestShouldCleanTaskDir_ChatHardDeletedFreshMtime: GCOrphanTTL
set to a year so any mtime-based path is unmistakably out, and the
fresh-mtime workdir still cleans on the chat-404 fast path.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Two related changes for the same UX problem (#1857 follow-up).
1. Orphan-reply rescue. The grouping in issue-detail.tsx put replies under
their parent's CommentCard, looking them up via repliesByParent.get(parentId).
When a reply's parent wasn't in the loaded timeline — pagination boundary,
merge truncation, future backend bug — the entire reply subtree dropped
off the screen, since the orphan replies sat in the map with no
CommentCard around to render them. MUL-1847 hit this on the OLD backend:
1 root + 29 replies, the root was the oldest entry and the merge dropped
it, so all 29 replies vanished from the UI even though the API returned
them.
The fix: a reply whose parent_id points to a comment NOT in the loaded
timeline is promoted to top-level. It still loses its visual indentation
under the missing parent, but it stops disappearing.
2. Page size 50. With activities now decoupled from the comment budget
(#2253) and the off-by-one fixed (#2259), 50 fits the typical issue
without any "Show older" interaction. Cost is bounded — SQL fetches
limit+1 = 51 comments + 50 activities through the keyset index from
migration 068; response body grows ~70% over 30 but stays well under
the legacy compat path's 200-row cap. UI renders 100 entries
comfortably; CommentCards memoize.
Frontend default in `client.ts` (`limit = 50`) matches the new backend
default (`timelineDefaultLimit = 50`) so pages walk consistently.
Test: render-level case in `issue-detail.test.tsx` mocks a timeline page
containing only an orphaned reply (parent_id refers to a missing id) and
asserts the reply text appears.
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): aggregate task_usage into daily rollup table to cut DB load
ListRuntimeUsage previously did a SUM(...) GROUP BY DATE(created_at), provider,
model over the raw task_usage stream once per runtime row on the runtimes
list and once per detail page load, scaling O(events) per call. This is the
hot read path responsible for sustained load on Postgres.
Switch the read path to a materialized daily rollup table maintained by a
pg_cron job:
- 072_task_usage_daily_rollup: schema for task_usage_daily +
task_usage_rollup_state, plus rollup_task_usage_daily_window(p_from, p_to)
(window primitive used by both cron and offline backfill, idempotent via
ON CONFLICT DO UPDATE adding deltas) and rollup_task_usage_daily() (cron
entry point — pg_try_advisory_lock(4242) for serialization, watermark
advancement, 5-minute safety lag for late-visible inserts). Also adds
idx_task_usage_created_at to help the two lazy endpoints
(ListRuntimeUsageByAgent / GetRuntimeUsageByHour) that still hit the
raw table.
- 073_task_usage_daily_pgcron: CREATE EXTENSION IF NOT EXISTS pg_cron in a
DO/EXCEPTION block (mirrors the migration 032 pg_bigm pattern so envs
without shared_preload_libraries=pg_cron skip gracefully) and schedules
rollup_task_usage_daily() every 5 minutes when the extension is present.
- queries/runtime_usage.sql ListRuntimeUsage rewritten to read from
task_usage_daily; sqlc regenerated. Other usage queries unchanged.
- cmd/backfill_task_usage_daily: one-shot Go command that walks
task_usage in monthly slices through rollup_task_usage_daily_window,
then stamps the watermark to now()-5m so the cron resumes cleanly.
Run once after migrations have applied, before relying on the rollup.
- runtime_test.go: TestGetRuntimeUsage_BucketsByUsageTime now invokes
rollup_task_usage_daily_window after fixture inserts so the handler
sees the rolled-up rows. Synthetic daily rows cleaned up after each
test.
- runtime_rollup_test.go: new tests covering aggregation correctness,
idempotency contract of ON CONFLICT DO UPDATE, and the watermark
advancing exactly to now()-5m via the cron entry point.
Deployment order: apply migrations → run backfill_task_usage_daily once
→ pg_cron picks up subsequent windows automatically. Today bucket may be
up to ~10 minutes stale (5 min cron + 5 min lag) by design.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): make task_usage_daily rollup safe to overlap, replay, and correct
Addresses 4 review blockers on the original PR:
1. Cron/backfill double-count race: the rollup function is now idempotent.
Window calls find DIRTY KEYS via task_usage.updated_at, then RECOMPUTE
each bucket from ground truth and REPLACE the daily row (no more
additive ON CONFLICT). Cron and backfill can now overlap safely.
2. Silent pg_cron absence: the read path is gated behind a new
USAGE_DAILY_ROLLUP_ENABLED feature flag (default off). The raw
task_usage scan is preserved as the fallback. Operators flip the
flag per-environment after backfill + cron are confirmed healthy
(task_usage_rollup_lag_seconds() helper added for monitoring).
3. UpsertTaskUsage corrections invisible to rollup: added
task_usage.updated_at column (default now(), backfilled from
created_at), and bumped it on conflict. Corrections now mark the
bucket dirty and the next window call recomputes it correctly.
4. CREATE INDEX blocking writes on hot table: split into separate
single-statement migrations using CREATE INDEX CONCURRENTLY
(074, 075), matching the 035/067 pattern.
Also: cron.schedule() removed from migrations entirely. Migration 076
only enables the extension (gracefully on unsupported envs); the actual
schedule is a documented operator runbook step that runs AFTER backfill.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): trigger-driven invalidation + online-safe migration for task_usage_daily
Round-2 review feedback on PR #2256:
1. Add explicit dirty-bucket queue (task_usage_daily_dirty) populated by
triggers on agent_task_queue (UPDATE OF runtime_id, DELETE) and
task_usage (DELETE). The rollup window function drains both this queue
and the updated_at-based discovery, so runtime reassignment and
issue-cascade deletes no longer leave the rollup divergent from the
raw query.
Triggers join via agent (not issue) to look up workspace_id, because
when the cascade comes from issue, the issue row is already gone by
the time atq's BEFORE DELETE fires; agent stays alive.
2. Make migration 072 online-safe: only ADD COLUMN updated_at TIMESTAMPTZ
(nullable, no default → metadata-only ALTER, no row rewrite) and a
separate ALTER for SET DEFAULT now() (also metadata-only). No bulk
UPDATE on the hot task_usage table. The rollup window function's
dirty_keys CTE handles legacy NULL rows via an OR branch, supported
by partial index idx_task_usage_created_at_legacy.
3. Refresh stale documentation in cmd/backfill_task_usage_daily/main.go
header to describe the current recompute/replace semantics, idempotent
re-runnability, and the actual migration numbering (072..077).
Tests:
- TestRollupTaskUsageDaily_InvalidationOnReassign: verifies usage moves
between runtime buckets after ReassignTasksToRuntime-style update.
- TestRollupTaskUsageDaily_InvalidationOnIssueDelete: verifies daily
bucket is cleared after issue delete cascades through atq → task_usage.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): close dirty-queue race + move legacy partial index to its own concurrent migration
Round-3 review feedback on PR #2256:
1. Blocker: dirty-queue invalidations could be silently lost under
concurrency. ON CONFLICT DO NOTHING let a late trigger see the row
already enqueued, no-op, and then the rollup drain (WHERE
enqueued_at < p_to) would delete the original row — losing the
late invalidation. Switched all three trigger enqueue paths to
ON CONFLICT DO UPDATE SET enqueued_at = GREATEST(existing,
EXCLUDED.enqueued_at), so any invalidation arriving during a
rollup tick keeps enqueued_at > p_to (p_to = now() - 5min) and
survives the post-tick drain.
2. High: idx_task_usage_created_at_legacy (partial index on hot
task_usage table) was being created in the regular 077 migration
without CONCURRENTLY. Moved to new migration 078 with
CREATE INDEX CONCURRENTLY, matching the pattern of 074/075.
077's down migration leaves the index alone (it is owned by 078).
3. Minor: gofmt -w on runtime_rollup_test.go and
backfill_task_usage_daily/main.go (tabs were lost in the original
heredoc append). PR description rewritten to describe the current
recompute/replace + dirty queue + feature flag design and the
072..078 migration ordering.
Tests still green: TestRollupTaskUsageDaily_* (including both new
invalidation regressions), TestGetRuntimeUsage_*, TestWorkspaceUsage_*.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(server): unify workspace_id source via agent in rollup window function
Round-4 review feedback (J) on PR #2256:
M1 (must-fix): The dirty queue triggers resolved workspace_id via
`agent.workspace_id`, but the window function's `dirty_from_updates`
discovery and `recomputed` recompute join used `issue.workspace_id`.
There is no schema-level FK guaranteeing
`agent.workspace_id == issue.workspace_id`. Any divergence (future
cross-workspace task scenarios, data repairs, migration bugs) would
cause:
- dirty queue rows with workspace_id from agent
- recompute join filtering by workspace_id from issue
- 0 matches in recompute → bucket erroneously hits the
deleted_empty branch and the daily row is silently dropped
- dirty_from_updates path attributing usage to the wrong workspace
Replaced both CTEs to JOIN agent (not issue) so trigger / discovery /
recompute share one workspace_id source. Comment in 077 explains the
constraint.
N1: Refreshed two stale references in
cmd/backfill_task_usage_daily/main.go (header now says "072..078";
stampWatermark warning now mentions migration 073, where the rollup
state table is actually introduced).
Test: New TestRollupTaskUsageDaily_WorkspaceMismatch constructs an
atq with agent.workspace_id != issue.workspace_id, asserts the bucket
lands under agent's workspace (not issue's), and re-asserts after a
runtime reassign in the foreign workspace. Acts as a canary if the
schema invariant changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
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>
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Pre-fix the gate was `len(comments) >= limit`, which fired even when the
issue had EXACTLY <limit> comments. The "Show older" affordance appeared,
the user clicked, the next page fetched zero rows. User flagged it on
MUL-1857 — "this issue happens to have 30 comments; the button shouldn't
appear in that case."
The fix is the standard over-fetch probe: ask the SQL for limit+1 rows; if
it returned more than limit, drop the extra and report hasMore=true.
Otherwise hasMore=false.
- New helper `commentOverflow(rows, limit) -> ([]db.Comment, bool)` replaces
the count-based `hasMoreCommentsBeyond`. Works for both DESC (latest /
before) and ASC (after / around-newer) since both want "keep first
<limit>".
- All four mode handlers (latest, before, after, around) now ask for
limit+1 comments and route through the helper.
- Activities still cap at <limit> with no overflow probe — they don't gate
pagination (#1857), so the boundary doesn't matter for them.
Tests:
- TestCommentOverflow pins the truth table with the boundary case
("exactly limit comments" → hasMore=false).
- TestListTimeline_ExactlyLimitCommentsHidesShowOlder is the DB-backed
regression: 30 comments, limit=30, asserts has_more_before=false and
next_cursor=nil.
Co-authored-by: multica-agent <github@multica.ai>
The pre-fix top "Show older" was a bare <button> sandwiched between two
horizontal divider lines, styled `text-xs text-muted-foreground`. Visually
it read as a divider, not an action — users on issues with hidden older
entries thought the comments had vanished and didn't notice the affordance.
Convert all three timeline pagination affordances to shadcn Button:
- Top: outline button with ChevronUp icon, "Show older"
- Bottom (in around-mode pages): outline button with ChevronDown icon,
"Show newer"; default-variant button with ArrowDownToLine icon,
"Jump to latest" (or "Jump to latest · N new")
No behavior change — same fetchOlder / fetchNewer / jumpToLatest hooks,
same i18n keys. Just the visual treatment.
Co-authored-by: multica-agent <github@multica.ai>
* fix(timeline): exclude activities from comment page budget
The /timeline endpoint paginated comments + activities through one shared
50-row budget, so an issue with a chatty agent (status flips, task_completed
markers, assignee toggles per run) could trigger "show older" with as few as
10-20 actual comments — users opened the page and thought their discussion
had vanished.
- Comment limit drops from 50 to 30 (the visible page size users wanted).
- has_more_before / has_more_after gate on comments alone via the new
hasMoreCommentsBeyond helper. Activity rows still ride along at the same
per-call SQL cap but no longer push real comments off-page.
- Merge functions stop truncating at the page limit; both pools are
individually bounded by SQL, so dropping rows here only re-introduced the
bug. The legacy (pre-cursor) path applies its 200-row cap inline.
- Test rewrite: TestHasMoreBeyond → TestHasMoreCommentsBeyond, replaced the
#2192 merge-truncation regression with a #1857 "dense activity does not
hide comments" test that pins the new contract directly.
Co-authored-by: multica-agent <github@multica.ai>
* fix(timeline): per-pool keyset cursor for comments and activities
Pre-fix, next_cursor / prev_cursor anchored on the merged page boundary
(oldest / newest entry overall). When activity rows were older than every
fetched comment — common on issues created with a status change before the
first comment — the latest page emitted a cursor pointing at that activity,
and the next "show older" call sent that timestamp into ListCommentsBefore,
skipping every unreturned comment in between. GPT-Boy flagged this on
PR #2253 with the 80-comment / 30-activity scenario where 50 comments
became permanently unreachable.
The fix splits the cursor into independent comment and activity positions:
- timelineCursor carries (CommentT, CommentID, ActivityT, ActivityID).
encode/decode signatures changed accordingly.
- New cursorPos type and four bounds helpers (commentBoundsDesc / Asc,
activityBoundsDesc / Asc) extract per-pool oldest/newest from fetched
rows, with a carry fallback so empty pools advance past the input cursor
instead of resetting.
- All four mode handlers (latest, before, after, around) now derive cursors
from each pool's own bounds. Removed the entryTimestamp / entryID helpers
that re-parsed the merged entry slice.
Tests:
- TestTimelineCursor_RoundTrip pins the encode/decode contract for the new
dual-pool format (and rejects garbage input).
- TestListTimeline_PerPoolCursorWalksAllComments reproduces GPT-Boy's exact
scenario (30 activities older than 80 comments, limit=30) and asserts
every comment is reachable through repeated `before=<cursor>` walks.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Parent and child issues already render their identifier on the issue
detail page; only the issue you're viewing is missing one. Add it to
the breadcrumb between the parent identifier (when present) and the
title, matching the existing parent identifier styling.
Refs multica-ai/multica#2243
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(daemon): use brew prefix symlink for self-restart so Linux Cellar deletion does not orphan runtimes
After brew upgrade on Linux, os.Executable() resolves /proc/self/exe to
the Cellar path (e.g. .../Cellar/multica/0.2.9/bin/multica), which
brew cleanup deletes. The previous IsBrewInstall() short-circuit skipped
EvalSymlinks to 'preserve' the symlink, but on Linux there was nothing
to preserve - the path was already resolved.
Use cli.GetBrewPrefix() to resolve the stable symlink path
<brewPrefix>/bin/multica for brew installs. Fall back to
EvalSymlinks(os.Executable()) with a warning log when GetBrewPrefix()
returns empty (brew binary missing from PATH).
Introduce package-level function vars (isBrewInstall, getBrewPrefix) so
the daemon test can override them without modifying the cli package.
Closes#1624
* fix(daemon): harden brew-prefix fallback and document the WHY
When `brew --prefix` is unavailable but the binary is under a known Cellar
root, recover the prefix from cli.MatchKnownBrewPrefix and target
<prefix>/bin/multica instead of falling back to the resolved Cellar path
(which brew cleanup just deleted).
- Extract knownBrewPrefixes + MatchKnownBrewPrefix in cli/update.go and
reuse from IsBrewInstall to keep one source of truth for the install-root
list.
- Add a WHY comment above the brew branch in triggerRestart explaining the
/proc/self/exe -> Cellar -> deleted-by-brew-cleanup chain.
- Cover both fallback paths (matched / unmatched) in daemon_test.go.
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
* fix(cli): add --content-file / --description-file for non-ASCII on Windows
Windows PowerShell 5.1 (the Win11 default) and cmd.exe re-encode HEREDOC
content through the active console codepage before piping it to a child
process. Characters the codepage cannot represent are silently replaced
with `?`, so agents on Chinese Win11 hosts emitting `--content-stdin` /
`--description-stdin` HEREDOCs land all of their Chinese as `?` in the
issue body and comments. The daemon log shows the original Chinese
correctly because slog writes to a file directly, so the regression
hides until the user opens the issue page.
Add a `--content-file <path>` / `--description-file <path>` source to
`resolveTextFlag`: the CLI reads the file straight off disk, preserves
UTF-8 bytes verbatim, and skips the shell entirely. The runtime config
injected into AGENTS.md / CLAUDE.md now surfaces this as the canonical
Windows fallback when the daemon host runs on Windows; non-Windows hosts
keep the existing stdin/HEREDOC guidance untouched.
Closes#2198, #2236.
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): route every Windows-host stdin directive at --content-file
GPT-Boy on PR #2247 caught that the previous patch only inserted a Windows
fallback into the Available Commands section. Two later prompt surfaces
still hard-coded `--content-stdin` and overrode it for the agent:
- The Codex-specific paragraph in `buildMetaSkillContent`, which always
said "always use `--content-stdin` with a HEREDOC".
- `BuildCommentReplyInstructions`, which is re-emitted on every turn for
comment-triggered tasks (both via the AGENTS.md/CLAUDE.md workflow and
the daemon's per-turn prompt) and mandated the same HEREDOC pipe.
On Windows hosts we now branch both surfaces to a file-based template:
the agent writes the body to a UTF-8 file with its file-write tool and
posts via `--content-file <path>`. Non-Windows hosts keep the existing
stdin/HEREDOC guidance untouched.
Tests:
- `TestBuildCommentReplyInstructionsWindowsUsesContentFile` pins the
Windows / non-Windows reply-instruction text directly.
- `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` asserts that
the end-to-end CLAUDE.md / AGENTS.md surface for a comment-triggered
Windows task has no remaining `--content-stdin` directive that could
override the Windows fallback (covers Claude + Codex providers).
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): make Windows comment block file-first, pin tests by GOOS
GPT-Boy's second review on PR #2247 flagged two follow-up blockers:
1. The Windows comment/description block in `buildMetaSkillContent` was
"stdin first, file caveat appended" — agents on Windows still saw
"Agent-authored comments should always pipe content via stdin" /
"MUST pipe via stdin" / `--description-stdin` directives before
reaching the Windows fallback, so the contradicting instruction was
live in the same prompt. Rewrite the entire Available Commands
bullet for Windows hosts as file-first: the headline line names
`--content-file`, the bulleted rules name `--content-file` /
`--description-file`, and stdin only appears in anti-prescriptive
"do NOT pipe via …" prose.
2. The existing non-Windows tests (TestBuildCommentReplyInstructions
IncludesTriggerID, TestInjectRuntimeConfigDirectsMultiLineWritesToStdin,
TestInjectRuntimeConfigCodexEmphasizesStdinForFormattedComments,
TestInjectRuntimeConfigCommentTriggerUsesHelper) all depended on
`runtimeGOOS` defaulting to non-Windows; they would silently fail on
a Windows test runner. Pin them to `runtimeGOOS = "linux"` via
save+restore and drop t.Parallel so they don't race with the
GOOS-mutating Windows tests.
Test additions:
- TestInjectRuntimeConfigWindowsRecommendsContentFile now asserts the
Windows AGENTS.md does NOT contain prescriptive stdin phrasings
(`MUST pipe via stdin`, `use --description-stdin and pipe a HEREDOC`,
`<<'COMMENT'`, `Agent-authored comments should always pipe content via
stdin`, `always use --content-stdin`) on top of the file-first
positive assertions. The ban list pins prescriptive substrings, not
bare flag names, so anti-prescriptive prose like "do NOT pipe via
--content-stdin" doesn't trip the ban.
- TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin gets the same
expanded ban list across the Available Commands, Codex paragraph,
and per-turn reply template surfaces.
- The non-Windows side of TestInjectRuntimeConfigWindowsRecommendsContentFile
pins that the Linux stdin/HEREDOC contract is still in place, so a
future refactor can't accidentally move every host to file-first.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Both `apps/desktop/build/icon.ico` (Windows installer + Multica.exe) and
`apps/desktop/build/icon.png` (Linux deb/rpm/AppImage) were the default
electron-vite scaffold "atom" placeholder. They were never updated when
the macOS `icon.icns` was switched to the Multica asterisk in #1074, and
have shipped as-is in every v0.2.x release including v0.2.26 — closes
GitHub #2195.
Source: 1024×1024 PNG extracted from the existing build/icon.icns
(icon_512x512@2x), so all three platforms now share the same artwork.
- icon.ico: BMP frames at 16/24/32/48/64/128 + PNG-compressed 256×256.
Matches electron-builder's "≥256×256" requirement and the BMP-then-PNG
format mix Windows Explorer / NSIS render best across Win10/11.
- icon.png: 1024×1024 RGBA, replacing the previous 512×512 placeholder.
No electron-builder.yml change needed — buildResources: build picks
both files up automatically.
Co-authored-by: multica-agent <github@multica.ai>
The chat window used to fire two parallel session queries (active subset
+ full list) and surfaced them through two UI entry points (the title
dropdown + a History icon panel). The two caches drifted during the
WS-invalidate window — visible as "completed → reload → ghost row"
flickers — and the History toggle was a redundant entry into the same
underlying data.
Collapse to one cache (full list, ?status=all) and one entry point
(dropdown). The dropdown groups locally into Active / Archived; the
archived group is collapsed by default with a count, and per-row
delete moves into the dropdown via hover-revealed trash + confirm
dialog. Backend stays untouched: old desktop builds still hit
GET /chat-sessions without ?status and continue receiving the active
subset, so installed clients are unaffected.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Importing a skill from a github.com URL probes the commits API to
disambiguate slash-bearing refs. On self-hosted servers the IP is often
already over GitHub's 60-req/hour unauthenticated limit, so the very
first probe returns 403 and the previous code aborted the entire
import ("validating ref \"main/skills/pptx\": github API returned
status 403").
Two changes make this resilient:
* Forward GITHUB_TOKEN as a bearer token on every api.github.com request
via a new doGitHubAPIGet / addGitHubAuthHeader helper. With a token,
the limit becomes 5000 req/hour and the issue disappears entirely.
* When the API still returns 401/403/429 (no token, or limit exhausted
on the higher tier) treat the probe as indeterminate via
errGitHubAPIBlocked, keep trying remaining candidates, and finally
fall back to parseGitHubURL's optimistic single-segment split. This
covers the common case (single-word refs like "main") even when the
API is fully blocked. A warn log points operators at GITHUB_TOKEN.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(claude): add API Response Compatibility section
Narrows the existing "no backwards compat" rule to internal code only,
and adds a new section that codifies the defensive boundary at API
edges: parse-don't-cast, never pin UI to a single field, enum drift
must downgrade not crash.
Driven by #2143/#2147/#2192 — all three were the desktop client white-
screening on backend response shape changes the client wasn't built
against.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(core): add zod-based API response validation layer
Introduces a defensive boundary so a malformed backend response
degrades into a safe fallback (empty page, [], etc.) instead of
throwing inside React render.
- Adds zod to the pnpm catalog and as a @multica/core dependency.
- New parseWithFallback helper in core/api/schema.ts that runs
safeParse, logs a warn with the endpoint + zod issues on failure,
and returns the caller-supplied fallback. Never throws.
- Schemas in core/api/schemas.ts are deliberately lenient (string
enums kept as z.string() so unknown values still parse, optional
fields default, nested records use .loose() for unknown keys).
- Wires setSchemaLogger from CoreProvider so warnings flow through
the same logger as the rest of the API client.
This is the primitive — see the next commit for the call-site wiring.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(api): guard top 5 high-risk endpoints with parseWithFallback
Wraps the response of the five endpoints whose UIs white-screened in
past incidents (#2143/#2147/#2192) so a contract drift returns a safe
fallback instead of crashing the consumer:
- listIssues → ListIssuesResponseSchema, fallback { issues: [], total: 0 }
- listTimeline → TimelinePageSchema, fallback empty page
- listComments → CommentsListSchema, fallback []
- listIssueSubscribers → SubscribersListSchema, fallback []
- listChildIssues → ChildIssuesResponseSchema, fallback { issues: [] }
getIssue is intentionally NOT wrapped: there is no sensible "empty
issue" — the entire detail page depends on real fields. The page-level
ErrorBoundary (separate commit) catches that case.
Adds schema.test.ts with 9 cases covering the five failure modes
listed in MUL-1828: missing fields, wrong types, enum drift, null
body, and null arrays.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(ui): add ErrorBoundary and wrap high-risk pages
Section-level error boundary (no third-party dep — class component +
default fallback in @multica/ui). Supports a fallback render prop and
resetKeys for auto-recovery on resource navigation.
Wraps the surfaces that white-screened in past incidents:
- IssueDetail (web + desktop + inbox split-pane) — keyed on issueId
so navigating to a different issue clears the boundary automatically.
- IssuesPage (web + desktop).
Boundaries are placed at consumer call sites rather than inside
IssueDetail itself so we don't have to refactor the 1100-line
component, and so a crash inside one inbox split-pane doesn't take
down the inbox list next to it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(core): make all API schemas .loose() to preserve unknown fields
zod 4 z.object() defaults to STRIP, which silently drops fields the
schema didn't list. That makes the schema layer a sync point: a future
PR adding a TS field but forgetting the schema would have the field
disappear at runtime while TS still claims it exists — the exact bug-
class this PR is meant to prevent, just inverted.
Apply .loose() to every object schema (TimelineEntry, TimelinePage,
Comment, Issue, ListIssuesResponse, Subscriber, ChildIssuesResponse)
so unknown server-side fields pass through unchanged. Add a regression
test that feeds a payload with extra fields at both entry and page
level, and a direct unit test for parseWithFallback decoupled from any
endpoint. Update the listIssues fallback test to use a wrong-type
payload — under .loose() the previous "{ unexpected: true }" payload
parses successfully (every declared field has a default) instead of
triggering the fallback path it was meant to exercise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(claude): strip field-specific examples from API Compatibility section
The original wording embedded current schema field names (entries,
has_more_before, has_more_after, cursor, status, type) directly in the
rules. CLAUDE.md should state the rule, not the implementation — once a
field is renamed the doc drifts out of sync with the code, and the
specific names don't add anything the abstract rule doesn't.
Keep the rule, drop the field-level archaeology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): guard IME composition on Enter-to-submit handlers
Chinese/Japanese/Korean IMEs use Enter to commit a multi-key
composition. When that Enter also triggers a submit/create handler,
the form fires before the user has finished typing.
Add a shared `isImeComposing` predicate in @multica/core/utils that
checks both `nativeEvent.isComposing` and `keyCode === 229` (Safari
clears isComposing on the commit keydown but keyCode stays 229).
Apply the guard to every Enter→action handler in packages/views where
the input can hold IME text: workspace name, agent name/description,
skill name, label name/edit, mention suggestion picker, property
picker search, delete-workspace typed confirmation.
Tiptap submit-shortcut already guards via `view.composing`; left as is.
Skipped numeric/email/URL/file-path inputs where IME does not apply.
Co-authored-by: multica-agent <github@multica.ai>
* style(agents): align Escape handling with early return in inspector
Three onKeyDown handlers in agent-detail-inspector.tsx now follow the same
shape as labels-panel: handle Escape with an explicit return, then the IME
guard, then Enter submit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(timeline): include merge-truncation case in has_more_before (#2192)
Older comments became unreachable on issues where activity-log entries
crowded them out of the latest 50-entry page. The 'show earlier' button
was hidden and no cursor was emitted because the has_more_before formula
only caught the per-table SQL cap case and missed the in-memory merge
truncation case.
Reproduces with 48 comments + 49 activities, default limit 50: neither
table individually returns >= limit rows, but their sum (97) exceeds the
merged page size, so the merge silently drops 47 older comments. The old
formula reported has_more_before=false; the client never asked for page 2.
Fix: extract hasMoreBeyond(c, a, e, limit) with the missing third
disjunct - comments + activities > entries - applied uniformly to
listTimelineLatest / Before / After / Around.
Backwards compatible: API contract unchanged. Pre-cursor clients
(<=v0.2.25) still hit listTimelineLegacy and never read these fields.
Newer clients see has_more_before flip from 'wrongly false' to correctly
true/false - no field renames, no shape changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(issues): show count badge when activities are coalesced (#2192)
The timeline coalesces consecutive same-actor + same-action activities
within a 2-minute window so 48 status_changed entries don't take 48 rows.
The count badge was only rendered for task_completed / task_failed; for
status_changed (and every other action) the coalesced batch silently
collapsed to a single line with no hint that N entries were merged.
Add a coalesced_badge translation and render '×N' next to the activity
text whenever coalesced_count > 1, suppressing it on task_completed /
task_failed which already include the count in their translation copy.
This pairs with the backend fix for #2192: once the older-comments page
becomes reachable again, the activity rows above it should make the
density of the merged batch visible rather than misleading the user
into thinking only one event happened.
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(issues): add Copy local workdir path to issue menu
Surface the daemon-pinned task work_dir on the AgentTaskResponse and add a
"Copy local workdir path" action to the issue dropdown / context menu. The
action picks the most recent task with a recorded work_dir and writes it
to the clipboard so users can jump straight to the local execution
directory to inspect results.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): preserve user activation in Copy local workdir path
Move the task list subscription out of useIssueActions and into
IssueActionsMenuItems, where Base UI lazily mounts the menu content
only after the user opens the menu. The click handler now reads
straight from the cached query result and writes to the clipboard
synchronously, so the awaited fetch no longer drops the browser's
transient user activation when the cache is cold (e.g. opening the
context menu on an issue list row that hasn't pre-populated the
ExecutionLogSection cache).
Per Emacs PR review.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add `multica workspace update` to edit workspace metadata
Closes the CLI-side gap for #2178: the `PATCH /api/workspaces/{id}`
endpoint and TS client method already exist, only the CLI subcommand
was missing. Supports partial updates of name, description, context,
and issue_prefix; long fields accept stdin via `--description-stdin` /
`--context-stdin`. `slug` stays immutable, `settings`/`repos` are out
of scope (deferred). Empty PATCH is rejected locally so we don't fire
a no-op `EventWorkspaceUpdated` broadcast. Permission gate is
unchanged (server-side admin/owner middleware).
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): address review on workspace update command
- Reject `--issue-prefix ""` (and whitespace-only) explicitly. The
server handler silently skips empty prefixes, so the previous
behavior was a 200 OK with no actual change — exactly the kind of
invisible no-op Emacs flagged in review.
- Restore the `## Issues` H2 in the zh CLI reference. The earlier
edit dropped it, leaving issue commands nested under the Workspaces
section.
Co-authored-by: multica-agent <github@multica.ai>
* docs(cli): list `workspace update` in the en + zh top-level reference
Mirrors the existing zh-only entry under apps/docs/content/docs/cli/
into the English overview so the new command is discoverable from
both locales.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Archiving the currently selected inbox item used to clear the selection
and leave the detail panel empty, forcing the user to click the next
item to keep going. Pick the next (older) item from the deduplicated
list, falling back to the previous (newer) one when archiving at the
bottom, and only clear when nothing is left.
Route the detail panel's onDone path through the same handleArchive so
the auto-select behavior is shared.
Co-authored-by: multica-agent <github@multica.ai>
PR #2101 swapped the openclaw runtime adapter from reading --json on
stderr to stdout. That fixed openclaw 2026.5+ but inverted the breakage
for pre-2026.5 builds — those still write JSON to stderr, so the
adapter now sees an empty stdout and falls through to the same
"openclaw returned no parseable output" failure that 2026.5+ users
saw before #2101.
Add a per-task version gate inside openclawBackend.Execute that runs
`openclaw --version`, parses the dotted version, and rejects anything
below 2026.5.5 with a hardcoded upgrade hint:
openclaw <detected> is below the minimum supported version 2026.5.5.
Run `openclaw update` to upgrade and try again.
The check is intentionally per-task and uncached so users who upgrade
do not need to restart the daemon — the next task automatically
re-checks. ~20ms per task is negligible vs. the typical run.
Co-authored-by: multica-agent <github@multica.ai>
Multica's openclaw runtime adapter has been reading agent output from
stderr since the early openclaw integration days. Current openclaw
(2026.5.5, c37871e) writes its --json blob exclusively to stdout:
$ openclaw agent --local --json --agent main --message 'say hi' >stdout 2>stderr
STDOUT bytes: 27401
STDERR bytes: 0
Result: every successful turn was followed by a daemon-generated system
comment 'openclaw returned no parseable output', visible to users,
looked like the agent broke when it didn't. Reproduced live on WOR-2,
turn at 2026-05-05 16:35 UTC; daemon log confirmed the full result JSON
arrived on the [openclaw:stdout] debug channel and was discarded while
the empty stderr pipe hit the no-events fallback.
Changes
- server/pkg/agent/openclaw.go: swap pipes, StdoutPipe() for the JSON
stream, cmd.Stderr = newLogWriter(...) for log overflow. Cleanup
goroutine now closes stdout on cancel. Comments and the read-error
errMsg updated to reflect the new pipe.
- server/pkg/agent/openclaw_test.go: TestOpenclawProcessOutputReadError
asserts on 'read stdout' (was 'read stderr'), string-only fix,
no behavior change. New TestOpenclawProcessOutputStdoutFixture feeds
a recorded openclaw 2026.5.5 --json blob through processOutput and
asserts result + messages parse cleanly.
- server/pkg/agent/testdata/openclaw-2026.5.5-stdout.json: 27401-byte
fixture captured fresh from the openclaw CLI for the regression test.
Side effects (net positive)
- Log lines openclaw writes to stderr (security warnings, tool errors)
now show up under [openclaw:stderr] instead of being silently consumed
by the JSON parser.
- Daemon's success_pattern heuristic (empty-output -> 'blocked')
becomes meaningful again because result.Output actually populates.
Closes WOR-10.
* fix(skills): drop SKILL.md content from list endpoints (#2174)
`GET /api/skills` and `GET /api/agents/{id}/skills` were SELECT *'ing the
skill row and shipping the full SKILL.md `content` blob to every caller.
SKILL.md bodies routinely run 50–200KB each, so a workspace with 30–40
skills returned multi-megabyte JSON arrays — past the CLI's 15s timeout
on high-latency links and locking out non-US users entirely.
Add `ListSkillSummariesByWorkspace` / `ListAgentSkillSummaries` sqlc
queries that omit `content`, plus a dedicated `SkillSummaryResponse`
wire shape so the contract is explicit (versus stuffing
`Content: ""` back into the existing struct). Detail endpoints
(`GET /api/skills/{id}`, agent CRUD return values) keep returning the
full body.
`AgentResponse.skills` and the matching TS `Agent.skills` now use
`SkillSummary[]` — frontend list/columns code already only read
id/name/description/config.origin, so the type narrowing matches actual
usage and prevents new code from accidentally depending on a content
field that won't be there.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agents): narrow embedded skills to AgentSkillSummary; gofmt agent.go
GPT-Boy review of #2180: the previous commit typed AgentResponse.Skills as
[]SkillSummaryResponse, but the agent list batch query
(ListAgentSkillsByWorkspace) only joins agent_id/id/name/description, so
the wider type left workspace_id/config/created_at/updated_at as zero
values. Define a dedicated AgentSkillSummary {id,name,description} that
matches what the batch query actually returns and what the frontend
actually reads (`agent.skills.map(s => s.name|s.id)`); the standalone
GET /api/agents/{id}/skills endpoint keeps SkillSummaryResponse for
callers that need the source/origin info.
Switch GetAgent's per-agent skills load from ListAgentSkills (full Skill
rows including content) back to ListAgentSkillSummaries to avoid reading
SKILL.md bodies just to discard them.
Re-run gofmt on agent.go to fix the field-tag alignment that drifted when
Skills changed type.
Co-authored-by: multica-agent <github@multica.ai>
* docs(types): correct SkillSummary JSDoc — Agent.skills is AgentSkillSummary[]
GPT-Boy spotted on review: comment said SkillSummary was "embedded in
Agent.skills", but that field is now AgentSkillSummary[]. Re-point the
reader at the right type to avoid future confusion.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
Markdown links like `[xx](/workspaces)` written in `*.zh.mdx` rendered
as bare `<a href="/workspaces">`, which Next's basePath rewrote to
`/docs/workspaces` and the docs middleware then routed to English —
silently kicking Chinese readers out of their locale on every internal
click.
Add a `LocaleLink` MDX `a` override that runs every internal href
through `prefixLocale(href, lang)` before passing it to `next/link`, and
wire a `DocsLocaleProvider` around the MDX body in both page entry
points so the override and `NumberedCard` know the active locale.
External links, in-page anchors, relative paths, already-prefixed
paths, and default-language pages are deliberately left untouched.
Closes the bug reported in https://github.com/multica-ai/multica/issues/2173.
Co-authored-by: multica-agent <github@multica.ai>
* feat(create-issue): add border beam to "switch to agent" button
Draws the eye to the manual→agent affordance so users discover quick
capture mode. Adds a reusable .border-beam utility (conic-gradient ring
on ::before, driven by an @property-animated angle) and applies it to
the switch-to-agent button alongside a brand-tinted background tint and
a hover icon flip. Honors prefers-reduced-motion.
Co-authored-by: multica-agent <github@multica.ai>
* style(border-beam): switch to magic-ui colorful palette
Replaces the single brand-color sweep with a rainbow trail
(#ffbe7b → #ff777f → #ff8ab4 → #a07cfe → #5b9dff), matching the
`colorVariant="colorful"` look from magic-ui's border-beam reference.
Static fallback under prefers-reduced-motion uses the same palette as a
linear gradient.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
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>
#2128 changed GET /api/issues/:id/timeline from a bare TimelineEntry[] to
a wrapped { entries, next_cursor, ... } object. Multica.app ≤ v0.2.25 still
in the wild reads the response body as TimelineEntry[] directly, so the
moment v0.2.26 backend rolled out, every old desktop hit
"timeline.filter is not a function" on any issue open — bug reports landed
within ten minutes of the v0.2.26 release (#2143, #2147).
The new client always sends ?limit=..., so absence of every pagination
param uniquely identifies a legacy caller. Detect that at the top of
ListTimeline and serve the old shape (ASC, []TimelineEntry, capped at 200)
through a dedicated listTimelineLegacy helper. New clients fall through
unchanged.
A new TestListTimeline_LegacyShapeForPreCursorClients pins the contract
(array shape, ASC order, "[]" not "null" on empty issues). Two existing
tests that used the empty query string have been updated to send
?limit=50, since the empty form is now reserved for the compat path.
The legacy branch can be deleted once desktop auto-update has rolled the
user base past v0.2.26.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(landing): align ZH copy with conventions and update tool list to 11
- Replace "Agent" with "智能体" in ZH marketing copy (lines 1-275) per
conventions.zh.mdx — landing was the only surface still using "Agent"
while UI, docs, and locales already use "智能体". Changelog-section
technical names (Agent SDK / Agent runtime / Cursor Agent) preserved.
- Replace the 4-tool list (Claude Code / Codex / OpenClaw / OpenCode)
with the actual 11 supported tools across hero card, how-it-works
step, and FAQ — this matches daemon-runtimes.mdx and the file's own
changelog entries that already record the rollout of Cursor, Copilot,
Gemini, Hermes, Kimi, Kiro CLI, and Pi.
- Drop the "plug in and go" line; replace with an honest sentence about
multica setup walking through OAuth + daemon start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(i18n): correct daemon/runtime drift across modals, onboarding, docs
- modals/zh-Hans: 4 places used "daemon" untranslated; conventions.zh.mdx
rules Daemon -> 守护进程. Aligned.
- onboarding/zh-Hans: line "把任务交给它们" was the only spot using "任务"
for the task entity; rest of the file already uses lowercase "task"
per conventions. Aligned.
- onboarding (en + zh-Hans) runtime_aside.what_suffix: said runtime IS
a background process. daemon-runtimes.mdx defines runtime = daemon ×
one AI coding tool (one machine + N tools = N runtimes). Replaced with
the correct definition so new users form the right mental model on
first contact.
- onboarding (en + zh-Hans) step_platform headline+lede: said "Connect a
runtime" but the next options are "install desktop / CLI / cloud
waitlist" — those install a runtime source, not connect to one.
Reworded.
- onboarding/zh-Hans: 4 places used "AI 编码工具"; docs use "AI 编程工具"
consistently. Unified on the docs term.
- daemon-runtimes (en + zh): added cross-link to /desktop-app for users
deciding between desktop daemon and CLI daemon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): localize starter-content (Getting Started project)
The Getting Started project + welcome issue + 10 sub-issues that land in
the workspace at the end of onboarding were hardcoded English. Chinese
users finished a Chinese onboarding flow and arrived to an all-English
workspace; the welcome issue's prompt to the agent was also English, so
the agent's first reply tended to be English regardless of what
templates the user picked.
This commit adds Chinese parity, fixes the runtime definition error
that was the source of similar drift in onboarding.json, and removes a
few hardcoded UI specifics that would silently rot.
Architecture:
- Long-form markdown (~600 lines per language) lives in TS sibling
files: starter-content-content-en.ts and starter-content-content-zh.ts.
JSON locales were considered, but multi-paragraph markdown becomes
unreadable single-line escape soup in JSON; keeping it in TS lets
reviewers see the rendered shape and catch markdown regressions in
code review.
- starter-content-templates.ts is now a thin orchestrator: imports both
content files, exports buildImportPayload({ ..., locale }), picks the
right one at runtime.
- StarterContentPrompt resolves locale from i18n.language (with a small
startsWith("zh") helper so "zh-Hans-CN" or future variants still hit
the ZH content).
Content fixes (apply to both EN and ZH):
- "A runtime is a small background process" was wrong (runtime = daemon
× one AI coding tool, per docs). Replaced with the correct definition
so the welcome agent doesn't seed an incorrect mental model.
- Removed hardcoded "tabs at the top: 6 tabs" / "(third row)" /
"6 templates" lists — those rot the moment product UI changes. Replaced
with descriptions that don't depend on exact counts/positions.
Conventions adherence (ZH):
- agent → 智能体, daemon → 守护进程, runtime → 运行时, workspace → 工作区
- task / issue / skill stay lowercase English (per conventions.zh.mdx)
- Product UI labels (Properties, Assignee, Status, Activity, Live card,
Inbox, Members, Settings, Runtimes, Configure, Repositories,
Instructions, Tasks, Skills, Autopilot, etc.) stay English so the
doc text matches what the user sees on screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(conventions): formalize mixed-rule for task / issue / skill in CN
The prior rule said issue/skill/task always render as lowercase English
in Chinese text. That worked for UI strings but never matched what the
sister docs actually do — tasks.zh.mdx is built around "执行任务",
issues.zh.mdx titles "Issue 与 project", skills.zh.mdx titles "Skills".
Three docs, three patterns, all sensible in their own context, none
matching the old rule. Conventions also explicitly cited the docs as
the voice standard, so the rule was internally inconsistent.
This commit promotes the de facto pattern to a written rule:
- UI strings, state names, code references → lowercase English
("排队中的 task", "创建子 issue", "为智能体注入 skill")
- Doc titles / section headings → Title-case English OR Chinese term
("Issue 与 project", "Skills", "执行任务")
- Doc prose where the entity is the running subject → Chinese term,
with English in parentheses on first mention
("**执行任务**(task)是智能体每一次工作的单位")
- API / DB fields → always task / issue / skill (`task_id`, etc.)
Provides the term mapping (task ↔ 执行任务) explicitly so future
translation PRs don't have to rediscover it.
No code or other doc changes — tasks.zh.mdx already follows this
pattern; this commit just formalizes it. Other ZH locale strings
remain lowercase per the UI rule (which the locale audit + PR #2139
verified).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add Projects page (en + zh) and Autopilot failure visibility note
The audit found that 'projects' was the most prominently missing docs
page — it appears as a sidebar nav item in onboarding's workspace
preview, but users clicking through to docs found nothing on the topic.
The other locale-but-no-doc pages (my-issues, labels, settings) are
listed as follow-ups; this PR ships the highest-impact one.
Also adds a missing piece in tasks.{mdx,zh.mdx}: the Autopilot
no-auto-retry callout explained the *why* but never the *how do I
notice* — added a sentence pointing users at Inbox + the issue
status revert + the Autopilot page's run history.
projects.mdx covers:
- What a project is (container for related issues)
- Fields: name, icon, description, lead, status, priority, progress
- Project-issue many-to-one relationship + how progress is computed
- Pinning to sidebar (personal preference)
- Resources section (GitHub repos passed to daemon)
- Delete behavior (issues unlinked, not deleted)
- Lead can be a member or an agent
Both pages registered in meta.json / meta.zh.json under "Workspace &
team" group, between issues and comments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(pr-template): add drift-prevention checkboxes for runtime/CN copy
Two failure modes the docs+onboarding audit found, both caused by
adding-a-thing without remembering all the places that thing surfaces:
1. New runtime / coding tool / UI tab gets recorded in changelog but not
in landing FAQ ("Multica supports 4 tools" while changelog shows the
11th was added) or starter-content tutorial ("6 tabs at the top:
Instructions / Skills / Tasks / Environment / Custom Args / Settings"
stays frozen the moment a tab is added or renamed).
2. Chinese copy added without checking the canonical glossary —
"Agent" survived in landing/zh.ts long after product UI standardized
on "智能体" because nobody routed landing through the conventions
review.
Adding two checklist items to the PR template so authors see the
specific paths to update at PR-creation time, before the drift ships.
This is the final batch (5 / 5) from the audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>