summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-05 16:30:21 +0100
committersoryu <soryu@soryu.co>2026-05-05 16:30:21 +0100
commit2de03bc02fefeeb40ea9becaf3b501cc513b1289 (patch)
tree7e89affec5fe1e61f99a4b9669f35cd83b661a02
parentcd09103183139788ba4297eaaa6f75d51a154c8a (diff)
downloadsoryu-fix-task-row-click.tar.gz
soryu-fix-task-row-click.zip
fix(doc-mode): make task rows clickable and render live transcriptfix-task-row-click
Task rows in the directive sidebar's `tasks/` subfolder were rendered as inert `<div>` 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 `<button>` elements that call `onSelect(directiveId, taskId)` and navigate to `/directives/<dirId>?task=<taskId>`. - EditorShell renders DocumentTaskStream with a breadcrumb when `selection.taskId` is set (winning over the document path). - Step rows whose backing task hasn't been spawned yet stay disabled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--makima/frontend/src/routes/document-directives.tsx160
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>