639 lines
22 KiB
TypeScript
639 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo } from "react";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { useLayoutStore } from "@/lib/state/layout-store";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
import { usePanelActions } from "@/hooks/use-panel-actions";
|
|
import { useSessionMessages } from "@/hooks/domains/session/use-session-messages";
|
|
import { useCustomPrompts } from "@/hooks/domains/settings/use-custom-prompts";
|
|
import { useSessionState } from "@/hooks/domains/session/use-session-state";
|
|
import {
|
|
deriveSessionInputMode,
|
|
resolvesSteeringAffordance,
|
|
} from "@/hooks/domains/session/session-input-mode";
|
|
import { useSessionMcp } from "@/hooks/domains/session/use-session-mcp";
|
|
import { useProcessedMessages } from "@/hooks/use-processed-messages";
|
|
import { useSessionModel } from "@/hooks/domains/session/use-session-model";
|
|
import { useQueue } from "@/hooks/domains/session/use-queue";
|
|
import { useContextFilesStore, type ContextFile } from "@/lib/state/context-files-store";
|
|
import { useCommentsStore, isPlanComment, type Comment } from "@/lib/state/slices/comments";
|
|
import { usePendingDiffCommentsByFile } from "@/hooks/domains/comments/use-diff-comments";
|
|
import {
|
|
usePendingPlanComments,
|
|
usePendingPRFeedback,
|
|
usePendingWalkthroughComments,
|
|
usePendingAgentMessageComments,
|
|
} from "@/hooks/domains/comments/use-pending-comments";
|
|
import { buildContextItems } from "../chat-context-items";
|
|
import {
|
|
useAutoDisablePlanMode,
|
|
useAutoDisableUnsupportedPlanMode,
|
|
usePlanLayoutHandlers,
|
|
} from "./use-plan-mode-helpers";
|
|
import type { ContextItem } from "@/lib/types/context";
|
|
import type { DiffComment } from "@/lib/diff/types";
|
|
import type {
|
|
AgentMessageComment,
|
|
PlanComment,
|
|
PRFeedbackComment,
|
|
WalkthroughComment,
|
|
} from "@/lib/state/slices/comments";
|
|
import type { ActiveDocument } from "@/lib/state/slices/ui/types";
|
|
import type { BuiltInPreset } from "@/lib/state/layout-manager/presets";
|
|
import { readLastAgentError } from "@/lib/session-last-agent-error";
|
|
import { clarificationTurnIdForSession } from "@/lib/utils/pending-clarification";
|
|
import { usePlanCommentMigration } from "@/hooks/domains/comments/use-plan-comment-migration";
|
|
|
|
const EMPTY_CONTEXT_FILES: ContextFile[] = [];
|
|
const PLAN_CONTEXT_PATH = "plan:context";
|
|
|
|
// Tracks sessions for which the plan layout has already been auto-applied.
|
|
// Module-scoped so it survives TaskChatPanel remounts (dockview's fromJSON
|
|
// tears down and rebuilds the portal-hosted panel, which would otherwise
|
|
// reset a component-local ref and cause the plan preset to be re-applied —
|
|
// clobbering the just-restored saved layout on env switch).
|
|
const autoAppliedPlanSessions = new Set<string>();
|
|
|
|
export type CommentsState = {
|
|
planComments: PlanComment[];
|
|
pendingCommentsByFile: Record<string, DiffComment[]>;
|
|
pendingPRFeedback: PRFeedbackComment[];
|
|
walkthroughComments: WalkthroughComment[];
|
|
messageComments: AgentMessageComment[];
|
|
markCommentsSent: (ids: string[]) => void;
|
|
handleRemoveCommentFile: (filePath: string) => void;
|
|
handleRemoveComment: (commentId: string) => void;
|
|
handleRemovePRFeedback: (commentId: string) => void;
|
|
handleClearPRFeedback: () => void;
|
|
handleRemoveWalkthroughComment: (commentId: string) => void;
|
|
handleClearWalkthroughComments: () => void;
|
|
handleClearMessageComments: () => void;
|
|
clearSessionPlanComments: () => void;
|
|
};
|
|
|
|
function removePendingCommentsForSession(sessionId: string | null, source: Comment["source"]) {
|
|
if (!sessionId) return;
|
|
const state = useCommentsStore.getState();
|
|
for (const id of [...state.pendingForChat]) {
|
|
const comment = state.byId[id];
|
|
if (comment?.sessionId === sessionId && comment.source === source) state.removeComment(id);
|
|
}
|
|
}
|
|
|
|
/** Re-focus chat input after dockview layout rebuild. */
|
|
function useRefocusChatAfterLayout() {
|
|
return useCallback(() => {
|
|
const unsub = useDockviewStore.subscribe((state) => {
|
|
if (!state.isRestoringLayout) {
|
|
unsub();
|
|
requestAnimationFrame(() => {
|
|
document.querySelector<HTMLElement>(".tiptap.ProseMirror")?.focus();
|
|
});
|
|
}
|
|
});
|
|
}, []);
|
|
}
|
|
|
|
type AutoApplyPlanLayoutOpts = {
|
|
enabled?: boolean;
|
|
resolvedSessionId: string | null;
|
|
taskId: string | null;
|
|
sessionMetaPlanMode: boolean;
|
|
currentStepHasPlanMode: boolean;
|
|
setActiveDocument: (sid: string, doc: ActiveDocument | null) => void;
|
|
applyBuiltInPreset: (preset: BuiltInPreset) => void;
|
|
setPlanMode: (sid: string, enabled: boolean) => void;
|
|
addContextFile: (sid: string, file: { path: string; name: string }) => void;
|
|
};
|
|
|
|
/** Auto-apply plan layout when session metadata has plan_mode enabled AND the current
|
|
* step actually configures enable_plan_mode. The step check guards against stale
|
|
* sessionMetaPlanMode from deepMerge hydration preserving deleted metadata keys. */
|
|
function useAutoApplyPlanLayout(opts: AutoApplyPlanLayoutOpts) {
|
|
const {
|
|
enabled = true,
|
|
resolvedSessionId,
|
|
taskId,
|
|
sessionMetaPlanMode,
|
|
currentStepHasPlanMode,
|
|
setActiveDocument,
|
|
applyBuiltInPreset,
|
|
setPlanMode,
|
|
addContextFile,
|
|
} = opts;
|
|
useEffect(() => {
|
|
if (!enabled || !resolvedSessionId || !taskId) return;
|
|
// Reset the guard when plan mode is disabled so future plan-mode steps
|
|
// in the same session can be auto-applied (e.g. after proceeding away and back).
|
|
if (!sessionMetaPlanMode && autoAppliedPlanSessions.has(resolvedSessionId)) {
|
|
autoAppliedPlanSessions.delete(resolvedSessionId);
|
|
return;
|
|
}
|
|
if (autoAppliedPlanSessions.has(resolvedSessionId)) return;
|
|
// Only auto-apply if both the session metadata AND the current step agree on plan mode.
|
|
// sessionMetaPlanMode can be stale (deepMerge hydration preserves deleted keys),
|
|
// so we cross-check with the step's actual configuration.
|
|
if (sessionMetaPlanMode && currentStepHasPlanMode) {
|
|
autoAppliedPlanSessions.add(resolvedSessionId);
|
|
setActiveDocument(resolvedSessionId, { type: "plan", taskId });
|
|
applyBuiltInPreset("plan");
|
|
setPlanMode(resolvedSessionId, true);
|
|
addContextFile(resolvedSessionId, { path: PLAN_CONTEXT_PATH, name: "Plan" });
|
|
}
|
|
}, [
|
|
resolvedSessionId,
|
|
taskId,
|
|
enabled,
|
|
sessionMetaPlanMode,
|
|
currentStepHasPlanMode,
|
|
setActiveDocument,
|
|
applyBuiltInPreset,
|
|
setPlanMode,
|
|
addContextFile,
|
|
]);
|
|
}
|
|
|
|
export function usePlanMode(
|
|
resolvedSessionId: string | null,
|
|
taskId: string | null,
|
|
options: { enableLayoutEffects?: boolean } = {},
|
|
) {
|
|
const enableLayoutEffects = options.enableLayoutEffects ?? true;
|
|
const activeDocument = useAppStore((state) =>
|
|
resolvedSessionId
|
|
? (state.documentPanel.activeDocumentBySessionId[resolvedSessionId] ?? null)
|
|
: null,
|
|
);
|
|
const closeDocument = useLayoutStore((state) => state.closeDocument);
|
|
const setActiveDocument = useAppStore((state) => state.setActiveDocument);
|
|
const setPlanMode = useAppStore((state) => state.setPlanMode);
|
|
const addContextFile = useContextFilesStore((s) => s.addFile);
|
|
const removeContextFile = useContextFilesStore((s) => s.removeFile);
|
|
const applyBuiltInPreset = useDockviewStore((s) => s.applyBuiltInPreset);
|
|
|
|
const planModeFromStore = useAppStore((state) =>
|
|
resolvedSessionId ? (state.chatInput.planModeBySessionId[resolvedSessionId] ?? false) : false,
|
|
);
|
|
const sessionMetaPlanMode = useAppStore((state) =>
|
|
resolvedSessionId
|
|
? state.taskSessions.items[resolvedSessionId]?.metadata?.plan_mode === true
|
|
: false,
|
|
);
|
|
const currentStepHasPlanMode = useAppStore((s) => {
|
|
if (!taskId) return false;
|
|
const task = s.kanban.tasks.find((t) => t.id === taskId);
|
|
const stepId = task?.workflowStepId;
|
|
if (!stepId) return false;
|
|
const step = s.kanban.steps.find((st) => st.id === stepId);
|
|
return step?.events?.on_enter?.some((a) => a.type === "enable_plan_mode") ?? false;
|
|
});
|
|
const planModeEnabled = planModeFromStore;
|
|
const planLayoutVisible = activeDocument?.type === "plan";
|
|
|
|
useAutoApplyPlanLayout({
|
|
enabled: enableLayoutEffects,
|
|
resolvedSessionId,
|
|
taskId,
|
|
sessionMetaPlanMode,
|
|
currentStepHasPlanMode,
|
|
setActiveDocument,
|
|
applyBuiltInPreset,
|
|
setPlanMode,
|
|
addContextFile,
|
|
});
|
|
|
|
useAutoDisablePlanMode({
|
|
enabled: enableLayoutEffects,
|
|
resolvedSessionId,
|
|
taskId,
|
|
sessionMetaPlanMode,
|
|
planModeFromStore,
|
|
applyBuiltInPreset,
|
|
closeDocument,
|
|
setActiveDocument,
|
|
setPlanMode,
|
|
removeContextFile,
|
|
});
|
|
|
|
const refocusChatAfterLayout = useRefocusChatAfterLayout();
|
|
const { togglePlanLayout, handlePlanModeChange } = usePlanLayoutHandlers({
|
|
enabled: enableLayoutEffects,
|
|
resolvedSessionId,
|
|
taskId,
|
|
setActiveDocument,
|
|
applyBuiltInPreset,
|
|
closeDocument,
|
|
setPlanMode,
|
|
addContextFile,
|
|
removeContextFile,
|
|
refocusChatAfterLayout,
|
|
});
|
|
|
|
return {
|
|
planModeEnabled,
|
|
planLayoutVisible,
|
|
activeDocument,
|
|
handlePlanModeChange,
|
|
togglePlanLayout,
|
|
};
|
|
}
|
|
|
|
export function useContextFiles(resolvedSessionId: string | null) {
|
|
const contextFiles = useContextFilesStore((s) =>
|
|
resolvedSessionId
|
|
? (s.filesBySessionId[resolvedSessionId] ?? EMPTY_CONTEXT_FILES)
|
|
: EMPTY_CONTEXT_FILES,
|
|
);
|
|
const hydrateContextFiles = useContextFilesStore((s) => s.hydrateSession);
|
|
const addContextFile = useContextFilesStore((s) => s.addFile);
|
|
const toggleContextFile = useContextFilesStore((s) => s.toggleFile);
|
|
const removeContextFile = useContextFilesStore((s) => s.removeFile);
|
|
const unpinFile = useContextFilesStore((s) => s.unpinFile);
|
|
const clearEphemeral = useContextFilesStore((s) => s.clearEphemeral);
|
|
|
|
useEffect(() => {
|
|
if (resolvedSessionId) hydrateContextFiles(resolvedSessionId);
|
|
}, [resolvedSessionId, hydrateContextFiles]);
|
|
|
|
const handleToggleContextFile = useCallback(
|
|
(file: { path: string; name: string; pinned?: boolean }) => {
|
|
if (resolvedSessionId) toggleContextFile(resolvedSessionId, file);
|
|
},
|
|
[resolvedSessionId, toggleContextFile],
|
|
);
|
|
|
|
const handleAddContextFile = useCallback(
|
|
(file: { path: string; name: string; pinned?: boolean }) => {
|
|
if (resolvedSessionId) addContextFile(resolvedSessionId, file);
|
|
},
|
|
[resolvedSessionId, addContextFile],
|
|
);
|
|
|
|
return {
|
|
contextFiles,
|
|
addContextFile,
|
|
removeContextFile,
|
|
unpinFile,
|
|
clearEphemeral,
|
|
handleToggleContextFile,
|
|
handleAddContextFile,
|
|
};
|
|
}
|
|
|
|
export function useCommentsState(
|
|
resolvedSessionId: string | null,
|
|
taskId: string | null,
|
|
): CommentsState {
|
|
const hydrateComments = useCommentsStore((state) => state.hydrateSession);
|
|
useEffect(() => {
|
|
if (resolvedSessionId) hydrateComments(resolvedSessionId);
|
|
}, [resolvedSessionId, hydrateComments]);
|
|
const planComments = usePendingPlanComments(taskId);
|
|
const pendingCommentsByFile = usePendingDiffCommentsByFile(resolvedSessionId);
|
|
const pendingPRFeedback = usePendingPRFeedback(resolvedSessionId);
|
|
const walkthroughComments = usePendingWalkthroughComments(resolvedSessionId);
|
|
const messageComments = usePendingAgentMessageComments(resolvedSessionId);
|
|
const markCommentsSent = useCommentsStore((state) => state.markCommentsSent);
|
|
const removeComment = useCommentsStore((state) => state.removeComment);
|
|
const clearSessionPlanComments = useCallback(() => {
|
|
const state = useCommentsStore.getState();
|
|
const ids = resolvedSessionId ? state.bySession[resolvedSessionId] : undefined;
|
|
if (!ids) return;
|
|
for (const id of ids) {
|
|
const c = state.byId[id];
|
|
if (c && isPlanComment(c)) state.removeComment(id);
|
|
}
|
|
}, [resolvedSessionId]);
|
|
const handleRemoveCommentFile = useCallback(
|
|
(filePath: string) => {
|
|
const comments = pendingCommentsByFile[filePath] || [];
|
|
for (const comment of comments) removeComment(comment.id);
|
|
},
|
|
[pendingCommentsByFile, removeComment],
|
|
);
|
|
const handleRemoveComment = useCallback(
|
|
(commentId: string) => removeComment(commentId),
|
|
[removeComment],
|
|
);
|
|
const handleRemovePRFeedback = useCallback(
|
|
(commentId: string) => removeComment(commentId),
|
|
[removeComment],
|
|
);
|
|
const handleClearPRFeedback = useCallback(() => {
|
|
removePendingCommentsForSession(resolvedSessionId, "pr-feedback");
|
|
}, [resolvedSessionId]);
|
|
const handleRemoveWalkthroughComment = useCallback(
|
|
(commentId: string) => removeComment(commentId),
|
|
[removeComment],
|
|
);
|
|
const handleClearWalkthroughComments = useCallback(() => {
|
|
removePendingCommentsForSession(resolvedSessionId, "walkthrough");
|
|
}, [resolvedSessionId]);
|
|
const handleClearMessageComments = useCallback(() => {
|
|
removePendingCommentsForSession(resolvedSessionId, "agent-message");
|
|
}, [resolvedSessionId]);
|
|
return {
|
|
planComments,
|
|
pendingCommentsByFile,
|
|
pendingPRFeedback,
|
|
walkthroughComments,
|
|
messageComments,
|
|
markCommentsSent,
|
|
handleRemoveCommentFile,
|
|
handleRemoveComment,
|
|
handleRemovePRFeedback,
|
|
handleClearPRFeedback,
|
|
handleRemoveWalkthroughComment,
|
|
handleClearWalkthroughComments,
|
|
handleClearMessageComments,
|
|
clearSessionPlanComments,
|
|
};
|
|
}
|
|
|
|
type ChatContextItemsOptions = {
|
|
planContextEnabled: boolean;
|
|
contextFiles: ContextFile[];
|
|
resolvedSessionId: string | null;
|
|
removeContextFile: (sid: string, path: string) => void;
|
|
unpinFile: (sid: string, path: string) => void;
|
|
comments: CommentsState;
|
|
taskId: string | null;
|
|
onOpenFile?: (path: string, repo?: string) => void;
|
|
onOpenFileAtLine?: (filePath: string) => void;
|
|
};
|
|
|
|
function useChatContextItems(opts: ChatContextItemsOptions) {
|
|
const {
|
|
planContextEnabled,
|
|
contextFiles,
|
|
resolvedSessionId,
|
|
removeContextFile,
|
|
unpinFile,
|
|
comments,
|
|
taskId,
|
|
onOpenFile,
|
|
onOpenFileAtLine,
|
|
} = opts;
|
|
const { addPlan } = usePanelActions();
|
|
const { prompts } = useCustomPrompts();
|
|
|
|
const promptsMap = useMemo(() => {
|
|
const map = new Map<string, { content: string }>();
|
|
for (const p of prompts) map.set(p.id, { content: p.content });
|
|
return map;
|
|
}, [prompts]);
|
|
|
|
const contextItems = useMemo<ContextItem[]>(
|
|
() =>
|
|
buildContextItems({
|
|
planContextEnabled,
|
|
contextFiles,
|
|
resolvedSessionId,
|
|
removeContextFile,
|
|
unpinFile,
|
|
addPlan,
|
|
promptsMap,
|
|
onOpenFile,
|
|
pendingCommentsByFile: comments.pendingCommentsByFile,
|
|
handleRemoveCommentFile: comments.handleRemoveCommentFile,
|
|
handleRemoveComment: comments.handleRemoveComment,
|
|
onOpenFileAtLine,
|
|
planComments: comments.planComments,
|
|
handleClearPlanComments: comments.clearSessionPlanComments,
|
|
pendingPRFeedback: comments.pendingPRFeedback,
|
|
handleRemovePRFeedback: comments.handleRemovePRFeedback,
|
|
handleClearPRFeedback: comments.handleClearPRFeedback,
|
|
walkthroughComments: comments.walkthroughComments,
|
|
handleRemoveWalkthroughComment: comments.handleRemoveWalkthroughComment,
|
|
handleClearWalkthroughComments: comments.handleClearWalkthroughComments,
|
|
messageComments: comments.messageComments,
|
|
handleClearMessageComments: comments.handleClearMessageComments,
|
|
taskId,
|
|
}),
|
|
[
|
|
planContextEnabled,
|
|
contextFiles,
|
|
resolvedSessionId,
|
|
removeContextFile,
|
|
unpinFile,
|
|
addPlan,
|
|
promptsMap,
|
|
onOpenFile,
|
|
comments.pendingCommentsByFile,
|
|
comments.handleRemoveCommentFile,
|
|
comments.handleRemoveComment,
|
|
onOpenFileAtLine,
|
|
comments.planComments,
|
|
comments.clearSessionPlanComments,
|
|
comments.pendingPRFeedback,
|
|
comments.handleRemovePRFeedback,
|
|
comments.handleClearPRFeedback,
|
|
comments.walkthroughComments,
|
|
comments.handleRemoveWalkthroughComment,
|
|
comments.handleClearWalkthroughComments,
|
|
comments.messageComments,
|
|
comments.handleClearMessageComments,
|
|
taskId,
|
|
],
|
|
);
|
|
|
|
return { contextItems, prompts };
|
|
}
|
|
|
|
function useSessionData(
|
|
resolvedSessionId: string | null,
|
|
session: ReturnType<typeof useSessionState>["session"],
|
|
taskId: string | null,
|
|
taskDescription: string | null,
|
|
) {
|
|
const {
|
|
messages,
|
|
isLoading: messagesLoading,
|
|
isInitialMessagesLoading,
|
|
historyRefreshPending,
|
|
historyInitialized,
|
|
hasMore: hasOlderMessages,
|
|
} = useSessionMessages(resolvedSessionId);
|
|
const turns = useAppStore((state) =>
|
|
resolvedSessionId ? state.turns.bySession[resolvedSessionId] : undefined,
|
|
);
|
|
const currentTurnId = useMemo(
|
|
() => clarificationTurnIdForSession(session?.state, turns),
|
|
[session?.state, turns],
|
|
);
|
|
const lastAgentError = useMemo(() => readLastAgentError(session?.metadata), [session?.metadata]);
|
|
const processed = useProcessedMessages(messages, taskId, resolvedSessionId, taskDescription, {
|
|
initialPromptPreview: session?.metadata?.initial_prompt_preview,
|
|
historyInitialized,
|
|
hasOlderMessages,
|
|
lastAgentError,
|
|
currentTurnId,
|
|
pendingAction: session?.pending_action,
|
|
});
|
|
const { sessionModel, activeModel } = useSessionModel(
|
|
resolvedSessionId,
|
|
session?.agent_profile_id,
|
|
);
|
|
const chatSubmitKey = useAppStore((state) => state.userSettings.chatSubmitKey);
|
|
const agentCommands = useAppStore((state) =>
|
|
resolvedSessionId ? state.availableCommands.bySessionId[resolvedSessionId] : undefined,
|
|
);
|
|
const {
|
|
clearAll: clearQueue,
|
|
editEntry: editQueueEntry,
|
|
removeEntry: removeQueueEntry,
|
|
...queueRest
|
|
} = useQueue(resolvedSessionId);
|
|
return {
|
|
messages,
|
|
messagesLoading,
|
|
isInitialMessagesLoading,
|
|
historyRefreshPending,
|
|
...processed,
|
|
sessionModel,
|
|
activeModel,
|
|
chatSubmitKey,
|
|
agentCommands,
|
|
clearQueue,
|
|
editQueueEntry,
|
|
removeQueueEntry,
|
|
...queueRest,
|
|
};
|
|
}
|
|
|
|
type TodoStatus = "pending" | "in_progress" | "completed" | "failed";
|
|
|
|
export function useSessionTodoItems(
|
|
resolvedSessionId: string | null,
|
|
messageTodos: Array<{ text: string; done?: boolean }>,
|
|
) {
|
|
const storeTodos = useAppStore((s) =>
|
|
resolvedSessionId ? s.sessionTodos.bySessionId[resolvedSessionId] : undefined,
|
|
);
|
|
return useMemo(() => {
|
|
if (storeTodos && storeTodos.length > 0) {
|
|
return storeTodos.map((e: { description: string; status: string }) => ({
|
|
text: e.description,
|
|
done: e.status === "completed",
|
|
status: e.status as TodoStatus,
|
|
}));
|
|
}
|
|
return messageTodos;
|
|
}, [storeTodos, messageTodos]);
|
|
}
|
|
|
|
function deriveQueueAwareSessionInput(
|
|
session: ReturnType<typeof useSessionState>["session"],
|
|
queuedCount: number,
|
|
) {
|
|
const inputMode = deriveSessionInputMode(session, queuedCount);
|
|
return { inputMode, isAgentBusy: inputMode === "queue" };
|
|
}
|
|
|
|
export type UseChatPanelStateOptions = {
|
|
sessionId: string | null;
|
|
taskId?: string | null;
|
|
/** Disable Dockview and plan-layout mutations for embedded multi-panel hosts. */
|
|
disableWorkbenchEffects?: boolean;
|
|
onOpenFile?: (path: string, repo?: string) => void;
|
|
onOpenFileAtLine?: (filePath: string) => void;
|
|
};
|
|
|
|
export function useChatPanelState({
|
|
sessionId,
|
|
taskId: taskIdHint = null,
|
|
disableWorkbenchEffects = false,
|
|
onOpenFile,
|
|
onOpenFileAtLine,
|
|
}: UseChatPanelStateOptions) {
|
|
const sessionState = useSessionState(sessionId, { taskIdHint });
|
|
const { resolvedSessionId, taskId } = sessionState;
|
|
const planMode = usePlanMode(resolvedSessionId, taskId, {
|
|
enableLayoutEffects: !disableWorkbenchEffects,
|
|
});
|
|
const {
|
|
supportsMcp,
|
|
mcpServers,
|
|
attachmentHistory: mcpAttachmentHistory,
|
|
} = useSessionMcp(sessionState.session?.agent_profile_id, sessionState.session?.id);
|
|
const planModeAvailable = supportsMcp;
|
|
|
|
// When MCP is available: full plan mode toggle (layout + chat input state)
|
|
// When MCP is unavailable: layout-only toggle (plan panel visible, but no plan context/border)
|
|
const {
|
|
handlePlanModeChange: rawHandlePlanModeChange,
|
|
togglePlanLayout,
|
|
planModeEnabled,
|
|
planLayoutVisible,
|
|
} = planMode;
|
|
const guardedHandlePlanModeChange = useCallback(
|
|
(enabled: boolean) => {
|
|
if (planModeAvailable) return rawHandlePlanModeChange(enabled);
|
|
// Toggle based on current layout state, ignoring the passed value
|
|
togglePlanLayout(!planLayoutVisible);
|
|
},
|
|
[planModeAvailable, rawHandlePlanModeChange, togglePlanLayout, planLayoutVisible],
|
|
);
|
|
|
|
const hasAgentProfile = Boolean(sessionState.session?.agent_profile_id);
|
|
useAutoDisableUnsupportedPlanMode({
|
|
planModeEnabled,
|
|
hasAgentProfile,
|
|
planModeAvailable,
|
|
resolvedSessionId,
|
|
});
|
|
|
|
const contextFilesState = useContextFiles(resolvedSessionId);
|
|
const { contextFiles, removeContextFile, unpinFile } = contextFilesState;
|
|
const sessionData = useSessionData(
|
|
resolvedSessionId,
|
|
sessionState.session,
|
|
taskId,
|
|
sessionState.taskDescription,
|
|
);
|
|
const comments = useCommentsState(resolvedSessionId, taskId);
|
|
const planCommentMigration = usePlanCommentMigration(taskId);
|
|
|
|
const planContextEnabled = useMemo(
|
|
() => contextFiles.some((f) => f.path === PLAN_CONTEXT_PATH),
|
|
[contextFiles],
|
|
);
|
|
|
|
const { contextItems, prompts } = useChatContextItems({
|
|
planContextEnabled,
|
|
contextFiles,
|
|
resolvedSessionId,
|
|
removeContextFile,
|
|
unpinFile,
|
|
comments,
|
|
taskId,
|
|
onOpenFile,
|
|
onOpenFileAtLine,
|
|
});
|
|
|
|
const todoItems = useSessionTodoItems(resolvedSessionId, sessionData.todoItems);
|
|
|
|
return {
|
|
...sessionState,
|
|
...planMode,
|
|
handlePlanModeChange: guardedHandlePlanModeChange,
|
|
...contextFilesState,
|
|
...sessionData,
|
|
...comments,
|
|
planCommentMigration,
|
|
contextItems,
|
|
planContextEnabled,
|
|
planModeAvailable,
|
|
mcpServers,
|
|
mcpAttachmentHistory,
|
|
prompts,
|
|
todoItems,
|
|
...deriveQueueAwareSessionInput(sessionState.session, sessionData.count),
|
|
supportsSteering: resolvesSteeringAffordance(sessionState.supportsSteering, sessionData.count),
|
|
};
|
|
}
|
|
|
|
/** The value returned by {@link useChatPanelState}, threaded through the chat input area and its extracted hooks. */
|
|
export type ChatPanelState = ReturnType<typeof useChatPanelState>;
|