murashit_codex-panel/src/features/threads-view/renderer.tsx

290 lines
9.1 KiB
TypeScript
Raw Normal View History

2026-05-31 03:37:18 +00:00
import type { ButtonHTMLAttributes, TargetedKeyboardEvent } from "preact";
2026-06-02 03:26:35 +00:00
import type { ComponentChild as UiNode } from "preact";
2026-05-31 10:03:49 +00:00
import { useLayoutEffect, useRef } from "preact/hooks";
2026-05-27 10:37:17 +00:00
2026-06-02 03:26:35 +00:00
import { IconButton } from "../../shared/ui/components";
import { renderUiRoot, unmountUiRoot } from "../../shared/ui/ui-root";
2026-05-27 10:37:17 +00:00
import type { ThreadsRowModel } from "./state";
2026-05-31 03:37:18 +00:00
type ButtonProps = ButtonHTMLAttributes & {
disabled?: boolean | undefined;
};
2026-05-27 10:37:17 +00:00
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");
2026-06-02 03:26:35 +00:00
renderUiRoot(parent, <ThreadsView model={model} actions={actions} />);
2026-05-27 10:37:17 +00:00
}
export function unmountThreadsView(parent: HTMLElement | null): void {
2026-06-02 03:26:35 +00:00
unmountUiRoot(parent);
2026-05-27 10:37:17 +00:00
}
2026-06-02 03:26:35 +00:00
function ThreadsView({ model, actions }: { model: ThreadsViewModel; actions: ThreadsViewActions }): UiNode {
2026-05-27 10:37:17 +00:00
return (
<>
<div className="nav-header codex-panel-threads__toolbar">
<div className="nav-buttons-container codex-panel-threads__toolbar-actions">
<ThreadsToolbarButton icon="message-square-plus" label="Open new panel" onClick={actions.openNewPanel} />
<ThreadsToolbarButton icon="refresh-cw" label="Refresh threads" onClick={actions.refresh} />
2026-05-27 10:37:17 +00:00
</div>
</div>
<div className="codex-panel-threads__list" role="list">
{model.rows.length === 0 ? (
<div className="codex-panel-threads__empty">{model.status ?? (model.loading ? "Loading threads..." : "No threads")}</div>
) : (
<>
{model.status ? <div className="codex-panel-threads__status">{model.status}</div> : null}
{model.rows.map((row) => (
<ThreadRow key={row.thread.id} row={row} actions={actions} />
))}
</>
)}
</div>
</>
);
}
2026-06-02 03:26:35 +00:00
function ThreadRow({ row, actions }: { row: ThreadsRowModel; actions: ThreadsViewActions }): UiNode {
2026-06-03 22:39:25 +00:00
const archiveConfirm = row.archiveConfirm;
const rowClassName = [
"codex-panel-ui__nav-row",
2026-05-27 10:37:17 +00:00
"codex-panel-threads__row",
2026-06-02 21:07:19 +00:00
row.selected ? "codex-panel-threads__row--selected" : "",
row.selected ? "is-selected" : "",
2026-05-27 10:37:17 +00:00
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(" ");
2026-05-27 10:37:17 +00:00
const open = () => {
if (row.rename.active || archiveConfirm.active) return;
2026-05-27 10:37:17 +00:00
actions.openThread(row.thread.id);
};
2026-05-31 03:37:18 +00:00
const openFromKeyboard = (event: TargetedKeyboardEvent<HTMLDivElement>) => {
2026-05-28 00:10:29 +00:00
if (row.rename.active || archiveConfirm.active) return;
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
actions.openThread(row.thread.id);
};
2026-05-27 10:37:17 +00:00
return (
<div className={rowClassName}>
2026-05-27 10:37:17 +00:00
{row.rename.active ? (
<RenameRow row={row} actions={actions} className={mainClassName} />
2026-05-27 10:37:17 +00:00
) : (
<>
<div
className={mainClassName}
role="button"
tabIndex={0}
aria-current={row.selected ? "true" : undefined}
onClick={open}
onKeyDown={openFromKeyboard}
>
2026-05-27 10:37:17 +00:00
<span className="codex-panel-threads__row-title">{row.title}</span>
</div>
<div
className={["codex-panel-threads__actions", archiveConfirm.active ? "codex-panel-threads__archive-confirm" : ""]
.filter(Boolean)
.join(" ")}
>
{!archiveConfirm.active ? (
<ThreadsRowButton
2026-05-27 10:37:17 +00:00
icon="pencil"
label="Rename thread"
className="codex-panel-threads__row-button"
onClick={(event) => {
event.stopPropagation();
2026-06-21 08:32:32 +00:00
actions.startRename(row.thread.id, row.rename.draft);
2026-05-27 10:37:17 +00:00
}}
/>
) : null}
2026-06-03 22:39:25 +00:00
<ArchiveActions row={row} archiveConfirm={archiveConfirm} actions={actions} />
2026-05-27 10:37:17 +00:00
</div>
</>
)}
</div>
);
}
2026-06-03 22:39:25 +00:00
function ArchiveActions({
row,
archiveConfirm,
actions,
}: {
row: ThreadsRowModel;
archiveConfirm: ThreadsRowModel["archiveConfirm"];
actions: ThreadsViewActions;
}): UiNode {
2026-05-27 10:37:17 +00:00
if (!archiveConfirm.active) {
return (
<ThreadsRowButton
2026-05-27 10:37:17 +00:00
icon="archive"
label="Archive thread"
className="codex-panel-threads__row-button"
onClick={(event) => {
event.stopPropagation();
actions.startArchive(row.thread.id);
}}
/>
);
}
const defaultSaveMarkdown = archiveConfirm.defaultSaveMarkdown;
return (
<>
<ArchiveModeButton row={row} saveMarkdown={!defaultSaveMarkdown} primary={false} actions={actions} />
<ArchiveModeButton row={row} saveMarkdown={defaultSaveMarkdown} primary={true} actions={actions} />
</>
);
}
function ArchiveModeButton({
row,
saveMarkdown,
primary,
actions,
}: {
row: ThreadsRowModel;
saveMarkdown: boolean;
primary: boolean;
actions: ThreadsViewActions;
2026-06-02 03:26:35 +00:00
}): UiNode {
2026-05-27 10:37:17 +00:00
const label = saveMarkdown ? "Save and archive thread" : "Archive thread without saving";
return (
<ThreadsRowButton
2026-05-27 10:37:17 +00:00
icon={saveMarkdown ? "save" : "trash"}
label={label}
className={primary ? "codex-panel-threads__archive-default" : "codex-panel-threads__archive-alternate"}
onClick={(event) => {
event.stopPropagation();
actions.archiveThread(row.thread.id, saveMarkdown);
}}
/>
);
}
function RenameRow({ row, actions, className }: { row: ThreadsRowModel; actions: ThreadsViewActions; className: string }): UiNode {
2026-05-27 10:37:17 +00:00
const inputRef = useRef<HTMLInputElement | null>(null);
useLayoutEffect(() => {
const input = inputRef.current;
if (!input) return;
if (input.ownerDocument.activeElement !== input) {
input.focus();
input.select();
}
2026-05-27 12:25:08 +00:00
}, [row.rename.draft]);
2026-05-27 10:37:17 +00:00
return (
<>
<div
className={`${className} codex-panel-threads__rename-form`}
2026-05-27 10:37:17 +00:00
onClick={(event) => {
event.stopPropagation();
}}
>
<div className="codex-panel-threads__rename-field">
<input
ref={inputRef}
className="codex-panel-ui__nav-inline-input codex-panel-threads__rename-input"
2026-05-27 10:37:17 +00:00
type="text"
2026-05-28 00:10:29 +00:00
value={row.rename.draft}
2026-06-02 03:26:35 +00:00
onInput={(event) => {
2026-05-27 12:25:08 +00:00
actions.updateRename(row.thread.id, event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
2026-05-31 03:37:18 +00:00
if (!event.isComposing && !row.rename.generating) actions.saveRename(row.thread.id, event.currentTarget.value);
2026-05-27 12:25:08 +00:00
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);
}}
2026-05-27 10:37:17 +00:00
/>
</div>
</div>
<div className="codex-panel-threads__actions codex-panel-threads__rename-actions">
<ThreadsRowButton
2026-05-27 10:37:17 +00:00
icon={row.rename.generating ? "loader" : "sparkles"}
label="Auto-name thread"
className="codex-panel-threads__row-button"
disabled={row.rename.generating}
onPointerDown={(event) => {
event.preventDefault();
event.stopPropagation();
}}
onClick={(event) => {
event.stopPropagation();
actions.autoNameThread(row.thread.id);
}}
/>
</div>
</>
);
}
function ThreadsToolbarButton({
2026-05-27 10:37:17 +00:00
icon,
label,
...props
}: {
icon: string;
label: string;
2026-06-02 03:26:35 +00:00
} & Omit<ButtonProps, "className" | "type">): UiNode {
2026-05-27 10:37:17 +00:00
return (
<IconButton
{...props}
icon={icon}
label={label}
className="clickable-icon nav-action-button codex-panel-ui__toolbar-action codex-panel-threads__toolbar-button"
2026-05-27 10:37:17 +00:00
/>
);
}
function ThreadsRowButton({
icon,
label,
className,
...props
}: {
icon: string;
label: string;
className: string;
} & Omit<ButtonProps, "className" | "type">): UiNode {
return <IconButton {...props} icon={icon} label={label} className={`clickable-icon codex-panel-ui__nav-row-action ${className}`} />;
}