558 lines
21 KiB
TypeScript
558 lines
21 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState, memo, useMemo } from "react";
|
|
import { PanelRoot, PanelBody } from "./panel-primitives";
|
|
import { getWebSocketClient } from "@/lib/ws/connection";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import { getLocalStorage } from "@/lib/local-storage";
|
|
import { useLayoutStore } from "@/lib/state/layout-store";
|
|
import { usePanelActions } from "@/hooks/use-panel-actions";
|
|
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
|
|
import { SHORTCUTS } from "@/lib/keyboard/constants";
|
|
import { useSessionMessages } from "@/hooks/domains/session/use-session-messages";
|
|
import { useSettingsData } from "@/hooks/domains/settings/use-settings-data";
|
|
import { useCustomPrompts } from "@/hooks/domains/settings/use-custom-prompts";
|
|
import { useSessionState } from "@/hooks/domains/session/use-session-state";
|
|
import { useProcessedMessages } from "@/hooks/use-processed-messages";
|
|
import { useSessionModel } from "@/hooks/domains/session/use-session-model";
|
|
import { useMessageHandler } from "@/hooks/use-message-handler";
|
|
import { useQueue } from "@/hooks/domains/session/use-queue";
|
|
import { useContextFilesStore, type ContextFile } from "@/lib/state/context-files-store";
|
|
import { VirtualizedMessageList } from "@/components/task/chat/virtualized-message-list";
|
|
import {
|
|
ChatInputContainer,
|
|
type ChatInputContainerHandle,
|
|
type MessageAttachment,
|
|
} from "@/components/task/chat/chat-input-container";
|
|
import { type QueuedMessageIndicatorHandle } from "@/components/task/chat/queued-message-indicator";
|
|
import { formatReviewCommentsAsMarkdown } from "@/lib/state/slices/comments/format";
|
|
import { useCommentsStore, isPlanComment } from "@/lib/state/slices/comments";
|
|
import { usePendingDiffCommentsByFile } from "@/hooks/domains/comments/use-diff-comments";
|
|
import { usePendingPlanComments } from "@/hooks/domains/comments/use-pending-comments";
|
|
import type { ContextItem } from "@/lib/types/context";
|
|
import { useIsTaskArchived } from "./task-archived-context";
|
|
import type { DiffComment } from "@/lib/diff/types";
|
|
import type { PlanComment } from "@/lib/state/slices/comments";
|
|
import { buildContextItems } from "./chat-context-items";
|
|
|
|
const EMPTY_CONTEXT_FILES: ContextFile[] = [];
|
|
const PLAN_CONTEXT_PATH = "plan:context";
|
|
|
|
type TaskChatPanelProps = {
|
|
onSend?: (message: string) => void;
|
|
sessionId?: string | null;
|
|
onOpenFile?: (path: string) => void;
|
|
showRequestChangesTooltip?: boolean;
|
|
onRequestChangesTooltipDismiss?: () => void;
|
|
/** Callback to open a file at a specific line (for comment clicks) */
|
|
onOpenFileAtLine?: (filePath: string) => void;
|
|
/** Whether the dockview group containing this panel is focused */
|
|
isPanelFocused?: boolean;
|
|
};
|
|
|
|
function resolveInputPlaceholder(
|
|
isAgentBusy: boolean,
|
|
activeDocumentType: string | undefined,
|
|
planModeEnabled: boolean,
|
|
): string {
|
|
if (isAgentBusy) return "Queue instructions to the agent...";
|
|
if (activeDocumentType === "file") return "Continue working on the file...";
|
|
if (planModeEnabled) return "Continue working on the plan...";
|
|
return "Continue working on the task...";
|
|
}
|
|
|
|
function usePlanMode(resolvedSessionId: string | null, taskId: string | null) {
|
|
const activeDocument = useAppStore((state) =>
|
|
resolvedSessionId
|
|
? (state.documentPanel.activeDocumentBySessionId[resolvedSessionId] ?? null)
|
|
: null,
|
|
);
|
|
const layoutBySession = useLayoutStore((state) => state.columnsBySessionId);
|
|
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 { addPlan } = usePanelActions();
|
|
|
|
const planModeEnabled = useMemo(() => {
|
|
if (!resolvedSessionId || !activeDocument || activeDocument.type !== "plan") return false;
|
|
const layout = layoutBySession[resolvedSessionId];
|
|
return layout?.document === true;
|
|
}, [resolvedSessionId, activeDocument, layoutBySession]);
|
|
|
|
useEffect(() => {
|
|
if (!resolvedSessionId) return;
|
|
const stored = getLocalStorage(`plan-mode-${resolvedSessionId}`, false);
|
|
if (stored) {
|
|
setPlanMode(resolvedSessionId, true);
|
|
addContextFile(resolvedSessionId, { path: PLAN_CONTEXT_PATH, name: "Plan", pinned: true });
|
|
}
|
|
}, [resolvedSessionId, setPlanMode, addContextFile]);
|
|
|
|
const handlePlanModeChange = useCallback(
|
|
(enabled: boolean) => {
|
|
if (!resolvedSessionId || !taskId) return;
|
|
if (enabled) {
|
|
setActiveDocument(resolvedSessionId, { type: "plan", taskId });
|
|
addPlan();
|
|
setPlanMode(resolvedSessionId, true);
|
|
addContextFile(resolvedSessionId, { path: PLAN_CONTEXT_PATH, name: "Plan", pinned: true });
|
|
} else {
|
|
closeDocument(resolvedSessionId);
|
|
setActiveDocument(resolvedSessionId, null);
|
|
setPlanMode(resolvedSessionId, false);
|
|
}
|
|
},
|
|
[resolvedSessionId, taskId, setActiveDocument, addPlan, closeDocument, setPlanMode, addContextFile],
|
|
);
|
|
|
|
return { planModeEnabled, activeDocument, handlePlanModeChange };
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
type CommentsState = {
|
|
planComments: PlanComment[];
|
|
pendingCommentsByFile: Record<string, DiffComment[]>;
|
|
markCommentsSent: (ids: string[]) => void;
|
|
handleRemoveCommentFile: (filePath: string) => void;
|
|
handleRemoveComment: (commentId: string) => void;
|
|
clearSessionPlanComments: () => void;
|
|
};
|
|
|
|
function useCommentsState(resolvedSessionId: string | null): CommentsState {
|
|
const hydrateComments = useCommentsStore((state) => state.hydrateSession);
|
|
useEffect(() => {
|
|
if (resolvedSessionId) hydrateComments(resolvedSessionId);
|
|
}, [resolvedSessionId, hydrateComments]);
|
|
const planComments = usePendingPlanComments();
|
|
const pendingCommentsByFile = usePendingDiffCommentsByFile();
|
|
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],
|
|
);
|
|
return {
|
|
planComments,
|
|
pendingCommentsByFile,
|
|
markCommentsSent,
|
|
handleRemoveCommentFile,
|
|
handleRemoveComment,
|
|
clearSessionPlanComments,
|
|
};
|
|
}
|
|
|
|
type UseChatPanelStateOptions = {
|
|
sessionId: string | null;
|
|
onOpenFile?: (path: string) => void;
|
|
onOpenFileAtLine?: (filePath: string) => void;
|
|
};
|
|
|
|
function useChatPanelState({ sessionId, onOpenFile, onOpenFileAtLine }: UseChatPanelStateOptions) {
|
|
const {
|
|
resolvedSessionId, session, task, taskId, taskDescription,
|
|
isStarting, isWorking, isAgentBusy, isFailed,
|
|
} = useSessionState(sessionId);
|
|
const { planModeEnabled, activeDocument, handlePlanModeChange } = usePlanMode(resolvedSessionId, taskId);
|
|
const {
|
|
contextFiles, removeContextFile, unpinFile, clearEphemeral,
|
|
handleToggleContextFile, handleAddContextFile,
|
|
} = useContextFiles(resolvedSessionId);
|
|
const { addPlan } = usePanelActions();
|
|
const { messages, isLoading: messagesLoading } = useSessionMessages(resolvedSessionId);
|
|
const {
|
|
allMessages, groupedItems, permissionsByToolCallId, childrenByParentToolCallId,
|
|
todoItems, agentMessageCount, pendingClarification,
|
|
} = useProcessedMessages(messages, taskId, resolvedSessionId, taskDescription);
|
|
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 { queuedMessage, isQueued, cancel: cancelQueue, updateContent: updateQueueContent } =
|
|
useQueue(resolvedSessionId);
|
|
const comments = useCommentsState(resolvedSessionId);
|
|
const { prompts } = useCustomPrompts();
|
|
|
|
const planContextEnabled = useMemo(
|
|
() => contextFiles.some((f) => f.path === PLAN_CONTEXT_PATH),
|
|
[contextFiles],
|
|
);
|
|
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,
|
|
taskId,
|
|
}),
|
|
[
|
|
planContextEnabled, contextFiles, resolvedSessionId, removeContextFile, unpinFile, addPlan,
|
|
promptsMap, onOpenFile, comments.pendingCommentsByFile, comments.handleRemoveCommentFile,
|
|
comments.handleRemoveComment, onOpenFileAtLine, comments.planComments,
|
|
comments.clearSessionPlanComments, taskId,
|
|
],
|
|
);
|
|
|
|
return {
|
|
resolvedSessionId, session, task, taskId, taskDescription,
|
|
isStarting, isWorking, isAgentBusy, isFailed,
|
|
planModeEnabled, activeDocument, handlePlanModeChange,
|
|
contextFiles, removeContextFile, unpinFile, clearEphemeral,
|
|
handleToggleContextFile, handleAddContextFile,
|
|
messages, messagesLoading,
|
|
allMessages, groupedItems, permissionsByToolCallId, childrenByParentToolCallId,
|
|
todoItems, agentMessageCount, pendingClarification,
|
|
sessionModel, activeModel, chatSubmitKey, agentCommands,
|
|
queuedMessage, isQueued, cancelQueue, updateQueueContent,
|
|
...comments,
|
|
contextItems, planContextEnabled, prompts,
|
|
};
|
|
}
|
|
|
|
type ChatInputAreaProps = {
|
|
chatInputRef: React.RefObject<ChatInputContainerHandle | null>;
|
|
queuedMessageRef: React.RefObject<QueuedMessageIndicatorHandle | null>;
|
|
clarificationKey: number;
|
|
onClarificationResolved: () => void;
|
|
handleSubmit: (
|
|
message: string,
|
|
reviewComments?: DiffComment[],
|
|
attachments?: MessageAttachment[],
|
|
inlineMentions?: ContextFile[],
|
|
) => Promise<void>;
|
|
handleCancelTurn: () => Promise<void>;
|
|
handleCancelQueue: () => Promise<void>;
|
|
handleQueueEditComplete: () => void;
|
|
showRequestChangesTooltip: boolean;
|
|
onRequestChangesTooltipDismiss?: () => void;
|
|
isPanelFocused?: boolean;
|
|
panelState: ReturnType<typeof useChatPanelState>;
|
|
isSending: boolean;
|
|
};
|
|
|
|
function ChatInputArea({
|
|
chatInputRef,
|
|
queuedMessageRef,
|
|
clarificationKey,
|
|
onClarificationResolved,
|
|
handleSubmit,
|
|
handleCancelTurn,
|
|
handleCancelQueue,
|
|
handleQueueEditComplete,
|
|
showRequestChangesTooltip,
|
|
onRequestChangesTooltipDismiss,
|
|
isPanelFocused,
|
|
panelState,
|
|
isSending,
|
|
}: ChatInputAreaProps) {
|
|
const {
|
|
resolvedSessionId, task, taskId, taskDescription, isStarting, isAgentBusy, isFailed,
|
|
planModeEnabled, activeDocument, handlePlanModeChange,
|
|
contextFiles, handleToggleContextFile, handleAddContextFile,
|
|
pendingCommentsByFile, chatSubmitKey, agentCommands,
|
|
isQueued, queuedMessage, updateQueueContent, contextItems, planContextEnabled, todoItems,
|
|
} = panelState;
|
|
const placeholder = resolveInputPlaceholder(isAgentBusy, activeDocument?.type, planModeEnabled);
|
|
return (
|
|
<div className="bg-card flex-shrink-0 px-2 pb-2 pt-1">
|
|
<ChatInputContainer
|
|
ref={chatInputRef}
|
|
key={clarificationKey}
|
|
onSubmit={handleSubmit}
|
|
sessionId={resolvedSessionId}
|
|
taskId={taskId}
|
|
taskTitle={task?.title}
|
|
taskDescription={taskDescription ?? ""}
|
|
planModeEnabled={planModeEnabled}
|
|
onPlanModeChange={handlePlanModeChange}
|
|
isAgentBusy={isAgentBusy}
|
|
isStarting={isStarting}
|
|
isSending={isSending}
|
|
onCancel={handleCancelTurn}
|
|
placeholder={placeholder}
|
|
pendingClarification={panelState.pendingClarification}
|
|
onClarificationResolved={onClarificationResolved}
|
|
showRequestChangesTooltip={showRequestChangesTooltip}
|
|
onRequestChangesTooltipDismiss={onRequestChangesTooltipDismiss}
|
|
pendingCommentsByFile={pendingCommentsByFile}
|
|
submitKey={chatSubmitKey}
|
|
hasAgentCommands={!!(agentCommands && agentCommands.length > 0)}
|
|
isFailed={isFailed}
|
|
isQueued={isQueued}
|
|
contextItems={contextItems}
|
|
planContextEnabled={planContextEnabled}
|
|
contextFiles={contextFiles}
|
|
onToggleContextFile={handleToggleContextFile}
|
|
onAddContextFile={handleAddContextFile}
|
|
todoItems={todoItems}
|
|
queuedMessage={queuedMessage?.content}
|
|
onCancelQueue={handleCancelQueue}
|
|
updateQueueContent={updateQueueContent}
|
|
queuedMessageRef={queuedMessageRef}
|
|
onQueueEditComplete={handleQueueEditComplete}
|
|
isPanelFocused={isPanelFocused}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function useSubmitHandler(
|
|
panelState: ReturnType<typeof useChatPanelState>,
|
|
onSend?: (message: string) => void,
|
|
) {
|
|
const [isSending, setIsSending] = useState(false);
|
|
const {
|
|
resolvedSessionId, sessionModel, activeModel, planContextEnabled, isAgentBusy, activeDocument,
|
|
planComments, contextFiles, prompts, markCommentsSent, clearSessionPlanComments, clearEphemeral,
|
|
} = panelState;
|
|
const { handleSendMessage } = useMessageHandler({
|
|
resolvedSessionId,
|
|
taskId: panelState.taskId,
|
|
sessionModel,
|
|
activeModel,
|
|
planMode: planContextEnabled,
|
|
isAgentBusy,
|
|
activeDocument,
|
|
planComments,
|
|
contextFiles,
|
|
prompts,
|
|
});
|
|
|
|
const handleSubmit = useCallback(
|
|
async (
|
|
message: string,
|
|
reviewComments?: DiffComment[],
|
|
attachments?: MessageAttachment[],
|
|
inlineMentions?: ContextFile[],
|
|
) => {
|
|
if (isSending) return;
|
|
setIsSending(true);
|
|
try {
|
|
let finalMessage = message;
|
|
if (reviewComments && reviewComments.length > 0) {
|
|
const reviewMarkdown = formatReviewCommentsAsMarkdown(reviewComments);
|
|
finalMessage = reviewMarkdown + (message ? message : "");
|
|
}
|
|
const hasReviewComments = !!(reviewComments && reviewComments.length > 0);
|
|
if (onSend) {
|
|
await onSend(finalMessage);
|
|
} else {
|
|
await handleSendMessage(finalMessage, attachments, hasReviewComments, inlineMentions);
|
|
}
|
|
if (reviewComments && reviewComments.length > 0)
|
|
markCommentsSent(reviewComments.map((c) => c.id));
|
|
if (planComments.length > 0) clearSessionPlanComments();
|
|
if (resolvedSessionId) clearEphemeral(resolvedSessionId);
|
|
} finally {
|
|
setIsSending(false);
|
|
}
|
|
},
|
|
[
|
|
isSending, onSend, handleSendMessage, markCommentsSent,
|
|
planComments.length, clearSessionPlanComments, resolvedSessionId, clearEphemeral,
|
|
],
|
|
);
|
|
|
|
return { isSending, handleSubmit };
|
|
}
|
|
|
|
function useChatPanelHandlers(
|
|
resolvedSessionId: string | null,
|
|
cancelQueue: () => Promise<void>,
|
|
chatInputRef: React.RefObject<ChatInputContainerHandle | null>,
|
|
) {
|
|
const handleCancelTurn = useCallback(async () => {
|
|
if (!resolvedSessionId) return;
|
|
const client = getWebSocketClient();
|
|
if (!client) return;
|
|
try {
|
|
await client.request("agent.cancel", { session_id: resolvedSessionId }, 15000);
|
|
} catch (error) {
|
|
console.error("Failed to cancel agent turn:", error);
|
|
}
|
|
}, [resolvedSessionId]);
|
|
|
|
const handleCancelQueue = useCallback(async () => {
|
|
try {
|
|
await cancelQueue();
|
|
} catch (error) {
|
|
console.error("Failed to cancel queued message:", error);
|
|
}
|
|
}, [cancelQueue]);
|
|
|
|
const handleQueueEditComplete = useCallback(() => {
|
|
chatInputRef.current?.focusInput();
|
|
}, [chatInputRef]);
|
|
|
|
useKeyboardShortcut(
|
|
SHORTCUTS.FOCUS_INPUT,
|
|
useCallback((event: KeyboardEvent) => {
|
|
const el = document.activeElement;
|
|
const isTyping =
|
|
el instanceof HTMLInputElement ||
|
|
el instanceof HTMLTextAreaElement ||
|
|
(el instanceof HTMLElement && el.isContentEditable);
|
|
if (isTyping) return;
|
|
const inputHandle = chatInputRef.current;
|
|
if (inputHandle) {
|
|
event.preventDefault();
|
|
inputHandle.focusInput();
|
|
}
|
|
}, [chatInputRef]),
|
|
{ enabled: true, preventDefault: false },
|
|
);
|
|
|
|
return { handleCancelTurn, handleCancelQueue, handleQueueEditComplete };
|
|
}
|
|
|
|
export const TaskChatPanel = memo(function TaskChatPanel({
|
|
onSend,
|
|
sessionId = null,
|
|
onOpenFile,
|
|
showRequestChangesTooltip = false,
|
|
onRequestChangesTooltipDismiss,
|
|
onOpenFileAtLine,
|
|
isPanelFocused,
|
|
}: TaskChatPanelProps) {
|
|
const isArchived = useIsTaskArchived();
|
|
const lastAgentMessageCountRef = useRef(0);
|
|
const chatInputRef = useRef<ChatInputContainerHandle>(null);
|
|
const queuedMessageRef = useRef<QueuedMessageIndicatorHandle>(null);
|
|
const [clarificationKey, setClarificationKey] = useState(0);
|
|
|
|
useSettingsData(true);
|
|
const panelState = useChatPanelState({ sessionId, onOpenFile, onOpenFileAtLine });
|
|
const { isSending, handleSubmit } = useSubmitHandler(panelState, onSend);
|
|
const {
|
|
resolvedSessionId, session, taskId, isWorking, messagesLoading, groupedItems,
|
|
allMessages, permissionsByToolCallId, childrenByParentToolCallId, agentMessageCount, cancelQueue,
|
|
} = panelState;
|
|
const { handleCancelTurn, handleCancelQueue, handleQueueEditComplete } = useChatPanelHandlers(
|
|
resolvedSessionId,
|
|
cancelQueue,
|
|
chatInputRef,
|
|
);
|
|
|
|
useEffect(() => {
|
|
lastAgentMessageCountRef.current = agentMessageCount;
|
|
}, [agentMessageCount]);
|
|
|
|
const handleClarificationResolved = useCallback(() => setClarificationKey((k) => k + 1), []);
|
|
|
|
return (
|
|
<PanelRoot>
|
|
<PanelBody padding={false}>
|
|
<VirtualizedMessageList
|
|
items={groupedItems}
|
|
messages={allMessages}
|
|
permissionsByToolCallId={permissionsByToolCallId}
|
|
childrenByParentToolCallId={childrenByParentToolCallId}
|
|
taskId={taskId ?? undefined}
|
|
sessionId={resolvedSessionId}
|
|
messagesLoading={messagesLoading}
|
|
isWorking={isWorking}
|
|
sessionState={session?.state}
|
|
worktreePath={session?.worktree_path}
|
|
onOpenFile={onOpenFile}
|
|
/>
|
|
</PanelBody>
|
|
{isArchived ? (
|
|
<div className="bg-muted/50 flex-shrink-0 px-4 py-3 text-center text-sm text-muted-foreground border-t">
|
|
This task is archived and read-only.
|
|
</div>
|
|
) : (
|
|
<ChatInputArea
|
|
chatInputRef={chatInputRef}
|
|
queuedMessageRef={queuedMessageRef}
|
|
clarificationKey={clarificationKey}
|
|
onClarificationResolved={handleClarificationResolved}
|
|
handleSubmit={handleSubmit}
|
|
handleCancelTurn={handleCancelTurn}
|
|
handleCancelQueue={handleCancelQueue}
|
|
handleQueueEditComplete={handleQueueEditComplete}
|
|
showRequestChangesTooltip={showRequestChangesTooltip}
|
|
onRequestChangesTooltipDismiss={onRequestChangesTooltipDismiss}
|
|
isPanelFocused={isPanelFocused}
|
|
panelState={panelState}
|
|
isSending={isSending}
|
|
/>
|
|
)}
|
|
</PanelRoot>
|
|
);
|
|
});
|