From 928598b1b8399a95918dc1b315274a9d175eb8d9 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 5 May 2026 16:30:46 +0100 Subject: fix(doc-mode): make task rows clickable and render live transcript (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task rows in the directive sidebar's `tasks/` subfolder were rendered as inert `
` elements with no click handler, and EditorShell had no branch for `selection.taskId` — so clicking a task did nothing visible. - StepRow and TaskRow are now `
))} @@ -411,9 +422,12 @@ function DirectiveFolder({ /> ))} @@ -504,6 +518,9 @@ function DocumentRow({ interface DocumentTasksFolderProps { documentId: string; + /** Parent directive id — needed so a clicked task row can navigate to + * /directives/?task=. */ + directiveId: string; /** Visual indent depth — mirrors the parent DocumentRow's indent so the * tasks/ row sits one level deeper than its parent doc. */ depth: "normal" | "deep"; @@ -514,13 +531,20 @@ interface DocumentTasksFolderProps { /** Bumped externally so the folder refetches its task list after a save * or status change elsewhere. Same nonce used for the directive folder. */ refreshNonce: number; + /** Currently-selected task id (drives row highlight). */ + selectedTaskId: string | null; + /** Click handler for step/task rows — navigates to the live transcript. */ + onSelectTask: (directiveId: string, taskId: string) => void; } function DocumentTasksFolder({ documentId, + directiveId, depth, defaultOpen, refreshNonce, + selectedTaskId, + onSelectTask, }: DocumentTasksFolderProps) { const [open, setOpen] = useState(defaultOpen); const [data, setData] = useState(null); @@ -591,10 +615,24 @@ function DocumentTasksFolder({ )} {data?.steps.map((step) => ( - + ))} {data?.tasks.map((task) => ( - + ))} )} @@ -628,15 +666,43 @@ const TASK_STATUS_DOT: Record = { interface StepRowProps { step: DirectiveStep; + directiveId: string; + selected: boolean; padLeft: string; + onSelect: (directiveId: string, taskId: string) => void; } -function StepRow({ step, padLeft }: StepRowProps) { +function StepRow({ + step, + directiveId, + selected, + padLeft, + onSelect, +}: StepRowProps) { const dot = STEP_STATUS_DOT[step.status] ?? "bg-[#556677]"; + // Steps without an underlying task can't be opened — the executor + // hasn't started yet so there's no transcript to show. Render them + // disabled so the user can see them in the list but knows they're + // inert. Same for steps stuck in pending/skipped. + const taskId = step.taskId; + const clickable = !!taskId; return ( -
clickable && onSelect(directiveId, taskId!)} + title={ + clickable + ? `${step.name} (${step.status})` + : `${step.name} — no task spawned yet (${step.status})` + } + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${ + selected + ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" + : clickable + ? "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" + : "text-[#556677] border-l-2 border-transparent cursor-not-allowed" + }`} > step -
+ ); } interface TaskRowProps { task: Task; + directiveId: string; + selected: boolean; padLeft: string; + onSelect: (directiveId: string, taskId: string) => void; } -function TaskRow({ task, padLeft }: TaskRowProps) { +function TaskRow({ + task, + directiveId, + selected, + padLeft, + onSelect, +}: TaskRowProps) { const dot = TASK_STATUS_DOT[task.status] ?? "bg-[#556677]"; // Supervisor tasks get a small "sup" tag so the user can spot // contract orchestrators in the list. const isSup = task.isSupervisor; return ( -
onSelect(directiveId, task.id)} title={`${task.name} (${task.status})`} - className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] text-[#9bc3ff]`} + className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] transition-colors ${ + selected + ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]" + : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent" + }`} > {isSup ? "sup" : "task"} -
+ ); } @@ -695,6 +776,7 @@ interface SidebarProps { onCreateContract: () => void; onCreateEphemeralTask: (directive: DirectiveSummary) => void; onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void; + onSelectTask: (directiveId: string, taskId: string) => void; refreshNonce: number; } @@ -708,6 +790,7 @@ function DocumentSidebar({ onCreateContract, onCreateEphemeralTask, onContextMenu, + onSelectTask, refreshNonce, }: SidebarProps) { // Flat sort: active first, then idle, paused, draft, inactive, archived. @@ -797,6 +880,7 @@ function DocumentSidebar({ onCreateDocument={onCreateDocument} onCreateEphemeralTask={onCreateEphemeralTask} onContextMenu={onContextMenu} + onSelectTask={onSelectTask} refreshNonce={refreshNonce} /> ))} @@ -927,6 +1011,50 @@ function EditorShell({ ); } + // --- Task path: task row clicked in the sidebar ------------------------ + // Renders the live transcript via DocumentTaskStream. Selection wins over + // the document path when both are somehow present (defensive). + if (selection?.taskId) { + const taskId = selection.taskId; + // Resolve a human label for the task: orchestrator/completion are + // labelled by role; step tasks borrow the step name; everything else + // is an ephemeral and just shows the task id slice. Look-up uses the + // already-fetched directive (with steps). + const stepWithTask = directive.steps.find((s) => s.taskId === taskId); + const label = + taskId === directive.orchestratorTaskId + ? "orchestrator" + : taskId === directive.completionTaskId + ? "completion" + : stepWithTask?.name ?? taskId.slice(0, 8); + const isStepBound = + taskId === directive.orchestratorTaskId || + taskId === directive.completionTaskId || + !!stepWithTask; + return ( +
+
+
+ + directives / + + {directive.title.trim().length > 0 + ? directive.title + : directive.id.slice(0, 8)} + + / + {label} +
+
+ +
+ ); + } + // --- Document path: documentId selected -------------------------------- if (documentId) { if (docLoading && !doc) { @@ -1161,6 +1289,15 @@ export default function DocumentDirectivesPage() { [bumpRefresh, navigate], ); + // Click on a task or step row → open the live transcript pane via + // ?task=. EditorShell switches to DocumentTaskStream when this is set. + const handleSelectTask = useCallback( + (directiveId: string, taskId: string) => { + navigate(`/directives/${directiveId}?task=${taskId}`); + }, + [navigate], + ); + // Modal state for the two new creation surfaces in the sidebar: // * + New contract → opens NewContractModal, calls useDirectives.create // * + New ephemeral task (per directive) → opens NewEphemeralTaskModal @@ -1265,6 +1402,7 @@ export default function DocumentDirectivesPage() { onCreateContract={() => setShowNewContract(true)} onCreateEphemeralTask={(d) => setNewEphemeralFor(d)} onContextMenu={handleContextMenu} + onSelectTask={handleSelectTask} refreshNonce={refreshNonce} /> -- cgit v1.2.3