feat(web): open tasks/sessions as floating windows with independent live chat
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

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:
2026-07-19 17:00:41 +02:00
parent e28e0e9ea3
commit 8657ac5669
14 changed files with 945 additions and 448 deletions

View File

@@ -1,17 +1,18 @@
<script lang="ts">
import { openQuestion } from '$lib/stores/workspace'
import { currentSession } from '$lib/stores/chat'
import { answerQuestion as postAnswer } from '$lib/api'
import { answerQuestion as postAnswer, type SessionQuestion } from '$lib/api'
import { Button } from '$lib/components/ui/button'
import { Textarea } from '$lib/components/ui/textarea'
import CircleHelpIcon from '@lucide/svelte/icons/circle-help'
// Prop-driven (not store-imported) — see SessionGraph.svelte for why.
let { sessionId, question }: { sessionId: string | null; question: SessionQuestion | null } = $props()
let freeText = $state('')
let submitting = $state(false)
async function submit(answer: string) {
const sid = $currentSession
const q = $openQuestion
const sid = sessionId
const q = question
if (!sid || !q || !answer.trim() || submitting) return
submitting = true
const ok = await postAnswer(sid, q.id, answer.trim())
@@ -23,8 +24,8 @@
}
</script>
{#if $openQuestion}
{@const q = $openQuestion}
{#if question}
{@const q = question}
<div class="flex shrink-0 flex-col gap-2 border-b bg-warning/5 px-3 py-2.5">
<div class="flex items-start gap-2">
<CircleHelpIcon class="mt-0.5 size-4 shrink-0 text-warning" />