/** Accessible, default-off Projects and Conversations workspace for HUX-03. */ import { Folder, GitBranch, Paperclip, Pin, Search } from "lucide-react"; import { useMemo, useState } from "react"; import type { FormEvent } from "react"; import { attachmentViews, branchLineage, projectsEnabled } from "./model.ts"; import type { ArtifactSummary, Conversation, Project, } from "./types.ts"; interface OrganizationWorkspaceProps { flags?: Iterable; projects: readonly Project[]; conversations: readonly Conversation[]; artifacts?: readonly ArtifactSummary[]; searchResults?: readonly Conversation[]; nextSearchCursor?: string | null; busy?: boolean; error?: string | null; onSearch?: (query: string, cursor: string | null) => void; onRenameProject?: (project: Project, name: string) => void; onRenameConversation?: (conversation: Conversation, title: string) => void; onUpdateProjectTags?: (project: Project, tags: string[]) => void; onUpdateConversationTags?: (conversation: Conversation, tags: string[]) => void; onMoveConversation?: (conversation: Conversation, projectId: string | null) => void; onToggleProjectPin?: (project: Project) => void; onToggleConversationPin?: (conversation: Conversation) => void; } function ordered(items: readonly T[]): T[] { return [...items].sort((left, right) => Number(right.pinned) - Number(left.pinned) || right.updatedAt.localeCompare(left.updatedAt)); } function Tags({values}: {values: readonly string[]}) { if (!values.length) return null; return ; } function EditableTitle({label, value, maxLength, onSave}: { label: string; value: string; maxLength: number; onSave?: (value: string) => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value); if (!onSave) return {value}; if (!editing) return ; return
{ event.preventDefault(); const next = draft.trim(); if (next && next !== value) onSave(next); setEditing(false); }}>
; } function EditableTags({label, values, onSave}: { label: string; values: readonly string[]; onSave?: (values: string[]) => void; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(values.join(", ")); const [invalid, setInvalid] = useState(false); if (!onSave) return ; if (!editing) return
; return
{ event.preventDefault(); const tags = draft.split(",").map((tag) => tag.trim()).filter(Boolean); const valid = tags.length <= 32 && new Set(tags).size === tags.length && tags.every((tag) => /^[a-z0-9][a-z0-9-]{0,39}$/.test(tag)); setInvalid(!valid); if (valid) { onSave(tags); setEditing(false); } }}> {invalid && Use up to 32 lowercase tags separated by commas.}
; } function ProjectList({projects, selectedId, onSelect, onRename, onTags, onPin}: { projects: readonly Project[]; selectedId: string | null; onSelect: (id: string | null) => void; onRename?: OrganizationWorkspaceProps["onRenameProject"]; onTags?: OrganizationWorkspaceProps["onUpdateProjectTags"]; onPin?: OrganizationWorkspaceProps["onToggleProjectPin"]; }) { return ; } function Lineage({conversation, conversations}: { conversation: Conversation; conversations: readonly Conversation[]; }) { const nodes = branchLineage(conversation.id, conversations); return

Branch lineage

    {nodes.map((node) =>
  1. {node.title} {node.branchPointMessageId && branched at message {node.branchPointMessageId}}
  2. )}
; } function Attachments({conversation, artifacts}: { conversation: Conversation; artifacts: readonly ArtifactSummary[]; }) { const items = attachmentViews(conversation, artifacts); return

Attached artifacts

{items.length ?
    {items.map((item) =>
  • {item.title}{item.missing ? "Still attached; details unavailable" : item.kind}
  • )}
:

No artifacts attached.

}
; } function ConversationList({items, projects, selected, onSelect, onRename, onTags, onMove, onPin}: { items: readonly Conversation[]; projects: readonly Project[]; selected: string | null; onSelect: (id: string) => void; onRename?: OrganizationWorkspaceProps["onRenameConversation"]; onTags?: OrganizationWorkspaceProps["onUpdateConversationTags"]; onMove?: OrganizationWorkspaceProps["onMoveConversation"]; onPin?: OrganizationWorkspaceProps["onToggleConversationPin"]; }) { return
    {ordered(items).map((conversation) =>
  • {onPin && }
    {onRename && onRename(conversation, title)} />} onTags(conversation, tags) : undefined} /> Resume conversation {onMove && }
  • )}
; } /** Render nothing unless the negotiated hux.foundation and hux.projects flags are enabled. */ export function OrganizationWorkspace(props: OrganizationWorkspaceProps) { const [projectId, setProjectId] = useState(null); const [selectedId, setSelectedId] = useState(null); const [query, setQuery] = useState(""); const enabled = projectsEnabled(props.flags); const baseItems = props.searchResults || props.conversations; const items = useMemo(() => projectId === null ? baseItems : baseItems.filter((item) => item.projectId === projectId), [baseItems, projectId]); const selected = props.conversations.find((item) => item.id === selectedId) || items[0] || null; if (!enabled) return null; function submitSearch(event: FormEvent) { event.preventDefault(); const value = query.trim(); if (value) props.onSearch?.(value, null); } return

Conversations

setQuery(event.target.value)} />
{props.error &&

{props.error}

}

{props.busy ? "Loading conversations" : `${items.length} conversations shown`}

{!items.length &&

No matching conversations.

} {props.nextSearchCursor && props.onSearch && }
; }