sidebar reorganized: Scope, Plan, Activity — collapsible + resizable
- TaskContextPanel restructured into 3 collapsible sections: Scope (graph), Plan (goal + steps + progress), Activity (timeline) - Collapsed headers show compact live status: 'Graph', 'Step X/N', 'N actions' - Sections are vertically resizable via drag handles - Plan section shows goal inline + step list + progress bar - GoalHeader and PlanProgress no longer rendered separately - ActivityTimeline header moved to TaskContextPanel
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { activityLog, type ActivityEntry, streaming } from '$lib/stores/chat'
|
||||
import { activityLog, type ActivityEntry } from '$lib/stores/chat'
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
||||
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
||||
@@ -41,15 +41,6 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center justify-between border-b px-3 py-2">
|
||||
<span class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Activity</span>
|
||||
{#if $streaming}
|
||||
<span class="flex items-center gap-1 text-[11px] text-primary">
|
||||
<LoaderCircleIcon class="size-3 animate-spin" />
|
||||
live
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
{#if $activityLog.length === 0}
|
||||
<div class="px-3 py-6 text-center text-xs text-muted-foreground">
|
||||
|
||||
@@ -1,23 +1,185 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { startWorkspace } from '$lib/stores/workspace'
|
||||
import GoalHeader from './GoalHeader.svelte'
|
||||
import PlanProgress from './PlanProgress.svelte'
|
||||
import { startWorkspace, planSteps, currentTask } from '$lib/stores/workspace'
|
||||
import { activityLog, streaming } from '$lib/stores/chat'
|
||||
import OperatorQuestion from './OperatorQuestion.svelte'
|
||||
import SessionGraph from './SessionGraph.svelte'
|
||||
import ActivityTimeline from './ActivityTimeline.svelte'
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
|
||||
import TargetIcon from '@lucide/svelte/icons/target'
|
||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||
|
||||
onMount(() => startWorkspace())
|
||||
|
||||
let scopeOpen = $state(true)
|
||||
let planOpen = $state(true)
|
||||
let activityOpen = $state(true)
|
||||
|
||||
// Resize: distribute height across the 3 content areas.
|
||||
// Heights stored in px, minus header sizes. Default even split.
|
||||
let heights = $state([300, 200, 200])
|
||||
let resizing = $state(-1)
|
||||
let resizeStartY = $state(0)
|
||||
let resizeStartH = $state<[number, number]>([0, 0])
|
||||
|
||||
function onPointerDown(i: number, e: PointerEvent) {
|
||||
e.preventDefault()
|
||||
resizing = i
|
||||
resizeStartY = e.clientY
|
||||
resizeStartH = [heights[i], heights[i + 1]]
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
window.addEventListener('pointerup', onPointerUp)
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (resizing < 0) return
|
||||
const dy = e.clientY - resizeStartY
|
||||
const minH = 80
|
||||
// Clamp each section to minimum
|
||||
const newA = Math.max(minH, resizeStartH[0] + dy)
|
||||
const newB = Math.max(minH, resizeStartH[1] - dy)
|
||||
const newHeights = [...heights]
|
||||
newHeights[resizing] = newA
|
||||
newHeights[resizing + 1] = newB
|
||||
heights = newHeights
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
resizing = -1
|
||||
window.removeEventListener('pointermove', onPointerMove)
|
||||
window.removeEventListener('pointerup', onPointerUp)
|
||||
}
|
||||
|
||||
// Plan collapsed status
|
||||
const planDone = $derived($planSteps.filter((s) => s.status === 'done').length)
|
||||
const planTotal = $derived($planSteps.length)
|
||||
|
||||
// Activity collapsed status
|
||||
const activityRunning = $derived($activityLog.filter((e) => e.status === 'running').length)
|
||||
const activityCount = $derived($activityLog.length)
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<GoalHeader />
|
||||
<PlanProgress />
|
||||
<OperatorQuestion />
|
||||
<div class="min-h-0 flex-1">
|
||||
<SessionGraph />
|
||||
|
||||
<!-- Scope -->
|
||||
<div class="flex shrink-0 flex-col border-b">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (scopeOpen = !scopeOpen)}
|
||||
>
|
||||
{#if scopeOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||
<span>Scope</span>
|
||||
{#if !scopeOpen}
|
||||
<span class="ml-auto font-normal normal-case">Graph</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if scopeOpen}
|
||||
<div style="height: {heights[0]}px">
|
||||
<SessionGraph />
|
||||
</div>
|
||||
<!-- resize handle -->
|
||||
<div
|
||||
class="h-1.5 cursor-row-resize border-b hover:bg-primary/30 touch-none"
|
||||
onpointerdown={(e) => onPointerDown(0, e)}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="max-h-[40%] overflow-y-auto border-t">
|
||||
<ActivityTimeline />
|
||||
|
||||
<!-- Plan -->
|
||||
<div class="flex shrink-0 flex-col border-b">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (planOpen = !planOpen)}
|
||||
>
|
||||
{#if planOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||
<span>Plan</span>
|
||||
{#if !planOpen}
|
||||
{#if planTotal > 0}
|
||||
<span class="ml-auto font-normal normal-case">Step {planDone}/{planTotal}</span>
|
||||
{:else if $currentTask?.goal}
|
||||
<span class="ml-auto max-w-[120px] truncate font-normal normal-case">{$currentTask.goal}</span>
|
||||
{:else}
|
||||
<span class="ml-auto font-normal normal-case text-muted-foreground">No plan yet</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
{#if planOpen}
|
||||
<div style="height: {heights[1]}px" class="flex flex-col overflow-y-auto">
|
||||
{#if $currentTask?.goal}
|
||||
<div class="flex items-center gap-1.5 px-3 py-1.5 text-xs">
|
||||
<TargetIcon class="size-3 shrink-0 text-muted-foreground" />
|
||||
<span class="truncate text-muted-foreground">{$currentTask.goal}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if planTotal > 0}
|
||||
<div class="flex items-center justify-between px-3 pb-1 text-[11px] text-muted-foreground">
|
||||
<span>{planDone}/{planTotal} done</span>
|
||||
</div>
|
||||
<div class="mx-3 h-1 rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary transition-all duration-500" style="width: {planTotal > 0 ? Math.round((planDone / planTotal) * 100) : 0}%"></div>
|
||||
</div>
|
||||
<ol class="flex flex-col gap-0.5 overflow-y-auto px-3 py-1 text-[11px]">
|
||||
{#each $planSteps as step (step.id)}
|
||||
<li class="flex items-start gap-1.5 {step.status === 'done' ? 'text-muted-foreground line-through decoration-muted-foreground/40' : ''}">
|
||||
<span class="mt-0.5 shrink-0">
|
||||
{#if step.status === 'done'}
|
||||
<div class="size-2 rounded-full bg-success" />
|
||||
{:else if step.status === 'running'}
|
||||
<LoaderCircleIcon class="size-2.5 animate-spin text-primary" />
|
||||
{:else if step.status === 'failed'}
|
||||
<div class="size-2 rounded-full bg-destructive" />
|
||||
{:else}
|
||||
<div class="size-2 rounded-full bg-muted-foreground/30" />
|
||||
{/if}
|
||||
</span>
|
||||
<span class="leading-tight">{step.title}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{:else}
|
||||
<div class="px-3 py-3 text-center text-xs text-muted-foreground">
|
||||
No plan yet
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- resize handle -->
|
||||
<div
|
||||
class="h-1.5 cursor-row-resize border-b hover:bg-primary/30 touch-none"
|
||||
onpointerdown={(e) => onPointerDown(1, e)}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Activity -->
|
||||
<div class="flex min-h-0 flex-1 flex-col">
|
||||
<button
|
||||
type="button"
|
||||
class="flex shrink-0 items-center gap-1.5 px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground hover:text-foreground"
|
||||
onclick={() => (activityOpen = !activityOpen)}
|
||||
>
|
||||
{#if activityOpen}<ChevronDownIcon class="size-3" />{:else}<ChevronRightIcon class="size-3" />{/if}
|
||||
<span>Activity</span>
|
||||
{#if !activityOpen}
|
||||
{#if $streaming && activityRunning > 0}
|
||||
<LoaderCircleIcon class="size-3 animate-spin text-primary" />
|
||||
<span class="font-normal normal-case text-primary">{activityRunning} running</span>
|
||||
{:else}
|
||||
<span class="ml-auto font-normal normal-case">{activityCount || '—'} action{activityCount === 1 ? '' : 's'}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
{#if activityOpen}
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<ActivityTimeline />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user