feat(web): new task opens straight into chat, context rail waits for content, frosted windows
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

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>
This commit is contained in:
2026-07-21 09:50:47 +02:00
parent ce34cfeac7
commit 6b6bfe1fd8
7 changed files with 107 additions and 23 deletions

View File

@@ -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 @@
<p class="text-sm text-muted-foreground">Task not found.</p>
<p class="text-xs text-muted-foreground/70">It may have been deleted.</p>
</div>
{:else}
{:else if hasContext}
<Splitpanes theme="oikos-theme" dblClickSplitter={false}>
<Pane>
<ChatThread
@@ -68,5 +101,18 @@
<TaskContextPanel {sessionId} />
</Pane>
</Splitpanes>
{:else}
<ChatThread
messages={$chatMessages}
streaming={$chatStreaming}
connectionState={$chatConnectionState}
error={$chatError}
chatErrors={$chatErrors}
activityLog={sessionActivityLog}
onSend={(text) => sendSessionMessage(sessionId, text)}
onCancel={() => cancelSessionStream(sessionId)}
onReconnect={() => loadSessionChat(sessionId)}
onDismissError={dismissError}
/>
{/if}
</div>