diff options
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 160 |
1 files changed, 149 insertions, 11 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index c5cf151..1714aed 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -28,6 +28,7 @@ import { pickUpOrders, } from "../lib/api"; import { DirectiveContextMenu } from "../components/directives/DirectiveContextMenu"; +import { DocumentTaskStream } from "../components/directives/DocumentTaskStream"; // Status dot color, matching the existing tabular UI's badge palette so the // document mode feels like a sibling of the existing list, not a foreign UI. @@ -177,6 +178,8 @@ interface DirectiveFolderProps { onCreateEphemeralTask: (directive: DirectiveSummary) => void; /** Right-click handler — opens DirectiveContextMenu with start/pause/PR/etc. */ onContextMenu: (e: React.MouseEvent, directive: DirectiveSummary) => void; + /** Click handler for task/step rows — navigates to the live transcript. */ + onSelectTask: (directiveId: string, taskId: string) => void; /** * Document refresh trigger — bumped externally so the folder refetches its * document list after a create/update happens elsewhere. Primarily used so @@ -195,8 +198,13 @@ function DirectiveFolder({ onCreateDocument, onCreateEphemeralTask, onContextMenu, + onSelectTask, refreshNonce, }: DirectiveFolderProps) { + const selectedTaskIdForFolder = + selection && selection.directiveId === directive.id + ? selection.taskId + : null; const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft; const orchestratorRunning = !!directive.orchestratorTaskId; @@ -370,9 +378,12 @@ function DirectiveFolder({ /> <DocumentTasksFolder documentId={doc.id} + directiveId={directive.id} depth="normal" defaultOpen={doc.status === "active"} refreshNonce={refreshNonce} + selectedTaskId={selectedTaskIdForFolder} + onSelectTask={onSelectTask} /> </div> ))} @@ -411,9 +422,12 @@ function DirectiveFolder({ /> <DocumentTasksFolder documentId={doc.id} + directiveId={directive.id} depth="deep" defaultOpen={false} refreshNonce={refreshNonce} + selectedTaskId={selectedTaskIdForFolder} + onSelectTask={onSelectTask} /> </div> ))} @@ -504,6 +518,9 @@ function DocumentRow({ interface DocumentTasksFolderProps { documentId: string; + /** Parent directive id — needed so a clicked task row can navigate to + * /directives/<directiveId>?task=<taskId>. */ + 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<DocumentTasksResponse | null>(null); @@ -591,10 +615,24 @@ function DocumentTasksFolder({ </div> )} {data?.steps.map((step) => ( - <StepRow key={`step-${step.id}`} step={step} padLeft={rowPadLeft} /> + <StepRow + key={`step-${step.id}`} + step={step} + directiveId={directiveId} + selected={!!selectedTaskId && step.taskId === selectedTaskId} + padLeft={rowPadLeft} + onSelect={onSelectTask} + /> ))} {data?.tasks.map((task) => ( - <TaskRow key={`task-${task.id}`} task={task} padLeft={rowPadLeft} /> + <TaskRow + key={`task-${task.id}`} + task={task} + directiveId={directiveId} + selected={task.id === selectedTaskId} + padLeft={rowPadLeft} + onSelect={onSelectTask} + /> ))} </div> )} @@ -628,15 +666,43 @@ const TASK_STATUS_DOT: Record<string, string> = { 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 ( - <div - title={`${step.name} (${step.status})`} - className={`w-full text-left flex items-center gap-1.5 ${padLeft} pr-3 py-1 font-mono text-[11px] text-[#9bc3ff]`} + <button + type="button" + disabled={!clickable} + onClick={() => 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" + }`} > <FileIcon /> <span @@ -648,24 +714,39 @@ function StepRow({ step, padLeft }: StepRowProps) { <span className="text-[9px] uppercase tracking-wide text-[#556677]"> step </span> - </div> + </button> ); } 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 ( - <div + <button + type="button" + onClick={() => 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" + }`} > <FileIcon /> <span @@ -677,7 +758,7 @@ function TaskRow({ task, padLeft }: TaskRowProps) { <span className="text-[9px] uppercase tracking-wide text-[#556677]"> {isSup ? "sup" : "task"} </span> - </div> + </button> ); } @@ -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 ( + <div className="flex-1 flex flex-col h-full overflow-hidden"> + <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]"> + <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]"> + <FileIcon /> + <span>directives /</span> + <span className="text-[#9bc3ff]"> + {directive.title.trim().length > 0 + ? directive.title + : directive.id.slice(0, 8)} + </span> + <span>/</span> + <span className="text-white">{label}</span> + </div> + </div> + <DocumentTaskStream + taskId={taskId} + label={label} + ephemeral={!isStepBound} + /> + </div> + ); + } + // --- 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=<id>. 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} /> </div> |
