mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
v0.2.18
932 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
c7a2d53f76 |
docs(changelog): publish v0.2.17 release notes (#1700)
* docs(changelog): publish v0.2.17 release notes Covers commits between v0.2.16 (2026-04-24) and the v0.2.17 cut (2026-04-26): --custom-env flag for agents, agent CLI stderr tail in failure messages, configurable update download timeout, plus reliability fixes around daemon cancellation, server heartbeat, Codex execenv, Pi skills path, Windows console, CJK markdown URLs, attachment downloads and autopilot run-only context. Both en.ts and zh.ts updated. * docs(changelog): trim small/internal items from v0.2.17 entry Drops items that read as internal polish or were too narrow to belong in release notes: - Skills landing intro polish - Codex execenv plugin-cache cleanup - CLI exact-name/ShortID assignee resolution - Settings invite role label rendering - Skills SKILL.md fast-path - CJK markdown URL-boundary fix - Relative attachment download URLs Keeps the user-facing wins: --custom-env, stderr-tail in failure messages, configurable update timeout, cancelled-task classification, heartbeat probe/claim split, plus the higher-impact fixes. |
||
|
|
c7bac0aa6b |
docs(changelog): publish v0.2.16 release notes (#1695)
Covers everything between v0.2.15 (2026-04-22) and v0.2.16 (2026-04-24): Chat V2, issue right-click context menu, in-app feedback + Help launcher, Autopilot modal redesign, Skills page redesign, bilingual flat docs site rewrite, plus the supporting agent / runtime / chat / desktop fixes. Both en.ts and zh.ts updated. |
||
|
|
68a312c297 |
fix(runtimes): fix pi skills dir to: .pi/skills (#1632)
change .pi/agent/skills to .pi/skills Pi loads skills from: Global: ~/.pi/agent/skills/ ~/.agents/skills/ Project: .pi/skills/ .agents/skills/ - ref: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/skills.md#locations |
||
|
|
c7e725ef66 |
feat: surface docs from onboarding + landing, unify Autopilot naming (#1613)
* docs(autopilot): rename Routines → Autopilots to match product UI
Unify naming between docs and product. Sidebar label, URL route,
CLI command, and onboarding copy all call this feature "Autopilot";
the docs were the only surface that diverged. Aligning the docs to
the product (rather than the reverse) because the 830+ code-side
references would be a much larger rename to propagate.
- Rename routines.mdx / routines.zh.mdx → autopilots.mdx / autopilots.zh.mdx
- Update meta.json / meta.zh.json index entries (routines → autopilots)
- Drop the reconciliation note ("docs say Routines, CLI says autopilot")
that shipped in the original routines.mdx and the cli.mdx section header
- Update cross-references in cli, how-multica-works, tasks,
assigning-issues, chat, mentioning-agents, daemon-runtimes (EN + ZH)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): link to docs from key steps and starter tasks
Users who want to dig deeper now have a next hop from inside the flow
instead of having to dig through the help menu. Placed as secondary
links (muted, underline-offset-4) so they don't pull focus from the
primary CTA on each step.
Placement — one link per surface, placed in secondary regions:
- Welcome: "Learn how Multica works" below the subhead
- Questionnaire: "Learn how agents work" in the Why-we-ask aside
- Runtime aside (shared by desktop + web): "Learn about runtimes"
- Agent step: "Creating your first agent" in the About-agents aside
- StarterContentPrompt dialog: "Learn how Multica works"
Starter tasks (content/starter-content-templates.ts): added a single
"Learn about X" tail link per task, only on first occurrence of each
concept within a branch. 8 links on the agent-guided branch + 8 on
the self-serve branch + 1 on the welcome issue header (17 total).
URL scheme: absolute https://multica.ai/docs/{slug} throughout —
absolute so desktop (Electron) opens them in the system browser, and
the /en prefix is omitted because the docs middleware redirects it
away (English is the default, Chinese is /zh/).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(landing): add docs link to footer and how-it-works section
Docs were previously reachable only from the in-app help menu. Landing
now surfaces them in two places, both locale-aware (/docs for English,
/docs/zh for Chinese):
- Footer Resources group: Documentation link was pointing at the
GitHub repo; replaced with the real docs URL
- How-It-Works section CTA row: added "Read the docs" between the
primary CTA and the GitHub link, same ghost styling
Locale resolution: href is picked per-render based on the landing's
current locale (cookie-driven via useLocale). The docs app itself
does not auto-detect language, so we must pick the right path
explicitly when emitting the link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): clean up Autopilot rename leftovers and link formatting
- comments.mdx: "not routine updates" → "not day-to-day updates"
(adjectival holdover now that the feature is renamed Autopilot;
zeroes out remaining "routine" mentions in user-facing docs)
- starter-content-templates.ts: move the arrow inside the markdown
link — "[text →](url)" instead of "→ [text](url)" — so the arrow
is part of the clickable region. 17 occurrences.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(onboarding): drop docs link from welcome screen and starter-content dialog
"Learn how Multica works" was showing up too often in the first two
screens users see. Keep the link in the post-import welcome issue
header (where users actually have time to explore); remove it from
the two earlier surfaces where it competes with the primary CTA.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
9ed1fa95fc |
feat(server): add readiness health endpoints (#1605)
* feat(server): add readiness health endpoints * fix(server): cache readiness checks * fix(server): raise readiness cache ttl --------- Co-authored-by: Eve <eve@multica.ai> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
fbf41bde73 |
feat(selfhost): ship public GHCR deployment flow
Publish stable GHCR self-host images, switch self-host deploys to official image pulls with a source-build fallback, and move self-host signup / Google OAuth config onto runtime /api/config. |
||
|
|
747d9492cf |
feat(changelog): surface release notes from sidebar menu + update prompt (#1485)
Two entry points to multica.ai/changelog so users actually find out what shipped: - Sidebar user menu (both expanded popover + collapsed dropdown variants) gains a "What's new" item with a Sparkles icon, sitting above Log out. Plain `<a target="_blank">` works on both surfaces: web opens a new tab, desktop's main-process setWindowOpenHandler intercepts and routes through openExternalSafely. The shared view doesn't need to branch. - Desktop's UpdateNotification "ready to restart" card grows a secondary "See changes" button next to "Restart now", giving the user a reason to actually restart instead of dismissing. Mirrors Conductor's update prompt pattern. The "available" / "downloading" states stay action-only — the changelog isn't useful before the download finishes. No version-detection / unread-tracking yet. Web users still need to click into the menu to see the changelog; that's a follow-up if the team wants Linear-style "new" dot. |
||
|
|
3036c6418e |
fix(onboarding): pin sync, welcome layout, runtime bootstrap state (#1482)
Follow-ups on the onboarding flow shipped in #1411. Pin state synchronization: - ImportStarterContent now publishes pin:created after commit so the sidebar refreshes without a hard reload (previously the pins landed in the DB but no event was fired). - ReorderPins publishes pin:reordered, keeping order in sync across web + desktop sessions. - StarterContentPrompt.onImport invalidates queries locally, mirroring the useCreatePin / useDeletePin / useReorderPins onSettled pattern, so the originating session's refresh doesn't depend on the WS round-trip (WS is the signal for OTHER sessions). - ImportStarterContent rejects malformed workspace_id up front with 400 instead of falling through to a misleading 403. Welcome step layout: - Switch the two-column hero from CSS Grid to a flex row. Both columns share the container's full height via items-stretch + justify-center, so the bg-muted/40 backdrop fills edge-to-edge on tall viewports and left/right content stays vertically centred. Desktop runtime bootstrap state: - New DesktopRuntimesPage wrapper subscribes to window.daemonAPI and forwards a `bootstrapping` prop to RuntimeList. While the bundled daemon is booting, the empty state renders "Starting local runtime…" instead of the misleading "Run multica daemon start" hint. Web leaves the prop undefined — behaviour unchanged. Small polish: - CLI install dialog caps at 85vh with an internal scroll so the Connect button stays reachable when multiple runtimes are registered. - Drop the env-aware CLI setup command; onboarding always targets cloud, so `multica setup` is enough — no need to thread apiUrl / appUrl through the dialog. Developer tooling: - pnpm dev:desktop:staging — parallel dev command that loads .env.staging (copilothub backend) via `electron-vite --mode staging`, so switching between local and staging no longer requires hand-editing env files. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b624cd98ad |
feat: identify clients via X-Client-Platform/Version/OS (#1477)
* feat: identify clients via X-Client-Platform/Version/OS
Adds client identification headers (and matching WS query params) across
all first-party clients so the server can split logs/metrics/gating by
caller without parsing User-Agent.
- HTTP: X-Client-Platform, X-Client-Version, X-Client-OS
- WS: client_platform, client_version, client_os query params
- Platform ∈ {web, desktop, cli, daemon}; OS ∈ {macos, windows, linux}
Wired through the shared TS ApiClient/WSClient via a new identity option
on CoreProvider. Web reads its version from package.json/env; Desktop
captures version + OS synchronously in preload via sendSync IPC. Go CLI
and daemon clients populate the same headers using runtime.GOOS
(normalized darwin → macos).
Server-side adds a ClientMetadata middleware that stashes the headers in
request context; the request logger and logger.RequestAttrs surface them
on every access log and handler-level log. Realtime hub logs the same
fields on websocket connect.
CORS allowlist extended for the new headers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: address client-identity PR nits
- Memoize the CoreProvider identity object on Web and Desktop, and key
WSProvider's effect on identity primitives instead of the object
reference, so unrelated parent re-renders no longer tear down and
reconnect the WebSocket.
- Add direct header-injection tests for the CLI and daemon Go HTTP
clients (X-Client-Platform/Version/OS) and a normalizeGOOS unit test
on both packages.
- Add a TS test for WSClient that asserts client_platform/client_version/
client_os land on the upgrade URL and never leak the auth token.
- Add a hub test that dials the WS endpoint with client_* query params
and asserts the "websocket connected" log entry surfaces them as
structured attributes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
3fd2fb2ae3 |
feat(onboarding): redesigned flow + post-landing starter content opt-in (#1411)
* docs(onboarding): add redesign proposal Captures motivation (two activation funnels), research-backed principles, final 5-step flow (welcome+questionnaire → workspace → runtime → agent → first-issue), Q1/Q2/Q3 personalization matrix, backend user_onboarding schema, API design, resume policy, and development ordering (frontend-first with Zustand stub, backend-last, server swap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): scaffold redesigned flow and state foundation Work-in-progress scaffold toward the redesign documented in docs/onboarding-redesign-proposal.md. This commit is intentionally broad — subsequent commits will replace step content and wire real personalization. Not ready for merge. Included: - packages/views/onboarding/: flow orchestrator + 5 step components (welcome/workspace/runtime/agent/complete) and the CLI install card. Step content is the placeholder version; Step 1 (questionnaire) and Step 5 (first issue) are the next changes. - packages/core/onboarding/: dev-phase Zustand store + types. Not persisted — every page refresh starts at Step 1 so each step can be iterated in isolation. Will swap to TanStack Query + PATCH /api/me/onboarding once the backend user_onboarding table ships (keeps the exported hook surface stable). - packages/core/paths/resolve.ts + .test.ts: centralized resolvePostAuthDestination. Priority is flipped so !hasOnboarded wins over workspace presence — during frontend development every login re-enters /onboarding. useHasOnboarded() reads from the store so the real onboarded_at semantic lands automatically once the backend ships. - Post-auth wiring: callback page, login page, landing redirect, dashboard guard, realtime workspace-loss handler, settings leave/ delete, invite acceptance, and desktop app shell all delegate to the shared resolver instead of inline logic. - Desktop overlay: 'onboarding' added as a WindowOverlay type alongside new-workspace / invite, with a navigation-adapter interception so push('/onboarding') opens the overlay. - packages/core/package.json / packages/views/package.json: add new subpath exports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(onboarding): revise questionnaire to role-driven 3-question form Aligns the proposal with the corrected product positioning: Multica is an AI agent orchestration platform for diverse users (developers, product leads, writers, founders), not a coding-focused tool. Key changes: - Drop Q1 "which agents do you already use?" — daemon auto-detects installed CLIs on PATH; asking is both redundant and less accurate - Add Q2 "what best describes you?" (role) to drive Step 4 template default and Onboarding Project sub-issue filtering - Keep Q1 team_size, refine Q3 use_case (recover writing/research option); all three now have "Other" with an 80-char text field - Q3 use_case_other is embedded into Step 5 first issue prompt so Other users get maximally personalized aha moments, not generic ones - Agent templates: 3 → 4 (Coding / Planning / Writing / Assistant), matrix driven by Q2 × Q3 - Onboarding Project sub-issues: surface Autopilot and Workspace Context (product differentiators), replace "orchestration" wording - Schema JSONB example and §5/§9 execution plan updated to match Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): align questionnaire shape with role-driven redesign Prepares the core state layer for the Step 1 questionnaire rewrite. Type-only and initial-value changes; no behavior changes (nothing was reading the removed `existing_agents` field, since no questionnaire UI exists yet). - Add `Role` type (Q2: developer / product_lead / writer / founder / other) - Add `*_other` sibling fields for team_size / role / use_case so each question's "Other" selection can carry 80-char free text - Drop `existing_agents` — daemon auto-detects CLIs on PATH at Step 3, so the signal no longer belongs in the questionnaire - Extend `TeamSize` / `UseCase` unions with `"other"` member - Refine `UseCase` option label (`writing` → `writing_research`) so it matches the widened Q3 scope in the proposal Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): implement Step 1 questionnaire Replaces the placeholder welcome step with the 3-question questionnaire defined in docs/onboarding-redesign-proposal.md §3.4. Answers land in the core onboarding store for later use by Steps 4 and 5. Added: - packages/views/onboarding/components/option-card.tsx — OptionCard + OtherOptionCard. Radio-group ARIA semantics; Enter/Space select; Other variant reveals an 80-char input that auto-focuses on mount. - packages/views/onboarding/steps/step-questionnaire.tsx — merges welcome + Q1/Q2/Q3 into one screen. Local draft state for responsiveness; writes to the core store only on submit. Skip/ Continue CTA swap driven by "any answered?"; the only disabled case is "picked Other but the text box is blank". - Test coverage for the CTA rules, Other-clear-on-switch behavior, initial-answers pre-fill, and full payload shape. Modified: - packages/views/onboarding/onboarding-flow.tsx — render questionnaire as the first step; persist answers and advance the stored current_step on submit. Other steps still run off local useState for now; full store-driven orchestration follows when Step 5 lands. Removed: - packages/views/onboarding/steps/step-welcome.tsx — superseded. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): split welcome + questionnaire, unblock scroll, drop Q1 evaluating Three fixes prompted by first real browser testing of the Step 1 questionnaire. All three are about making the flow usable before pursuing visual polish. 1. Split Welcome and Questionnaire into two screens The previous merge-welcome-into-questionnaire decision dropped Multica's product introduction entirely. For a product with no established mental model (AI agents as first-class teammates in a task platform), first-time users need 5 seconds of framing before the questionnaire makes sense. StepWelcome carries that framing; it's UI-only (not a persisted step), shown only on first entry (pristine store), and skipped automatically on resume. 2. Remove `my-auto` vertical centering from both platform shells Long questionnaire content pushed the centered block's top above the scroll origin, making Continue/Skip unreachable. Top-alignment + natural body/overlay scroll is the boring-but-correct baseline for content of variable height. 3. Drop Q1 "Just exploring for now" option Q1 asks about team structure, not attitude. "Evaluating" was a category error. Low-commitment users already have a zero-friction path (skip all questions). Removing the option simplifies the question and the downstream mapping table. Types, store initial value, proposal doc (§3.1 flow diagram, §3.4 options, §3.5 sub-issue sorting, §3.6 conditionals, §4.1 JSONB schema, §5.2 file list, §7 decisions row, §9.2 execution order) all synced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): center short steps, scroll long ones — correctly this time Previous attempt removed `my-auto` thinking it was responsible for blocked scrolling. That diagnosis was wrong: the real blocker was the root layout's \`body { overflow: hidden }\` (an app-shell convention so sidebar/topbar stay put while the inner content region scrolls). Removing `my-auto` broke vertical centering of short steps (Welcome) without fixing the scroll issue. Correct fix: - Web: page now owns its own scroll container — `h-full overflow-y-auto` on the outermost div decouples from the body's overflow-hidden. - Desktop: the overlay's existing `flex-1 overflow-auto` container already provided scroll; just restoring `my-auto` was sufficient. - Both platforms: inner `flex min-h-full flex-col items-center` + content `my-auto` gives the "short centers, long top-aligns and overflows down" behavior. Per the flex spec, auto margins are ignored on overflowing boxes (they overflow in the end direction), so Continue/Skip remain reachable via scroll even on long steps like the questionnaire. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): add progress indicator + stable header anchor Adds a consistent visual anchor at the top of every step (except Welcome), so transitioning between steps of different content heights no longer shifts the vertical baseline. - packages/core/onboarding/step-order.ts — single source of truth for step order; indicator math reads from here so adding/reordering a step touches only one line - packages/views/onboarding/components/step-header.tsx — dot row + "Step N of M" counter; three dot states (done/current/pending); accessible progressbar semantics - onboarding-flow.tsx — non-welcome steps now render under a shared `<div flex flex-col gap-8>` wrapper with StepHeader on top. Maps the local `complete` render step to the store's `first_issue` until Step 5 lands (one-line function, self-deleting). - step-welcome.tsx — keeps its own min-h-[60vh] + justify-center so the short intro still feels centered once the shell drops my-auto - apps/web + apps/desktop shells — removed `my-auto`. Every non-welcome step now anchors to the same top position, so only the content below the header changes during transitions. Welcome's own internal centering handles its "short content, no header" case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): add web Step 3 platform fork (Desktop / CLI / waitlist) Web users now see a three-way choice at the runtime step instead of being dropped directly into CLI install instructions: - Primary CTA: Download Multica Desktop (bundled runtime) - Alternate: install the CLI (reveals existing StepRuntimeConnect) - Alternate: join the cloud waitlist (captures email, completes onboarding early with cloud_waitlist_email set) Desktop unchanged — its platform shell doesn't pass cliInstructions, so OnboardingFlow routes it straight to StepRuntimeConnect for the bundled-daemon auto-connect path. Rename step-runtime.tsx → step-runtime-connect.tsx to reflect its new single responsibility (connect UI only; platform choice lives in StepPlatformFork). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): capture optional use-case on cloud waitlist Adds a textarea to the waitlist form asking what the user wants to use Multica for. Optional (submit still works with email alone) but surfaces a clear prompt + placeholder example so most users will fill it in. Stored as cloud_waitlist_description alongside the email. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): make !hasOnboarded a first-class gate on both platforms Triggering condition was wrong on both sides. Web's dashboard-guard only checked hasOnboarded when the URL slug failed to resolve; desktop's App.tsx effect returned early when wsCount > 0 before even looking at hasOnboarded. Users with existing workspaces never got routed into onboarding regardless of their flag state. Also wire store.complete() into the happy-path finish — previously only the waitlist branch wrote onboarded_at, so every normal completion left the flag false and (now that triggers work) would loop users back into onboarding on refresh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): Step 5 auto-bootstrap — welcome issue + Getting Started project After agent creation, the flow transitions to a loader screen that runs the bootstrap in the background: - Creates a welcome issue with a Q3-driven prompt, assigned to the new agent (so it starts working immediately) - Creates a "Getting Started" project with tutorial sub-issues filtered by Q1/Q2/Q3 - Stores first_issue_id + onboarding_project_id via store.complete() - Navigates the user straight into the welcome issue detail page, where they see the agent already responding Degraded path: if welcome issue fails, shows error with Retry / Continue anyway. If project or sub-issues fail, logs and proceeds with just the welcome issue — the aha moment still happens. No-agent paths (runtime skip, agent skip) short-circuit to onComplete without bootstrap. Local flow step union now aligns with the store enum; removed the mapLocalToStoreStep bridge and deleted the old step-complete.tsx placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): converge all no-agent paths to a single bootstrap step Before: skip-runtime, skip-agent, and waitlist each finished onboarding independently, bypassing Step 5 entirely. Users without an agent landed in an empty workspace with no tutorial project — the "self-serve" case had no bootstrap at all. Now: all three paths converge on the first_issue step with agent=null. Bootstrap branches on agent presence: - agent ✓ → welcome issue (assigned to agent) + project + agent-guided sub-issues ("watch your agent do X"). Lands on the welcome issue. - agent ✗ → project only + self-serve sub-issues ("try X yourself" — configure runtime, create agent, write first issue, etc.). Lands on the workspace issues list with the Getting Started project in the sidebar. Both web and desktop shells already handle firstIssueId=undefined → fall back to /<slug>/issues, so no shell-side change was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): pin starter project + assign sub-issues to the user Bootstrap now also: - Pins the Getting Started project so users see it in the sidebar immediately (both paths) - Pins the welcome issue too (path A only) so the first conversation with the agent stays one click away - Assigns every sub-issue to the current user (via their workspace member record). Only the welcome issue stays assigned to the agent — that's the aha-moment hand-off; everything else is for the user to work through Pin calls are fire-and-forget (failure logged but non-blocking). Member lookup is defensive — if listMembers fails or the user isn't found, sub-issues gracefully fall back to unassigned rather than breaking the bootstrap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): remove cloud waitlist option Cloud runtime is not on the immediate roadmap and there's no backend table to persist emails. Keeping the UI around would silently drop user submissions — small trust leak. Revisit once cloud product lands alongside a proper waitlist table + notification pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): persist onboarded_at end-to-end Phase 1 of bringing onboarding from dev stub to production. A single persisted column drives every trigger — no separate user_onboarding table yet (that's a later phase for questionnaire persistence, cloud waitlist, analytics). Backend - Migration 050: ALTER TABLE "user" ADD COLUMN onboarded_at TIMESTAMPTZ (no backfill — existing users see onboarding next login, Skip affordance lands later) - sqlc: MarkUserOnboarded with COALESCE for idempotency - UserResponse DTO + userToResponse now emit onboarded_at via existing util.TimestampToPtr helper — single edit covers GetMe, VerifyCode, GoogleLogin, LoginWithToken - New handler POST /api/me/onboarding/complete - Route registered in the authenticated user-scoped group Frontend - User type gets onboarded_at: string | null - api.markOnboardingComplete() - Auth store adds refreshMe() — lightweight getMe + setUser, complements existing initialize() - useHasOnboarded switches source from onboarding-store (dev stub) to auth-store (user.onboarded_at). Every call site — dashboard guard, desktop App.tsx, invite page fallback, realtime workspace-loss handler, settings leave/delete — picks up the real signal without any direct change - onboarding-store.complete() now hits the server: POST + refreshMe before local state update, so the next router effect sees the non-null timestamp and won't bounce the user back Triggers + route guards - StepWorkspace drops the Skip button — every onboarding user must create their own workspace even if invited into one - /onboarding page redirects already-onboarded users away (guards against manual URL access) - login page + auth callback: onboarding wins over ?next= for unonboarded users; invite links are revisitable after onboarding Tests - apps/web callback tests updated: mocks now return User objects so onboarded_at is readable; new "onboarded user honors next" scenario added, "unonboarded ignores next" scenario kept - test/helpers mockUser gets onboarded_at field - questionnaire already-existing strict-required tests bundled in from a prior uncommitted change Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): review findings — dead state, error recovery, cache races From independent review of the prior onboarded_at commit. - Remove the dead OnboardingState.onboarded_at field, its INITIAL_STATE entry, and its write in store.complete(). useHasOnboarded now reads auth-store exclusively; leaving a parallel field here violates the "don't duplicate server data in Zustand" rule and risks drifting into a second source of truth. - Wrap handleBootstrapDone/handleBootstrapSkip in try/catch with toast recovery. complete() is idempotent server-side (COALESCE), so a retry after a failed POST/refreshMe is free — letting the error bubble into the React error boundary trapped the user with no way forward. - RedirectIfAuthenticated: swap `!list` for `isFetched`-gated check, matching the pattern added on the /onboarding page. Same one-tick race where a stale cache [] could fire a premature replace before the fresh list settles. - (Self-review fixups picked up along the way) /onboarding page now waits for workspacesFetched before redirecting already-onboarded users, and login handleSuccess reads useAuthStore.getState() so the hasOnboarded value is fresh after setUser (the closure captured a stale pre-login value otherwise). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): shrink store surface + firm up flow invariants Post-review cleanup. End-to-end flow is already complete (user.onboarded_at is the single source of truth); these are quality-of-life fixes on top. Store surface - Drop six dead fields from OnboardingState (workspace_id, runtime_id, agent_id, first_issue_id, onboarding_project_id, platform_preference) and the PlatformPreference type. None had readers — they were stub placeholders for a future user_onboarding table that isn't coming this phase. CLAUDE.md "don't design for hypothetical future". - store.complete() signature simplifies to () — no more patch arg, since the only patch fields were the ones just deleted. Welcome as a first-class step - Add "welcome" to OnboardingStep enum and make it INITIAL_STATE's current_step. Removes the pristine-heuristic "did user see welcome?" check, which could misfire on remount. - pickInitialStep() collapses to `state.current_step ?? "welcome"`. - ONBOARDING_STEP_ORDER stays unchanged (welcome isn't a progress point). advance() chain - Every transition handler now persists the new current_step to the store (handleWorkspaceCreated, handleRuntimeNext, handleAgentCreated, handleAgentSkip). Refresh lands on the right step instead of jumping back to Step 2. Invariants - OnboardingFlow throws on null user instead of spreading defensive `?? ""` and `if (userId)` that silently degraded to unassigned sub-issues. Shell guards already ensure user is present. - Desktop WindowOverlay's onComplete gains a paths.root() fallback when workspace is undefined — matches web's symmetry. docs/product-overview.md: committed from untracked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): persist questionnaire + current_step; resume + Back End-to-end questionnaire persistence + resume capability. User answers are now server-side (analytics-ready); refreshing or revisiting lands on the furthest reached step with previous answers pre-filled; a Back button on each step lets users edit earlier answers without losing progress. Backend - Migration 051: ALTER TABLE "user" ADD onboarding_current_step TEXT, onboarding_questionnaire JSONB NOT NULL DEFAULT '{}'::jsonb - sqlc: new PatchUserOnboarding with sqlc.narg for optional fields (COALESCE preserves unspecified columns). MarkUserOnboarded also clears current_step — once complete, the step pointer has no meaning - Handler PATCH /api/me/onboarding accepting partial {current_step, questionnaire}. Questionnaire passthrough via json.RawMessage, no server-side validation of inner shape (keeps schema evolution free) - UserResponse DTO emits both new fields; userToResponse coalesces JSONB to '{}' defensively Frontend - User type gains onboarding_current_step + onboarding_questionnaire - api.patchOnboarding(payload) - Delete Zustand onboarding store — replaced with plain async advanceOnboarding() / completeOnboarding() that call the API and sync auth store. Source of truth is the user object, no client-side shadow state that could drift - pickInitialStep reads user.onboarding_current_step; StepQuestionnaire initial pre-fills from user.onboarding_questionnaire - Monotonic furthestStepRef: Back edits don't regress server-side progress, and re-submit returns the user to where they were - Back buttons on Steps 2/3/4. Back is local-only — just changes the rendered step, no PATCH - Loading indicator on Welcome + Questionnaire submit buttons while PATCH is in flight - CreateWorkspaceForm.onSuccess accepts Promise<void> so the flow can await advance() from its onCreated handler Test mocks (helpers + callback test) updated with new User fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): resume to Step 3+ needs workspace/runtime fallback Self-review caught: resume lands the user on their saved step, but React state (workspace, runtime, agent) is empty on fresh mount. The render conditions gate on those — without fallbacks the page stays blank. - workspaceListOptions() query fills runtimeWorkspace from cache when stepping past Step 2. Only one workspace exists during onboarding (StepWorkspace always creates one), so [0] is unambiguous. - StepWorkspace accepts an `existing` prop. On resume / Back to Step 2 with a pre-existing workspace, render a "Continue with <name>" confirmation instead of the create form, which would otherwise hit a slug conflict the moment the user clicks Create. - runtimeListOptions(wsId, "me") similarly seeds Step 4's runtime — prefer first online, fall back to first. Step 5 resume path unchanged: if `agent` React state is null on re-entry, bootstrap runs the self-serve branch. Not ideal (user may have actually created an agent), but bootstrap's list-check approach (future work) will handle orphan detection symmetrically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): delete all skip/resume jump logic Flow always starts from Welcome. Questionnaire answers still pre-fill from user.onboarding_questionnaire. current_step is still PATCHed for future analytics but no UI code reads it for navigation. Removed from onboarding-flow.tsx: - pickInitialStep + isOnboardingStep (no server-driven entry point) - furthestStepRef + resolveNextStep (no edit-vs-first-pass branching) - runtimes useQuery + stepRuntime fallback (user walks through Step 3 linearly, so runtime React state is always populated by Step 4) - workspace resume fallback in runtimeWorkspace (same reasoning) Kept: - advanceOnboarding({ current_step, questionnaire? }) — server persistence, analytics-ready - StepQuestionnaire's initial prop from stored answers - workspaces useQuery (gated to step === "workspace" only) for existing-workspace detection on Step 2 to prevent slug conflicts when a previous onboarding was abandoned - Back buttons + handleBack (local-only navigation) - Error recovery on completeOnboarding via try/catch + toast Every transition handler is now a straight advance + setStep line. Users who close mid-flow and return walk the full flow from Welcome again — slight extra clicks, but each step shows meaningful confirm UI (existing workspace, connected runtimes, etc.) so it doesn't feel like repeated work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): grandfather existing users in the onboarded_at migration Folded the backfill into 050 itself (branch has not shipped to prod, so editing the migration in place is clean). Without this, once this branch deploys, every pre-existing user would be walled off into onboarding on their next login — a real production incident. Uses created_at rather than NOW() so analytics like "signup → onboarded interval" read correctly for pre-launch users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(onboarding): Step 1 questionnaire — two-column editorial layout Matches the onboarding(3) design spec: full-bleed two-column on lg+ (main + "Why we ask" side rail), collapses to single column below. - StepQuestionnaire rewritten with: - Mono 01/02/03 markers per question - Serif question headings (22px) - Editorial serif title ("Three answers. We'll handle the rest.") - Right-side rationale panel explaining what each answer unlocks - Sticky footer with hint + Continue CTA - Embeds StepHeader on the left column so it escapes the flow's narrow max-w-xl wrapper, same pattern Welcome uses - OptionCard redesigned: radio-dot marker + inset ring on select, matches design's .opt pattern - OtherOptionCard: text input appears below the row (not inside the card) with bottom-border-only styling, aligned under the label - onboarding-flow: questionnaire now early-returns full-bleed, joining Welcome as a hero-layout step Placeholder copy updated to match design examples; tests adjusted. * fix(onboarding): questionnaire uses 3-region app-shell layout Previous version had everything in a single scroll container with a sticky footer. As the user scrolled into the questions, the Back button and StepHeader progress indicator scrolled out of view, and sticky-bottom had edge cases with width-constrained flex nesting. Classic 3-region shell now: - Fixed header row: Back button (left) + StepHeader progress indicator — persistently visible regardless of scroll position - Scrollable middle: eyebrow / serif title / lede / 3 question blocks. Uses `flex-1 overflow-y-auto min-h-0` — the min-h-0 is the critical bit that lets a flex-1 child shrink below content height inside a flex column - Fixed footer row: hint (hidden < sm) + Continue CTA — always reachable, never scrolled off Right "Why we ask" panel is now an independent grid column with its own overflow, so the two columns scroll independently instead of the whole page having one shared scrollbar. Side panel width reduced 520 → 480 to give the question column more room on 1280/1366 screens where 1fr_520 left ~760px for content; 1fr_480 gives ~800-900px which comfortably fits the 620px max-w content column plus breathing room. * fix(onboarding): questionnaire needs DragStrip like every full-window view Traffic lights were overlapping the StepHeader progress dots because Step 1 escaped onboarding-flow's non-welcome wrapper (which renders <DragStrip />) without rendering its own. The codebase convention per packages/views/platform/drag-strip.tsx is: every full-window view places a DragStrip as the first flex child of each visible column. Adds DragStrip at the top of both the left (shell) and right ("Why we ask") columns, matching step-welcome.tsx which already did this. Traffic lights now land in the 48px transparent strip with no content collision; dragging from any top edge moves the window on Electron; border-l between columns runs edge-to-edge. Also made the right column's scroll container use `min-h-0 flex-1 overflow-y-auto` so its internal scroll activates independently of the left column. (Separately investigated: useImmersiveMode is no longer called anywhere in production code — the codebase has fully committed to the DragStrip pattern. No action needed on the hook itself.) * style(onboarding): drop top/bottom borders on questionnaire shell * style(onboarding): use chat-style scroll fade mask instead of border The questionnaire's scroll area now fades softly at top/bottom edges via `useScrollFade` (already used by chat-message-list.tsx) — the same mask-image linear-gradient pattern that fades content under the header/footer based on scroll position: - At top: only bottom fades (hint: more content below) - At bottom: only top fades (hint: content above) - In middle: both fade - Fits entirely: no mask This replaces the removed border-b/border-t on the header/footer with a softer, more editorial visual separation while giving an actual scroll-position affordance the border can't. * feat(onboarding): show "n of 3 answered" progress next to Continue Gives the user a glance-able progress signal as they fill the questionnaire. Static text, no extra UI primitives, no dynamic state variants — just `{n} of 3 answered` updating in place, left of the Continue button. Replaces the static "Your answers shape the next screens..." hint, which was always there regardless of progress and added noise. Same canContinue gate as before (all 3 answered), just derived from the new per-question check so we don't compute validity twice. * style(onboarding): drop redundant lede under questionnaire title The title already conveys the "we'll handle the rest for you" promise — the lede just rephrased it at length. Removed; bumped the question-list top margin (mt-8 → mt-10) to keep breathing room. * feat(onboarding): land redesigned flow + post-landing starter content opt-in This commit bundles the final onboarding-redesign work that sat in the working tree with today's architectural reshape of how starter content is handled. Splitting across sqlc-regenerated files would be fragile, so it ships as one logical unit — "onboarding is ready for production". Flow redesign (Steps 1–5) ------------------------- - Editorial two-column shells on Steps 1/2/3/4 (DragStrip + hero column + aside panel) — Welcome, Questionnaire, Workspace, Runtime, Agent - Web-only Step 3 fork (Download desktop / Install CLI / Cloud waitlist) lives alongside desktop's direct runtime picker; cloud path is interest-capture only, doesn't advance the flow - DragStrip extracted to packages/views/platform as a cross-platform component — 48px transparent drag row, no-op on web - recommend-template.ts + test: Q1–Q3 → AgentTemplate mapping Cloud waitlist -------------- - Migration 052: cloud_waitlist_email VARCHAR(254) + cloud_waitlist_reason TEXT - Handler: net/mail.ParseAddress + length bounds + reason trim - Frontend: CloudWaitlistExpand component + api.joinCloudWaitlist Drop persisted onboarding_current_step -------------------------------------- - The interim implementation persisted the user's furthest-reached step; the final design starts every entry at Welcome, so the column is dead - Migration 051 no longer adds it; migration 053 drops it IF EXISTS on any environment that ran the interim 051 — schema converges cleanly - UserResponse / User type / patchOnboarding signature all drop the field Post-landing starter content (new architecture) ----------------------------------------------- Why: the old design ran bootstrap inside Step 5 (welcome issue + Getting Started project + sub-issues, all in one try block). That had three defects — (1) non-idempotent: Retry after partial failure created duplicates; (2) sub-issue assignee raced listMembers → showed as "Unknown"; (3) skipped users (paths A/C/D) never got any starter content. All three are structural, not patchable. New design: onboarding ends at completeOnboarding() as before (gate is unchanged for useDashboardGuard). The 4 completion paths (Welcome skip / full flow / Runtime skip / Error recover) all just call completeOnboarding() and navigate to workspace. On landing, a StarterContentPrompt dialog renders exactly once per user (starter_content_state == null) with Import / No thanks. The dialog is mandatory — no X, no ESC, no outside-click — so state always ends in a terminal value. - Migration 054: starter_content_state TEXT, backfill 'skipped_legacy' for pre-feature onboarded users so they're never prompted - Server POST /api/me/starter-content/import: transactional claim (NULL → 'imported') + bulk create project + optional welcome issue + sub-issues + pins, all in one tx. 409 Conflict on second call - Server POST /api/me/starter-content/dismiss: transactional NULL → 'dismissed' - Import decides agent-guided vs self-serve by inspecting the workspace's agent list at dialog time — fixes path A (Welcome skip + existing agent) which was previously excluded from starter content - starter-content-templates.ts replaces bootstrap.ts: pure template builders, no API calls. Copy is reviewed as UI; server owns atomicity - StepFirstIssue is now just completeOnboarding() + navigate; error surface collapses to a Retry button (no more "Continue anyway" branch) - OnboardingCelebration + just-completed.ts removed (replaced by StarterContentPrompt which reads server state, not sessionStorage) Handler hardening ----------------- - PatchOnboarding: MaxBytesReader 16KB so the JSONB column can't be weaponized as bulk storage (every /api/me read returns the payload) - JoinCloudWaitlist: net/mail format check + explicit 254-char cap - ImportStarterContent: MaxBytesReader 64KB (templates are markdown-heavy but still bounded); welcome issue's agent_id verified in-workspace Tests ----- - Existing onboarding_test.go (waitlist) passes - step-platform-fork.test.tsx + recommend-template.test.ts (new) - apps/web test helpers updated for User.starter_content_state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(onboarding): resolve Unknown assignee/creator + tighten prompt copy Two surface issues on the post-landing starter content dialog: 1. Unknown assignee & Created by ------------------------------- ImportStarterContent stored `member.id` (the membership row UUID) in `assignee_id` and `creator_id` for sub-issues. That mismatched the rest of the codebase — AssigneePicker and resolveActor in issue.go both store `user_id` for type="member", and `useActorName.getMemberName` looks members up by `user_id`. The mismatch meant the lookup never matched any member and fell through to the "Unknown" fallback. Fix: use `parseUUID(userID)` for both fields. The existing membership check stays for the 403 signal; we just no longer need the returned `member.ID`. 2. Dialog copy too long, button labels unclear ---------------------------------------------- Old copy was 3–4 paragraphs of instruction; users need to read less than that to make a binary choice. Buttons "Import starter tasks" and "No thanks" also didn't make it clear what "No thanks" actually does — it starts a blank workspace, so say so. New: - Title: "Welcome — add starter tasks?" - Body: one sentence describing the seeded content - Left button: "Start blank workspace" - Right button: "Add starter tasks" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(onboarding): server decides starter content branch Problem: the old ImportStarterContent gated the agent-guided vs self-serve branch on a client-supplied `welcome_issue.agent_id` or null `welcome_issue`. The client made that decision by reading its React Query cache of the workspace's agent list — any timing quirk (cache not populated, stale, race with WS event) could lie to the server, and there was no way for the server to disagree. Users with an agent in the DB could still end up on the self-serve branch. Fix: the server is now authoritative. The client always sends both template arrays (agent_guided_sub_issues, self_serve_sub_issues) and a welcome_issue_template (title + description + priority, NO agent_id). Inside the import transaction the server runs ListAgents on the workspace — if there's at least one agent, it picks agents[0] (same ordering the client used: created_at ASC), uses agent_guided_sub_issues, and creates the welcome issue assigned to that agent. Otherwise it uses self_serve_sub_issues and skips the welcome issue. Side effect: the Unknown assignee/creator bug is structurally gone — no client-supplied id flows into assignee_id/creator_id for type= "member". The server uses actorID = parseUUID(userID) everywhere, matching resolveActor in issue.go. Client surface also simplifies: StarterContentPrompt drops useQuery(agentListOptions), the hasAgent check, the agentsFetched button gate, and the branch-specific copy. Dialog description is a single generic line ("If you already have an agent, we'll also seed a welcome issue it replies to right away"). buildImportPayload no longer takes an agentId parameter — one unconditional return shape. Payload grows ~15 KB (both sub-issue arrays always present); still well under the 64 KB MaxBytesReader cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(onboarding): clarify runtime prerequisite, revert dialog agent list Step 3 runtime (desktop step-runtime-connect.tsx) — scanning and empty subtitles now name the local AI coding tools Multica drives (Claude Code, Codex, Cursor, and others), so users understand a runtime alone isn't enough: they also need one of those tools installed on the machine. Uses "and others" rather than a closed list so we don't lock the copy to exactly three integrations. StarterContentPrompt dialog — reverted the short-lived "try Coding, Planning, Writing agents and more" rewrite. That was a misread of feedback meant for the Step 3 prerequisite, not the dialog. The dialog's current single-sentence "how agents, issues, and context work in Multica" is enough. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
536f4286f1 |
docs: add v0.2.11 changelog (2026-04-21) (#1447)
* docs: add v0.2.11 changelog (2026-04-21) Combines the v0.2.9 / v0.2.10 / v0.2.11 releases (plus post-v0.2.11 main commits) into a single landing-page entry, covering the PostHog pipeline, desktop cross-platform packaging, board pagination and the recent inbox / agent-task / markdown fixes. * docs: trim v0.2.11 changelog to user-visible highlights Drop minor fixes and CLI/daemon polish items — keep only the headline features and the visible user-facing fixes. * docs: reprioritize v0.2.11 changelog for external readers Drop internal MUL-/#PR references, swap in the higher-impact fixes (daemon workspace isolation, multica update + Windows daemon, board card description, PostHog default off) that a self-hosted user actually notices. * docs: drop PostHog items from v0.2.11, promote multica update to feature Analytics plumbing is not user-perceivable; replace the PostHog feature and the PostHog default-off fix with multica update (CLI self-update) as a feature and keep the Windows daemon persistence as a fix. * docs: add OpenClaw model read fix to v0.2.11 changelog |
||
|
|
52c9bd72cb |
fix(desktop): unblock Windows + Linux release packaging (#1443)
Two unrelated bugs were preventing the GitHub-hosted runner desktop release matrix from succeeding: 1. Windows job failed with `spawnSync electron-vite ENOENT`. On Windows the package-local binaries are `.cmd` shims and Node's `spawnSync` does not consult PATHEXT unless going through a shell. Pass `shell: true` for both the electron-vite and electron-builder spawns; on POSIX hosts these are real executables so the shell hop is harmless. 2. Linux `.deb`/`.rpm` job failed with electron-builder errors: `Please specify project homepage` and `Please specify author 'email'`. fpm requires a maintainer when generating .deb, and electron-builder derives it from the app package.json metadata. Add `description`, `homepage`, `repository`, `author` (with email) and `license` to apps/desktop/package.json so the Linux targets have the metadata they need. Refs: https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows Refs: https://www.electron.build/configuration.html#metadata Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
df86f559e0 |
fix(desktop): default shareable URL to localhost web in dev (#1438)
The renderer's navigation adapter fell back to https://multica.ai when VITE_APP_URL was unset (i.e. desktop dev builds), so "Copy link" in a dev build produced a production URL instead of one pointing at the running dev web frontend. Match the fallback used by pages/login.tsx (http://localhost:3000) so dev links stay on the dev host. |
||
|
|
a3a6158d96 |
fix: harden desktop packaging PATH lookup (#1435)
Co-authored-by: Eve <eve@multica.ai> |
||
|
|
637bdc8eb3 |
feat(analytics): full PostHog pipeline + 6 funnel events (MUL-1122) (#1367)
* feat(analytics): add PostHog client with async batch shipping Introduces server/internal/analytics, the shipping layer for the product funnel defined in docs/analytics.md. Capture is non-blocking — events are enqueued into a bounded channel and a background worker batches them to PostHog's /batch/ endpoint. A broken backend drops events rather than blocking request handlers. Local dev and self-hosted instances run a noop client until the operator sets POSTHOG_API_KEY. This is PR 1 of MUL-1122; signup and workspace_created emission land in the follow-up commit so this change is independently reviewable. * feat(server): emit signup and workspace_created analytics events Wires analytics.Client through handler.New and main, then emits the first two funnel events: - signup fires from findOrCreateUser (which now reports isNew), covering both the verification-code and Google OAuth entry points — a single emission site guarantees Google signups aren't missed. - workspace_created fires after the CreateWorkspace transaction commits, with is_first_workspace computed from a post-commit ListWorkspaces count so we can distinguish fresh-user activation from returning-user expansion. Tests use analytics.NoopClient so nothing ships from test runs. PR 1 of MUL-1122; runtime_registered and issue_executed follow in later PRs per the plan. * refactor(analytics): drop is_first_workspace from workspace_created Stamping "is this the user's first workspace?" at emit time races under concurrent CreateWorkspace requests: two transactions committing close together can both read a post-commit count greater than one and both emit false. Fixing it at the SQL layer requires a schema change we don't want in PR 1. PostHog answers the same question exactly from the event stream (funnel on "first time user does X" / cohort on $initial_event), so removing the property loses no information and makes the emit side race-free. * docs(analytics): document self-host safety defaults Spell out why self-hosted instances never ship events upstream by default (empty POSTHOG_API_KEY → noop client) and explain how operators can point at their own PostHog project without any code change. * feat(analytics): emit runtime_registered, issue_executed, team_invite_* Three server-side funnel events, all gated on first-time state transitions so retries and re-runs don't inflate the WAW buckets: - runtime_registered fires from DaemonRegister when UpsertAgentRuntime reports (xmax = 0) — i.e. the row was inserted, not updated. Heartbeats and re-registrations stay silent. - issue_executed fires from CompleteTask after an atomic UPDATE issue SET first_executed_at = now() WHERE id = $1 AND first_executed_at IS NULL flips the column for the first time. Retries, re-assignments, and comment-triggered follow-up tasks hit the WHERE clause and no-op. Carries nth_issue_for_workspace so the ≥1/≥2/≥5/≥10 buckets filter without extra queries. - team_invite_sent fires from CreateInvitation and team_invite_accepted from AcceptInvitation, closing the expansion funnel. Adds a 050 migration for issue.first_executed_at plus a partial index so the workspace-scoped executed-count query doesn't scan the never-executed tail. * feat(config): surface PostHog key via /api/config Extends AppConfig with posthog_key / posthog_host sourced from env on every request (so operators can rotate the key via secret refresh without a restart). Reading the key off the server — rather than baking it into the frontend bundle via NEXT_PUBLIC_* — means self-hosted instances inherit the blank key automatically and never ship events upstream. * feat(analytics): wire posthog-js identify + UTM capture on the client Adds @multica/core/analytics — a thin wrapper around posthog-js that owns attribution capture and identity merge. Posthog-js config comes from /api/config (not NEXT_PUBLIC_*), so self-hosted instances whose server returns an empty key automatically run the SDK inert. captureSignupSource stamps a multica_signup_source cookie with UTM params and the referrer's origin (never the full referrer — that can leak OAuth code/state in the callback URL). The backend signup event reads this cookie on new-user creation. Identity flows: - auth-initializer fires identify() right after getMe() resolves, on both cookie and token paths. A getConfig/getMe race is handled by buffering a pending identify inside the analytics module and flushing it once initAnalytics finishes. - auth store calls identify() on verifyCode / loginWithGoogle / loginWithToken and resetAnalytics() on logout so the next login merges cleanly without bleeding events. * docs(analytics): describe runtime_registered, issue_executed, invite events Fills in the schema for the remaining funnel events. Captures the design commentary that belongs next to the contract rather than in a PR description — in particular why issue_executed uses the atomic first_executed_at flip instead of counting task-terminal events, and why runtime_registered relies on xmax = 0 rather than a query-then-write. * fix(analytics): drop non-atomic nth_issue_for_workspace from issue_executed Computing the workspace's Nth-issue ordinal at emit time is not atomic under concurrent first-completions — two transactions can both run MarkIssueFirstExecuted, then both run CountExecutedIssuesInWorkspace, and both observe count=1 before either has committed, so both events go out stamped as n=1. Serialising it would mean a per-workspace advisory lock or a SERIALIZABLE-isolated tx; PostHog answers the same question exactly at query time via row_number() partitioned by workspace_id, so the emit-time property adds risk without adding information. Removes the property from analytics.IssueExecuted, deletes the unused CountExecutedIssuesInWorkspace query, and regenerates sqlc. The partial index stays — any future workspace-scoped executed-issue query will want it. * fix(analytics): wire $pageview and harden signup_source cookie payload Two frontend fixes from the PR review: - PageviewTracker, mounted under WebProviders, fires capturePageview on every Next.js App Router path / query-string change. Without this the capturePageview helper in @multica/core/analytics was never called and the acquisition funnel's / → signup step was empty. - captureSignupSource now caps each UTM / referrer value at 96 chars *before* JSON.stringify, and drops the whole cookie when the serialised payload still exceeds 512 chars. Previously the overall slice(0, 256) could leave a half-JSON string on the wire that neither the backend nor PostHog could parse. Both capturePageview and identify now buffer a single pending call when fired before initAnalytics resolves — otherwise the initial "/" pageview and same-turn login identify race the /api/config fetch and get dropped. resetAnalytics clears both buffers so a logout→login cycle stays clean. * fix(analytics): URL-decode signup_source cookie on read Go does not URL-decode Cookie.Value automatically, so the frontend's JSON-then-encodeURIComponent payload was landing in PostHog as percent-encoded garbage (%7B%22utm_source...). Unescape on read so the backend receives the original JSON string the frontend intended, and drop values that fail to decode or exceed the server-side cap — sending truncated garbage is worse than sending nothing. Oversized-cookie guard matches the frontend's SIGNUP_SOURCE_MAX_LEN. * docs(analytics): reflect nth-issue drop, $pageview wiring, cookie encoding Pulls the schema doc back in line with the code: issue_executed no longer advertises nth_issue_for_workspace (with a note about why PostHog derives it at query time instead), the frontend $pageview section names the actual PageviewTracker component that fires it, and the signup_source section documents the per-value cap / overall drop rule and the encode-on-write / decode-on-read contract. --------- Co-authored-by: Jiang Bohan <bhjiang@outlook.com> |
||
|
|
6f63fae41a |
feat(desktop): support macOS cross-platform packaging (#1262)
* feat(desktop): support macOS cross-platform packaging * fix(desktop): use releaseType instead of publishingType in electron-builder publish config publishingType is not a valid electron-builder key; the correct GitHub provider option is releaseType. The previous value was silently ignored, causing uploads to be skipped and breaking auto-update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(release): standardize artifact naming across desktop and CLI Unified scheme: `multica-<kind>-<version>-<platform>-<arch>.<ext>` so a filename alone reveals kind, version, platform, and CPU arch. Desktop (apps/desktop/electron-builder.yml): mac → multica-desktop-<v>-mac-<arch>.{dmg,zip} linux → multica-desktop-<v>-linux-<arch>.{deb,AppImage} (fixes `\${name}` expanding the scoped `@multica/desktop` into a broken `@multica/desktop-*` filename path) windows → multica-desktop-<v>-windows-<arch>.exe CLI (.goreleaser.yml): multica_<os>_<arch>.tar.gz → multica-cli-<v>-<os>-<arch>.tar.gz (adds `-cli` marker + version; switches `_` to `-` for consistency) Matrix update in apps/desktop/scripts/package.mjs `--all-platforms`: - drop mac x64 (Intel not a target yet) - add linux arm64 Final: mac arm64, win x64/arm64, linux x64/arm64. Downstream updates so install paths match the new CLI names: - scripts/install.sh - scripts/install.ps1 (URL + checksum regex) - CLI_INSTALL.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(release): use multica_{os}_{arch} CLI archive naming Standardize on the GoReleaser default 'multica_{os}_{arch}.{tar.gz|zip}' asset names. Install scripts and the desktop CLI bootstrap now resolve assets via checksums.txt so they work without hardcoding versions. The Go self-update path queries the GitHub release API and accepts either the new or legacy 'multica-cli-<version>-...' names so existing releases keep updating cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(release): ship both legacy and versioned CLI archive names GoReleaser now produces both 'multica_{os}_{arch}.{ext}' (legacy) and 'multica-cli-{version}-{os}-{arch}.{ext}' (versioned) archives in every release. The legacy name keeps already-released CLIs self-updating; the versioned name is what new clients should use going forward. Self-update / install paths flipped to prefer the versioned name and fall back to legacy: - server/internal/cli/update.go (multica update) - apps/desktop/src/main/cli-release-asset.ts (desktop CLI bootstrap) - scripts/install.sh, scripts/install.ps1 (fresh install) Homebrew formula is pinned to the versioned archive via 'ids: [versioned]'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(desktop): also build Linux .rpm packages Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(release): build Linux/Windows Desktop installers in CI; detect Windows ARM64 in install.ps1 Address review feedback on PR #1262: - .github/workflows/release.yml: add a 'desktop' job that runs after the CLI 'release' job and packages the Desktop installers for Linux (AppImage/deb/rpm) and Windows (NSIS) on x64 and arm64, then publishes them to the same GitHub Release via electron-builder. macOS Desktop continues to ship through the manual release-desktop skill so it can be signed and notarized with Apple Developer credentials. - scripts/install.ps1: detect Windows ARM64 hosts via RuntimeInformation::OSArchitecture so the new windows-arm64 CLI archive is downloaded on ARM64 machines instead of always falling back to amd64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(release): split Windows arm64 auto-update channel to avoid latest.yml collision electron-builder's update metadata file is hardcoded to `latest.yml` for Windows regardless of arch (only Linux gets an arch-suffixed name; see app-builder-lib's getArchPrefixForUpdateFile). With two separate electron-builder invocations for Windows x64 and arm64, both publish `latest.yml` to the same GitHub Release and the second upload silently overwrites the first — leaving one of the two architectures with auto- update metadata pointing at the other arch's installer. Route Windows arm64 to its own `latest-arm64` channel: * scripts/package.mjs appends `-c.publish.channel=latest-arm64` only for the Windows arm64 invocation, so x64 keeps producing `latest.yml` and arm64 produces `latest-arm64.yml` alongside it. * updater.ts pins `autoUpdater.channel = 'latest-arm64'` on Windows arm64 clients so they fetch the matching metadata file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
4368e1be18 |
docs: add v0.2.8 changelog (2026-04-20) (#1418)
Summarizes recent releases (v0.2.7 → v0.2.8) on the landing page Change Log in both en and zh. Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai> |
||
|
|
b291db11c2 |
feat(agents): add per-agent model field with provider-aware dropdown (#1399)
Adds a first-class `model` field on agents so users can pick the LLM model from the create / settings UI instead of editing `custom_env` / `custom_args`. Each provider's dropdown is populated from the live CLI when possible (`opencode models`, `pi --list-models`, `openclaw agents list --json`, `cursor-agent --list-models`, hermes ACP `session/new` → `SessionModelState`), with a static catalog for providers that don't enumerate.
Daemon resolves the runtime model as `agent.model → MULTICA_<PROVIDER>_MODEL → ""` — empty passes through so each backend's CLI picks its own default, avoiding static-guess drift.
Per-provider honouring:
- Claude / Codex / OpenCode / Cursor / Gemini / Pi / Copilot — CLI `--model` / thread payload.
- OpenClaw — `opts.Model` is mapped to `--agent <name>` (the CLI rejects `--model`).
- Hermes — `session/set_model` ACP RPC; stderr is sniffed for provider-level errors so HTTP 4xx from the configured LLM surfaces instead of "empty output"; explicit-model failures mark the task `failed`.
Supporting changes: migration 050 adds `agent.model`; daemon ↔ server heartbeat piggyback carries a model-discovery request; new REST endpoints under `/api/runtimes/{id}/models`; `multica agent create --model` / `update --model`; shared `ModelDropdown` in `packages/views/agents` (searchable, creatable, provider-grouped, default-badge, runtime-supported gate).
|
||
|
|
824d943848 |
fix(auth): derive cookie Secure flag from FRONTEND_ORIGIN scheme (#1390)
The session cookie's Secure flag was tied to APP_ENV, and the docker-compose self-host stack defaults APP_ENV to "production". On plain-HTTP self-host deployments (LAN IP, private network) the browser silently drops Secure cookies, leaving every subsequent /api/* call anonymous and surfacing as 401 "auth: no token found" right after a successful login. Derive Secure from the scheme of FRONTEND_ORIGIN so HTTPS origins get Secure cookies and plain-HTTP origins get non-secure cookies the browser will actually store. Also harden cookieDomain() against the other common trap: COOKIE_DOMAIN=<ip>, which RFC 6265 forbids and browsers reject. Log a one-shot warning and fall back to host-only. Docs: correct the COOKIE_DOMAIN description (it was labelled as CloudFront-only but applies to session cookies too) and call out the IP-literal pitfall in SELF_HOSTING_ADVANCED.md, self-hosting.mdx, and .env.example. Refs #1321 |
||
|
|
193046fabc |
docs: add v0.2.7 changelog (2026-04-18) (#1385)
* docs: add v0.2.7 changelog entry (2026-04-18) * docs: trim v0.2.7 changelog to headline items |
||
|
|
62a7c05589 |
feat(desktop): hourly update poll + manual check button in settings (#1366)
* feat(desktop): hourly update poll + manual check button in settings The previous updater only ran one check 5s after launch, so a missed or failed initial check meant the user had to fully restart the app to see a new release. Add a 1h background poll for long sessions and a "Check now" button under a new Updates tab in Settings so the user can trigger a check on demand without waiting. The button reuses the existing autoUpdater pipeline — when an update is available the existing corner notification still drives the download flow; the settings tab only surfaces the immediate check result (up-to-date / available / error). * fix(desktop): trust electron-updater's isUpdateAvailable for the manual check Per review: deriving `available` from a version-string compare is wrong — `updateInfo.version` can differ from `app.getVersion()` while electron-updater still suppresses `update-available` (pre-release channels, staged rollouts, downgrade scenarios, min-system-version gates). In those cases the settings tab would say "vX is available" but no corner download prompt would ever appear. Use `result?.isUpdateAvailable` instead, which is electron-updater's own answer. |
||
|
|
4ce3e5ddf4 |
fix(auth): hand off session to Desktop when web is already logged in (#1364)
When Desktop opens /login?platform=desktop in the browser and the user already has a valid web session, the page previously bounced them to their workspace and Desktop never received a token. Now we mint a bearer token via issueCliToken and redirect through the multica:// deep link so Desktop completes sign-in without a second Google round-trip. Refs: MUL-1080 |
||
|
|
b428f36ca6 |
feat: add ALLOW_SIGNUP + ALLOWED_EMAIL_* for self-hosted instances (#1098)
Closes #930 - Added environment variables to control signups - Updated frontend to hide signup text when disabled - Added backend check to block new user creation via magic link - Updated .env.example |
||
|
|
6cd49e132d |
docs(selfhost): clarify 888888 master code is disabled by default in Docker (#1313)
Following #1307, the Docker self-host stack defaults to APP_ENV=production, which disables the 888888 master verification code on auth.go:169. The installer banners and self-hosting docs still told operators to log in with 888888, leaving them stuck. Update install.sh, install.ps1, SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, and self-hosting.mdx to document the three login paths: configure RESEND_API_KEY (recommended), set APP_ENV=development to enable 888888 for private evaluation, or read the dev verification code from backend container logs. Also warn against enabling APP_ENV=development on public instances. |
||
|
|
2317533da4 |
fix(auth): validate next= redirect target to prevent open redirect (#1309)
* refactor(auth): add sanitizeNextUrl helper in @multica/core/auth Extracts a reusable helper that returns a post-login redirect URL only when it's a safe single-slash relative path, and null otherwise. Rejects absolute URLs, protocol-relative URLs, backslashes, and control characters so call sites can safely pass the result to router.push(). Keeping the rule in a single helper (with direct unit tests) avoids each consumer re-implementing the validation and drifting. * fix(auth): validate next= redirect target to prevent open redirect Closes #1116 Next.js router.push accepts absolute URLs, so a crafted `/login?next=https://evil.example` would send the user off-origin after a successful login. The Google OAuth callback has the same vector via the `state=next:<url>` payload. Sanitize both entry points through `sanitizeNextUrl` from `@multica/core/auth` so only safe single-slash relative paths survive; null results fall through to the existing workspace-list-based default without any hard-coded path. --------- Co-authored-by: JunghwanNA <70629228+shaun0927@users.noreply.github.com> |
||
|
|
c85c43ed0e | docs: add v0.2.5 changelog entry (2026-04-17) (#1269) | ||
|
|
eecb3a2bc8 |
fix(desktop): use releaseType instead of publishingType in electron-builder publish config (#1268)
electron-builder 26.8.1 rejects publishingType under the GitHub publisher; the correct option for selecting draft/prerelease/release is releaseType. Using publishingType caused schema validation to fail during packaging. Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
bf31fa4b39 |
fix(web): move /docs rewrite to beforeFiles (#1266)
* feat(docs): mount docs site at /docs subpath via basePath + multi-zone Configure the Fumadocs site so it can be served at multica.ai/docs: - Add basePath: '/docs' to apps/docs/next.config.mjs - Flatten routes: drop standalone home, render content/docs/index.mdx at the root, move catch-all from app/docs/[[...slug]] to app/[...slug] - Wrap children with DocsLayout in the root layout (was a separate segment-level layout under app/docs/) - Set source loader baseUrl to '/' so URL slugs no longer carry the basePath (Next.js prepends it automatically) - Strip the now-redundant '/docs/' prefix from internal MDX links and drop the duplicate "Documentation" nav entry - Add app/not-found.tsx for App Router 404 handling Wire up multi-zone routing so apps/web proxies /docs/* to the docs app: - Add DOCS_URL env (default http://localhost:4000) and rewrites for /docs and /docs/:path* in apps/web/next.config.ts - Whitelist DOCS_URL in turbo.json globalEnv * fix(web): move /docs rewrite to beforeFiles so [workspaceSlug] doesn't shadow it The /docs rewrite was running in the default afterFiles slot, which is evaluated *after* file-system routing. apps/web/app/[workspaceSlug]/ matched /docs first as a workspace named "docs" (which doesn't exist) and returned 404 before the rewrite to the docs Vercel project ever fired. Splitting rewrites into beforeFiles/afterFiles puts /docs and /docs/:path* ahead of route resolution so they always proxy to the docs zone. |
||
|
|
7c6158f3c9 |
feat(docs): mount docs site at /docs subpath via basePath + multi-zone (#1160)
Configure the Fumadocs site so it can be served at multica.ai/docs: - Add basePath: '/docs' to apps/docs/next.config.mjs - Flatten routes: drop standalone home, render content/docs/index.mdx at the root, move catch-all from app/docs/[[...slug]] to app/[...slug] - Wrap children with DocsLayout in the root layout (was a separate segment-level layout under app/docs/) - Set source loader baseUrl to '/' so URL slugs no longer carry the basePath (Next.js prepends it automatically) - Strip the now-redundant '/docs/' prefix from internal MDX links and drop the duplicate "Documentation" nav entry - Add app/not-found.tsx for App Router 404 handling Wire up multi-zone routing so apps/web proxies /docs/* to the docs app: - Add DOCS_URL env (default http://localhost:4000) and rewrites for /docs and /docs/:path* in apps/web/next.config.ts - Whitelist DOCS_URL in turbo.json globalEnv |
||
|
|
e9131dfe2b |
fix(web): remove dashboard loading.tsx to eliminate double skeleton flash (#1256)
The route-level loading.tsx creates a Suspense boundary that shows a generic skeleton on every page navigation within the dashboard. Since every page already handles its own data-loading skeleton via TanStack Query isLoading, this causes two sequential skeleton flashes: loading.tsx skeleton → page skeleton → content. Removing it makes the old page stay visible during route transitions (typically <100ms), then the new page renders directly with its own skeleton — a single, smooth transition. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fe01d58064 |
docs(cli): document project commands and --project flag for issues (#1253)
The project CRUD commands (list, get, create, update, delete, status) and the `--project` flag on issue commands have been implemented in the CLI but were not yet documented. Add them to both the docs site reference and the repo-level CLI_AND_DAEMON.md so the feature is discoverable. Closes MUL-867 Co-authored-by: Eve <eve@multica.ai> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
fc1938fe7d |
refactor(desktop): centralize shell.openExternal through a single wrapper (#1255)
Make the http/https scheme allowlist structurally enforced instead of a convention. Move the allowlist check + shell.openExternal call into a single openExternalSafely wrapper in external-url.ts, have both main-process call sites (the IPC handler and setWindowOpenHandler) go through it, and add an ESLint no-restricted-syntax rule that bans direct shell.openExternal usage anywhere under apps/desktop/src/main/ except external-url.ts itself. This is the follow-up to #1124: same safety guarantee, but a reviewer can no longer accidentally reintroduce a bare shell.openExternal somewhere that bypasses the check — the lint rule catches it at CI time. Also restores the scheme info in the warn log (lost when the helper was extracted). Test coverage extended to the cases the original PR review flagged but didn't ship: casing (FILE:// / HTTPS://), javascript: / data:, ftp / smb, vscode:// / ms-msdt:, mailto / tel, credentials-in-URL, empty / malformed. Added two openExternalSafely tests (electron mocked) confirming allowed URLs forward and rejected URLs do not. Closes a follow-up bullet from the internal #1115 / #1124 review. |
||
|
|
1ea6e6a078 |
fix(desktop): restrict shell.openExternal to http/https schemes (#1124)
* fix(desktop): restrict shell.openExternal to http/https schemes The Electron main-process IPC handler for shell:openExternal called shell.openExternal with whatever string the renderer passed, with no scheme validation. Under this app's intentional webSecurity: false and sandbox: false configuration (#648), any unsafe content path in the renderer reaching this IPC becomes a way to dispatch arbitrary OS protocol handlers — file://, smb://, vscode://, Windows ms-msdt:, and so on. Parse the URL and reject anything outside http/https (the only schemes any legitimate call site uses today). Matches the Electron security checklist guidance for openExternal on non-isolated renderers. Closes #1115 * Close the desktop external-open gap on target=_blank links The original fix validated only the IPC path, but the renderer could still trigger shell.openExternal through setWindowOpenHandler for target="_blank" links and window.open(). This change reuses one allowlist helper for both sinks and adds a focused unit test for the helper contract. Constraint: Desktop shell.openExternal must stay limited to http/https despite webSecurity=false and sandbox=false Rejected: Duplicate URL validation logic in each sink | easy to drift and harder to test Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep all desktop external-open paths on the same validator so new sinks do not bypass the allowlist Tested: pnpm --dir /Users/jh0927/Workspace/multica-pr1124-followup --filter @multica/desktop test -- src/main/external-url.test.ts Tested: pnpm --dir /Users/jh0927/Workspace/multica-pr1124-followup --filter @multica/desktop typecheck Not-tested: Full desktop app manual smoke run Related: #1115 --------- Co-authored-by: shaun0927 <shaun0927@users.noreply.github.com> |
||
|
|
c15212c0e4 |
fix(views): align skeleton loading states with actual page layouts (#1251)
- Issues/MyIssues: remove incorrect border-b from toolbar skeleton, add viewMode-aware skeleton (list vs board) - Issue Detail: fix content padding (max-w-4xl mx-auto) and sidebar width (w-80), remove independent reactions/subscribers/timeline skeleton flashes — components now render with empty defaults - Agents/Skills: gate skeleton on data query isLoading instead of auth store isLoading so skeleton covers actual data fetch - Projects/Autopilots: add sticky column header skeleton row - Autopilot Detail: add PageHeader skeleton, flesh out section structure - Invite: replace plain text with Card-shaped skeleton - Chat: migrate ChatMessageSkeleton to use Skeleton component - Workspace layout: show MulticaIcon loading indicator instead of blank Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
dcd050ca69 |
fix(desktop): set electron-builder publishingType to release (#1242)
Our CLI release flow pre-creates a *published* GitHub Release via `gh release create`. electron-builder's default `publishingType: draft` conflicts with `existingType=release` and causes the DMG/ZIP/blockmaps/ latest-mac.yml uploads to be silently skipped, which breaks electron-updater auto-update on installed clients (observed on v0.2.4, had to fall back to `gh release upload` manually). Explicitly setting `publishingType: release` aligns electron-builder with our release flow so desktop artifacts are uploaded to the existing published release automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
80a24bf627 |
refactor(desktop): tabs are per-workspace, not cross-workspace (#1239)
* refactor(desktop): tabs are per-workspace, not cross-workspace Tabs are now grouped by workspace in the store; the TabBar shows only the active workspace's tabs, and switching workspace swaps the visible group. Before this change tabs were a flat list that spanned workspaces, which produced a confusing experience: working in acme with three tabs, then switching to butter and back, still showed whatever tabs you happened to open while you were in butter alongside your acme work. The bug had the same shape as the pre-workspace-overlay bug we fixed in #1237 — a concept ("workspace") was encoded in data (tab paths) but ignored by the UI that displayed it (TabBar). The fix is structural: make the data model match the concept. Key changes: - **Schema**: `{ activeWorkspaceSlug, byWorkspace: {slug: {tabs, activeTabId}} }`. The invariant "every tab belongs to a workspace group" is enforced at sanitize time and at migration time; there is no longer a root `/` sentinel. - **NavigationAdapter** detects cross-workspace pushes and delegates to `switchWorkspace(slug, path)` instead of navigating the active tab's router. All existing call sites in shared code (sidebar dropdown, settings post-delete redirect, invite-accept, cmd+k) keep calling `push(paths.workspace(x).issues())` unchanged. - **TabContent** renders only the active workspace's tabs under Activity. Cross-workspace state preservation is an explicit non-goal — switching workspaces should feel like switching. - **WorkspaceRouteLayout** auto-heal no longer navigates the tab router to `/`. Stale-slug cleanup is a store-level op (`validateWorkspaceSlugs`) that drops the whole stale group in one go. - **App.tsx** bootstrap seeds `activeWorkspaceSlug` when null and the user has workspaces; the new-workspace overlay opens/closes based on workspace count independently of any route. - **Persistence migration** (v1 → v2) groups old flat tabs by extracted slug, drops root / transition / reserved-slug tabs, and picks an active workspace from the old active tab's owning group. No data loss for existing users with workspace-scoped tabs. Web is unchanged — tabs are a desktop-only concept. `packages/views`, `packages/core`, `apps/web` are all untouched. `setCurrentWorkspace` in core remains the single source of truth for the API client's workspace header, driven by `WorkspaceRouteLayout` as before. Tests: 19 tab-store tests (sanitize, migration, switchWorkspace, validate, close-last-reseeds, reset). 38 desktop tests total pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: stable selectors + defensive guards on tab-store Addresses self-review findings on #1239. **C1 — perf cliff from unstable selector returns.** The previous `useActiveTab()` selector used `.find()` inside, so every router tick on the active tab (which replaces the Tab object via immutable spread in updateTab / updateTabHistory) forced every subscriber to re-render. Replaced with finer-grained selectors: - `useActiveTabIdentity()` — { slug, tabId } primitives (stable across unrelated updates). - `useActiveTabRouter()` — stable object reference for a tab's lifetime. - `useActiveTabHistory()` — { historyIndex, historyLength } numbers. `useTabHistory` and `DesktopNavigationProvider` now consume the primitive selectors, so back/forward buttons don't churn on every path change. A non-hook `getActiveTab(state)` helper covers the event-handler case. **I1 — `switchWorkspace` no-ops on empty slug.** Defensive guard in case a malformed path ever reaches the adapter's detector. **I2 — merge warns on path/slug mismatch.** Previously silent drop; now `console.warn` makes the condition visible during debugging. **Misc — TabRouterInner takes `tab` prop directly.** Passing the Tab object eliminates a redundant store read per rendered tab. Known follow-up (not this PR): `packages/core/realtime/use-realtime-sync.ts` still uses `window.location.assign` for workspace-deleted eviction — that's a full renderer reload on desktop, which post-refactor wastes the careful in-memory tab state we just set up. Fixing cleanly requires a navigation-callback injection pattern through CoreProvider, which is cross-cutting and deserves its own PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workspace): navigate away BEFORE leave/delete mutation to avoid CancelledError Symptom: deleting the current workspace logged "current workspace deleted, switching" from the realtime handler and surfaced an "Uncaught (in promise) CancelledError" from TanStack Query's refetchQueries batch. Root cause: a three-way race between the mutation's own invalidateQueries(workspaceKeys.list()), the settings page's navigateAwayFromCurrentWorkspace() fetchQuery, and the realtime workspace:deleted handler's relocateAfterWorkspaceLoss fetchQuery. All three refetched the same query concurrently; TanStack Query cancelled the in-flight loser(s), and the rejection bubbled out of invalidateQueries as an unhandled promise rejection. Fix: invert the order. Compute the destination from the current cached workspace list, navigate immediately, *then* fire the mutation. By the time the backend fires workspace:deleted, the active workspace is already something else — the realtime handler's "current === deleted" check fails and its relocate branch no-ops. Only one refetch happens (the mutation's onSettled), no race, no cancellation. navigateAwayFromCurrentWorkspace no longer needs async/fetchQuery since it reads from cache and returns before the mutation fires. Applies to both Leave and Delete flows. Both web and desktop benefit since the code is in packages/views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(desktop): clear workspace singleton + flex drag strip + defer seeding Three issues that the last round of delete-workspace fixes missed. **1. `setCurrentWorkspace` singleton leaks after delete.** Navigating before the mutation (prior fix) changed the URL but nothing cleared the core platform's currentSlug/currentWsId singleton. Three downstream consumers still believed the deleted workspace was active: - `useRealtimeSync`'s `workspace:deleted` handler: its `getCurrentWsId() === deleted` check fired, triggering a parallel relocate that raced the mutation's invalidate and the settings page's navigate — CancelledError + `window.location.assign` (white screen reload). - Chrome gating: `{slug && <AppSidebar />}` stayed truthy, the sidebar mounted, and `useWorkspaceId` inside it threw because the workspace was gone from the list cache. - API client's `X-Workspace-Slug` header: stale on the next call. Fix: `navigateAwayFromCurrentWorkspace` now calls `setCurrentWorkspace(null, null)` before pushing. The next workspace's `WorkspaceRouteLayout` re-sets the singleton when it mounts; for the last-workspace case, null is the correct state (overlay has no workspace context). Same family as the previous logout bug: persist only writes to storage, reset on logout must also wipe in-memory state. Here the singleton is another in-memory bit that survives a URL change if we don't explicitly clear it. **2. "Cannot update a component while rendering" warning.** The per-workspace-tabs refactor kept the validate+seed call in render phase (matching the pre-refactor pattern). It worked before because `validateWorkspaceSlugs` is idempotent; the new `switchWorkspace` seed is not, and triggers a TabBar re-render during AppContent's render. Moved to `useLayoutEffect` — synchronously after render, before paint, no flicker. **3. Welcome-screen drag region didn't work on desktop.** The absolute-positioned `h-10 z-10` drag strip relied on z-index stacking to beat the content wrapper's no-drag for hit-testing, which wasn't reliable for `-webkit-app-region` on the overlay. Replaced with a flex child (`h-12 shrink-0` at top of the overlay's flex-col), so the drag region owns its own layout space — any pixel in the top 48 is unambiguously drag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(CLAUDE): desktop-specific rules — routing, singleton, drag, UX split Codifies the lessons from the recent desktop refactor series (#1237, #1238, #1239) so future work doesn't re-derive them from bugs. Covers: - **Route categories** (session / transition / error) — explains why `/workspaces/new` and `/invite/:id` are overlay state, not routes, on desktop; stale slugs auto-heal instead of rendering error pages. - **`setCurrentWorkspace` singleton hygiene** — unmount doesn't clear it; any code leaving workspace context must call `setCurrentWorkspace(null, null)` explicitly. - **Workspace destructive operations ordering** — navigate first, mutate after, to avoid the three-way refetch race that surfaces as CancelledError + full-page reload. - **Tab isolation** — tabs are grouped per workspace; cross-workspace push is intercepted by the navigation adapter and translated into switchWorkspace. - **Drag region pattern** — flex child at top, not absolute overlay; `-webkit-app-region` hit-testing is unreliable with z-index stacking. - **UX vs platform chrome split** — UX affordances (Back, Log out, welcome copy) in packages/views/; platform chrome (drag, immersive mode, tab system) in desktop-only code. Also patches the Cross-Platform Development Rules' rule #2 which previously said "add a route in both apps" unconditionally — added the exception for pre-workspace transition flows pointing at the new Desktop-specific Rules section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |