diff --git a/web/src/app.css b/web/src/app.css index e3627b7..4756740 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -254,7 +254,12 @@ flex-direction: column; box-sizing: border-box; pointer-events: auto; - background: var(--card); + /* Frosted glass — same recipe as the desktop's "What should Nomos do?" + launcher card (bg-card/70 backdrop-blur), so floating windows read as + part of the same surface language instead of opaque panels. */ + background: color-mix(in oklab, var(--card) 70%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); color: var(--card-foreground); border: 1px solid var(--border); border-radius: var(--radius-lg); diff --git a/web/src/lib/components/ChatThread.svelte b/web/src/lib/components/ChatThread.svelte index 0ecdb85..142a790 100644 --- a/web/src/lib/components/ChatThread.svelte +++ b/web/src/lib/components/ChatThread.svelte @@ -100,13 +100,15 @@ }) const inputMinSize = $derived(threadHeight > 0 ? (oneLinePx / threadHeight) * 100 : 12) + // Keep the input pinned to inputMinSize (one line) until the user actually + // drags the splitter — not just on the first measurement. A floating + // window's threadHeight is 0/wrong for a frame or two while it animates + // open, and locking the percentage to that first reading left the input + // several lines tall once the window reached full size (fixed 2026-07-21). let inputSize = $state(12) - let inputSizeDefaulted = false + let userResizedInput = false $effect(() => { - if (!inputSizeDefaulted && threadHeight > 0) { - inputSize = inputMinSize - inputSizeDefaulted = true - } + if (!userResizedInput) inputSize = inputMinSize }) function isNearBottom(): boolean { @@ -172,12 +174,12 @@
Your resident operator. Ask about the fleet, or tell it to act.
diff --git a/web/src/lib/components/SessionChatWindow.svelte b/web/src/lib/components/SessionChatWindow.svelte index 143ccc2..b8bfb6d 100644 --- a/web/src/lib/components/SessionChatWindow.svelte +++ b/web/src/lib/components/SessionChatWindow.svelte @@ -7,6 +7,7 @@ import { Pane, Splitpanes } from 'svelte-splitpanes' import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat' import { activityLogFor } from '$lib/stores/activity' + import { workspaceFor, startSessionWorkspace } from '$lib/stores/workspace' import ChatThread from '$lib/components/ChatThread.svelte' import TaskContextPanel from '$lib/components/TaskContextPanel.svelte' @@ -26,14 +27,46 @@ const chatNotFound = chat.notFound // eslint-disable-next-line svelte/valid-compile const sessionActivityLog = activityLogFor(sessionId) + // Started here (rather than left to TaskContextPanel's own onMount) so the + // workspace is already tracking touched entities/plan/questions before the + // context rail ever mounts — it needs that live even while the rail stays + // hidden (see hasContext below). + // eslint-disable-next-line svelte/valid-compile + const workspace = workspaceFor(sessionId) + // eslint-disable-next-line svelte/valid-compile + const touchedEntities = workspace.touched + // eslint-disable-next-line svelte/valid-compile + const openQuestion = workspace.openQuestion let loading = $state(true) + // The context rail (Scope/Activity) is only worth its screen space once + // there's something in it — a brand-new task otherwise opens to an empty + // "entities appear here" placeholder next to an equally empty activity + // list. Show it the moment any of the three has real content, and keep it + // shown from then on (no flicker back to hidden if e.g. touched entities + // later expire). + let hasContext = $state(false) + $effect(() => { + if (!hasContext && ($sessionActivityLog.length > 0 || $touchedEntities.length > 0 || $openQuestion !== null)) { + hasContext = true + } + }) + + // startSessionWorkspace's cleanup is registered via onDestroy below rather + // than returned from this callback — onMount ignores a returned function + // once the callback is async (its return value is a Promise, not the + // cleanup itself). + const stopWorkspace = startSessionWorkspace(sessionId) + onMount(async () => { await loadSessionChat(sessionId) loading = false }) - onDestroy(() => stopSessionPolling(sessionId)) + onDestroy(() => { + stopSessionPolling(sessionId) + stopWorkspace() + }) // Resizable right rail — sized smaller by default since task windows open // narrower than the full page. @@ -48,7 +81,7 @@Task not found.
It may have been deleted.