3e676e3bb2
* 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.
59 lines
2.2 KiB
TypeScript
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>
|
|
);
|
|
}
|