mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
bf1e375015f0bb6e2f736eaeb53ee1fa5cd84bdd
358 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f9dfb3b9fc | Fix duplicate desktop back navigation (#3210) | ||
|
|
b9bf2653be |
fix(desktop): retry tab scroll restore until virtualized content rehydrates (#3214)
Follow-up to #3196. Switching tabs and back on a long issue still landed at scrollTop=0 because issue-detail uses Virtuoso with customScrollParent — Virtuoso wires its scroll/resize observers in a passive useEffect, which fires *after* useLayoutEffect. So at the moment the restore hook ran, the spacer that gives the scroll container its tall scrollHeight hadn't been re-established yet (scrollHeight === clientHeight), and the browser silently clamped `scrollTop = saved` down to 0. Diagnostic console output confirmed this: marker key=true saved=10356.5 currentScrollTop=0 scrollHeight=750 clientHeight=750 → set scrollTop to 10356.5 actually now 0 Fix: keep the synchronous set as the fast path, then if the assignment was clamped, retry across rAF frames for up to ~500ms (30 frames at 60fps). That gives Virtuoso's passive effect time to re-establish the spacer, after which the next tick succeeds. Cancel any in-flight retry when the effect tears down (Activity hidden again or component unmount). Existing 4 tests in use-tab-scroll-restore.test.tsx still pass — the synchronous fast path covers the simple-content case they exercise. A jsdom regression for the Virtuoso scenario didn't reproduce reliably (the clamp + rAF interplay needs a real browser), so this relies on manual verification: open issue-detail, scroll deep into comments, switch tabs, switch back — scroll position now holds. |
||
|
|
90455abd8d |
fix(desktop): preserve tab scroll position across Activity visibility cycles (MUL-2602) (#3196)
Closes #3183. Tabs render under `<Activity mode="visible|hidden">`, which keeps React state but drops DOM scrollTop when the subtree leaves layout. Switching to another tab and back sent users to the top of long discussions. `useTabScrollRestore` records the scrollTop of every element marked with `data-tab-scroll-root` while the tab is visible (capture-phase scroll listener) and restores them in a useLayoutEffect on the next visible transition, before paint. Saved offsets are dropped when the tab's path changes so intra-tab navigation lands at scroll=0 instead of inheriting the previous route's position. Mark scroll containers in views with `data-tab-scroll-root` (issue detail + chat message list ship with the marker; other views can adopt the convention as needed). `useAutoScroll` previously called `scrollToBottom()` on every effect mount, which would have overwritten the restored offset every time a chat tab cycled back to visible. Guard it with a once-per-instance ref. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
ba9714a364 |
feat(desktop): support macOS swipe navigation (#2997)
Co-authored-by: chener <chener@M5Air.local> |
||
|
|
41788d2728 |
fix(settings): i18n the desktop Updates tab (MUL-2515) (#3014)
* fix(settings): i18n the desktop Updates tab (MUL-2515) The Updates tab in Settings was hardcoded English, so Chinese users saw a jagged untranslated panel. Wrap the desktop settings route in a component so the tab label can pull from i18n, move the panel copy to a new desktop.updates namespace under settings, and translate it for zh-Hans. Co-authored-by: multica-agent <github@multica.ai> * fix(settings): polish zh-Hans Updates tab copy (MUL-2515) Address review feedback on PR #3014: - "桌面 app" → "桌面端" to match runtime voice - "检查中…" → "检查中..." per zh conventions (ASCII ellipsis) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fbd965e5bf |
feat(onboarding): v3 — thin server, frontend-orchestrated welcome (#3008)
* feat(onboarding): Multica Helper as general workspace assistant + blocking modal
Reshape Multica Helper from an onboarding-only guide into the workspace's
general-purpose AI assistant. The agent's permanent identity (injected as
`## Agent Identity` into every task's CLAUDE.md / AGENTS.md / GEMINI.md
via execenv.InjectRuntimeConfig) is rewritten to three sections that don't
overlap with what the brief already provides:
- Who I am (built-in workspace assistant, not onboarding-only)
- What Multica is + docs/source/issues URLs as knowledge sources
- What I can do (CLI = manifest, `multica --help` is the source of truth)
- Tone (concise, like a colleague, match user's language)
Bootstrap moves out of the in-flow Step 4. Runtime step now exits the
onboarding shell with no bootstrap call; a blocking OnboardingHelperModal
mounts inside the workspace layout (web + desktop) and gates purely on
`me.onboarded_at == null`. The user picks one of three starter prompts
(intro / assign / second_agent) and the modal calls
BootstrapOnboardingRuntime with a new optional `starter_prompt` field that
becomes the seeded onboarding issue's description.
Side effects required to make `onboarded_at == null` an honest signal:
- CreateWorkspace no longer marks onboarded (was atomic with CreateMember).
The "member exists ⟹ onboarded_at != null" invariant is intentionally
broken; guards (useDashboardGuard / desktop App.tsx) already tolerate
this — comments updated to reflect the new contract.
- AcceptInvitation still marks (invitee skips the modal in someone
else's workspace). Code comment added warning future removers.
- resolvePostAuthDestination flips to workspace-presence-first: a user
with a workspace lands in it regardless of `onboarded_at`, so the
modal can pick up an interrupted setup on relogin.
Other backend changes:
- `onboardingAssistantDescription` rewritten ("Built-in workspace assistant…")
- `onboardingAssistantInstructions` rewritten to the 3-section identity
- `bootstrapOnboardingRuntimeRequest.StarterPrompt` (optional, 2 KiB rune
cap, empty-falls-back-to onboardingIssueDescription)
Frontend changes:
- Delete `packages/views/onboarding/steps/step-teammate.tsx` (no longer a
persisted step)
- `ONBOARDING_STEP_ORDER` and `OnboardingStep` type drop `"teammate"`
- `handleRuntimeNext` exits via `onComplete(workspace, undefined)` — no
bootstrap, `onboarded_at` stays NULL so the modal fires
- Runtime step next-button copy → "Start exploring" / "开始探索"
- New `packages/views/workspace/onboarding-helper-modal.tsx`:
Base UI Dialog, dismissible=false, three localized cards, mutation
invalidates agents + issues queries then navigates to the seeded issue
- Mounted in both `apps/web/app/[workspaceSlug]/layout.tsx` and
`apps/desktop/src/renderer/src/components/workspace-route-layout.tsx`
Tests:
- Backend: TestBootstrapOnboardingRuntime_{With,No}StarterPrompt and
TestCreateWorkspace_DoesNotMarkOnboarded
- Frontend: onboarding-helper-modal.test.tsx covers all four gating
conditions, three-card behavior, mutation pending state, and the
"no close button" invariant
Compatibility:
- Already-onboarded users: zero impact (modal can't fire)
- Invitees: AcceptInvitation still marks → modal can't fire
- Skip-runtime path: BootstrapOnboardingNoRuntime still marks → modal can't fire
- Old desktop / web clients: legacy teammate-step path keeps working
(bootstrap accepts missing starter_prompt) — the new modal only fires
on the new frontend bundle
- Avatar SVG kept (asterisk variant) — no migration of existing Helper
agents, only newly-created Helpers pick up the new instructions/description
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(desktop): suppress OnboardingHelperModal while a WindowOverlay is open
On desktop, App.tsx auto-creates a tab pointing at the user's first
workspace as soon as workspaces.length flips from 0 → 1 (during onboarding
Step 2). The new tab mounts WorkspaceRouteLayout under the overlay,
which mounts OnboardingHelperModal. The modal's Portal renders to
document.body — appearing AFTER the WindowOverlay in DOM order, so its
z-50 wins and the modal floats in front of the still-active onboarding
Step 3 (runtime).
Suppress the modal whenever any WindowOverlay is active. When the overlay
closes (onComplete fires after the user finishes onboarding), the modal
re-evaluates `me.onboarded_at == null` and pops on its own.
Web is unaffected (onboarding flow lives at /onboarding, not under
/[workspaceSlug]/, so WorkspaceRouteLayout never mounts during the
onboarding flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(onboarding): add v2 refactor plan
Captures the design + 8-step implementation order for collapsing the
onboarding state machine: single mark-onboarded entry point, persisted
Step 3 user choice, dumb Modal, single install-runtime seed call site.
Includes old-user compatibility analysis (4 existing gates) and per-PR
risk/rollback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(db): persist Step 3 runtime choice on user record (MUL-onboarding-v2)
Adds onboarding_runtime_id UUID NULL + onboarding_runtime_skipped BOOLEAN
columns to "user" and the CHECK constraint enforcing the 3-state machine
(unset / picked-runtime / explicit-skip; the fourth combination is
forbidden). ON DELETE SET NULL on the FK so a deleted runtime degrades
to "unset" rather than dangling.
PatchUserOnboarding gains the two narg fields plus CASE expressions that
collapse the runtime/skipped pair atomically — a follow-up PATCH that
flips one side now clears the other in the same statement, instead of
preserving it via per-field COALESCE and tripping the CHECK constraint.
Backwards compatible for existing users: both new fields default to
(NULL, false), which is the "unset" leaf of the state machine, and four
upstream gates on me.onboarded_at != null already short-circuit the
new fields' readers for everyone who's already onboarded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server): collapse onboarding side effects to service layer
Introduces OnboardingService.MarkComplete and
WorkspaceContentService.{Ensure,Seed}InstallRuntimeIssue as the single
authorities for the two onboarding side effects that used to be
duplicated across four handlers:
- MarkUserOnboarded + claim starter_content_state +
optional install-runtime fallback seed: was inline in
BootstrapOnboardingRuntime, BootstrapOnboardingNoRuntime,
AcceptInvitation, and CompleteOnboarding.
- install-runtime issue seeding: was inline in CreateWorkspace and
AcceptInvitation as a "no runtime yet" fallback.
After this refactor:
- MarkUserOnboarded is called from exactly one place (the service).
- install-runtime issue is seeded from exactly one place (the service).
- CreateWorkspace deliberately does not seed — the new
/ensure-onboarding-content endpoint (also added here) lets the
workspace-entry init component request the seed on first mount, so
workspaces created but never opened don't accumulate stale issues.
- The PatchOnboarding handler now accepts the new runtime_id /
runtime_skipped fields and rejects (uuid, skipped=true) up front.
- UserResponse exposes the two new persisted fields so the frontend
can read them off `me` without an extra round-trip.
Handler-side tests added: TestPatchOnboarding_RuntimeChoiceSwitch (the
explicit cross-request switch path that the original COALESCE design
would have 500'd on) + TestPatchOnboarding_PreserveUntouched.
Old handler-local file no_runtime_issue.go is deleted; its content
moved to service/workspace_content.go with the helpers exported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): API + types for persisted onboarding runtime choice
User type / Zod schema gain onboarding_runtime_id (string | null) and
onboarding_runtime_skipped (boolean); EMPTY_USER + test fixture updated
to match. api.patchOnboarding accepts the new optional fields and the
new api.ensureOnboardingContent endpoint is wired so the workspace
shell can request the fallback seed.
Two new store helpers — recordOnboardingRuntimeChoice(runtimeId) and
recordOnboardingRuntimeSkipped() — replace the prior pattern of
Step 3 calling bootstrap directly. They PATCH the user's choice, sync
the auth store, and return. Mutually exclusive on the server side via
the CHECK constraint; the client just ships one intent at a time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(workspace): WorkspaceOnboardingInit single decision point + dumb Modal
Replaces OnboardingHelperModal's self-gating render path with a 4-branch
dispatcher that runs once on workspace-shell mount:
branch 0 me.onboarded_at != null → ensure install-runtime issue
fallback, render nothing
branch 1 me.onboarding_runtime_skipped → SkipBootstrapping component:
loading veil → bootstrap →
navigate. On failure shows
a Retry UI instead of
silently freezing the veil
branch 2 me.onboarding_runtime_id → render Modal with the
runtime id from `me` (no
internal list query)
branch 3 (none of the above) → useEffect navigate back to
/onboarding so the user
walks Step 3 again
The Modal itself is now a dumb component — receives `workspace` and
`runtimeId` as props, no internal gates, no runtimeListOptions query.
Tests rewritten to cover the props-driven render + pick-card paths;
the prior gating tests move into the new
workspace-onboarding-init.test.tsx alongside the M2 retry-on-failure
behaviour.
Mounted in both apps/web/app/[workspaceSlug]/layout.tsx and the desktop
workspace-route-layout. Desktop keeps its `!overlayActive` suppression
guard so the init doesn't portal-jump in front of an active
WindowOverlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): Step 3 records user choice instead of calling bootstrap
handleRuntimeNext now PATCHes the user's pick (recordOnboardingRuntime
{Choice,Skipped}) and navigates straight into the workspace shell. The
workspace-entry WorkspaceOnboardingInit reads the persisted choice off
`me` and runs the appropriate branch — Step 3 is pure intent capture
with zero side effects on its own.
PATCH must succeed before navigation: if it fails the user stays on
Step 3 with a toast, because navigating with no persisted intent would
land them in WorkspaceOnboardingInit's branch 3 "no decision yet" rescue
and trigger a redirect loop back to /onboarding.
The prior asymmetry (Connect deferred bootstrap to the workspace, Skip
ran bootstrap inline) is gone — both paths defer to the workspace
shell now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): v3 — thin server, frontend-orchestrated welcome
Collapse v2's persisted runtime-choice fields + 4-branch dispatcher +
OnboardingService/WorkspaceContentService stack down to a single rule:
`onboarded_at` is the only state field, layout hard-gates on it, and the
welcome experience after Step 3 is owned entirely by the frontend.
V3 flow
- Step 3 button: await POST /api/me/onboarding/complete (mark only) +
park a transient signal in `useWelcomeStore` + navigate
- Workspace layout: hard gate `onboarded_at == null` -> /onboarding
- `<WelcomeAfterOnboarding />` reads the welcome-store signal:
- runtime path: find-or-create Multica Helper via generic createAgent
with bilingual instructions from `templates/helper-instructions.ts`,
blocking modal with 3 starter cards, pick -> createIssue + navigate
- skip path: provision install-runtime (in_progress) -> agent-guide
(todo, body embeds install-runtime mention chip) -> follow-up comment
on install-runtime mentioning agent-guide; then pop celebration
modal with 🎉 emoji pop animation, 2 read-only preview cards, single
[Got it] CTA that navigates to install-runtime
Server cleanup
- Drop OnboardingService, WorkspaceContentService, v2 runtime-choice
columns/CHECK on user, EnsureOnboardingContent endpoint
- CompleteOnboarding/AcceptInvitation call qtx.MarkUserOnboarded
directly (no service indirection)
- BootstrapOnboardingRuntime / BootstrapOnboardingNoRuntime kept as a
deprecation shim in onboarding_shim.go for desktop < v3 during the
rollout window — handlers inlined to qtx.* calls, no service layer
Localization
- Persisted strings (issue titles/bodies, Helper instructions/
description, comment prefix) live as TS const `{en, zh}` maps in
`packages/views/onboarding/templates/` — i18n bundle staleness can no
longer write raw key paths into DB
- UI-rendered strings (modal copy, status chips, buttons) stay in
`packages/views/locales/{en,zh-Hans}/onboarding.json`
- Language picked from live `i18n.language` (not `me.language`, which is
null for new users until they pick a preference)
Race protection
- Module-level promise dedupe (`findOrCreateHelper`, `seedIssueDeduped`,
`postCommentDeduped`) so React StrictMode double-mount can't fire two
parallel API calls that the server would then 409
Cross-references between the two skip-path issues render via Multica's
mention-chip protocol `[<identifier>](mention://issue/<uuid>)` so they
match the styled IssueChip pills used elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): welcome-after-onboarding modal redesign + cross-user safety
Welcome modal polish (the post-Step-3 surface this branch already
introduced):
Runtime path
- Helper avatar replaces the bouncy 🎉 hero; tone-down animation to
fade. New copy: "Hi, welcome to Multica / I'm your first Agent
assistant" + capability hint sentence so users discover assignment +
chat from the first screen.
- Cards changed from "click = submit" to multi-select with the existing
border-primary + ring selection pattern used by compact-runtime-row;
bottom CTA "Assign N tasks to me →" appears only with N>0.
- New starter cards: intro / tour / welcome_page (the last one tells
Helper to paste an HTML welcome page into the issue comment — works
on any runtime regardless of fs access).
- Success state added between createIssue and navigation: 🎉 +
"All set!" + "Sit tight ☕ — your {agentName} is on it" + inbox/chat
hints, single [Got it] button.
- Title/prompt for starter cards now live in TS const
HELPER_STARTER_PROMPTS (persisted to DB — must not depend on i18n
bundle being loaded); subtitle stays in onboarding.json.
Skip path
- Body restructured into three independent ```md blocks (Name /
Description / Instructions) so each picks up the markdown renderer's
per-block copy button — no manual extraction.
- ZH body now embeds the ZH Helper Description + Instructions (was
Chinese-around-English-block).
- Follow-up comment uses Multica's mention-chip protocol
[identifier](mention://issue/uuid) so it renders as the styled
IssueChip pill.
- Issue titles bilingual with "Step 1 / Step 2" prefix.
Cross-user / cross-workspace safety (code review feedback)
- web onLogout + desktop handleDaemonLogout now call
useWelcomeStore.reset() so user B logging into the same browser
doesn't inherit user A's signal.
- WelcomeAfterOnboarding gates on
currentWorkspace.id === signal.workspaceId — prevents firing the
modal in workspace B when the signal was parked for workspace A
(desktop multi-tab, back/forward, deep-link).
- Module-level promise dedupes (pendingHelperSetup,
pendingIssueSeed, pendingCommentSeed) for the three API calls so
React 18+ StrictMode dev double-mount can't race-create duplicates.
Other small fixes carried in this commit
- Helper instructions / agent description / starter card titles all
read i18n.language (not me.language, which is null for new users
who haven't picked a UI language preference yet).
- Reverted welcome-emoji-pop animation to a small fade for the runtime
avatar (kept the bouncy variant for the skip 🎉 hero where the
celebration is the whole point).
- Removed the duplicate 🎉 from the skip modal title (kept the hero
one only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(views): i18n hardcoded "Close" in welcome FullScreenError
CI lint (i18next/no-literal-string) blocked on a literal "Close" string
inside `FullScreenError` — surfaced as a nit in the original code
review but missed in the merge. Add `error_close` to onboarding.json
(EN: "Close" / ZH: "关闭") and thread it through as a `closeLabel`
prop, matching the existing `retryLabel` plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
41753d17a2 |
feat(desktop): pin tab (MUL-2449) (#2914)
* feat(desktop): pin tab — keep parked tabs anchored across navigations (MUL-2449)
Adds tab pinning to the desktop tab bar. Pinned tabs render as icon-only at
the left, suppress the X close button, and intercept any `navigation.push()`
that would change their pathname — those are redirected into a new tab so
the pinned tab stays parked on its original route. Search/hash/back/forward
stay in-tab so pinned filter and drawer state still work.
Implements the FINAL combo from the MUL-2449 RFC §4: right-click menu +
⌘⇧P shortcut (D1 a+c), icon-only visual (D1v i), pathname-change → new tab
with same-path-allowed (D2a/b A), back / refresh allowed (D2c/d A), pinned
auto-cluster left and persist (D3a/b A), pinned can't be X-closed (D3c A),
dedupe respected (D4a A), default Issues tab pinnable (D4b A), drag clamped
to its zone (D4c A), deep link prefers pinned (D4e A).
Store changes:
- Tab.pinned added; togglePin maintains the "pinned first" invariant by
inserting at the zone boundary.
- moveTab clamps cross-zone drags so dnd-kit can't violate the ordering.
- Persistence bumped v2 → v3 with a defaulting migration (pinned=false).
Rehydrate sorts pinned-first as a defensive net.
Navigation:
- tryRouteToPinnedNewTab compares the active tab router's live pathname
to the target. Same-pathname push (query / hash / sub-router) falls
through to the router; different pathname → openTab + setActiveTab
(foreground; respects dedupe).
UI:
- Tab bar wraps each tab in a shadcn ContextMenu with Pin/Unpin + Close
(Close disabled for pinned or last-remaining tab).
- Pinned tabs use a narrower icon-only layout with an accent left border
and a divider between the pinned and unpinned groups.
- Global keydown listener registers ⌘⇧P / Ctrl+Shift+P to toggle pin on
the active tab.
Tests: - tab-store: togglePin ordering, moveTab boundary clamping, v2→v3
migration.
- navigation: pinned push → new foreground tab; same-pathname push stays
in tab; cross-workspace still wins over pin.
Co-authored-by: multica-agent <github@multica.ai>
* test(desktop): cover TabNavigationProvider.push pin interception (MUL-2449)
Add pathname-diff / same-pathname cases for the per-tab navigation
adapter. Existing tests only exercised the root-level
DesktopNavigationProvider, but in-tab AppLink / page clicks flow
through TabNavigationProvider — so a future refactor that drops the
pin check from that provider would silently regress.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(desktop): pin tab — hover button, full title, drop ⌘⇧P (MUL-2449)
Jiayuan's interactive review of PR #2914 surfaced three changes to the
RFC's D1 (entry / visual) decisions:
1. Drop the ⌘⇧P global shortcut — it added a keybinding for a
low-frequency action and crowded the shortcut namespace.
2. Reveal a Pin / Unpin button on tab hover instead of relying on the
right-click menu as the primary entry; right-click remains as a
fallback (and for Close).
3. Pinned tabs keep their full title and width. The only weak visual
differences vs. unpinned tabs are the accent left border and the
suppressed X close button.
Removes the global keydown listener (no other doc / handler referenced
it). Adds a hover-only Pin / Unpin span next to the existing close
affordance, both gated by group-hover. Drops the icon-only width /
hidden-title styling for pinned tabs.
Tests: new tab-bar.test.tsx covers Pin / Unpin button rendering, click
handlers (togglePin), the hidden-X invariant on pinned tabs, and the
full-title rendering. 146 passed, typecheck clean.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(desktop): pin tab — drop accent left border, swap leading icon to Pin (MUL-2449)
Jiayuan reported that the accent left border on pinned tabs reads as a
heavy black edge in light mode and looks unrefined. Replace it with a
quieter identifier: pinned tabs swap their route icon for a Pin glyph
in the leading slot (same size, no extra horizontal space). The hidden
X close button stays as the secondary cue. RFC §3 D1v moves from
iii FINAL to iv FINAL; iii is demoted to v2 FINAL → v3 REMOVED.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
72339f347b |
fix(desktop): keep local machine row visible after stopping daemon (#2906)
The Start button lives in `DaemonRuntimeActions`, which is rendered in the per-machine detail pane and only when the selected machine is flagged `isCurrent`. After the user manually stopped the daemon, `status.daemonId` went back to undefined, so no machine could be matched as `isCurrent` — the local row either disappeared (when the server-side runtime had been GC'd) or moved into the "remote" section (when it was still present but unmatched). Either way the Start button was unreachable until the app was restarted. Two-part fix: - `DesktopRuntimesPage` now caches the last-known daemonId/deviceName so the local match keeps working while the runtime is still on the server (recently_lost / offline window). - `buildRuntimeMachines` accepts an `ensureLocalMachine` flag; when no real runtime matches, a placeholder local row is synthesized so the Start button still has a home. Desktop opts in via a new `hasLocalMachine` prop on `RuntimesPage`. The empty state is also suppressed when this prop is set so the placeholder row isn't hidden behind the "register a runtime" hint on first launch. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
591e47842d |
refactor(onboarding): remove starter-content kit; unify install-runtime issue across mark-onboarded paths (MUL-2438) (#2884)
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438) Drops the post-onboarding ImportStarterContent / DismissStarterContent flow (handler + routes + StarterContentPrompt + templates + locale strings + analytics event). The bug — web onboarding seeding 6+ starter issues without a runtime — only existed through that path; with it gone the source disappears. The "install a runtime" issue from BootstrapOnboardingNoRuntime is now the canonical no-runtime onboarding seed. The title/description and a LockAndFindActiveDuplicate-deduped seeder move to handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace / AcceptInvitation seed it whenever the workspace has no runtime yet, so every mark-onboarded entry point lands the user on a concrete next step. starter_content_state column is kept and continues to be claimed as 'imported' in all five entry points so older desktop builds (which still render the legacy dialog on NULL) don't surface it to accounts created after this change. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438) 054 only covered pre-feature users. Anyone onboarded between then and the starter-content kit removal could still sit at NULL, and old desktop clients gate the legacy StarterContentPrompt on `starter_content_state IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL would surface a dialog whose buttons 404. Mark them 'imported' to match the new helper's claim semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f92deaf939 |
feat(desktop): foreground new tab for explicit Open-in-new-tab CTAs (MUL-2434) (#2869)
Add optional `opts.activate` to NavigationAdapter.openInNewTab. Default
stays `false` so cmd/ctrl+click on links/mentions keeps browser-style
background semantics. The two explicit toolbar entry points
(attachment-preview-modal, html-attachment-preview) opt in with
`{ activate: true }` so the new tab gains focus after the modal closes.
Both desktop providers (root + per-tab) now use the tab id returned by
`store.openTab` to call `setActiveTab` only when `activate` is true.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
6901325761 |
fix(desktop): open HTML preview in background tab and close modal (MUL-2418) (#2854)
Two independent root causes made "Open in new tab" on a desktop
attachment-preview modal feel like "the popup is still there and the
current tab got replaced":
1. `AttachmentPreviewModal.handleOpenInNewTab` never called `onClose()`,
so the modal stayed mounted over the new tab.
2. Both `DesktopNavigationProvider.openInNewTab` and
`TabNavigationProvider.openInNewTab` called
`store.setActiveTab(tabId)` after `store.openTab(...)`, which stole
focus to the new tab — violating the type contract
("Desktop only: open a path in a new background tab") and matching
neither Chrome's cmd+click default nor the user's expectation.
Fixes:
- Modal: always call `onClose()` after dispatching the navigation
(desktop adapter path and web `window.open` fallback path).
- Desktop navigation: drop the post-`openTab` `setActiveTab` call in both
providers. `openTab` already preserves `activeTabId` for new paths and
switches to the existing tab when the path is already open, which is
exactly the background-tab semantics the type contract advertises.
Tests:
- `attachment-preview-modal.test.tsx`: assert `onClose` is invoked on
both the desktop and web fallback branches.
- `pageview-tracker.test.tsx`: rename the "openInNewTab / addTab" case
so the comment no longer claims `openInNewTab` activates the new tab.
- New `apps/desktop/.../platform/navigation.test.tsx`: assert that
`openInNewTab` on both providers calls `openTab` and never
`setActiveTab` for same-workspace paths, and routes cross-workspace
paths through `switchWorkspace`.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
fd6ccbe371 |
feat(desktop): forward renderer console + crash events to main stderr in dev (#2853)
When the renderer crashes hard enough to leave a white window (React
boundary unrecoverable, syntax error during initial mount, preload
script throw), DevTools can't be opened and the only signal in the
`make dev` terminal is the daemon-manager 5s polling complaint
("Render frame was disposed before WebFrameMain could be accessed").
That's a downstream symptom — the actual JS error is unreachable, so
the user has no path to diagnose without restarting the renderer
(which loses the failure mode entirely).
Add four webContents listeners on the main BrowserWindow, gated by
`is.dev` so packaged builds keep their stderr clean:
- `console-message`: forwards every renderer `console.*` to main's
stderr with file:line. React error boundaries, `window.onerror`, and
unhandled-rejection handlers all surface here.
- `render-process-gone`: serialises the GoneDetails (`crashed` / `oom`
/ `killed` / `launch-failed`) so the user sees *why* the renderer
died, not just that it did.
- `did-fail-load`: catches loadURL/loadFile failures. Skip
`errorCode === -3 (ABORTED)` because that's the normal HMR-induced
navigation abort.
- `preload-error`: the one error class DevTools can never show, because
preload runs before the window owns a console. Without this listener
preload throws are invisible.
All output is prefixed with `[renderer <tag>]` so it's easy to grep
distinct from main's own logs.
No behavioural change in production: the entire block is inside an
`is.dev` guard. Packaged builds keep their existing stderr.
|
||
|
|
6f21cb8f3e |
[codex] Simplify onboarding runtime bootstrap (#2836)
* feat(onboarding): simplify runtime bootstrap * fix(onboarding): close private-helper reuse hole and guide-issue nav race - server: when bootstrap looks for an existing Multica Helper, require Visibility="workspace" so a private helper owned by another member can't be auto-assigned to the onboarding issue (and trigger a task as that private agent), which would have bypassed canAccessPrivateAgent. - web onboarding page: refreshMe() inside bootstrap flips hasOnboarded before onComplete fires, letting the guard's router.replace overtake onComplete's router.push to the new guide issue. Mark the page as "completing" right before navigating so the guard stays silent during the in-flight transition. Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): escape daemon command literals to satisfy i18next/no-literal-string Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
c577a29c10 |
feat(onboarding): v2 per-question questionnaire (source/role/use_case) (#2814)
* feat(onboarding): per-question v2 questionnaire (source/role/use_case) Replaces the 3-questions-on-one-screen gate with three lightweight, individually-skippable steps. New step order: welcome → source → role → use_case → workspace → runtime → agent → first_issue - New v2 questionnaire schema: source/role/use_case + per-slot `*_skipped` markers. `team_size` removed. - Click-to-advance card grid with lucide + emoji icons (RFC Option B). - Skip is a footer text button; Other expands a free-text input. - Recommendation table updated for new role × use_case vocabulary, with use_case-only fallback when role is skipped. - DB migration v1 → v2 maps existing role/use_case answers and drops team_size; historical nulls stay null (not retroactively skipped). - Re-entry treats skipped slots as fresh; analytics record kept in DB. - onboarding_questionnaire_submitted event payload updated: source replaces team_size, per-slot skip booleans added. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): tighten question UX (Continue, layout, brand icons) Address review feedback on Source/Role/Use-case: - Replace auto-advance with an explicit Continue button so selections are reviewable. Continue is disabled until something is picked (and, for Other, until the free-text input is non-empty). - Move Back/Skip/Continue inline under the option grid; drop the duplicate Back from the top header — the page now has a single, anchored action row. - Swap the placeholder lucide marks for real brand SVGs on Source: Google, X, LinkedIn, YouTube, and an OpenAI mark for the AI-assistant option. Generic options stay on lucide. - Replace the awkward expanded underline input on the Other card with an inline borderless input that swaps in for the label slot, so the Other state has the same height and weight as the other cards. E2E smoke test updated to click Continue between question steps. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): unify step nav, rename Runtime step around "where agents run" - Refactor the Source/Role/Use case questionnaire steps to use the same 3-region chrome (header with Back + step indicator, scrolling main, sticky footer with Skip + Continue) that Workspace/Runtime/Agent already use, so the Back/Skip/Continue affordances stay in the same on-screen position across the whole flow. - Reframe the Runtime step around the user-visible question — "Where will your agents run?" — instead of the internal "runtime" concept. The aside panel keeps the educational "What's a runtime?" copy for users who want to learn. - Drop the hard-coded "Step 3 · Runtime" eyebrow on the web fork step: Runtime is now step 5 of 7 after the per-question split, and the step indicator already shows the correct count. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): tighten Skip/Continue spacing in step footer Group Skip and Continue inside a sub-flex with gap-2 so they read as a single action cluster on the right, while the status hint still anchors left via mr-auto. Applied to both the questionnaire steps and the runtime step so the footer layout stays consistent across onboarding. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): move Skip/Continue inline below form, drop sticky footer The sticky bottom footer left a large dead zone between the form content and the action buttons — most onboarding steps only fill the top third of the viewport. Move the hint + Skip + Continue inline, directly below the form/options grid, so the buttons sit where the eye already is after picking an option. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): match Skip button size to Continue (size="lg") Skip used the default button size (h-8) while Continue used size="lg" (h-9), so the two adjacent action buttons rendered visibly different heights. Promote Skip to size="lg" in step-question and step-runtime-connect so they line up. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): reframe step 3 as 'connect a computer' / 'pick an agent runtime' Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): replace cloud waitlist with "Coming soon", reword CLI intro - Web Step 3 cloud card: remove "Join waitlist" CTA + dialog and render a static "Coming soon" badge instead. Drops CloudWaitlistDialog, the cloud DialogState, waitlistSubmitted local state, and the onWaitlistSubmitted prop on StepPlatformFork (desktop's StepRuntimeConnect still owns its own waitlist path). - Tighten cloud_subtitle to drop the "join the waitlist" half now that the action is gone. - cli_install.intro: "AI coding tool" → "agent runtime", EN + zh-Hans. Tests updated to match: asserts the Coming soon badge is non-actionable and drops the four cloud-dialog scenarios (now unreachable). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): refresh button, "agent runtime" wording, coming-soon card Three fixes on the desktop Step 3 empty state per review: 1. Empty headline + hints now say "agent runtime", matching the picker-context terminology established earlier in this PR. 2. Add a Refresh button (header pill in Found, inline with the headline in Empty). Desktop wires it to restart the bundled daemon so a freshly-installed Claude/Codex/Cursor CLI is picked up — the daemon's PATH probe runs once at boot, so without a restart the install would only take effect on next launch. 3. "Use a cloud computer" loses the waitlist dialog and renders as a disabled "Coming soon" badge, aligning with the web fork. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): address review follow-ups (i18n, step-order, version, tests) - runtime-aside-panel: point "Learn more" to /docs/install-agent-runtime, branching by language so zh users land on /docs/zh/... - zh-Hans: unify Cloud "Coming soon" wording to "即将推出"; translate step_workspace.preview.more_meta ("and more" -> "等等") - onboarding-flow: derive forward navigation from ONBOARDING_STEP_ORDER via advanceFrom(curr) so inserting/reordering a step only requires editing the canonical array; runtime → agent/first_issue branch keeps its bespoke routing with a comment explaining why - onboarding handler: gate questionnaireAnswers.complete() on Version == 2 so a future schema bump can't be silently mis-counted against v2 funnel semantics - add unit tests for step-source / step-role / step-use-case (option click, Skip patch, Other free-text) and step-question shell (canContinue + pendingOther state machine) Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): rename useCaseFallback to fallbackFromUseCase ESLint's react-hooks/rules-of-hooks treats any function starting with "use" as a React hook. The helper is a pure switch — give it a name that doesn't trip the rule. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
baedc48f59 |
fix(editor): source-view highlight + HTML attachment open-in-new-tab (#2812)
* fix(editor): bump hast-util-to-html to v9 so lowlight output actually serializes
Source view of fenced ```html (and any other code block falling through to
the lowlight branch in ReadonlyContent) silently rendered as un-highlighted
escaped text. Root cause was a stale dep pin: `hast-util-to-html: ^4.0.1`
predates the package's ESM/named-export rewrite — v4 only exports a CJS
default function, so the `import { toHtml } from "hast-util-to-html"` in
code-block-static.tsx:19 and readonly-content.tsx:32 resolved to
`undefined` at runtime. The try/catch in both call sites caught the
"toHtml is not a function" throw and fell through to escapeHtml plain
text, so no `.hljs-*` spans ever made it to the DOM and the syntax-color
CSS added in #2808 had nothing to attach to.
Bumping to ^9.0.5 (matches the v9 line that lowlight@3 / remark / rehype
ship in the rest of the tree) makes the named `toHtml` export available
and source-view highlighting works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): open HTML attachment in new tab + full-page preview route
Adds a third toolbar button to HtmlAttachmentPreview between Maximize and
Download: open the attachment in a new app tab (desktop) or browser tab
(web). The full-screen modal stays — they serve different scenarios:
modal for a quick "see it bigger" without leaving the issue context,
new-tab when the user wants to keep the rendered HTML around while
working on something else.
Components:
- New workspace path: `/{slug}/attachments/{id}/preview?name={filename}`.
Lives outside the (dashboard) group on web so the iframe gets the full
viewport — sidebar would defeat the point. Desktop registers the route
inside `WorkspaceRouteLayout` so workspace context resolution still
runs (no slug → no path is built).
- `packages/views/attachments/attachment-preview-page.tsx`: shared full-
page view that reuses `useAttachmentHtmlText` for the iframe srcDoc.
Sandbox stays `allow-scripts` (no allow-same-origin) — same security
posture as the inline preview.
- `HtmlAttachmentPreview`: adds Open-in-new-tab button. Routes through
`useNavigation().openInNewTab` when available (desktop), falls back to
`window.open(getShareableUrl(path))` on web. Button is hidden when no
workspace slug is in scope (shouldn't happen in practice, but the
shared component must not throw outside a workspace route).
Tests cover: desktop openInNewTab call args, web window.open fallback,
and that the failure-mode toolbar still surfaces all three actions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): drop now-stale @ts-expect-error on hast-util-to-html imports
v9 ships bundled type declarations, so the directives added for v4 trigger
TS2578 ("Unused '@ts-expect-error' directive") on CI typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a4a18605eb |
fix(desktop): handle Cmd/Ctrl +/-/0 zoom in main process (MUL-2354) (#2791)
The default Electron application menu's zoomIn/zoomOut roles do not fire reliably on macOS — Cmd+= would zoom in but Cmd+- could not undo it, so users got stuck at the zoomed-in level with no way back. Move the shortcut into before-input-event so the same handler covers every platform and every keyboard layout. preventDefault here blocks both the renderer keydown and the menu accelerator, so there's no double-zoom risk on macOS. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f1c9617b5e | feat(runtimes): Redesign runtimes machine layout (#2747) | ||
|
|
2f0e5b589e | [codex] Add member and agent task views | ||
|
|
ee48e58b8f |
feat(desktop): silent background auto-download for updates (MUL-2224) (#2631)
* feat(desktop): silent background auto-download for updates (MUL-2224)
Flip electron-updater to autoDownload=true so new releases are pulled in
the background without user action; the UI now only surfaces a
"ready to install" prompt once the package is fully downloaded.
- updater.ts: autoDownload=true; update-downloaded forwards version +
releaseNotes; single-flight guard around checkForUpdates() so startup,
periodic, and manual triggers don't pile up overlapping downloads.
- preload: update-downloaded payload now carries { version, releaseNotes? }.
- update-notification.tsx: drop available/downloading UI; ready state has
Later / Restart now and renders the version from the download event.
- updates-settings-tab.tsx: settings copy now describes background download
+ restart prompt instead of a download prompt.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): swallow unhandled downloadPromise rejection in updater (MUL-2224)
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
29082f7cfe |
feat: implement Squad feature MVP (#2505)
* feat: implement Squad feature MVP
- Add migration 084_squad: squad, squad_member, squad_activity_log tables
- Extend issue.assignee_type to support 'squad'
- Add sqlc queries for squad CRUD, member management, activity logs
- Add Go handler with full Squad API (CRUD, members, activity log)
- Register routes: /api/squads/*, /api/issues/{id}/squad-activity, /api/squad-activity
- Add Squad trigger logic:
- Assign Squad immediately triggers leader
- Every external comment on squad-assigned issue triggers leader
- Anti-loop: squad members' comments don't trigger leader
- Dedup: skip if leader already has pending task
- Add squad activity log API (方案 B) for leader no-op recording
- Add frontend TypeScript types (Squad, SquadMember, SquadActivityLog)
- Add protocol events: squad:created, squad:updated, squad:deleted
Co-authored-by: multica-agent <github@multica.ai>
* fix: address PR review blocking issues
1. validateAssigneePair now accepts 'squad' assignee_type
2. All squad endpoints validate workspace ownership via GetSquadInWorkspace
3. CreateSquadActivityLog restricted to squad leader agent only
4. AddSquadMember validates member exists in workspace
5. UpdateSquad auto-adds new leader to squad members
6. DeleteSquad transfers assigned issues to leader before deletion
7. IssueAssigneeType includes 'squad' in frontend types
Co-authored-by: multica-agent <github@multica.ai>
* feat: soft-delete squads via archive instead of hard delete
- Add migration 085: archived_at + archived_by columns on squad table
- ListSquads now excludes archived squads (ListAllSquads for admin)
- DeleteSquad → ArchiveSquad (sets archived_at, preserves all records)
- Transfer squad-assigned issues to leader before archiving
- SquadResponse includes archived_at/archived_by fields
- Frontend Squad type updated with nullable archived fields
Co-authored-by: multica-agent <github@multica.ai>
* feat: re-add Squads frontend entry (sidebar nav + pages)
Re-applies the frontend squad entry that was lost during a merge:
- Sidebar nav: Squads item with Users icon
- Paths: squads() and squadDetail() in workspace paths
- Routes: /squads and /squads/[id] pages
- Views: SquadsPage (list) and SquadDetailPage
- i18n: en 'Squads' / zh '小队'
- Reserved slug: 'squads'
Co-authored-by: multica-agent <github@multica.ai>
* fix: fix SquadsPage rendering - use PageHeader children pattern
PageHeader takes children, not title/actions props. The incorrect
usage caused a React rendering error. Now matches the pattern used
by autopilots and agents pages.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): add API client methods and package export for squads pages
* feat: complete Squad frontend - create dialog, member management, API methods
- Add CreateSquadModal with name/description/leader selection
- Register 'create-squad' in modal registry
- Wire 'New Squad' button to open the modal
- Add full API client methods: createSquad, updateSquad, deleteSquad,
addSquadMember, removeSquadMember
- Rewrite SquadDetailPage with:
- Member list showing resolved names
- Add/remove member UI
- Archive squad button
- Back navigation to squads list
Co-authored-by: multica-agent <github@multica.ai>
* feat: improve Squad UI - match create agent dialog style
- CreateSquadModal: proper Dialog with Header/Description/Footer,
agent picker with avatars, textarea for description
- SquadDetailPage: centered max-w-2xl layout, ActorAvatar for members,
Crown badge for leader, textarea for member description,
improved spacing and visual hierarchy
- Renamed 'role' field label to 'Description' in add member form
(describes the member's responsibilities in the squad)
Co-authored-by: multica-agent <github@multica.ai>
* feat(squad): add avatar, instructions; drop unique-name constraint
- 086: add squad.avatar_url
- 087: drop unique constraint on squad.name (squads with the same
name are legitimate across teams; uniqueness was an accidental
product constraint)
- 088: add squad.instructions (text, default '')
- UpdateSquad now COALESCEs avatar_url + instructions
- handler exposes Instructions in SquadResponse and accepts it in
UpdateSquad
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): assignable + mention target; trigger leader on assign
- assignee picker and @mention suggestion list squads alongside
agents and members; renders squad avatar/icon
- creating or updating an issue with assignee_type=squad enqueues
a task for the squad's current leader (mirrors agent-assignee
parking-lot rule: skip backlog only)
- workspace queries/hooks expose squads where needed for the
pickers
- locales updated for new picker copy
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): agent-style detail page with members + instructions tabs
- restructure squad detail page to mirror the agent detail page:
320px inspector (creator, leader, created/updated) + tabbed
pane (Members | Instructions) with dirty-guard AlertDialog
- inline name + avatar editing on the inspector
- inline description editor (modal textarea)
- members tab: leader + member picker with role descriptions,
swap leader, edit member roles, remove
- instructions tab: ContentEditor + Save (mirrors agent pattern)
- squads list shows the squad avatar/icon
- core types + api.updateSquad accept avatar_url + instructions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): inject leader briefing on claim (protocol + roster + instructions)
When a squad's leader agent claims a task on a squad-assigned issue,
append a system-level briefing to the agent's Instructions composed of:
1. Squad Operating Protocol — hard-coded rules: leader is a
coordinator, dispatch via @mention, stop after dispatching,
resume on re-trigger, do not work outside the roster.
2. Squad Roster — leader self-row plus one row per non-archived
member with a literal mention markdown string ([@Name](mention://
agent|member/<UUID>)) the leader can paste verbatim. Round-trips
through util.ParseMentions, enforced by a contract test.
3. Squad Instructions — the user-defined squad.instructions block,
omitted entirely when empty so we do not leave a dangling heading.
Non-leader members claiming the same issue receive no briefing.
Tests cover: full squad with mixed agent/human members, lone leader,
archived agents skipped, empty user instructions, mention round-trip,
and the leader/non-leader claim-handler gate.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(squad): tell leader not to restate issue context in dispatch comment
After observing leaders padding their delegation comments with full
re-summaries of the issue body and prior discussion, make the
Operating Protocol explicit:
- assignees on Multica already have the full issue (title,
description, all comments, attachments) and workspace context;
- delegation comments should add only what cannot be inferred
(who is picked, why, extra constraints), aim for two or three
sentences;
- restating context is now an explicit hard rule violation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(squad): unify leader evaluation into activity_log, add CLI command
- Squad member comments now trigger leader (only leader self-excluded)
- Replace squad_activity_log with activity_log (action: squad_leader_evaluated)
- Add CLI: multica squad activity <issue-id> <outcome> --reason
- Add API: POST /api/issues/{id}/squad-evaluated
- Update squad operating protocol to require evaluation recording
- Remove squad_activity_log table from schema and generated code
* feat(cli): add squad list, get, member list commands
* fix(squad): address review findings (P1+P2)
P1 fixes:
- Add 'squads' to reserved_slugs.json (source of truth)
- Add 'create-squad' to ModalType union
- Remove unused leaderOpen/selectedLeader in create-squad modal
- Replace literal JSX strings with i18n selectors (en + zh-Hans)
P2 fixes:
- Add 'squad' to mention regex (MentionRe)
- Fix human member lookup in squad briefing (use GetUser directly)
- Add squads routes to desktop app
- Add squad:created/updated/deleted to WSEventType + invalidation
- Reject archived squads as issue assignees
* fix(squad): restore zh-Hans key, publish activity event, invalidate issues on archive
- Restore create_project.title in zh-Hans modals.json (dropped by prior edit)
- Publish activity:created WS event after squad leader evaluation
- Invalidate issue queries on squad:deleted (archive transfers assignees)
- Add creator info to squad list cards
* fix(squad): realtime sync, rerun support, leader validation
- Use workspaceKeys.squads prefix for detail/member queries (realtime invalidation)
- Publish squad:updated after add/remove/role-change member mutations
- Support rerun for squad-assigned issues (targets leader agent)
- Reject assignment to squads whose leader is archived
---------
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
454c8e3d1a |
feat: in-app preview for non-image attachments (#2528)
* feat(storage): add GetReader to Storage interface Adds a streaming read method to the Storage abstraction so callers can pull object bytes without forcing a full in-memory load. S3Storage wraps GetObject; LocalStorage opens the file with path-traversal and sidecar guards. Tests cover happy path, traversal rejection, sidecar rejection, and missing key. Used in the next commit by the attachment-preview proxy endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add attachment preview proxy endpoint GET /api/attachments/{id}/content streams the raw bytes of a text-previewable attachment back to the client. Exists to (a) bypass CloudFront CORS, which is not configured on the CDN, and (b) bypass Content-Disposition: attachment which Chromium honors for iframe document loads. Media types (image/video/audio/pdf) intentionally do NOT go through this endpoint — clients render them directly from the signed CloudFront download_url, which is already served with Content-Disposition: inline. Hard cap: 2 MB. Larger files return 413. Anything outside the text whitelist returns 415. The whitelist (isTextPreviewable) mirrors the client-side dispatcher; the cross-reference comment in file.go flags the manual sync until a JSON SSOT generator lands. Response always uses Content-Type: text/plain; charset=utf-8 so a hostile HTML payload can't be re-interpreted as a document. The original MIME ships via X-Original-Content-Type for client dispatch. Cache-Control: no-store so revoked attachment access takes effect immediately on the next request. Tests cover happy path (md), extension fallback when content_type is generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and the isTextPreviewable table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core/api): add getAttachmentTextContent + preview error types Adds an ApiClient method that fetches the text body of an attachment via the new /api/attachments/{id}/content proxy. Two typed errors — PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the preview modal render specific fallbacks instead of a generic failure. Refactors the private fetch() into a shared fetchRaw() helper so the new method inherits the standard infra: auth headers, 401 → handleUnauthorized recovery, X-Request-ID, error logging, and the ApiError contract. The previous draft bypassed all of these by calling window.fetch directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(views/editor): add AttachmentPreviewModal + Eye entry points In-app preview for non-image attachments. An Eye icon now sits next to the existing Download button on file cards / readonly file cards / the standalone AttachmentList. Clicking it opens a full-screen modal that dispatches by content_type: pdf: <iframe src={download_url}> — Chromium PDFium video/*: <video controls src={download_url}> — native controls audio/*: <audio controls src={download_url}> — native controls md: <ReadonlyContent> — full markdown pipeline html: <iframe srcdoc sandbox=""> — fully restricted text: <code class="hljs"> — lowlight highlight Media types render directly from the signed CloudFront download_url (server marks them inline-disposition). Text types fetch through the new /api/attachments/{id}/content proxy via TanStack Query, wrapped in useAttachmentPreview() so each entry point owns its own modal state without depending on a global Provider mount. Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger than create-issue's max-w-4xl since PDF / video need room, but capped to viewport on small screens. Sub-renderers use h-full to follow the fixed modal height instead of viewport-relative units. Images are intentionally NOT touched — the existing ImageLightbox (extensions/image-view.tsx) already handles them correctly. The new modal would be churn without user-visible benefit. Adds i18n keys under attachment.* (en + zh-Hans) and registers Preview/Download/Upload in the conventions glossary so future translations stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(desktop): enable Chromium PDF viewer for attachment preview Adds webPreferences.plugins: true to the main BrowserWindow so the bundled Chromium PDFium plugin activates inside iframes — required for the attachment preview modal's PDF dispatch. Default is false in Electron; without it <iframe src=*.pdf> renders blank. Security trade-off, accepted intentionally and documented inline: 1. This window already runs with webSecurity: false + sandbox: false, so plugins: true does NOT meaningfully widen the renderer's attack surface beyond what is already accepted. 2. The only PDFs that reach an iframe here are signed CloudFront URLs we ourselves issued; user-supplied URLs are routed through setWindowOpenHandler → openExternalSafely and cannot land in this renderer. 3. Chromium's PDFium plugin is itself sandboxed and only handles application/pdf — no Flash/Java/other historical plugin surfaces. If we ever tighten webSecurity / sandbox, the follow-up is to host the PDF viewer in a dedicated BrowserView with plugins scoped to that view, keeping the main renderer plugin-free. Old desktop builds ship without the preview modal, so the Eye button never appears and PDF preview is gated by the same release — zero regression risk for users on stale clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
26924dcc98 |
fix(desktop): restore Multica icon + WM_CLASS on Linux (MUL-2145) (#2525)
Closes the regression reported in https://github.com/multica-ai/multica/issues/2515 that PR #2437 only half-fixed in v0.2.31. Two gaps remained on Ubuntu/GNOME: 1. The .deb shipped only the source 1024×1024 PNG under /usr/share/icons/hicolor/, with no usable smaller sizes. GNOME's hicolor lookup walks 16…512 and falls back to the theme default when none match, so the launcher had no icon. The auto-generation pass in electron-builder silently produced only the source size for us. Drop pre-rendered 16/24/32/48/64/128/256/512 PNGs into build/icons/ and point `linux.icon` at the directory so packaging stops depending on the toolchain re-running that generation correctly. 2. WM_CLASS at runtime was `@multica/desktop`, while the .desktop file declared `StartupWMClass=Multica`. PR #2437 assumed Electron derives WM_CLASS from electron-builder.yml's `productName`, but Electron reads `app.getName()`, which reads the *packaged ASAR's* package.json — productName if present, otherwise name. Our source apps/desktop/package.json had no top-level productName, so the ASAR carried only `name: "@multica/desktop"` and Chromium emitted that as WM_CLASS, breaking the .desktop association and the dock icon. Fixed in two anchors for belt-and-braces: add `"productName": "Multica"` to apps/desktop/package.json (so the ASAR carries it and app.getName() resolves correctly by default), and call `app.setName("Multica")` in the production branch alongside the existing dev-only setName so a future regression in package.json or the build pipeline cannot silently re-break WM_CLASS. The `StartupWMClass: Multica` declaration in electron-builder.yml stays pinned and the surrounding comment has been rewritten to record the correct WM_CLASS derivation. Verification on a real Ubuntu install: - `dpkg-deb -c multica-desktop-*-linux-amd64.deb | grep hicolor` lists ≥8 sizes. - `xprop WM_CLASS` on the running window prints `"multica", "Multica"`. - Launcher and dock both show the Multica logo with no manual ~/.local/share/icons workaround. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6a48022123 | fix(desktop): prevent tab close router sync loop (#2393) | ||
|
|
7fcc8159ba |
fix(desktop): route attachment downloads through Electron native system on Linux (#2441)
* fix(desktop): route attachment downloads through Electron native system on Linux Replaces shell.openExternal with webContents.downloadURL for attachment downloads in the Electron desktop app. On Linux/Ubuntu, opening a CloudFront URL serving Content-Type: text/html via the system browser causes the browser to render the HTML inline instead of downloading. Electron's native downloadURL shows a save dialog and saves the file directly, fixing HTML downloads regardless of Content-Type. * test(views): update desktop download test to match the new downloadURL bridge The test still referenced the old openExternal bridge. Updated it to assert desktopAPI.downloadURL() instead. * fix(desktop): add URL scheme allowlist to download IPC handler Addresses review feedback on PR #2441. The file:download-url IPC handler called webContents.downloadURL directly, bypassing the http/https allowlist enforced by openExternalSafely. Adds downloadURLSafely() alongside the existing openExternalSafely wrapper, reuses the same isSafeExternalHttpUrl check, and extends the ESLint no-restricted-syntax rule to ban direct webContents.downloadURL calls. Also handles nits: observable warning on null mainWindow, removes dead openExternal field from DesktopBridge, adds desktop-branch failure test. |
||
|
|
451c46c43f |
refactor(usage): rename Dashboard → Usage + dynamic per-agent leaderboard (#2511)
The page added in #2462 lived at `/{slug}/dashboard` and was titled "Dashboard", which collides with the conventional meaning ("personal landing surface") and doesn't tell new users what the page is for. Its actual contents — token spend, cost, run time, task counts — map cleanly onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that. Renames (user-visible) - Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop memory router) - Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon LayoutDashboard → BarChart3 (page header icon swapped in sync) - Page title in en/zh-Hans - Reserved-slugs: add `usage` to workspace route segments group; `dashboard` stays reserved in the marketing group (back-compat against workspace slug collisions + keeps the name free for a future Home page) - i18n namespace `dashboard` → `usage` across resources-types.ts, locales/index.ts, and the moved JSON files - WORKSPACE_ROUTE_SEGMENTS in editor link-handler - paths.workspace(slug).dashboard() → .usage(), with matching test expectation updates Per-agent leaderboard polish (`packages/views/dashboard/components/ dashboard-page.tsx`) - Card title "Cost & run time by agent" → "Leaderboard" with a 4-way Segmented control: Tokens / Cost / Time / Tasks - Active metric drives row order, progress-bar width, and the emphasised column header / cell — keeping ranking, visual quantity, and column emphasis in lockstep so users always see what's being measured - Default sort = Tokens (most universally meaningful; Cost still one click away) - Project filter dropdown: - Show ProjectIcon next to the selected project + each list item; FolderKanban as the "All projects" fallback (matches ProjectPicker language) - alignItemWithTrigger={false} so "All projects" doesn't get pushed above the trigger and clipped when the header sits at the top of the viewport (was the root cause of "can't re-select All projects" once a project was selected) - max-h-72 to cap the dropdown when workspaces accrue many projects; matches the runtime-detail Select precedent - Folder name `packages/views/dashboard/*` and `DashboardPage` component name intentionally left in place — user-visible rename only, no broad code refactor. Old `/dashboard` routes are not redirected because the page only landed in #2462 (a few days ago); no real users, external links, or desktop-tab persistence have settled on it yet. |
||
|
|
96695a79c5 |
feat(dashboard): workspace/project token + run-time dashboard MUL-1882 (#2462)
* feat(dashboard): workspace/project token + run-time dashboard
Add a `/{slug}/dashboard` page showing per-agent token spend and execution
time across the whole workspace, with an optional project filter.
Backend:
- Three new sqlc queries against task_usage + agent_task_queue: daily
usage, per-agent usage, per-agent total run-time. All optionally
scoped to a project via sqlc.narg('project_id'), reaching project
through the issue join.
- Handlers under /api/dashboard return the same wire shape the runtime
page already consumes (model preserved for client-side cost math).
Frontend: - Shared DashboardPage in packages/views/dashboard reusing KpiCard,
DailyCostChart, ActorAvatar, and estimateCost from the runtime page
so the visual style and pricing math stay in lock-step.
- Period selector (7/30/90d), project dropdown, four KPI tiles
(cost, tokens, run time, tasks), daily cost chart, and a combined
"cost + run time by agent" list.
- Routed in both web (app/[slug]/(dashboard)/dashboard) and desktop
(memory router); sidebar nav entry added under Workspace group.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard): drop stale project filter and stop double-counting tasks
Two issues caught in PR #2462 review:
1. Project filter held the previous selection's UUID across workspace
switches and project deletions: the dropdown gracefully showed
"All projects" (because the title lookup missed) while the three
dashboard queries kept forwarding the dead UUID, leaving the UI
looking like a full-workspace view but populated with empty
project-scoped data. Validate the picked UUID against the current
projects list before passing it to the queries.
2. The "by agent" table read its task count from the token rollup,
which is grouped per (agent, model). A single task that spans two
models lands twice and the agent's row reads e.g. "2 tasks" when
the real count is 1. Prefer `ListDashboardAgentRunTime`'s per-agent
distinct count when available; fall back to the token aggregate
only for agents with no terminal run yet (in-flight tasks).
Extract the merge into `mergeAgentDashboardRows` so the precedence
rules are unit-tested directly.
Co-authored-by: multica-agent <github@multica.ai>
* test(dashboard): allocate per-workspace issue.number explicitly
TestDashboardEndpoints creates two issues in the shared fixture
workspace. issue.number defaults to 0 (migration 020), and the table
carries UNIQUE (workspace_id, number), so the second insert raced the
first on the same default and failed in CI.
Allocate MAX(number) + 1 per insert so each row gets a fresh number
without stepping on rows other tests left behind in the same workspace.
Co-authored-by: multica-agent <github@multica.ai>
* feat(dashboard): rollup table + cron-driven aggregation for dashboard
Mirror the per-runtime rollup in `task_usage_daily` (migrations 073/077/082)
to remove the per-request raw aggregation the dashboard was doing.
Migration 084 adds:
- `task_usage_dashboard_daily` keyed on
(bucket_date, workspace_id, agent_id, project_id, model) — the
dimensions the dashboard actually queries, with project_id nullable
via UNIQUE NULLS NOT DISTINCT (PG15+) so "no-project" buckets
upsert cleanly.
- `task_usage_dashboard_rollup_state` watermark table.
- `task_usage_dashboard_dirty` invalidation queue.
- Triggers on agent_task_queue DELETE, task_usage DELETE, and
issue.project_id UPDATE — the cases the updated_at watermark can't
see. The project_id trigger re-attributes existing rollup rows when
a user moves an issue across projects.
- `rollup_task_usage_dashboard_daily_window(from, to)` —
idempotent recompute primitive (same shape as 077).
- `rollup_task_usage_dashboard_daily()` cron entry — own advisory
lock (4244) so it serialises independently of the runtime rollup.
- `task_usage_dashboard_rollup_lag_seconds()` health helper.
Sqlc queries `ListDashboardUsageDailyRollup` /
`ListDashboardUsageByAgentRollup` read from the new table; the handler
dispatches between rollup and raw on a separate
`UseDailyRollupForDashboard` config flag
(`USAGE_DASHBOARD_ROLLUP_ENABLED` env). Same fail-safe default (false →
raw) so operators can roll out independently of the per-runtime flag.
Bucket date is UTC (the dashboard aggregates across runtimes that may
sit in different tzs; there's no single correct local boundary).
Adds `cmd/backfill_task_usage_dashboard_daily` mirroring the existing
per-runtime backfill — operator runs it once before flipping the flag.
Tests: - TestDashboardEndpoints now also exercises the rollup read path
(raw vs. rollup, same project-scoped totals).
- TestDashboardRollupReattributesOnProjectChange verifies the
issue.project_id trigger enqueues both old + new buckets and the
next rollup tick zeroes the old project + populates the new one.
Co-authored-by: multica-agent <github@multica.ai>
* fix(dashboard-rollup): close two invalidation gaps
Two leak paths missed by migration 084 review:
1. Issue cascade DELETE — the atq BEFORE DELETE trigger runs AFTER the
issue row is gone, so `LEFT JOIN issue` returns NULL project_id and
the original-project bucket never gets cleared (issue 077 calls this
out for the runtime rollup but didn't need to act on it). Adds an
`issue BEFORE DELETE` trigger that enqueues using OLD.project_id
while the issue row is still readable.
2. `LinkTaskToIssue` (quick-create task attaching to a real issue post-
completion) UPDATEs `agent_task_queue.issue_id` from NULL to a real
id. Migration 084 only watched DELETE on atq, so usage already
rolled up under the no-project bucket stayed attributed to NULL
forever. Extends the atq trigger to fire on UPDATE OF issue_id too,
enqueueing both OLD (NULL project) and NEW (linked issue's project).
Tests: - TestDashboardRollupClearsOnIssueDelete asserts rollup row drops to
zero after issue delete + rollup tick.
- TestDashboardRollupReattributesOnLinkTaskToIssue verifies tokens
move from the NULL bucket to the project bucket after the UPDATE.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
b58567ed6c |
fix(desktop): restore Multica app icon on Linux (#2437)
Fixes three gaps in the Linux desktop build that combined to render the
Multica window with the system Settings (gear) icon on Ubuntu:
1. Force `linux.executableName: multica` so the scoped npm name
`@multica/desktop` stops leaking into `executableName`, the `.desktop`
filename, the `Icon=` field, and `/usr/share/icons/hicolor/*/apps/*.png`.
The leading `@` in the previously-generated `@multicadesktop` violates
freedesktop desktop-entry naming, breaking GNOME's window↔.desktop
association and forcing the theme-default icon. (The artifact-filename
side of the same scoped-name leak was already patched in 10618b1f;
this commit closes the desktop/icon-identity side.)
2. Always set `BrowserWindow({ icon })` on Linux — previously gated on
`is.dev`. AppImage direct-launches never install the `.desktop` entry,
so without an explicit window icon the WM has no other path to the
bundled image. The resolved path now points into `app.asar.unpacked/`
(matching the existing `bundledCliPath()` convention in
`daemon-manager.ts`) since the Linux native icon code path requires a
real filesystem path, not an asar-internal one.
3. Pin `linux.desktop.entry.StartupWMClass: Multica` explicitly. The
value already matches the productName-derived default, so this is a
build-time no-op today, but it makes the WM_CLASS↔StartupWMClass
matching contract auditable in config — future changes to
`productName` or `app.setName()` now show up as a diff against this
file instead of silently re-breaking the icon association.
Fixes https://github.com/multica-ai/multica/issues/2424.
|
||
|
|
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> |
||
|
|
00ba0aa4e6 |
fix(desktop): replace Electron placeholder icons with Multica asterisk for Windows + Linux (#2248)
Both `apps/desktop/build/icon.ico` (Windows installer + Multica.exe) and `apps/desktop/build/icon.png` (Linux deb/rpm/AppImage) were the default electron-vite scaffold "atom" placeholder. They were never updated when the macOS `icon.icns` was switched to the Multica asterisk in #1074, and have shipped as-is in every v0.2.x release including v0.2.26 — closes GitHub #2195. Source: 1024×1024 PNG extracted from the existing build/icon.icns (icon_512x512@2x), so all three platforms now share the same artwork. - icon.ico: BMP frames at 16/24/32/48/64/128 + PNG-compressed 256×256. Matches electron-builder's "≥256×256" requirement and the BMP-then-PNG format mix Windows Explorer / NSIS render best across Win10/11. - icon.png: 1024×1024 RGBA, replacing the previous 512×512 placeholder. No electron-builder.yml change needed — buildResources: build picks both files up automatically. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
820d57535e |
feat(desktop): load runtime self-host config (#2012)
* feat(desktop): load runtime self-host config Co-authored-by: multica-agent <github@multica.ai> * docs: document desktop runtime self-host config Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): address runtime config review feedback Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Cheese <congvc@congvc-c00.taila6fa8a.ts.net> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: congvc <congvc-dev@gmail.com> |
||
|
|
ae9098637d |
feat(analytics): suppress PostHog $pageview on desktop tab/workspace switches (#2033)
* feat(analytics): suppress PostHog $pageview on desktop tab/workspace switches Desktop tab switches were emitting a $pageview every time the user clicked between already-open tabs (or workspaces), since the tracker fired on any change to the resolved active path. Real-data audit showed this was the single largest source of PostHog quota burn — desktop accounted for 51% of all $pageviews at ~34 pv/user/30d vs web's ~10 — and the re-emitted paths add no signal because the original navigation already fired. Detect "tab switch" as `(workspace, tabId)` identity changing while the surface stays `tab`, and skip the capture in that case while still updating the ref so the next in-tab navigation compares against the right baseline. Login transitions, overlay open/close, and intra-tab navigation continue to fire as before. Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): only suppress $pageview for re-activations of known tabs Prior commit suppressed every (workspace, tabId) change while the surface stayed `tab`, which also swallowed the first $pageview for newly opened tabs (`openInNewTab` / `addTab`) and for cross-workspace `switchWorkspace` into a not-yet-seen tab. Track an observed `(workspace, tabId) → path` map seeded from the persisted tab store on mount. Suppress only when the active key is already in the map AND its recorded path matches the current path — i.e. genuine re-activation of an already-known tab. New tabs and cross-workspace navigation to a fresh tab now correctly emit one pageview. Adds a vitest covering the three behaviors GPT-Boy flagged plus the intra-tab navigation, overlay/login transitions, and persistence-restored mount paths. Wires the `@/` alias into `vitest.config.ts` so component tests can resolve renderer-relative imports. Co-authored-by: multica-agent <github@multica.ai> * refactor(analytics): reuse tab-store helpers and inline observed-tabs seed Replace the two ad-hoc tab selectors with the existing `useActiveTabIdentity()` + `getActiveTab()` helpers from tab-store, which already provide the (slug, tabId) primitive pair and the active tab lookup with the same stability guarantees. Move the observed-tabs Map seeding from a useEffect into a synchronous first-render initializer. The seed runs once per mount before any state-driven effect, so the previous useEffect-then-defensive-fallback pattern in the second effect was unreachable. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
51bc5a818f |
fix(onboarding): decouple from workspace state and route invitees correctly (#1936)
PR #1868 conflated "has workspace" with "completed onboarding" — restore `onboarded_at` as the single signal, and route invited users through a dedicated /invitations page before they ever see onboarding. - Backend: CreateWorkspace + AcceptInvitation atomically set onboarded_at alongside the member insert, establishing the invariant "member row exists ↔ onboarded_at != null" at the DB layer. - Migration 065: one-shot backfill closes the dirty rows produced by PR #1868 (users with a workspace but onboarded_at == null). - Entry points (web callback, login, desktop App): if onboarded_at is null, look up pending invitations by email and route to the new batch /invitations page; otherwise the resolver picks workspace / new-workspace as before. - OnboardingPage: stops bouncing on hasWorkspaces; only hasOnboarded bounces. Unblocks the user from completing Step 3 (workspace creation) → Steps 4 / 5. - StarterContentPrompt: only shows when the user is the solo member of the workspace, so invited users never get prompted to import starter content into someone else's workspace. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
68ed2a32d9 |
fix(desktop): prevent Cmd+R / Ctrl+R / F5 from reloading the page (#1896)
In a desktop app an accidental page reload destroys in-memory state (tabs, drafts, WS connections) with no URL bar to navigate back. Add a before-input-event listener on the main BrowserWindow that intercepts Cmd+R / Ctrl+R (with or without Shift) and F5, calling preventDefault() to block the reload. DevTools refresh still works. |
||
|
|
5bf0e7022d |
fix(auth): route invitees to their workspace instead of forcing /onboarding (#1868)
* fix(auth): route invitees to their workspace instead of forcing /onboarding Workspace presence now wins over `onboarded_at` across every post-auth entry point, so a user invited into an existing workspace lands inside that workspace instead of being trapped in the new-workspace wizard. The redesigned onboarding flow (#1411) intentionally flipped the priority during frontend development so every login re-entered /onboarding; the backend `onboarded_at` field shipped but the flipped priority was never restored. Closes #1837. - packages/core/paths/resolve.ts: has-workspace beats !hasOnboarded. Onboarding is reachable only when the user has zero workspaces. - apps/web/app/auth/callback/page.tsx: drop the early-return on !onboarded so a `next=/invite/<id>` survives Google OAuth round-trips. - apps/web/app/(auth)/login/page.tsx: same removal in both the already-authenticated effect and the post-login handler. - packages/views/layout/use-dashboard-guard.ts: stop bouncing in-workspace users to /onboarding; rely on the resolver for zero-workspace cases. - apps/desktop/src/renderer/src/App.tsx: window-overlay now opens onboarding only when wsCount === 0 AND !hasOnboarded. - apps/web/app/(auth)/onboarding/page.tsx: defense-in-depth — bounce away if the visitor already has a workspace, even on direct URL access. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(auth): fix URLSearchParams leaking state across callback tests The previous cleanup `mockSearchParams.forEach((_v, k) => mockSearchParams.delete(k))` silently skipped entries because forEach advances its index while the underlying URLSearchParams shrinks, so a `state=next:/invite/...` set in one test bled into the next. Snapshot keys via Array.from before deleting. Also rewrites the assertions to match the new policy: an unonboarded user with a safe `next=` honors it, with a workspace lands in that workspace, and only with zero workspaces falls back to /onboarding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
665ac39730 |
fix(ci): restore frontend checks (#1878)
Co-authored-by: Devv <devv@Devvs-Mac-mini.local> |
||
|
|
e024ab1232 |
fix(desktop): show git-described version in dev instead of stale 0.1.0 (#1867)
Packaged builds are unaffected: scripts/package.mjs already injects the git tag into electron-builder's extraMetadata.version, so the .app users download from GitHub Release reports the right version through app.getVersion() and the auto-updater's latest.yml comparison works correctly. Dev mode (`pnpm dev:desktop`) didn't go through that path though, so app.getVersion() returned the static "0.1.0" from package.json — the new Settings → Updates panel surfaced this and made it look like the dev build was ancient. Add a tiny getAppVersion() helper that falls back to `git describe --tags --always --dirty` only when !app.isPackaged, and use it for the app-info IPC. No change to packaged behavior; if git is unavailable for any reason, we silently fall back to app.getVersion(). |
||
|
|
f4eb83bd41 |
feat(desktop): show current version in Updates settings (#1861)
Surface the running app version (from app.getVersion via preload's appInfo) at the top of Settings → Updates so users have a clear place to check which build they're on, instead of only seeing it inline after clicking "Check now". |
||
|
|
866b901943 |
fix(desktop): use themed Toaster wrapper instead of bare sonner (#1835)
#1831 fixed the Toaster wrapper to follow next-themes' resolvedTheme, but the desktop renderer was importing `Toaster` directly from `sonner` and never going through the wrapper. So the success toast still rendered light on a dark UI. Switch the import to `@multica/ui/components/ui/sonner` to match the web app and pick up the theme + icon overrides. |
||
|
|
21e3cfaa01 |
Agent runtime status redesign: split presence into availability + last-task (#1794)
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type Lays the API + type contract for the front-end agent presence cache: - New `GET /api/active-tasks` returns active (queued/dispatched/running) tasks plus failed tasks within the last 2 minutes for the current workspace. The 2-minute window powers a UI-side auto-clearing "Failed" agent state without back-end pollers. - `agent_task_queue` has no workspace_id column, so the query JOINs agent; `SELECT atq.*` keeps `failure_reason` (migration 055) on the wire. - Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend classifiers (agent_error / timeout / runtime_offline / runtime_recovery / manual) to copy without parsing free-text errors. - New `api.getActiveTasksForWorkspace()` client method; workspace is resolved server-side from the X-Workspace-Slug header (no path param, matching /api/agents and /api/runtimes conventions). Includes the joint engineering plan and designer brief that scope the broader Agent / Runtime status redesign — Phase 0 is this contract plus the front-end derivation layer landing in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge Adds the front-end derivation layer that turns raw server data into the user-facing 5-state agent / 4-state runtime enums. UI files are deliberately untouched in this commit — derivation lives behind hooks (useAgentPresence, useRuntimeHealth) that any component can call with zero additional network traffic. Architecture: - Derivation is pure functions in packages/core/{agents,runtimes}; the back-end stays free of UI translation. Agents algorithm: runtime offline > recent failed (2-min window) > running > queued > available. Runtimes algorithm: status + last_seen_at -> online / recently_lost / offline / about_to_gc. - A single workspace-wide active-tasks query backs all per-agent presence reads, eliminating N+1 across hover cards, list rows, and pickers. 30-second tick re-renders the hooks so the failed window expires even when no underlying data changes. - WS task lifecycle events (dispatch / completed / failed / cancelled) invalidate active-tasks via the prefix dispatcher. completed/failed were removed from specificEvents so they go through both the prefix invalidate and the existing chat ws.on() handlers. Reconnect refetch picks up active-tasks too. - Desktop bridges window.daemonAPI.onStatusChange directly into the runtimes cache via setQueryData, giving the local daemon sub-second feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace switches automatically rebind the subscription; daemon_id matching covers the same-daemon-multiple-providers case. 24 derivation unit tests cover all branches plus null/empty/boundary inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing completed_at). Full core suite: 112 tests passing. Typecheck green across all 8 workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(agent-status): redesign agent runtime status as two orthogonal dimensions Splits the conflated 5-state agent presence into two independent axes: - AgentAvailability (3-state): online / unstable / offline — drives the dot indicator everywhere a dot appears. Pure runtime reachability; never sticky-red because of a past task outcome. - LastTaskState (5-state): running / completed / failed / cancelled / idle — surfaced as text + icon on focused surfaces (hover card, agent detail page, agents list, runtime detail). Never colours the dot. Major changes: * Domain layer: AgentPresence union → AgentAvailability + LastTaskState. derive-presence split into deriveAgentAvailability + deriveLastTaskState + deriveAgentPresenceDetail orchestrator. Tests reorganised into three groups (availability invariants, last-task invariants, composition). * Visual config: presenceConfig (5 entries) → availabilityConfig (3) + taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips. * Workspace-level presence prefetch: new useWorkspacePresencePrefetch hook + WorkspacePresencePrefetch mount component, wired into DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards render synchronously with no skeleton flash on first hover. * ActorAvatar hover: flipped default — disableHoverCard removed, enableHoverCard added (default false). Opt-in at ~14 decision-moment surfaces; pickers / decoration sub-chips stay plain. Status dot decoupled (showStatusDot prop) so picker rows can show presence without nesting popovers. * Hover cards: AgentProfileCard simplified — availability dot only, Detail link top-right (logs live on the detail page). New MemberProfileCard mirrors the structure: name + role + email + top-2 owned agents (sorted by 30d run count) with click-through to agent detail. * Agents list: split Status into two columns — availability (3-color dot + label) and Last run (task icon + label, optional running counts). Two independent filter chip groups (Status + Last run); combination acts as intersection ("online + failed" finds broken- but-alive agents). * Other UI surfaces (issue list/board/detail, comments, autopilots, projects, runtimes, mention autocomplete, subscribers picker) updated to the new dot semantics; status dot now strictly 3-color. Server changes accompany the client redesign — workspace-wide agent-task-snapshot endpoint, runtime usage queries, etc. — to feed the derive layer with the data it needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-detail): drop last-task chip from detail header + inspector The Recent work section on the agent detail page already shows the same data (with task titles, timestamps, error context) — surfacing "Completed" / "Failed" / etc. up in the header was redundant chrome. Detail surfaces now show only the 3-state availability dot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tables): handle narrow viewports across agents / skills / runtimes Three table layouts were squeezing content into adjacent cells at intermediate widths. Each fix is small and targeted: * runtime-list: the Runtime cell's base name had `shrink-0`, so it refused to truncate when its grid column was narrowed under width pressure — the name visually overflowed into the Health column ("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health column was also a fixed 9.5rem reservation for the worst-case "Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it competes fairly with Runtime. * skills-page: had a single grid template with no responsive breakpoints — all 6 columns were rendered at any width and got visually jammed below md. Added a <md template that drops Source + Updated; the row markup hides those cells via `hidden md:block` / `md:contents`. * agent-list-item: the new Last run column was reserved at minmax(8rem, max-content); on narrow md viewports the 8rem floor pushed the row past available width. Changed to minmax(0,max-content) so the cell shrinks under pressure (its content already truncates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(agent-card): hover-only Detail + add Runtime row + breathing room Three small polish tweaks to the agent hover card: - Detail link gets `mr-1` + fades in only on card hover (group-hover). It was visually flush against the popover edge and competing for attention; now it stays out of the way during a quick glance and surfaces only when the user is dwelling on the card. - Runtime row is back, in the meta block (cloud/local icon + runtime name). The earlier removal was over-aggressive — knowing where an agent runs is part of "who is this agent". The wifi badge stays dropped because the availability dot in the header already conveys reachability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(runtime): wifi-style health icon (4-state) for runtime list + agent card Replaces the 6px coloured dot with a wifi-shape icon that carries both state (Wifi vs WifiOff) and severity (success/warning/muted/destructive). Mapping: - online → Wifi (success) - recently_lost → WifiHigh (warning) — transient hiccup, fewer bars - offline → WifiOff (muted) — long unreachable - about_to_gc → WifiOff (destructive) — sweeper coming soon Used in two places: - Runtime list: replaces HealthDot in the dedicated leading-icon column. Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized). - Agent profile card RuntimeRow: derives runtime health from runtime + clock (matching the 4-state semantics) and renders HealthIcon next to the runtime name. Cloud runtimes always read as online. The duplicate signal with the header availability dot is intentional — it confirms WHICH runtime is the one currently in the dot's state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
01855f6b09 |
revert(chat): Chat V2 — restore right-bottom floating drawer (#1580) (#1792)
* Revert "fix(chat): prevent UI flicker when streaming response finalizes (#1583)" This reverts commit |
||
|
|
bbe73ade8b |
feat(desktop): dock unread badge + focus-gated inbox notifications (#1445)
* feat(desktop): dock unread badge + focus-gated inbox notifications Wire two OS-level integrations for inbox activity. Both degrade cleanly on web and unsupported platforms. - Unread badge on the macOS dock / Linux Unity launcher. Derived from the same inbox list the UI renders, deduplicated per issue, capped as "99+" on macOS via `app.dock.setBadge` (setBadgeCount truncates at 99). New `useInboxUnreadCount` hook (core/inbox) + `useDesktopUnreadBadge` (views/platform) keep renderer and main in sync via a `badge:set` IPC. - Native OS notification on `inbox:new`, fired from the renderer only when `document.hasFocus()` is false — in-focus feedback is the existing inbox sidebar's unread styling, so we don't fight macOS's deliberate foreground suppression. Clicking the banner focuses the main window and navigates to `/inbox?issue=<key>` via the shared `multica:navigate` bus. Refactors `inbox-page.tsx` to read the unread count through the new hook (was a per-render inline filter). * fix(desktop): pin notification routing to source workspace + mark read on URL select Two bugs GPT-Boy caught on PR #1445: 1. A notification from workspace A used `getCurrentSlug()` at click time, so if the user switched to workspace B before clicking the banner (macOS Notification Center persists banners), routing landed on `/B/inbox?issue=<A key>` and 404'd. Fix: round-trip the emit-time `slug` through the IPC payload and use it in the click handler. 2. Notification-click navigation set the URL param but never fired the mark-read mutation (only InboxPage's click-handler did). The row stayed unread and the dock badge didn't decrement. Fix: move the mark-read logic from handleSelect into a useEffect keyed on the selected item — it now covers both click-to-select and URL-param-select. IPC payload gains `slug` and `itemId`; preload types + main handler + the desktop bridge are updated to match. |
||
|
|
2e7da8c63f |
fix(desktop): disable RPM build-id symlinks to avoid Slack conflict (#1734)
Electron apps share an identical upstream Electron binary, so its GNU build-id is the same across every Electron RPM (Slack, VS Code, Discord, etc.). The default fpm/rpm behavior owns /usr/lib/.build-id/<hash> symlinks, which collide between packages and make `dnf install` fail when any other Electron app is already installed. Pass `_build_id_links none` to rpmbuild via fpm so the multica-desktop RPM no longer claims those paths. Fixes multica-ai/multica#1723. |
||
|
|
7067d8f125 |
refactor(skills): redesign list page and add skill detail page (#1607)
* feat(core): add skill detail path and query helpers - paths.workspace(slug).skillDetail(id) → /:slug/skills/:id - skillDetailOptions(wsId, skillId) for fetching a single skill - selectSkillAssignments(agents) folds the cached agent list into Map<skillId, Agent[]>; returns a stable reference so consumers can memoize against agent-array identity without re-rendering on unrelated agent updates Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(views): add cross-platform openExternal helper On Electron, route through window.desktopAPI.openExternal so the http/https-only guard in the main process kicks in — direct window.open inside Electron opens a new renderer window instead of handing the URL to the OS shell. On web, fall back to window.open with noopener+noreferrer. SSR-safe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(skills): extract edit-permission hook and origin helper - use-can-edit-skill: mirrors the server's rule (admin/owner ∨ creator) so the UI can hide/disable actions instead of waiting for a 403. Takes wsId explicitly per the repo rule for workspace-aware hooks. - lib/origin: discriminated view over Skill.config.origin (manual / runtime_local / clawhub / skills_sh) so consumers don't spread JSONB parsing across the UI tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(skills): rewrite skills list page and collapse import UI - SkillsPage rewritten: new hero header, single table layout with columns (Name / Used by / Source · Added by / Updated), agent avatar stack per skill, filter tabs aligned with Issues/MyIssues header (Button variant=outline + Tooltip + bg-accent active state). - CreateSkillDialog: dedicated dialog for the manual/import entry points, replaces the inline row-triggered dialog. - runtime-local import: dialog variant deleted; panel is now the single entry point, embeddable inside CreateSkillDialog. Panel covered by a new test. - Deleted runtime-local-skill-row (no longer needed — row rendering lives in SkillsPage directly) and the old skills-page.test.tsx (structure diverged beyond salvaging; will be re-added alongside the detail-page tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skills): add skill detail page and wire routes on web and desktop - SkillDetailPage: dedicated view for a single skill (name, description, origin, assignments, file listing). Uses skillDetailOptions and the new origin / use-can-edit-skill helpers. - apps/web: /:workspaceSlug/skills/:id Next.js route. - apps/desktop: /:slug/skills/:id added to the memory router under WorkspaceRouteLayout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(skills): bump runtime-local-skill-import-panel timeouts for CI The test chains a five-step async cascade (runtime list → setSelectedRuntimeId effect → skills query → auto-select effect → row render). Comfortable on local (~600ms) but tight against RTL's 1 s default on CI where jsdom + Vitest import takes ~100s. Bump findByText and the two waitFor calls to 5 s each — no production behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
35aca57939 |
feat(chat): Chat V2 — sidebar entry + main-area page (#1580)
* feat(chat): Chat V2 — sidebar entry + main-area page Replace the floating drawer + FAB with a first-class workspace route `/:slug/chat`. Sidebar gets a single `Chat` entry under Inbox with an unread dot; session history lives inside the Chat tab via a popover rather than leaking into the global sidebar (keeps Multica's "nouns in the nav" semantic — Inbox / Issues / Projects are work objects, Chat is a tool). - Add `paths.workspace(slug).chat()` + update link-handler route set. - New `ChatPage` view with PageHeader, history popover, centered messages/composer column, and empty-state starter prompts. - Delete `ChatWindow`, `ChatFab`, resize helpers, and standalone `ChatSessionHistory` (history now embedded in the popover). - Drop `isOpen`/`toggle`/`showHistory`/resize fields from `useChatStore` — the page is a route now, not an overlay. - Wire the new `/chat` route on web (App Router) and desktop (react-router + tab-store icon mapping). Addresses MUL-1322. * fix(chat): align composer width with message column The ChatPage wrapper added px-4 on top of ChatInput's own px-5, making the composer 32px narrower than the messages column. Drop the outer px-4 so both share the same max-w-3xl outer + px-5 inner padding provided by ChatMessageList / ChatInput. * fix(chat): taller default composer (~3 lines visible, 8 max) min-h 4rem → 7rem, max-h 10rem → 15rem. Empty state previously showed only 1 text row after pb-9 for the action bar; raise the floor so there's visible writing room and lift the ceiling so a longer draft can grow before scrolling kicks in. * fix(chat): restore anchor + in-flight indicator + cold-start session restore Three issues surfaced by review: 1. ContextAnchorButton always disabled on /:slug/chat — useRouteAnchorCandidate only matches issue/project/inbox pathnames, so moving chat to its own route dropped 'bring the page I was on into the conversation'. Track the last anchor-eligible location globally (new useAnchorTracker mounted in AppSidebar + lastAnchorLocation on useChatStore) and substitute it when on /chat. 2. No global 'Multica is working' cue after ChatFab deletion. Subscribe the sidebar Chat entry to pendingChatTasksOptions and swap the unread dot for a spinner while any chat task is in flight. 3. ChatPage restore effect latched didRestoreRef before the sessions query resolved, so cold-start direct nav to /chat landed on the empty state even when the server had an active session. Wait for isSuccess before locking the ref. * fix(chat): clear lastAnchorLocation on workspace rehydration The pathname captured in workspace A would otherwise be reused against workspace B's wsId, triggering a cross-workspace issue/project fetch and silently leaking anchor context into chat messages. --------- Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
5e51f5b356 |
feat(desktop): add right-click context menu with clipboard actions (#1575)
Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
95bcffef8c |
fix(desktop): expose search params from root navigation adapter (#1547)
DesktopNavigationProvider stubbed `searchParams` to an empty URLSearchParams, so any shell-level consumer of useNavigation() that looked at query params read blanks. The miss surfaced in focus-mode: on /inbox?issue=<id>, ChatWindow's useRouteAnchorCandidate couldn't see the selection, so the Focus button stayed disabled. Mirror the full location (pathname + search) from the active tab's router — same subscription pattern TabNavigationProvider already uses ~30 lines below. InboxPage itself was fine because it's rendered inside TabNavigationProvider; the bug only hit components mounted at the shell root (ChatWindow, ChatFab, and any future sibling). No test: the fix is an identical copy of a production-shipped pattern in the same file, and the mock surface needed to exercise the adapter (useActiveTabRouter + memory router + tab store) exceeds the fix itself. Verified via pnpm typecheck across all packages. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
88b892f1ca |
fix(desktop): preserve last-opened workspace on app start (MUL-1269) (#1515)
The workspace query defaults `data` to `[]` before the first fetch, so the bootstrap effect ran with an empty valid-slug set, wiped the persisted `activeWorkspaceSlug`, then fell back to `workspaces[0]` once the real list arrived — dropping the user on the default workspace on every launch. Gate the effect on `workspaceListFetched` so validation runs only against the real list, and re-read the store after `validateWorkspaceSlugs` to avoid acting on a stale snapshot. Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
205e8c1e9c |
feat(analytics): client_type super-property + Desktop $pageview (MUL-1253) (#1490)
* feat(analytics): client_type super-property + Desktop $pageview (MUL-1253)
Register a `client_type` super-property ("desktop" | "web") plus optional
`app_version` inside `initAnalytics`, so every PostHog event from the
renderer can be split by client without relying on `$lib` (both Electron
and Next.js report "web"). `appVersion` flows in from `ClientIdentity`
via `CoreProvider` → `AuthInitializer`.
Add a Desktop `PageviewTracker` mounted in `DesktopShell` that fires
`$pageview` whenever the active tab's path changes, mirroring the Web
tracker. Restores the `/ → signup → workspace_created` funnel for the
desktop client and enables web-vs-desktop breakdowns.
* fix(analytics): preserve super-props on reset + cover overlay/login pageviews
Two blockers from PR review:
1. `posthog.reset()` wipes persisted super-properties, so after logout or
account switch the next session's events silently dropped `client_type`
and `app_version` until a full reload. Cache the set at init time and
re-register it inside `resetAnalytics()` so the breakdown survives the
auth transition. Added unit tests to pin the invariant.
2. Desktop `PageviewTracker` only watched the active tab path, which
missed pre-workspace overlays (`/onboarding`, `/workspaces/new`,
`/invite/<id>`) — those aren't tab routes on desktop — and also missed
the logged-out `/login` state. Move the tracker to the app root and
derive the visible path from `(user, overlay, activeTabPath)` with
overlay > tab precedence so the `$pageview` stream matches the
surface the user actually sees.
|