fix: restore command-center task focus (#3065)
This commit is contained in:
@@ -217,6 +217,10 @@
|
||||
animation: settings-target-highlight 1400ms ease-out;
|
||||
}
|
||||
|
||||
.task-sidebar-row-reveal {
|
||||
animation: task-sidebar-row-reveal 1400ms ease-out;
|
||||
}
|
||||
|
||||
[data-settings-dirty="true"] {
|
||||
border-color: color-mix(in oklab, var(--success) 65%, var(--border)) !important;
|
||||
}
|
||||
@@ -254,12 +258,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes task-sidebar-row-reveal {
|
||||
0%,
|
||||
35% {
|
||||
background-color: color-mix(in oklab, var(--primary) 28%, transparent);
|
||||
box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--primary) 55%, transparent);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: inset 0 0 0 2px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-settings-target-highlight="true"] {
|
||||
animation: none;
|
||||
background-color: color-mix(in oklab, var(--primary) 10%, transparent);
|
||||
box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--primary) 55%, transparent);
|
||||
}
|
||||
|
||||
.task-sidebar-row-reveal {
|
||||
animation: none;
|
||||
background-color: color-mix(in oklab, var(--primary) 28%, transparent);
|
||||
box-shadow: inset 0 0 0 2px color-mix(in oklab, var(--primary) 55%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* WebKit can leave a fixed dialog's transform animation on a separate layer,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { act, cleanup, render } from "@testing-library/react";
|
||||
import type { DockviewApi } from "dockview-react";
|
||||
import { StateProvider } from "@/components/state-provider";
|
||||
import { defaultState } from "@/lib/state/default-state";
|
||||
import { useDockviewStore } from "@/lib/state/dockview-store";
|
||||
import type { TaskSession, TaskId } from "@/lib/types/http";
|
||||
import { makeReorderingAutoSessionApi } from "./dockview-session-tabs.test-utils";
|
||||
import { useAutoSessionTab } from "./dockview-session-tabs";
|
||||
|
||||
const TASK_ID = "task-1" as TaskId;
|
||||
const ACTIVE_SESSION_ID = "session-a";
|
||||
const SIBLING_SESSION_ID = "session-b";
|
||||
|
||||
function Harness() {
|
||||
useAutoSessionTab(ACTIVE_SESSION_ID);
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderHookWithHydratedSessions() {
|
||||
const sessions = [ACTIVE_SESSION_ID, SIBLING_SESSION_ID].map((id) => ({ id }) as TaskSession);
|
||||
|
||||
return render(
|
||||
<StateProvider
|
||||
initialState={{
|
||||
...defaultState,
|
||||
tasks: {
|
||||
...defaultState.tasks,
|
||||
activeTaskId: TASK_ID,
|
||||
activeSessionId: ACTIVE_SESSION_ID,
|
||||
},
|
||||
taskSessionsByTask: {
|
||||
...defaultState.taskSessionsByTask,
|
||||
itemsByTaskId: { [TASK_ID]: sessions },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Harness />
|
||||
</StateProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
useDockviewStore.setState({ api: null });
|
||||
});
|
||||
|
||||
describe("useAutoSessionTab", () => {
|
||||
it("reconciles every hydrated session when Dockview becomes ready later", () => {
|
||||
// @covers AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.1
|
||||
useDockviewStore.setState({ api: null });
|
||||
renderHookWithHydratedSessions();
|
||||
|
||||
const { api } = makeReorderingAutoSessionApi();
|
||||
act(() => {
|
||||
useDockviewStore.setState({ api: api as DockviewApi });
|
||||
});
|
||||
|
||||
expect(api.panels.map((panel) => panel.id)).toEqual(
|
||||
expect.arrayContaining([`session:${ACTIVE_SESSION_ID}`, `session:${SIBLING_SESSION_ID}`]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -611,6 +611,7 @@ export function useAutoSessionTab(effectiveSessionId: string | null) {
|
||||
const prevTaskIdRef = useRef<string | null>(null);
|
||||
const prevSessionIdRef = useRef<string | null>(null);
|
||||
const appStore = useAppStoreApi();
|
||||
const dockviewApi = useDockviewStore((state) => state.api);
|
||||
|
||||
// Key-based dependency so the effect re-runs when the task's session list
|
||||
// changes (add/remove). Inside the effect we re-read the real array from
|
||||
@@ -629,5 +630,5 @@ export function useAutoSessionTab(effectiveSessionId: string | null) {
|
||||
prevTaskIdRef,
|
||||
prevSessionIdRef,
|
||||
});
|
||||
}, [effectiveSessionId, sessionIdsKey, appStore]);
|
||||
}, [appStore, dockviewApi, effectiveSessionId, sessionIdsKey]);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,54 @@ test.describe("Multi-session UX", () => {
|
||||
await expect(tab2).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test("command panel navigation renders all existing session tabs", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const title = "Command panel multi-session task";
|
||||
const task = await apiClient.createTask(seedData.workspaceId, title, {
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
});
|
||||
const primary = await apiClient.seedTaskSession(task.id, {
|
||||
state: "WAITING_FOR_INPUT",
|
||||
sessionId: `command-panel-primary-${task.id}`,
|
||||
agentProfileId: seedData.agentProfileId,
|
||||
startedAt: "2026-08-26T00:00:00Z",
|
||||
});
|
||||
const secondary = await apiClient.seedTaskSession(task.id, {
|
||||
state: "WAITING_FOR_INPUT",
|
||||
sessionId: `command-panel-secondary-${task.id}`,
|
||||
agentProfileId: seedData.agentProfileId,
|
||||
startedAt: "2026-08-26T00:01:00Z",
|
||||
});
|
||||
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
await expect(kanban.taskCardByTitle(title)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
await testPage.keyboard.press(`${modifier}+k`);
|
||||
const dialog = testPage.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
||||
await dialog.getByRole("combobox").fill(title);
|
||||
const option = dialog.getByRole("option").filter({ hasText: title });
|
||||
await expect(option).toBeVisible({ timeout: 10_000 });
|
||||
await option.click();
|
||||
|
||||
await expect(testPage).toHaveURL(new RegExp(`/t/${task.id}$`));
|
||||
const session = new SessionPage(testPage);
|
||||
await expect(session.sessionTabBySessionId(primary.session_id)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(session.sessionTabBySessionId(secondary.session_id)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("+ dropdown shows sessions with correct numbering", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
|
||||
@@ -349,6 +349,7 @@ test.describe("sidebar scrolling", () => {
|
||||
"aria-current",
|
||||
"true",
|
||||
);
|
||||
await expect(targetRow).toHaveClass(/task-sidebar-row-reveal/, { timeout: 1_000 });
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
TASK_ROW_DOM_ATTR,
|
||||
TASK_ROW_REVEAL_CLASS,
|
||||
cancelSidebarTaskReveal,
|
||||
revealSidebarTask,
|
||||
taskRowSelector,
|
||||
@@ -41,9 +42,18 @@ function mountRow(viewport: HTMLElement, taskId: string, rect: Rect) {
|
||||
return row;
|
||||
}
|
||||
|
||||
function setReducedMotion(reducedMotion: boolean) {
|
||||
vi.stubGlobal(
|
||||
"matchMedia",
|
||||
vi.fn(() => ({ matches: reducedMotion })),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("taskRowSelector", () => {
|
||||
@@ -53,20 +63,32 @@ describe("taskRowSelector", () => {
|
||||
});
|
||||
|
||||
describe("revealSidebarTask", () => {
|
||||
it("scrolls an off-screen rendered row with nearest alignment", async () => {
|
||||
it("smoothly scrolls and cues an off-screen rendered row", async () => {
|
||||
setReducedMotion(false);
|
||||
const viewport = mountViewport();
|
||||
const row = mountRow(viewport, TEST_TASK_ID, { x: 0, y: 120, width: 320, height: 24 });
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
expect(row.scrollIntoView).toHaveBeenCalledWith({ block: "nearest", inline: "nearest" });
|
||||
expect(row.scrollIntoView).toHaveBeenCalledWith({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
});
|
||||
|
||||
it("scrolls an off-screen row above the viewport with nearest alignment", async () => {
|
||||
it("uses immediate nearest scrolling and a non-animated cue for reduced motion", async () => {
|
||||
setReducedMotion(true);
|
||||
const viewport = mountViewport();
|
||||
const row = mountRow(viewport, TEST_TASK_ID, { x: 0, y: -24, width: 320, height: 24 });
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
expect(row.scrollIntoView).toHaveBeenCalledWith({ block: "nearest", inline: "nearest" });
|
||||
expect(row.scrollIntoView).toHaveBeenCalledWith({
|
||||
behavior: "auto",
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not reposition a row that is already inside the viewport", async () => {
|
||||
@@ -75,8 +97,29 @@ describe("revealSidebarTask", () => {
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
expect(row.scrollIntoView).not.toHaveBeenCalled();
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
});
|
||||
|
||||
it("restarts the cue without letting stale cleanup clear the newer cue", async () => {
|
||||
vi.useFakeTimers();
|
||||
const viewport = mountViewport();
|
||||
const row = mountRow(viewport, TEST_TASK_ID, { x: 0, y: 120, width: 320, height: 24 });
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
vi.advanceTimersByTime(1_000);
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
vi.advanceTimersByTime(400);
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
|
||||
vi.advanceTimersByTime(999);
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("revealSidebarTask edge cases", () => {
|
||||
it("finds a row that renders after a later animation frame", async () => {
|
||||
const viewport = mountViewport();
|
||||
const callbacks: Array<() => void> = [];
|
||||
@@ -123,6 +166,19 @@ describe("revealSidebarTask", () => {
|
||||
expect(row.scrollIntoView).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears an applied cue when navigation is canceled", async () => {
|
||||
vi.useFakeTimers();
|
||||
const viewport = mountViewport();
|
||||
const row = mountRow(viewport, TEST_TASK_ID, { x: 0, y: 120, width: 320, height: 24 });
|
||||
|
||||
await expect(revealSidebarTask(TEST_TASK_ID, (callback) => callback())).resolves.toBe(true);
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(true);
|
||||
|
||||
cancelSidebarTaskReveal();
|
||||
|
||||
expect(row.classList.contains(TASK_ROW_REVEAL_CLASS)).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores a matching row inside a hidden sidebar viewport", async () => {
|
||||
const hiddenViewport = mountViewport(false);
|
||||
const hiddenRow = mountRow(hiddenViewport, TEST_TASK_ID, {
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
export const TASK_ROW_DOM_ATTR = "data-task-row-id";
|
||||
export const TASK_SIDEBAR_SCROLL_SELECTOR = '[data-testid="task-sidebar-scroll"]';
|
||||
export const TASK_ROW_REVEAL_CLASS = "task-sidebar-row-reveal";
|
||||
|
||||
const MAX_TASK_NAVIGATION_ATTEMPTS = 60;
|
||||
const TASK_ROW_REVEAL_DURATION_MS = 1400;
|
||||
let latestNavigationRequestId = 0;
|
||||
let latestCueId = 0;
|
||||
|
||||
type ActiveTaskRowCue = {
|
||||
cueId: number;
|
||||
row: HTMLElement;
|
||||
timeoutId: number;
|
||||
};
|
||||
|
||||
let activeTaskRowCue: ActiveTaskRowCue | null = null;
|
||||
|
||||
/** Invalidates the current reveal so a pending selection cannot scroll a stale row. */
|
||||
export function cancelSidebarTaskReveal(): void {
|
||||
latestNavigationRequestId += 1;
|
||||
if (!activeTaskRowCue) return;
|
||||
|
||||
window.clearTimeout(activeTaskRowCue.timeoutId);
|
||||
activeTaskRowCue.row.classList.remove(TASK_ROW_REVEAL_CLASS);
|
||||
activeTaskRowCue = null;
|
||||
}
|
||||
|
||||
/** CSS selector for a rendered task row by its stable task id. */
|
||||
@@ -63,6 +79,35 @@ function defaultRequestFrame(callback: () => void): void {
|
||||
}
|
||||
}
|
||||
|
||||
function prefersReducedMotion(): boolean {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.matchMedia === "function" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
);
|
||||
}
|
||||
|
||||
/** Restarts the short-lived cue on the latest command-selected row. */
|
||||
function cueTaskRow(row: HTMLElement): void {
|
||||
if (activeTaskRowCue) {
|
||||
window.clearTimeout(activeTaskRowCue.timeoutId);
|
||||
activeTaskRowCue.row.classList.remove(TASK_ROW_REVEAL_CLASS);
|
||||
}
|
||||
|
||||
const cueId = ++latestCueId;
|
||||
row.classList.remove(TASK_ROW_REVEAL_CLASS);
|
||||
// Force a reflow so selecting the same task twice restarts its animation.
|
||||
void row.offsetWidth;
|
||||
row.classList.add(TASK_ROW_REVEAL_CLASS);
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (activeTaskRowCue?.cueId !== cueId) return;
|
||||
row.classList.remove(TASK_ROW_REVEAL_CLASS);
|
||||
activeTaskRowCue = null;
|
||||
}, TASK_ROW_REVEAL_DURATION_MS);
|
||||
activeTaskRowCue = { cueId, row, timeoutId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals a rendered task row in the visible desktop sidebar.
|
||||
*
|
||||
@@ -92,8 +137,13 @@ export function revealSidebarTask(
|
||||
return;
|
||||
}
|
||||
if (!isInsideViewport(match.row, match.viewport)) {
|
||||
match.row.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
match.row.scrollIntoView({
|
||||
behavior: prefersReducedMotion() ? "auto" : "smooth",
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
});
|
||||
}
|
||||
cueTaskRow(match.row);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
spec: docs/specs/ui/requirements/task-agent-tab-reconciliation.md
|
||||
created: 2026-08-26
|
||||
status: implemented
|
||||
---
|
||||
|
||||
# Implementation Plan: Command-center Task Focus
|
||||
|
||||
## Overview
|
||||
|
||||
Repair two UI outcomes that follow command-panel task selection. First, make
|
||||
Agent-tab reconciliation react when Dockview becomes ready after the selected
|
||||
task's sessions have already hydrated. Second, extend the existing sidebar
|
||||
reveal with smooth minimum-distance scrolling and a transient row cue that
|
||||
respects reduced-motion preferences.
|
||||
|
||||
Applicable contracts:
|
||||
|
||||
- `docs/specs/ui/requirements/task-agent-tab-reconciliation.md`
|
||||
- `docs/specs/ui/system-design/task-agent-tab-reconciliation.md`
|
||||
- `docs/specs/ui/requirements/command-panel-sidebar-task-reveal.md`
|
||||
- `docs/specs/ui/system-design/command-panel-sidebar-task-reveal.md`
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
`useAutoSessionTab` reacts to the effective session and the active task's
|
||||
session-ID key. Its effect calls `runAutoSessionTabEffect`, which reads the
|
||||
Dockview API through `useDockviewStore.getState()` and returns when that API is
|
||||
null. The hook does not subscribe to the API value. Route hydration can settle
|
||||
before Dockview `onReady`. In that order, API publication does not change an
|
||||
effect dependency. The sibling Agent panels remain absent until another event,
|
||||
such as reload, runs reconciliation.
|
||||
|
||||
A temporary React hook test reproduced this order: hydrate task sessions with
|
||||
a null API, render the hook, then publish a fake API. The test failed because
|
||||
the API was never read after publication. The temporary test was removed after
|
||||
the diagnosis. Task 01 owns the permanent RED regression.
|
||||
|
||||
The sidebar already has bounded route-aware reveal behavior, but
|
||||
`scrollIntoView` has no `behavior` option and the row receives only its normal
|
||||
active state. The existing UI review-target navigation provides the nearest
|
||||
reduced-motion and transient-cue precedent.
|
||||
|
||||
## Delivery order
|
||||
|
||||
Both work orders are frontend-only and have no code dependency. Execute them
|
||||
sequentially in the primary session because implementation delegation has not
|
||||
been authorized.
|
||||
|
||||
- [x] [Task 01: Reconcile Agent tabs on workbench readiness](task-01-reconcile-agent-tabs-on-readiness.md)
|
||||
- [x] [Task 02: Animate and mark the command-selected sidebar row](task-02-animate-command-selected-sidebar-row.md)
|
||||
|
||||
## Verification strategy
|
||||
|
||||
Task 01 uses a hook-level timing regression for the API-late order, existing
|
||||
pure reconciliation tests, and a user-level Cmd+K multi-session scenario.
|
||||
Task 02 uses DOM unit tests for scroll options and cue lifecycle, the existing
|
||||
overflowing-sidebar Playwright scenario, and the existing phone command-panel
|
||||
navigation regression.
|
||||
|
||||
After both work orders pass, run these package-level checks:
|
||||
|
||||
1. `cd apps/web && pnpm run typecheck`
|
||||
2. `cd apps && pnpm --filter @kandev/web lint`
|
||||
3. `cd apps && pnpm --filter @kandev/web build:vite`
|
||||
4. `git diff --check`
|
||||
|
||||
No new user-facing copy is planned. If implementation adds copy, it must add
|
||||
all required locale entries and run the repository i18n checks.
|
||||
|
||||
## Mobile contract
|
||||
|
||||
Phone and tablet keep their existing task route and session controls. They do
|
||||
not mount desktop Dockview tabs or use the hidden desktop sidebar as a scroll
|
||||
target. Task 01 preserves the shared session-membership source, and Task 02
|
||||
retains the existing focused mobile Playwright regression.
|
||||
|
||||
## Risks and boundaries
|
||||
|
||||
- A broad Dockview subscription can cause redundant reconciliation. Subscribe
|
||||
only to API readiness and keep the existing session-key dependency.
|
||||
- Smooth scrolling makes immediate geometry assertions unreliable. Browser
|
||||
coverage must poll for final containment and use unit coverage for the exact
|
||||
`scrollIntoView` options.
|
||||
- Cue cleanup must not let an older timer remove a newer cue.
|
||||
- Filtered and collapsed rows remain intentional no-ops. The repair must not
|
||||
mutate sidebar preferences to expose them.
|
||||
- No backend API, session lifecycle, saved layout, or public documentation
|
||||
contract changes.
|
||||
|
||||
## Results
|
||||
|
||||
Completed on 2026-08-26.
|
||||
|
||||
- Task 01 adds Dockview API readiness to `useAutoSessionTab` dependencies and
|
||||
permanently covers the session-hydrated-before-workbench-ready ordering.
|
||||
- Task 01's focused unit run passed 2 files and 35 tests. Its command-panel
|
||||
multi-session Chromium scenario passed 1 test without a page reload.
|
||||
- Task 02 adds reduced-motion-aware nearest scrolling and a restartable
|
||||
transient row cue. Its focused unit run passed 1 file and 10 tests.
|
||||
- Task 02's desktop reveal scenario passed 1 test, including cue presence,
|
||||
nested-viewport containment, active state, and unchanged document scroll.
|
||||
The existing mobile command-panel scenario passed 1 test.
|
||||
- `pnpm run typecheck`, the web lint, and the Vite production build passed.
|
||||
- The managed E2E runner created disposable backend, fixture-plugin, and web
|
||||
build artifacts. None were added to the change.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
id: "01-reconcile-agent-tabs-on-readiness"
|
||||
title: "Reconcile Agent tabs on workbench readiness"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/requirements/task-agent-tab-reconciliation.md"
|
||||
---
|
||||
|
||||
# Task 01: Reconcile Agent Tabs on Workbench Readiness
|
||||
|
||||
## Outcome
|
||||
|
||||
Opening a task with several sessions renders every Agent tab on the first task
|
||||
view, including when task hydration finishes before Dockview becomes ready.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add a permanent hook regression for API-late readiness.
|
||||
- Make `useAutoSessionTab` react to Dockview API readiness while preserving its
|
||||
existing session and active-panel reconciliation.
|
||||
- Add a command-panel multi-session Playwright scenario with no page reload.
|
||||
|
||||
## Exclusions
|
||||
|
||||
- Session lifecycle, ordering, or backend changes.
|
||||
- Saved layout geometry and non-Agent active-panel rules.
|
||||
- Mobile session-control redesign.
|
||||
|
||||
## Requirements and design
|
||||
|
||||
- `REQ-UI-TASK-AGENT-TAB-RECONCILIATION-001`
|
||||
- `AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.1` through
|
||||
`AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.5`
|
||||
- `docs/specs/ui/system-design/task-agent-tab-reconciliation.md`
|
||||
|
||||
## Implementation acceptance
|
||||
|
||||
1. A hook test fails before the production change when sessions hydrate before
|
||||
the Dockview API, then passes after the API is published.
|
||||
2. API readiness triggers one reconciliation path that reads the latest task
|
||||
and session state. Existing session-key updates continue to reconcile.
|
||||
3. Cmd+K opens a seeded multi-session task and shows all Agent tabs without a
|
||||
reload.
|
||||
|
||||
## TDD and verification
|
||||
|
||||
1. RED: add the API-late hook case, run it against current production code, and
|
||||
record the missing reconciliation assertion.
|
||||
2. Unit GREEN: `cd apps/web && pnpm exec vitest run components/task/dockview-session-tabs.hook.test.tsx components/task/dockview-session-tabs.test.ts`
|
||||
3. Typecheck: `cd apps/web && pnpm run typecheck`
|
||||
4. E2E GREEN: `cd apps/web && pnpm e2e:run --host --no-build tests/session/multi-session-ux.spec.ts --grep "command panel navigation renders all existing session tabs"`
|
||||
|
||||
Make sure that Playwright discovers the focused scenario. Then accept the
|
||||
browser result as evidence.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/components/task/dockview-session-tabs.ts`
|
||||
- `apps/web/components/task/dockview-session-tabs.hook.test.tsx`
|
||||
- `apps/web/e2e/tests/session/multi-session-ux.spec.ts`
|
||||
|
||||
## Dependencies and parallelism
|
||||
|
||||
No code dependency. Execute in the primary session. Do not delegate unless the
|
||||
user explicitly authorizes implementation agents.
|
||||
|
||||
## Results
|
||||
|
||||
Completed on 2026-08-26.
|
||||
|
||||
### RED evidence
|
||||
|
||||
- `pnpm exec vitest run components/task/dockview-session-tabs.hook.test.tsx`
|
||||
failed 1 test because publishing the late Dockview API did not trigger a
|
||||
reconciliation.
|
||||
|
||||
### GREEN evidence
|
||||
|
||||
- `pnpm exec vitest run components/task/dockview-session-tabs.hook.test.tsx components/task/dockview-session-tabs.test.ts` passed 2 files and 35 tests.
|
||||
- `pnpm run typecheck` passed.
|
||||
- `pnpm e2e:run --host --no-build tests/session/multi-session-ux.spec.ts --grep "command panel navigation renders all existing session tabs"` passed 1 Chromium test.
|
||||
|
||||
The implementation reads the current application state inside the existing
|
||||
reconciliation path and only adds the Dockview API readiness subscription.
|
||||
No backend or mobile surface changed.
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
id: "02-animate-command-selected-sidebar-row"
|
||||
title: "Animate and mark the command-selected sidebar row"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
spec: "../../specs/ui/requirements/command-panel-sidebar-task-reveal.md"
|
||||
---
|
||||
|
||||
# Task 02: Animate and Mark the Command-selected Sidebar Row
|
||||
|
||||
## Outcome
|
||||
|
||||
Command-panel task navigation smoothly reveals an off-screen desktop sidebar
|
||||
row and gives the selected row a short visual cue. Reduced-motion users get an
|
||||
immediate scroll and a non-animated cue.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add motion-preference-aware nearest scrolling to the existing reveal helper.
|
||||
- Add a restartable, bounded row cue and CSS reduced-motion behavior.
|
||||
- Extend unit and desktop Playwright coverage while retaining the phone
|
||||
navigation regression.
|
||||
|
||||
## Exclusions
|
||||
|
||||
- Sidebar filtering, expansion, ordering, or persistence changes.
|
||||
- Command-panel search and task-route loading changes.
|
||||
- Mobile task-switcher redesign.
|
||||
|
||||
## Requirements and design
|
||||
|
||||
- `REQ-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001`
|
||||
- `AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.1` through
|
||||
`AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.10`
|
||||
- `docs/specs/ui/system-design/command-panel-sidebar-task-reveal.md`
|
||||
|
||||
## Implementation acceptance
|
||||
|
||||
1. Off-screen rows use smooth nearest scrolling in normal motion and immediate
|
||||
nearest scrolling under reduced motion. Already visible rows do not move.
|
||||
2. Every successful reveal restarts one transient cue on the current row. An
|
||||
older request or cleanup timer cannot mark or clear the newer target.
|
||||
3. Desktop E2E proves final nested-viewport containment, active state, cue
|
||||
presence, and unchanged document scroll. Phone navigation remains direct.
|
||||
|
||||
## TDD and verification
|
||||
|
||||
1. RED: extend the DOM unit tests with exact smooth-scroll, reduced-motion, and
|
||||
cue lifecycle assertions before changing production code.
|
||||
2. Unit GREEN: `cd apps/web && pnpm exec vitest run lib/sidebar/task-navigation.test.ts`
|
||||
3. Typecheck: `cd apps/web && pnpm run typecheck`
|
||||
4. Desktop E2E GREEN: `cd apps/web && pnpm e2e:run --host --no-build tests/task/sidebar-scroll-preservation.spec.ts --grep "reveals a command-selected task$"`
|
||||
5. Mobile E2E GREEN: `cd apps/web && pnpm e2e:run --host --no-build --project mobile-chrome tests/task/mobile-command-panel-task-navigation.spec.ts`
|
||||
|
||||
Make sure that Playwright discovers each focused scenario. Then accept each
|
||||
browser result as evidence.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/lib/sidebar/task-navigation.ts`
|
||||
- `apps/web/lib/sidebar/task-navigation.test.ts`
|
||||
- `apps/web/app/globals.css`
|
||||
- `apps/web/e2e/tests/task/sidebar-scroll-preservation.spec.ts`
|
||||
- `apps/web/e2e/tests/task/mobile-command-panel-task-navigation.spec.ts`
|
||||
|
||||
## Dependencies and parallelism
|
||||
|
||||
No code dependency. Execute in the primary session. Do not delegate unless the
|
||||
user explicitly authorizes implementation agents.
|
||||
|
||||
## Results
|
||||
|
||||
Completed on 2026-08-26.
|
||||
|
||||
### RED evidence
|
||||
|
||||
- The extended DOM suite failed 4 expected assertions for smooth and reduced
|
||||
motion options, visible-row cueing, and cue restart behavior.
|
||||
|
||||
### GREEN evidence
|
||||
|
||||
- `pnpm exec vitest run lib/sidebar/task-navigation.test.ts` passed 1 file and 10 tests.
|
||||
- `pnpm run typecheck` passed.
|
||||
- `pnpm e2e:run --host --no-build tests/task/sidebar-scroll-preservation.spec.ts --grep "reveals a command-selected task$"` passed 1 Chromium test.
|
||||
- `pnpm e2e:run --host --no-build --project mobile-chrome tests/task/mobile-command-panel-task-navigation.spec.ts` passed 1 mobile test.
|
||||
- The web lint, Vite production build, and `git diff --check` passed.
|
||||
|
||||
The helper keeps one latest-row cue, guards cleanup by generation, and leaves
|
||||
hidden or missing rows as no-ops. The existing mobile test remains unchanged
|
||||
and confirms that phone navigation does not target the desktop sidebar.
|
||||
@@ -128,6 +128,7 @@ contract that other capabilities can reuse.
|
||||
- [Nested Submodule Review](requirements/submodule-review.md)
|
||||
- [Task Confirmation Warning Hierarchy](requirements/confirmation-warning-hierarchy.md)
|
||||
- [Task Layout Profiles](requirements/task-layout-profiles.md)
|
||||
- [Task Agent Tab Reconciliation](requirements/task-agent-tab-reconciliation.md)
|
||||
- [Task Listing Display Preferences](requirements/task-listing-display-preferences.md)
|
||||
- [Task transcript history visibility](requirements/task-prompt-transcript-visibility.md)
|
||||
- [Task Review Shortcut Switcher](requirements/task-review-shortcut.md)
|
||||
@@ -168,6 +169,8 @@ contract that other capabilities can reuse.
|
||||
- [Task Confirmation Warning Hierarchy](system-design/confirmation-warning-hierarchy.md)
|
||||
- [Resizable Markdown Table Columns](system-design/resizable-markdown-tables.md)
|
||||
- [Task Layout Profiles](system-design/task-layout-profiles.md)
|
||||
- [Task Agent Tab Reconciliation](system-design/task-agent-tab-reconciliation.md)
|
||||
- [Command-panel Sidebar Task Reveal](system-design/command-panel-sidebar-task-reveal.md)
|
||||
- [Terminal Rendering](system-design/terminal-rendering.md)
|
||||
- [Task Transcript History Visibility](system-design/task-prompt-transcript-visibility.md)
|
||||
|
||||
|
||||
@@ -1,105 +1,69 @@
|
||||
---
|
||||
status: draft
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-08-05
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
|
||||
# Command-panel Sidebar Task Reveal Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
When the desktop sidebar contains more tasks than fit in its scroll viewport, opening a task from Cmd+K can leave the newly active row above or below the visible portion of the list. The workbench changes correctly, but the sidebar no longer provides visible context for where that task sits.
|
||||
When the desktop task list overflows, a task opened from Cmd+K can be outside
|
||||
the visible part of the sidebar. The UI owns the scroll and visual-feedback
|
||||
contract that helps the user find the newly active task without changing task
|
||||
state or sidebar preferences.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001: Command-panel Sidebar Task Reveal
|
||||
|
||||
**Intent:** When the desktop sidebar contains more tasks than fit in its scroll viewport, opening a task from Cmd+K can leave the newly active row above or below the visible portion of the list. The workbench changes correctly, but the sidebar no longer provides visible context for where that task sits.
|
||||
**Intent:** Keep the command-selected task visible and easy to locate in the
|
||||
desktop sidebar after navigation.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.1:** Selecting a task from Cmd+K continues to open the task's canonical route.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.2:** If that task already has a rendered row in the visible desktop task sidebar, the sidebar scrolls by the minimum amount needed to bring the row inside its own task-list viewport.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.3:** The revealed row carries the normal active-task highlight after navigation. The reveal does not move keyboard focus, scroll the document, or reset the task list to its top.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.4:** A row that is already fully visible is not unnecessarily repositioned.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.5:** Reveal waits through bounded asynchronous route and sidebar rendering instead of relying on a fixed delay. Command-panel selection queues the task ID while guarded navigation is pending and starts the reveal only after the canonical task route and matching active-task state render.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.6:** If another command-panel task is selected before the previous reveal finishes, the latest request immediately invalidates the earlier one; a late row from the earlier request must never be scrolled into view, even while the newer route is waiting behind a navigation guard.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.7:** If the active sidebar view filters the task out, a saved group or subtask branch hides it, or the desktop sidebar is not rendered, task navigation still succeeds and sidebar preferences remain unchanged.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.8:** **GIVEN** an overflowing desktop task sidebar and a Cmd+K result whose rendered row is below the task-list viewport, **WHEN** the user selects that result, **THEN** the task route opens and the active row is fully inside the task-list viewport.
|
||||
|
||||
## Migrated source detail
|
||||
|
||||
## Why
|
||||
|
||||
When the desktop sidebar contains more tasks than fit in its scroll viewport, opening a task from
|
||||
Cmd+K can leave the newly active row above or below the visible portion of the list. The workbench
|
||||
changes correctly, but the sidebar no longer provides visible context for where that task sits.
|
||||
|
||||
## What
|
||||
|
||||
- Selecting a task from Cmd+K continues to open the task's canonical route.
|
||||
- If that task already has a rendered row in the visible desktop task sidebar, the sidebar scrolls
|
||||
by the minimum amount needed to bring the row inside its own task-list viewport.
|
||||
- The revealed row carries the normal active-task highlight after navigation. The reveal does not
|
||||
move keyboard focus, scroll the document, or reset the task list to its top.
|
||||
- A row that is already fully visible is not unnecessarily repositioned.
|
||||
- Reveal waits through bounded asynchronous route and sidebar rendering instead of relying on a
|
||||
fixed delay. Command-panel selection queues the task ID while guarded navigation is pending and
|
||||
starts the reveal only after the canonical task route and matching active-task state render.
|
||||
- If another command-panel task is selected before the previous reveal finishes, the latest request
|
||||
immediately invalidates the earlier one; a late row from the earlier request must never be
|
||||
scrolled into view, even while the newer route is waiting behind a navigation guard.
|
||||
- If the active sidebar view filters the task out, a saved group or subtask branch hides it, or the
|
||||
desktop sidebar is not rendered, task navigation still succeeds and sidebar preferences remain
|
||||
unchanged.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- Failure to find a rendered sidebar row is a no-op for the sidebar after the bounded reveal
|
||||
attempt; it never blocks or reverses task navigation.
|
||||
- A hidden desktop sidebar must not become a scroll target on phone-sized viewports.
|
||||
|
||||
## Mobile contract
|
||||
|
||||
- Phone navigation keeps its existing direct task-route outcome. Cmd+K does not open the mobile
|
||||
task-switcher sheet or interact with the CSS-hidden desktop sidebar.
|
||||
- The existing mobile task switcher remains the nearest shipped navigation exemplar: an inset
|
||||
bottom drawer with its own vertical scroll owner. This repair does not change that composition,
|
||||
its safe-area handling, or its touch targets.
|
||||
- Desktop and phone continue to share task search and route navigation; only the desktop sidebar
|
||||
receives the additional reveal behavior.
|
||||
|
||||
## Scenarios
|
||||
|
||||
- **GIVEN** an overflowing desktop task sidebar and a Cmd+K result whose rendered row is below the
|
||||
task-list viewport, **WHEN** the user selects that result, **THEN** the task route opens and the
|
||||
active row is fully inside the task-list viewport.
|
||||
- **GIVEN** an overflowing desktop task sidebar and a Cmd+K result whose rendered row is above the
|
||||
task-list viewport, **WHEN** the user selects that result, **THEN** the task route opens and the
|
||||
active row is fully inside the task-list viewport.
|
||||
- **GIVEN** a dirty settings page that blocks navigation after a Cmd+K task selection, **WHEN** the
|
||||
user confirms leaving after the initial reveal retry budget expires, **THEN** the task route opens
|
||||
and the active row is still revealed after the guarded navigation completes.
|
||||
- **GIVEN** two successive Cmd+K task selections where the first row is initially missing, **WHEN**
|
||||
the second task is revealed and the first row later renders, **THEN** only the second task is
|
||||
scrolled into view.
|
||||
- **GIVEN** a Cmd+K task result whose sidebar row is already fully visible, **WHEN** the user selects
|
||||
it, **THEN** task navigation succeeds without an unnecessary sidebar jump.
|
||||
- **GIVEN** a Cmd+K task result excluded by the active sidebar view or hidden by a persisted
|
||||
collapse, **WHEN** the user selects it, **THEN** task navigation succeeds without changing that
|
||||
view or collapse preference.
|
||||
- **GIVEN** a phone viewport, **WHEN** the user selects a task through Cmd+K, **THEN** the task route
|
||||
opens and no hidden desktop sidebar becomes the reveal target.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.1:** Selecting a task from
|
||||
Cmd+K shall open the task's canonical route.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.2:** When the selected task has
|
||||
a rendered row outside the visible desktop task-list viewport, the sidebar
|
||||
shall scroll smoothly. It shall use the minimum distance that makes the row
|
||||
visible.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.3:** After navigation, the
|
||||
selected row shall keep the normal active-task state and show a transient
|
||||
reveal cue that distinguishes it from adjacent rows.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.4:** When the row is already
|
||||
fully visible, the sidebar shall not reposition it, but the transient reveal
|
||||
cue shall still identify it.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.5:** The reveal shall not move
|
||||
keyboard focus, scroll the document, reset the task list, or change a saved
|
||||
sidebar view or collapse preference.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.6:** The reveal shall wait
|
||||
through bounded asynchronous route and sidebar rendering. It shall start
|
||||
only after the canonical route and matching active-task state render.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.7:** When another task is
|
||||
selected before an earlier reveal finishes, the latest request shall cancel
|
||||
the earlier request. The stale row shall not scroll or receive the cue.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.8:** When the active sidebar
|
||||
view, a collapsed branch, or a hidden desktop sidebar omits the row, task
|
||||
navigation shall still succeed without changing sidebar preferences.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.9:** When the user prefers
|
||||
reduced motion, the sidebar shall use immediate minimum-distance scrolling
|
||||
and a non-animated transient cue.
|
||||
- **AC-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001.10:** On a phone viewport,
|
||||
Cmd+K shall keep the existing direct task-route outcome and shall not target
|
||||
the CSS-hidden desktop sidebar.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing sidebar filters, the active saved view, collapsed groups, collapsed subtask branches, or
|
||||
task ordering to force a row to render.
|
||||
- Changing task search, task-route loading, or backend task APIs.
|
||||
- Changing filters, saved views, task ordering, or collapsed branches so a row
|
||||
renders.
|
||||
- Expanding the desktop sidebar rail when the user has collapsed it.
|
||||
- Opening or redesigning the mobile task-switcher sheet.
|
||||
- Changing command-panel task search, task-route loading, or backend task APIs.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
- [Command-panel sidebar task reveal](../../../plans/command-panel-sidebar-task-reveal/plan.md)
|
||||
- [Initial command-panel sidebar task reveal](../../../plans/command-panel-sidebar-task-reveal/plan.md)
|
||||
- [Command-center task focus repair](../../../plans/command-center-task-focus/plan.md)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
status: active
|
||||
system: ui
|
||||
created: 2026-08-26
|
||||
owners:
|
||||
- kandev
|
||||
---
|
||||
|
||||
# Task Agent Tab Reconciliation Requirements
|
||||
|
||||
## Overview
|
||||
|
||||
A desktop task can have several agent sessions. The task system owns session
|
||||
membership, while the UI owns the projection of those sessions into task
|
||||
workbench tabs. Every current session must appear without requiring a page
|
||||
refresh, regardless of whether session hydration or workbench readiness
|
||||
finishes first.
|
||||
|
||||
## Requirements
|
||||
|
||||
### REQ-UI-TASK-AGENT-TAB-RECONCILIATION-001: Task Agent Tab Reconciliation
|
||||
|
||||
**Intent:** Show a complete and current set of agent chats whenever a desktop
|
||||
task workbench becomes usable.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
- **AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.1:** When the active task's session
|
||||
list and desktop workbench become ready in either order, the workbench shall
|
||||
show one Agent tab for every current task session. A page refresh shall not
|
||||
be necessary.
|
||||
- **AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.2:** The effective active session
|
||||
shall own the active Agent tab. Other current sessions shall appear as
|
||||
inactive sibling tabs without taking focus from a valid selected non-Agent
|
||||
panel.
|
||||
- **AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.3:** When session membership changes
|
||||
after workbench readiness, the UI shall add new session tabs and remove stale
|
||||
session tabs from the active task.
|
||||
- **AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.4:** When Cmd+K opens a task that
|
||||
already has multiple sessions, all Agent tabs shall appear during the first
|
||||
task render without a manual reload.
|
||||
- **AC-UI-TASK-AGENT-TAB-RECONCILIATION-001.5:** Phone and tablet task surfaces
|
||||
shall continue to project the same task session membership through their
|
||||
existing session controls without mounting desktop workbench tabs.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Changing session creation, lifecycle, ordering, or backend APIs.
|
||||
- Changing saved desktop layout geometry or the active-panel rules for valid
|
||||
non-Agent panels.
|
||||
- Redesigning phone or tablet task navigation and session controls.
|
||||
|
||||
## Implementation plan
|
||||
|
||||
- [Command-center task focus repair](../../../plans/command-center-task-focus/plan.md)
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
requirements:
|
||||
- REQ-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001
|
||||
---
|
||||
|
||||
# Command-panel Sidebar Task Reveal System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
This design extends the existing command-panel navigation with desktop sidebar
|
||||
motion and visual feedback. It does not change route ownership, task search, or
|
||||
sidebar persistence.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REQ-UI-COMMAND-PANEL-SIDEBAR-TASK-REVEAL-001` | [Control flow](#control-flow), [Motion and visual feedback](#motion-and-visual-feedback), [Responsive behavior](#responsive-behavior) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- `use-command-panel-task-navigation.ts` owns the pending selected task ID. It
|
||||
starts a reveal only after the route and active-task state match that ID.
|
||||
- `lib/sidebar/task-navigation.ts` finds the selected row inside a visible
|
||||
`task-sidebar-scroll` viewport. It owns bounded retries, latest-request
|
||||
cancellation, scroll behavior, and the transient cue.
|
||||
- `task-item.tsx` exposes the stable task-row DOM attribute used by the helper.
|
||||
- `globals.css` owns the reveal animation and its reduced-motion variant.
|
||||
|
||||
## Control flow
|
||||
|
||||
1. Cmd+K selection cancels an earlier reveal, records the selected task ID,
|
||||
and starts canonical task navigation.
|
||||
2. The navigation hook waits until both the route and active-task state match.
|
||||
3. The reveal helper searches visible sidebar viewports for the matching row.
|
||||
4. If the row is missing, the helper retries for its bounded animation-frame
|
||||
budget. A newer request invalidates the current generation immediately.
|
||||
5. When the row exists, the helper scrolls it only when it is outside the
|
||||
sidebar viewport. It then restarts the transient cue on that row.
|
||||
|
||||
A missing row ends as a sidebar no-op. It never blocks or reverses task
|
||||
navigation.
|
||||
|
||||
## Motion and visual feedback
|
||||
|
||||
The helper uses `scrollIntoView` with nearest block and inline alignment.
|
||||
Normal motion uses smooth scrolling. Reduced motion uses immediate scrolling.
|
||||
The cue uses a dedicated class on the interactive task row and is removed
|
||||
after a bounded interval. Repeated selection restarts the cue, and a new
|
||||
selection clears any earlier cue before it marks the new row.
|
||||
|
||||
CSS provides an animated emphasis for normal motion. Under
|
||||
`prefers-reduced-motion: reduce`, the same class provides a static visual
|
||||
emphasis until cleanup, with no animation. The existing `aria-current` state
|
||||
remains the durable active-task signal.
|
||||
|
||||
## Responsive behavior
|
||||
|
||||
The helper ignores viewports and rows hidden by responsive CSS. Desktop uses
|
||||
the sidebar animation and cue. Phone Cmd+K navigation continues directly to
|
||||
the task route and uses the existing mobile task and session controls. It does
|
||||
not open the task-switcher sheet or target hidden desktop markup.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
- A filtered or collapsed row exhausts the bounded retry budget and leaves
|
||||
sidebar state unchanged.
|
||||
- Guarded navigation does not consume the reveal budget because the hook does
|
||||
not start the helper until navigation settles.
|
||||
- A superseded generation cannot scroll or mark a stale row.
|
||||
|
||||
## Verification design
|
||||
|
||||
Vitest covers smooth and reduced-motion scroll options, visible-row behavior,
|
||||
cue restart and cleanup, latest-request cancellation, hidden viewports, and
|
||||
bounded failure. The existing sidebar Playwright suite covers command-panel
|
||||
navigation, nested-viewport containment, the transient cue, and unchanged
|
||||
document scroll. The mobile command-panel suite protects direct navigation and
|
||||
the hidden desktop sidebar boundary.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
status: current
|
||||
system: ui
|
||||
requirements:
|
||||
- REQ-UI-TASK-AGENT-TAB-RECONCILIATION-001
|
||||
---
|
||||
|
||||
# Task Agent Tab Reconciliation System Design
|
||||
|
||||
## Purpose and boundaries
|
||||
|
||||
This design makes desktop Agent-tab reconciliation depend on both task session
|
||||
state and Dockview readiness. The task system remains authoritative for session
|
||||
membership and the active session. This UI design only projects that state into
|
||||
the desktop workbench.
|
||||
|
||||
## Requirement mapping
|
||||
|
||||
| Requirement | Design section |
|
||||
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `REQ-UI-TASK-AGENT-TAB-RECONCILIATION-001` | [Components and responsibilities](#components-and-responsibilities), [Control flow](#control-flow), [Responsive behavior](#responsive-behavior) |
|
||||
|
||||
## Components and responsibilities
|
||||
|
||||
- `task-detail-route.tsx` loads the selected task and its session list for the
|
||||
first client render.
|
||||
- `StateHydrator` merges that payload into the application store before the
|
||||
task workbench settles.
|
||||
- `DockviewDesktopLayout` publishes the live `DockviewApi` through the Dockview
|
||||
store when `onReady` fires.
|
||||
- `useAutoSessionTab` observes the effective session, current task session IDs,
|
||||
and Dockview API readiness. It invokes the existing reconciliation body when
|
||||
any of these inputs changes.
|
||||
- `runAutoSessionTabEffect` reads the current application state, removes stale
|
||||
Agent panels, ensures the active session panel, and adds current sibling
|
||||
session panels without activating them.
|
||||
|
||||
## Data and contracts
|
||||
|
||||
No new API, persisted value, or store field is required. The existing
|
||||
`taskSessionsByTask.itemsByTaskId` collection is the session-membership source.
|
||||
The existing Dockview store `api` value is the readiness source.
|
||||
|
||||
## Control flow
|
||||
|
||||
1. Route loading and application-store hydration can finish before or after
|
||||
Dockview calls `onReady`.
|
||||
2. `useAutoSessionTab` subscribes to both sources instead of reading Dockview
|
||||
readiness only inside an effect triggered by session changes.
|
||||
3. The effect does nothing while no Dockview API exists.
|
||||
4. When the API becomes available, the readiness subscription triggers the
|
||||
effect with the latest active task, effective session, and session list.
|
||||
5. Existing reconciliation creates the active session panel first, preserves
|
||||
valid non-Agent focus, and adds all sibling session panels as inactive tabs.
|
||||
6. Later membership or active-session changes use the same path.
|
||||
|
||||
The readiness change is an event source, not a second copy of session state.
|
||||
The effect always reads current application state when it runs.
|
||||
|
||||
## Failure and recovery
|
||||
|
||||
A null Dockview API is a temporary no-op. API readiness causes deterministic
|
||||
reconciliation, so recovery does not depend on a timer, route retry, or page
|
||||
reload. A session that disappears before readiness is not materialized because
|
||||
the effect reads the latest session list.
|
||||
|
||||
## Responsive behavior
|
||||
|
||||
Desktop uses Dockview Agent tabs. Phone and tablet layouts do not mount the
|
||||
desktop Dockview workbench. Their existing session controls continue to read
|
||||
the same application-store membership. No mobile composition or touch target
|
||||
changes.
|
||||
|
||||
## Verification design
|
||||
|
||||
A React hook regression test hydrates a multi-session task while the Dockview
|
||||
API is null. The test publishes the API later and verifies reconciliation.
|
||||
Existing pure reconciliation tests continue to cover active-tab and sibling
|
||||
behavior. A Playwright scenario opens a multi-session task from Cmd+K and
|
||||
verifies that every Agent tab appears without a reload.
|
||||
Reference in New Issue
Block a user