"use client"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { IconCheck, IconChevronDown, IconChevronUp, IconEdit, IconFile, IconInfoCircle, IconRobot, IconUser, IconX, } from "@tabler/icons-react"; import ReactMarkdown from "react-markdown"; import { toast } from "sonner"; import { Button } from "@kandev/ui"; import { Textarea } from "@kandev/ui/textarea"; import { cn } from "@/lib/utils"; import { QueueEntryNotFoundError } from "@/lib/api/domains/queue-api"; import { stripSystemTags } from "@/lib/utils/system-tags"; import { ImagePreviewDialog } from "@/components/task/chat/image-preview-dialog"; import { SenderTaskBadge, type SenderTaskInfo, } from "@/components/task/chat/messages/sender-task-badge"; import { WorkflowStepMessageBadge, workflowMessageInfoFromMetadata, type WorkflowStepMessageInfo, } from "@/components/task/chat/messages/workflow-step-message-badge"; import { markdownComponents, remarkPlugins } from "@/components/shared/markdown-components"; import type { QueuedMessage } from "@/lib/state/slices/session/types"; type QueuedAttachment = NonNullable[number]; type AttachmentRowProps = { attachments: QueuedAttachment[]; interactive: boolean; }; /** * Renders queued-message attachments as compact thumbnails (images) and chips * (other resources). Used in both display and edit views; `interactive=false` * disables the click-to-open behavior so it stays a passive context cue while * editing the message text. */ function AttachmentRow({ attachments, interactive }: AttachmentRowProps) { if (attachments.length === 0) return null; const images = attachments.filter((a) => a.type === "image"); const files = attachments.filter((a) => a.type !== "image"); return (
{images.map((att, i) => ( ))} {files.map((_, i) => ( Attachment ))}
); } /** Imperative handle for the ghost row, used by chat input "edit last queued" affordance. */ export type QueuedGhostMessageHandle = { startEdit: () => void; }; type SenderKind = "user" | "agent" | "workflow" | "system"; export function isWorkflowQueuedMessage(entry: QueuedMessage): boolean { return ( entry.queued_by === "workflow" || entry.queued_by === "workflow-auto-start" || workflowMessageInfoFromMetadata(entry.metadata) !== null ); } function senderKindOf(entry: QueuedMessage): SenderKind { if (isWorkflowQueuedMessage(entry)) return "workflow"; if (!entry.queued_by) return "system"; // Inter-task messages dispatched via dispatchTaskMessage hardcode // queued_by="agent"; that's the only signal needed. if (entry.queued_by === "agent") return "agent"; return "user"; } function senderLabel(entry: QueuedMessage): string { const kind = senderKindOf(entry); if (kind === "agent") { const title = entry.metadata?.sender_task_title; return typeof title === "string" && title.length > 0 ? `From ${title}` : "From agent"; } if (kind === "workflow") return "Workflow"; if (kind === "system") return "System"; return "You"; } type SenderIconProps = { entry: QueuedMessage }; function senderIconFor(kind: SenderKind): { Icon: typeof IconUser; tone: string } { if (kind === "agent") { return { Icon: IconRobot, tone: "text-amber-500 dark:text-amber-400" }; } if (kind === "system") { return { Icon: IconInfoCircle, tone: "text-muted-foreground" }; } if (kind === "workflow") { return { Icon: IconInfoCircle, tone: "text-muted-foreground" }; } return { Icon: IconUser, tone: "text-blue-500 dark:text-blue-300" }; } function SenderIcon({ entry }: SenderIconProps) { const { Icon, tone } = senderIconFor(senderKindOf(entry)); return ; } /** * Returns the inter-task sender info when this entry was queued by another * task's agent (queued_by="agent" + sender_task_id in metadata). User-typed * entries have no SenderTaskInfo — they render with just the IconUser cue. */ function getSenderTaskInfo(entry: QueuedMessage): SenderTaskInfo | null { if (senderKindOf(entry) !== "agent") return null; const meta = entry.metadata as | { sender_task_id?: string; sender_task_title?: string } | undefined; const id = meta?.sender_task_id; if (typeof id !== "string" || id.length === 0) return null; return { id, snapshotTitle: meta?.sender_task_title ?? "" }; } function getWorkflowMessageInfo(entry: QueuedMessage): WorkflowStepMessageInfo | null { const info = workflowMessageInfoFromMetadata(entry.metadata); if (info) return info; return entry.queued_by === "workflow" || entry.queued_by === "workflow-auto-start" ? {} : null; } type EditViewProps = { value: string; saving: boolean; attachments: QueuedAttachment[]; onChange: (v: string) => void; onSave: () => void; onCancel: () => void; textareaRef: React.RefObject; }; function EditView({ value, saving, attachments, onChange, onSave, onCancel, textareaRef, }: EditViewProps) { const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); onCancel(); } else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); onSave(); } }; return (