Commit Graph

86 Commits

Author SHA1 Message Date
Molecule AI Frontend Engineer
dc5f74231d fix(canvas): add role=alert to deploy error in EmptyState
WCAG 1.3.1 / 4.1.3: the error div that appears after a failed workspace
deploy or blank-workspace create had no ARIA live region, so screen
readers never announced it. Adding role="alert" makes the message an
implicit aria-live="assertive" region so assistive technology surfaces
the error immediately without requiring the user to navigate to it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 21:16:14 +00:00
molecule-ai[bot]
6c6e703ec9 feat(canvas): semantic search UI for memory inspector (issue #783)
Adds a debounced (300ms) search input to MemoryInspectorPanel with
?q= fetch, similarity_score% badges, skeleton rows during re-fetches,
search-specific empty state, and an immediate-reset clear button.
Tests: 722 passing (+4 new: debounce, badge present/absent, clear).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 17:04:33 +00:00
3915e2b9e8 fix(gate-conflict): merge main into feat/issue-753-audit-trail-panel
Resolves 4 merge conflicts: Toolbar.tsx (2), Canvas.a11y.test.tsx (1),
Canvas.pan-to-node.test.tsx (1). All conflicts were additive — PR adds
selectedNodeId/setPanelTab selectors and the Audit toolbar button; main
didn't have them. Took PR additions throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:39:12 +00:00
Hongming Wang
64776f0bed 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]
6d530bfd51 feat(canvas): audit trail visualization panel (issue #753)
- AuditTrailPanel SidePanel tab showing the workspace audit ledger from
  GET /workspaces/:id/audit with cursor-based pagination (?cursor=, ?limit=50)
- Color-coded event-type badges: delegation=blue-500, decision=violet-500,
  gate=yellow-500, hitl=orange-500
- chain_valid=false renders red tamper warning indicator
- Event-type filter bar (All / Delegation / Decision / Gate / HITL) resets
  pagination and reloads with ?event_type= param
- Relative timestamps refreshed every 30 s without re-fetching
- Empty state with icon and descriptive copy
- Toolbar Audit button (ledger icon) switches panel to audit tab for
  selected workspace, or shows toast if no workspace is selected
- 29 new unit tests across formatAuditRelativeTime, AuditEntryRow, and
  AuditTrailPanel component integration suites
- Update SidePanel.tabs.test.tsx for 13-tab count and audit as last tab
- Add setPanelTab to Canvas test store mocks (Toolbar now reads it)

Closes #753

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:03:28 +00:00
molecule-ai[bot]
2aea674747 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]
79f806e2d3 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
3cb21b4bd5 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
99d5ef6866 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
0adf5d5a5f 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
bb18f79343 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
6b5248e173 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
6668a95d6f 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
e22d2d2257 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 e70bb94 — 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
b522484e2e 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
07240e7095 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
bfe4f65c92 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]
6c72958e80 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
1e0c2eed3b 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
6cecf44f45 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
d4696a5977 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]
1225e9429d 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
7871fd7251 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
81c26b9fdd 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
f936af3181 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
54bb543ff7 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
071fb0da88 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
a4b1886c35 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
ac6d6ce5bd 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
04c1b16871 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
120b886d00 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
242306d239 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
dda82f5de4 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
65a98d5206 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
43c99e0af7 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
4d0f2d4c79 fix(canvas): C1/C2/C3/C5 dark-theme CSS and ReactFlow colorMode 2026-04-16 10:45:16 +00:00
Canvas Agent
026921ae62 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
3daa9083b8 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
29d8f18952 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
f5b81710df 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
3fd9594c11 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
a363b56f25 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
04df479e4f 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
0a0eab6657 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
e810986f44 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
1c92b46bfc 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
3783451bc3 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
8dbd7efc1a 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
8d70d8e7e6 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
2a68e55401 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