{#if loading}
diff --git a/web/src/lib/components/EntitySheet.svelte b/web/src/lib/components/EntitySheet.svelte
deleted file mode 100644
index 31d302b..0000000
--- a/web/src/lib/components/EntitySheet.svelte
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
- {currentSlug ?? 'Entity detail'}
- Entity detail panel
-
- {#if currentSlug}
- {#key currentSlug}
- (currentSlug = s)} />
- {/key}
- {/if}
-
-
diff --git a/web/src/lib/components/SessionGraph.svelte b/web/src/lib/components/SessionGraph.svelte
index 106c033..fb7973c 100644
--- a/web/src/lib/components/SessionGraph.svelte
+++ b/web/src/lib/components/SessionGraph.svelte
@@ -13,12 +13,7 @@
import { fetchGraph, type Entity } from '$lib/api'
import { messages } from '$lib/stores/chat'
import { touched, healthDiffs } from '$lib/stores/workspace'
- import { relativeTime } from '$lib/utils'
- import { Badge } from '$lib/components/ui/badge'
- import { Button } from '$lib/components/ui/button'
- import EntitySheet from '$lib/components/EntitySheet.svelte'
- import ExternalLinkIcon from '@lucide/svelte/icons/external-link'
- import XIcon from '@lucide/svelte/icons/x'
+ import { openEntityWindow, wmState } from '$lib/stores/windows'
interface Node extends Entity {
x?: number
@@ -46,8 +41,6 @@
let nodes = $state([])
let links = $state([])
let selected = $state(null)
- let sheetSlug = $state(null)
- let sheetOpen = $state(false)
let sim: Simulation | null = null
@@ -66,14 +59,6 @@
let cw = $state(300)
let ch = $state(300)
- // This component sits INSIDE one of TaskContextPanel's own resizable slots
- // (Scope), so — unlike a top-level section — its total budget can change at
- // any time from outside (dragging the outer Scope/Plan handle), including
- // while the detail panel below is open. asideHeight tracks that live budget
- // so detailHeight can self-clamp to it instead of trusting a one-time seed.
- let asideEl = $state(null)
- let asideHeight = $state(300)
-
function collectSlugs(value: unknown, out: Set) {
if (typeof value === 'string') {
const m = value.match(SLUG_RE)
@@ -217,13 +202,12 @@
return () => ro.disconnect()
})
+ // The highlight ring/dim styling below is tied to the node whose window
+ // was last opened — once that window is closed (from EntityDesktop, not
+ // necessarily from here), the ring should go with it rather than pointing
+ // at a window that no longer exists.
$effect(() => {
- if (!asideEl) return
- const ro = new ResizeObserver((entries) => {
- asideHeight = Math.max(entries[0].contentRect.height, 1)
- })
- ro.observe(asideEl)
- return () => ro.disconnect()
+ if (selected && !$wmState.windows[selected.slug]) selected = null
})
onDestroy(() => sim?.stop())
@@ -267,52 +251,11 @@
return typeof end === 'object' ? end.slug : end
}
- // ─── graph / detail resize ───────────────────────────────────────────
- // Same drag handle, same feel as TaskContextPanel's Scope/Plan/Activity
- // split — but the graph side stays flex-1 (always auto-fills whatever's
- // left) rather than tracking its own pixel number. Only detailHeight is
- // explicit, and it's continuously clamped against asideHeight (this
- // component's actual live budget) rather than a value seeded once — so
- // resizing the OUTER Scope section while the detail panel is open can't
- // push this panel past its container the way a one-time seed could.
- const MIN_GRAPH = 80
- const MIN_DETAIL = 80
- const HANDLE = 6
-
- let detailHeight = $state(200)
- let resizing = $state(false)
- let resizeStartY = $state(0)
- let resizeStartH = $state(0)
-
- function maxDetailHeight(): number {
- return Math.max(MIN_DETAIL, asideHeight - MIN_GRAPH - HANDLE)
- }
-
- $effect(() => {
- const max = maxDetailHeight()
- if (detailHeight > max) detailHeight = max
- })
-
- function onPointerDown(e: PointerEvent) {
- e.preventDefault()
- resizing = true
- resizeStartY = e.clientY
- resizeStartH = detailHeight
- window.addEventListener('pointermove', onPointerMove)
- window.addEventListener('pointerup', onPointerUp)
- }
- function onPointerMove(e: PointerEvent) {
- if (!resizing) return
- const dy = e.clientY - resizeStartY
- detailHeight = Math.min(maxDetailHeight(), Math.max(MIN_DETAIL, resizeStartH - dy))
- }
- function onPointerUp() {
- resizing = false
- window.removeEventListener('pointermove', onPointerMove)
- window.removeEventListener('pointerup', onPointerUp)
- }
-
// ─── drag / select ───────────────────────────────────────────────────
+ // A click (pointerdown+up with no movement in between) opens the entity
+ // straight in its own floating window (EntityDesktop) instead of a
+ // click-through mini-panel — `selected` now only drives the highlight/dim
+ // styling below, so you can see at a glance which node you last opened.
let dragState: { node: Node; moved: boolean } | null = null
function toLocal(clientX: number, clientY: number) {
@@ -334,6 +277,10 @@
dragState.moved = true
nodes = [...nodes]
}
+ function selectAndOpen(node: Node) {
+ selected = node
+ openEntityWindow(node.slug)
+ }
function onUp() {
if (!dragState) return
const { node, moved } = dragState
@@ -341,15 +288,7 @@
node.fy = null
sim?.alphaTarget(0)
dragState = null
- if (!moved) {
- const wasNull = selected === null
- const next = selected?.slug === node.slug ? null : node
- // A reasonable starting size on first open — the clamp effect above
- // keeps it honest against the live container size from here on, so
- // this doesn't need to be exact.
- if (next && wasNull) detailHeight = Math.min(maxDetailHeight(), Math.round(ch * 0.45))
- selected = next
- }
+ if (!moved) selectAndOpen(node)
}
const selectedRelations = $derived(
@@ -362,15 +301,9 @@
})
: []
)
-
- function openFull() {
- if (!selected) return
- sheetSlug = selected.slug
- sheetOpen = true
- }
-