feat(web): open tasks/sessions as floating windows with independent live chat
Clicking a task now opens it as a wmkit floating window (like entity windows already do) instead of navigating away from wherever you were. Several task windows can be open and actively streaming at once, each fully independent — no "which one's on screen" guard needed, since each window owns its own store bundle: - chat.ts: chatFor(sessionId)/loadSessionChat/sendSessionMessage give each window its own messages/streaming/connectionState, alongside the existing singleton path the main Chat page still uses unchanged. - workspace.ts: same split for plan/questions/touched/health-diffs (workspaceFor/startSessionWorkspace), each with its own live-event watermark since several windows can watch the same event stream. - activity.ts: activityLogFor(sessionId) mirrors the global derivation. SessionGraph.svelte, OperatorQuestion.svelte, and ActivityTimeline.svelte were converted from store-importing to prop-driven (matching the new ChatThread.svelte, extracted from Chat.svelte's transcript/input so both the main page and task windows share one implementation instead of duplicating markup/styling) so each can render either the global "current session" or a specific window's session. Also: minimized-window taskbar chips now cap at a max width with middle-ellipsis truncation instead of growing unbounded, and the window header's title/action-button row is fixed to genuinely match heights (not just share a center point) for more robust alignment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
91
web/src/lib/components/SessionChatWindow.svelte
Normal file
91
web/src/lib/components/SessionChatWindow.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<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 { 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
|
||||
let loading = $state(true)
|
||||
|
||||
onMount(async () => {
|
||||
await loadSessionChat(sessionId)
|
||||
loading = false
|
||||
})
|
||||
|
||||
onDestroy(() => stopSessionPolling(sessionId))
|
||||
|
||||
// Resizable right rail — same behavior as Chat.svelte's, sized smaller by
|
||||
// default since task windows open narrower than the full page.
|
||||
const RAIL_MIN = 220
|
||||
const RAIL_MAX = 480
|
||||
let railWidth = $state(260)
|
||||
let resizing = $state(false)
|
||||
|
||||
function startResize(e: PointerEvent) {
|
||||
e.preventDefault()
|
||||
resizing = true
|
||||
const startX = e.clientX
|
||||
const startW = railWidth
|
||||
function move(ev: PointerEvent) {
|
||||
railWidth = Math.min(RAIL_MAX, Math.max(RAIL_MIN, startW + (startX - ev.clientX)))
|
||||
}
|
||||
function up() {
|
||||
resizing = false
|
||||
window.removeEventListener('pointermove', move)
|
||||
window.removeEventListener('pointerup', up)
|
||||
}
|
||||
window.addEventListener('pointermove', move)
|
||||
window.addEventListener('pointerup', up)
|
||||
}
|
||||
</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}
|
||||
<ChatThread
|
||||
messages={$chatMessages}
|
||||
streaming={$chatStreaming}
|
||||
connectionState={$chatConnectionState}
|
||||
error={$chatError}
|
||||
chatErrors={$chatErrors}
|
||||
onSend={(text) => sendSessionMessage(sessionId, text)}
|
||||
onCancel={() => cancelSessionStream(sessionId)}
|
||||
onReconnect={() => loadSessionChat(sessionId)}
|
||||
onDismissError={dismissError}
|
||||
/>
|
||||
|
||||
<div class="flex shrink-0" style="width: {railWidth}px">
|
||||
<button
|
||||
type="button"
|
||||
class="group/rz relative w-1.5 shrink-0 cursor-col-resize touch-none"
|
||||
onpointerdown={startResize}
|
||||
aria-label="Resize task panel"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 transition-colors {resizing
|
||||
? 'bg-primary/60'
|
||||
: 'bg-border group-hover/rz:bg-primary/50'}"
|
||||
></span>
|
||||
</button>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<TaskContextPanel {sessionId} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user