diff --git a/web/src/lib/components/GraphBackground.svelte b/web/src/lib/components/GraphBackground.svelte index 58a8ab1..023b267 100644 --- a/web/src/lib/components/GraphBackground.svelte +++ b/web/src/lib/components/GraphBackground.svelte @@ -198,8 +198,14 @@ const z = (s.z + tg.z) / 2 const a = project(s.x, s.y!, z) const b = project(tg.x, tg.y!, z) + const dx = b.x - a.x + const dy = b.y - a.y + const len = Math.max(Math.hypot(dx, dy), 1) + const curve = Math.min(len * 0.15, 40) + const mx = (a.x + b.x) / 2 - (dy / len) * curve + const my = (a.y + b.y) / 2 + (dx / len) * curve ctx.moveTo(a.x, a.y) - ctx.lineTo(b.x, b.y) + ctx.quadraticCurveTo(mx, my, b.x, b.y) } ctx.stroke() diff --git a/web/src/lib/components/SessionGraph.svelte b/web/src/lib/components/SessionGraph.svelte index fb7973c..3f66120 100644 --- a/web/src/lib/components/SessionGraph.svelte +++ b/web/src/lib/components/SessionGraph.svelte @@ -11,10 +11,22 @@ type Simulation } from 'd3-force' import { fetchGraph, type Entity } from '$lib/api' - import { messages } from '$lib/stores/chat' - import { touched, healthDiffs } from '$lib/stores/workspace' + import type { ChatMessage } from '$lib/stores/chat' + import type { TouchedEntity, HealthDiff } from '$lib/stores/workspace' import { openEntityWindow, wmState } from '$lib/stores/windows' + // Prop-driven (not store-imported) so this can render either the main + // page's global "current session" data or a floating task window's own + // per-session data — see TaskContextPanel.svelte, which supplies both. + let { messages, touched, healthDiffs }: { messages: ChatMessage[]; touched: TouchedEntity[]; healthDiffs: HealthDiff[] } = $props() + + // SVG ids are document-global, not scoped to this — several task + // windows can each have their own Scope graph open at once, and without a + // per-instance suffix every one of them would define (and reference) + // , so only the first in the document would ever + // actually paint (the rest resolve to nothing, background reads blank). + const dotGridId = `dot-grid-${crypto.randomUUID().slice(0, 8)}` + interface Node extends Entity { x?: number y?: number @@ -75,7 +87,7 @@ // get_health_summary would otherwise dump all 168 entities into the graph). const candidateSlugs = $derived.by(() => { const out = new Set() - for (const m of $messages) { + for (const m of messages) { collectSlugs(m.text, out) for (const t of m.tools) collectSlugs(t.args, out) } @@ -234,15 +246,15 @@ // object identity fine and this is small (≤12 touched, ≤8 diffs). const touchedBySlug = $derived.by(() => { const m: Record = {} - for (const t of $touched) m[t.slug] = true + for (const t of touched) m[t.slug] = true return m }) const diffBySlug = $derived.by(() => { const m: Record = {} - for (const d of $healthDiffs) if (!(d.slug in m)) m[d.slug] = d + for (const d of healthDiffs) if (!(d.slug in m)) m[d.slug] = d return m }) - const nowTouching = $derived($touched[0] ?? null) + const nowTouching = $derived(touched[0] ?? null) function endpoint(end: string | Node): Node | undefined { return typeof end === 'object' ? end : nodes.find((n) => n.slug === end) @@ -348,28 +360,32 @@ onpointercancel={onUp} > - + - + {#each links as link} {@const s = endpoint(link.source)} {@const t = endpoint(link.target)} {#if s?.x != null && t?.x != null && s?.y != null && t?.y != null} {@const focus = selected && (s.slug === selected.slug || t.slug === selected.slug)} - {link.type} - + {/if} {/each}