From 8e2bbcab1a7b3b9005803d7ce3bfce7fa483a4d7 Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sat, 16 May 2026 18:04:17 +0100
Subject: fix(directives): tasks folder visibility, circular auto-save timer,
 unlock-to-edit (#133)

* feat: soryu - makima: Fix tasks/ folder visibility and rename for multi-contract directives

* feat: soryu - makima: Replace auto-save countdown text/bar with a circular timer

* feat: soryu - makima: Require Unlock before editing a locked contract body
---
 makima/frontend/src/routes/document-directives.tsx | 54 ++++++++++++++++++----
 1 file changed, 46 insertions(+), 8 deletions(-)

(limited to 'makima/frontend/src/routes/document-directives.tsx')

diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 06e427a..479dcd8 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -266,6 +266,11 @@ function DirectiveFolder({
     return { activeDocs: active, shippedDocs: shipped };
   }, [docs]);
 
+  // When a directive owns more than one contract, the two `tasks/` folders
+  // would otherwise be ambiguous. We pass this down to DocumentTasksFolder
+  // so it can rename itself to `tasks - <contract name>/` for clarity.
+  const multipleContracts = activeDocs.length + shippedDocs.length > 1;
+
   const handleCreate = useCallback(async () => {
     if (creating) return;
     setCreating(true);
@@ -433,6 +438,8 @@ function DirectiveFolder({
                     refreshNonce={refreshNonce}
                     selectedTaskId={selectedTaskIdForFolder}
                     onSelectTask={onSelectTask}
+                    contractLabel={fileLabel(doc, directive)}
+                    multipleContracts={multipleContracts}
                   />
                 </div>
               ))}
@@ -477,6 +484,8 @@ function DirectiveFolder({
                           refreshNonce={refreshNonce}
                           selectedTaskId={selectedTaskIdForFolder}
                           onSelectTask={onSelectTask}
+                          contractLabel={fileLabel(doc, directive)}
+                          multipleContracts={multipleContracts}
                         />
                       </div>
                     ))}
@@ -627,6 +636,13 @@ interface DocumentTasksFolderProps {
   selectedTaskId: string | null;
   /** Click handler for step/task rows — navigates to the live transcript. */
   onSelectTask: (directiveId: string, taskId: string) => void;
+  /** Human-readable contract label (already resolved via fileLabel). Used to
+   *  disambiguate multiple tasks/ folders under the same directive. */
+  contractLabel: string;
+  /** True when the parent directive owns more than one contract — drives the
+   *  `tasks - <contractLabel>/` rename so the two sibling tasks/ folders are
+   *  distinguishable. Single-contract directives keep the plain `tasks/`. */
+  multipleContracts: boolean;
 }
 
 function DocumentTasksFolder({
@@ -637,6 +653,8 @@ function DocumentTasksFolder({
   refreshNonce,
   selectedTaskId,
   onSelectTask,
+  contractLabel,
+  multipleContracts,
 }: DocumentTasksFolderProps) {
   const [open, setOpen] = useState(defaultOpen);
   const [data, setData] = useState<ContractTasksResponse | null>(null);
@@ -672,24 +690,29 @@ function DocumentTasksFolder({
 
   const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0);
 
-  // Don't render the folder at all if we've fetched and the document has
-  // no tasks. This is the cleanest visual: a draft document just shows up
-  // as a single row with no children. The empty-folder check is gated on
-  // a successful fetch so we don't flash "no tasks/" rows during loading.
-  if (data && total === 0 && !loading && !error) {
-    return null;
-  }
+  // Folder always renders (even when empty) so the user can click into a
+  // fresh contract's tasks/ folder and see it stay visible. The empty state
+  // shows a muted "no tasks yet" placeholder inside the open body — same
+  // visual weight as the existing "Loading tasks…" / error placeholders.
+  //
+  // When the parent directive owns multiple contracts, both tasks/ folders
+  // are disambiguated as `tasks - <contractLabel>/` so the user can tell
+  // them apart. Single-contract directives keep the plain `tasks/` label.
+  const headerLabel = multipleContracts
+    ? `tasks - ${contractLabel}/`
+    : "tasks/";
 
   return (
     <div>
       <button
         type="button"
         onClick={() => setOpen((v) => !v)}
+        title={headerLabel}
         className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`}
       >
         <Caret open={open} />
         <FolderIcon open={open} />
-        <span>tasks/</span>
+        <span className="truncate text-left">{headerLabel}</span>
         {total > 0 && (
           <span className="ml-auto text-[10px] text-[#556677]">{total}</span>
         )}
@@ -706,6 +729,11 @@ function DocumentTasksFolder({
               {error}
             </div>
           )}
+          {data && total === 0 && !loading && !error && (
+            <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677] italic`}>
+              no tasks yet
+            </div>
+          )}
           {data?.steps.map((step) => (
             <StepRow
               key={`step-${step.id}`}
@@ -1428,6 +1456,16 @@ function EditorShell({
           onPickUpOrders={async () => {
             await pickUpOrders();
           }}
+          // Locked-and-started (`active`), `queued`, `shipped`, and
+          // `archived` contracts must be unlocked before edits are
+          // accepted. Only `draft` is freely editable; everything else
+          // shows the in-editor Unlock affordance.
+          editable={doc.status === "draft"}
+          onRequestUnlock={async () => {
+            const updated = await unlockDirectiveContract(doc.id);
+            setDoc(updated);
+            onDocumentChanged();
+          }}
         />
       </div>
     );
-- 
cgit v1.2.3

