import type { ButtonHTMLAttributes, TargetedKeyboardEvent } from "preact"; import type { ComponentChild as UiNode } from "preact"; import { useLayoutEffect, useRef } from "preact/hooks"; import { IconButton } from "../../shared/ui/components"; import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root"; import type { ThreadsRowModel } from "./state"; type ButtonProps = ButtonHTMLAttributes & { disabled?: boolean | undefined; }; export interface ThreadsViewModel { status: string | null; loading: boolean; rows: ThreadsRowModel[]; } export interface ThreadsViewActions { refresh: () => void; openNewPanel: () => void; openThread: (threadId: string) => void; startRename: (threadId: string, value: string) => void; updateRename: (threadId: string, value: string) => void; saveRename: (threadId: string, value: string) => void; cancelRename: (threadId: string) => void; autoNameThread: (threadId: string) => void; startArchive: (threadId: string) => void; archiveThread: (threadId: string, saveMarkdown: boolean) => void; } export function renderThreadsView(parent: HTMLElement, model: ThreadsViewModel, actions: ThreadsViewActions): void { parent.addClass("codex-panel-threads"); renderUiRoot(parent, ); } export function unmountThreadsView(parent: HTMLElement | null): void { unmountUiRoot(parent); } function ThreadsView({ model, actions }: { model: ThreadsViewModel; actions: ThreadsViewActions }): UiNode { return ( <>
{model.rows.length === 0 ? (
{model.status ?? (model.loading ? "Loading threads..." : "No threads")}
) : ( <> {model.status ?
{model.status}
: null} {model.rows.map((row) => ( ))} )}
); } function ThreadRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsViewActions }): UiNode { const archiveConfirm = row.archiveConfirm; const rowClassName = [ "codex-panel-ui__nav-row", "codex-panel-threads__row", row.selected ? "codex-panel-threads__row--selected" : "", row.selected ? "is-selected" : "", row.rename.active ? "codex-panel-threads__row--renaming" : "", archiveConfirm.active ? "codex-panel-threads__row--archive-confirming" : "", ] .filter(Boolean) .join(" "); const mainClassName = [ "codex-panel-ui__nav-item", "codex-panel-threads__row-main", row.live ? `codex-panel-threads__row--${row.live.status}` : "", row.selected ? "codex-panel-threads__row--selected" : "", ] .filter(Boolean) .join(" "); const open = () => { if (row.rename.active || archiveConfirm.active) return; actions.openThread(row.thread.id); }; const openFromKeyboard = (event: TargetedKeyboardEvent) => { if (row.rename.active || archiveConfirm.active) return; if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); actions.openThread(row.thread.id); }; return (
{row.rename.active ? ( ) : ( <>
{row.title}
{!archiveConfirm.active ? ( { event.stopPropagation(); actions.startRename(row.thread.id, row.rename.draft); }} /> ) : null}
)}
); } function ArchiveActions({ row, archiveConfirm, actions, }: { row: ThreadsRowModel; archiveConfirm: ThreadsRowModel["archiveConfirm"]; actions: ThreadsViewActions; }): UiNode { if (!archiveConfirm.active) { return ( { event.stopPropagation(); actions.startArchive(row.thread.id); }} /> ); } const defaultSaveMarkdown = archiveConfirm.defaultSaveMarkdown; return ( <> ); } function ArchiveModeButton({ row, saveMarkdown, primary, actions, }: { row: ThreadsRowModel; saveMarkdown: boolean; primary: boolean; actions: ThreadsViewActions; }): UiNode { const label = saveMarkdown ? "Save and archive thread" : "Archive thread without saving"; return ( { event.stopPropagation(); actions.archiveThread(row.thread.id, saveMarkdown); }} /> ); } function RenameRow({ row, actions, className }: { row: ThreadsRowModel; actions: ThreadsViewActions; className: string }): UiNode { const inputRef = useRef(null); useLayoutEffect(() => { const input = inputRef.current; if (!input) return; if (input.ownerDocument.activeElement !== input) { input.focus(); input.select(); } }, [row.rename.draft]); return ( <>
{ event.stopPropagation(); }} >
{ actions.updateRename(row.thread.id, event.currentTarget.value); }} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); if (!event.isComposing && !row.rename.generating) actions.saveRename(row.thread.id, event.currentTarget.value); return; } if (event.key === "Escape") { event.preventDefault(); actions.cancelRename(row.thread.id); } }} onBlur={(event) => { if (!row.rename.generating) actions.saveRename(row.thread.id, event.currentTarget.value); }} />
{ event.preventDefault(); event.stopPropagation(); }} onClick={(event) => { event.stopPropagation(); actions.autoNameThread(row.thread.id); }} />
); } function ThreadsToolbarButton({ icon, label, ...props }: { icon: string; label: string; } & Omit): UiNode { return ( ); } function ThreadsRowButton({ icon, label, className, ...props }: { icon: string; label: string; className: string; } & Omit): UiNode { return ; }