mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 01:59:24 +02:00
agent/lambda/768b92e0
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4fac8d772f |
feat(attribution): Human Attribution Phase 1 (MUL-4302) (#5150)
* feat(attribution): Phase 1 foundation — provenance schema + resolver (MUL-4302)
Human Attribution, Phase 1 (地基) first increment. Every agent run must be
traceable to exactly one accountable human AND record at which waterfall level
that human was resolved, so a NULL originator can be told apart from a genuine
'no human in the chain'.
- migration 149: add originator_source (waterfall label) + delegation/retry/
rerun/rule-version lineage + kind-tagged trigger evidence to agent_task_queue.
No FK, no cascade, no CHECK on the source enum (MUL-4302 §7); nullable ADD
COLUMNs = fast metadata-only change on the hot queue table.
- internal/attribution: the accountable-human vocabulary (Source, EvidenceKind,
TriggerKind) + pure, unit-tested classification rules (ClassifyComment/
ClassifyDirect). No DB, no authorization — provenance labeling only.
- service: attributionFor{IssueTask,TriggerComment} gather facts and delegate to
the pure classifier; the legacy originator resolvers now delegate here so
there is one source of truth. originator_user_id's VALUE is unchanged, so the
Composio-overlay and canInvokeAgent A2A authorization boundaries are
byte-for-byte preserved (MUL-4302 §1.3).
- enqueueIssueTask / enqueueMentionTask stamp originator_source + evidence;
CreateRetryTask carries the parent attribution forward and records
retry_of_task_id so retry and manual rerun stay separable (MUL-4302 §5).
Verified: go build ./..., go vet, gofmt clean; new attribution unit tests +
enqueue stamping integration test green; existing resolve_originator tests
unchanged.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): split accountable_user_id from originator, close enqueue bypasses (MUL-4302)
Phase 1, per Bohan's decision on the MUL-4302 thread: audit and authorization
answer different questions and get different columns.
- Migration 151 adds agent_task_queue.accountable_user_id (no FK, no cascade).
Authorization keeps reading ONLY originator_user_id (canInvokeAgent A2A gate,
Composio overlay); audit/UI/usage read accountable_user_id + source + evidence.
- Invariant (finalizeAttribution, single chokepoint + §11 tests): originator
non-null ⟹ accountable equals it. The two diverge only when originator is null
(autopilot / degraded fallback), which is the deferred rule_owner/owner_fallback
increment; this lands the column + mirror-write so that split has a home.
- Close the NULL-source enqueue bypasses Elon flagged: chat, quick-create,
deferred-fallback and run_only-autopilot now stamp originator_source + evidence
(+ accountable where a human exists). Autopilot stays unattributed until the
rule-version snapshot table lands, but is no longer a silent NULL-source row.
Retry inherits accountable_user_id like the rest of the attribution lineage.
- Fix assign/promote attribution (§4): a member who assigns/promotes an existing
issue is now the accountable human (and, by the invariant, originator) ahead of
the issue creator. Threaded as an OPTIONAL actor override, so comment/rerun/
autopilot paths keep today's resolution and create-with-assignee (creator ==
actor) is unchanged. The squad leader gate already judged the same member.
Also merges origin/main: renumbers the attribution migration 149→150 (main took
149 for issue_origin_agent_create) and folds agent_create into ClassifyDirect's
origin inheritance.
go build/vet/gofmt clean; attribution unit tests + service stamp/actor tests +
handler suite pass on a fresh DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* docs(attribution): fix accountable NULL semantics + close chat/quick-create evidence boundary (MUL-4302)
Addresses Elon's 2nd-round review on PR #5150 (pre-merge doc/evidence items):
- Migration 151 no longer overclaims NULL. accountable_user_id is NULL not only
on pre-migration rows but on NEW rows whose audit source resolved no human yet
(run_only autopilot writes originator_source='unattributed' with NULL
accountable until rule_owner lands). Header + COMMENT ON COLUMN reworded so a
schema reader does not misjudge the invariant.
- Chat now uses the UNIFORM evidence pair (kind=chat, ref=chat_session_id), like
autopilot_run/issue_assignment, instead of relying only on the dedicated
chat_session_id column — new EvidenceChat kind. Added a service test asserting
chat stamps direct_human + chat evidence.
- Quick-create is documented as the ONE intentional no-antecedent-row path: no
comment/issue/session/run exists at enqueue time (the run creates the issue), so
trigger_evidence_kind/ref stay NULL while the human rides originator/accountable
and source is direct_human — not a NULL-source bypass.
No authorization behavior change. attribution + service + handler suites pass on a
DB migrated through 151.
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): renumber migrations 150/151 → 157/158 after merging main (MUL-4302)
main's #5162 ("unblock release migrations") renumbered the chat migrations and
took 150 (agent_task_coalesced_comments) and 151 (chat_read_cursor), colliding
with this branch's attribution migrations. Renumber them above main's new highest
(156) so TestMigrationNumericPrefixesStayUniqueAfterLegacySet passes:
- 150_agent_task_attribution → 157_agent_task_attribution
- 151_agent_task_accountable_user → 158_agent_task_accountable_user
Fixed the internal "migration 150" references in 158's header to 157. Migrations
apply cleanly through 158 on a fresh DB; migration lint green.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot rule_owner — accountable = rule version publisher (MUL-4302)
Implements rule_owner (MUL-4302 §3.4), the first attribution source where the
accountable human diverges from the (NULL) authorization originator.
- Migration 159 adds the append-only autopilot_rule_version snapshot table (no FK,
no cascade); migration 160 adds its CONCURRENTLY lookup index.
- Write-on-publish: CreateAutopilot appends v1 (publisher = creator); UpdateAutopilot
appends a new version when a SUBSTANTIVE autopilot-row field changes (assignee /
status / execution_mode) — cosmetic edits (title/description/template) write none.
Both run inside the existing handler tx (atomic with the autopilot write).
- Dispatch resolution: both autopilot execution modes now resolve the active rule
version and stamp originator_source='rule_owner', accountable_user_id=publisher,
rule_version_id=<snapshot>, with originator_user_id left NULL (authorization
unchanged). run_only stamps CreateAutopilotTask directly; create_issue resolves in
attributionForIssueTask so both modes attribute identically. A missing version /
non-member publisher degrades to unattributed — never fabricates a human.
- finalizeAttribution now enforces the invariant ONE-WAY: it mirrors originator onto
accountable only when originator is valid, leaving an explicitly-set accountable
(rule_owner / future owner_fallback) intact when originator is NULL. Added
rule_version_id to CreateAgentTask so the create_issue path persists it too.
Also merges origin/main and renumbers this branch's attribution migrations
150/151 → 157/158 (main's #5162 took 150/151); rule_version table is 159/160.
Tests: attribution unit RuleOwner + one-way invariant table; service integration
tests proving an autopilot-origin issue stamps rule_owner + rule_version_id (and
degrades to unattributed with no version). Full service/attribution/handler/
migration suites pass on a DB migrated through 160; build/vet/gofmt clean.
Deferred (same PR): trigger-table republish (cron/webhook/event_filters) and
system-pause/archive versioning; owner_fallback + fail-closed; manual rerun.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): manual autopilot trigger → direct_human to the triggering member (MUL-4302)
Elon's blocking finding: a member manually triggering an autopilot was attributed
rule_owner (accountable = rule publisher, originator NULL) like a schedule/webhook
run, so member B triggering member A's autopilot landed accountable=A and carried
no originator authorization context for the run. Per MUL-4302 §4 a manual "run now"
is a direct human action and must attribute direct_human to the triggering member.
- Thread the triggering member from TriggerAutopilot into dispatch: new
DispatchAutopilotManual carries actorUserID (resolved via resolveActor +
memberActorUserID, so only a member actor is a human; an A2A agent actor falls
back to rule_owner). DispatchAutopilot / DispatchAutopilotForPlan keep their
public signatures (pass an invalid actor); only the internal dispatchAutopilot /
dispatchCreateIssue / dispatchRunOnly gained the param, so the many existing
callers are untouched.
- run_only: dispatchRunOnly stamps direct_human (originator == accountable ==
actor, no rule_version) for a manual actor, else rule_owner. CreateAutopilotTask
gains an originator_user_id param for the manual case.
- create_issue: dispatchCreateIssue enqueues a manual trigger via the actor-carrying
*WithHandoff entry points; attributionForIssueTask's autopilot-origin rule_owner
branch is now guarded on !actorUserID.Valid, so a valid actor falls through to the
direct_human override. Both execution modes attribute identically.
- schedule / webhook keep rule_owner (no actor). Trigger-table + system-pause/archive
versioning remain the pre-merge follow-ups.
Tests: the run_only row assertion Elon asked for (schedule → rule_owner row on
CreateAutopilotTask), plus manual direct_human on BOTH modes (run_only and
create_issue), including a manual actor distinct from the rule publisher. Full
service/attribution/handler/migration/scheduler/cmd suites pass on a DB migrated
through 160; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): owner_fallback + fail-closed policy, and manual-rerun direct_human (MUL-4302)
Two of the three remaining Phase 1 items (trigger-table / system-pause versioning
is deferred — see PR description).
owner_fallback + fail-closed (§1/§3.5) — the never-null accountable guarantee:
- attribution.OwnerFallback degrades an UNATTRIBUTED result to owner_fallback:
accountable = agent owner, originator stays NULL (audit-only, authz untouched),
Source.Precise()==false. finalizeAttribution's one-way invariant already allows
accountable-set / originator-NULL divergence, so nothing else changes.
- Migration 161 adds workspace.attribution_fail_closed (default FALSE) + a lean
GetWorkspaceAttributionFailClosed read. (Also added the column to ListWorkspaces'
explicit column list so its row type stays db.Workspace.)
- applyAttributionFallback is applied at every enqueue boundary (issue, mention,
chat, quick-create, deferred-fallback, autopilot run_only): unattributed →
owner_fallback (agent owner) by default, or ErrAttributionFailClosed when the
workspace is fail-closed, which the caller surfaces to refuse the enqueue (the
run does not start). So no run is left without an accountable human, and a
compliance workspace can block unattributable runs instead.
manual rerun (§5) — a rerun is a NEW direct_human trigger to the rerunning member:
- RerunIssue threads the acting member (resolved in the handler via resolveActor)
down to enqueueRerunTask, and attributionForIssueTask is now actor-first so the
actor wins over an INHERITED trigger comment (a rerun keeps the comment for the
daemon's prompt context but must attribute to whoever clicked rerun, not the
original comment's human).
- rerun_of_task_id lineage is recorded via a targeted SetAgentTaskRerunOf update on
the rerun path only (keeping the shared CreateAgentTask insert untouched), so
system retry (retry_of_task_id) and human rerun stay separable in reporting.
Tests: OwnerFallback unit test; owner_fallback + fail-closed-refusal + manual-rerun
(direct_human + rerun_of_task_id) service tests; the prior "degrades to unattributed"
test updated to owner_fallback. Full service/attribution/handler/migration/scheduler/
cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): close fail-open holes + move rerun_of_task_id into creation snapshot (MUL-4302)
Addresses Elon's two must-fixes on PR #5150.
1. accountable-never-null / fail-closed had fail-open holes. applyAttributionFallback
now, for an UNATTRIBUTED run, refuses the enqueue (ErrAttributionFailClosed) in
THREE cases instead of silently degrading to a runnable NULL-accountable task:
- workspace policy read fails (or no workspace) → fail closed; we cannot confirm
fallback is permitted, so we don't run an unattributable task on a DB hiccup.
(Only the rare unattributed path pays this; precise runs never read the policy.)
- workspace is fail-closed → refuse (unchanged).
- owner_fallback has no valid agent owner → refuse rather than enqueue a task with
a NULL accountable_user_id.
ErrAttributionFailClosed's doc now covers all three "cannot guarantee an
accountable human" refusals. Added missing-owner / policy-read-failure /
precise-passthrough tests.
2. manual rerun rerun_of_task_id was a post-notify UPDATE (race: the queued event /
daemon claim could see rerun_of_task_id = NULL, and a failed update degraded the
run to a plain direct_human). It now rides the CreateAgentTask insert — threaded
through enqueueIssueTask / enqueueMentionTask as a creation param (like
retry_of_task_id) so it is written in the same statement before the daemon is
notified. Removed the SetAgentTaskRerunOf follow-up query.
Also merges origin/main (unrelated CLI fix #5167, no conflict). Full service /
attribution / handler / migration / scheduler / cmd suites pass on a DB migrated
through 161; build / vet / gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): rule_owner versioning on trigger edits + system-pause/archive (MUL-4302)
The final remaining Phase 1 item: substantive publishes beyond the autopilot row now
republish the rule version, so a run's rule_owner accountable follows whoever last
changed what the rule does.
- Extracted the config-summary + insert into service.RecordAutopilotRuleVersion so
the handler and the (different-package) failure monitor share one writer; the
handler's recordAutopilotRuleVersion is now a thin wrapper.
- Trigger edits: UpdateAutopilotTrigger and DeleteAutopilotTrigger republish the rule
version with the acting member as publisher, ATOMICALLY (tx-wrapped mutation +
version write, mirroring CreateAutopilot/UpdateAutopilot). CreateAutopilotTrigger
republishes best-effort — the webhook path mints its token with a retry loop that
cannot share one tx, and a create is usually initial setup already covered by v1;
a failed write there is benign (active version stays the current publisher, the new
trigger fires under it, no immediate daemon claim rides it).
- Archive (DeleteAutopilot) republishes (member, status=archived), tx-wrapped.
- System auto-pause (failure monitor) republishes with a 'system' publisher,
best-effort — a background sweep to a non-dispatching state (a paused autopilot
never dispatches; a later member resume supersedes).
- RotateWebhookToken / SetSigningSecret deliberately do NOT version: they rotate
credentials, not the rule's behavior (not §3.4 substantive).
Semantics: a system-published (no-member) active version degrades dispatch to
unattributed → owner_fallback, never fabricating a human.
Tests: republish-reattributes (member A → member B supersedes → dispatch resolves to
B; system publisher → unattributed). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Also merges origin/main (unrelated frontend feature #5074).
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): make trigger-create rule-version republish atomic (MUL-4302)
Addresses Elon's final Phase 1 blocking finding: CreateAutopilotTrigger recorded the
rule-version republish best-effort AFTER the trigger insert. If member B added a
schedule/webhook trigger to member A's autopilot and the version write failed, future
schedule/webhook dispatches would keep attributing to A — violating the rule_owner
invariant that the last member to substantively change the rule owns future runs
("no immediate daemon claim" doesn't save it, since the miss surfaces at the LATER
trigger firing).
Both create paths now write the version in the SAME tx as the trigger INSERT:
- schedule create: wrap CreateAutopilotTrigger + recordAutopilotRuleVersion in one tx.
- webhook create: each mint-with-retry attempt runs in its own tx (insert + version
commit together; a token collision rolls that attempt back and retries with a fresh
token; a version-write failure rolls the trigger back). Passes ap + the acting
member id into the helper.
- removed the best-effort recordTriggerRuleVersionBestEffort helper (and the now-unused
slog import).
Test: TestCreateTrigger_RepublishesRuleVersionAtomically drives both create paths
through the handler and asserts a rule version is published by the acting member.
Existing webhook/trigger/archive handler tests still pass. Also merges origin/main
(unrelated avatar feature #5074). Full service/attribution/handler/migration/
scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt clean.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.1 — surface run attribution on the task API (MUL-4302 §9)
First Phase 2 (visibility) increment: the agent-task API now returns the resolved
accountable-human provenance so the UI can render an "on behalf of" badge.
- AgentTaskResponse gains an `attribution` object: source label (never blank —
pre-migration NULL renders "unattributed") + `precise` flag (false for the degraded
owner_fallback / backfill / unattributed sources), the initiator (accountable) and
originator (authorization) user refs, the evidence {kind, ref_id} pointer, and the
rule_version / delegated / retry / rerun lineage ids.
- The label + evidence + raw ids are built in the PURE taskToResponse (no DB), so
every task response carries them. Names are hydrated separately, only on the
user-facing surfaces (ListAgentTasks, ListWorkspaceAgentTaskSnapshot, RerunIssue,
CancelTaskByUser) — daemon-claim paths stay lean.
- Hydration resolves initiator/originator from the GLOBAL user table (departed-member
safe) via a new batch GetUsersByIDs query (no N+1); best-effort, so a lookup hiccup
leaves the raw ids intact.
Tests: pure taskAttributionBase (direct_human / rule_owner NULL-originator /
owner_fallback degraded / pre-migration→unattributed) + DB hydration (fills known
ref, leaves unknown id un-filled, skips nil). Full handler/service/attribution/
migration/scheduler/cmd suites pass on a DB migrated through 161; build/vet/gofmt
clean. The field is additive — the frontend's parseWithFallback ignores unknown keys,
so nothing breaks until the UI increment consumes it.
Also merges origin/main (unrelated editor feature #5090).
Remaining Phase 2 (next increments, same PR): frontend zod schema + "on behalf of"
badge + evidence-chain jump; append-only correction events (write + display).
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): Phase 2.2 — on-behalf-of badge in the execution log (MUL-4302 §9)
Surface the accountable human on every agent run row:
- AttributionBadge composes Badge + ActorAvatar, shows "on behalf of <member>"
with the resolution source as a tooltip; degraded (non-precise) attribution
gets a warning tone, and an unresolved initiator renders an explicit
"no responsible member" chip.
- Wire the badge into both active and past rows of the execution log.
- Mirror the attribution shape into AgentTaskResponseSchema (defensive, .loose())
so the cancel-task path carries it through zod; add parse tests.
- Export TaskAttribution/AttributionUser/TaskEvidence from @multica/core/types
and add the attribution block to all four issues.json locales.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): hydrate initiator names on issue-facing task endpoints + bound the badge (MUL-4302 §9)
Address Elon's PR #5150 review:
- ListTasksByIssue (the execution-log data source), GetActiveTaskForIssue and the
issue-scoped CancelTask now call hydrateTaskAttributions, so the "on behalf of
<member>" badge shows the real member name on issue detail instead of falling
back to "someone". Mirrors the existing ListAgentTasks / snapshot behavior.
- AttributionBadge: cap width (max-w-40, min-w-0) and truncate the name span so a
long name / narrow right column can't squeeze out trigger/status/actions; keep
the avatar shrink-0.
- Add a handler test asserting the issue task list returns a hydrated
attribution.initiator.name.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): use semantic AvatarSize 'xs' for the badge avatar
main refactored ActorAvatar.size from a raw pixel number to the semantic
AvatarSize union (packages/ui/lib/avatar-size). Switch the on-behalf-of badge
avatar from size={14} to size="xs" (16px) after merging main.
Co-authored-by: multica-agent <github@multica.ai>
* fix(attribution): stage-cascade falls back to parent-issue provenance, not agent owner (MUL-4302)
When closing the last sub-issue in a Stage wakes the parent's assignee agent, the
run was enqueued via a system-authored child-done comment with no actor, which the
resolver classified as unattributed and then degraded to owner_fallback (the agent's
own owner). That is the wrong accountable human: the woken run should be accountable
to whoever caused the parent issue to exist.
attributionForIssueTask now detects a system-authored trigger comment and falls
through to the parent issue's own provenance — the same creator / agent_create-origin
/ autopilot-origin chain a direct enqueue resolves (so an agent-decomposed parent
attributes via delegation to the human who drove it; a member-created parent to that
member; an autopilot parent to the rule publisher). owner_fallback is now only the
last resort when the parent provenance itself has no human.
- Extract attributionFromComment so attributionForIssueTask can inspect author_type
without a second GetComment; authorization resolution stays byte-identical.
- Add a DB-backed test asserting a system child-done comment resolves to the parent
issue's origin human (delegation), not owner_fallback.
Co-authored-by: multica-agent <github@multica.ai>
* feat(attribution): autopilot runs attribute to the firing trigger's creator (MUL-4302)
Per Bohan: an autopilot schedule/webhook run should be accountable to the human
who created the SPECIFIC trigger that fired it, not the rule publisher. (Manual
triggers already attribute to the invoking member via direct_human — unchanged.)
- Migration 162: add autopilot_trigger.created_by_type/created_by_id (nullable, no
FK/cascade). Capture the creating member at both trigger-create sites (schedule +
webhook).
- New precise source trigger_owner: originator stays NULL (an autonomous fire
carries no human authorization — same authz-safe divergence as rule_owner),
accountable = the trigger's member creator.
- triggerOwnerAttribution resolves run.trigger_id → creator; wired into run_only
dispatch and the create_issue path (bridging issue → active run → trigger_id).
Legacy triggers with no recorded creator, and agent-created triggers, degrade to
rule_owner then owner_fallback — nothing regresses.
- Frontend: trigger_owner source label in all four locales + badge switch case.
- Tests: attribution TriggerOwner unit + Precise/invariant; DB-backed resolver
tests (member creator → trigger_owner; creatorless → rule_owner fallback).
Co-authored-by: multica-agent <github@multica.ai>
* chore(attribution): re-trigger CI (dropped synchronize event on
|
||
|
|
591e47842d |
refactor(onboarding): remove starter-content kit; unify install-runtime issue across mark-onboarded paths (MUL-2438) (#2884)
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438) Drops the post-onboarding ImportStarterContent / DismissStarterContent flow (handler + routes + StarterContentPrompt + templates + locale strings + analytics event). The bug — web onboarding seeding 6+ starter issues without a runtime — only existed through that path; with it gone the source disappears. The "install a runtime" issue from BootstrapOnboardingNoRuntime is now the canonical no-runtime onboarding seed. The title/description and a LockAndFindActiveDuplicate-deduped seeder move to handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace / AcceptInvitation seed it whenever the workspace has no runtime yet, so every mark-onboarded entry point lands the user on a concrete next step. starter_content_state column is kept and continues to be claimed as 'imported' in all five entry points so older desktop builds (which still render the legacy dialog on NULL) don't surface it to accounts created after this change. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438) 054 only covered pre-feature users. Anyone onboarded between then and the starter-content kit removal could still sit at NULL, and old desktop clients gate the legacy StarterContentPrompt on `starter_content_state IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL would surface a dialog whose buttons 404. Mark them 'imported' to match the new helper's claim semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
20c2f45b4a |
fix(views): surface backend error messages on mutation failures (MUL-2317) (#2772)
* fix(views): surface backend error messages on mutation failures (MUL-2317)
Mutation toasts across the views package were swallowing the backend
`error` string and showing only a generic i18n fallback. This made it
impossible for users to see why an operation failed (most visibly:
creating an issue with a duplicate title produced a vague "Failed to
create issue" toast).
The fix has three pieces:
1. Create-issue duplicate branch (A段)
- New schema `DuplicateIssueErrorBodySchema` in core/api/schemas.ts.
- `create-issue.tsx` parses `ApiError.body` via `parseWithFallback`
and renders a dedicated amber-toned toast with a "view existing"
link when the server returns `{ code: "active_duplicate_issue",
issue: {...} }`. Schema drift downgrades to the normal error toast.
- Schema intentionally omits `issue.status` so the toast does not
depend on `StatusIcon`, which has no fallback for unknown enums.
2. User-facing mutation failure toasts (B段)
- 47 sites converted to `err instanceof Error && err.message ?
err.message : <existing fallback>` — preserves all existing
code-specific branches (slug conflict, agent_unavailable,
daemon_version_unsupported) and i18n keys.
- Covers Type 1 (onError) and Type 2 (catch block) patterns across
issues, projects, autopilots, inbox, runtimes, squads, comments,
batch actions, workspace create, and agent config tabs.
3. Autopilot partial-success (Type 3)
- New i18n keys `toast_create_partial_with_reason` /
`toast_update_partial_with_reason` (double-brace `{{reason}}`).
- `autopilot-dialog.tsx` captures `err.message` in the schedule
`catch` and routes to the `_with_reason` variant when present,
preserving the partial-success semantic (autopilot saved, schedule
failed) while exposing the actual reason.
Explicitly out of scope:
- `packages/core/` mutation hooks (no global onError, no UI dependency)
- No `toastApiError` helper (matches existing 14+ correct sites)
- Sub-issue link aggregate `Promise.allSettled` keeps count-based toast
(N independent requests cannot collapse to one err.message); only
added a dev-side `console.error` per rejection.
- Clipboard catches and `useUpdateChatSession` (not API mutation toasts)
Tests:
- `packages/core/api/schemas.test.ts` — schema contract (valid body,
forward-compat fields, rename rejection, missing issue, wrong types).
- `packages/views/modals/create-issue.test.tsx` — duplicate toast +
view link, schema-drift fallback, err.message surfacing, non-Error
fallback (4 new cases).
- `packages/views/autopilots/components/autopilot-dialog-i18n.test.ts`
— real i18next, asserts rendered text contains the reason verbatim
(guards against `{reason}` vs `{{reason}}` regression).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): unify rotate-token catch + cover dialog partial-success render
Address reviewer feedback on PR #2772:
1. webhook-token rotate (`autopilot-detail-page.tsx`) now follows the
`err.message ?? fallback` ternary used by the sibling trigger
delete/add paths, instead of swallowing the error.
2. Extract `formatSchedulePartialFailureToast` so the dialog's
partial-success branches and the i18n test exercise the same
helper. The test now drives the actual format function, so a
variable-name typo at the call site (e.g. `{ msg }` instead of
`{ reason }`) fails the substring assertion.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(modals): drop user.type for title in success path to dodge CI 5s timeout
The success-path test typed the 42-character title via userEvent which
triggers a controlled re-render per keystroke. On the slower CI runner
the whole test crept up to ~5s and intermittently tripped the default
vitest timeout. Setting the value in one shot via fireEvent.change cuts
the cost while leaving the submit + toast interactions on userEvent.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
454c8e3d1a |
feat: in-app preview for non-image attachments (#2528)
* feat(storage): add GetReader to Storage interface Adds a streaming read method to the Storage abstraction so callers can pull object bytes without forcing a full in-memory load. S3Storage wraps GetObject; LocalStorage opens the file with path-traversal and sidecar guards. Tests cover happy path, traversal rejection, sidecar rejection, and missing key. Used in the next commit by the attachment-preview proxy endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(server): add attachment preview proxy endpoint GET /api/attachments/{id}/content streams the raw bytes of a text-previewable attachment back to the client. Exists to (a) bypass CloudFront CORS, which is not configured on the CDN, and (b) bypass Content-Disposition: attachment which Chromium honors for iframe document loads. Media types (image/video/audio/pdf) intentionally do NOT go through this endpoint — clients render them directly from the signed CloudFront download_url, which is already served with Content-Disposition: inline. Hard cap: 2 MB. Larger files return 413. Anything outside the text whitelist returns 415. The whitelist (isTextPreviewable) mirrors the client-side dispatcher; the cross-reference comment in file.go flags the manual sync until a JSON SSOT generator lands. Response always uses Content-Type: text/plain; charset=utf-8 so a hostile HTML payload can't be re-interpreted as a document. The original MIME ships via X-Original-Content-Type for client dispatch. Cache-Control: no-store so revoked attachment access takes effect immediately on the next request. Tests cover happy path (md), extension fallback when content_type is generic, 415 (pdf), 413 (>2MB), foreign workspace (404 isolation), and the isTextPreviewable table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(core/api): add getAttachmentTextContent + preview error types Adds an ApiClient method that fetches the text body of an attachment via the new /api/attachments/{id}/content proxy. Two typed errors — PreviewTooLargeError (413) and PreviewUnsupportedError (415) — let the preview modal render specific fallbacks instead of a generic failure. Refactors the private fetch() into a shared fetchRaw() helper so the new method inherits the standard infra: auth headers, 401 → handleUnauthorized recovery, X-Request-ID, error logging, and the ApiError contract. The previous draft bypassed all of these by calling window.fetch directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(views/editor): add AttachmentPreviewModal + Eye entry points In-app preview for non-image attachments. An Eye icon now sits next to the existing Download button on file cards / readonly file cards / the standalone AttachmentList. Clicking it opens a full-screen modal that dispatches by content_type: pdf: <iframe src={download_url}> — Chromium PDFium video/*: <video controls src={download_url}> — native controls audio/*: <audio controls src={download_url}> — native controls md: <ReadonlyContent> — full markdown pipeline html: <iframe srcdoc sandbox=""> — fully restricted text: <code class="hljs"> — lowlight highlight Media types render directly from the signed CloudFront download_url (server marks them inline-disposition). Text types fetch through the new /api/attachments/{id}/content proxy via TanStack Query, wrapped in useAttachmentPreview() so each entry point owns its own modal state without depending on a global Provider mount. Modal sizing: max-w-6xl × min(90vh, 100vh - 2rem) — slightly larger than create-issue's max-w-4xl since PDF / video need room, but capped to viewport on small screens. Sub-renderers use h-full to follow the fixed modal height instead of viewport-relative units. Images are intentionally NOT touched — the existing ImageLightbox (extensions/image-view.tsx) already handles them correctly. The new modal would be churn without user-visible benefit. Adds i18n keys under attachment.* (en + zh-Hans) and registers Preview/Download/Upload in the conventions glossary so future translations stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(desktop): enable Chromium PDF viewer for attachment preview Adds webPreferences.plugins: true to the main BrowserWindow so the bundled Chromium PDFium plugin activates inside iframes — required for the attachment preview modal's PDF dispatch. Default is false in Electron; without it <iframe src=*.pdf> renders blank. Security trade-off, accepted intentionally and documented inline: 1. This window already runs with webSecurity: false + sandbox: false, so plugins: true does NOT meaningfully widen the renderer's attack surface beyond what is already accepted. 2. The only PDFs that reach an iframe here are signed CloudFront URLs we ourselves issued; user-supplied URLs are routed through setWindowOpenHandler → openExternalSafely and cannot land in this renderer. 3. Chromium's PDFium plugin is itself sandboxed and only handles application/pdf — no Flash/Java/other historical plugin surfaces. If we ever tighten webSecurity / sandbox, the follow-up is to host the PDF viewer in a dedicated BrowserView with plugins scoped to that view, keeping the main renderer plugin-free. Old desktop builds ship without the preview modal, so the Eye button never appears and PDF preview is gated by the same release — zero regression risk for users on stale clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
48e3131bf9 |
feat: harden desktop frontend against API response drift (MUL-1828) (#2208)
* docs(claude): add API Response Compatibility section Narrows the existing "no backwards compat" rule to internal code only, and adds a new section that codifies the defensive boundary at API edges: parse-don't-cast, never pin UI to a single field, enum drift must downgrade not crash. Driven by #2143/#2147/#2192 — all three were the desktop client white- screening on backend response shape changes the client wasn't built against. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(core): add zod-based API response validation layer Introduces a defensive boundary so a malformed backend response degrades into a safe fallback (empty page, [], etc.) instead of throwing inside React render. - Adds zod to the pnpm catalog and as a @multica/core dependency. - New parseWithFallback helper in core/api/schema.ts that runs safeParse, logs a warn with the endpoint + zod issues on failure, and returns the caller-supplied fallback. Never throws. - Schemas in core/api/schemas.ts are deliberately lenient (string enums kept as z.string() so unknown values still parse, optional fields default, nested records use .loose() for unknown keys). - Wires setSchemaLogger from CoreProvider so warnings flow through the same logger as the rest of the API client. This is the primitive — see the next commit for the call-site wiring. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(api): guard top 5 high-risk endpoints with parseWithFallback Wraps the response of the five endpoints whose UIs white-screened in past incidents (#2143/#2147/#2192) so a contract drift returns a safe fallback instead of crashing the consumer: - listIssues → ListIssuesResponseSchema, fallback { issues: [], total: 0 } - listTimeline → TimelinePageSchema, fallback empty page - listComments → CommentsListSchema, fallback [] - listIssueSubscribers → SubscribersListSchema, fallback [] - listChildIssues → ChildIssuesResponseSchema, fallback { issues: [] } getIssue is intentionally NOT wrapped: there is no sensible "empty issue" — the entire detail page depends on real fields. The page-level ErrorBoundary (separate commit) catches that case. Adds schema.test.ts with 9 cases covering the five failure modes listed in MUL-1828: missing fields, wrong types, enum drift, null body, and null arrays. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(ui): add ErrorBoundary and wrap high-risk pages Section-level error boundary (no third-party dep — class component + default fallback in @multica/ui). Supports a fallback render prop and resetKeys for auto-recovery on resource navigation. Wraps the surfaces that white-screened in past incidents: - IssueDetail (web + desktop + inbox split-pane) — keyed on issueId so navigating to a different issue clears the boundary automatically. - IssuesPage (web + desktop). Boundaries are placed at consumer call sites rather than inside IssueDetail itself so we don't have to refactor the 1100-line component, and so a crash inside one inbox split-pane doesn't take down the inbox list next to it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(core): make all API schemas .loose() to preserve unknown fields zod 4 z.object() defaults to STRIP, which silently drops fields the schema didn't list. That makes the schema layer a sync point: a future PR adding a TS field but forgetting the schema would have the field disappear at runtime while TS still claims it exists — the exact bug- class this PR is meant to prevent, just inverted. Apply .loose() to every object schema (TimelineEntry, TimelinePage, Comment, Issue, ListIssuesResponse, Subscriber, ChildIssuesResponse) so unknown server-side fields pass through unchanged. Add a regression test that feeds a payload with extra fields at both entry and page level, and a direct unit test for parseWithFallback decoupled from any endpoint. Update the listIssues fallback test to use a wrong-type payload — under .loose() the previous "{ unexpected: true }" payload parses successfully (every declared field has a default) instead of triggering the fallback path it was meant to exercise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(claude): strip field-specific examples from API Compatibility section The original wording embedded current schema field names (entries, has_more_before, has_more_after, cursor, status, type) directly in the rules. CLAUDE.md should state the rule, not the implementation — once a field is renamed the doc drifts out of sync with the code, and the specific names don't add anything the abstract rule doesn't. Keep the rule, drop the field-level archaeology. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
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> |
||
|
|
5b4ee7c5e1 | fix(workspace): surface slug conflicts (#895) | ||
|
|
f41a0cf423 |
feat(views): extract packages/views — shared business UI + navigation adapter
- Create NavigationAdapter interface (push, replace, back, pathname, searchParams) - Create AppLink component replacing next/link in 4 files - Replace useRouter → useNavigation in 3 files (issue-detail, create-issue, create-workspace) - Create WebNavigationProvider wrapping Next.js useRouter/usePathname/useSearchParams - Move ~85 feature UI files (issues, editor, modals, my-issues, skills, runtimes) to packages/views/ - Add store singleton registration pattern (registerAuthStore, registerWorkspaceStore) - Create data-aware wrappers in packages/views/common/ (ActorAvatar, Markdown) - Update all app-layer imports to @multica/views/* - Add @source directive for Tailwind to scan views package - packages/views/ has zero next/* imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e1e7f68330 |
feat: extract packages/core — Turborepo infrastructure + headless business logic
Phase 1: Monorepo infrastructure - Add Turborepo with turbo.json pipeline (build, dev, typecheck, test) - Update pnpm-workspace.yaml to include packages/* - Create shared TypeScript config (packages/tsconfig) Phase 2: Extract packages/core (zero react-dom, all-platform reuse) - Move domain types, API client, logger, utils → packages/core/ - Move TanStack Query modules (issues, inbox, workspace, runtimes) - Move Zustand stores (auth, workspace, issues, navigation, modals) - Move realtime sync (WSProvider, hooks, ws-updaters) - Refactor auth/workspace stores to factory pattern for DI - Refactor ApiClient with onUnauthorized callback - Refactor useWorkspaceId to React Context (WorkspaceIdProvider) - Refactor WSProvider to accept wsUrl + store props - Create apps/web/platform/ bridge layer (api singleton, store instances) - Update 91 import paths across apps/web/ - Fix 3 test files for new import paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |