// One global wmkit window manager for the whole app (mounted once by // WindowLayer.svelte inside Desktop.svelte) — this is what lets an entity // opened from Knowledge Base, chat, or anywhere else land in the same // floating window layer, with several windows open side by side, rather than // each page owning its own single-entity sidebar/sheet. import { createManager, createDesktop, wmStore } from '@surdeddd/wmkit/svelte' import { persist } from '@surdeddd/wmkit/persist' import { appById, appWindowId } from '$lib/apps' import { sessions } from '$lib/stores/chat' import { heading } from '$lib/tasks' export const wm = createManager({ defaultSize: { width: 480, height: 560 } }) export const dk = createDesktop(wm, { // topEdge:'maximize' + preview gives the classic drag-to-top-maximizes // affordance; magnetism/keyboard are wmkit defaults worth turning on now // that windows are the whole app's primary surface, not a secondary layer. snap: { topEdge: 'maximize', preview: true }, keyboard: true, magnetism: true, // Animates a minimized window toward its taskbar button instead of just // vanishing — Taskbar.svelte tags each button with this same attribute. minimizeTarget: (win) => document.querySelector(`[data-taskbar-btn="${CSS.escape(win.id)}"]`) }) export const wmState = wmStore(wm) // Layout survives reloads: every window id is self-describing (app:, // session:, or a bare entity slug — see EntityDesktop/WindowLayer's // content branch), so a hydrated window needs no extra bookkeeping to know // what to render once the desktop remounts. export const wmPersist = persist(wm, { key: 'oikos-windows', debounce: 300, autoRestore: true }) // A task window is titled from the operator's (truncated) prompt at // creation time — openTaskWindow below and chat.ts's startTask both only // know the raw text, not the goal/heading the backend eventually derives // for the session. Whenever the sessions list refreshes (loadSessions(), // called all over — on task events, after a turn completes, ...) resync // any open task window's title to the session's real heading, so the // taskbar/titlebar stop showing the placeholder forever. sessions.subscribe((list) => { for (const s of list) { const id = `session:${s.id}` const win = wm.get(id) if (!win) continue const title = heading(s) if (win.title !== title) wm.update(id, { title }) } }) // Classic show-desktop toggle: minimize everything, or if everything's // already minimized (a prior show-desktop, or the operator minimized them // all by hand), bring them all back rather than being a one-way action. // Shared by the taskbar button and the desktop's right-click menu. export function toggleShowDesktop(): void { const anyVisible = wm.getState().order.some((id) => wm.get(id)?.stage !== 'minimized') if (anyVisible) wm.minimizeAll() else wm.restoreAll() } // Opens (or focuses/restores) a registry app's window. Apps are // single-instance — double-clicking an already-open app's icon should never // stack a second window, same dedupe pattern as openEntityWindow below. export function openAppWindow(appId: string): void { const app = appById.get(appId) if (!app) return const id = appWindowId(appId) if (wm.get(id)) { wm.restore(id) wm.focus(id) return } wm.open({ id, title: app.title, width: app.width, height: app.height, minWidth: app.minWidth, minHeight: app.minHeight }) } // Opens a window for the entity, or focuses (and restores, if minimized) the // existing one — wm.open() throws if a window with this id already exists, // and slugs make natural, stable window ids (also dedupes "same entity // opened twice" into one window instead of stacking duplicates). export function openEntityWindow(slug: string | null): void { if (!slug) return if (wm.get(slug)) { wm.restore(slug) wm.focus(slug) return } wm.open({ id: slug, title: slug }) } // Singleton "compose a new task" window — the Tasks app's New Task button // opens this rather than a dialog, since everything else in the desktop is // already a window. It renders as an empty chat (NewTaskChat, in // WindowLayer.svelte) sized like a real task window rather than a separate // compose screen, and closes itself once the task's session window takes // over. export const NEW_TASK_WINDOW_ID = 'new-task' export function openNewTaskWindow(): void { if (wm.get(NEW_TASK_WINDOW_ID)) { wm.restore(NEW_TASK_WINDOW_ID) wm.focus(NEW_TASK_WINDOW_ID) return } wm.open({ id: NEW_TASK_WINDOW_ID, title: 'New task', width: 900, height: 640, minWidth: 600, minHeight: 400 }) } // Same dedupe/restore/focus pattern as openEntityWindow, for a task/session's // chat window. Id is namespaced `session:` — distinct from entity window // ids (always a bare `type:identifier` slug, and task ENTITIES already use // `task:` as their own slug) so a task's chat window and its entity // detail window never collide over the same wmkit id. See // WindowLayer.svelte for the id -> content-component branch. export function openTaskWindow(sessionId: string | null, title: string): void { if (!sessionId) return const id = `session:${sessionId}` if (wm.get(id)) { wm.restore(id) wm.focus(id) return } wm.open({ id, title, width: 900, height: 640, minWidth: 600, minHeight: 400 }) }