import { Spinner, WorktreeLinedIcon } from "@tutti-os/ui-system";
import { AskLinedIcon } from "@tutti-os/ui-system/icons";
import { resolveAgentGUIConversationSortTimeUnixMs } from "./model/agentGuiConversationModel";
import type { AgentGUINodeViewModel } from "./model/agentGuiNodeTypes";
import type { AgentGUIViewLabels } from "./AgentGUINodeView";
import styles from "./AgentGUINode.styles";
export interface ConversationSection {
id: string;
kind: "pinned" | "project" | "conversations";
label: string;
project: AgentGUINodeViewModel["rail"]["conversations"][number]["project"];
items: AgentGUINodeViewModel["rail"]["conversations"];
}
export function ConversationMeta({
item,
nowMs,
labels,
hideTime = false
}: {
item: AgentGUINodeViewModel["rail"]["conversations"][number];
nowMs: number;
hideTime?: boolean;
labels: Pick<
AgentGUIViewLabels,
| "relativeTimeJustNow"
| "relativeTimeMinutes"
| "relativeTimeHours"
| "relativeTimeDays"
| "relativeTimeMonths"
| "relativeTimeYears"
>;
}): React.JSX.Element | null {
"use memo";
const kind = conversationMetaKind(item);
const worktreeGlyph =
item.isolation?.mode === "worktree" ? : null;
const worktreeData = worktreeGlyph ? "true" : undefined;
if (kind === "loading") {
return (
{worktreeGlyph}
);
}
if (kind === "waiting") {
return (
{worktreeGlyph}
);
}
if (kind === "failed") {
return (
{worktreeGlyph}
);
}
if (kind === "unread-complete") {
return (
{worktreeGlyph}
);
}
return hideTime ? (
worktreeGlyph ? (
{worktreeGlyph}
) : null
) : (
{worktreeGlyph}
{formatConversationRelativeTime(
resolveAgentGUIConversationSortTimeUnixMs(item),
nowMs,
labels
)}
);
}
function WorktreeGlyph(): React.JSX.Element {
return (
);
}
export function filterConversationSectionsBySearchMatches(
sections: readonly ConversationSection[],
matchingConversations: AgentGUINodeViewModel["rail"]["conversations"]
): ConversationSection[] {
const matchingConversationIds = new Set(
matchingConversations.map((conversation) => conversation.id)
);
return sections
.map((section) => ({
...section,
items: section.items.filter((item) =>
matchingConversationIds.has(item.id)
)
}))
.filter((section) => section.items.length > 0);
}
function conversationMetaKind(
conversation: AgentGUINodeViewModel["rail"]["conversations"][number]
): "loading" | "waiting" | "failed" | "unread-complete" | "time" {
if (conversation.status === "failed") {
return "failed";
}
if (conversation.needsUserAction) {
return "waiting";
}
if (conversation.status === "working" || conversation.status === "waiting") {
return "loading";
}
if (conversation.hasUnreadCompletion) {
return "unread-complete";
}
return "time";
}
function formatConversationRelativeTime(
updatedAtUnixMs: number,
nowMs: number,
labels: Pick<
AgentGUIViewLabels,
| "relativeTimeJustNow"
| "relativeTimeMinutes"
| "relativeTimeHours"
| "relativeTimeDays"
| "relativeTimeMonths"
| "relativeTimeYears"
>
): string {
const elapsedMs = Math.max(0, nowMs - updatedAtUnixMs);
const elapsedMinutes = Math.floor(elapsedMs / (60 * 1000));
if (elapsedMinutes < 1) {
return labels.relativeTimeJustNow;
}
if (elapsedMinutes < 60) {
return labels.relativeTimeMinutes(elapsedMinutes);
}
const elapsedHours = Math.floor(elapsedMinutes / 60);
if (elapsedHours < 24) {
return labels.relativeTimeHours(elapsedHours);
}
const elapsedDays = Math.floor(elapsedHours / 24);
if (elapsedDays < 30) {
return labels.relativeTimeDays(elapsedDays);
}
const elapsedMonths = Math.floor(elapsedDays / 30);
if (elapsedMonths < 12) {
return labels.relativeTimeMonths(elapsedMonths);
}
return labels.relativeTimeYears(Math.floor(elapsedDays / 365));
}
function LoadingGlyph(): React.JSX.Element {
"use memo";
return (
);
}
function AttentionGlyph(): React.JSX.Element {
"use memo";
return (
);
}