mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
agent/lambda/78002cb4
2744 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0bd6ba9354 |
fix(issues): cleaner board card hover with shadow elevation (#1600)
Replace translucent tinted hover (border-accent/50 + bg-accent/20) with a single-dimension shadow lift. The previous overlay was visually weak because --accent is nearly identical to --card, so a 20% tint rendered as almost no change. Active (popup-open) state now uses solid bg-accent so hover and active are distinguished by different dimensions — elevation vs color — instead of competing on the same axis. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
40cea8454d |
feat(autopilot): redesign modal — simpler schema, consistent schedule UI (#1595)
Drop priority and project_id from autopilot. project_id was never exposed in the UI and priority duplicated the agent's own task queue priority. Redesign the create/edit modal as a Runbook (left) + Configuration (right) layout. Rework the Schedule section around a single visual shell so every picker aligns pixel-for-pixel on the same row: - TimeInput (new): segmented HH:MM control adapted from openstatusHQ/time-picker, driven by keyboard (ArrowUp/Down to step, ArrowLeft/Right to jump segment, digit typing with a 2s two-digit window). Replaces <input type="time">, whose native UI broke the design system. Supports a minuteOnly variant for hourly schedules. - TimezonePicker (new): searchable Popover with a fixed-width left check slot so rows stay aligned and GMT offsets never collide with the selected indicator. - Runbook editor now lives in a bordered card, giving the placeholder an input surface instead of bare document flow. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d54daa62c5 |
feat(issues): right-click context menu + unified issue actions (#1594)
* feat(issues): add right-click context menu on list rows and board cards Extract the detail page's ⋯ dropdown (~180 lines of inline JSX) into a shared `useIssueActions` hook plus two thin wrappers so the same action set (status / priority / assignee / due date / sub-issue ops / pin / copy link / delete) can be mounted as both a DropdownMenu and a Base UI ContextMenu. Right-click on any list row or board card now opens the full action menu without entering the detail page. Shell-level modals replace the detail-page-local state for set-parent / add-child / delete-confirm / backlog-agent-hint, so any trigger (detail page, list, board) can open them through `useModalStore`. Detail page detects its own deletion via a query-transition effect, avoiding the need to smuggle callbacks through the store. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(issues): hover and active styling on list rows and board cards Mirror the sidebar's same-color/different-intensity pattern for the new right-click context menu states. Base UI adds `data-popup-open` to the ContextMenuTrigger when the menu is open; `hover:not-data-[popup-open]` suppresses hover feedback on the already-active item. List rows apply the pattern directly to background color (`accent/60` hover, `accent` active). Board cards additionally modulate the card's border and a lighter background tint (`accent/20` hover, `accent/40` active) so the card's own bg/border/shadow identity stays intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(modals): show target issue banner in SetParent/AddChild pickers When triggered from an issue's action menu, the IssuePickerModal now displays a banner at the top showing "Setting parent of" / "Adding sub-issue to" followed by the originating issue's status, identifier, and title. Previously the operation target was only implied by the modal's sr-only title. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(modals): create-issue gains ⋯ overflow menu with parent issue linkage Add a dropdown-menu with "Set parent issue..." / "Remove parent" at the end of the property pill row. The ⋯ button is always the last DOM child of the row so it stays at the tail even when the row wraps to multiple lines. Menu state reflects current selection — unset shows a single "Set parent…" entry, set shows the current identifier plus a separate Remove option. When a parent is set (either via the new menu or via `data.parent_issue_id` from a "Create sub-issue" trigger), a chip appears in the pill row showing "Sub-issue of {identifier}" with the same click-to-change / click-×-to-clear semantics. This replaces the old header breadcrumb disclosure that was neither editable nor visible in the form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(issues): group relationship actions under "More" submenu Nest Create sub-issue / Set parent issue / Add sub-issue inside a `More >` submenu in the issue actions menu (both Dropdown and Context variants). Top-level keeps Status/Priority/Assignee/Due date category submenus plus Pin and Copy link; the relationship ops are lower-frequency and will grow with future relation types (blocks, duplicates, related) that fit the same category. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(modals): create-issue adds Add sub-issue with deferred linking The create modal's ⋯ menu gains an "Add sub-issue..." entry that queues existing issues as children of the new one. Picked issues appear as chips in the pill row (downward arrow, distinct from the upward parent chip), each individually removable. Linking is deferred because the new issue's ID doesn't exist at pick time. Once createIssueMutation resolves, we run updateIssueMutation for every queued child in parallel and surface any partial failures via toast — the new issue itself is already committed and never rolls back. Parent and child pickers exclude each other so a single issue can't occupy both relations simultaneously. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish(issues): add MoreHorizontal icon to "More" submenu trigger The "More" label was visually misaligned because every other top-level entry has a leading icon. Use MoreHorizontal (same icon as the outer ⋯ trigger — semantically "more options, nested") and drop the `inset` padding hack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(modals): drop target-issue banner from IssuePickerModal The banner sat directly above the search input and rendered the target issue with bolder styling than the "Setting parent of" / "Adding sub-issue to" caption, which made it read like a pre-selected search result rather than a context label. Users opening the modal from a menu item already carry the context, so the extra chrome was redundant. Remove the contextIssue / contextLabel API from IssuePickerModal and drop the now-unused issueDetailOptions query in SetParentIssueModal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * polish(modals): exclude current parent from create-issue parent picker Re-opening the parent picker to change the already-set parent used to show that parent in the results — picking it was a silent no-op. Mirror the child picker's exclude-list construction so the current parent is always filtered out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c2e08418f |
feat(docs-site): rewrite docs as bilingual flat content tree (#1591)
* chore(docs-site): add @multica/ui bridge and dev:docs script Link @multica/ui as a workspace dep of @multica/docs so the docs app can consume the shared design tokens (tokens.css, base.css) via a relative import — same pattern the web and desktop apps use. Add a top-level pnpm dev:docs script for a one-command docs dev server (port 4000). Preparation for the docs site rewrite tracked in docs/docs-outline.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(docs-site): apply Multica tokens and pure-sans typography Replace Fumadocs' neutral color preset with a @theme inline bridge that maps the --color-fd-* chrome tokens to Multica's --background / --foreground / --border / --sidebar-* etc. Sidebar, nav, cards now pick up Multica's cool-gray palette automatically, and switching Multica's .dark flips Fumadocs chrome with it. Typography: pure sans (36px / weight 600 / tight tracking h1, h2+h3 tuned to match), landing continuity without serif display. Code blocks: pinned to near-black (oklch(0.12 0.01 250)) regardless of page theme so they read as a continuation of the landing hero surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): add rewrite plan and outline tracker Two planning documents for the docs site rewrite: - docs/docs-rewrite-plan.md — strategic rationale (positioning, reader personas, design principles, visual direction, phase breakdown). - docs/docs-outline.md — execution tracker. 25 v1 pages with per-page entries (source files, audience, what-to-write, what-not-to-write, ⚠️ verify-before-drafting). Workflow: claim via Owner + Status, read source, verify checklist, draft, review, ship. Language: zh only for v1. Outline is the source of truth for scope and status; the earlier "EN first, ZH as Phase 10" line in rewrite-plan.md is superseded. Welcome (§1.1) is claimed under this tracker and currently in 👀 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(docs-site): write first Welcome page (zh) — §1.1 Implements §1.1 Welcome per docs/docs-outline.md. Chinese-first (per outline language decision); terms translated to their clearest Chinese equivalents (issue → 任务, agent → 智能体, daemon → 守护进程, etc.), product proper nouns and commands kept in English. Voice: reference-style, not marketing. Follows google-gemini/docs-writer skill rules (BLUF opener, second-person, active voice, no hype, overview prose before every list). Content: - Opens by describing Multica as a 任务协作 platform and how humans + AI 智能体 share the same 工作区 - Two interaction modes: 分配任务 and 聊天 - 智能体在哪里运行: local daemon (today), cloud runtime (soon, waitlist). 10 providers listed from source (server/pkg/agent/*.go). - Three usage paths split into back-end (Cloud / Self-host) and client (Desktop) choices — Desktop bundles CLI and auto-starts daemon. - Status: 👀 In review. Also simplifies content/docs/meta.json to just ["index"] (placeholder page entries removed; IA skeleton will be populated in Phase 2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(docs-site): wire up client-side Mermaid rendering Add a <Mermaid> React component under apps/docs/components/ that dynamic- imports the mermaid package in useEffect and renders the resulting SVG. Deps added: mermaid@^11.14.0 and next-themes@^0.4.6 (transitively present via fumadocs-ui but needs explicit declaration to be importable). Design choices: - Client-side render (not build-time). No Playwright / browser automation in CI. Mermaid bundle (~400 KB) is loaded only on pages that use the component, thanks to the dynamic import. - Theme flips automatically — useTheme() from next-themes re-invokes mermaid.initialize() with the correct theme on .dark toggle. - SSR safe: the component returns a "Rendering diagram…" placeholder on the server; the SVG appears after hydration. - securityLevel "strict" — diagrams render as static SVG with no inline script or event handlers. Usage in mdx (explicit import, same pattern as Cards/Callout): import { Mermaid } from "@/components/mermaid"; <Mermaid chart={` graph LR User --> Server `} /> Verified by a scratch /app/mermaid-test/ route that compiled to 4665 modules and returned HTTP 200 (cleanup done pre-commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(docs-site): adopt v2 editorial palette and typography Replace the Linear/Vercel-style cool-gray token override with a warm editorial palette (bg matches landing #f7f7f5, brand-color primary via Multica's existing --brand hue 255) and wire Source Serif 4 for heading typography. Italic is avoided sitewide — Chinese italic renders as a synthetic slant against upright-designed glyphs and reads as broken; emphasis is carried by serif/sans contrast, brand color, and weight. Sidebar adopts the product app's active-fill pattern (solid sidebar-accent background, no ::before mark). Code blocks drop the always-dark hero treatment and follow page theme so the reading column stays coherent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(docs-site): add editorial MDX components New components/editorial.tsx exposes Byline, NumberedCards/NumberedCard, and NumberedSteps/Step — the "wow moment" pieces from v2-editorial (ruled-divider bylines, No. 01 serif card numbering, large serif step counters). All escape prose via not-prose so they run their own type scale. DocsHero is rewritten as an editorial showpiece: title accepts ReactNode so callers can pass a brand-color em accent, eyebrow becomes a small uppercase sans label, lede uses serif at 20px. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(docs-site): rewrite welcome page as editorial showpiece Welcome page now opens with an editorial hero (eyebrow + serif h1 with brand-color em accent on "共处一方。" + serif lede), a ruled byline strip carrying the section / updated / read-time metadata, and then flows into prose. The three deployment paths switch from fumadocs's <Cards> to <NumberedCards> so each gets a No. 01/02/03 label, and the "next steps" list becomes a <NumberedSteps> block with large serif counters. These are the highest-impact visual moments on the page; the rest of the guide pages still get the global editorial chrome without needing per-page code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(docs-site): add bilingual flat content tree with i18n routing Restructures the docs site from nested topic folders (cli/, getting-started/, developers/, guides/) into a flat content tree, and adds Chinese alongside English. The old nested structure forced contributors to think about both the topic AND the user-journey grouping; the flat tree lets a single meta.json control reading order with separator labels, and lets the same slug serve both languages via the `foo.zh.mdx` parser convention. Routing - New `app/[lang]/` segment hosts layout, home, slug page, and not-found - Self-contained basePath-aware middleware (fumadocs's built-in middleware isn't basePath-aware, so its rewrite/redirect targets break under /docs) - `hideLocale: 'default-locale'` keeps English URLs prefix-less; Chinese lives under /docs/zh/ - Sitemap excluded from middleware matcher so crawlers don't get rewritten into a non-existent locale-prefixed sitemap route - Default-language redirect preserves search string (UTM safety) - Home page declares its own generateStaticParams (Next layout params don't cascade) so /docs/ and /docs/zh are SSG, not dynamic per request SEO - New app/sitemap.ts emits hreflang alternates for every page - absoluteDocsUrl normalizes the home `/` so canonical URLs don't carry a trailing slash that mismatches the page's own canonical link - apps/web/app/robots.ts now advertises the docs sitemap Search - CJK tokenizer registered for the zh locale (Orama's English regex strips Han characters; without this Chinese search either returns empty or throws) Chrome - Custom DocsSettings replaces fumadocs's default icon-only sidebar footer with two labelled buttons (language + theme), matching the editorial design language Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
71cc646951 |
fix(chat): prevent UI flicker when streaming response finalizes (#1583)
The live timeline was rendered in a separate <div> from the persisted messages list. When the streamed task finished and its ChatMessage landed, the live <div> unmounted and a new <MessageBubble> mounted — two different DOM elements showing the same content. useAutoScroll's ResizeObserver + MutationObserver fired on both the unmount and the mount, causing the visible jump-then-re-render. Merge the two paths: inject a synthetic assistant message with the pending task_id while streaming, and key every assistant bubble by task_id. When the real message arrives (same task_id), React preserves the DOM element across the invalidate → refetch window — no remount, no double scroll, no flicker. Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
bb767e0ea6 |
fix(chat): prevent chatbox jump when sending first message (#1582)
The ChatInput wrapper toggled between pb-8 (empty state) and pb-4 (has messages), causing a 16px vertical jump the moment hasMessages flipped. EmptyState already centers itself inside flex-1, so the extra padding wasn't needed — collapse to a single pb-4. Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
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> |
||
|
|
e0e91fc792 |
feat(daemon): harden agent mention-loop instructions (#1581)
* feat(daemon): harden agent mention-loop instructions Two agents that mention each other via `mention://agent/<id>` can fall into an infinite reply loop — each says "I'm done" in prose but keeps `@mentioning` the other, which re-enqueues their run. Adding hard caps on agent-to-agent turns conflicts with Multica's design principle of giving agents the same authorship freedom as humans, so this change hardens the instructions that the harness injects instead. - Replace the terse "mentions are actions" blurb with a full Mentions protocol: `side-effecting` warning, explicit "when NOT to mention" (replying to another agent, sign-offs, thanks) and "when a mention IS appropriate" (human escalation, first-time delegation, user asked). - Add a pre-workflow decision step for comment-triggered runs: decide whether a reply is warranted at all, decide whether to include any `@mention`, and clarify that the post-a-comment rule is mandatory *if* you reply — silence is a valid exit for agent-to-agent threads. - Thread the triggering comment's author kind + display name (`TriggerAuthorType` / `TriggerAuthorName`) from the claim endpoint through the daemon task type, per-turn prompt, and CLAUDE.md workflow. When the author is another agent, both surfaces now name that agent and warn against sign-off mentions. - Soften the old closing line that told agents to `always` use the mention format — the word generalized to member/agent mentions and encouraged the very behavior that causes loops. Refs GH#1576, MUL-1323. * fix(daemon): remove MUST-respond conflict and sanitize trigger author name Addresses two blocking points on PR #1581: 1. buildCommentPrompt told the agent "You MUST respond to THIS comment" and unconditionally appended the reply command — directly conflicting with the new agent-to-agent silence-as-valid-exit workflow. Models were likely to keep following the older must-reply rule and fall back into the loop this PR is trying to close. Rewrite the header as "Focus on THIS comment — do not confuse it with previous ones" (keeps the anti-stale-comment signal) and change BuildCommentReplyInstructions to open with "If you decide to reply, post it by running exactly this command" so the reply command is available but conditional across both prompt surfaces. 2. Raw agent/user display names were being embedded directly into the high-priority prompt and CLAUDE.md via TriggerAuthorName. Agent and member names are only validated as non-empty at write time, so a name containing newlines, backticks, or fake mention markup would turn the field into a cross-agent prompt-injection surface. Add execenv.SanitizePromptField — strip control runes, collapse whitespace, drop markdown structural characters (backtick, asterisk, brackets, pipe, angle brackets, hash, backslash), truncate to 64 runes — and apply it at both embed sites (per-turn prompt and CLAUDE.md). Defense-in-depth at the consumption layer so this works for already-stored names without a migration. Tests: TestSanitizePromptField covers the policy; TestBuildPromptSanitizesAgentName plants an attack payload in TriggerAuthorName and checks the rendered prompt does not leak the newline-anchored injection or the fake mention markup. TestBuildPromptCommentTriggered*{,ByMember} updated to lock in the conditional reply-command framing. * refactor(daemon): trim redundant CLAUDE.md preamble and drop name sanitizer Per PR #1581 feedback: 1. Remove the `if ctx.TriggerAuthorType == "agent"` preamble block in runtime_config.go. It duplicated what workflow steps 4 and 5 already say ("Decide whether a reply is warranted", "Never @mention the agent you are replying to as a thank-you or sign-off"), so the signal lands the same without the extra ~7 lines of CLAUDE.md. The per-turn prompt preamble in prompt.go stays — that surface has no numbered workflow below it and would otherwise lose the silence-as-exit signal. 2. Delete execenv.SanitizePromptField + its test. Workspace agents are created by trusted team members, so the cross-agent name-injection surface it defended isn't realistic in the current trust model. 3. Drop TriggerAuthorType/Name from execenv.TaskContextForEnv and stop populating them in daemon.go — they're no longer read by the execenv package. The same fields on daemon.Task stay because prompt.go still needs them to label the triggering author in the per-turn prompt. Tests simplified to match the leaner shape: CLAUDE.md regression guards now assert that the anti-loop phrases live in the numbered workflow, and the sanitizer-specific tests are removed. |
||
|
|
977b0c0558 |
feat(agents): show profile card on agent avatar hover (#1577)
* feat(agents): show profile card on agent avatar hover Hovering an agent avatar now opens a preview card with name, status, runtime mode + connectivity, model, skills, and owner. Wired through the shared ActorAvatar wrapper so every render site gets it; opt-out via disableHoverCard in pickers and the agent's own detail header where the card would be redundant or interfere with click selection. * fix(agents): keyboard-focusable hover card + opt out on settings avatar - Make the agent profile-card hover trigger focusable (tabIndex=0 with visible focus ring), so keyboard users can open the card. Drops cursor-default so the trigger inherits the parent control's cursor instead of fighting it. - Disable the hover card on the agent settings avatar — it's a click-to-upload target on the agent's own settings page, where the card would be redundant and the trigger conflicted with the upload affordance. * fix(agents): scope hover-card tab stop to standalone avatars only Detect a focusable ancestor (link/button/role=button/tabindex>=0) at mount and only flip the agent profile-card trigger to tabIndex=0 when none exists. Avatars rendered inside an existing focusable parent (issue list rows wrapped in AppLink, button-style cards, etc.) keep the trigger unfocusable so they don't add redundant nested tab stops or bloat keyboard navigation. Standalone avatars (e.g. comment author, issue detail meta) remain keyboard-accessible with a focus-visible ring. --------- Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
17136742b9 |
fix(runtimes): fix dark mode chart visibility and invalid CSS color syntax (#1573)
All chart components used `hsl(var(--chart-X))` but `--chart-X` holds a full oklch value, not bare HSL components — making the expression invalid CSS. Browsers silently fell back to black, so bars/areas/heatmap cells were invisible against the dark background. - Replace `hsl(var(--chart-X))` with `var(--color-chart-X)` across all runtime chart components and the landing feature section - Fix heatmap opacity using `color-mix(in oklch, ...)` instead of the invalid `hsl(var(--chart-3) / 0.3)` syntax; switch to foreground color so cells blend with the neutral theme in both light and dark mode - Raise dark-mode chart-2 through chart-5 lightness values so they contrast clearly against the dark background Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
13daede63e |
docs: remove Star History chart from README (#1574)
Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
6107211a6e |
docs(selfhost): correct WebSocket guidance for LAN access (#1567)
The previous note claimed the frontend's auto-derived WebSocket URL worked on LAN without extra configuration. It does not: Next.js `rewrites()` only proxy HTTP requests, so the `Upgrade` handshake required for WebSocket never reaches the Go backend, and real-time features (chat streaming, live issue updates, notifications) silently fail when accessing the app via a non-localhost host. Replace the incorrect sentence with a dedicated subsection that points users at the reverse-proxy recipe (already documented above, includes the correct /ws Upgrade headers) and, for setups without a proxy, documents the build-time NEXT_PUBLIC_WS_URL + selfhost.build.yml override path. Refs: GH #1522 |
||
|
|
044d1443b5 |
fix(issues): keep reply editor expand icon muted on focus (#1565)
The expand button relied on the parent row's inherited color, which flipped to text-foreground via group-focus-within while the editor was focused. The attach and submit buttons set text-muted-foreground on themselves and stayed muted regardless of focus, so expand was the only one changing color — inconsistent with the "default muted" convention the other icon-buttons in this editor follow. Give expand its own text-muted-foreground and drop the now-unused color classes from the button row container. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8f10741a4d |
feat(daemon/gc): tighten GC defaults + flex duration suffix (#1559)
* feat(daemon/gc): tighten GC defaults + flex duration suffix Driven by user feedback in #1539 (40 GB VPS filling within 24h of heavy AI-coding usage): the existing TTLs were sized for desktop/laptop deployments and are too lenient for small-disk, long-running daemons. - GCTTL: 5d → 24h. Done/canceled issues almost never need a multi-day grace period in AI-coding workflows. - GCOrphanTTL: 30d → 72h. Covers crash-leftover and pre-GC directories without a month-long wait. - Issue-deleted orphans (API returns 404) are now cleaned on the next GC cycle regardless of mtime. The issue row is gone; there is nothing left to protect. - parseFlexDuration: accept a `d` (day) suffix in addition to the stdlib time.ParseDuration syntax. MULTICA_GC_TTL=5d now works; previously only 120h was accepted. * fix(daemon/gc): address review — 404 safety + decimal/overflow in duration parser Two issues flagged in PR review: 1. 404-immediate-clean is unsafe. The /gc-check endpoint returns 404 for both "issue deleted" AND "daemon token has no access to the workspace" (anti-enumeration, see requireDaemonWorkspaceAccess). Clean-on-404 would let a scoped-down daemon token wipe taskDirs whose issues are still live. Restore the mtime gate against GCOrphanTTL. With the new 72h default we still shrink the original 30d window dramatically without the cross-workspace hazard. Lock the behavior in with a new test that asserts a recent 404 is skipped. 2. parseFlexDuration mishandled decimals and swallowed Atoi errors: "0.5d" → 7m12s (regex matched only the "5d"), "1.5d" → 1h7m12s, and 20+ digit day values Atoi-errored silently to 0. Match the full decimal number with `\d*\.\d+|\d+` and parse with ParseFloat so fractional days and oversized inputs both go through time.ParseDuration correctly — fractions as sub-hour durations, overflow as a returned error. |
||
|
|
cbe0cbef56 |
fix(daemon): retry local-skill reports on transient server errors (#1561)
Review follow-up on PR #1557: the server-side change started returning 500 when the store write failed, but the daemon's handleLocalSkillList / handleLocalSkillImport were discarding the ReportLocalSkill*Result error return. Net effect was a silent drop — the daemon moved on, the request stayed in "running" on the server, and the user saw the same "daemon did not respond within 30 seconds" timeout the store refactor was supposed to kill. Fix: route both report calls through reportLocalSkillResultWithRetry, which retries on 5xx + network errors with 0 / 0.5s / 2s / 4s backoff (total ~6.5s, well inside the 60s server-side running timeout), stops on 4xx (request expired / cross-workspace rejection — retry won't help), bails on context cancel, and logs Error on exhaustion so ops has a footprint to grep for. Tests (server/internal/daemon/local_skill_report_test.go, 6 new cases): - 500 twice then success -> 3 attempts, second retry lands - 404 -> exactly 1 attempt (permanent, no retry) - import 502 then success -> 2 attempts - All-500 -> burns through all backoff slots then gives up with ERROR log - Context cancel mid-backoff -> exactly 1 attempt, cancellation logged - Smoke: report paths hit /api/daemon/runtimes/<rt>/local-skills{,import}/<req>/result localSkillReportBackoffs is var-assignable so tests can swap in zero-delay schedules without paying real sleep latency. |
||
|
|
502add4bd1 |
fix(issues): restore compact single-line reply editor, keep expand overlap fix (#1562)
#1558 fixed the expand button covering trailing text, but also collapsed the reply editor's "empty = 1 line, has content = 2 lines" behavior by making the button row a permanent flex sibling below the editor. Restore the original absolute-positioned button row on both editors: - comment-input: back to `pb-8` container + `absolute bottom-1 right-1.5` buttons (pre-#1558 layout; never had the overlap bug). - reply-input: absolute buttons + `pb-7` gated on `!isEmpty || isExpanded`. Empty → single-line compact; any content → two-row layout with buttons below text (no overlap by construction). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5ef957ca1b |
fix(skills): resolve aliased skills.sh imports (#1432)
* fix(skills): resolve aliased skills.sh imports * fix(skills): harden alias fallback scan |
||
|
|
6d9ca9de93 |
fix(daemon): suppress agent terminal windows on Windows (#1474)
* fix(daemon): suppress agent terminal windows on Windows (#1471) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add hideAgentWindow to detectCLIVersion and avoid SysProcAttr overwrite - Add missing hideAgentWindow(cmd) call in detectCLIVersion (claude.go:554) so --version checks don't flash console windows on Windows. - Refactor hideAgentWindow to preserve existing SysProcAttr fields instead of overwriting the entire struct. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
e994d77982 |
feat(help): mark external links with arrow, move Feedback last (#1560)
Add an ArrowUpRight glyph next to Docs and Change log to signal they open externally, and reorder so Feedback (internal modal) sits at the bottom. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ad803b86ec |
fix(skills): shared-state runtime local-skill stores (MUL-1288) (#1557)
* fix(skills): shared-state runtime local-skill stores (MUL-1288)
Fixes the bug Bohan surfaced on MUL-1288: behind prod's multi-node API the
runtime-local-skill list/import flow would intermittently time out or 404.
Root cause: LocalSkillListStore and LocalSkillImportStore were per-process
sync.Mutex+map, so when the frontend POST, the daemon heartbeat and the
frontend GET landed on different API instances, each saw a different
pending set. Confirmed against production daemon logs — the failed
request_id never showed up in the daemon's "runtime local skills
requested" log, even though other requests around the same window worked.
Per Yushen's guidance (server must stay stateless; state lives in
storage), migrate both stores to Redis so every node agrees on the same
pending set.
What changed
- LocalSkillListStore / LocalSkillImportStore are now interfaces. Methods
take context.Context and return error.
- InMemoryLocalSkill{List,Import}Store — renamed from the existing types,
kept as the default for single-node dev and the in-process test suite.
- RedisLocalSkill{List,Import}Store — new. Keyed on
mul:local_skill:{list,import}:<id> (JSON record, TTL = retention), with
a per-runtime ZSET mul:local_skill:{list,import}:pending:<runtime_id>
(score = created_at UnixNano) providing cross-node ordering. PopPending
wins the claim via ZREM == 1, so concurrent pops from different nodes
never return the same request twice.
- NewRouter gets an optional *redis.Client; when non-nil it swaps in the
Redis-backed stores. main.go hoists the existing Redis client (already
used by the realtime relay) so both subsystems share one client.
- Handler fields flip to interface types; handler.New still constructs
in-memory stores by default.
- Daemon heartbeat's PopPending call sites thread r.Context() through so
Redis operations inherit request cancellation. Errors warn instead of
poisoning the heartbeat response.
Tests
- Existing in-memory tests updated for the new signatures (ctx + error).
- New runtime_local_skills_redis_store_test.go covers:
- Create/Get/Complete round trip preserves skills payload
- PopPending across two *store instances sharing one rdb (the exact
regression: node A creates, node B pops)
- N concurrent PopPending on one record => exactly one winner
- Pending-timeout threshold transitions the record and removes the zset
member so a later PopPending doesn't return a timed-out request
- Import store round-trips CreatorID (which is json:"-" on the public
struct — needs a Redis envelope so ReportLocalSkillImportResult can
still attribute the created Skill)
- Per-runtime isolation — a PopPending for runtime B does not disturb
A's pending zset
- Tests skip gracefully if REDIS_TEST_URL is unset; CI now spins up a
redis:7-alpine service and exports the URL so the suite actually runs
there.
Out of scope
PingStore / UpdateStore / ModelListStore have the same shape and the
same latent bug (they just fire rarely enough to have gone unnoticed).
Migrating them to Redis is a follow-up — MUL-1288 is specifically the
local-skills break Bohan is blocked on.
* fix(skills): atomic Redis claim + surface store write failures (PR #1557 review)
Two real gaps GPT-Boy flagged:
1. RedisLocalSkill{List,Import}Store.PopPending was doing ZREM then SET as
two separate round-trips. If the SET failed for any reason — transient
Redis error, context cancellation, pod getting SIGKILL'd mid-call — the
request was already gone from the pending zset but the stored record
still said "pending", and no subsequent PopPending would re-dispatch
it. Exactly the "request disappears" class of bug this PR is supposed
to kill.
Fix: push the claim into a Lua script so Redis runs ZREM + SET as one
atomic unit. If ZREM returns 0 (another node won the race), SET is
skipped and the caller retries.
2. ReportLocalSkill{List,Import}Result handlers were logging Complete/Fail
store failures at Warn and still returning 200 OK. That made the
daemon think the report landed when it hadn't, leaving the request
stuck in "running" until the server-side timeout and — worse for the
import flow — leaving the just-created Skill row orphaned in Postgres
so every retry collided with the unique-name constraint.
Fix: escalate to Error + return 500 so the daemon (and monitoring) can
see the write failed. For the import flow, Complete failure after the
Skill row is already committed also triggers a best-effort DeleteSkill
so a daemon retry lands on a clean slate instead of hitting
"a skill with this name already exists" forever.
Tests
- New TestRedisLocalSkillListStore_PopPendingAtomicClaim asserts the
happy-path invariant: after one PopPending the record is "running"
AND a second PopPending returns nothing. Deliberately does NOT poke
Redis internals directly so the test survives any future key-layout
refactor.
- Existing cross-instance / concurrent / timeout / per-runtime tests
continue to pass against the Lua-based claim path (verified locally
against a scratch redis-server; 8/8 Redis tests green).
|
||
|
|
b51d1c4dc3 |
fix(cli): make browser-login work from a machine that isn't the server (#1556)
* fix(cli): make browser-login work from a machine that isn't the server The #923 callback host fix only worked when the CLI and the self-hosted server ran on the same box. In a cross-machine setup — `multica login` from a laptop against a self-hosted server on a NAS — the flow silently wedged on two issues: 1. The callback host was derived from `--app-url`, so the `cli_callback` URL pointed at the server's IP and the browser could never reach the CLI's local listener on the laptop. The OAuth token never came back and subsequent `/api/workspaces` calls 401'd on stale state. 2. `net.Listen("tcp", ...)` on macOS can produce an IPv6-only socket. Browsers and `curl` resolve `localhost`/`127.0.0.1` to IPv4 first and get "connection refused" even when the URL is otherwise correct. Changes: - Derive the callback host from the CLI's own outbound interface by dialing the server (UDP, no packets sent — just asks the kernel which source IP it would use). Falls back to loopback for public app URLs and to the app IP for offline detection. - Add `--callback-host` flag on `login` and `setup self-host` so reverse-proxy / FQDN users can override auto-detection — this is the follow-up @hassaanz asked for on #923. - Pin the callback listener to `tcp4` so macOS never lands on an IPv6-only socket. - `multica setup self-host`: when the user explicitly passes a remote `--server-url` but omits `--app-url`, infer app URL from the server host and warn instead of silently defaulting to `localhost:3000`. Unit tests cover the binding-decision matrix (public, localhost, same- machine LAN, cross-machine LAN, outbound-detect failure, flag override) and the new setup helpers. Reported by @RafeRoberts in #1494 with very clear repro details. * fix(cli): prompt for app_url instead of guessing on remote server_url Per GPT-Boy's review on MUL-1260: deriving app_url as http://<server-host>:3000 breaks for the common api.example.com + app.example.com split and for https-fronted deploys — the setup flow would still open a broken login URL, just slightly later. Replace the guess with an interactive prompt. If the user hits enter (or stdin is unavailable), fail loudly with a clear usage hint instead of proceeding with bad data. |
||
|
|
efc08a1e37 |
fix(issues): stop expand button from covering text in comment/reply editors (MUL-1297) (#1558)
The comment and reply editors positioned their three trailing buttons (expand, attach, submit) with `absolute` and relied on `pr-14` / `pb-8` magic numbers to reserve space. The reserved 56px is smaller than the actual 80px button row, so the leftmost button (expand) visibly overlaps the trailing characters of a long line of text. Restructure the button row as a normal flex sibling below the editor. Text can no longer flow under the buttons, and the layout no longer needs the `pr-14` hack, `pb-8` padding, or the ResizeObserver that toggled `pb-7` when content overflowed. Also align the expand button in comment-input with the reply-input version (`h-6 w-6` + `h-3.5 w-3.5` icon) so the two entry points match. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6fd1255873 |
feat(runtimes): remove Test Connection / runtime ping feature (#1554)
* feat(runtimes): remove Test Connection / runtime ping feature The Test Connection action invoked a real single-turn agent run to verify runtime connectivity. In practice it was expensive (reuses none of the normal task exec env, so it also gave misleading results) and low value — daemon heartbeat + Online status already covers the "is the runtime alive" question. Dropping the whole end-to-end probe path: - deletes server handler and in-memory PingStore - drops pending_ping from the heartbeat response and daemon poll loop - removes daemon.handlePing, PendingPing, ReportPingResult - removes the CLI `multica runtime ping` command - removes the PingSection UI block and RuntimePing types / api methods * docs: fix runtime CLI subcommand list in product-overview |
||
|
|
6c72c71e3e |
feat(analytics): add onboarding_runtime_detected event on desktop Step 3 (#1553)
Answers "did the user have an AI CLI installed locally when they hit Step 3" — currently unanswerable from the existing funnel because the bundled daemon fails to register at all when zero CLIs are on PATH, so `runtime_registered` is silent on that cohort. Splits the 40% of `completion_path=runtime_skipped` into "had CLIs, skipped anyway" vs "no CLIs available, had no choice" — the two cases need opposite product fixes. Fires once per Step 3 mount in `step-runtime-connect.tsx` (desktop only), when the scanning phase resolves — either immediately on first runtime registration or after the 5 s empty timeout. Reports `runtime_count`, `online_count`, sorted `providers`, convenience booleans (`has_claude` / `has_codex` / `has_cursor`), and `detect_ms`. Also writes `has_any_cli` + `detected_cli_count` via `$set` as cohort signals. Not emitted from the web Step 3 (`step-platform-fork.tsx`) — web users don't run the bundled daemon, so their runtime list can reflect daemons on other machines and would corrupt the "CLI installed locally" signal. Refs MUL-1250. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
83a3683d07 |
feat(landing): add sticky date navigation to changelog page (#1552)
* feat(landing): add sticky date navigation to changelog page
Adds a right-side "On this page" nav that lists every release date and
scroll-spies the active entry as the user reads through the changelog.
Dates are formatted per locale (e.g. "April 22" / "4月22日").
* feat(landing): move changelog date nav to left as timeline sidebar
Moves the date navigation from the right to the left and restyles it
as a grouped timeline:
- Releases are grouped under a month-year header ("April 2026").
- A vertical rail connects a dot per release; the active dot is filled
with a soft halo ring, the row text goes full-opacity + semibold.
- Clicking a date smooth-scrolls to the release and pins the hash; a
short nav lock suppresses scroll-spy flicker while the page animates.
- Sidebar is sticky up to viewport height, scrollable when there are
many releases; on <lg the sidebar collapses and content falls back
to the existing centered layout.
- Entry headers now render the full localized date for clarity.
Label changed from "On this page" / "本页目录" to "All releases" /
"历史版本" to match the new nav-style role.
* fix(landing): align changelog nav day/version columns
Reserve a fixed-width right-aligned slot for the day number so
single-digit days (e.g. "1", "9") don't shift the version column.
---------
Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
|
||
|
|
fae3afee79 |
fix(agents): drop auto-loading Local Runtime Skills section from Skills tab (#1551)
* fix(agents): drop auto-loading Local Runtime Skills section from Skills tab Every visit to an agent's Skills tab fired POST /api/runtimes/<id>/local-skills + a polling GET, which: - Created noise on every tab open (the section was rarely the user's reason for entering the tab — they came in for workspace skills). - Currently 404s under the dev backend's multi-replica deploy because the runtime-local-skills request store is in-process; the polling GET frequently lands on a different replica than the POST. The protocol fix is tracked separately; this PR just stops the unsolicited polling. Removes the entire `Local Runtime Skills` inline section, the `runtimeLocalSkillsOptions` query, and the per-skill Import dialog mount on this tab. Users who want to import a local skill go through the Skills page's `+ Add Skill` → `From Runtime` tab — the same flow that handles all other skill creation, only triggered explicitly. Top blue callout stays — still accurate: local runtime skills are auto-available to the agent, importing creates an editable workspace copy. * test(agents): replace stale Local Runtime Skills assertion with negative case The previous test required the inline section + auto-loading runtime local skills query, both removed in this PR. Replace it with a regression test that asserts the section is gone, the per-row import button is gone, and the top informational callout still renders so we know the tab body actually mounted. Drops the now-unused @multica/core/runtimes mock; if a future change re-introduces that import, the missing mock would surface immediately. |
||
|
|
91424752ac |
feat(realtime): phase 0 — extract Broadcaster interface + add metrics (MUL-1138) (#1429)
* feat(realtime): phase 0 — extract Broadcaster interface + add metrics Phase 0 of the WebSocket horizontal-scaling plan tracked in MUL-1138. This change is intentionally behavior-preserving: it sets up the seams needed for later phases (subscribe/unsubscribe protocol, scope-level fanout, Redis Streams relay) without altering any wire protocol or producer call sites. What changed - New realtime.Broadcaster interface covering the three fanout methods producers already use on *Hub (BroadcastToWorkspace, SendToUser, Broadcast). *Hub continues to satisfy it; a future Redis-backed implementation can be dropped in without touching listeners. - registerListeners now depends on realtime.Broadcaster instead of *realtime.Hub, isolating the bus → realtime fanout layer behind an interface. - New realtime.Metrics singleton with atomic counters: connects, disconnects, active connections, slow-client evictions, total messages sent/dropped, and per-event-type send counters. Wired into Hub register/unregister/broadcast paths and into every listener. - New GET /health/realtime endpoint returning a JSON snapshot of the metrics so we can observe baseline fanout pressure before phase 1. Why phase 0 first GPT-Boy's only-Redis plan and CC-Girl's review both call out the same prerequisite: get a Broadcaster seam and visibility in place before introducing scope-level subscriptions or a Redis relay. Doing this as a standalone step keeps each later PR focused and trivially revertable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(realtime): only-Redis fanout — scopes, subscribe protocol, Redis Streams relay (MUL-1138) Implements the final-version plan agreed in MUL-1138 on top of phase 0: * Hub: 4 scope types (workspace/user/task/chat), per-client subscription set, subscribe/unsubscribe WS frames, ScopeAuthorizer hook for task/chat scope auth, first/last-subscriber callbacks for the relay, workspace+user auto-subscribe on connect. * RedisRelay: Broadcaster impl that XADDs every event into ws:scope:{type}:{id}:stream and XREADGROUPs only the scopes for which this node has live subscribers. Per-node consumer group, heartbeat, stale-consumer sweeper, MAXLEN cap, lag/disconnect metrics. * Listeners: route task:* events to ScopeTask, chat:* events to ScopeChat; workspace remains the default for everything else. * events.Event: optional TaskID / ChatSessionID hints so the listener layer can pick the right scope without re-parsing payloads. * Handler: publishTask / publishChat helpers; chat + task message publishers updated to use them. * main.go: when REDIS_URL is set, wrap the hub with NewRedisRelay and pass the relay (instead of the hub) to registerListeners. A db-backed ScopeAuthorizer enforces that task/chat subscribes belong to the caller's workspace. * Metrics: per-scope subscribe/deny counters, redis connect state, node id, lag/dropped counters surfaced via /health/realtime. Behavior in single-node mode (REDIS_URL unset) is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(realtime): address PR #1429 review must-fix items (MUL-1138) - listeners: keep task/chat events on workspace fanout until the WS client supports scope-subscribe + reconnect-replay. Routing them through BroadcastToScope today (without any client subscriber) would silently drop every chat / task message and break the live timeline, chat unread badges, and pending-task UI. The server-side scope infra (Hub subscribe/unsubscribe, ScopeAuthorizer, Redis Streams relay) stays in place so flipping the switch in the client follow-up PR is a one-line change. - scope_authorizer: ScopeChat now enforces CreatorID == userID, mirroring the HTTP layer (handler/chat.go: GetChatSession / SendChatMessage / MarkChatSessionRead). Without this, any workspace member who learned a session_id could subscribe to chat:message / chat:done / chat:session_read for a peer's private chat. The same creator-only check is applied to ScopeTask when the task is a chat task (task.ChatSessionID set). Issue tasks remain workspace-scoped. - Refactor scope authorizer to depend on a narrow scopeAuthQuerier interface so its decisions can be unit-tested without a live DB. - Add tests: * listeners_scope_test.go pins the workspace-fanout fallback for task:message / task:progress / chat:message / chat:done / chat:session_read. * scope_authorizer_test.go covers chat creator-only access, chat-task creator-only access, and issue-task workspace-only access (creator allowed, peer denied, cross-workspace denied, missing session denied, empty userID denied). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: CC-Girl <cc-girl@multica.ai> |
||
|
|
d97aec83d7 |
fix: pass model to Hermes ACP and add hermes to InjectRuntimeConfig (#1203)
* fix: pass model to Hermes ACP session/new and add hermes to InjectRuntimeConfig - hermes.go: include opts.Model in session/new params so Hermes uses the configured model instead of its default (fixes local LLM failures) - runtime_config.go: add "hermes" to the AGENTS.md provider list so Hermes receives the Multica runtime instructions and skill discovery Fixes: https://github.com/multica-ai/multica/issues/1195 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(hermes): drop false native-skill claim and add regression tests The previous change added 'hermes' to the 'skills discovered automatically' branch of buildMetaSkillContent, but resolveSkillsDir has no Hermes case so skills still land in the .agent_context/skills/ fallback. AGENTS.md ended up claiming native discovery while the files were somewhere else, which would mislead Hermes (and future debuggers). - Move 'hermes' to the fallback branch alongside 'gemini' so AGENTS.md points Hermes at .agent_context/skills/ — matching where writeContextFiles actually writes them. - Extract buildHermesSessionParams so the session/new payload is unit-testable. - Add regression tests covering: * buildHermesSessionParams includes/omits 'model' correctly * InjectRuntimeConfig('hermes') writes AGENTS.md with the fallback hint * writeContextFiles('hermes') writes skills to .agent_context/skills/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: CC-Girl <cc-girl@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> |
||
|
|
d6e7824ff1 |
feat(feedback): in-app feedback flow + Help launcher (#1546)
* feat(feedback): add in-app feedback flow and Help launcher Replaces the duplicated bottom-sidebar user popover and "What's new" links with a single Help menu (Docs / Feedback / Change log) pinned to the sidebar footer. Feedback opens a rich-text modal that POSTs to a new /api/feedback endpoint; submissions land in a dedicated feedback table with per-user hourly rate limiting (10/hr) to deter spam without adding middleware infrastructure. User identity (avatar + name + email) moves into the workspace dropdown header so the sidebar is no longer visually redundant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(feedback): harden submit path and cap request body - Read editor markdown via ref at submit time instead of debounced state, so ⌘+Enter immediately after typing doesn't drop the last keystrokes. - Block submission while images are still uploading; toast prompts the user to wait instead of silently sending markdown with blob: URLs that get stripped. - Cap /api/feedback request body at 64 KiB via MaxBytesReader so an authenticated client can't bloat the metadata JSONB column with an oversized url field. - Add Go handler tests covering happy path, empty-message rejection, and the hourly rate limit boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(analytics): instrument feedback funnel Adds two events pairing frontend intent with backend conversion so we can compute a completion rate for the in-app Feedback modal: - `feedback_opened` (frontend) — fires once on FeedbackModal mount. Source is currently always "help_menu" but the type is a union so future entry points have to extend it explicitly. Workspace id is attached when present. - `feedback_submitted` (backend) — fires from CreateFeedback after the DB insert succeeds and the hourly rate-limit check has passed. Message content itself is never sent to PostHog; the event carries a coarse length bucket (0-100 / 100-500 / 500-2000 / 2000+), an image-presence flag, and the client platform / version pulled from X-Client-* headers via middleware.ClientMetadataFromContext. Affects no existing funnel; seeds a new Feedback funnel for product triage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f2ba087f74 |
fix(editor): preserve nested ordered lists through readonly render (#1512)
Default @tiptap/markdown serializer emitted nested list items with 2-space indent, but CommonMark (remark-gfm) requires ≥3 spaces under a `1.` marker — so ReadonlyContent (autopilot detail / issue / comment) flattened nested ordered lists, with third-level items glued onto their parent line. Configure Markdown extension with indentation.size = 3. Closes #1510 |
||
|
|
059356cce7 |
docs(claude-md): trim implementation archaeology, keep rules (#1540)
CLAUDE.md is loaded into context every conversation; verbose race-condition post-mortems and code-organization rationales rot fast and crowd out the actionable rules they were meant to support. Strip the archaeology, keep the load-bearing constraints. - Workspace identity singleton + destructive ops (~22 -> 11 lines): keep the "must call setCurrentWorkspace(null, null) when leaving context" rule and the 4-step destructive order; drop the three-way race autopsy (already documented inline in workspace-tab.tsx where it belongs). - Drag region (~27 -> 3 lines): keep "every full-window desktop view must mount <DragStrip /> as first flex child"; drop hit-testing rationale, canonical-file inventory, and useImmersiveMode escape-hatch trivia. - UX vs platform chrome (~3 -> 0 lines): delete entirely. The rule duplicates "Cross-Platform Development Rules" above; the rest is purely why-we-organized-it-this-way narrative. Common Zustand footguns kept as-is - both items are real rules (stable selector references, hooks accepting wsId as parameter), not archaeology. Net: -36 lines, no rule lost. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7375bda9b5 |
fix(landing): scope landing route to always-light palette (MUL-1277) (#1537)
* fix(landing): scope landing route to always-light palette The landing page sections use hardcoded light colors (bg-white / #0a0d12), but shared components rendered inside — notably CloudWaitlistExpand on /download — use semantic tokens that flip to dark values under next-themes' `.dark` class, producing a mismatched dark card on an otherwise light page when the user's OS is in dark mode. Add a `.landing-light` class on the landing layout wrapper that re-declares all color tokens to their light values for the subtree, so nested token-driven components stay in lockstep with the hardcoded palette. * test(agent): serialize fake-executable writes to avoid ETXTBSY on CI TestKimiBackendInvokesACPSubcommand (and its Kimi/Codex siblings) write a shell script to a per-test TempDir and then fork/exec it. With t.Parallel() enabled across the package, a concurrent goroutine's fork can inherit the still-open write fd to another test's new executable; Linux then rejects the subsequent exec with ETXTBSY (seen as fork/exec /tmp/.../kimi: text file busy on GitHub Actions). Introduce writeTestExecutable, which holds syscall.ForkLock.RLock across OpenFile→Write→Close. Fork (which takes ForkLock.Lock) cannot run while we hold RLock, so no sibling fork inherits our write fd. Ran the three callers with -count=10 under -p=1 and the full package with no failures. |
||
|
|
9dcc082920 |
docs(handler): note that GetConfig is public-only and what may be returned (#1538)
Adds a doc comment on GetConfig spelling out that the endpoint is mounted on the unauthenticated route group (so the login page can fetch GoogleClientID / AllowSignup before the user is signed in) and that only instance-level public fields may be added. Prevents accidentally returning user- or tenant-scoped data from this handler in the future. |
||
|
|
98edc6b9ff | fix(auth): make /api/config publicly accessible (#1530) | ||
|
|
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> |
||
|
|
2cced51d64 |
docs(changelog): publish v0.2.14 + v0.2.15 release notes (#1517)
* docs(changelog): publish v0.2.14 + v0.2.15 release notes Summarises the 25 commits shipped today across both releases for the public changelog page, in English and Chinese. * docs(changelog): merge v0.2.14+v0.2.15 into one entry, trim, reclassify Gemini as fix Per review: today's two releases read better as one set of notes; tightened bullets; moved the Gemini 3 runtime-list update from Features to Fixes. * docs(changelog): drop last 3 features from v0.2.15 entry per review |
||
|
|
6717db1fad |
feat(agents): surface task source on AgentTaskResponse + use it in Tasks tab (#1455)
Follow-up to #1453. That PR fixed the Tasks tab crash by filtering empty issue_id out of the detail lookup and rendering a neutral "Task without linked issue" label, but every issue-less task — chat-spawned or autopilot-spawned — looked the same. The server already stores the origin in `agent_task_queue.chat_session_id` / `autopilot_run_id`; only the HTTP serializer was dropping them. Server: - `taskToResponse` now populates `ChatSessionID` and the new `AutopilotRunID` on `AgentTaskResponse`. Backward compatible: both omit when UUID is invalid, and existing clients ignore unknown fields. Types: - `AgentTask` (TS) gains `chat_session_id?` + `autopilot_run_id?` and a comment clarifying when `issue_id` is empty. Tasks tab: - Row label for issue-less tasks is picked from the populated source field: "Chat session" for chat tasks, "Autopilot run" for autopilot tasks, "Task without linked issue" as the neutral fallback. Rows stay inert (no anchor) in all three cases; existing issue-linked path is unchanged. Tests: - Two new regression tests assert the chat and autopilot labels render correctly and neither row becomes an anchor. Existing neutral-label test stays as the "neither source populated" case. |
||
|
|
2a248b8548 |
fix(openclaw): raise agent discovery timeout to 30s (#1495)
'discoverOpenclawAgents' runs several 'openclaw' subprocesses under one context; 5s was too short on cold starts or under load, causing empty listings in the model picker. Increase the per-discovery cap to 30s. |
||
|
|
f84d216794 |
fix(views): restore issue-mention class on <a> for mention card (#1516)
PR #1502's IssueChip extraction moved the `issue-mention` class from the outer <a> into IssueChip's inner <span>, breaking three consumers that select on `<a>.issue-mention` directly: - `.rich-text-editor a.issue-mention` underline-exemption in content-editor.css (stopped matching -> mentions in editor gained a spurious underline). - `link-hover-card.tsx` classList check that suppresses the URL preview on issue mentions (stopped matching -> hover card wrongly pops up over mention chips). - Tailwind Typography prose (`prose a { text-decoration: underline }`) covers a separate path — markdown bubbles in chat. prose's specificity (0,1,1) beats `.no-underline` (0,1,0), so `not-prose` is the right escape hatch on the AppLink. Put `issue-mention` back on the <a> in both wrappers (IssueMentionCard and the editor's MentionView), and add `not-prose` only to the markdown wrapper. IssueChip's BASE_CLASS keeps `issue-mention` too (inert on the span; removing it is a separate scope that needs a full consumer audit). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
101da19b02 |
feat(download): fall back to previous release within 1h freshness window (#1514)
New /download visitors were seeing grayed-out macOS buttons in the 20-ish minutes after a tag push because CI only builds Linux/Windows — Mac is still packaged manually and uploads tens of minutes later. Swap the `/releases/latest` fetch for `/releases?per_page=2` and, when the latest release is under an hour old, render the previous (fully-populated) release instead. After the freshness window, page auto-switches to latest. Frontend-only change — GitHub "latest" marker, electron-updater, and homebrew paths are untouched. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>v0.2.15 |
||
|
|
dc8096fb6e |
fix(agent): expose Gemini 3 + CLI aliases in Gemini runtime model list (#1508)
Gemini CLI has no `models list` subcommand, so Multica can't do real dynamic discovery. Instead, swap the static catalog from fixed version names (2.0/2.5 only) to the CLI's own aliases (`auto`, `pro`, `flash`, `flash-lite`, `auto-gemini-2.5`) plus explicit pins for Gemini 3 preview and 2.5 variants. Aliases are resolved inside the Gemini CLI per user entitlement + quota, so new model releases light up without a Multica redeploy. Default is `auto`, matching Google's recommended selection. Fixes multica-ai/multica#1503. |
||
|
|
2dae42f58a | Tighten Vercel ignore rules (#1513) | ||
|
|
f6dd47c944 |
fix(chat): disable focus button on pages without an anchor (#1509)
The focus toggle was only disabled when focusMode was already ON *and* the current page had no anchor. Off-state on the same page stayed clickable — clicking turned it on, and the button instantly greyed out, making the missing fourth state visible. Decouple "clickable" from focusMode: the button is disabled whenever the current page has no anchor, regardless of the persisted on/off preference. Both the chip render (context-anchor.tsx:173) and send path (chat-window.tsx:176) already guard on candidate presence, so leaving focusMode=true on an unanchorable page has no side effects — the preference is preserved for the next anchorable page. Tooltip now reads "Nothing to share with Multica on this page" whenever the button is disabled, regardless of focusMode. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f98a67dd90 |
ci(release): build docker images natively per arch and merge manifests (#1507)
Multi-arch images were built on a single amd64 runner with QEMU emulating arm64. The Next.js build (Dockerfile.web) under emulation took 30+ minutes per release and was the long pole of the workflow. Split each image build across two native runners (amd64 on ubuntu-latest, arm64 on ubuntu-24.04-arm), push by digest, then merge into a manifest list with docker buildx imagetools. QEMU is no longer needed. Backend and web each become a (matrix build + merge) pair, replacing the previous single docker-images job. Per-platform GHA cache scopes avoid cross-arch cache eviction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
90ccd97469 |
fix: add .vercelignore for Vercel web deploys (#1505)
* fix: ignore non-web files in vercel deploy * fix: keep docs app in vercel uploads --------- Co-authored-by: Eve <eve@multica.ai> |
||
|
|
180a534511 |
chore(docs): remove shipped plan and proposal docs (#1504)
All 15 files deleted here correspond to already-merged PRs — their execution is done, their rationale lives in commit messages and PR descriptions, and nothing in the tree references them (grep across *.ts / *.tsx / *.go / *.mjs / *.json returns zero hits). Removed: docs/download-redesign.md → PR #1500 docs/download-positioning.md → PR #1500 docs/onboarding-redesign-proposal.md → PR #1411 docs/workspace-url-refactor-proposal.md → PR #1131 / #1138 docs/plans/2026-04-07-tanstack-query-migration.md docs/plans/2026-04-08-board-dnd-rewrite.md docs/plans/2026-04-08-drag-upload-enhancement.md docs/plans/2026-04-08-image-view-enhancement.md docs/plans/2026-04-08-monorepo-extraction.md docs/plans/2026-04-09-desktop-app.md docs/plans/2026-04-09-monorepo-extraction.md docs/plans/2026-04-09-upload-attachment-fixes.md docs/plans/2026-04-15-workspace-slug-url-refactor.md docs/plans/2026-04-16-remove-onboarding-and-fix-daemon-bootstrap.md docs/plans/2026-04-16-unify-workspace-identity-resolver.md Empty `docs/plans/` directory goes with them (git drops empty dirs automatically). Active, non-plan docs stay: analytics.md, design.md, product-overview.md, codex-sandbox-troubleshooting.md. Any future plan can live on a feature branch under `.claude/plans/` (harness-scoped, not committed) or as a PR description. No need to land them in-tree. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2d0916ee38 |
feat(chat): focus mode — share current page as context (#1502)
* refactor(views): extract IssueChip shared primitive from mention card IssueMention (in editor NodeView) and IssueMentionCard shared 95% of their markup — StatusIcon + identifier + title inside a bordered chip. They drifted into two parallel implementations so changes had to be made in two places. Extract the presentational chip into IssueChip. The navigable variants (IssueMentionCard, the editor NodeView) become thin shells that layer routing + cmd/shift behaviour onto the shared chip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(chat): add focus mode to share current page as context Adds a Focus button next to the chat submit. When on, the chat auto-attaches whatever the user is viewing (issue, project, or inbox-selected issue) as a context prefix on outgoing messages, so the agent knows what "this" refers to without the user pasting ids. The attached object is derived from the route + react-query cache on every render — no separate copy in state. Only the boolean focusMode is persisted (global to the user, not per-workspace), matching the "my preference" mental model. The button has three visual states driven by two dimensions (focusMode + whether the current route resolves to an anchorable object): - off: ghost + muted, click turns on - on + anchor: secondary (bright), click turns off - on + none: disabled (nothing to attach here) The derived anchor renders above the input as a chip — IssueChip for issues, a new ProjectChip for projects — wrapped in AppLink so the visual target matches the clickable target (mirrors IssueMentionCard's hover + navigation). Prefix format reuses the editor's mention markdown: Context: [MUL-1](mention://issue/<uuid>) — "Fix login bug" Context: Project "Authentication" so the agent sees an identical token whether the user @-mentioned inline or focus-mode attached. Backend is untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>v0.2.14 |
||
|
|
5335edd50d |
feat(web): /download page + desktop promotion across landing, login, onboarding (#1500)
* docs(download): add redesign plan and copy positioning source of truth Captures motivation (Desktop is Multica's native form; CLI is a distinct scenario for servers/remote boxes, not a Desktop fallback), four-step execution plan, and every touchpoint's current-vs-new copy in EN + ZH. Subsequent UI steps read strings from the positioning doc instead of inventing them inline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): /download page with OS auto-detection New landing-group route that serves as the single canonical download destination. Auto-detects OS + arch via navigator.userAgentData (Chromium) with UA-string fallback, then surfaces the matching Desktop installer as the primary CTA. All platforms stay visible below, plus a CLI section (positioned for servers / remote boxes / headless setups, not as a lightweight Desktop) and a Cloud waitlist. Version + asset URLs come from api.github.com/repos/.../releases/latest with Vercel ISR (revalidate=300) so every release automatically propagates — no manual redeploy. Optional GITHUB_TOKEN env var lifts the 60/hr unauthenticated rate limit for local dev. Failure degrades cleanly to "Version unavailable" + a link to GitHub releases. Also points landing hero + footer Download links at /download (previously pointed at the GitHub releases page directly), and re-exports CloudWaitlistExpand from @multica/views/onboarding so the new Cloud section can reuse the existing form. Intel Mac has no binary today (electron-builder targets mac arm64 only); the page is honest about it and routes Intel users to CLI. i18n copy sourced verbatim from docs/download-positioning.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): rewrite Step 3 fork + web Welcome Desktop CTA Welcome screen now self-segments: on web (runtimeInstructions present), the primary CTA is "Download Desktop" with a benefit-led subtitle ("Desktop bundles the runtime — nothing to install. Continue on web to connect your own CLI.") that lets developers with their own CLI recognize their path while guiding everyone else toward the desktop app. Desktop branch drops the "3 minutes" estimate in favor of the aha promise. Download button is a real <a href> link so middle-click / copy-link / screen readers all behave correctly. Step 3 fork drops the stale isMac gate — Windows / Linux binaries now ship, the macOS-only muted card was a lie. The single Desktop card now routes to /download (not GitHub releases directly) so users land on the auto-detect page. CLI card is reframed around its real scenario (servers, remote dev boxes, headless) rather than posing as a lightweight Desktop, and the CLI dialog's stall tier redirects users to Desktop instead of Cloud waitlist when the daemon never registers — Desktop is the genuine retreat. cli-install-instructions gets a one-liner acknowledging the CLI's server use case, mirroring the card copy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web,auth): desktop promotion on login + solid landing hero download LoginPage accepts a new `extra?: ReactNode` slot rendered below the Google button. The web shell injects a hardcoded-EN "Prefer the desktop app? Download →" nudge there — catching users at their lowest-investment moment, before they've typed an email. Desktop's login wrapper omits the slot (a download prompt inside the app would be absurd), so only the web surface renders it. Copy is English-only for now because the /login route sits outside the landing group's LocaleProvider. Lifting locale detection into the root layout would force every page dynamic and kill the Router Cache — a trade-off not worth two strings. The `auth.login.extra*` i18n keys added during Step 2 are removed for the same reason: they're dead code without a LocaleProvider wrapping login. Landing hero "Download Desktop" upgrades from ghost to solid and swaps its handwritten monitor SVG for lucide-react's Download icon. Both hero CTAs are now solid-weighted — the icon + distinct label differentiates them. href already points to /download from the Step 2 landing nav pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web/download): anchor dark LandingHeader with relative wrapper LandingHeader's dark variant uses `absolute top-0 inset-x-0`, which only reads correctly when wrapped by a positioned ancestor — see multica-landing.tsx:14 for the canonical pattern. Without the wrapper the header escaped to the initial containing block and appeared fixed as users scrolled the page. Also drops the <main> element around the body sections for consistency with the rest of the landing group (neither multica-landing nor about-page-client wraps in <main>). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(landing/hero): keep Download Desktop as ghost to preserve CTA hierarchy Upgrading to solid alongside the existing "Start free trial" CTA killed the primary / secondary distinction — both buttons were white on dark, competing for attention. Revert to ghost so the conversion CTA (trial) stays the visual primary. The lucide Download icon swap stays (cleaner than the handwritten monitor SVG). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(onboarding): update platform-fork assertions for /download route The Desktop card in Step 3 now opens the new /download page instead of GitHub releases, and the post-click feedback text changed to match ("Continuing on the download page…" in place of "Downloading Multica…"). Update the expectations and drop the isMac navigator stub that was only needed when the component had a macOS-only primary branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Merge origin/main into NevilleQingNY/download-redesign Main added onboarding funnel analytics (#1489) that captures `is_mac` as a dimension for each Step 3 path selection. This branch had removed the `isMac` state because the UI no longer branches on it (Windows / Linux desktop builds ship now). Git auto-merged the two diffs into a file that referenced a deleted variable. Reintroduce `isMac` as a lazy client-only computation scoped to analytics capture only — the UI stays platform-agnostic. Handlers fire client-side so SSR safety isn't needed; a plain const reads navigator on first render. typecheck passes across all 6 packages; all 166 views tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(analytics): instrument download funnel across 5 surfaces + /download Closes the gap left by PR #1489: onboarding analytics captured Step 3 path selection but missed the four surfaces that advertise the desktop app earlier in the funnel (landing hero, landing footer, login, Welcome), and the /download page itself had zero coverage — so we could see the last-mile path but not the top-of-funnel entry nor the page-to-installer conversion. Three new events, wired via `@multica/core/analytics`: 1. `download_intent_expressed` fires on any CTA pointing at /download. `source` splits the five surfaces cleanly; every authenticated emission also writes `platform_preference=desktop` on the person (same convention Step 3 already uses). 2. `download_page_viewed` fires once per /download mount after OS detect resolves. Carries `detected_os`, `detected_arch`, `detect_confident` (Chromium userAgentData vs UA fallback), and `version_available` so the Safari-on-Mac arm64-default cohort and GitHub-rate-limited degraded sessions are each isolable. Also $set_once's `first_detected_os/arch` on the person so every downstream event gains a platform dimension without re-emitting. 3. `download_initiated` fires on every installer click — Hero's primary CTA and each All Platforms matrix row. `primary_cta` splits hero-recommended from manual picks; `matched_detect` quantifies detect accuracy from the single event (no cross-join to download_page_viewed needed). Augments the existing `onboarding_runtime_path_selected` with a `source: "step3"` property — literal today, reserved for future surfaces reusing the same event name. `is_mac` kept for backward-compat with PR #1489's dashboards; the new events use `detected_os` + `detected_arch` instead. New `setPersonPropertiesOnce` wire helper in `packages/core/analytics/download.ts` for `$set_once` — mirrors the backend's `Event.SetOnce` semantics. docs/analytics.md update lands in the follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(analytics): document download_intent_expressed / page_viewed / initiated Adds the three new download-funnel events to the frontend-only section. Also notes the semantic shift on onboarding_runtime_path_selected: its `path: "download_desktop"` now signals Step 3 path choice, not actual download start — download_intent_expressed is the new canonical "user expressed intent to download desktop" signal across surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |