mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[eric] ui: fix the windows guide cursor jumping instead of gliding (it was being rebuilt from scratch every frame) and remove the new chat / history pills above the composer; bump 1.1.69
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openswarm",
|
||||
"version": "1.1.68",
|
||||
"version": "1.1.69",
|
||||
"description": "OpenSwarm — AI Agent Orchestrator",
|
||||
"author": "openswarm-ai",
|
||||
"main": "main.js",
|
||||
|
||||
@@ -25,28 +25,39 @@ const stripFramerProps = (props: any) => {
|
||||
};
|
||||
|
||||
// Components that drive position via `animate={{ x, y }}` (ACPopup, ACMultiChoice, etc.) would otherwise lose their layout when the animate prop is stripped, because they have no fallback style.transform. We salvage the latest numeric x/y from animate and apply them as a transform so the div lands in the right place; no animation, just static placement.
|
||||
// One cached component per tag. CRITICAL: without the cache the Proxy getter
|
||||
// returns a NEW forwardRef component on every `motion.div` access, so React
|
||||
// sees a different component type each render and REMOUNTS the DOM node every
|
||||
// time. A freshly-mounted node has no previous transform to ease from, so CSS
|
||||
// transitions never run (getAnimations() stays empty) and the cursor jumps
|
||||
// instantly instead of gliding; the breathing pulse never animates either.
|
||||
// Caching gives each tag a stable identity so React reconciles in place.
|
||||
const tagComponentCache: Record<string, any> = {};
|
||||
const motionShim: any = new Proxy({}, {
|
||||
get: (_target, tag: string) => {
|
||||
return React.forwardRef((props: any, ref: any) => {
|
||||
let translate = '';
|
||||
const a = props.animate;
|
||||
if (a && typeof a === 'object' && !Array.isArray(a)) {
|
||||
const ax = typeof a.x === 'number' ? a.x : null;
|
||||
const ay = typeof a.y === 'number' ? a.y : null;
|
||||
if (ax !== null || ay !== null) {
|
||||
translate = `translate(${ax ?? 0}px, ${ay ?? 0}px)`;
|
||||
if (!tagComponentCache[tag]) {
|
||||
tagComponentCache[tag] = React.forwardRef((props: any, ref: any) => {
|
||||
let translate = '';
|
||||
const a = props.animate;
|
||||
if (a && typeof a === 'object' && !Array.isArray(a)) {
|
||||
const ax = typeof a.x === 'number' ? a.x : null;
|
||||
const ay = typeof a.y === 'number' ? a.y : null;
|
||||
if (ax !== null || ay !== null) {
|
||||
translate = `translate(${ax ?? 0}px, ${ay ?? 0}px)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const stripped = stripFramerProps(props);
|
||||
if (translate) {
|
||||
const existing = stripped.style && stripped.style.transform;
|
||||
stripped.style = {
|
||||
...(stripped.style || {}),
|
||||
transform: existing ? `${existing} ${translate}` : translate,
|
||||
};
|
||||
}
|
||||
return React.createElement(tag, { ...stripped, ref });
|
||||
});
|
||||
const stripped = stripFramerProps(props);
|
||||
if (translate) {
|
||||
const existing = stripped.style && stripped.style.transform;
|
||||
stripped.style = {
|
||||
...(stripped.style || {}),
|
||||
transform: existing ? `${existing} ${translate}` : translate,
|
||||
};
|
||||
}
|
||||
return React.createElement(tag, { ...stripped, ref });
|
||||
});
|
||||
}
|
||||
return tagComponentCache[tag];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,12 @@ const SPRING = { type: 'spring' as const, stiffness: 260, damping: 26 };
|
||||
// On Windows the motionWin shim strips Framer Motion's animate prop, so controls.set({x,y}) never moves the wrapper. We bypass by reading the same store the popups read and applying style.transform directly; Mac is unaffected since Framer's own transform writes win the cascade.
|
||||
const IS_WIN = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows');
|
||||
|
||||
// Windows fallback-ease duration. Mirrors the CSS `transform 420ms` transition
|
||||
// on the cursor wrapper below; on Windows the Director holds for this (plus a
|
||||
// small settle margin) after a moveTo/fadeOut so the CSS ease actually plays
|
||||
// before the next step's instant write lands.
|
||||
const WIN_EASE_MS = 420;
|
||||
|
||||
const AgenticCursor = forwardRef<AgenticCursorHandle>((_props, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const controls = useAnimationControls();
|
||||
@@ -100,7 +106,22 @@ const AgenticCursor = forwardRef<AgenticCursorHandle>((_props, ref) => {
|
||||
async moveTo(x, y, transition) {
|
||||
// Stop prior tracker so it doesn't snap the cursor back to its old anchor mid-animation.
|
||||
stopTrackingInternal();
|
||||
// Order matters and is identical to the pre-Windows version on Mac: Framer's spring drives the popup via onUpdate during the animation, then writePos confirms the final position. On Windows controls.start is the shim no-op so this resolves instantly and writePos (instant=false) hands the CSS transition the target to ease toward.
|
||||
if (IS_WIN) {
|
||||
// Windows has no Framer runtime (controls.start is a no-op); the visual
|
||||
// hop is the CSS transition on the wrapper, driven by cursorStore.
|
||||
// TWO-STEP so Chromium actually animates: (1) commit the eased
|
||||
// transition at the CURRENT position (cursorStore now flushes an
|
||||
// instant-change), let it paint, then (2) move. Changing transform in
|
||||
// the same recalc that flips transition none->420ms makes Chromium
|
||||
// apply the move instantly (teleport). Then HOLD for the ease so the
|
||||
// Director doesn't begin the next step mid-glide.
|
||||
writePos(posRef.current.x, posRef.current.y, true, false);
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
writePos(x, y, true, false);
|
||||
await new Promise((r) => setTimeout(r, WIN_EASE_MS + 30));
|
||||
return;
|
||||
}
|
||||
// Mac path, byte-identical to the pre-Windows version: Framer's spring drives the popup via onUpdate during the animation, then writePos confirms the final position.
|
||||
await controls.start({
|
||||
x,
|
||||
y,
|
||||
@@ -110,6 +131,18 @@ const AgenticCursor = forwardRef<AgenticCursorHandle>((_props, ref) => {
|
||||
},
|
||||
async fadeOut(to) {
|
||||
stopTrackingInternal();
|
||||
if (IS_WIN) {
|
||||
// Glide to the exit point via the same two-step arm as moveTo so the
|
||||
// CSS ease actually runs, then hide. The opacity fade has no Framer
|
||||
// runtime on Windows, so the cursor just disappears once it eases to `to`.
|
||||
writePos(posRef.current.x, posRef.current.y, true, false);
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
writePos(to.x, to.y, true, false);
|
||||
await new Promise((r) => setTimeout(r, WIN_EASE_MS + 30));
|
||||
cursorStore.set({ visible: false });
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
await controls.start({ x: to.x, y: to.y, transition: SPRING });
|
||||
writePos(to.x, to.y, true, false);
|
||||
await controls.start({
|
||||
|
||||
@@ -33,11 +33,18 @@ export const cursorStore = {
|
||||
|
||||
// Visibility transitions bypass coalescing (mounts/unmounts must flush immediately).
|
||||
const visibilityChanged = merged.visible !== state.visible;
|
||||
// `instant` flips the Windows CSS-transition mode (snap vs ease). Commit it
|
||||
// immediately, like visibility, so moveTo can arm the eased transition a
|
||||
// paint BEFORE it moves the cursor: a same-position arm write is otherwise
|
||||
// coalesced silently here, so the move and the none->420ms transition flip
|
||||
// land in one recalc and Chromium renders it as an instant jump. No-op on
|
||||
// Mac (the wrapper there is Framer-driven and ignores `instant`).
|
||||
const instantChanged = merged.instant !== state.instant;
|
||||
const dx = Math.abs(merged.x - state.x);
|
||||
const dy = Math.abs(merged.y - state.y);
|
||||
const significantMove = dx >= COALESCE_PX || dy >= COALESCE_PX;
|
||||
|
||||
if (visibilityChanged) {
|
||||
if (visibilityChanged || instantChanged) {
|
||||
state = merged;
|
||||
pendingState = null;
|
||||
rafScheduled = false;
|
||||
|
||||
@@ -405,65 +405,6 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, Props>(
|
||||
|
||||
return (
|
||||
<>
|
||||
{(inputOpen || historyOpen) && (
|
||||
// Image #54: paired mode pills above the composer/popover.
|
||||
// The two states are mutually exclusive: opening one closes the
|
||||
// other so the body underneath only renders one thing at a time.
|
||||
<Box sx={{ display: 'flex', gap: 0.5, mb: 0.75, pl: 0.25 }}>
|
||||
<Box
|
||||
onClick={() => {
|
||||
if (historyOpen) {
|
||||
handleCloseHistory();
|
||||
onNewAgent();
|
||||
}
|
||||
// If already in inputOpen, this is a no-op (we're already
|
||||
// in new chat). The visible active styling tells the user
|
||||
// that. Clicking again does nothing intentionally.
|
||||
}}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.3,
|
||||
fontSize: '0.74rem', fontWeight: 600,
|
||||
color: c.text.primary,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${inputOpen && !historyOpen ? c.border.medium : c.border.subtle}`,
|
||||
boxShadow: inputOpen && !historyOpen ? c.shadow.sm : 'none',
|
||||
px: 0.85, py: 0.3, borderRadius: 999,
|
||||
cursor: historyOpen ? 'pointer' : 'default',
|
||||
'&:hover': historyOpen ? { bgcolor: c.bg.elevated } : {},
|
||||
}}>
|
||||
<AddRounded sx={{ fontSize: 12 }} />
|
||||
New Chat
|
||||
</Box>
|
||||
<Box
|
||||
onClick={() => {
|
||||
if (historyOpen) {
|
||||
handleCloseHistory();
|
||||
return;
|
||||
}
|
||||
// Close the composer first; inputOpen takes precedence in
|
||||
// the render branch below so the popover would be hidden
|
||||
// behind it otherwise.
|
||||
if (inputOpen) onCancel();
|
||||
setHistoryOpen(true);
|
||||
}}
|
||||
role="button"
|
||||
sx={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 0.3,
|
||||
fontSize: '0.74rem', fontWeight: 600,
|
||||
color: historyOpen ? c.text.primary : c.text.secondary,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${historyOpen ? c.border.medium : c.border.subtle}`,
|
||||
boxShadow: historyOpen ? c.shadow.sm : 'none',
|
||||
px: 0.85, py: 0.3, borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { bgcolor: c.bg.elevated },
|
||||
}}>
|
||||
<HistoryRounded sx={{ fontSize: 12 }} />
|
||||
History
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<MotionBox
|
||||
ref={containerRef}
|
||||
layout
|
||||
|
||||
Reference in New Issue
Block a user