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.
Follow-up to #1434. The merge-in of db-reset from main happened during
#1434's conflict resolution and didn't get a `##` description, so it
doesn't appear in `make help`. Add the one-line description so the
target surfaces under the Database grouping alongside the other `db-*`
commands.
* feat: add awk style make help
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
* Address review nits for make help
- Add ##@ Help section so help / makehelp targets are no longer orphaned
between the intro blurb and ##@ Self-hosting.
- Explicitly set .DEFAULT_GOAL := help and document that bare `make` now
prints help instead of launching selfhost. This is a safer default for
onboarding, but it is a behavior change from the previous first-target
default (selfhost).
---------
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
* 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>
The v0.2.6 self-host security fix (#1307) defaults APP_ENV to "production"
in docker-compose.selfhost.yml, which disables the 888888 master verification
code. The follow-up docs PR (#1313) covered SELF_HOSTING.md and the
installers, but `.env.example` — the file users actually copy and edit —
still makes no mention of APP_ENV, so operators who don't read the prose
docs hit the exact same "888888 stopped working after upgrade" confusion
reported in #1331.
- Add APP_ENV= to .env.example with a comment block that spells out the three
cases (Docker default, local dev, evaluation) and warns against enabling
the bypass on public instances. Keeping the value empty preserves the
current `make dev` UX (Go server reads empty → treats as non-production →
888888 works locally) while `${APP_ENV:-production}` in the compose file
still ensures public Docker deployments are safe by default.
- Update the existing 888888 mention under # Email so it no longer
contradicts the new gating rule.
- Update the `make selfhost` post-start banner, which still told operators
to "Log in with any email + verification code: 888888" even after #1307
disabled that path by default.
* fix(daemon): normalize hostname by stripping .local mDNS suffix
Daemons started via different methods (standalone CLI vs desktop app
bundled binary) resolve the hostname differently on macOS — one gets
'computer' and the other 'computer.local'. This caused duplicate runtime
registrations for the same machine.
Stripping the .local suffix at the point of hostname resolution ensures
both always register under the same identifier.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): move empty-host fallback to after .local trim; fix Makefile @ prefix
- Reorder: TrimSuffix runs first, then empty-check, so a hostname of
just ".local" doesn't propagate as an empty daemon_id/device_name
- Add missing @ prefix on migrate command in Makefile so it isn't
echoed twice at startup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(agent): add GitHub Copilot CLI backend
Integrate Copilot CLI as a new agent backend using the stable
`-p` JSONL mode (`--output-format json`), following the same
spawn-CLI-scan-JSONL pattern established by claude.go.
Backend (server/pkg/agent/copilot.go):
- Spawn `copilot -p <prompt> --output-format json --allow-all-tools --no-ask-user`
- Parse streaming JSONL events (system/assistant/user/result/log)
- Extract session ID for resume support (`--resume <id>`)
- Accumulate per-model token usage for billing
- Filter blocked args to prevent protocol-critical flag overrides
Daemon config:
- Probe MULTICA_COPILOT_PATH / MULTICA_COPILOT_MODEL env vars
- Copilot uses AGENTS.md (native discovery) and default skills path
Frontend:
- Add Copilot logo SVG and provider switch case
Tests: 14 unit tests covering arg building, event parsing, usage
accumulation, and edge cases. All Go + TS checks pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(daemon): add restart subcommand, make daemon uses it
- `daemon start` keeps original behavior: errors if already running
- `daemon restart` stops existing daemon then starts fresh
- `make daemon` now runs `daemon restart --profile local`
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(copilot): address review nits 1-5
- Nit 1: Add MinVersions["copilot"] = "1.0.0"
- Nit 2: Seed activeModel from session.start.data.selectedModel (falls
back to opts.Model, then "copilot"). First-turn tokens now get correct
model attribution.
- Nit 3: Handle assistant.reasoning/reasoning_delta → MessageThinking,
reasoningText in assistant.message → MessageThinking,
session.warning → MessageLog{warn}
- Nit 4: Extract handleCopilotEvent() method shared by production and
tests — no more duplicated switch body that can drift
- Nit 5: Deltas write to output buffer as defense-in-depth; if process
dies before assistant.message, output is non-empty
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent): add Cursor Agent CLI runtime support
Add cursor-agent as a new agent backend, following the same pattern as
existing providers. The implementation spawns cursor-agent CLI with
stream-json output, parses JSONL events into the unified Message type,
and supports session resume, usage tracking, and auto-approval (--yolo).
Changes:
- server/pkg/agent/cursor.go: cursorBackend implementation
- server/pkg/agent/cursor_test.go: unit tests for args, parsing, errors
- server/pkg/agent/agent.go: register "cursor" in New() factory
- server/internal/daemon/config.go: probe cursor-agent in PATH
- server/internal/daemon/execenv/context.go: cursor skill discovery path
- server/internal/daemon/execenv/runtime_config.go: AGENTS.md injection
- packages/views/.../provider-logo.tsx: cursor logo in UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): address PR review for cursor backend
1. Fix token usage double-counting: usage is now taken exclusively from
"result" events (session totals). Per-message usage in "assistant"
events is intentionally ignored. "step_finish" usage is only used as
fallback when no "result" usage is available.
2. Remove dead code: isCursorUnknownSessionError() and its regex were
defined but never called. Removed along with corresponding test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): add missing CustomArgs, SystemPrompt, MaxTurns, and debug logging to cursor backend
- Add cursorBlockedArgs and filterCustomArgs support for safe custom arg passthrough
- Add --system-prompt and --max-turns flag support to buildCursorArgs
- Add debug logging of command args before execution (consistent with all other backends)
- Move stdout-close goroutine inside main goroutine (consistent with claude.go pattern)
- Add tests for SystemPrompt/MaxTurns and CustomArgs filtering
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: make daemon uses local profile & update Cursor logo to official brand
- Makefile: make daemon now runs 'daemon start --profile local' for local dev
- Replace Cursor runtime logo with official brand SVG (removed background rect)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent): remove unsupported --system-prompt and --max-turns from cursor-agent
cursor-agent CLI does not support these flags. Instructions are already
injected via AGENTS.md and .cursor/skills/ files.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent): prevent step_finish + result usage double-counting in cursor
Split usage accumulation into separate stepUsage and resultUsage maps.
After stream ends, use resultUsage if available (session totals from
result event), otherwise fall back to stepUsage (sum of step_finish).
This prevents 2x counting when result.usage already includes totals.
Added table-driven test covering: result-only, step_finish-only,
step_finish+result (no double count), and multi-model scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(agent): fix misleading comment on cursor -p flag
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: yushen <ldnvnbl@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Ensures the database schema is always up to date when starting the app,
preventing silent API failures caused by missing columns after pulling
latest changes.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Decouple install.sh from environment configuration — install.sh now only
installs the CLI binary (and optionally Docker via --with-server), while
all environment configuration moves to `multica setup` subcommands.
Key changes:
- install.sh: remove config writes, rename --local to --with-server
- multica setup: add cloud/self-host subcommands with --server-url,
--app-url, --port, --frontend-port flags and --profile support
- Add config overwrite protection with interactive prompt
- Remove redundant commands: `config local`, `auth login` alias
- Replace silent multica.ai fallbacks with explicit errors
- Onboarding wizard: dynamically show correct setup command for
Cloud vs Self-host environments
- Update all docs, landing page, and install scripts for consistency
Simplifies local development from 3+ commands to a single `make dev`
that auto-detects environment (main/worktree), creates env files,
installs dependencies, starts PostgreSQL, runs migrations, and launches
both backend and frontend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: skip Docker check in ensure-postgres.sh when remote DATABASE_URL is set
When DATABASE_URL points to a non-localhost host, the script now skips
all Docker operations and only verifies remote DB connectivity via
pg_isready directly.
* fix: honor DATABASE_URL for remote postgres preflight
* fix(make): clarify stop output for remote database
* docs: add local deployment protocol guidance to SELF_HOSTING.md
Clarify that local deployments without TLS should use http:// and ws://
instead of https:// and wss://.
---------
Co-authored-by: Junlong Liu <junlong.liu@shopee.com>
setup-worktree was using FORCE=1 to unconditionally regenerate
.env.worktree, which overwrites any manual edits (e.g. switching
to the main DB). Now it only generates the file if it doesn't exist.
Remove the frontend-only knowledge base page, sidebar nav entry, and
unused BookOpen import. Fix setup-worktree to always generate unique
ports via init-worktree-env.sh instead of skipping when .env.worktree
already exists with default ports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adapt runtime features (usage tracking, ping, heartbeat) to main's
multi-workspace architecture. Update frontend imports from @multica/types
to @/shared/types after the package consolidation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new "Runtimes" sidebar tab to manage local agent runtimes with three
main capabilities: runtime status overview, token usage tracking (reading
Claude Code and Codex CLI local JSONL logs via daemon), and an interactive
connection test that sends a ping through the daemon to verify end-to-end
agent CLI connectivity.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ReposRoot was a daemon-level config that locked all tasks to a single
git repo. Replace with RepoPath in TaskContext so the server can specify
the repo per task. When not provided, daemon falls back to directory mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename the binary and all references from multica-cli to multica for a
cleaner command-line experience.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts:
- CLAUDE.md: merge feature-based frontend docs from main with comprehensive
architecture docs from HEAD
- Makefile: merge .PHONY targets from both branches
- settings/page.tsx: keep Context field using main's Label component
- auth-context.test.tsx: accept main's deletion (moved to features/auth/)
- cmd/daemon/daemon.go: accept HEAD's deletion (moved to internal/daemon/)
- daemon/client.go: port requestError type and isWorkspaceNotFoundError from
main's old daemon into the new internal/daemon package
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Add Client.SendHeartbeat/Register methods — no more direct postJSON calls
2. Use url.Values for query params to prevent URL injection
3. Unexport helpers (envOrDefault, durationFromEnv, sleepWithContext)
4. CLI resolveWorkspaceID falls back to daemon.json
5. Implement agent stop (PUT /api/agents/{id} with status=offline)
6. Add --output flag to agent get for consistent UX
7. Add server/multica to .gitignore for stray builds
8. Inject version/commit via -ldflags in Makefile build target
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract daemon logic from cmd/daemon/ into internal/daemon/ package and
create a new unified CLI entry point at cmd/multica/ using cobra. The CLI
supports `daemon` as a long-running subcommand plus ctrl subcommands for
agent/runtime management, config, status, and version.
Server, migrate, and seed binaries remain unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix kanban board columns not adapting to available width (w-64 → flex-1)
- Fix workspace name not updating in sidebar after save in settings
- Fix comments leaking across issues when navigating between issue details
- Fix duplicate issue appearing on create (race between callback and WebSocket)
- Add real-time WebSocket listeners for agents and inbox pages
- Add `make check` one-click verification pipeline (typecheck + tests + E2E)
- Add E2E test fixtures for self-contained test data setup/teardown
- Add settings E2E test and updateWorkspace unit test
- Make `make start/setup` reuse existing PostgreSQL if already running
- Update CLAUDE.md with AI agent verification loop and E2E test patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add idempotent seed tool with duplicate detection for agents/issues/comments
- Add migration CLI supporting up/down with schema_migrations tracking
- Add Makefile targets: make setup (first-time), make start, make stop
- Update .gitignore for test artifacts and compiled binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>