"use client"; import { memo, useCallback } from "react"; import { Button } from "@kandev/ui/button"; import { IconAlertCircle, IconX } from "@tabler/icons-react"; import { GridSpinner } from "@/components/grid-spinner"; import type { Message, TaskSessionState } from "@/lib/types/http"; import type { RenderItem } from "@/hooks/use-processed-messages"; import { MessageRenderer } from "@/components/task/chat/message-renderer"; import { TurnGroupMessage } from "@/components/task/chat/messages/turn-group-message"; import { PrepareProgress } from "@/components/session/prepare-progress"; import { useAppStore, useAppStoreApi } from "@/components/state-provider"; import { dismissLastAgentError } from "@/lib/api/domains/session-api"; import { type LastAgentError, lastAgentErrorStamp, readLastAgentError, } from "@/lib/session-last-agent-error"; export type MessageListProps = { items: RenderItem[]; messages: Message[]; /** Action messages rendered after the env prep error status in the footer. */ footerActionMessages?: Message[]; permissionsByToolCallId: Map; childrenByParentToolCallId: Map; taskId?: string; sessionId: string | null; messagesLoading: boolean; isWorking: boolean; sessionState?: TaskSessionState; worktreePath?: string; onOpenFile?: (path: string) => void; }; export function getItemKey(item: RenderItem): string { if ( item.type === "turn_group" || item.type === "prepare_progress" || item.type === "agent_error_notice" ) return item.id; return item.message.id; } export function getSessionRunningState(sessionState: string | null | undefined) { return sessionState === "CREATED" || sessionState === "STARTING" || sessionState === "RUNNING"; } export function getLastTurnGroupId(items: RenderItem[]) { for (let i = items.length - 1; i >= 0; i--) { const item = items[i]; if (item.type === "turn_group") return item.id; } return null; } // The chat banner stays visible until the user explicitly dismisses it, even // after the agent resumes — so the user can read the full error message at // their own pace. The sidebar icon, by contrast, also auto-hides once the // agent posts a new message (see agentErrorMessageForTask). export function LastAgentErrorNotice({ sessionId, error, }: { sessionId: string | null; error: LastAgentError | null; }) { const stamp = error ? lastAgentErrorStamp(error) : ""; const dismissedStamp = useAppStore((state) => sessionId ? state.dismissedAgentErrors[sessionId] : undefined, ); const dismissAgentError = useAppStore((state) => state.dismissAgentError); const setTaskSession = useAppStore((state) => state.setTaskSession); const store = useAppStoreApi(); const dismiss = useCallback(() => { if (!sessionId || !stamp) return; void dismissLastAgentError(sessionId, stamp) .then((resp) => { const current = readLastAgentError( store.getState().taskSessions.items[sessionId]?.metadata, ); if (current && lastAgentErrorStamp(current) !== stamp) return; dismissAgentError(sessionId, stamp); setTaskSession(resp.session); }) .catch((err: unknown) => { console.error("Failed to dismiss last agent error", err); }); }, [dismissAgentError, sessionId, stamp, setTaskSession, store]); if (!error || dismissedStamp === stamp) return null; return (
); } export function MessageListStatus({ isLoadingMore, hasMore, showLoadingState, messagesLoading, isInitialLoading, messagesCount, onLoadMore, }: { isLoadingMore: boolean; hasMore: boolean; showLoadingState: boolean; messagesLoading: boolean; isInitialLoading: boolean; messagesCount: number; /** * Explicitly load the previous page of older messages. Rendered as a button so * older history is always reachable even when the scroll-up IntersectionObserver * fails to re-arm (e.g. pinned at the very top with the sentinel always in view). */ onLoadMore?: () => void; }) { return ( <> {isLoadingMore && hasMore && (
Loading older messages...
)} {hasMore && !isLoadingMore && onLoadMore && (
)} {showLoadingState && (
Loading messages...
)} {!messagesLoading && !isInitialLoading && messagesCount === 0 && (
No messages yet. Start the conversation!
)} ); } export const MessageItem = memo(function MessageItem({ item, sessionId, permissionsByToolCallId, childrenByParentToolCallId, taskId, worktreePath, onOpenFile, isLastGroup, isTurnActive, onScrollToMessage, }: { item: RenderItem; sessionId: string | null; permissionsByToolCallId: Map; childrenByParentToolCallId: Map; taskId?: string; worktreePath?: string; onOpenFile?: (path: string) => void; isLastGroup: boolean; isTurnActive: boolean; onScrollToMessage: (id: string) => void; }) { if (item.type === "prepare_progress") { return ; } if (item.type === "agent_error_notice") { return ; } if (item.type === "turn_group") { return ( ); } return ( ); });