From d513f93c84ae985738e0f696fcb72fa1153046ef Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Tue, 28 Apr 2026 17:35:08 +0100
Subject: feat: document UI with contract blocks, expandable logs, and
 interaction controls (#97)

* feat: soryu-co/soryu - makima: Rename tasks to contracts in directive API and types

* feat: soryu-co/soryu - makima: Add contract interaction panel with comment and interrupt

* feat: soryu-co/soryu - makima: Build expandable contract log feed in StepsDiagram

* feat: soryu-co/soryu - makima: Rename tasks to contracts throughout document UI and add contract block support

* feat: soryu-co/soryu - makima: Add comment and interrupt controls to expanded step log feed

* feat: soryu-co/soryu - makima: Audit and fix Document UI feature flag visibility and missing implementations

* feat: soryu-co/soryu - makima: Add expandable step rows with live log feed in StepsDiagram

* WIP: heartbeat checkpoint

* feat: soryu-co/soryu - makima: Integrate all document UI components and final polish
---
 .../document/nodes/StepsDiagramComponent.tsx       | 142 +++++++++++++++++----
 1 file changed, 119 insertions(+), 23 deletions(-)

(limited to 'frontend/src/components/document/nodes/StepsDiagramComponent.tsx')

diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
index 606c0ab..53f860e 100644
--- a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
+++ b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx
@@ -1,18 +1,20 @@
 import React, { useEffect, useState, useRef, useCallback } from 'react';
 import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi';
+import { StepLogFeed } from './StepLogFeed';
 import './StepsDiagram.css';
 
 interface StepsDiagramComponentProps {
   directiveId: string;
+  onExpandContract?: (step: DirectiveStep) => void;
 }
 
 type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped';
 
 const STATUS_LABELS: Record<string, string> = {
-  pending: 'Pending',
+  pending: 'Queued',
   ready: 'Ready',
-  running: 'Running',
-  completed: 'Done',
+  running: 'Executing',
+  completed: 'Fulfilled',
   failed: 'Failed',
   skipped: 'Skipped',
 };
@@ -23,18 +25,40 @@ function formatTime(dateStr: string): string {
   return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
 }
 
-function StepCard({ step }: { step: DirectiveStep }) {
+interface StepCardProps {
+  step: DirectiveStep;
+  isExpanded: boolean;
+  onToggleExpand: () => void;
+  onCollapse: () => void;
+}
+
+function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProps) {
   const status = (step.status || 'pending').toLowerCase() as StepStatus;
+  const hasTask = !!step.taskId || !!step.contractId;
+  const canExpand = hasTask && ['running', 'completed', 'failed'].includes(status);
 
   return (
-    <div className={`steps-diagram-card steps-diagram-card--${status}`}>
-      <div className="steps-diagram-card-header">
+    <div className={`steps-diagram-card steps-diagram-card--${status} ${isExpanded ? 'steps-diagram-card--expanded' : ''}`}>
+      <div
+        className={`steps-diagram-card-header ${canExpand ? 'steps-diagram-card-header--clickable' : ''}`}
+        onClick={canExpand ? onToggleExpand : undefined}
+        role={canExpand ? 'button' : undefined}
+        tabIndex={canExpand ? 0 : undefined}
+        onKeyDown={canExpand ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined}
+      >
         <span className="steps-diagram-card-name">{step.name}</span>
-        <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}>
-          {STATUS_LABELS[status] || status}
-        </span>
+        <div className="steps-diagram-card-header-right">
+          <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}>
+            {STATUS_LABELS[status] || status}
+          </span>
+          {canExpand && (
+            <span className={`steps-diagram-expand-icon ${isExpanded ? 'expanded' : ''}`}>
+              &#x25B6;
+            </span>
+          )}
+        </div>
       </div>
-      {step.description && (
+      {step.description && !isExpanded && (
         <p className="steps-diagram-card-desc">{step.description}</p>
       )}
       <div className="steps-diagram-card-footer">
@@ -46,20 +70,68 @@ function StepCard({ step }: { step: DirectiveStep }) {
           <span className="steps-diagram-card-time">
             Completed {formatTime(step.completedAt)}
           </span>
+          {hasTask && (
+            <button
+              className={`steps-diagram-card-expand-btn ${expanded ? 'steps-diagram-card-expand-btn--open' : ''}`}
+              onClick={() => setExpanded((v) => !v)}
+              title={expanded ? 'Collapse log feed' : 'Expand log feed'}
+            >
+              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <polyline points="6 9 12 15 18 9" />
+              </svg>
+            </button>
+          )}
+        </div>
+        {step.description && (
+          <p className="steps-diagram-card-desc">{step.description}</p>
         )}
+        <div className="steps-diagram-card-footer">
+          <span className="steps-diagram-card-index">#{step.orderIndex}</span>
+          {status === 'running' && (
+            <span className="steps-diagram-card-progress">In progress...</span>
+          )}
+          {status === 'completed' && step.completedAt && (
+            <span className="steps-diagram-card-time">
+              Completed {formatTime(step.completedAt)}
+            </span>
+          )}
+        </div>
       </div>
+
+      {/* Expandable log feed */}
+      {isExpanded && hasTask && (
+        <StepLogFeed
+          taskId={step.taskId || step.contractId}
+          stepName={step.name}
+          stepStatus={status}
+          onCollapse={onCollapse}
+        />
+      )}
     </div>
   );
 }
 
-export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProps) {
+export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) {
   const [steps, setSteps] = useState<DirectiveStep[]>([]);
   const [directiveStatus, setDirectiveStatus] = useState<string>('');
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
+  const [expandedStepId, setExpandedStepId] = useState<string | null>(null);
   const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
   const prevStepCountRef = useRef(0);
 
+  const toggleStep = useCallback((stepId: string) => {
+    setExpandedSteps((prev) => {
+      const next = new Set(prev);
+      if (next.has(stepId)) {
+        next.delete(stepId);
+      } else {
+        next.add(stepId);
+      }
+      return next;
+    });
+  }, []);
+
   const fetchSteps = useCallback(async () => {
     try {
       const data: DirectiveWithSteps = await getDirective(directiveId);
@@ -67,7 +139,7 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
       setDirectiveStatus(data.status || '');
       setError(null);
     } catch (err) {
-      setError(err instanceof Error ? err.message : 'Failed to load steps');
+      setError(err instanceof Error ? err.message : 'Failed to load contracts');
     } finally {
       setLoading(false);
     }
@@ -86,11 +158,29 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
     prevStepCountRef.current = steps.length;
   }, [steps.length]);
 
+  // Keyboard shortcut: Escape to collapse expanded step
+  useEffect(() => {
+    const handler = (e: KeyboardEvent) => {
+      if (e.key === 'Escape' && expandedStepId) {
+        setExpandedStepId(null);
+      }
+    };
+    document.addEventListener('keydown', handler);
+    return () => document.removeEventListener('keydown', handler);
+  }, [expandedStepId]);
+
+  const toggleExpand = useCallback((stepId: string) => {
+    setExpandedStepId(prev => prev === stepId ? null : stepId);
+  }, []);
+
+  const collapseExpanded = useCallback(() => {
+    setExpandedStepId(null);
+  }, []);
+
   const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length;
   const totalCount = steps.length;
   const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase());
   const isBuilding = isActive && steps.length === 0;
-  const isAddingSteps = isActive && steps.length > 0 && steps.length > prevStepCountRef.current;
 
   // Group steps by orderIndex
   const groupedSteps: Map<number, DirectiveStep[]> = new Map();
@@ -106,12 +196,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
     return (
       <div className="steps-diagram" contentEditable={false}>
         <div className="steps-diagram-header">
-          <span className="steps-diagram-header-title">Steps</span>
+          <span className="steps-diagram-header-title">Contract Steps</span>
           <span className="steps-diagram-header-author">Authored by Makima</span>
         </div>
         <div className="steps-diagram-loading">
           <div className="steps-diagram-spinner" />
-          <span>Loading steps...</span>
+          <span>Loading contracts...</span>
         </div>
       </div>
     );
@@ -121,22 +211,22 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
     return (
       <div className="steps-diagram" contentEditable={false}>
         <div className="steps-diagram-header">
-          <span className="steps-diagram-header-title">Steps</span>
+          <span className="steps-diagram-header-title">Contract Steps</span>
           <span className="steps-diagram-header-author">Authored by Makima</span>
         </div>
-        <div className="steps-diagram-error">Failed to load steps: {error}</div>
+        <div className="steps-diagram-error">Failed to load contracts: {error}</div>
       </div>
     );
   }
 
   return (
-    <div className="steps-diagram" contentEditable={false}>
+    <div className={`steps-diagram ${expandedStepId ? 'steps-diagram--has-expanded' : ''}`} contentEditable={false}>
       <div className="steps-diagram-header">
         <div className="steps-diagram-header-left">
-          <span className="steps-diagram-header-title">Steps</span>
+          <span className="steps-diagram-header-title">Contract Steps</span>
           {totalCount > 0 && (
             <span className="steps-diagram-header-count">
-              {completedCount}/{totalCount} completed
+              {completedCount}/{totalCount} fulfilled
             </span>
           )}
         </div>
@@ -148,12 +238,12 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
           <div className="steps-diagram-planning-dots">
             <span /><span /><span />
           </div>
-          <span>Makima is building the plan...</span>
+          <span>Makima is drafting contracts...</span>
         </div>
       )}
 
       {totalCount === 0 && !isBuilding && (
-        <div className="steps-diagram-empty">No steps defined yet.</div>
+        <div className="steps-diagram-empty">No contract steps defined yet.</div>
       )}
 
       {totalCount > 0 && (
@@ -168,7 +258,13 @@ export function StepsDiagramComponent({ directiveId }: StepsDiagramComponentProp
               )}
               <div className="steps-diagram-group">
                 {groupSteps.map((step) => (
-                  <StepCard key={step.id} step={step} />
+                  <StepCard
+                    key={step.id}
+                    step={step}
+                    isExpanded={expandedStepId === step.id}
+                    onToggleExpand={() => toggleExpand(step.id)}
+                    onCollapse={collapseExpanded}
+                  />
                 ))}
               </div>
             </React.Fragment>
-- 
cgit v1.2.3

