Replace hand-rolled pointer-resize logic (TaskContextPanel's 3-way vertical split, SessionChatWindow's rail, ChatThread's message/input split) with svelte-splitpanes, themed onto the app's existing border/primary tokens. - TaskContextPanel: Scope/Plan/Event-log sections collapse to a fixed header height and restore their last size on reopen. - ChatThread: input area is now a separate resizable pane, clamped to a measured one-line minimum and a 45% max, instead of a fixed max-h textarea. - Send button restyled to sit inside the input's corner (Claude-style), swapping the up-arrow for a corner-down-left return icon. - Adds a $app/environment shim + optimizeDeps exclude, since svelte-splitpanes assumes SvelteKit and this is a plain Vite app. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.7 KiB
Svelte
68 lines
2.7 KiB
Svelte
<script lang="ts">
|
|
// Floating-window content for a task/session — the per-window counterpart
|
|
// to the main Chat page (thread + rail), fully self-contained per
|
|
// sessionId via chat.ts's chatFor()/loadSessionChat()/sendSessionMessage()
|
|
// and workspace.ts's workspaceFor()/startSessionWorkspace(), so several of
|
|
// these can be open (and independently live) at once without the "which
|
|
// one's on screen" guarding the main page's singleton stores need.
|
|
import { onDestroy, onMount } from 'svelte'
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import { chatFor, loadSessionChat, sendSessionMessage, cancelSessionStream, stopSessionPolling, dismissError, chatErrors } from '$lib/stores/chat'
|
|
import ChatThread from '$lib/components/ChatThread.svelte'
|
|
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
|
|
|
let { sessionId }: { sessionId: string } = $props()
|
|
|
|
// Svelte's `$store` auto-subscription only works on a plain identifier
|
|
// bound directly to a store, not a member expression — chatFor() returns
|
|
// an object of stores, so pull each one out into its own identifier here.
|
|
const chat = chatFor(sessionId)
|
|
const chatMessages = chat.messages
|
|
const chatStreaming = chat.streaming
|
|
const chatConnectionState = chat.connectionState
|
|
const chatError = chat.error
|
|
const chatNotFound = chat.notFound
|
|
let loading = $state(true)
|
|
|
|
onMount(async () => {
|
|
await loadSessionChat(sessionId)
|
|
loading = false
|
|
})
|
|
|
|
onDestroy(() => stopSessionPolling(sessionId))
|
|
|
|
// Resizable right rail — sized smaller by default since task windows open
|
|
// narrower than the full page.
|
|
let railSize = $state(24)
|
|
</script>
|
|
|
|
<div class="flex h-full min-h-0">
|
|
{#if loading}
|
|
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">Loading…</div>
|
|
{:else if $chatNotFound}
|
|
<div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center">
|
|
<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}
|
|
<Splitpanes theme="oikos-theme" dblClickSplitter={false}>
|
|
<Pane>
|
|
<ChatThread
|
|
messages={$chatMessages}
|
|
streaming={$chatStreaming}
|
|
connectionState={$chatConnectionState}
|
|
error={$chatError}
|
|
chatErrors={$chatErrors}
|
|
onSend={(text) => sendSessionMessage(sessionId, text)}
|
|
onCancel={() => cancelSessionStream(sessionId)}
|
|
onReconnect={() => loadSessionChat(sessionId)}
|
|
onDismissError={dismissError}
|
|
/>
|
|
</Pane>
|
|
<Pane bind:size={railSize} minSize={18} maxSize={40}>
|
|
<TaskContextPanel {sessionId} />
|
|
</Pane>
|
|
</Splitpanes>
|
|
{/if}
|
|
</div>
|