[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:
Eric
2026-05-26 15:41:13 -07:00
parent 440a3e22f6
commit 8c5b3bdf64
5 changed files with 73 additions and 81 deletions
+1 -1
View File
@@ -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