171 lines
5.0 KiB
TypeScript
171 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@kandev/ui/button";
|
|
import { GridSpinner } from "@/components/grid-spinner";
|
|
import type { Message, TaskSessionState, TaskState } 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";
|
|
|
|
export type MessageListProps = {
|
|
items: RenderItem[];
|
|
messages: Message[];
|
|
/** Action messages rendered after the env prep error status in the footer. */
|
|
footerActionMessages?: Message[];
|
|
permissionsByToolCallId: Map<string, Message>;
|
|
childrenByParentToolCallId: Map<string, Message[]>;
|
|
taskId?: string;
|
|
sessionId: string | null;
|
|
messagesLoading: boolean;
|
|
isWorking: boolean;
|
|
sessionState?: TaskSessionState;
|
|
taskState?: TaskState;
|
|
worktreePath?: string;
|
|
onOpenFile?: (path: string) => void;
|
|
};
|
|
|
|
export function getItemKey(item: RenderItem): string {
|
|
if (item.type === "turn_group" || item.type === "prepare_progress") 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;
|
|
}
|
|
|
|
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 && (
|
|
<div className="text-center text-xs text-muted-foreground py-2">
|
|
Loading older messages...
|
|
</div>
|
|
)}
|
|
{hasMore && !isLoadingMore && onLoadMore && (
|
|
<div className="flex justify-center py-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="cursor-pointer text-xs text-muted-foreground"
|
|
data-testid="load-older-messages"
|
|
onClick={onLoadMore}
|
|
>
|
|
Load older messages
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{showLoadingState && (
|
|
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
|
<GridSpinner className="text-primary mr-2" />
|
|
<span>Loading messages...</span>
|
|
</div>
|
|
)}
|
|
{!messagesLoading && !isInitialLoading && messagesCount === 0 && (
|
|
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
|
<span>No messages yet. Start the conversation!</span>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function MessageItem({
|
|
item,
|
|
sessionId,
|
|
permissionsByToolCallId,
|
|
childrenByParentToolCallId,
|
|
taskId,
|
|
worktreePath,
|
|
onOpenFile,
|
|
isLastGroup,
|
|
isTurnActive,
|
|
messages,
|
|
sessionState,
|
|
taskState,
|
|
onScrollToMessage,
|
|
}: {
|
|
item: RenderItem;
|
|
sessionId: string | null;
|
|
permissionsByToolCallId: Map<string, Message>;
|
|
childrenByParentToolCallId: Map<string, Message[]>;
|
|
taskId?: string;
|
|
worktreePath?: string;
|
|
onOpenFile?: (path: string) => void;
|
|
isLastGroup: boolean;
|
|
isTurnActive: boolean;
|
|
messages: Message[];
|
|
sessionState?: TaskSessionState;
|
|
taskState?: TaskState;
|
|
onScrollToMessage: (id: string) => void;
|
|
}) {
|
|
if (item.type === "prepare_progress") {
|
|
return <PrepareProgress sessionId={item.sessionId} />;
|
|
}
|
|
if (item.type === "turn_group") {
|
|
return (
|
|
<TurnGroupMessage
|
|
group={item}
|
|
sessionId={sessionId}
|
|
permissionsByToolCallId={permissionsByToolCallId}
|
|
childrenByParentToolCallId={childrenByParentToolCallId}
|
|
taskId={taskId}
|
|
worktreePath={worktreePath}
|
|
onOpenFile={onOpenFile}
|
|
isLastGroup={isLastGroup}
|
|
isTurnActive={isTurnActive}
|
|
allMessages={messages}
|
|
sessionState={sessionState}
|
|
taskState={taskState}
|
|
onScrollToMessage={onScrollToMessage}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<MessageRenderer
|
|
comment={item.message}
|
|
isTaskDescription={item.message.id === "task-description"}
|
|
sessionState={sessionState}
|
|
taskState={taskState}
|
|
taskId={taskId}
|
|
permissionsByToolCallId={permissionsByToolCallId}
|
|
childrenByParentToolCallId={childrenByParentToolCallId}
|
|
worktreePath={worktreePath}
|
|
sessionId={sessionId ?? undefined}
|
|
onOpenFile={onOpenFile}
|
|
allMessages={messages}
|
|
onScrollToMessage={onScrollToMessage}
|
|
/>
|
|
);
|
|
}
|