mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 14:09:22 +02:00
v0.2.29
781 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
807201086c |
perf(issues): stop full timeline re-render on every WS event (#2329)
* perf(issues): stop full timeline re-render on every WS event (MUL-1941) Two compounding causes made every Comment/reply WS event re-render every sibling thread on the issue detail page — visible during AI streaming as a flash across all 10 nested replies under a parent and as the green reply-input losing its draft. 1) `useCreateComment.onSettled` invalidated the timeline query, forcing a full `GET /timeline` refetch on every comment submit. The response replaced every entry's reference even when the content was unchanged, poisoning every downstream React.memo. The `comment:created` WS broadcast already keeps the cache fresh and `useWSReconnect` invalidates on disconnect, so the redundant refetch had no upside. Drop it. 2) The `timelineView` useMemo passed the full `repliesByParent: Map` to every CommentCard. Each WS event rebuilt the Map (new ref), so React.memo on CommentCard fell back to a re-render for *every* card, not just the one whose thread changed. Replace the Map prop with a per-thread `replies: TimelineEntry[]` slice, precomputed once via `collectThreadReplies` and stabilized against the prior render — when a thread's flat list is shallow-equal to last time, reuse the previous array reference so unrelated cards keep their memo. ResolvedThreadBar gets the same `replies` prop, so the collapsed count + author list still match the expanded view without re-walking the graph. Verified: pnpm typecheck + pnpm test for @multica/views and @multica/core (334 + 214 tests, all passing). Co-authored-by: multica-agent <github@multica.ai> * fix(realtime): mark timeline stale without refetching active queries (MUL-1941) Per GPT-Boy's review on PR #2329: dropping `useCreateComment.onSettled`'s invalidate wasn't enough. The global `useRealtimeSync` runs in WSProvider for the lifetime of the app and re-invalidates the timeline on every `comment:created` / `comment:updated` / `comment:deleted` / `comment:resolved` / `comment:unresolved` / `activity:created` / `reaction:added` / `reaction:removed` event. With `staleTime: Infinity` on the QueryClient default, the active timeline query refetches on every invalidate — replacing every entry's reference and busting the per-thread memoization the prior commit just put in place. Switch the global handler's `invalidateQueries` to `refetchType: "none"`. Active observers now stay fresh via the granular `setQueryData` handlers in `useIssueTimeline`; inactive issues' caches are still marked stale, so when IssueDetail mounts later, `refetchOnMount` triggers a fresh fetch the same way it did before. `comment:resolved` / `comment:unresolved` previously had no granular handler — only the global invalidate kept the cache in sync. Add useWSEvent handlers in `useIssueTimeline` that replace the matching entry via `commentToTimelineEntry`, and extend that helper to carry the resolved_at / resolved_by_type / resolved_by_id fields so resolved state survives the round-trip (it was silently dropped on every `comment:updated` too — fixed as a side effect). Tests: 3 new cases covering resolved / unresolved / cross-issue isolation in the timeline hook. All 337 + 214 unit tests + full monorepo typecheck pass. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
1d7aaf582c | fix(editor): avoid parsing JSON and large text paste (#2301) | ||
|
|
003dfd9b4b |
feat(quick-create): add project picker that remembers last pick (#2321)
* feat(quick-create): add project picker that remembers last pick Quick-create users targeting one project repeatedly had to restate "in project X" in every prompt. The modal now exposes a project picker beside the agent picker, persists the selection per-workspace, and pins the agent's `multica issue create` invocation to that project so the prompt text doesn't have to. The picked project also flows to the daemon as ProjectID/ProjectTitle and its github_repo resources override the workspace repo fallback — same treatment issue-bound tasks already get. Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): move project picker into property pill row Reviewer feedback: the picker felt out of place wedged next to the agent header. Move it into a property toolbar row above the footer, reusing the shared `ProjectPicker` + `PillButton` so its placement and styling line up exactly with the manual create panel. This also drops the bespoke dropdown / aria / label strings that were only needed while the picker rendered inline beside "Created by". Co-authored-by: multica-agent <github@multica.ai> * fix(quick-create): clear stale persisted project + carry across mode switch Two review-blocking bugs in PR #2321: 1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0 and only cleared local state, leaving lastProjectId pointing at a deleted project. The next open re-seeded the dead UUID and submit hit the server's `project not found` rejection. Gate on the query's `isSuccess` so we can tell "loading" apart from "loaded as empty", and clear both local state and the persisted preference when the selection isn't in the resolved list. 2. ManualCreatePanel's switchToAgent dropped the picked project from the carry payload, so flipping manual → agent silently fell back to the agent panel's own lastProjectId — potentially routing the issue to a different project than the one shown in manual mode. Forward project_id alongside prompt / agent_id, and add a regression test. Co-authored-by: multica-agent <github@multica.ai> * test(quick-create): pass new isExpanded props in stale-project tests Main got an expand button on AgentCreatePanel via #2320 while this branch was open, adding `isExpanded` / `setIsExpanded` to the panel's required props. The two new stale-project tests still passed `{ onClose }` only, which CI's typecheck (run on the main+branch merge) caught while my local run did not. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3f20999597 |
refactor(timeline): drop server-side comment + timeline pagination (#2322)
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929) The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <github@multica.ai> * fix(timeline): address review feedback on pagination removal Three issues caught in PR #2322 review: 1. /timeline broke for stale clients between #2128 and this PR. They send ?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema; the new flat-array response was failing schema validation and falling back to an empty timeline. Restore the wrapped shape on those query params (DESC entries, null cursors, has_more_*=false), keeping the flat ASC array for bare requests. Around-mode now also fills target_index from the merged slice so legacy clients can still scroll-to-anchor without a follow-up. 2. The agent prompts in runtime_config.go and prompt.go still told agents that `multica issue comment list` accepts --limit/--offset and to use `--limit 30` on truncated output. With those flags removed in this PR, new agent runs would hit "unknown flag" or skip context. Update the prompt copy to "returns all comments, capped at 2000; --since for incremental polling". 3. useCreateComment's onSuccess was a bare append to the timeline cache with no id-dedupe, so a fast comment:created WS event firing before onSuccess produced a transient duplicate. Restore the id guard the old prependToLatestPage helper used to provide. Adds two new boundary tests: - TestListTimeline_LegacyWrappedShape_OnPaginationParams - TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex Co-authored-by: multica-agent <github@multica.ai> * test(handler): fix timeline test assertions for handler-package isolation The TestListTimeline_* assertions assumed CreateIssue would seed an "issue_created" activity_log row, but the activity listener that publishes those rows is registered in cmd/server/main.go — handler-package tests don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the test expected ≥6. Drop the auto-activity assumption: assert exactly 5 entries in TestListTimeline_MergesCommentsAndActivities, and tighten TestListTimeline_EmptyIssue to assert a fully-empty timeline. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fd3cb4e5b3 |
feat(modals): add expand button to agent create dialog (#2320)
Mirrors the manual create panel's expand affordance so the agent panel can grow to the same wider footprint when the user wants more room for a long prompt or pasted screenshots. Expand state is shared across modes via the shell, so the user's preference persists when toggling between agent and manual. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4872dc50bd |
fix(priority): align dropdown badge colors with PriorityIcon semantic tokens (#2315)
The priority badge in the issue/project priority picker dropdown used a parallel `bg-priority` orange color family (with opacity gradient for level intensity), while the standalone PriorityIcon outside the dropdown used semantic tokens — destructive for Urgent, warning for High/Medium, info for Low. The two languages produced an inconsistency users noticed most clearly on Low: blue in the list, orange in the picker. Switch the dropdown badges to the same semantic tokens as the icon, and remove the now-unused `--priority` / `--color-priority` design token from both `packages/ui/styles/tokens.css` and `apps/web/app/custom.css`. Closes multica-ai/multica#2289 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f922673463 |
feat(execution-log): one-click retry for failed/cancelled tasks (#2313)
* feat(execution-log): add one-click retry for failed/cancelled tasks (MUL-1922)
Adds a Retry icon button to past-run rows in the issue execution log so
users can re-enqueue failed or cancelled tasks without leaving the page.
The button calls POST /api/issues/{id}/rerun (already exposed by the CLI
issue rerun command) which cancels any prior task on the assignee and
spawns a fresh task with a new agent session.
Co-authored-by: multica-agent <github@multica.ai>
* fix(execution-log): reset retry button state on rerun success
The previous handler only reset `retrying` on error, but the past row
stays mounted (its `task.id` is unchanged) after a successful rerun, so
the Retry button hovered into a permanent spinner. Move the reset into
a finally block so both paths clear the loading state.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
73b401d47a |
i18n(views): translate workspace slug error messages (#2312)
The slug_reserved error introduced in #2228 was hardcoded English, and the older inline format/conflict errors in step-workspace.tsx had the same problem. Move all of them to the workspace + onboarding locale namespaces (en + zh-Hans) and drop the now-unused string constants from slug.ts. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c926dfe44b | fix(views): validate workspace slug against reserved ones when creating (#2228) | ||
|
|
c3832302b9 |
fix(transcript): expand long single-line Agent messages (multica#2282) (#2308)
Agent text rows in the run-records dialog only got a chevron when the message had a newline; a long single-line reply was rendered with truncate and the trailing content was unreachable. Other event types (tool_use, tool_result, thinking, error) are expandable on any non-empty content — bring text in line. Also lead the collapsed summary with the first non-empty line instead of the last, so multi-paragraph replies preview the lede rather than the closing remark and the row stays stable while messages stream. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0cd50e14eb |
feat(agent-live-card): show queued tasks in issue live banner (MUL-1897) (#2307)
The issue-detail "agent live" banner only showed dispatched/running tasks.
A task that was queued — runtime offline, busy on a prior task, or held
behind a coalesced sibling — left the issue silent until claim, which
reads as "the trigger never landed".
Include 'queued' in `ListActiveTasksByIssue`, then branch the renderer:
queued banners use a non-spinning Clock, "{name} 排队中 / is queued"
copy, "queued for Ns" elapsed anchored on `created_at`, and hide the
transcript button (no execution log yet). Cancel still works because
`CancelAgentTask` already accepts queued.
Client-side re-sort by lifecycle (running → dispatched → queued) so the
sticky slot stays on the most-active task even when a queued sibling
was created more recently.
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
ce00e05169 |
Add canonical PostHog core metrics events (#2302)
* Add canonical PostHog core metrics events Co-authored-by: multica-agent <github@multica.ai> * Address analytics review feedback Co-authored-by: multica-agent <github@multica.ai> * Tighten analytics review follow-ups Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bb3d2b70ea |
fix(ui): let DropdownMenu popup size to content (#2306)
DropdownMenuContent had `w-(--anchor-width)` which locks the popup width to the trigger. With icon-sm kebab triggers (~32px) the popup was clamped by `min-w-32` to 128px, and longer items like "Unresolve thread" / "标记为已解决" wrapped onto two lines. Anchor-width matching is the right behavior for Select / Combobox (both keep that class), but a generic kebab menu should size to its own content. Drop the `w-(--anchor-width)` and keep `min-w-32` as the floor. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bf186504b0 |
fix(timeline): sync around state on falsy prop transitions (#1968 follow-up) (#2230)
When the inbox split-pane is open and the user clicks a comment-notification
for issue X, then a non-comment notification for the SAME issue (status,
assignment, sub-issue), <IssueDetail> stays mounted (keyed on issueId in
inbox-page.tsx so composer drafts and scroll position survive). The hook's
internal `around` state has to react to the prop transitioning back to falsy
— otherwise the around-mode cache is re-served on every subsequent click and
entries outside the original window appear "lost" until a hard refresh.
The truthy guard on the effect skipped the falsy branch:
useEffect(() => {
if (options.around) setAround(options.around); // ← skipped on null
}, [options.around]);
Replace it with an unconditional sync. useState's initialiser already covers
the mount-time read; the effect now covers all subsequent prop transitions
including → null.
Adds a regression test that asserts the hook re-keys useInfiniteQuery on the
truthy → undefined transition.
Co-authored-by: Sara <sara@sara.local>
|
||
|
|
3b3be9d7bd |
feat(comments): resolve threads with collapsible bar (MUL-1895) (#2300)
* feat(comments): resolve threads with collapsible bar (MUL-1895)
Adds a Linear-style resolve action on comment thread roots. Resolved
threads collapse to a single "N resolved comments from X" bar in the
activity feed; clicking expands the thread inline (per-session, not
persisted). Replying inside a resolved thread auto-unresolves it.
Backend
- migration 069: resolved_at, resolved_by_type, resolved_by_id on comment
- sqlc ResolveComment / UnresolveComment queries (idempotent via COALESCE)
- POST/DELETE /api/comments/{id}/resolve handlers, root-only validation
- CreateComment auto-clears resolved_at when a reply lands in a resolved
thread, publishing comment:unresolved
- comment:resolved / comment:unresolved events; CommentResponse and
TimelineEntry both surface the new fields
Frontend
- Comment + TimelineEntry types extended; payloads typed; WS sync wired
- useResolveComment optimistic mutation with rollback
- ResolvedThreadBar component for the collapsed view
- Resolve / Unresolve menu items on root comments; Collapse strip on the
expanded resolved card
- en + zh-Hans locale strings
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): cover agent reply path, expand-state hygiene, nested counts (MUL-1895)
Addresses three review issues from Emacs on PR #2300:
1. TaskService.createAgentComment bypasses Handler.CreateComment, so the
auto-unresolve wired into the handler did not fire when an agent replied
in a resolved thread (task / mention / on_comment paths). Extracted the
logic to TaskService.AutoUnresolveThreadOnReply so both reply paths share
it; rewired Handler.CreateComment to call the new method.
2. Resolving an already-expanded thread no longer collapses it back to the
bar because expandedResolved still contained the id. Added
clearResolvedExpand + handleResolveToggle wrapper so resolve / unresolve
always wipe the session expand entry.
3. ResolvedThreadBar received only direct children, while CommentCard's
expanded view recurses through descendants. Extracted the recursive
walk into thread-utils.collectThreadReplies and called from both —
counts and author lists now match.
Co-authored-by: multica-agent <github@multica.ai>
* test(comments): mock useResolveComment + add zh-Hans plural key
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
bf0665a1a8 |
fix(desktop): copy issue link reflects connected env, not localhost (#2298)
* fix(desktop): derive appUrl from apiUrl in dev so copy-link follows the connected env Local desktop dev was hardcoding appUrl to http://localhost:3000, so the "Copy issue link" output pointed at localhost even when the renderer was connected to a remote (e.g. test) backend — the resulting URL only worked on the developer's machine. - runtime-config dev path now mirrors the production loader: when VITE_APP_URL is unset, derive appUrl from apiUrl (host-only). The localhost api host is special-cased to keep the local web port (3000), while a remote api host (api.test.x) yields a remote appUrl. - Web navigation adapter now implements getShareableUrl directly with window.location.origin instead of leaving it undefined. - NavigationAdapter.getShareableUrl is now required; copyLink callers drop the window.location fallback branch and call it unconditionally. - Add the missing getShareableUrl mock in issue-detail.test.tsx. Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): strip leading api. label when deriving appUrl Address Emacs' code review on PR #2298. The previous derivation kept the api hostname unchanged, so VITE_API_URL=https://api.test.multica.ai produced appUrl=https://api.test.multica.ai — not the env's actual web URL. Multica's convention exposes the api at api.<web-host>; strip that leading label (when the host has at least 3 labels, to avoid mangling short hosts like api.local) so a single api configuration produces the correct shareable web origin. - api.multica.ai → multica.ai - api.test.multica.ai → test.multica.ai - api-staging.x.com → unchanged (no leading "api." label) - congvc-x99.ts.net → unchanged Update both the dev and production tests; also fix the existing runtime-config-loader test that asserted the unstripped value. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
bda475cbba |
refactor(reserved-slugs): single JSON source for backend + frontend (#2148)
Reserved workspace slugs lived in two parallel files (`workspace_reserved_slugs.go` and `packages/core/paths/reserved-slugs.ts`) with no parity check. Adding or renaming a global route on one side without the other would slip through CI and surface only when a real user hit the collision. Collapse the two lists into one source: `server/internal/handler/reserved_slugs.json`. Go embeds the JSON via `//go:embed` and parses it at package init; the TS file is regenerated by `scripts/generate-reserved-slugs.mjs` (run via `pnpm generate:reserved-slugs`). CI re-runs the generator and `git diff --exit-code`s the TS output, so a stale TS file cannot land. The slug set is unchanged (87 entries, byte-equivalent slug literals). Update CLAUDE.md to describe the new "edit JSON, run generator" workflow. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fe8326fa0c |
feat(agents): add search box to skill picker dialog (#2269)
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> |
||
|
|
0b64f09c12 |
fix(runtimes): exclude archived agents from counts (#2166)
* fix(runtimes): exclude archived agents from counts * test(runtimes): align workload fixture with shared types |
||
|
|
b1d874ef50 |
fix(timeline): rescue orphaned replies + bump page size to 50 (#2263)
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> |
||
|
|
bbbbcf9b6e |
fix(timeline): make Show older / Show newer affordances clearly clickable (MUL-1858) (#2257)
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> |
||
|
|
161194b86f |
fix(timeline): exclude activities from comment page budget (#2253)
* 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> |
||
|
|
14ab487c95 |
feat(issues): show identifier in detail page breadcrumb (#2244)
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> |
||
|
|
47aa32a04d |
refactor(chat): unify session list into single dropdown with grouped active/archived (#2220)
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> |
||
|
|
d82a2d8a04 | feat(skills): support importing skills from github.com URLs (#2209) | ||
|
|
48e3131bf9 |
feat: harden desktop frontend against API response drift (MUL-1828) (#2208)
* 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> |
||
|
|
dce51e3a27 |
fix(views): guard IME composition on Enter-to-submit handlers (#2207)
* 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> |
||
|
|
099dda0603 |
fix(timeline): include merge-truncation case in has_more_before (#2192) (#2204)
* 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> |
||
|
|
fe956fc670 |
feat(issues): add Copy local workdir path to issue menu (#2196)
* 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> |
||
|
|
f9cdd487e0 | fix(projects): pre-fill the status and project to match the parent issue when creating sub-issue (#2177) | ||
|
|
d07c7c2a15 |
feat(inbox): auto-select next item after archiving the selected one (#2190)
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> |
||
|
|
9c00ecfdb4 |
fix(issues): blur sticky agent live card (#2170)
* fix(issues): blur sticky agent live card * fix(issues): drop inner live-card blur * fix(issues): match sticky live-card radius |
||
|
|
d0ac67dea2 |
fix(skills): drop SKILL.md content from list endpoints (#2180)
* 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> |
||
|
|
c3ddb57b82 |
feat(create-issue): add border beam to switch-to-agent button (#2157)
* 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> |
||
|
|
d16c48172a |
fix(projects): pre-fill project on per-status "+" create-issue (#2155)
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>
|
||
|
|
32740d0ee3 |
docs+i18n: fix terminology/runtime drift across landing, onboarding, docs (#2146)
* 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> |
||
|
|
c784a6a9ee |
feat(chat): copy assistant reply + collapse process into a single outer fold (#2151)
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> |
||
|
|
9306d60451 |
fix(agent-live-card): self-heal stale 'is working' banner via reconcile (#2142)
* 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> |
||
|
|
4a749f103b |
docs(views): explain min-h-[60vh] mobile fallback in agent overview pane (#2061)
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. |
||
|
|
e3dd31cbe5 |
feat(notifications): add system notifications toggle in settings (#2132)
* 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> |
||
|
|
5cf1d01076 |
feat(settings): rename Appearance tab to Preferences and persist active tab in URL (#2131)
- 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> |
||
|
|
6d59505575 |
fix(quick-create): remove duplicate keyboard shortcut on agent submit button (#2130)
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> |
||
|
|
58db751089 |
ci(lint): enable lint in CI + fix existing lint debt (#2129)
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> |
||
|
|
ba147708a6 |
fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) (#2128)
* 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> |
||
|
|
3447764b03 |
feat(i18n): full rollout — 21 namespaces translated (en + zh-Hans) (#1853)
* 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>
|
||
|
|
1b3c78e4b5 |
fix(pins): unpin missing sidebar rows (#2062)
* fix(pins): unpin missing sidebar rows * fix(pins): guard missing pin auto-unpin |
||
|
|
140678c4b3 |
fix(web): redesign 404 + break NoAccessPage redirect loop (#2122)
* 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> |
||
|
|
a4fac51cf5 |
fix(projects): add resource_count breadcrumb instead of inlining resources (#2118)
* 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> |
||
|
|
2b967338a8 |
fix(runtimes): narrow CostCell usage window from 180d to 14d (#2119)
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> |
||
|
|
6ef9be10d6 |
fix(chat): expose History panel + delete affordance from chat header (#2117)
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> |