Closes#430.
During the session fetch on SaaS deployments, AuthGate returned null —
causing a white/blank screen flash for 200–500ms before the zinc-950
canvas background appeared.
Replace with a fixed zinc-950 div so the browser always paints the
correct dark background from the first frame. The canvas loading UI
renders on top once the session resolves, with no visible transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Single-container tenant architecture: Go platform (:8080) + Canvas
Node.js (:3000) in one Fly machine, with Go's NoRoute handler reverse-
proxying non-API routes to the canvas. Browser only talks to :8080.
Changes:
platform/Dockerfile.tenant — multi-stage build (Go + Node + runtime).
Bakes workspace-configs-templates/ + org-templates/ into the image.
Build context: repo root.
platform/entrypoint-tenant.sh — starts both processes, kills both if
either exits. Fly health check on :8080 covers the Go binary; canvas
health is implicit (proxy returns 502 if canvas is down).
platform/internal/router/canvas_proxy.go — httputil.ReverseProxy that
forwards unmatched routes to CANVAS_PROXY_URL (http://localhost:3000).
Activated by NoRoute when CANVAS_PROXY_URL env is set.
platform/internal/router/router.go — wire NoRoute → canvasProxy when
CANVAS_PROXY_URL is present; no-op otherwise (local dev unchanged).
platform/internal/middleware/securityheaders.go — relaxed CSP to allow
Next.js inline scripts/styles/eval + WebSocket + data: URIs. The
strict `default-src 'self'` was blocking all canvas rendering.
canvas/src/lib/api.ts — changed `||` to `??` for NEXT_PUBLIC_PLATFORM_URL
so empty string means "same-origin" (combined image) instead of falling
back to localhost:8080.
canvas/src/components/tabs/TerminalTab.tsx — same `??` fix for WS URL.
Verified: tenant machine boots, canvas renders, 8 runtime templates +
4 org templates visible, API routes work through the same port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both components use useState/useEffect/useCallback/useRef but were
missing the 'use client' directive. Without it Next.js App Router
renders them as server HTML — React never hydrates them and event
handlers are silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chip labels (status badge, active-task count, current-task text) were
rendered at text-[7px] — well below the 9px minimum required to meet
WCAG 1.4.3 readability. Raised all three to text-[9px] so the labels
are legible without magnification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WorkspaceNode.tsx — three text-[7px] labels carry meaningful content
that users must read, making them WCAG 1.4.3 failures at default zoom:
• Status label (failed/degraded/provisioning) — critical signal
• Active-tasks count — task load indicator
• currentTask banner text — live work description
Bumped to text-[9px] minimum. Decorative elements (+N overflow) unchanged.
TracesTab.tsx — two text-[9px] text-zinc-600 labels:
• Token count ("1234 tok")
• Expand chevron ("▼"/"▶")
zinc-600 on zinc-900 ≈ 2.6:1 (fails WCAG AA 4.5:1 for small text).
Changed to text-zinc-500 ≈ 4.6:1. Size unchanged (already at minimum 9px).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of position collision after node deletion:
handleCanvasEvent(WORKSPACE_PROVISIONING) used nodes.length as the
grid placement index. handleCanvasEvent(WORKSPACE_REMOVED) shrinks
the array, so the next provisioned node reuses a lower index and
lands at the exact same (x, y) as an existing live node.
Example (4-col grid, COL_SPACING=320):
Provision A → idx 0 → (100, 100)
Provision B → idx 1 → (420, 100)
Provision C → idx 2 → (740, 100)
Remove A → nodes.length drops to 2
Provision D → idx 2 → (740, 100) ← COLLISION with C
Fix 1 — monotonic _provisioningSequence counter (only ever increases):
- Replaces nodes.length as the placement index
- Immune to deletions; every provisioned node gets a unique grid slot
- resetProvisioningSequence() exported for test teardown only
Fix 2 — the existing restart-path guard (if exists → update, not create)
already provides idempotency for duplicate WS events on known nodes;
confirmed: restart path does NOT increment the counter.
Tests: +4 new cases (grid wrap, collision regression, restart-path
counter isolation, multi-provision positions). 485/485 pass.
Build: next build ✓ clean.
Note: complementary to PR #44's origin-offset fix (closed without
merging) — that fix addressed nodes stacking at (0,0); this fix
addresses position collisions after deletions. Both should land.
Co-authored-by: Canvas Agent <agent@canvas.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Templates 7-8 (LangGraph Agent, OpenClaw Agent) were silently hidden
by a hard-coded `.slice(0, 6)` cap. The grid container already has
`max-h-[240px] overflow-y-auto` to handle overflow — the slice was
redundant and harmful. Remove it so all API-returned templates render.
Co-authored-by: UIUX Designer <uiux@molecule-ai.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of position collision after node deletion:
handleCanvasEvent(WORKSPACE_PROVISIONING) used nodes.length as the
grid placement index. handleCanvasEvent(WORKSPACE_REMOVED) shrinks
the array, so the next provisioned node reuses a lower index and
lands at the exact same (x, y) as an existing live node.
Example (4-col grid, COL_SPACING=320):
Provision A → idx 0 → (100, 100)
Provision B → idx 1 → (420, 100)
Provision C → idx 2 → (740, 100)
Remove A → nodes.length drops to 2
Provision D → idx 2 → (740, 100) ← COLLISION with C
Fix 1 — monotonic _provisioningSequence counter (only ever increases):
- Replaces nodes.length as the placement index
- Immune to deletions; every provisioned node gets a unique grid slot
- resetProvisioningSequence() exported for test teardown only
Fix 2 — the existing restart-path guard (if exists → update, not create)
already provides idempotency for duplicate WS events on known nodes;
confirmed: restart path does NOT increment the counter.
Tests: +4 new cases (grid wrap, collision regression, restart-path
counter isolation, multi-provision positions). 485/485 pass.
Build: next build ✓ clean.
Note: complementary to PR #44's origin-offset fix (closed without
merging) — that fix addressed nodes stacking at (0,0); this fix
addresses position collisions after deletions. Both should land.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two helper paragraphs in ChannelsTab.tsx used text-[9px] text-zinc-600:
- Chat IDs discover hint (line 254)
- Allowed Users hint (line 281)
9px fails WCAG 1.4.3 by size alone; zinc-600 on zinc-800/900 bg is
~2.6:1 contrast (fails AA). Changed to text-[11px] text-zinc-500
(~3.8:1 at 11px — acceptable for non-body helper text).
Found in UX audit Run 13 (2026-04-16).
Co-authored-by: UIUX Designer <uiux@molecule-ai.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
CI 5/6 pass (E2E cancel = run-supersession pattern). Dev Lead review 04:21: ✅ Approved. Fixes cross-tenant token exposure: PausePollersForToken now scoped to requesting workspace_id via SQL WHERE clause. Closes#329.
Adds a public /pricing route the apex + tenant canvas can both serve.
Three-tier plan cards (Free, Starter, Pro) with per-plan CTA buttons
that dispatch correctly regardless of the user's state:
Free → redirect to signup
Anonymous + paid → redirect to signup (Stripe opens post-auth)
Authed + paid → POST /cp/billing/checkout, redirect to Stripe URL
No tenant slug → inline error ("pick an org first")
Network failures → surfaced in an ARIA alert banner
Files:
- src/lib/billing.ts — plan metadata + startCheckout + openBillingPortal
wrappers over /cp/billing/{checkout,portal}
- src/components/PricingTable.tsx — client component, lazy session
probe on first CTA click (no probe for anonymous browsers)
- src/app/pricing/page.tsx — server-rendered shell with SEO metadata,
links to legal pages in the footer
- Tests: 10 billing helper tests + 9 PricingTable tests (17 total,
additional ones cover the plan-list canonical order)
Design notes:
- The pricing data (features + prices) is a static const in billing.ts,
not fetched from the API. Changing prices requires a deploy — which
we'd need to do anyway for tier definition changes.
- PLAN_ID 'starter' is flagged highlighted=true so the middle card gets
the 'Most popular' visual treatment. One source of truth; test locks it.
- Session probe is lazy (first CTA click, not mount) so anonymous
visitors don't generate a /cp/auth/me request just to read the page.
AuthGate interaction:
- On apex (no tenant slug), AuthGate passthrough — /pricing renders freely
- On tenant subdomain, AuthGate still bounces anonymous users to login
before reaching /pricing — this is the correct UX for the "I'm already
logged in and want to upgrade my own org" flow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a GDPR/ePrivacy-compliant cookie banner to the canvas root layout.
Privacy-preserving default: no optional cookies are considered accepted
until the user clicks "Accept all". Clicking "Necessary only" or
dismissing records "rejected" and the banner does not re-appear until
the cookie-policy version bumps.
- New CookieConsent component wired into src/app/layout.tsx so it
renders on every canvas route
- Persists decision to localStorage as {decision, decidedAt, version}
- Versioned schema: bumping CURRENT_VERSION re-prompts every user
- Exports hasConsent() helper for feature code that needs to gate
analytics / functional cookies on user choice
- ARIA: role=dialog + aria-labelledby/aria-describedby so screen
readers announce it as a dialog
- Same storage key + schema as the control-plane legal-page banner
(see molecule-controlplane PR #XX) so a user who accepts on one
surface does not re-see the banner on the other
Tests: 12 Vitest cases covering first-visit render, accept/reject
persistence, version re-prompt, invalid-JSON recovery, privacy link
attrs, ARIA markup, and the hasConsent helper under every state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses the three release-blocking WCAG violations from the UX audit
(3rd consecutive cycle) and the new ChatTab ARIA gap from Audit #2.
Changes:
- Toaster: split into polite (success/info) + assertive (error) live
regions, both always in DOM so screen readers register them before
any toast fires. Adds x dismiss button on every toast. Errors no
longer auto-expire after 4s — persist until explicitly dismissed.
- ConfirmDialog: on open, requestAnimationFrame focuses the first
button inside the dialog. Tab/Shift-Tab is now trapped inside the
dialog while open. Added role="dialog" aria-modal="true" and
aria-labelledby pointing to the title h3.
- WorkspaceNode: outer div gains role="button", tabIndex={0},
aria-label, aria-pressed, and onKeyDown (Enter/Space => selectNode,
ContextMenu key => openContextMenu). Keyboard-only users can now
reach and activate workspace nodes.
- ChatTab sub-tab bar: role="tablist" on wrapper, role="tab" +
aria-selected + aria-controls on each button, matching
role="tabpanel" + id on each panel div. Textarea gets
aria-label="Message to agent".
453/453 Vitest tests pass. Production build clean (Next.js 15).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New nodes were placed at (0,0) or close to it, causing them to spawn
behind the toolbar/palette chrome and require manual panning to find.
Add GRID_ORIGIN_X/Y = 100 offset so the first node lands in clear canvas
space, and update the position assertion in the unit test accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps the canvas root so every tenant-subdomain request checks for a
valid session and bounces to app.moleculesai.app/cp/auth/login with a
return_to pointing back at the current URL. Local dev + vercel preview
URLs + apex pass through unchanged.
Files:
- canvas/src/lib/auth.ts: fetchSession() probes /cp/auth/me
(credentials:include for cross-origin cookie); returns Session on 200,
null on 401 (anonymous, no throw), throws on 5xx so transient
outages don't leak the UI.
- canvas/src/lib/auth.ts: redirectToLogin() builds the cp login URL
with window.location.href as return_to; CP's isSafeReturnTo check
rejects cross-domain bounces.
- canvas/src/components/AuthGate.tsx: client component wrapping
children. State machine: loading → authenticated | anonymous. In
non-SaaS mode (no tenant slug) skips the gate entirely.
- canvas/src/app/layout.tsx: wraps the root body in <AuthGate>.
Tests: +6 auth.ts (200 / 401 null / 5xx throw / credentials:include /
redirectToLogin href + signup variant). Full suite 453 green (was 447).
Pairs with molecule-controlplane PR #16 (return_to cookie handshake
on the cp side).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Canvas will be served at <slug>.moleculesai.app (Vercel). API calls go
cross-origin to https://app.moleculesai.app. This commit wires the
client side:
- canvas/src/lib/tenant.ts: getTenantSlug() derives the slug from
window.location.hostname, case-insensitive, matching the control
plane's reservedSubdomains list (app/www/api/admin/…). Server-side
+ localhost + vercel preview URLs + apex all return "" so local dev
keeps working.
- canvas/src/lib/api.ts: adds X-Molecule-Org-Slug header + sets
credentials:"include" on every fetch. The control plane's CORS
middleware allows the origin + credentials; the session cookie has
Domain=.moleculesai.app so the browser ships it.
- canvas/src/lib/api/secrets.ts: same treatment (secrets API uses its
own fetch helper — shared slug+credentials logic applied).
Tests: +6 (tenant.test.ts covers slug / reserved / case / non-SaaS /
preview URL / apex). Full canvas suite 447/447 green.
Not in this PR:
- WS URL derivation for terminal/socket.ts (separate follow-up; WS
needs its own slug-aware URL and the canvas terminal isn't used in
SaaS launch day-one).
- Next.js rewrites (decided against; cross-origin with credentials
is cleaner than path-level rewrites for session cookies).
Deploys to Vercel once merged — no manual config needed (env already set).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds Z as a keyboard equivalent for the existing double-click zoom-to-team
gesture (WCAG 2.1.1). When a team node is selected, pressing Z dispatches
molecule:zoom-to-team, which fitBounds to the parent and all children.
Input elements are guarded so Z still types normally in text fields.
Adds a 6th help panel entry documenting the Dbl-click / Z gesture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
globals.css: append @media (prefers-reduced-motion: reduce) block that zeroes
animation/transition durations, disables .animate-in/.slide-in-from-* entry
animations (Toaster, ApprovalBanner, SidePanel slide), strips dashdraw and
node-appear keyframes from React Flow elements.
Components: replace all bare animate-pulse (13 occurrences across WorkspaceNode,
StatusDot, Toolbar, SidePanel, Legend, SearchDialog, TerminalTab, TemplatePalette)
with motion-safe:animate-pulse so status indicator pulsing stops for users with
vestibular disorders. Replace 3 animate-bounce occurrences in ChatTab typing
indicator with motion-safe:animate-bounce.
Tests: new canvas/src/__tests__/reduced-motion.test.ts (12 tests) verifies the
@media block is present in globals.css and that every component file uses the
motion-safe: variant rather than bare animation classes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gap 1 — WS_URL now derives from NEXT_PUBLIC_PLATFORM_URL when
NEXT_PUBLIC_WS_URL is not set (http→ws, appends /ws; https→wss).
Operators need only one env var. NEXT_PUBLIC_WS_URL remains an explicit
override escape hatch.
Gap 2 — Add canvas/.env.example documenting NEXT_PUBLIC_PLATFORM_URL
(required) and NEXT_PUBLIC_WS_URL (optional override, commented out).
Gap 3 — Toolbar fires showToast("Live updates restored", "success")
when wsStatus transitions connecting→connected. mountedRef (set after
2 s) suppresses the toast on the very first page-load connection so
only genuine reconnects notify the user.
Gap 4 — New canvas/src/store/__tests__/socket.url.test.ts (6 tests):
· fallback to ws://localhost:8080/ws when no env set
· http→ws derivation from NEXT_PUBLIC_PLATFORM_URL
· https→wss derivation
· NEXT_PUBLIC_WS_URL override takes precedence
· api.ts PLATFORM_URL fallback
· api.ts reads NEXT_PUBLIC_PLATFORM_URL
375/375 tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a live/reconnecting/offline pill to the Toolbar so users can see
at a glance whether the canvas is receiving real-time updates.
Changes:
- canvas/src/store/canvas.ts: add wsStatus ('connected'|'connecting'|
'disconnected') field + setWsStatus action to CanvasState (initial:
'connecting')
- canvas/src/store/socket.ts: wire setWsStatus into ReconnectingSocket —
'connecting' on connect() call, 'connected' in onopen, 'connecting'
in onclose (will reconnect), 'disconnected' in disconnect()
- canvas/src/components/Toolbar.tsx: subscribe to wsStatus; render
WsStatusPill (green "Live" / amber pulsing "Reconnecting" / red
"Offline") after the workspace count section
- canvas/src/store/__tests__/socket.test.ts: add setWsStatus: vi.fn()
to the canvas store mock (global factory, beforeEach reset, and the
mid-test override in the onmessage test)
369/369 canvas tests passing, production build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw Parent Workspace ID text input with a <select> populated
from GET /workspaces (T{tier} · {name} format, graceful fallback on
fetch error). Raise all interactive button text from text-[8px]/[9px]
to text-[11px] across SkillsTab, ScheduleTab, secrets-section,
ActivityTab, SidePanel, ChatTab; non-interactive labels/badges to
text-[10px]. Adds 7 CreateWorkspaceDialog unit tests (372/372 passing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap CanvasInner return in React Fragment to host skip-nav link as sibling of <main>
- Add <a href="#canvas-main"> skip link (sr-only, revealed on focus) before <main>
- Add id="canvas-main" to <main> element
- Add aria-label="Molecule AI workspace canvas" to ReactFlow wrapper
- Add Canvas.a11y.test.tsx: 4 jsdom tests covering all three a11y landmarks
369/369 tests pass; next build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UX Audit Run 6 critical finding: Legend panel and workspace node cards used 8px and 9px
text (6–7pt), which is physically unreadable and fails WCAG minimum guidelines.
- Legend.tsx: raise all text-[8px]/[9px]/[10px] → text-[11px] across every sub-component
(StatusItem labels, TierItem badge+label, CommItem icon+label, section headers)
- WorkspaceNode.tsx: raise text-[8px]/[9px] → text-[10px] for all readable labels in
the main card (status text, skill badges, task/error banners, tier badge, sub count,
Team Members header) and TeamMemberChip primary name/role text
Compact 7px elements inside TeamMemberChip (tier/sub badges, status micropills) retained
to preserve dense canvas layout — only human-readable labels were upgraded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- computeAutoLayout() BFS tree layout seeds from anchored nodes; assigns
distinct x/y to workspaces returned at 0,0 by the API and persists via PATCH
- buildNodesAndEdges() accepts layoutOverrides map so hydration uses computed
positions instead of raw 0,0 coordinates
- canvas-events WORKSPACE_PROVISIONING grid layout replaces offset===offset
assignment that caused position:{x:t,y:t} in the minified bundle
- 8 new vitest tests cover computeAutoLayout and override behaviour (365 pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Walks the real UI end-to-end:
1. Creates + registers a workspace on the platform
2. Opens the detail side panel
3. Clicks the Files tab (force-click since it's in an overflow-x bar)
4. Asserts all 3 split components render:
- FilesToolbar: "+ New" + "Upload" buttons
- FileTree: the config.yaml seeded by the default template
- FileEditor: "Select a file to edit" empty-state
Saves screenshots at /tmp/filestab-{1,2,3}-*.png for manual review.
Run: cd canvas && npx playwright test e2e/filestab-smoke.spec.ts
Requires platform on :8080 + canvas on :3000.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pure restructure — no behavior change. Extracts FileTree, FileEditor,
FilesToolbar, useFilesApi hook, and tree utilities into sibling files
under canvas/src/components/tabs/FilesTab/. Top-level FilesTab.tsx is
now 240 lines (glue + confirmations); re-exports buildTree/TreeNode so
the existing import path and tests remain stable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up to the quality-fixes-pass2 code review.
## Go: direct unit tests for PR #5 extracted helpers (~47 new tests)
a2a_proxy_test.go:
- resolveAgentURL: cache hit, cache-miss DB hit, not-found, null-URL,
docker-rewrite guard
- dispatchA2A: build error, canvas timeout, agent timeout, success
- handleA2ADispatchError: context deadline, generic error, build error
- maybeMarkContainerDead: nil-provisioner, runtime=external short-circuits
- logA2AFailure, logA2ASuccess: activity_logs row content + status
delegation_test.go:
- bindDelegateRequest: valid / malformed / bad-UUID
- lookupIdempotentDelegation: no-key / no-match / failed-row-deleted / existing-pending
- insertDelegationRow: insertOK / insertHandledByIdempotent /
insertTrackingUnavailable
- insertDelegationOutcome: zero-value is insertOutcomeUnknown sentinel
discovery_test.go:
- discoverWorkspacePeer: online / not-found / access-denied + 2 edges
- writeExternalWorkspaceURL: 3 cases
- discoverHostPeer: smoke test documents the unreachable-by-design path
activity_test.go:
- parseSessionSearchParams: defaults + custom limit/offset/q
- buildSessionSearchQuery: no-filters + with-query shapes
- scanSessionSearchRows: empty / single / multiple rows
Package coverage: 56.1% → 57.6%. Every helper extracted in PR #5 is
now at or near 100% line coverage (see PR notes for the 4 remaining
gaps, all blocked on provisioner interface mockability).
## Defensive enum zero-value fix
insertDelegationOutcome now starts with insertOutcomeUnknown=0 as a
sentinel so an un-initialized variable can't silently read as
"success". insertOK, insertHandledByIdempotent, insertTrackingUnavailable
shift to 1/2/3. No caller changes needed.
## Canvas: ConfirmDialog.singleButton test (5 cases)
canvas/src/components/__tests__/ConfirmDialog.test.tsx covers:
- default render (both buttons)
- singleButton hides Cancel
- singleButton: Escape still fires onCancel
- singleButton: backdrop-click still fires onCancel
- singleButton: onConfirm fires on click
vitest total: 352 → 357, all passing.
## Docstring clarity
ConfirmDialog.tsx: expanded singleButton prop comment to explicitly
instruct callers to pass the same handler for onConfirm/onCancel when
using it as an info toast (matches TemplatePalette usage).
## ErrorBoundary clipboard observability
.catch(() => {}) silently swallowed rejections. Now:
.catch((e) => console.warn("clipboard write failed:", e))
so permission-denied / insecure-context failures surface in the console.
## Verification
- go build ./... clean
- go vet ./... clean
- go test -race ./internal/... — all pass
- canvas npm run build — clean
- canvas npm test -- --run — 357/357 pass
- tests/e2e/test_api.sh — 46/62 pass; all 16 failures are pre-existing
(token-auth enforcement + stale test workspaces + missing Docker
network). None involve handlers touched in PR #5.
- Manual: platform + canvas running locally, title=Molecule AI,
/workspaces returns [], /health returns ok. Identified + killed a
stale Next.js server from the old Starfire-AgentTeam repo that was
serving the old brand on IPv4 port 3000.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Post-review fixes on top of the quality-pass-2 branch.
1. delegation.go: replaced insertDelegationRow's (bool, bool) return
with a typed insertDelegationOutcome enum (insertOK /
insertHandledByIdempotent / insertTrackingUnavailable). Eliminates
the positional-boolean decoding the caller had to do. Internal, no
behavior change.
2. ConfirmDialog.tsx: added singleButton prop. When true, hides the
Cancel button for single-action info toasts (Esc still dismisses
via onCancel). TemplatePalette's import notice uses it.
3. ErrorBoundary.tsx: fixed the floating clipboard promise. Added
.catch(() => {}) so a rejected writeText (permission denied,
insecure context) doesn't surface as unhandled rejection.
4. a2a_proxy_test.go: added 5 direct unit tests for
normalizeA2APayload (invalid JSON, wraps-bare, preserves-existing-
id, preserves-existing-messageId, missing-method). Fills the unit-
test gap for the helper extracted in the last pass.
Verification:
- go test -race ./internal/handlers/... passes (incl. 5 new tests)
- go build ./... clean
- canvas npm run build clean
- canvas npm test -- --run -> 352/352
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swap in the new molecular-graph icon across canvas favicon, in-app logo,
and README branding paths. Add HANDOFF.md as the cross-session context
doc carried over from the Starfire→Molecule AI migration. Fix stale
"Starfire" reference in the pre-commit hook header.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>