Commit Graph

82 Commits

Author SHA1 Message Date
Hongming Wang
b7072d87f1
Merge pull request #751 from Molecule-AI/feat/issue-744-a2a-topology-overlay
feat(canvas): A2A topology overlay with animated delegation edges
2026-04-17 09:15:10 -07:00
molecule-ai[bot]
c11792b861
feat(canvas): A2A topology overlay with animated delegation edges (issue #744)
- New A2ATopologyOverlay component polls /activity fan-out every 60s and
  writes directed edges to a2aEdges store slice (separate from topology edges)
- buildA2AEdges aggregates delegate rows per source→target pair; violet-500
  animated edge when last call <5 min ago, blue-500 static otherwise
- Toolbar toggle persists to localStorage (molecule:show-a2a-edges)
- Canvas.tsx merges a2aEdges into allEdges via useMemo; pointerEvents:none
  on all edge elements keeps nodes draggable
- 24 new unit tests across pure function, helper, and component suites
- Fix Canvas.a11y and Canvas.pan-to-node store mocks (missing A2A fields)

Closes #744

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:45:34 +00:00
molecule-ai[bot]
cff3794371
feat(canvas): add MemoryInspectorPanel for workspace KV memory (issue #730)
Builds MemoryInspectorPanel.tsx — a focused inspector for per-workspace
platform memory entries. Replaces MemoryTab in the SidePanel "memory" tab.

- GET /workspaces/:id/memory loads entries (flat MemoryEntry[] — confirmed
  with Backend Engineer: fields are key/value/version/expires_at/updated_at,
  no scope, write verb is POST not PATCH)
- Empty state: "No memory entries yet" with icon
- Click entry -> expand -> show JSON value, version badge, relative timestamp
- Edit flow: textarea pre-filled with JSON.stringify(value), Save calls POST
  with if_match_version for optimistic concurrency, optimistic update with
  rollback on 409/error, invalid-JSON guard
- Delete flow: button -> ConfirmDialog -> optimistic removal -> DELETE call
- Refresh button re-fetches entries
- 665 tests pass (43 files), next build clean, 'use client' check passes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 15:24:53 +00:00
Molecule AI Frontend Engineer
a7cd538fc3 feat(canvas): add max effort level to ConfigTab dropdown (#653)
Adds a fifth option to the effort <select> in the Claude Settings section:

  <option value="max">max — absolute ceiling</option>

The dropdown now offers: low / medium / high / xhigh / max.

effort is typed as string? so no interface update required.
Test updated: source-assertion count "four" → "five", new toYaml
serialization test for effort: max.

641/641 tests pass. Build clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:58:29 +00:00
Molecule AI Frontend Engineer
848b745b6e feat(canvas): expose effort + task_budget in ConfigTab (#608)
Adds two new Claude API primitives (Opus 4.7+) as configurable workspace
fields in the Config tab form:

  effort: 'low' | 'medium' | 'high' | 'xhigh'
    Maps to output_config.effort in the Anthropic Messages API.
    Controls thinking depth — xhigh enables extended thinking mode.

  task_budget: integer (token count, 0 = unset)
    Maps to output_config.task_budget.total; requires beta header
    task-budgets-2026-03-13. Lets operators cap token spend per task.

Both fields are stored as top-level keys in config.yaml and read by
claude_sdk_executor.py (workspace-template side, tracked in #608).

Canvas changes:
- form-inputs.tsx: effort?: string, task_budget?: number added to
  ConfigData; DEFAULT_CONFIG initialises them to "" / 0
- yaml-utils.ts: toYaml() emits effort + task_budget (omits when
  empty/zero); parseYaml() already handles plain string/integer keys
- ConfigTab.tsx: new collapsible "Claude Settings" section (defaultOpen=false)
  shown when runtime === "claude-code" OR model name contains "claude"
  or "anthropic". Dropdown for effort (4 options + unset), number input
  for task_budget (step 1000, 0 = unset).

Tests (25 cases in ClaudeSettings.test.tsx):
  - toYaml serialises all four effort values + omits empty/undefined
  - toYaml serialises task_budget + omits 0/undefined
  - effort appears before task_budget in YAML output
  - parseYaml round-trips both fields correctly
  - DEFAULT_CONFIG shape assertions
  - Source assertions for section guards + option values
  - React rendering: section visible for claude-code/claude model,
    hidden for non-Claude runtime (crewai + gpt-4o)

640/640 tests pass. Build clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:24:36 +00:00
Molecule AI Frontend Engineer
28dfa185aa fix(canvas): mock WorkspaceUsage in BudgetLimit.DetailsTab test
DetailsTab renders WorkspaceUsage alongside BudgetSection. The test suite
sets api.get to return [] (a valid empty peers list) but WorkspaceUsage
calls api.get for metrics and crashes on undefined input_tokens when the
mock returns an array instead of a WorkspaceMetrics object.

Add a stub vi.mock following the same pattern already used for BudgetSection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:22:07 +00:00
040f674a6a fix(gate-1): resolve merge conflicts with main
Three add/add + content conflicts, all mechanical:
- WorkspaceUsage.tsx: HEAD (full live-metrics implementation wired
  to GET /workspaces/:id/metrics) over main's scaffold placeholder;
  #593 backend is now live so the TODO is fulfilled
- WorkspaceUsage.test.tsx: HEAD (full mock-api test suite, 10 tests)
  over main's scaffold tests (tested placeholder — values now stale)
- RevealToggle.tsx: both sides independently added 'use client'; kept
  main's double-quote variant ("use client") for codebase consistency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:16:36 +00:00
Molecule AI Frontend Engineer
bfe4e09b7e fix(canvas): move vi.mock to module top level in ZoomShortcut.test (#632)
The vi.mock("../../../store/canvas") call was nested inside an it()
block. Vitest hoists all vi.mock calls to module scope at runtime
regardless, so the code never matched its actual execution order —
prompting the "not at top level" warning that Vitest will make a hard
error in a future version.

Move the mock to after the imports, remove the now-redundant inline
call from the it() body, and add a comment explaining the hoisting rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:09:39 +00:00
Molecule AI Frontend Engineer
a60ece77c6 fix(canvas): use explicit empty-string check in BudgetSection to preserve zero-credit budget
parseInt("0", 10) || null evaluates to null, silently converting a
zero-credit budget to unlimited. Switch to raw !== "" ? parseInt() : null
so budget_limit: 0 is sent correctly. Adds regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:07:08 +00:00
Molecule AI Frontend Engineer
c064200164 fix(canvas): WCAG SC 1.3.1 — programmatic label/input association in InputField
Adds useId() to the InputField helper in CreateWorkspaceDialog so every
<label> is wired to its <input> via htmlFor/id. Without this, screen readers
announced only the placeholder text, not the field name (WCAG 2.1 SC 1.3.1
Level A violation, build 4JIwTGVMjDGNLO8iMGJeC).

Affected fields: Name (required), Role, Budget limit (USD), Template.
The Hermes provider fields were already correctly wired.

Adds 6 new tests in CreateWorkspaceDialog.a11y.test.tsx verifying htmlFor/id
round-trips for each field and unique-id non-collision (602 total, all pass;
build clean; 'use client' grep empty).

Note: #554 (hydration error UI) and #556 (tier radio arrow-key nav) are
confirmed fixed in commit 76defba — audit cycle 2 was run against the
pre-fix build. #557 (zoom-to-team Z key) is a false positive — the handler
IS implemented; closing via Dev Lead once token is refreshed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:07:08 +00:00
Molecule AI Frontend Engineer
2152323cd1 feat(#541): budget settings UI with usage stats and 402 handling
Adds a dedicated BudgetSection component to the workspace details panel:
- GET /workspaces/:id/budget on mount — populates live stats (used/limit/remaining)
- Stats row + blue-500 progress bar (capped at 100%; hidden when unlimited)
- PATCH /workspaces/:id/budget for saving; input blank → budget_limit: null
- "Budget exceeded — messages blocked" amber/zinc-950 banner on any 402 response
  (GET or PATCH); banner clears on a successful subsequent save
- 'use client'; dark zinc theme throughout (zinc-800/700 inputs, blue-500 accents)

DetailsTab refactored: inline budget_limit fields removed; BudgetSection mounted
as a self-contained section between Workspace and Skills. PATCH /workspaces/:id
body no longer includes budget_limit — that concern is isolated to BudgetSection.

Tests: 21 new cases in BudgetSection.test.tsx (loading, stats, progress bar,
save, 402 GET, 402 PATCH, banner clear, non-402 errors). BudgetLimit.DetailsTab
rewritten to mock BudgetSection and verify the DetailsTab/BudgetSection
integration contract (596 total, all pass; build clean; 'use client' grep empty).

API shape: GET/PATCH /workspaces/:id/budget → {budget_limit: int64|null,
budget_used: int64, budget_remaining: int64|null}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:07:07 +00:00
Molecule AI Frontend Engineer
5d081769e5 feat(canvas): budget_limit input in workspace creation and settings UI (#541)
- Adds optional Budget limit (USD) numeric field to CreateWorkspaceDialog;
  blank = null (unlimited), populated = parsed float sent as budget_limit in
  POST /workspaces body
- Adds budget_limit field to DetailsTab edit form; saves via
  PATCH /workspaces/:id; pre-fills from current WorkspaceNodeData
- Shows 'Budget limit exceeded' warning badge when budgetUsed > budgetLimit
  (forward-compatible — badge hidden when budgetUsed is absent)
- Extends WorkspaceData, WorkspaceNodeData, and buildNodesAndEdges to carry
  budgetLimit / budgetUsed fields ready for backend hydration (issue #541 BE PR)
- Ships 22 new tests across CreateWorkspaceDialog and BudgetLimit.DetailsTab
  suites (575 total, all passing); npm run build clean; 'use client' grep empty

API shape confirmed from workspace.go and CreateWorkspacePayload struct:
  field name: budget_limit | type: number | null | units: USD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:06:36 +00:00
Molecule AI Frontend Engineer
e89d9a1239 feat(canvas): wire live metrics API in WorkspaceUsage (#592)
WorkspaceUsage now fetches GET /workspaces/:id/metrics on mount and on
workspaceId change. Displays input_tokens and output_tokens formatted
with toLocaleString, and estimated_cost_usd as $X.XXXXXX. Shows three
zinc-700 skeleton rows while loading; surfaces error text on failure.
Stale-fetch guard via ignore flag prevents state updates after unmount.

Also fixes missing 'use client' in RevealToggle.tsx (#603) — the
onClick handler requires client-side hydration.

Tests updated: 12 tests covering loading skeleton, API call correctness,
token formatting, cost formatting, error state, and workspaceId refetch.
All 551 canvas tests pass; build clean.

Closes #592
Closes #603

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 06:00:14 +00:00
molecule-ai[bot]
ff756a3920
Merge pull request #600 from Molecule-AI/feat/issue-592-workspace-cost-transparency
feat(canvas): scaffold WorkspaceUsage component for #592
2026-04-17 05:32:40 +00:00
Molecule AI Frontend Engineer
a6a559d62c feat(canvas): scaffold WorkspaceUsage component for #592
Adds WorkspaceUsage component to canvas/src/components/ with three
placeholder stat rows (Input tokens, Output tokens, Estimated cost)
and a "pending #593" badge. Wires into DetailsTab between the Workspace
and Skills sections. No API calls yet — fetch logic will be added once
GET /workspaces/:id/metrics lands in #593.

9 tests in WorkspaceUsage.test.tsx; all 548 canvas tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 05:16:57 +00:00
Hongming Wang
0e55e97cc3 fix(canvas): add hermes + gemini-cli to deploy preflight required keys
Hermes requires OPENROUTER_API_KEY (or any of its 15 providers).
Gemini CLI requires GOOGLE_API_KEY. Without these entries, the
MissingKeysModal doesn't fire and workspaces start without keys,
causing crash loops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:45:54 -07:00
Hongming Wang
c06ac8aa8a fix(canvas): 5 UX polish fixes — error handling, a11y, loading state
1. ScheduleTab + ChannelsTab: wrap toggle/delete in try/catch with
   error feedback (was silently swallowing API failures)
2. MemoryTab: "+Add" button now auto-expands Advanced section
3. SidePanel: keyboard-navigated tabs scroll into view
4. TracesTab: emoji aria-hidden, env-var hint in <details>
5. page.tsx: show Spinner while hydrating instead of flash of EmptyState

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:39:44 -07:00
molecule-ai[bot]
b37f71b6da
fix(canvas): hydration error UI (#554), radio arrow-key nav (#556), zoom-to-team context menu (#557) (#565)
- #554 CRITICAL: Add hydrationError state to Zustand store; catch handler now
  calls setHydrationError instead of silent console.error; page renders a
  full-screen zinc-950 error banner with a Retry button that reloads the page
- #556 MEDIUM: Add roving tabIndex + ArrowDown/Up/Left/Right keyboard handler
  to the tier radio group in CreateWorkspaceDialog (WCAG 2.1 compliant)
- #557 MEDIUM: Add "Zoom to Team" menu item to ContextMenu (visible only when
  node has children); dispatches molecule:zoom-to-team for keyboard accessibility
- Bonus: add missing 'use client' directive to RevealToggle.tsx

Co-authored-by: Molecule AI Frontend Engineer <frontend-engineer@agents.moleculesai.app>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 00:35:54 +00:00
Hongming Wang
5caefc5909
Merge pull request #532 from Molecule-AI/fix/issue-450-csp-nonce
fix(canvas): nonce-based CSP replaces unsafe-inline/unsafe-eval in production
2026-04-16 14:15:12 -07:00
Molecule AI Frontend Engineer
d13e3935a9 fix(canvas): replace unsafe-inline/unsafe-eval with nonce-based CSP (#450)
Removes 'unsafe-inline' and 'unsafe-eval' from script-src in the
production Content-Security-Policy, replacing them with a per-request
nonce + 'strict-dynamic'. This closes the XSS gap reported in #450
where the CSP header gave false assurance.

Key decisions:
- 'strict-dynamic' propagates nonce trust to Next.js dynamic chunk
  imports — no need to enumerate every chunk URL
- style-src retains 'unsafe-inline': React Flow writes inline style=""
  attributes for node positioning which cannot be nonce'd, and CSS
  injection is accepted as significantly lower risk than script injection
- Dev mode keeps the permissive policy so HMR/fast-refresh keep working
- buildCsp() is exported for unit testing (21 tests added)

Additional hardening in production CSP:
  object-src 'none', base-uri 'self', frame-ancestors 'none',
  upgrade-insecure-requests, connect-src limited to wss: (not ws:)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 20:35:27 +00:00
Molecule AI Frontend Engineer
b109a569ac feat(canvas): hermes provider picker in CreateWorkspaceDialog (#493)
When the user sets template="hermes", surface a provider dropdown
(15 providers, defaulting to anthropic) and a masked API key input.
On submit the chosen key is sent as `secrets: { [ENV_VAR]: key }` so
the backend can persist it encrypted before the container boots,
fixing the silent preflight failure reported in #493.

- Adds HERMES_PROVIDERS constant (exported for tests)
- Validates API key presence before POST when template is hermes
- Uses violet accent to visually distinguish the hermes section
- 11 new unit tests covering picker visibility, default, env-var
  mapping, validation, and POST payload shape

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 20:25:58 +00:00
Hongming Wang
96b909b8f3 fix: code review findings — token UI, auth hardening, WS dedup
1. Settings panel: wire TokensTab into "API Tokens" tab (was imported
   but not rendered). Rename "API Keys" → "Secrets", add "API Tokens"
   tab. Fix docs link → doc.moleculesai.app/docs/tokens.

2. Referer match hardening: require exact host match or trailing slash
   to prevent evil.com subdomain bypass. Cache CANVAS_PROXY_URL at
   init time instead of per-request os.Getenv.

3. Extract shared deriveWsBaseUrl() to lib/ws-url.ts — eliminates
   duplicate 12-line derivation in socket.ts and TerminalTab.tsx.

4. Token list pagination: add ?limit= and ?offset= params (default
   50, max 200) to GET /workspaces/:id/tokens.

507/507 canvas tests pass, Go build + vet clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:42:26 -07:00
Hongming Wang
25bd9241d1 fix(tenant): WebSocket URL derivation + AdminAuth same-origin for tenant image
Two bugs on the combined tenant image (canvas + API same-origin):

1. WebSocket URL: NEXT_PUBLIC_WS_URL="" (empty string for same-origin)
   was preserved by ?? operator, producing an invalid WS URL. Now derives
   from window.location when both env vars are empty. Same fix applied
   to TerminalTab.

2. AdminAuth blocking canvas: same-origin requests have no Origin header,
   so neither AdminAuth nor CanvasOrBearer could authenticate the canvas.
   Added isSameOriginCanvas() that checks Referer against request Host,
   gated behind CANVAS_PROXY_URL (only active on tenant image). This
   lets the canvas create/list workspaces, view events, etc. without a
   bearer token when served from the same Go process.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:43:01 -07:00
Hongming Wang
2049870057 fix(canvas): address all code review findings on PR #482
- Reconcile TIER_CONFIG/TIER_COLORS into single TIER_CONFIG with both
  `color` (pill style) and `border` (bordered badge style) fields
- Remove TemplatePalette alias indirection (TIER_LABELS_SHARED → direct import)
- Extract inline spinner SVGs to shared Spinner component (3 copies → 1)
- Migrate status dot colors from 6 remaining files to shared tokens:
  SearchDialog, StatusDot, Legend, ContextMenu, Toolbar + add statusDotClass()
- Add COMM_TYPE_LABELS to design-tokens, used by CommunicationOverlay sr-only
- Update reduced-motion tests: components that delegate to design-tokens
  pass the guard check via import detection; add design-tokens.ts own test
- 507/507 tests pass, build clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:48:47 -07:00
Hongming Wang
cd30430979 fix(canvas): UX improvements — shared tokens, focus rings, loading spinners, a11y
- Extract STATUS_CONFIG, TIER_CONFIG, TIER_COLORS to shared design-tokens.ts
  (eliminates 3 duplicate definitions across WorkspaceNode, EmptyState, TemplatePalette)
- Add focus-visible:ring-2 ring-blue-500 to WorkspaceNode, SidePanel tabs,
  EmptyState buttons, TemplatePalette buttons (keyboard navigation now visible)
- Replace "Loading..." text with animated spinner SVG in EmptyState,
  TemplatePalette sidebar, and OrgTemplatesSection
- Add disabled:cursor-not-allowed + suppress hover styling when disabled
  on EmptyState template buttons and TemplatePalette deploy buttons
- Brighten SidePanel tab hover from bg-zinc-800/20 to bg-zinc-800/40
  and text from zinc-300 to zinc-200
- Add screen reader labels to CommunicationOverlay directional arrows
  and status icons (sr-only text for "sent", "received", "to", status)

Fixes #422, #424, #427

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 07:35:44 -07:00
Hongming Wang
1a85ca7656 fix(canvas): template layout + org card styling
- Wider modal (max-w-2xl), 3-col grid, no max-height clipping
- Org template cards: violet→blue, consistent rounded-xl styling
- Container scrolls vertically instead of cutting off
2026-04-16 06:42:41 -07:00
Canvas Agent
66fd7c3ccf fix(a11y): WCAG ARIA fixes for time-sensitive components (Fixes #Fix1/#Fix2/#Fix3)
- ApprovalBanner: add role="alert" aria-live="assertive" aria-atomic="true" to
  each pending approval card; aria-hidden="true" on decorative ⚠ icon span
- TerminalTab: add role="status" aria-live="polite" to connection status bar;
  add role="alert" to inline error message div
- BundleDropZone: extract shared processFile(); add hidden <input type="file">
  with id/accept/aria-label; add sr-only focus:not-sr-only keyboard trigger
  button; add role="status" aria-live="polite" to result toast

Tests: 7 new assertions in aria-time-sensitive.test.tsx covering all 3 fixes
(496/496 pass, build clean)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Canvas Agent
477b6c06b7 fix(canvas): fitView on new workspace provision — respects user zoom level (#426)
Replace setCenter(x, y, {zoom:1}) with fitView({nodes:[{id}]}) in the
molecule:pan-to-node handler (Canvas.tsx). The old implementation forced
zoom=1 regardless of the user's current zoom level, which was jarring when
panned/zoomed away. fitView adapts to whatever zoom the user had and
gracefully fits the new node in view.

Tests:
- Canvas.pan-to-node.test.tsx: fitView called with correct nodeId after
  100ms debounce; debounce coalesces rapid successive events.
- canvas-events-pan.test.ts: molecule:pan-to-node dispatched for new
  provisions only, NOT on restart of an existing node.

Fixes #426.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 05:40:40 -07:00
Hongming Wang
f308765529
Merge pull request #447 from Molecule-AI/fix/canvas-dark-theme-a11y-sweep
fix(canvas): UIUX Cycle 15 dark-theme & a11y sweep (C1–C5, A1–A4, F1, M1)
2026-04-16 05:21:10 -07:00
Hongming Wang
f3dffbba8b
Merge pull request #443 from Molecule-AI/fix/issue-430-authgate-blank-flash
fix(canvas): replace AuthGate null loading state with zinc-950 backdrop
2026-04-16 05:16:53 -07:00
Canvas Agent
c33b59a93a fix(canvas): QA blockers — ChatTab aria-controls, AuthGate test, CommunicationOverlay status icons
BLOCKER 1 (ChatTab.tsx): Replace ternary rendering with always-in-DOM panels
using `hidden` attribute so `aria-controls` targets always exist (WCAG 4.1.2).
Add `id` to tab buttons for `aria-labelledby` back-reference. Non-blocking:
change `key={i}` → `key={line + i}` on activity log items.

BLOCKER 2 (AuthGate.test.tsx): Create test file asserting the loading state
renders a `.bg-zinc-950.fixed.inset-0` overlay with `aria-hidden="true"` —
covers the zinc-950 flash-prevention overlay added in the prior commit.

BLOCKER 3 (CommunicationOverlay.tsx): Add `aria-hidden="true"` to the status
icon span so decorative glyphs (✓ ✕ ⏱) are not announced by screen readers.

Tests: 490/490 passing. Build: clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:53:52 +00:00
Canvas Agent
c936b451a9 fix(canvas): C1/C2/C3/C5 dark-theme CSS and ReactFlow colorMode 2026-04-16 10:45:16 +00:00
Canvas Agent
966920355a fix(canvas): persist SidePanel width to localStorage (issue #425)
Width was initialized to 480px on every render, so clicking a different
workspace node (which re-mounts SidePanel) discarded any resize the user
had done.

Fix:
- localStorage-backed useState initializer (SSR-safe typeof window guard)
- Validates the stored value: must be a finite integer ≥ 320px
- Persists the width in the mouseUp handler via a widthRef that stays in
  sync with the live drag value — avoids spamming localStorage on every
  pixel during the drag
- Extra guard: onMouseUp bails early if not actually dragging (prevents
  spurious saves on unrelated window mouseup events)
- Named constants replace magic numbers 480 / 320

Tests: 5 new cases in SidePanel.tabs.test.tsx — default fallback, valid
saved value, too-small saved value, NaN saved value, drag-persist roundtrip.

Closes #425

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:40:08 +00:00
Canvas Agent
28f3e33581 fix(canvas): UIUX Cycle 15 dark-theme & a11y sweep (C1-C5, A1-A4, F1, M1)
- C4: OnboardingWizard skip button — aria-label + text-zinc-400 (was zinc-600)
- A1+M1: CommunicationOverlay — aria-label on both icon buttons, aria-hidden
  on decorative arrow glyphs (↗↙ toggle, ✕ close, → comms rows)
- A2: ChatTab sub-tab bar — ARIA roving tabIndex + ArrowLeft/ArrowRight
  keyboard navigation (role=tablist/tab already present)
- A4: SearchDialog search input — focus-visible:ring-2 ring-blue-500 replaces
  bare focus:outline-none so keyboard focus is visible
- F1: AuthGate loading state — zinc-950 full-screen backdrop instead of null
  (prevents white flash on SaaS tenant load)
- A3: SidePanel tab bar — wrap in relative container + right-edge fade
  gradient so truncated tabs are visually signalled

C2 (settings-panel.css input backgrounds) and C3 (Canvas.tsx colorMode="dark")
were already in place; verified by code audit before this commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:35:32 +00:00
Canvas Agent
eb391cf429 fix(canvas): replace AuthGate null loading state with zinc-950 backdrop
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>
2026-04-16 10:30:24 +00:00
Hongming Wang
4ff5a6d12f
Merge pull request #409 from Molecule-AI/fix/use-client-ui-components
fix(next): add missing 'use client' to TestConnectionButton and KeyValueField
2026-04-16 03:08:24 -07:00
Hongming Wang
8d523633f8
Merge pull request #405 from Molecule-AI/fix/wcag-zinc600-smalltext-sweep
fix(wcag): sweep text-zinc-600→zinc-500 on small-text labels across 9 components
2026-04-16 03:08:17 -07:00
Hongming Wang
b8cb14f46e feat(tenant): combined platform + canvas Docker image with reverse proxy
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>
2026-04-16 02:46:47 -07:00
Canvas Agent
eaa6975967 fix(next): add missing 'use client' to TestConnectionButton and KeyValueField
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>
2026-04-16 09:10:22 +00:00
Canvas Agent
d6e9fbe984 fix(a11y): raise TeamMemberChip label text 7px→9px in WorkspaceNode
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>
2026-04-16 09:06:56 +00:00
UIUX Designer
a4350121dd fix(wcag): sweep text-zinc-600→zinc-500 across 9 components with small text
zinc-600 on zinc-900/950 background ≈ 2.6:1 contrast (WCAG AA requires
4.5:1 for text under 18pt). Found 15 instances across 9 components where
small-text data labels used this low-contrast pairing.

Files and what they label:
  EmptyState.tsx:132     — skill count + model on template cards (new-user visible)
  SidePanel.tsx:230      — workspace ID in panel footer (copyable, functional)
  ActivityTab.tsx:210    — entry timestamp (8px)
  ActivityTab.tsx:214    — expand chevron affordance (9px)
  ActivityTab.tsx:236    — "→" direction arrow between agents (9px)
  ActivityTab.tsx:278    — entry ID (8px, font-mono)
  ScheduleTab.tsx:284    — empty-state description text (9px)
  ScheduleTab.tsx:320    — schedule prompt preview (9px, truncate)
  ScheduleTab.tsx:323    — last/next/run-count metadata row (8px)
  SkillsTab.tsx:380      — "Examples" section header (9px uppercase)
  TracesTab.tsx:132      — trace ID (8px, font-mono)
  AgentCommsPanel.tsx:166 — message timestamp (9px)
  secrets-section.tsx:59  — secret key name (9px, font-mono)
  secrets-section.tsx:308 — encryption notice (9px)
  MissingKeysModal.tsx:175 — missing key identifier (9px, font-mono)

Fix: zinc-600 → zinc-500 across all 15 instances. Purely cosmetic —
no logic, no layout, no interactive behaviour changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 07:53:00 +00:00
Hongming Wang
fd86e404ee
Merge pull request #392 from Molecule-AI/fix/wcag-node-and-traces-text
Code review passed:
- WorkspaceNode.tsx: 3 × text-[7px]→text-[9px] on status badge, active-task count, currentTask banner — correct targets; decorative text-[7px] (tier badges, skill overflow +N, Team header, font-mono) correctly left unchanged
- TracesTab.tsx: 2 × text-zinc-600→text-zinc-500 on token count + expand chevron — correct; line 72 empty-state dim label correctly left at zinc-600
- 485/485 tests pass with changes applied
- Pure class-string changes, no logic affected
LGTM — merging.
2026-04-16 00:33:59 -07:00
UIUX Designer
a02780e979 fix(wcag): bump WorkspaceNode status/task labels 7px→9px, TracesTab zinc-600→zinc-500
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>
2026-04-16 07:25:34 +00:00
Hongming Wang
bab145b520
fix(canvas): replace nodes.length grid index with monotonic sequence counter (#388)
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>
2026-04-16 00:25:33 -07:00
Hongming Wang
98e7d90213
fix(canvas): show all templates in EmptyState grid, not just first 6 (#387)
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>
2026-04-16 00:19:24 -07:00
Canvas Agent
c260e679d1 fix(canvas): replace nodes.length grid index with monotonic sequence counter
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>
2026-04-16 07:18:11 +00:00
Hongming Wang
f31051be14
fix(a11y): raise ChannelsTab help text from 9px to 11px minimum (#382)
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>
2026-04-15 23:47:36 -07:00
Hongming Wang
a205c92428
fix(security): scope PausePollersForToken to requesting workspace (closes #329)
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.
2026-04-15 21:22:50 -07:00
Hongming Wang
64df8eeb84
Merge pull request #251 from Molecule-AI/feat/cookie-consent-banner
feat(canvas): cookie consent banner
2026-04-15 13:49:53 -07:00
Hongming Wang
cdf9f6de2d feat(canvas): /pricing route with plan selector + Stripe checkout
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>
2026-04-15 13:41:44 -07:00