mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-30 19:59:38 +02:00
[eric] canvas: fullscreen glides straight to the zone, shell wash renders identically to the canvas wash (grain seam class dead), grain hook survives two consumers, tool-ui repairs learn agent id aliases and array rows
This commit is contained in:
@@ -31,7 +31,8 @@ import { findBrowserByWebContentsId } from '@/shared/browserRegistry';
|
||||
import { byPreviewRecency } from '@/shared/previewOrder';
|
||||
import { useClaudeTokens, useThemeAccent, useThemeWash } from '@/shared/styles/ThemeContext';
|
||||
import SpacesStrip from '@/app/pages/Dashboard/desktop/SpacesStrip';
|
||||
import { washBackgroundUrl, effectiveWashStops } from '@/shared/styles/washBackground';
|
||||
import { washOpaqueBackgroundUrl, washUnderlayColor, effectiveWashStops } from '@/shared/styles/washBackground';
|
||||
import { useGrainTileUrl } from '@/shared/styles/useGrainTileUrl';
|
||||
import { ErrorSlime } from '@/app/components/feedback/ErrorSlime';
|
||||
|
||||
const AppShell: React.FC = () => {
|
||||
@@ -76,7 +77,8 @@ const AppShell: React.FC = () => {
|
||||
// Arc/Zen fullscreen ground: ONE themed wash across the whole window (sidebar sits on it borderless,
|
||||
// the content floats as a rounded card). Mirrors the DashboardCanvas wash formula.
|
||||
const { accent: themeAccent, gradient: themeGradient } = useThemeAccent();
|
||||
const { washOpacity: themeWashOpacity } = useThemeWash();
|
||||
const { washOpacity: themeWashOpacity, grain: themeWashGrain } = useThemeWash();
|
||||
const shellGrainUrl = useGrainTileUrl(themeWashGrain);
|
||||
const fsWashStops = effectiveWashStops(themeGradient, themeAccent);
|
||||
// During an active free trial the user CAN run things, so a red "no model connected" warning is misleading and discouraging (it sits right above the working starter chips). The trial flips connection_mode back to own_key the moment it's spent, so this banner returns then, landing the connect-a-model nudge after the win, not before it.
|
||||
const freeTrialActive = useAppSelector((s) => {
|
||||
@@ -410,7 +412,16 @@ const AppShell: React.FC = () => {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column', height: '100vh', bgcolor: c.bg.secondary,
|
||||
...(fsWashStops ? { backgroundImage: washBackgroundUrl(fsWashStops, themeWashOpacity), backgroundSize: '100% 100%' } : {}),
|
||||
// Identical rendering to the canvas wash (opaque pre-blend + the same baked grain tile), so any
|
||||
// sliver of shell peeking past the viewport reads as continuous texture, never a tint/grain seam.
|
||||
...(fsWashStops ? {
|
||||
backgroundColor: washUnderlayColor(fsWashStops, themeWashOpacity, c.bg.page),
|
||||
backgroundImage: shellGrainUrl
|
||||
? `${shellGrainUrl}, ${washOpaqueBackgroundUrl(fsWashStops, themeWashOpacity, c.bg.page)}`
|
||||
: washOpaqueBackgroundUrl(fsWashStops, themeWashOpacity, c.bg.page),
|
||||
backgroundSize: shellGrainUrl ? 'auto, 100% 100%' : '100% 100%',
|
||||
backgroundRepeat: shellGrainUrl ? 'repeat, no-repeat' : 'no-repeat',
|
||||
} : {}),
|
||||
}}>
|
||||
{/* Sidebar retired: dashboards switch via the macOS-Spaces top strip; a slim band below the
|
||||
spaces hot zone keeps the frameless window draggable (the sidebar's drag strip is gone). */}
|
||||
|
||||
@@ -193,7 +193,18 @@ function rebaseline(entry: TiledEntry, cam: Camera, tx: number, ty: number): voi
|
||||
export function registerTiledCard(id: string, zone: string, origin: { x: number; y: number }, cam: Camera): void {
|
||||
const el = document.querySelector<HTMLElement>(`[data-select-id="${CSS.escape(id)}"]`);
|
||||
if (!el) return;
|
||||
const entry: TiledEntry = { el, zone, originX: origin.x, originY: origin.y };
|
||||
// Derive the TRUE origin up front from the painted rect and the element's current transform
|
||||
// (matrix e/f are its translate in canvas units), so the enter glide heads straight for the zone
|
||||
// instead of gliding to a wrong spot and snapping at settle (the left-then-center jerk).
|
||||
let ox = origin.x;
|
||||
let oy = origin.y;
|
||||
try {
|
||||
const r0 = el.getBoundingClientRect();
|
||||
const m = new DOMMatrixReadOnly(getComputedStyle(el).transform);
|
||||
ox = (r0.left - cam.panX) / cam.zoom - m.e;
|
||||
oy = (r0.top - cam.panY) / cam.zoom - m.f;
|
||||
} catch { /* keep the passed origin */ }
|
||||
const entry: TiledEntry = { el, zone, originX: ox, originY: oy };
|
||||
entries.set(id, entry);
|
||||
startObserving();
|
||||
// Tiling usually commits alongside chrome collapsing, so a cached workspace is untrustworthy here.
|
||||
|
||||
@@ -32,7 +32,9 @@ export function useGrainTileUrl(opacity: number): string | null {
|
||||
} else {
|
||||
const img = sourceImage ?? new Image();
|
||||
sourceImage = img;
|
||||
img.onload = () => bake(img);
|
||||
// addEventListener, never onload=: with two consumers (shell + canvas) the second onload
|
||||
// assignment silently erased the first, and that instance stayed grainless forever.
|
||||
img.addEventListener('load', () => bake(img), { once: true });
|
||||
if (!img.src) img.src = './grain-texture.png';
|
||||
if (img.complete && img.naturalWidth > 0) bake(img);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ interface VendoredToolUiProps {
|
||||
quietFail?: boolean;
|
||||
}
|
||||
|
||||
const warnedShapes = new Set<string>();
|
||||
|
||||
type Gate =
|
||||
| { state: 'pending' }
|
||||
| { state: 'ok'; parsed: Record<string, unknown> }
|
||||
@@ -67,9 +69,11 @@ function repairCommonAgentShapes(props: Record<string, unknown>): Record<string,
|
||||
if (typeof o === 'string') return { id: slugFor(o, i), label: o };
|
||||
if (o && typeof o === 'object' && !Array.isArray(o)) {
|
||||
const obj = { ...(o as Record<string, unknown>) };
|
||||
if (obj.id == null || obj.id === '') obj.id = slugFor(obj.label, i);
|
||||
// Agents reach for value/name/key and title/text as synonyms; honor them before inventing a slug.
|
||||
if (obj.id == null || obj.id === '') obj.id = obj.value ?? obj.key ?? obj.name ?? null;
|
||||
if (obj.id == null || obj.id === '') obj.id = slugFor(obj.label ?? obj.title ?? obj.text, i);
|
||||
else if (typeof obj.id !== 'string') obj.id = String(obj.id);
|
||||
if (typeof obj.label !== 'string' || !obj.label) obj.label = String(obj.label ?? obj.id);
|
||||
if (typeof obj.label !== 'string' || !obj.label) obj.label = String(obj.label ?? obj.title ?? obj.text ?? obj.name ?? obj.id);
|
||||
return obj;
|
||||
}
|
||||
return o;
|
||||
@@ -93,7 +97,14 @@ function repairCommonAgentShapes(props: Record<string, unknown>): Record<string,
|
||||
});
|
||||
}
|
||||
if (Array.isArray(out.data)) {
|
||||
// Row arrays (instead of keyed objects) zip against the column keys, in order.
|
||||
const colKeys = Array.isArray(out.columns)
|
||||
? (out.columns as Array<Record<string, unknown>>).map((c, i) => String((c && typeof c === 'object' ? (c.key ?? c.id ?? c.label) : c) ?? `col${i + 1}`))
|
||||
: null;
|
||||
out.data = out.data.map((row) => {
|
||||
if (Array.isArray(row) && colKeys && colKeys.length > 0) {
|
||||
return Object.fromEntries(row.map((v, i) => [colKeys[i] ?? `col${i + 1}`, v]));
|
||||
}
|
||||
if (!row || typeof row !== 'object' || Array.isArray(row)) return row;
|
||||
return Object.fromEntries(Object.entries(row as Record<string, unknown>).map(([k, v]) => {
|
||||
if (v !== null && typeof v === 'object' && !Array.isArray(v)) return [k, JSON.stringify(v)];
|
||||
@@ -171,12 +182,13 @@ function VendoredToolUi({ name, props, extraProps, quietFail = false }: Vendored
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entry, propsKey]);
|
||||
|
||||
const warnedRef = useRef<string | null>(null);
|
||||
if (!entry) return null;
|
||||
if (gate.state === 'bad') {
|
||||
// Schema jargon is for the console; the transcript gets one quiet human line. Once per payload, not per render.
|
||||
if (warnedRef.current !== propsKey) {
|
||||
warnedRef.current = propsKey;
|
||||
// Schema jargon is for the console, once per component+issue SHAPE for the whole session; a
|
||||
// transcript full of the same agent mistake used to print 37 copies of the identical warning.
|
||||
const shapeKey = `${name}:${gate.problem}`;
|
||||
if (!warnedShapes.has(shapeKey)) {
|
||||
warnedShapes.add(shapeKey);
|
||||
console.warn(`[tool-ui] ${name} payload didn't validate:`, gate.problem);
|
||||
}
|
||||
if (quietFail) return null;
|
||||
|
||||
Reference in New Issue
Block a user