New Task now opens directly as an empty ChatThread (NewTaskChat) instead of a separate compose screen, sized like a real task window. The Scope/Activity context rail in a task window no longer renders until there's actually something to show (touched entities, activity, or an open question), avoiding an empty-placeholder sidebar on every new task. Also fixes the chat input defaulting to several lines tall on window open, centers the empty-chat greeting vertically, and gives floating windows the same frosted-glass look as the desktop's task launcher card. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
128 lines
5.3 KiB
TypeScript
128 lines
5.3 KiB
TypeScript
// 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:<id>,
|
|
// session:<id>, 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:<id>` — distinct from entity window
|
|
// ids (always a bare `type:identifier` slug, and task ENTITIES already use
|
|
// `task:<uuid>` 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 })
|
|
}
|