forked from molecule-ai/molecule-core
Session's accumulated UX work across frontend and platform. Reviewable in four logical sections — diff is large but internally cohesive (each section fixes a gap the next one depends on). ## Chat attachments — user ↔ agent file round trip - New POST /workspaces/:id/chat/uploads (multipart, 50 MB total / 25 MB per file, UUID-prefixed storage under /workspace/.molecule/chat-uploads/). - New GET /workspaces/:id/chat/download with RFC 6266 filename escaping and binary-safe io.CopyN streaming. - Canvas: drag-and-drop onto chat pane, pending-file pills, per-message attachment chips with fetch+blob download (anchor navigation can't carry auth headers). - A2A flow carries FileParts end-to-end; hermes template executor now consumes attachments via platform helpers. ## Platform attachment helpers (workspace/executor_helpers.py) Every runtime's executor routes through the same helpers so future runtimes inherit attachment awareness for free: - extract_attached_files — resolve workspace:/file:///bare URIs, reject traversal, skip non-existent. - build_user_content_with_files — manifest for non-image files, multi-modal list (text + image_url) for images. Respects MOLECULE_DISABLE_IMAGE_INLINING for providers whose vision adapter hangs on base64 payloads (MiniMax M2.7). - collect_outbound_files — scans agent reply for /workspace/... paths, stages each into chat-uploads/ (download endpoint whitelist), emits as FileParts in the A2A response. - ensure_workspace_writable — called at molecule-runtime startup so non-root agents can write /workspace without each template having to chmod in its Dockerfile. Hermes template executor + langgraph (a2a_executor.py) + claude-code (claude_sdk_executor.py) all adopt the helpers. ## Model selection & related platform fixes - PUT /workspaces/:id/model — was 404'ing, so canvas "Save" silently lost the model choice. Stores into workspace_secrets (MODEL_PROVIDER), auto-restarts via RestartByID. - applyRuntimeModelEnv falls back to envVars["MODEL_PROVIDER"] so Restart propagates the stored model to HERMES_DEFAULT_MODEL without needing the caller to rehydrate payload.Model. - ConfigTab Tier dropdown now reads from workspaces row, not the (stale) config.yaml — fixes "badge shows T3, form shows T2". ## ChatTab & WebSocket UX fixes - Send button no longer locks after a dropped TASK_COMPLETE — `sending` no longer initializes from data.currentTask. - A2A POST timeout 15 s → 120 s. LLM turns routinely exceed 15 s; the previous default aborted fetches while the server was still replying, producing "agent may be unreachable" on success. - socket.ts: disposed flag + reconnectTimer cancellation + handler detachment fix zombie-WebSocket in React StrictMode. - Hermes Config tab: RUNTIMES_WITH_OWN_CONFIG drops 'hermes' — the adaptor's purpose IS the form, banner was contradictory. - workspace_provision.go auto-recovery: try <runtime>-default AND bare <runtime> for template path (hermes lives at the bare name). ## Org deploy/delete animation (theme-ready CSS) - styles/theme-tokens.css — design tokens (durations, easings, colors). Light theme overrides by setting only the deltas. - styles/org-deploy.css — animation classes + keyframes, every value references a token. prefers-reduced-motion respected. - Canvas projects node.draggable=false onto locked workspaces (deploying children AND actively-deleting ids) — RF's authoritative drag lock; useDragHandlers retains a belt-and- braces check. - Organ cancel button (red pulse pill on root during deploy) cascades via existing DELETE /workspaces/:id?confirm=true. - Auto fit-view after each arrival, debounced 500 ms so rapid sibling arrivals coalesce into one fit (previous per-event fit made the viewport lurch continuously). - Auto-fit respects user-pan — onMoveEnd stamps a user-pan timestamp only when event !== null (ignores programmatic fitView) so auto-fits don't self-cancel. - deletingIds store slice + useOrgDeployState merge gives the delete flow the same dim + non-draggable treatment as deploy. - Platform-level classNames.ts shared by canvas-events + useCanvasViewport (DRY'd 3 copies of split/filter/join). ## Server payload change - org_import.go WORKSPACE_PROVISIONING broadcast now includes parent_id + parent-RELATIVE x/y (slotX/slotY) so the canvas renders the child at the right parent-nested slot without doing any absolute-position walk. createWorkspaceTree signature gains relX, relY alongside absX, absY; both call sites updated. ## Tests - workspace/tests/test_executor_helpers.py — 11 new cases covering URI resolution (including traversal rejection), attached-file extraction (both Part shapes), manifest-only vs multi-modal content, large-image skip, outbound staging, dedup, and ensure_workspace_writable (chmod 777 + non-root tolerance). - workspace-server chat_files_test.go — upload validation, Content-Disposition escaping, filename sanitisation. - workspace-server secrets_test.go — SetModel upsert, empty clears, invalid UUID rejection. - tests/e2e/test_chat_attachments_e2e.sh — round-trip against a live hermes workspace. - tests/e2e/test_chat_attachments_multiruntime_e2e.sh — static plumbing check + round-trip across hermes/langgraph/claude-code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
7.8 KiB
CSS
152 lines
7.8 KiB
CSS
/**
|
|
* Org-deploy animation module.
|
|
*
|
|
* Loaded globally (see app/globals.css). All values come from
|
|
* theme-tokens.css so a theme swap needs zero edits here.
|
|
*
|
|
* Component contract — canvas/src/components/canvas code adds
|
|
* these classes to the React Flow node / edge wrappers:
|
|
*
|
|
* .mol-deploy-spawn One-shot entry animation on a
|
|
* node that just arrived. Applied
|
|
* by canvas-events.ts for 600 ms
|
|
* then removed.
|
|
* .mol-deploy-shimmer Persistent border shimmer while
|
|
* a node's status === "provisioning".
|
|
* Removed when status flips to
|
|
* "online" / "failed".
|
|
* .mol-deploy-parent-pulse One-shot acknowledgement pulse
|
|
* on the parent when a child lands.
|
|
* Applied for parent-pulse duration
|
|
* then removed.
|
|
* .mol-deploy-locked Applied to every non-root node
|
|
* inside a deploying org so it dims
|
|
* and the cursor signals un-
|
|
* draggable.
|
|
* .mol-deploy-root-complete One-shot pop + glow on the root
|
|
* when the last child comes online.
|
|
*
|
|
* Edges use React Flow edge data to pick styling — see the
|
|
* selectors below the node keyframes.
|
|
*
|
|
* Reduced motion is handled at the bottom via the same guard
|
|
* globals.css already installs for other animations.
|
|
*/
|
|
|
|
/* ────────────────────────────────────────────────────────
|
|
Keyframes — kept terse; values come from variables so
|
|
duplication across themes is nil.
|
|
──────────────────────────────────────────────────────── */
|
|
|
|
@keyframes mol-deploy-spawn {
|
|
/* Gentle fade-in-place. The earlier "spring from parent" motion
|
|
collided with the server-computed grid positions (parent and
|
|
child used different coord origins once the parent was placed
|
|
on the client's grid instead of the template's absolute
|
|
coords), which landed children in wrong slots. Keeping the
|
|
animation to a simple opacity+scale lets the server's layout
|
|
win — and reads as "node arrived" without the over-engineered
|
|
spring. */
|
|
from { opacity: 0; transform: scale(0.85); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
/* mol-deploy-parent-pulse keyframe removed with the effect — the
|
|
box-shadow expanding ring made the parent card visibly "flash" on
|
|
every child arrival when the grow pass also bumped width/height.
|
|
Kept as a deliberate non-class so the theme-tokens vars can drop
|
|
with it on the next theme pass. */
|
|
|
|
@keyframes mol-deploy-root-complete {
|
|
0% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
|
|
40% { transform: scale(var(--mol-deploy-root-scale-peak)); box-shadow: var(--mol-deploy-root-glow); }
|
|
100% { transform: scale(1); box-shadow: 0 0 0 0 transparent; }
|
|
}
|
|
|
|
/* (mol-deploy-edge-draw keyframe removed with the edge effects.) */
|
|
|
|
@keyframes mol-deploy-cancel-pulse {
|
|
0%, 100% { box-shadow: 0 0 0 0 var(--mol-deploy-cancel-ring); }
|
|
50% { box-shadow: 0 0 0 10px transparent; }
|
|
}
|
|
|
|
/* ────────────────────────────────────────────────────────
|
|
Node classes
|
|
──────────────────────────────────────────────────────── */
|
|
|
|
/* Qualify with .react-flow__node so this rule beats the default
|
|
`node-appear` animation defined later in globals.css. Without
|
|
the qualifier, CSS source-order wins and the standard
|
|
node-appear overrides our scale/opacity keyframe, visually
|
|
dropping the "spawn from parent" motion. */
|
|
.react-flow__node.mol-deploy-spawn {
|
|
animation:
|
|
mol-deploy-spawn var(--mol-duration-spawn) var(--mol-easing-bounce-out) both;
|
|
}
|
|
|
|
/* Provisioning signal — the earlier rotating conic-gradient border
|
|
read as distracting "spinner" clutter during a 15-child org
|
|
import (dozens of them spinning simultaneously). A static dim
|
|
(reduced opacity + saturation) communicates "this one is still
|
|
coming online" without the motion noise. The locked-child style
|
|
already uses the same pattern — we reuse the filter values so
|
|
a provisioning ROOT node and a locked CHILD look consistent. */
|
|
.mol-deploy-shimmer {
|
|
filter: saturate(var(--mol-deploy-locked-saturation)) opacity(var(--mol-deploy-locked-opacity));
|
|
transition: filter var(--mol-duration-base) var(--mol-easing-standard);
|
|
}
|
|
|
|
.mol-deploy-locked {
|
|
filter: saturate(var(--mol-deploy-locked-saturation)) opacity(var(--mol-deploy-locked-opacity));
|
|
cursor: not-allowed !important;
|
|
transition: filter var(--mol-duration-base) var(--mol-easing-standard);
|
|
}
|
|
|
|
.react-flow__node.mol-deploy-root-complete {
|
|
animation: mol-deploy-root-complete var(--mol-duration-root-complete) var(--mol-easing-emphasize) both;
|
|
}
|
|
|
|
/* ────────────────────────────────────────────────────────
|
|
Edge classes — intentionally inert.
|
|
|
|
Earlier revisions painted incoming edges with a dashed-blueprint
|
|
→ animated-laser-trace effect as the child landed. User feedback
|
|
on the first demo was "remove connection line effects" — the
|
|
moving dashes read as noise during a multi-child deploy. Keeping
|
|
the class hooks so canvas-events.ts event handlers can still
|
|
apply/strip them without blowing up, but the styling is a no-op
|
|
(edges fall through to the default styling in globals.css).
|
|
If a future demo wants the effect back, wire the rules below.
|
|
──────────────────────────────────────────────────────── */
|
|
|
|
/* ────────────────────────────────────────────────────────
|
|
Cancel-deployment pill — rendered by OrgCancelButton.tsx
|
|
attached to the root node during deploy. Class `.mol-deploy-cancel`
|
|
is always applied; the pulse is additive.
|
|
──────────────────────────────────────────────────────── */
|
|
.mol-deploy-cancel {
|
|
background: var(--mol-deploy-cancel-bg);
|
|
color: var(--mol-deploy-cancel-text);
|
|
transition: background var(--mol-duration-fast) var(--mol-easing-standard);
|
|
}
|
|
.mol-deploy-cancel:hover {
|
|
background: var(--mol-deploy-cancel-bg-hover);
|
|
}
|
|
.mol-deploy-cancel-pulse {
|
|
animation: mol-deploy-cancel-pulse var(--mol-duration-parent-pulse) var(--mol-easing-standard) infinite;
|
|
}
|
|
|
|
/* ────────────────────────────────────────────────────────
|
|
Reduced-motion guard — mirror globals.css's policy so this
|
|
module stays WCAG 2.3.3 compliant without relying on the
|
|
global file being loaded first.
|
|
──────────────────────────────────────────────────────── */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.react-flow__node.mol-deploy-spawn,
|
|
.react-flow__node.mol-deploy-root-complete,
|
|
.mol-deploy-cancel-pulse {
|
|
animation: none !important;
|
|
}
|
|
/* Dim-light signal is already static; no override needed. */
|
|
}
|