forked from molecule-ai/molecule-core
ab32e47953
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9eb22333a5 |
fix(deploy-modal): snap provider radio when model resolves to a provider
The TemplatePalette deploy modal (MissingKeysModal → ProviderPickerModal) let the model field and provider radio drift apart. When a hermes template defaulted the model to "MiniMax-M2.7-highspeed" but the radio defaulted to providers[0] (Anthropic), the env-var input below asked for ANTHROPIC_API_KEY. A user pasting their MINIMAX_API_KEY there (or just dismissing the dialog) ended up with a workspace whose runtime_config.model=MiniMax + ANTHROPIC_API_KEY env — the hermes adapter then crashed during boot before /registry/register, surfacing as WORKSPACE_PROVISION_FAILED 12 minutes later. Caught 2026-05-02 on hongming/Hermes Agent (workspace 95ed3ff2-… ended with: "container started but never called /registry/register"). Sibling of the ConfigTab cascade fix in PR #2516 (task #236) — same pattern, different surface. Plumbs the template's full ModelSpec[] (with required_env per model) into the picker. When the typed model matches a registry entry, snap the radio so the env-var fields underneath match what the model actually needs. Free-text models (typed slug not in the registry) and models with no required_env (local/self-hosted endpoints) leave the radio alone — the user can still pick a provider manually. Backwards-compat: callers that don't pass `models` get the pre-cascade behavior, pinned by a regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9abc9a0487 |
feat(canvas): always prompt provider+model on template deploy
Previously the picker modal opened only when preflight failed OR the template offered ≥2 provider options. Single-provider templates with saved keys (claude-code, langgraph) deployed silently using the template's compiled-in default model — denying the user a final chance to override before an EC2 boots and burns billing on the wrong tier. The picker UI already supports the "all-keys-saved single-provider" case as a confirm-only prompt (provider radio is hidden, model input is pre-filled with template.model), so flipping shouldShowPicker to unconditional is a one-line change with the picker UX absorbing it. Test plan - Existing "single-provider skips picker when preflight.ok" regression guard inverted to assert picker always opens. - Three happy-path tests refactored to drive through the picker via a new deployThroughPicker helper instead of expecting an immediate POST. - POST-failure tests likewise refactored — the failure now surfaces through the picker click-through path, not the direct deploy() call. - 15/15 tests pass; deploy-preflight.test.ts unchanged + 20/20. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3ba924d174 |
review: drop destructive Override + single-fetch configuredKeys
Self-review of #2460 found two issues: 1. Critical: Override button in ProviderPickerModal called /settings/secrets when no workspaceId, overwriting the GLOBAL secret used by every workspace. The only consumers of this modal today (TemplatePalette, EmptyState via useTemplateDeploy) never pass workspaceId, so Override was always destructive. Removed entirely — the picker still solves the user-reported bug (always-ask + reuse saved keys); per-workspace key override can be a separate PR that plumbs secrets through POST /workspaces. 2. Optional: /settings/secrets was being fetched twice — once inside checkDeploySecrets (silently) and again in the hook to populate configuredKeys. Surfaced configuredKeys on PreflightResult so the hook re-uses the existing fetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0608e15ab3 |
feat(canvas): always prompt for provider+model on multi-provider template deploy
Clicking a hermes template tile silently deployed when global env covered the API key, producing "No LLM provider configured" 500 because the workspace booted with no explicit model slug — the adapter fell back to its compiled-in default which 401s on the user's actual provider key. Fix: in useTemplateDeploy, open the picker whenever the template declares ≥2 provider options, even when preflight.ok=true. The modal renders pre-saved keys as Saved (with an Override link) and adds a model input pre-filled from the template's default. Single- provider templates (claude-code, langgraph) still skip the picker since there's nothing to choose. POST /workspaces now includes the picker's model slug so hermes- style routing reads the prefix at install time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4028b81e04 |
refactor(canvas): route panel WS subscriptions through global socket
Both AgentCommsPanel and ChatTab's activity-feed opened raw
`new WebSocket(WS_URL)` instances per mount, with no onclose handler
and no reconnect logic. When the underlying connection dropped — idle
timeout, browser background-tab throttle, network jitter — the per-
panel sockets stayed dead until the panel re-mounted (refresh or
sub-tab unmount/remount). Live agent-comms bubbles and live activity
feed lines silently went missing in the gap, manifesting as "the
delegation didn't show up until I refreshed."
The global ReconnectingSocket in store/socket.ts already owns
reconnect, exponential backoff, health-check, and HTTP fallback poll.
Routing component subscribers through it gives every consumer those
guarantees for free, with one TCP connection per tab instead of N.
Three new pieces:
- store/socket-events.ts: tiny pub/sub bus. emitSocketEvent fan-outs
every decoded WSMessage to the listener Set; subscribeSocketEvents
returns an unsubscribe. A throwing listener is logged and isolated
so it can't break siblings.
- store/socket.ts: ws.onmessage now calls emitSocketEvent(msg) right
after applyEvent(msg), so the store's derived state and component
subscribers stay in lockstep on every event arrival.
- hooks/useSocketEvent.ts: React hook that registers exactly once
per mount, capturing the latest handler in a ref so the closure
sees current state/props without re-subscribing on every render.
Refactored sites:
- AgentCommsPanel: replaced its WebSocket-in-useEffect block with
useSocketEvent. Same parsing logic; the panel no longer opens its
own connection.
- ChatTab activity feed: split the previous useEffect in two — one
seeds the activity log when `sending` flips, the other subscribes
unconditionally and gates work on `sending` inside the handler.
Hooks can't be conditional, so the gate has to live in the body
rather than around the effect.
The ws-close graceful-close helper is no longer needed in either
site; the global socket owns its own teardown.
Tests: 6 new tests for the bus contract (single delivery, fan-out
order, unsubscribe, throwing-listener isolation, no-subscriber emit,
duplicate-subscribe Set semantics). All 27 existing socket tests
still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a9d2d46682 |
test(canvas): unit tests for useTemplateDeploy (#2071)
[Molecule-Platform-Evolvement-Manager] Closes the first item from #2071 (Canvas test gaps follow-up): adds behavioural coverage for the shared template-deploy hook that both TemplatePalette (sidebar) and EmptyState (welcome grid) drive. 10 cases across 4 buckets: **Happy path (4):** - preflight ok → POST /workspaces → onDeployed fires with new id - caller-supplied canvasCoords flows into the POST body - default coords fall in [100,500) × [100,400) when canvasCoords omitted - template.runtime is preferred over the resolveRuntime fallback (locks the deduped-fallback table contract added in #2061) **Preflight failures (2):** - network throw sets error AND clears `deploying` (regression test for the "stranded button" bug called out in the SUT's inline comment — drop the try block and you'll fail this test) - not-ok-with-missing-keys opens the modal without firing POST **Modal lifecycle (2):** - 'keys added' click retries POST without re-running preflight (verifies the executeDeploy / deploy split — preflight call count stays at 1, POST count goes to 1) - 'cancel' click closes modal without firing POST **POST failures (2):** - Error rejection surfaces the message - non-Error rejection surfaces the "Deploy failed" fallback Mocks `@/lib/api`, `@/lib/deploy-preflight`, and `@/components/MissingKeysModal` (stand-in component exposes the two callbacks as test-id buttons — the real radix modal is irrelevant to this hook's behavior). Test file follows the `vi.hoisted` + import-after-mocks pattern from `canvas/src/app/__tests__/orgs-page.test.tsx`. ## Test plan - [x] All 10 cases pass locally (`vitest run useTemplateDeploy.test.tsx`) - [x] No changes to the SUT — pure additive coverage - [ ] CI green Follow-ups for the rest of #2071 (separate PRs): - A2AEdge rendering + click-to-select-source - OrgCancelButton cancel flow + optimistic state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5adc8a74d5 |
feat(canvas+org): env preflight, EmptyState parity, shared useTemplateDeploy hook
Builds on #2061. Three internally-cohesive sub-features; easiest to read in order. ## 1. Org-level env preflight Server - `OrgTemplate` + `OrgWorkspace` gain `required_env: string[]` and `recommended_env: string[]` YAML fields. - `GET /org/templates` walks the tree and returns the tree-union (deduped, sorted) of both. `collectOrgEnv` dedup prefers required when the same key is declared at both tiers. - `POST /org/import` preflights against `global_secrets` WHERE `octet_length(encrypted_value) > 0` (empty-value rows used to be counted as "configured" and the per-container preflight still failed at start time). 412 Precondition Failed + `missing_env` list when required keys are absent. `force=true` bypasses with an audit log line. DB lookup failure now returns 500 (was: silent fall-through that defeated the guard). Env-var NAMES validated against `^[A-Z][A-Z0-9_]{0,127}$` so a malicious template can't ship pathological names into the UI or DB. Canvas - New `OrgImportPreflightModal`: red "Required" section (blocking) and yellow "Recommended" section (non-blocking, import stays enabled, shows live missing-count next to the Import button). - Per-key password input → `PUT /settings/secrets` → strike-through on save. Functional `setDrafts` throughout (no stale-closure clobbers on rapid successive saves). `useEffect` seed keyed on a sorted-join string signature so a parent re-render with a new array identity doesn't clobber typed inputs. - `TemplatePalette.handleImport` branches: zero env declarations → straight to import; any declarations → fetch configured global secret keys, open the modal. Tests (Go): `TestCollectOrgEnv_*` (5) cover union-across-levels, required-wins-over-recommended (including same-struct), dedup, empty, invalid-name rejection. ## 2. EmptyState parity with TemplatePalette The "Deploy your first agent" grid used to call `POST /workspaces` with no preflight while the sidebar palette ran `checkDeploySecrets` + `MissingKeysModal` first. Same template deployed two different ways → first-run users saw containers boot in `failed` state without guidance. Now both surfaces share one preflight + modal handshake. EmptyState's previous `interface Template` dropped `runtime`, `models`, and `required_env` — silently discarding exactly the fields the preflight needs. `Template` now lives in `deploy-preflight.ts` and is imported from there by both surfaces. ## 3. useTemplateDeploy hook With the preflight + modal wiring now duplicated across EmptyState + TemplatePalette + (going forward) any third surface, extracted the pattern into `canvas/src/hooks/useTemplateDeploy.tsx`: const { deploy, deploying, error, modal } = useTemplateDeploy({ canvasCoords: ..., // optional, default random onDeployed: (id) => ..., }); Closes three drift surfaces that the duplication had created: - `resolveRuntime` id→runtime fallback table (moved to `deploy-preflight.ts`). EmptyState had a narrower fallback that would have silently disagreed with the palette on any future id needing a non-identity mapping. - `checkDeploySecrets` call signature. One owner. - `MissingKeysModal` JSX wiring. One owner. Narrow try/catch around `checkDeploySecrets` so a preflight network failure clears `deploying` and surfaces via `setError` instead of stranding the button forever. `modal: ReactNode` (not a `renderModal()` function) — the previous memoization bought nothing since consumers called it inline every render. Named `MissingKeysInfo` interface for the state shape. ## 4. Viewport auto-fit user-pan gate fix During org deploy the canvas was meant to pan+zoom to follow each arriving workspace (`molecule:fit-deploying-org` event → debounced fitView). In practice the fit stayed stuck on wherever the first fit landed. Root cause: React Flow v12 fires `onMoveEnd` with a truthy `event` at the END of a programmatic `fitView` animation. The original "respect-user-pan" gate stamped `userPannedAtRef` in `onMoveEnd`, so our own fit completing looked like a user pan, and every subsequent auto-fit short-circuited for the rest of the deploy. Fix: stop trusting `onMoveEnd` for user-intent detection. Register explicit `wheel` + `pointerdown` listeners on `document` with capture phase and `target.closest('.react-flow__pane')` filter. Capture-phase immunity to `stopPropagation`; pane-filter rejects toolbar / modal / side-panel clicks (the old `window` fallback caught those). `onMoveEnd` simplified to only drive the debounced viewport save. Also: fit event dispatched on root arrivals (not just children), so the canvas centers on the just-landed root immediately instead of waiting ~2s for the first child. Animation 600ms → 400ms so successive per-arrival fits don't pile up visually. End-state fit stays at 1200ms — intentional asymmetry ("settling" vs "tracking"), documented in code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
24fec62d7f |
initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1) with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo. Brand: Starfire → Molecule AI. Slug: starfire / agent-molecule → molecule. Env vars: STARFIRE_* → MOLECULE_*. Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform. Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent. DB: agentmolecule → molecule. History truncated; see public repo for prior commits and contributor attribution. Verified green: go test -race ./... (platform), pytest (workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |