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>
79 lines
2.5 KiB
Svelte
79 lines
2.5 KiB
Svelte
<script lang="ts">
|
|
import { messages, streaming, connectionState, sendMessage, cancelStream, reconnect, error, chatErrors, dismissError } from '$lib/stores/chat'
|
|
import ChatThread from '$lib/components/ChatThread.svelte'
|
|
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
|
|
|
let { showRail = true }: { showRail?: boolean } = $props()
|
|
|
|
// Resizable right rail (session graph). Persisted so it survives reloads.
|
|
const RAIL_MIN = 260
|
|
const RAIL_MAX = 620
|
|
function loadRailWidth(): number {
|
|
if (typeof localStorage === 'undefined') return 320
|
|
const v = Number(localStorage.getItem('oikos-rail-width'))
|
|
return v >= RAIL_MIN && v <= RAIL_MAX ? v : 320
|
|
}
|
|
let railWidth = $state(loadRailWidth())
|
|
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
|
|
localStorage.setItem('oikos-rail-width', String(railWidth))
|
|
window.removeEventListener('pointermove', move)
|
|
window.removeEventListener('pointerup', up)
|
|
}
|
|
window.addEventListener('pointermove', move)
|
|
window.addEventListener('pointerup', up)
|
|
}
|
|
|
|
const suggestions = [
|
|
'What needs my attention right now?',
|
|
'Summarize fleet health',
|
|
'Any pending approvals or open signals?',
|
|
'What changed in the last hour?'
|
|
]
|
|
</script>
|
|
|
|
<div class="flex h-full min-h-0">
|
|
<ChatThread
|
|
messages={$messages}
|
|
streaming={$streaming}
|
|
connectionState={$connectionState}
|
|
error={$error}
|
|
chatErrors={$chatErrors}
|
|
onSend={sendMessage}
|
|
onCancel={cancelStream}
|
|
onReconnect={reconnect}
|
|
onDismissError={dismissError}
|
|
{suggestions}
|
|
/>
|
|
|
|
{#if showRail}
|
|
<div class="hidden shrink-0 xl:flex" 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 />
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|