Files
José Almeida 3e676e3bb2 feat(web): double-click tab to toggle maximize (#823)
* feat(web): double-click tab to toggle maximize

Adds a dblclick gesture on dockview tabs that toggles maximize/restore on
the containing group, mirroring the existing maximize button. Preview tabs
keep their promote-to-pin shortcut: first dblclick pins, then dblclick
maximizes. Sidebar group is excluded.

* fix(web): add cursor-pointer to tab wrappers for consistency

* fix(web): add cursor-pointer to plan-tab wrapper

* fix(web): make tab dblclick maximize resilient to layout rebuild

Refactor the dblclick-to-maximize handler so it reads the panel's group ID
and store state at call time instead of from a render-time closure, and
guards against firing while the dockview store is mid-layout-restore.

This addresses oscillation when double-clicking a session tab on a task
that has multiple sessions: the maximize call now stops event propagation,
respects isRestoringLayout, and always uses the live api.group.id, so it
remains correct even after dockview rebuilds groups via fromJSON.
2026-05-05 13:55:46 +01:00

59 lines
2.2 KiB
TypeScript

"use client";
import { useEffect, useLayoutEffect } from "react";
import { DockviewDefaultTab, type IDockviewPanelHeaderProps } from "dockview-react";
import { useAppStore } from "@/components/state-provider";
import { useTabMaximizeOnDoubleClick } from "./use-tab-maximize";
/**
* Custom tab component for the Plan panel.
* Shows a small indicator dot when the agent has written/updated the plan but
* the user hasn't focused the Plan panel yet. Focusing the tab clears it.
*/
export function PlanTab(props: IDockviewPanelHeaderProps) {
const { api } = props;
const onDoubleClick = useTabMaximizeOnDoubleClick(api);
const activeTaskId = useAppStore((s) => s.tasks.activeTaskId);
const plan = useAppStore((s) => (activeTaskId ? s.taskPlans.byTaskId[activeTaskId] : null));
const lastSeen = useAppStore((s) =>
activeTaskId ? s.taskPlans.lastSeenUpdatedAtByTaskId[activeTaskId] : undefined,
);
const markTaskPlanSeen = useAppStore((s) => s.markTaskPlanSeen);
// Clear the indicator when the tab becomes active.
useEffect(() => {
const disposable = api.onDidActiveChange((event) => {
if (event.isActive && activeTaskId) markTaskPlanSeen(activeTaskId);
});
return () => disposable.dispose();
}, [api, activeTaskId, markTaskPlanSeen]);
// If the tab is already active when the plan changes (user is viewing it),
// treat updates as immediately seen. Use useLayoutEffect so the seen-mark
// commits before paint — otherwise the dot flashes for one frame between
// the WS update render and the seen-mark render.
const planUpdatedAt = plan?.updated_at;
useLayoutEffect(() => {
if (api.isActive && activeTaskId) markTaskPlanSeen(activeTaskId);
}, [api, activeTaskId, markTaskPlanSeen, planUpdatedAt]);
const hasUnseen = plan?.created_by === "agent" && lastSeen !== plan.updated_at;
return (
<div
data-testid="plan-tab"
className="relative cursor-pointer select-none"
onDoubleClick={onDoubleClick}
>
<DockviewDefaultTab {...props} />
{hasUnseen && (
<span
data-testid="plan-tab-indicator"
className="absolute top-0.5 left-0 size-2 rounded-full bg-primary pointer-events-none"
/>
)}
</div>
);
}