mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
v0.2.13
84 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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).
|
||
|
|
c0be1b7ce9 |
fix(slugs): audit admin/multica/new/www + reserve in slug list (MUL-972) (#1359)
Follow-up to PR #1188 / migration 047, which intentionally omitted the five historical conflict slugs (admin / multica / new / setup / www) from the reserved-slug audit because each had one production workspace using it at the time and we did not want to block deploy on owner outreach. MUL-972 closed that loop on prd for four of the five: * admin (99cd10e4-…) → renamed to legacy-admin-99cd10e4 * multica (dcd796aa-…) → renamed to legacy-multica-dcd796aa * new (e391e3ed-…) → renamed to legacy-new-e391e3ed * www (5e8d38b2-…) → workspace deleted (was empty: 0 issues / projects / agents, owner-only member; 18 workspace-FK relations all CASCADE) This PR: 1. Adds migration 049_audit_legacy_reserved_slugs which audits those four slugs against workspace.slug at startup. If any future workspace slips in with one of them, startup fails loudly via RAISE EXCEPTION instead of being silently shadowed by a global route. Mirrors the structure of 047. 2. Adds 'multica' / 'www' / 'new' to the reserved-slug allow-deny list in both the Go handler and the shared TS list (admin was already in both). Keeps the two lists in lockstep per the convention enforced in workspace_reserved_slugs.go header. setup is STILL exempt from the audit and is intentionally NOT added to the reserved list. The setup workspace (b43f0bc2-…) is a real production user (owner: Roberto Betancourth, building a chants/Alabanzas app) and is being handled out-of-band via owner outreach. A separate follow-up migration will fold setup into the audit once that workspace's slug has been migrated. Migration is intentionally shipped AFTER the prd data fix (not before): 049 will RAISE EXCEPTION on any remaining conflict, so we want the data state clean first. Rollout order: prd data fix (done by db-boy on 2026-04-20) → this PR. Tested: - go test ./server/internal/handler/ -run TestReserved → pass - pnpm --filter @multica/core test consistency → pass (4/4 in consistency.test.ts; global-prefix↔reserved invariant holds) Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
63800f05ff |
fix(agent): add per-agent mcp_config field to restore MCP access (#1168)
* fix(agent): add per-agent mcp_config field to restore MCP access Closes #1111 The --strict-mcp-config flag was added defensively in #592 to prevent Claude agents from inheriting MCP state from the outer Claude Code session. It was meant to be paired with --mcp-config <path> to inject a controlled set of MCPs, but that path was never implemented, which silently stripped all user-scope MCPs from spawned agents. This PR completes the original design by: - Adding a nullable mcp_config jsonb column to the agents table - Wiring mcp_config through AgentResponse, Create/Update requests - Piping it into ExecOptions.McpConfig in the daemon - Serializing to a temp file and passing --mcp-config <path> in buildClaudeArgs - Blocklisting --mcp-config in claudeBlockedArgs to prevent override via custom_args Does not touch Codex provider (tracked separately in #674). Does not implement Multica MCP auto-injection (out of scope). * fix: disambiguate JSON null vs absent for mcp_config |
||
|
|
a73336dcf8 |
feat(daemon): persistent UUID identity + legacy-id merge at register-time (#1220)
* feat(daemon): persistent UUID identity + legacy-id merge at register-time daemon_id is now a stable UUID persisted to `<profile-dir>/daemon.id` on first start, replacing the hostname-derived id that drifted whenever `.local` appeared/disappeared, a system was renamed, or a profile switched — each of which used to mint a fresh `agent_runtime` row and strand agents on the old one. To migrate existing installs without operator intervention, the daemon reports every legacy id it may have registered under previously (`host`, `host` with `.local` stripped, and `host[-profile]` variants for both). At register-time the server looks up each candidate row scoped to (workspace, provider), re-points its agents and tasks onto the new UUID-keyed row, records which legacy id was subsumed in the new `legacy_daemon_id` column for audit, and deletes the stale row. Result: users running `xxx.local`-keyed runtimes today transparently land on the new UUID row on next daemon restart. The hostname-prefix `MigrateAgentsToRuntime` / `daemon_id LIKE '...-%'` compatibility shim is no longer needed and has been removed along with the handler call that invoked it. * fix(daemon): handle bidirectional .local drift and case drift in legacy merge Review on #1220 flagged two gaps in the legacy-id migration candidate set: 1. Reverse .local: LegacyDaemonIDs only added the stripped variant when the current hostname ended in `.local`. The opposite direction — DB has `foo.local`, current host is `foo` — was missed, so runtimes registered under the `.local` variant stayed orphaned after upgrade. Now both variants (`foo` and `foo.local`) are always emitted, regardless of what `os.Hostname()` currently returns, plus their `-<profile>` suffix forms. 2. Case drift: os.Hostname() has been observed returning different casings on the same machine across mDNS/reboot state. A case-sensitive `=` comparison stranded rows like `Jiayuans-MacBook-Pro.local` when the daemon later reported `jiayuans-macbook-pro.local`. FindLegacyRuntimeByDaemonID now uses `LOWER(daemon_id) = LOWER(@daemon_id)` on both sides, so casing differences merge rather than orphan. The (workspace_id, provider) prefix still bounds the scan to a tiny set of rows so the non-indexed LOWER() comparison has negligible cost. Tests: TestLegacyDaemonIDs gets the mixed-case + reverse-direction cases; daemon_test.go adds TestDaemonRegister_MergesLegacyDaemonIDRuntime_ReverseDotLocal and TestDaemonRegister_MergesLegacyDaemonIDRuntime_CaseDrift. * fix(daemon): consolidate every case-duplicate legacy runtime, not just the first Follow-up review on #1220: after switching to `LOWER(daemon_id) = LOWER(@daemon_id)`, the single-row lookup still only merged one legacy row per candidate. If a machine already had two rows in the DB that differed only in casing (e.g. `Jiayuans-MacBook-Pro.local` AND `jiayuans-macbook-pro.local` coexisting because earlier hostname drift already minted a duplicate), only one of them got consolidated and the other stayed orphaned — violating the "no duplicate runtime per machine after backfill" acceptance. - FindLegacyRuntimeByDaemonID → FindLegacyRuntimesByDaemonID (:many) - mergeLegacyRuntimes iterates every returned row and dedupes across overlapping legacy candidates so `foo` and `foo.local` both resolving to the same stored row don't double-process Test: TestDaemonRegister_MergesAllCaseDuplicateLegacyRuntimes seeds two case-duplicate rows with one agent each and confirms both rows are deleted and both agents end up on the new UUID-keyed row. |
||
|
|
3ea6b5c7b8 |
fix(agent): return 409 on duplicate agent name (#1182)
- Migration 046 adds UNIQUE(workspace_id, name) with dedup (keep most recently updated) - CreateAgent handler returns 409 Conflict scoped to constraint name agent_workspace_name_unique - Dedup verified as (0 rows) against worktree DB; rerun against staging/production before applying - Down migration drops the constraint only; deleted rows and cascaded data are not restored Co-authored-by: Anup Joy <joyanup@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
6d6bc5a6f2 |
fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list (#1188)
* fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list
Two related changes:
1. Rename the global workspace-creation route from /new-workspace to
/workspaces/new. The hyphenated word-group `new-workspace` is a
common user workspace name (last deploy was blocked by a real user
with exactly this slug). Industry consensus from auditing Linear,
Vercel, Notion, Slack, GitHub: zero major SaaS uses hyphenated
word-group root routes — they all use single words or `/{noun}/{verb}`
pairs. Reserving the noun `workspaces` automatically protects the
entire `/workspaces/*` subtree, so future workspace-related routes
(`/workspaces/{id}/edit`, `/workspaces/{id}/billing`, etc.) need no
additional reserved slugs or audit migrations.
2. Extend the reserved slug list to cover the minimal set recommended by
the URL-design audit: full auth flow vocab, RFC 2142 mailbox names
(postmaster, abuse, noreply...), hostname confusables (mail, ftp,
static, cdn...), and likely-future platform routes (docs, support,
status, legal, privacy, terms, security, etc.). Production data
audit confirmed zero conflicts for every newly added slug, so
migration 047 (the safety net) passes cleanly.
Slugs intentionally NOT added despite being in scope of the audit:
admin, multica, new, setup, www. Each has one production workspace
already using it; adding them now would block deploy. They will be
handled in a follow-up PR via owner outreach + targeted rename.
Also adds a CLAUDE.md convention rule: new global routes MUST use a
single word or `/{noun}/{verb}` pair, never hyphenated word groups.
This prevents the pattern from regenerating itself.
This PR does NOT resolve the currently-blocked prd deploy — that requires
the existing `slug='new-workspace'` workspace (owner: Dhruv Raina) to be
renamed by ops. After that workspace is renamed and migration 046 passes,
this PR's migration 047 will also pass on its first run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: drop migration 046, sweep stale comments, drive reserved test from map
Address code review on PR #1188:
1. Delete migration 046 (audit_new_workspace_slug). It audits "new-workspace"
which is no longer a reserved slug after this PR's rename. Removing 046
has an unexpected upside: it directly unblocks the currently-stuck prd
deploy. Migration 046 had never successfully applied (it was the source
of the deploy block); the audit-only nature means down-rollback is a
no-op. The user workspace previously caught by 046 (slug='new-workspace',
owner: Dhruv Raina) is now safe — `new-workspace` is no longer reserved,
so the slug correctly resolves to that workspace and the global route
`/workspaces/new` doesn't shadow it.
2. Refactor workspace_test.go to drive its reserved-slug list from the
reservedSlugs map directly via `for slug := range reservedSlugs`. The
previous hand-copied list was already drifting (40-ish entries vs 58 in
the map). Now drift is impossible.
3. Sweep ~10 stale `/new-workspace` references in code comments to
`/workspaces/new`. Comments only — runtime unchanged. The references
in reserved-slugs.ts/workspace_reserved_slugs.go and CLAUDE.md are
intentionally kept as anti-pattern examples ("don't add hyphenated
word-group root routes like /new-workspace").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
6a2432b16b |
refactor: remove onboarding flow, fix daemon zero-workspace bootstrap (#1175)
* fix(daemon): allow startup with zero workspaces The daemon used to fail fast with "no runtimes registered" when the initial workspace sync returned zero workspaces. This masked a latent bug: a newly-signed-up user has no workspaces yet, so the daemon would crash immediately after login instead of waiting for the first workspace to be created. workspaceSyncLoop already polls every 30s (daemon.go:107, 365) to discover new workspaces — the fail-fast check at startup was bypassing this dynamic discovery. Remove the check so the daemon stays resident and picks up the first workspace whenever it appears. PR #1001 partially addressed this for the "server has workspaces but local CLI config is empty" case. This finishes the job for the true zero-workspace state, which until now was masked by the onboarding wizard always creating a workspace before the daemon started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract CreateWorkspaceForm for reuse Modal and the upcoming /new-workspace page share the same form + mutation + slug validation. Extract to a shared component so they can't drift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(views): add NoAccessPage for unknown or inaccessible workspace slugs Rendered when the URL slug doesn't resolve to a workspace the user has access to. Deliberately doesn't distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(paths): add /new-workspace route and reserve slug on both sides Adds paths.newWorkspace() builder, registers /new-workspace as a global (pre-workspace) prefix, and reserves the "new-workspace" slug on both frontend and backend (kept in sync per convention). Existing "onboarding" reservation retained — removing it would desync FE/BE and leaves no future fallback if an onboarding route is revived. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(migrations): audit no existing workspace uses 'new-workspace' slug Migration 046 blocks deploy if any workspace in the DB has slug = 'new-workspace', which would shadow the new global workspace creation route at /new-workspace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add /new-workspace route on web and desktop Renders the CreateWorkspaceForm as a full-page workspace creation flow, used as the destination for first-time users with zero workspaces. Replaces the 4-step onboarding wizard with a single form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show NoAccessPage on unknown workspace slug, hold null during active removal Layouts render NoAccessPage when the URL slug doesn't resolve to an accessible workspace — except when the slug previously resolved during this layout instance's lifetime. URL and cache are two asynchronous signals: there will always be a short window where the URL still points at the old workspace but the cache has already been invalidated (e.g. just after a delete/leave mutation, or a realtime workspace:deleted event). Rendering NoAccessPage during that window would flash "Workspace not available" with recovery buttons in front of a user who just deleted the workspace themselves — jarring and wrong. useWorkspaceSeen classifies the two cases: - slug was seen before, now gone → user's intent is changing (caller is navigating away); render null, no flash - slug never seen → user is genuinely looking at an inaccessible workspace (stale bookmark, revoked access, link from a former teammate); render NoAccessPage with recovery options NoAccessPage deliberately does not distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: redirect zero-workspace users to /new-workspace instead of /onboarding Switches 8 call sites and the CLI: - Web: login, auth callback, landing redirect-if-authenticated - Desktop: routes.tsx IndexRedirect - Shared: dashboard guard, invite page fallback, workspace-tab on delete, realtime sync on workspace loss - CLI: cmd_login.go waitForOnboarding now opens /new-workspace Also adds /new-workspace to navigation store's lastPath exclusion list so it doesn't get persisted as a 'last visited' page. Adds a desktop App.tsx effect that restarts the daemon when workspace count transitions 0 → ≥1, so first-workspace creation triggers immediate daemon pickup rather than waiting up to 30s for the daemon's workspaceSyncLoop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove onboarding flow The 4-step onboarding wizard (workspace → runtime → agent → demo issues) is replaced by: - /new-workspace: a single-page workspace creation form (Phase 3) - NoAccessPage: explicit feedback when a slug doesn't resolve (Phase 4) - daemon zero-workspace bootstrap (Phase 1) so the daemon doesn't crash before the user creates their first workspace - desktop daemon restart on first workspace creation (Phase 5) for instant pickup instead of the 30s workspaceSyncLoop tick Deletions: - packages/views/onboarding/ (OnboardingWizard + 4 step components + tests) - apps/web/app/(auth)/onboarding/page.tsx - apps/desktop/src/renderer/src/components/onboarding-gate.tsx (+test) - OnboardingGate wrapper in desktop-layout.tsx - OnboardingRoute + /onboarding route in desktop routes.tsx - paths.onboarding() builder + /onboarding from GLOBAL_PREFIXES - packages/views/package.json onboarding export - /onboarding from navigation store's EXCLUDED_PREFIXES Retained (intentional): - 'onboarding' in RESERVED_SLUGS (both FE + BE) — kept for FE/BE sync and future-proofing if /onboarding is ever revived Also drops 4 demo issues that onboarding used to create on the new workspace ('Say hello', 'Set up repo', etc.). New workspaces are now fully empty; all list views already render empty-state UI correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: clean stale 'onboarding' references in comments and CLI helpers Batch cleanup of references to the removed onboarding flow: - 13 comment sites mentioning 'onboarding' updated to reflect the new /new-workspace flow or removed where no longer accurate - CLI waitForOnboarding renamed to waitForWorkspaceCreation (function name + docstring); behavior unchanged The 'onboarding' reserved slug entries (frontend + backend) are intentionally retained — see prior commit rationale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract shared NewWorkspacePage shell The web (/new-workspace) and desktop (NewWorkspaceRoute) pages had identical outer layout — same container, heading, and copy — with only the onSuccess navigation primitive differing. That's exactly the No-Duplication Rule pattern: extract the shared UI, inject the platform-specific behavior. The apps now only own the thin auth guard (web needs it, desktop routes below WorkspaceRouteLayout already handle it) and the onSuccess → navigate call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove rollback compat layer and tighten daemon restart trigger Two cleanup items: 1. Drop localStorage['multica_workspace_id'] double-write in both workspace layouts. That write was added as a rollback safety net for the workspace-slug URL refactor (PR #1138) — the refactor has since landed and stabilized, so the compat shim is no longer needed. Per CLAUDE.md: don't keep compat layers beyond their purpose. 2. Tighten the desktop daemon-restart trigger. The previous ref-based logic fired a restart on any 0→1 workspace-count transition, including account switches (user A logout → user B login). Scope it precisely to 'this session started with zero workspaces and just gained one' using a three-state ref (null=undecided, true=empty-start, false=already-restarted-or-started-nonempty). Account switches are already handled by daemon-manager.ts on token change, so this avoids a redundant restart there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): redirect to /login on logout and unauthenticated workspace visits Two gaps previously left users stuck on blank workspace pages: 1. app-sidebar logout() cleared all state but never moved the URL. The current path is /{workspaceSlug}/... which has no meaning without auth; the workspace layout would then see user=null, render null (via the hasBeenSeen short-circuit), and the user saw a blank page thinking logout didn't work. 2. The workspace layouts (web + desktop) had no !user handling at all. Any path that leaves user=null — token expiration, cross-tab logout, or fresh visit to a workspace URL without a session — resulted in the same blank screen. Fix: - app-sidebar.logout() explicitly push(paths.login()) after authLogout() to cover the primary (user-initiated) logout path. - Both workspace layouts get a defensive useEffect that redirects to /login whenever auth has settled and user is null. Covers token expiration, realtime logout, and any other silent session loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a36252ca99 |
refactor(runtime): derive runtime usage from task_usage only (#1167)
* refactor(runtime): derive runtime usage from task_usage only
The daemon used to scan each runtime's local CLI log directory every 5
minutes (Claude Code, Codex, OpenCode, OpenClaw, Hermes) and post daily
aggregates to /api/daemon/runtimes/{id}/usage. Those directories are
shared with the user's own local CLI sessions, so the user's personal
usage was being counted as Daemon-executed usage. Cursor and Gemini had
no scanner at all, so their runtime-level aggregates were always zero.
Switch GetRuntimeUsage to aggregate task_usage (already scoped to
Daemon-executed tasks) via agent_task_queue.runtime_id. Single source of
truth; Cursor/Gemini/Copilot get runtime usage for free; no reliance on
external CLI log formats.
Removes:
- server/internal/daemon/usage/ (all scanners)
- Daemon.usageScanLoop + providerToRuntimeMap
- Client.ReportUsage
- ReportRuntimeUsage handler + POST /api/daemon/runtimes/{id}/usage
- UpsertRuntimeUsage / GetRuntimeUsageSummary queries
- runtime_usage table (migration 046)
Refs: MUL-786
* fix(runtime): bucket daily usage by task_usage.created_at, not enqueue time
ListRuntimeUsage was aggregating by DATE(atq.created_at) and filtering
on atq.created_at. agent_task_queue.created_at is the enqueue timestamp,
which drifts from actual token-production time: a task queued at 23:58
and executed at 00:05 was attributed to yesterday; a task sitting in
the queue overnight was counted on the queue day.
The ?days=N cutoff also became a rolling window (now() - N) instead of
a calendar-day boundary, silently clipping the morning of the earliest
day returned.
Switch bucket + filter to task_usage.created_at (~= task completion /
usage-report time) and snap the since cutoff to start-of-day via
DATE_TRUNC.
Add a regression test covering both scenarios: cross-midnight task
attributes to the day tokens were reported, and the earliest day's
pre-cutoff rows are still included.
|
||
|
|
fe358feff0 |
Reapply "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139) (#1141)
This reverts commit
|
||
|
|
b30fd98605 |
Revert "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139)
This reverts commit
|
||
|
|
75d12c26c5 |
feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)
* Reapply "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit
|
||
|
|
9b94914bc8 |
Revert "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit
|
||
|
|
59ace95a1e |
feat: workspace URL refactor + slug-first API identity (#1131)
* feat: workspace URL refactor + slug-first API identity
Make the URL the single source of truth for workspace identity.
All workspace-scoped URLs now carry the workspace slug as the first
path segment (/{slug}/issues, /{slug}/projects, etc.), matching the
industry standard (Linear, Notion, Vercel, GitHub).
## Key architectural changes
**URL-driven workspace identity:**
- Web routes moved under app/[workspaceSlug]/(dashboard)/
- Desktop routes nested under /:workspaceSlug
- paths.ts builder centralises all URL construction
- reserved-slugs validation (backend + frontend + DB migration audit)
**Slug-first API contract:**
- Frontend sends X-Workspace-Slug header (from URL) instead of X-Workspace-ID (UUID)
- Backend middleware resolves slug → UUID via GetWorkspaceBySlug, falls back to
X-Workspace-ID for CLI/daemon backwards compatibility
- WebSocket auth accepts ?workspace_slug query param with SlugResolver callback
**State cleanup:**
- Deleted: useWorkspaceStore (Zustand mirror), switchWorkspace/hydrateWorkspace/
clearWorkspace, localStorage["multica_workspace_id"], api._workspaceId
- useCurrentWorkspace() derives from URL slug + React Query workspace list
- useWorkspaceId() is now a bridge hook (no Context, derives from useCurrentWorkspace)
- WorkspaceIdProvider removed from DashboardGuard
- Paired module vars (slug + UUID) in workspace-storage.ts for non-React consumers
**Layout simplified:**
- Render-phase ref guard sets workspace context synchronously (no async gate)
- DashboardGuard handles auth redirect, loading state, and workspace resolution
- Subscriber notifications deferred via queueMicrotask (React 19 compat)
- persist namespace uses slug (immutable) instead of UUID
## Issues resolved
MUL-43 (share links), MUL-509 (mobile workspace switch), MUL-723 (workspace in URL),
MUL-727 (create workspace flash), MUL-728 (delete workspace no-navigate),
MUL-820 (sidebar Join not switching)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve code review C3/C4/C5/C6 — desktop deadlock + hardcoded paths
C3: Desktop OnboardingGate was calling useCurrentWorkspace() outside
WorkspaceSlugProvider → always null → permanent onboarding deadlock.
Rewrite to use useQuery(workspaceListOptions()) which reads React Query
cache directly without slug context. Remove DashboardGuard from
DesktopShell (auth gating handled by AppContent, workspace routing by
WorkspaceRouteLayout per-tab).
C4: Landing page "Dashboard" links hardcoded /issues (no longer valid).
Changed to / — proxy handles redirect to /{lastSlug}/issues.
C5: autopilots-page.tsx had one hardcoded /autopilots/${id} link.
Changed to wsPaths.autopilotDetail(id).
C6: inbox-page.tsx hardcoded /inbox paths. Changed to wsPaths.inbox().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): wrap shell in WorkspaceSlugProvider from module var
AppSidebar calls useWorkspacePaths() → useRequiredWorkspaceSlug() which
throws outside WorkspaceSlugProvider. In the desktop shell, the sidebar
renders at the shell level (outside any tab's WorkspaceRouteLayout).
Fix: DesktopShell reads the current slug via useSyncExternalStore on
the workspace-storage singleton. When slug is available, wraps the
entire shell in WorkspaceSlugProvider. When null (first mount before
any tab's WorkspaceRouteLayout sets it), shows a loading spinner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(desktop): migrate old tab paths + fix shell slug deadlock
Tab store rehydration: old-format paths like "/issues/abc" (missing
workspace slug prefix) are reset to "/" so IndexRedirect picks the
correct workspace. Detection: if the first segment is a known route
name (issues, projects, etc.) rather than a workspace slug, it's an
old-format path.
Desktop shell: TabContent must always render (not gated behind slug
check) so WorkspaceRouteLayout can mount and call setCurrentWorkspace.
Only sidebar and shell-level UI (chat, modals, search) gate on slug
being present.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
ce447c7f06 |
feat(agent): add custom CLI arguments support (#986)
* feat(agent): add custom CLI arguments support Allow users to configure custom CLI arguments per agent that get appended to the agent subprocess command at launch time. This enables use cases like specifying different models (--model o3), max turns, or other provider-specific flags without needing separate runtimes. Changes: - Add custom_args JSONB column to agent table (migration 041) - Update API handler to accept/return custom_args in create/update - Pass custom_args through claim endpoint to daemon - Append custom_args to CLI commands for all agent backends - Add ExecOptions.CustomArgs field in agent package - Add Custom Args tab in agent detail UI - Add --custom-args flag to CLI agent create/update commands Closes MUL-802 * fix(agent): filter protocol-critical flags from custom_args Add per-backend filtering of custom_args to prevent users from accidentally overriding flags that the daemon hardcodes for its communication protocol (e.g. --output-format, --input-format, --permission-mode for Claude). This follows the same pattern as custom_env's isBlockedEnvKey: we only block the small, stable set of flags that would break the daemon↔agent protocol — not every possible dangerous flag. Workspace members are trusted for everything else. Each backend defines its own blocked set: - Claude: -p, --output-format, --input-format, --permission-mode - Gemini: -p, --yolo, -o - Codex: --listen - OpenCode: --format - OpenClaw: --local, --json, --session-id, --message - Hermes: none (ACP is positional) Includes unit tests for the filtering logic. * fix(agent): address code review nits for custom_args - Replace module-level `nextArgId` counter with `crypto.randomUUID()` in custom-args-tab.tsx to avoid SSR ID conflicts - Add unit tests for custom args passthrough and blocked-arg filtering in both Claude and Gemini arg builders |
||
|
|
f94b0100cd |
refactor(autopilot): remove broken concurrency policies and fix multiple bugs (#1048)
Remove the concurrency_policy system (skip/queue/replace) — skip had an orphan bug that permanently blocked triggers, queue didn't actually queue, and replace didn't cancel running tasks. Every trigger now simply executes. Bug fixes: - Listener now handles in_review status (was silently ignored) - Issue deletion fails linked autopilot runs before DELETE (prevents orphans) - ComputeNextRun rejects invalid timezones instead of silent UTC fallback - dispatchCreateIssue post-commit failures now properly fail the run Reliability: - Scheduler recovers lost triggers on startup (crash recovery) - New index on autopilot_run(issue_id) for deletion lookups - Migration 043 cleans up historical orphaned/skipped/pending runs |
||
|
|
d88fe2608e |
feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents Introduce the Autopilot feature — recurring automations that assign work to AI agents on a schedule or manual trigger. Supports two execution modes: create_issue (creates an issue for the agent to work on) and run_only (directly enqueues an agent task without issue pollution). Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService with concurrency policies (skip/queue/replace), HTTP CRUD + trigger endpoints, background cron scheduler (30s tick), event listeners for issue→run and task→run status sync. Frontend: types, API client methods, TanStack Query hooks with optimistic mutations, realtime cache invalidation, list page with create dialog, detail page with trigger management and run history, sidebar nav + routes for both web and desktop apps. * feat(autopilot): improve UX — trigger config, edit dialog, template gallery - Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone - Fix Select components showing UUIDs instead of names (Base UI render function pattern) - Add Edit button on detail page opening a unified edit dialog - Remove project/concurrency/issue-title-template from create/edit (simplify for users) - Add trigger configuration inline during autopilot creation - Add template gallery on empty state (6 step-by-step workflow templates) - Rename "Description" to "Prompt" throughout UI - Inject autopilot run timestamp into issue description for agent date awareness - Treat issue status "in_review" as run completion (fixes skip on next trigger) - Make migration idempotent with IF NOT EXISTS clauses |
||
|
|
ff1d348274 |
feat(security): invitation acceptance flow for workspace members (#1019)
* feat(security): replace instant member-add with invitation acceptance flow Users invited to a workspace must now explicitly accept the invitation before becoming a member. This fixes the security vulnerability where knowing someone's email was enough to auto-register their runtime to your workspace. Changes: - Add workspace_invitation table with pending/accepted/declined/expired states - Replace CreateMember with CreateInvitation (same endpoint, new behavior) - Add accept/decline/revoke/list invitation API endpoints - Add invitation WS events for real-time notification - Frontend: invitation accept/decline UI in workspace switcher - Frontend: pending invitations section in members settings tab * fix(invitation): address PR review nits - Fix invitation:revoked listener to send event to invitee user (was no-op) - Remove duplicate queryClient2 in app-sidebar.tsx, reuse existing queryClient - Add expires_at > now() filter to ListPendingInvitationsByWorkspace query |
||
|
|
a744cd4f45 |
feat(chat): redesign state, header, and unread tracking
State management - Pending task / live timeline are now Query-cache single source; Zustand mirror removed (fixes duplicate assistant render caused by the invalidate→refetch race window) - WS subscriptions moved from ChatWindow to global useRealtimeSync so pending state survives minimize and refresh - New GET /chat/sessions/:id/pending-task to recover live state on mount - Drafts persisted per-session (was per-workspace) Unread tracking - Migration 040: chat_session.unread_since (event-driven; old chats stay clean — no mass backfill) - POST /chat/sessions/:id/read clears unread; broadcasts chat:session_read so other devices sync - New GET /chat/pending-tasks aggregate for the FAB - ChatFab: brand-color impulse animation while running, brand-dot badge of unread session count - ChatWindow auto-marks read when user is viewing the session Header redesign - Two independent dropdowns: agent (avatar + name + My/Others grouping) at the input bottom-left; session (title + agent avatar) in the header - ⊕ new-chat button replaces the old + and history buttons - Session dropdown lists all sessions across agents with avatars - Empty state: 3 clickable starter prompts that send immediately - Mention link renderer falls through to default span on null — fixes @member/@agent/@all silently disappearing app-wide - User messages render through Markdown - Enter submits in chat input only (with IME guard + codeBlock skip); bubble menu hidden in chat Misc - Partial index on agent_task_queue for fast pending-task lookup - 2 new storage keys added to clearWorkspaceStorage - useMarkChatSessionRead has onError rollback - chat.* namespace logs across store, mutations, components, realtime Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
4165401d16 |
feat(agent): support custom environment variables for router/proxy mode
Add per-agent custom_env configuration that gets injected into the agent subprocess at launch time. This enables users to configure custom API endpoints (ANTHROPIC_BASE_URL), API keys (ANTHROPIC_API_KEY), and cloud provider modes (CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX) without requiring code changes. Changes: - Migration 040: add custom_env JSONB column to agent table - Backend: custom_env in agent CRUD API + claim endpoint - Daemon: merge custom_env into subprocess environment variables - Frontend: env var editor in agent settings (key-value pairs with visibility toggle for sensitive values) Closes #816 Related: #807, #809 |
||
|
|
f1d81cdfaa |
feat(search): add project search support to Cmd+K search (#663)
Projects are now searchable alongside issues in the Cmd+K search dialog. Results are grouped by type (Projects / Issues) with project icon, status, and description snippet highlighting. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9b62485a86 |
feat: add pin to sidebar for issues and projects (#653)
* feat: add pin to sidebar for issues and projects
Add per-user pinning of issues and projects to the sidebar for quick access.
- New `pinned_item` table with per-user, per-workspace scoping
- REST API: GET/POST /api/pins, DELETE /api/pins/{type}/{id}, PUT /api/pins/reorder
- Sidebar "Pinned" section between Personal and Workspace nav (hidden when empty)
- Pin/unpin actions in issue and project detail dropdown menus
- Optimistic mutations with WebSocket invalidation for real-time sync
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add drag-and-drop reordering and visible pin buttons
- Sidebar pinned items now support drag-and-drop reordering via @dnd-kit
- Add visible pin/unpin icon button in issue and project detail headers
- Add useReorderPins mutation with optimistic updates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove drag handle and fix page refresh after reorder
- Remove GripVertical drag handle — whole item is now draggable, aligning
with other sidebar elements
- Prevent link navigation after drag using wasDragged ref
- Remove onSettled invalidation from reorder mutation to prevent
unnecessary refetch after optimistic update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
bf8abba24d |
fix(db): relax pending task unique index to per-(issue, agent) (#637)
The idx_one_pending_task_per_issue index only allowed one pending task per issue across all agents, causing different agents' queued/dispatched tasks to block each other. This mismatched the code-level dedup which checks per (issue_id, agent_id). Replace with idx_one_pending_task_per_issue_agent on (issue_id, agent_id) so each agent can independently have one pending task. Fixes MUL-495 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
88982ad23f |
feat(issues): display token usage per issue in detail sidebar (#581)
* feat(issues): display token usage per issue in detail sidebar Add a new "Token usage" section to the issue detail right sidebar that shows aggregated input/output tokens, cache tokens, and run count across all tasks for the issue. Backed by a new SQL query and API endpoint. * fix(db): add index on agent_task_queue(issue_id) for usage queries The GetIssueUsageSummary query joins agent_task_queue filtered by issue_id across all statuses. The existing partial index (migration 022) only covers queued/dispatched rows, so completed tasks require a sequential scan. Add a general index to prevent performance degradation as task volume grows. |
||
|
|
7620a5a7e9 |
fix(search): LOWER/LIKE for pg_bigm 1.2 index compatibility (#621)
* fix(search): use LOWER/LIKE instead of ILIKE for pg_bigm 1.2 compatibility pg_bigm 1.2 on RDS does not support ILIKE index scans. Replace all ILIKE expressions with LOWER(column) LIKE LOWER(pattern) so the GIN indexes are utilized. Rebuild gin_bigm_ops indexes on LOWER() expressions. Closes MUL-482 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(search): lowercase pattern in Go, add buildSearchQuery unit tests - Lowercase phrase/terms in Go (strings.ToLower) so SQL only needs LOWER() on the column side, avoiding redundant per-query LOWER() on the pattern - Add 5 unit tests for buildSearchQuery asserting SQL shape: no ILIKE, LOWER on columns only, lowercased args, multi-term AND, number match, include-closed flag, special char escaping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fb475915c1 |
fix(chat): add workspace scoping, error logging, and query cleanup
- CancelTaskByUser: verify task belongs to current workspace for both chat and issue tasks, preventing cross-workspace cancellation - Log errors for TouchChatSession and CreateChatMessage instead of silently discarding them - Add ON DELETE CASCADE to chat_session.creator_id FK - Add staleTime: Infinity to chat query options (project convention) - Remove dead useSendChatMessage mutation (replaced by direct api call) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1f717c9059 |
feat(chat): add ownership checks, optimistic messages, and cleanup
- Add creator ownership verification on chat session endpoints (get, archive, send, list messages) - Add CancelTaskByUser handler with ownership check instead of unrestricted CancelTask - Show user messages optimistically before server response - Remove unused streamingContent from chat store and sendMessage mutation import - Make QueryProvider devtools flag a prop instead of reading process.env in core package - Add proper FK constraint on chat_session.creator_id → user(id) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
821b6ece57 |
merge: resolve conflicts with main (project feature)
Merge both chat and project types/events/routes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
245beed829 |
feat(projects): add priority attribute to projects (#565)
Add priority field (urgent/high/medium/low/none) to projects, matching the existing issue priority system. Includes database migration, API support for create/update/list filtering, and UI for the create dialog, project list table, and project detail page. |
||
|
|
68e2a14ba2 |
feat(projects): add Project entity with full-stack CRUD support (#552)
Implements the Project concept as a higher-level grouping for issues. Hierarchy: workspace → project → issue → sub-issue. Backend: - Migration 034: project table + issue.project_id FK - sqlc queries for project CRUD - Project handler with list/get/create/update/delete - Issue handler updated to support project_id in create/update - Routes at /api/projects, WebSocket event constants Frontend (new monorepo structure): - @multica/core: Project types, API client methods, queries/mutations, status config, realtime sync - @multica/views: Projects list page, detail page (overview + issues tabs), project picker for issue detail panel - apps/web: Route pages, sidebar navigation entry All TypeScript type checks and tests pass. |
||
|
|
f9a430e100 |
merge: resolve conflicts with main branch monorepo extraction
Update chat feature imports to use new package paths: - @/shared/types → @multica/core/types - @/shared/api → @/platform/api - @core/* → @multica/core/* - @/features/realtime → @multica/core/realtime - @/components/ui/* → @multica/ui/components/ui/* Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
50f9e673e8 |
feat(chat): add agent chat feature (full stack)
Implement the Master Agent chat feature allowing users to chat with agents directly from a floating window, separate from the issue-based workflow. Backend: - New chat_session and chat_message tables (migration 033) - Make issue_id nullable on agent_task_queue for chat tasks - REST API: create/list/get/archive sessions, send/list messages - EnqueueChatTask in TaskService with session_id persistence - WS events: chat:message, chat:done - Daemon: chat task type with separate prompt builder - ClaimTaskByRuntime populates chat context (session, message, repos) Frontend: - ChatSession/ChatMessage types + API client methods - core/chat: TanStack Query options, mutations with optimistic updates, WS updaters - features/chat: Zustand store, ChatFab (floating button), ChatWindow with real-time streaming via task:message events - Mounted in dashboard layout (bottom-right corner) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e3f34ace8e |
Merge pull request #541 from multica-ai/fix/pg-bigm-ci-migration
fix(search): make pg_bigm migration graceful for CI |
||
|
|
a2a021a0dd |
fix(search): make pg_bigm migration graceful when extension unavailable
CI uses pgvector/pgvector:pg17 which doesn't ship pg_bigm. Wrap CREATE EXTENSION and index creation in DO/EXCEPTION blocks so the migration succeeds without pg_bigm — indexes are skipped and search falls back to plain LIKE scans. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ff27a249cc |
feat(runtime): add owner tracking, filtering, and delete (#535)
Add owner_id to agent_runtime table to track who registered each runtime. Backend: new delete endpoint with role-based permissions (owner/admin can delete any, members only their own), list filtering by owner (?owner=me), and agent dependency check before deletion. Frontend: Mine/All filter toggle in runtime list, owner display in list items and detail view, delete button with AlertDialog confirmation. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
23136da34f |
feat(search): implement full-text search for issues (#507)
* feat(search): implement full-text search for issues Add pg_bigm-based full-text search across issue titles and descriptions, with API endpoint, CLI subcommand, and web Cmd+K search dialog. - Migration 032: pg_bigm extension + GIN indexes on title/description - Server: GET /api/issues/search?q=... with pagination and total count - CLI: `multica issue search <query>` with table/json output - Web: Cmd+K command palette using cmdk, with debounced search Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(search): address review feedback on search implementation 1. Escape LIKE special characters (%, _, \) in handler to prevent matching anomalies from user input. 2. Wire AbortController signal into searchIssues fetch so in-flight requests are actually cancelled on new input. 3. Fix offset=0 falsy check — use !== undefined instead of truthiness. 4. Merge results + count into single query using COUNT(*) OVER() window function, eliminating the duplicate DB round-trip. 5. Exclude done/cancelled issues by default; add include_closed parameter to API, CLI (--include-closed), and web client. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(search): default web search to include all statuses Pass include_closed: true in the web Cmd+K search so results include done and cancelled issues by default, matching the reviewer's request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(search): add comment search with snippet extraction Extend search to cover issue comments in addition to title/description. Results are deduplicated at the issue level, with match_source and matched_snippet fields indicating where and what matched. - Migration 033: pg_bigm GIN index on comment.content - SQL: EXISTS subquery for comment matching, correlated subquery for snippet extraction, 3-tier ranking (title > description > comment) - Server: SearchIssueResponse with match_source and matched_snippet - Web: show comment icon + snippet below issue title when matched - CLI: MATCH column shows source and truncated snippet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(search): redesign search dialog to match Linear's spacious style - Widen dialog from sm (384px) to xl (576px) with top-20% positioning - Larger search input with icon, generous padding, and ESC hint - Use cmdk primitives directly for full style control - Taller result list (400px / 50vh), spacious result items (py-2.5) - Rounded-lg items with accent highlight on selection - Cleaner border separator between input and results --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
8403c97688 |
Merge pull request #482 from multica-ai/agent/j/674c6839
feat(usage): add per-task token usage tracking |
||
|
|
8a8d3ea20e |
feat(usage): add per-task token usage tracking
Extract token usage from Claude Code's stream-json output in real-time during task execution, replacing the inaccurate global JSONL log scanner. - New `task_usage` table: tracks (task_id, provider, model) level usage - Agent SDK: parse `message.usage` from assistant messages, accumulate per-model and return in Result - Daemon: convert agent usage to entries, send with CompleteTask - Server: store usage on task completion, expose workspace-level aggregation APIs (GET /api/usage/daily, GET /api/usage/summary) |
||
|
|
7c79611309 |
refactor: remove agent triggers config field (#469)
* refactor: remove agent triggers config field Remove the triggers field from agent configuration. The on_assign, on_comment, and on_mention behaviors are now always enabled (hardcoded), as decided in the Agentflow design discussion (MUL-372). Changes: - Database: migration 032 drops triggers column from agent table - Backend: remove triggers from create/update agent APIs and response - Backend: simplify trigger-checking logic to always-enabled - Frontend: remove TriggersTab UI and AgentTrigger types - Tests: remove trigger config unit tests (no longer configurable) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: also remove agent tools config field Remove the tools field from agent configuration alongside triggers. The tools field was a placeholder — stored in the DB and shown in the UI but never passed to the daemon or used at runtime. - Database: migration 032 now also drops tools column - Backend: remove tools from create/update agent APIs and response - Frontend: remove ToolsTab UI, AgentTool type, and tools tab - Update landing page copy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove tools/triggers columns from test fixtures The test fixtures still referenced the dropped tools and triggers columns when inserting agent rows, causing CI failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
09764c5f51 |
feat(agent): replace hard delete with archive/restore (#346)
* feat(agent): replace hard delete with archive/restore
Replace agent deletion with soft archive pattern. Archived agents
are preserved in the database with all historical references intact
but cannot be assigned, mentioned, or trigger tasks.
Backend:
- Add archived_at/archived_by columns to agent table (migration 031)
- Replace DELETE /api/agents/{id} with POST /api/agents/{id}/archive
- Add POST /api/agents/{id}/restore endpoint
- ListAgents excludes archived by default (?include_archived=true to include)
- Skip archived agents in task triggers (on_assign, on_comment, on_mention)
- Block assignment to archived agents
- Cancel pending tasks on archive
- New events: agent:archived, agent:restored (replacing agent:deleted)
Frontend:
- Agent type includes archived_at/archived_by fields
- Mention autocomplete and assignee picker filter out archived agents
- Agent list shows archived agents with muted styling
- Agent detail shows archive banner with restore button
- Delete button replaced with Archive button and updated confirmation dialog
- API client: archiveAgent/restoreAgent replace deleteAgent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): self-review fixes for archive feature
- Fix: workspace store now fetches agents with include_archived=true
so archived agents are actually visible in the frontend (the archived
UI was dead code before — ListAgents excludes archived by default)
- Fix: add error logging for CancelAgentTasksByAgent in ArchiveAgent
- Fix: add idempotency guards — return 409 Conflict when archiving
an already-archived agent or restoring a non-archived agent
- Fix: revert unnecessary extra GetAgent query in ReconcileAgentStatus
(archived agents won't have running tasks after CancelAgentTasksByAgent)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
b8c784dda3 |
merge: resolve conflicts with main
- Take main's router.go, rich-text-editor.tsx, comment-card.tsx - Remove deleted daemon_pairing.go - Keep issue mention card feature |
||
|
|
94ddbfb4d9 |
fix(tests): merge main, renumber migration, fix execenv test assertions
Merge main to pick up 028_task_trigger_comment migration. Renumber daemon_token migration to 029. Fix execenv tests that expected CLI hints in issue_context.md after they were moved to CLAUDE.md. |
||
|
|
afdfee78b9 |
feat(daemon): add authentication for daemon API routes
Issue daemon auth tokens (mdt_) on pairing session claim, bound to workspace_id + daemon_id with 1-year expiry. Add DaemonAuth middleware that validates these tokens and falls back to JWT/PAT for backward compatibility. Apply middleware to all daemon routes except pairing endpoints. |
||
|
|
b5674869ed |
fix(auth): enforce auth on daemon API routes (#224)
* fix(auth): enforce auth middleware and workspace membership on daemon API routes Daemon routes were registered without the Auth middleware, meaning the server accepted unauthenticated requests to register runtimes, claim tasks, etc. The daemon client already sends a Bearer token — the server just wasn't validating it. - Split /api/daemon routes: pairing-session endpoints stay public (used before the daemon has a token), all others now require Auth middleware - Add workspace membership check in DaemonRegister so only workspace members can register runtimes - Update test to include X-User-ID header matching the new auth requirement Closes MUL-90 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(daemon): remove dead pairing-session feature The daemon pairing flow was never completed — the daemon authenticates via CLI config token, not pairing sessions. Remove all related code: - Delete daemon_pairing.go handler (4 unused handlers) - Remove pairing routes from router.go (3 public + 1 protected) - Delete /pair/local page + test from frontend - Remove DaemonPairingSession types and API client methods - Add migration 029 to drop daemon_pairing_session table - Update LOCAL_DEVELOPMENT.md to reflect actual auth flow Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1e22633301 |
Merge remote-tracking branch 'origin/main' into feature/file-upload-cloudfront
# Conflicts: # apps/web/components/common/rich-text-editor.tsx # apps/web/features/issues/components/comment-card.tsx # apps/web/package.json # pnpm-lock.yaml |
||
|
|
5a7b34cab5 |
Merge pull request #211 from multica-ai/agent/lambda/5e70a174
fix(agent): fix agent visibility defaults and permission model |
||
|
|
feb5f57662 |
fix: merge main and renumber migration 028 → 030
Main now has 028 (task_trigger_comment) and 029 (daemon_token). Renumber agent_default_private migration to 030 to avoid conflict. |
||
|
|
ac17fd33bf | Merge remote-tracking branch 'origin/main' into agent/lambda/5e70a174 | ||
|
|
35f77de9cc |
Revert "Merge pull request #214 from multica-ai/agent/lambda/4771d426"
This reverts commit |