diff --git a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte index 6adcb63c5..cfa355a68 100644 --- a/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte +++ b/web/workspace/src/lib/workspace/sidebar/WorkersNavSection.svelte @@ -11,7 +11,12 @@ workspaceWorkersStore, type SidebarWorker, } from './worker-subscription'; - import { canShowWorkerInSidebar, sidebarWorkerActivity } from './workers'; + import { + canShowWorkerInSidebar, + sidebarWorkerActivity, + visibleWorkersForSidebar, + workerOwnsSidebarPath, + } from './workers'; import { sidebarWorkdirMeta } from './worker-workdir-meta'; const COLLAPSED_WORKER_COUNT = 6; @@ -32,7 +37,12 @@ let menuTrigger = $state(null); let busyAction = $state<{ workerKey: string; kind: WorkerActionKind } | null>(null); let visibleWorkers = $derived( - expanded ? workers : workers.slice(0, COLLAPSED_WORKER_COUNT), + visibleWorkersForSidebar(workers, { + workspaceId, + currentPath, + expanded, + limit: COLLAPSED_WORKER_COUNT, + }), ); let hiddenWorkerCount = $derived( Math.max(0, workers.length - COLLAPSED_WORKER_COUNT), @@ -173,13 +183,14 @@ {@const activity = sidebarWorkerActivity(worker)} {@const key = workerKey(worker)} {@const label = worker.display_name || worker.label} + {@const active = workerOwnsSidebarPath(workspaceId, worker, currentPath)} {@const workdir = sidebarWorkdirMeta(worker.workdir_attachments)}
  • {#if activity === 'worker-running'} diff --git a/web/workspace/src/lib/workspace/sidebar/workers.test.ts b/web/workspace/src/lib/workspace/sidebar/workers.test.ts index 02f6a8950..c3cc83454 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.test.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.test.ts @@ -3,6 +3,8 @@ import { canShowWorkerInSidebar, compareWorkersForSidebar, sidebarWorkerActivity, + visibleWorkersForSidebar, + workerOwnsSidebarPath, } from "./workers.ts"; import type { Worker } from "./types.ts"; @@ -79,6 +81,39 @@ Deno.test("sidebar workers sort running then idle then stopped", () => { assertEquals(workers.map((candidate) => candidate.worker_id).join(","), "2,1,4,3"); }); +Deno.test("collapsed sidebar keeps the current Worker visible", () => { + const workers = Array.from({ length: 8 }, (_, index) => + worker({ + worker_id: String(index + 1), + resource_key: `W-${index + 1}`, + display_name: `Worker ${index + 1}`, + })); + const currentPath = "/w/workspace-1/workers/W-8-worker-8/console"; + + assertEquals( + workerOwnsSidebarPath("workspace-1", workers[7], currentPath), + true, + ); + assertEquals( + visibleWorkersForSidebar(workers, { + workspaceId: "workspace-1", + currentPath, + expanded: false, + limit: 6, + }).map((candidate) => candidate.resource_key).join(","), + "W-1,W-2,W-3,W-4,W-5,W-8", + ); + assertEquals( + visibleWorkersForSidebar(workers, { + workspaceId: "workspace-1", + currentPath, + expanded: true, + limit: 6, + }).length, + 8, + ); +}); + Deno.test("fatal child stop clears the sidebar SubWorker spinner activity", () => { const parent = { state: "idle", has_running_internal_workers: true }; assertEquals(sidebarWorkerActivity(parent), "subworker-running"); diff --git a/web/workspace/src/lib/workspace/sidebar/workers.ts b/web/workspace/src/lib/workspace/sidebar/workers.ts index ba44bc2ff..f648540d2 100644 --- a/web/workspace/src/lib/workspace/sidebar/workers.ts +++ b/web/workspace/src/lib/workspace/sidebar/workers.ts @@ -1,3 +1,5 @@ +import { workerHref } from '../resource-links'; +import { ownsRoutePath } from './route-ownership'; import type { Worker } from './types'; export type SidebarWorkerActivity = @@ -31,6 +33,38 @@ export function canOpenWorkerConsole( return canShowWorkerInSidebar(worker); } +type SidebarWorkerLink = Pick; + +export function workerOwnsSidebarPath( + workspaceId: string, + worker: SidebarWorkerLink, + currentPath: string, +): boolean { + return ownsRoutePath(workerHref(workspaceId, worker), currentPath); +} + +export function visibleWorkersForSidebar( + workers: readonly T[], + options: { + workspaceId: string; + currentPath: string; + expanded: boolean; + limit: number; + }, +): readonly T[] { + if (options.expanded || workers.length <= options.limit) return workers; + if (options.limit <= 0) return []; + + const visible = workers.slice(0, options.limit); + const currentIndex = workers.findIndex((worker) => + workerOwnsSidebarPath(options.workspaceId, worker, options.currentPath) + ); + if (currentIndex < options.limit) return visible; + + visible[visible.length - 1] = workers[currentIndex]; + return visible; +} + type SortableWorker = Pick< Worker, 'state' | 'display_name' | 'runtime_id' | 'worker_id'