Files
kandev/apps/web/hooks/use-session-layout-state.ts
T
João Salavisa fb56af4e4e fix(web): aggregate changes count across all repos to stop flicker (#986)
Addresses #980 — multi-repo workspaces showed a flickering / stale Changes count because the source hook returned only the last-arriving repo's status. `useSessionChangesCount` now sums file counts across the per-repo `byEnvironmentRepo` map plus commits and is wired into the three call sites; single-repo behaviour is unchanged.
2026-05-22 23:08:46 +01:00

173 lines
5.7 KiB
TypeScript

"use client";
import { useState, useCallback, useMemo } from "react";
import { useAppStore } from "@/components/state-provider";
import { useSessionChangesCount } from "@/hooks/domains/session/use-session-changes-count";
import { getPlanLastSeen } from "@/lib/local-storage";
import { executeApprove } from "@/lib/services/session-approve";
import type { OpenFileTab } from "@/lib/types/backend";
import type { MobileSessionPanel } from "@/lib/state/slices/ui/types";
export type SelectedDiff = {
path: string;
content?: string;
};
type UseSessionLayoutStateOptions = {
sessionId?: string | null;
};
function useSelectedDiffState() {
const [selectedDiff, setSelectedDiff] = useState<SelectedDiff | null>(null);
const handleSelectDiff = useCallback((path: string, content?: string) => {
setSelectedDiff({ path, content });
}, []);
const handleClearSelectedDiff = useCallback(() => {
setSelectedDiff(null);
}, []);
return { selectedDiff, handleSelectDiff, handleClearSelectedDiff };
}
function useOpenFileRequestState() {
const [openFileRequest, setOpenFileRequest] = useState<OpenFileTab | null>(null);
const handleOpenFile = useCallback((file: OpenFileTab) => {
setOpenFileRequest(file);
}, []);
const handleFileOpenHandled = useCallback(() => {
setOpenFileRequest(null);
}, []);
return { openFileRequest, handleOpenFile, handleFileOpenHandled };
}
/**
* Shared hook for session layout state used across mobile, tablet, and desktop layouts.
* Consolidates common state and logic to avoid duplication.
*/
export function useSessionLayoutState(options: UseSessionLayoutStateOptions = {}) {
const { sessionId = null } = options;
// --- Core session state ---
const activeTaskId = useAppStore((state) => state.tasks.activeTaskId);
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const effectiveSessionId = activeSessionId ?? sessionId ?? null;
const sessionKey = effectiveSessionId ?? "";
const activeSession = useAppStore((state) =>
effectiveSessionId ? (state.taskSessions.items[effectiveSessionId] ?? null) : null,
);
const setTaskSession = useAppStore((state) => state.setTaskSession);
// --- Agent state ---
const isAgentWorking = activeSession?.state === "STARTING" || activeSession?.state === "RUNNING";
const isPassthroughMode = useMemo(() => {
if (!activeSession?.agent_profile_snapshot) return false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const snapshot = activeSession.agent_profile_snapshot as any;
return snapshot?.cli_passthrough === true;
}, [activeSession?.agent_profile_snapshot]);
const { selectedDiff, handleSelectDiff, handleClearSelectedDiff } = useSelectedDiffState();
const { openFileRequest, handleOpenFile, handleFileOpenHandled } = useOpenFileRequestState();
// --- Git status for badges ---
// `useSessionChangesCount` reads the per-repo statuses so the badge count
// stays correct in multi-repo workspaces and doesn't flicker as sibling
// repos overwrite the legacy single-status map.
const totalChangesCount = useSessionChangesCount(effectiveSessionId);
// --- Mobile session state (computed before plan badge to use in badge logic) ---
const activePanelBySessionId = useAppStore((state) => state.mobileSession.activePanelBySessionId);
const isTaskSwitcherOpen = useAppStore((state) => state.mobileSession.isTaskSwitcherOpen);
const setMobileSessionPanel = useAppStore((state) => state.setMobileSessionPanel);
const setMobileSessionTaskSwitcherOpen = useAppStore(
(state) => state.setMobileSessionTaskSwitcherOpen,
);
const currentMobilePanel: MobileSessionPanel = effectiveSessionId
? (activePanelBySessionId[effectiveSessionId] ?? "chat")
: "chat";
// --- Plan badge ---
const plan = useAppStore((state) =>
activeTaskId ? state.taskPlans.byTaskId[activeTaskId] : null,
);
const hasUnseenPlanUpdate = useMemo(() => {
// Don't show badge if we're viewing the plan
if (!activeTaskId || !plan || currentMobilePanel === "plan") return false;
if (plan.created_by !== "agent") return false;
const lastSeen = getPlanLastSeen(activeTaskId);
return plan.updated_at !== lastSeen;
}, [activeTaskId, plan, currentMobilePanel]);
// --- Approve button logic ---
const showApproveButton =
!!activeSession?.review_status && activeSession.review_status !== "approved" && !isAgentWorking;
const handleApprove = useCallback(async () => {
if (!effectiveSessionId || !activeTaskId) return;
try {
await executeApprove(effectiveSessionId, activeTaskId, setTaskSession);
} catch (error) {
console.error("Failed to approve session:", error);
}
}, [effectiveSessionId, activeTaskId, setTaskSession]);
const handlePanelChange = useCallback(
(panel: MobileSessionPanel) => {
if (effectiveSessionId) {
setMobileSessionPanel(effectiveSessionId, panel);
}
},
[effectiveSessionId, setMobileSessionPanel],
);
const handleMenuClick = useCallback(() => {
setMobileSessionTaskSwitcherOpen(true);
}, [setMobileSessionTaskSwitcherOpen]);
return {
// Core session
activeTaskId,
activeSessionId,
effectiveSessionId,
sessionKey,
activeSession,
// Agent state
isAgentWorking,
isPassthroughMode,
// Diff selection
selectedDiff,
handleSelectDiff,
handleClearSelectedDiff,
// File open
openFileRequest,
handleOpenFile,
handleFileOpenHandled,
// Git status
totalChangesCount,
// Plan
plan,
hasUnseenPlanUpdate,
// Approve
showApproveButton,
handleApprove,
// Mobile session panel
currentMobilePanel,
handlePanelChange,
isTaskSwitcherOpen,
handleMenuClick,
setMobileSessionTaskSwitcherOpen,
};
}