From 5221540e179a4e0fe8e73f99124ba188dc1deca3 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Thu, 6 Aug 2026 12:22:00 -0700 Subject: [PATCH] [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 --- .../src/app/components/Layout/AppShell.tsx | 17 ++++++++++--- .../pages/Dashboard/canvas/tiledGeometry.ts | 13 +++++++++- frontend/src/shared/styles/useGrainTileUrl.ts | 4 +++- frontend/src/toolui/VendoredToolUi.tsx | 24 ++++++++++++++----- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index cba5ee1d..1cbdc6f9 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -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 ( {/* 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). */} diff --git a/frontend/src/app/pages/Dashboard/canvas/tiledGeometry.ts b/frontend/src/app/pages/Dashboard/canvas/tiledGeometry.ts index 982325b9..66fd3564 100644 --- a/frontend/src/app/pages/Dashboard/canvas/tiledGeometry.ts +++ b/frontend/src/app/pages/Dashboard/canvas/tiledGeometry.ts @@ -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(`[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. diff --git a/frontend/src/shared/styles/useGrainTileUrl.ts b/frontend/src/shared/styles/useGrainTileUrl.ts index be53d6f7..b0193427 100644 --- a/frontend/src/shared/styles/useGrainTileUrl.ts +++ b/frontend/src/shared/styles/useGrainTileUrl.ts @@ -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); } diff --git a/frontend/src/toolui/VendoredToolUi.tsx b/frontend/src/toolui/VendoredToolUi.tsx index bcf8716a..0fafa6e5 100644 --- a/frontend/src/toolui/VendoredToolUi.tsx +++ b/frontend/src/toolui/VendoredToolUi.tsx @@ -39,6 +39,8 @@ interface VendoredToolUiProps { quietFail?: boolean; } +const warnedShapes = new Set(); + type Gate = | { state: 'pending' } | { state: 'ok'; parsed: Record } @@ -67,9 +69,11 @@ function repairCommonAgentShapes(props: Record): Record) }; - 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): Record>).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).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(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;