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>
166 lines
3.8 KiB
CSS
166 lines
3.8 KiB
CSS
@import "xterm/css/xterm.css";
|
|
/* Theme tokens MUST load before any feature stylesheet that
|
|
references them so custom properties are in scope. */
|
|
@import "../styles/theme-tokens.css";
|
|
@import "../styles/settings-panel.css";
|
|
@import "../styles/org-deploy.css";
|
|
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
background: #09090b;
|
|
color: #e4e4e7;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
/* React Flow overrides for dark theme */
|
|
.react-flow__edge-path {
|
|
stroke: #3f3f46 !important;
|
|
stroke-width: 1.5 !important;
|
|
}
|
|
|
|
.react-flow__edge.animated .react-flow__edge-path {
|
|
stroke-dasharray: 5;
|
|
animation: dashdraw 0.5s linear infinite;
|
|
}
|
|
|
|
@keyframes dashdraw {
|
|
to {
|
|
stroke-dashoffset: -10;
|
|
}
|
|
}
|
|
|
|
.react-flow__handle {
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.react-flow__node {
|
|
/* Transform transition drives the "spawn from parent" motion —
|
|
org-deploy sets the node's initial position to the parent's
|
|
absolute coords, then repositions to the real slot, and this
|
|
transition interpolates the translate() in between.
|
|
Non-deploy workspace moves (drag, nest) get the same smoothing
|
|
for free. */
|
|
transition:
|
|
box-shadow var(--mol-duration-fast) ease,
|
|
transform var(--mol-duration-spawn) var(--mol-easing-bounce-out);
|
|
}
|
|
/* Drag events must feel instant — React Flow adds this class
|
|
for the lifetime of the gesture. */
|
|
.react-flow__node.dragging {
|
|
transition: box-shadow var(--mol-duration-fast) ease;
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #3f3f46;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #52525b;
|
|
}
|
|
|
|
/* Selection */
|
|
::selection {
|
|
background: rgba(59, 130, 246, 0.3);
|
|
}
|
|
|
|
/* Panel slide animation */
|
|
@keyframes slide-in-from-right {
|
|
from { transform: translateX(100%); }
|
|
to { transform: translateX(0); }
|
|
}
|
|
|
|
@keyframes slide-in-from-bottom {
|
|
from { transform: translateY(10px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
.slide-in-from-bottom {
|
|
animation-name: slide-in-from-bottom;
|
|
}
|
|
|
|
@keyframes slide-in-from-top {
|
|
from { transform: translateY(-10px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
.slide-in-from-top {
|
|
animation-name: slide-in-from-top;
|
|
}
|
|
|
|
.animate-in {
|
|
animation-fill-mode: both;
|
|
}
|
|
|
|
.slide-in-from-right {
|
|
animation-name: slide-in-from-right;
|
|
}
|
|
|
|
.duration-200 {
|
|
animation-duration: 200ms;
|
|
}
|
|
|
|
/* Subtle node entrance */
|
|
@keyframes node-appear {
|
|
from { opacity: 0; transform: scale(0.9); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
.react-flow__node {
|
|
animation: node-appear 0.3s ease-out;
|
|
}
|
|
|
|
/* ============================================================
|
|
REDUCED MOTION — WCAG 2.3.3
|
|
Disable all animations/transitions for users who prefer it.
|
|
============================================================ */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
|
|
/* Custom slide-in / animate-in classes used by Toaster & ApprovalBanner */
|
|
.animate-in,
|
|
.slide-in-from-bottom,
|
|
.slide-in-from-top,
|
|
.slide-in-from-right {
|
|
animation: none !important;
|
|
transform: none !important;
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
/* React Flow animated edges */
|
|
.react-flow__edge.animated .react-flow__edge-path {
|
|
animation: none !important;
|
|
stroke-dasharray: none !important;
|
|
}
|
|
|
|
/* React Flow node appear animation */
|
|
.react-flow__node {
|
|
animation: none !important;
|
|
}
|
|
}
|