From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Sun, 11 Jan 2026 05:52:14 +0000
Subject:  Contract system

---
 makima/frontend/src/routes/files.tsx | 221 ++++++++++++++++++++++++++++++-----
 1 file changed, 194 insertions(+), 27 deletions(-)

(limited to 'makima/frontend/src/routes/files.tsx')

diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx
index 3ba2d52..6cfb3ca 100644
--- a/makima/frontend/src/routes/files.tsx
+++ b/makima/frontend/src/routes/files.tsx
@@ -12,8 +12,8 @@ import {
   useFileSubscription,
   type FileUpdateEvent,
 } from "../hooks/useFileSubscription";
-import type { FileDetail as FileDetailType, BodyElement, Task } from "../lib/api";
-import { createTask } from "../lib/api";
+import type { FileDetail as FileDetailType, BodyElement, Task, ContractSummary } from "../lib/api";
+import { createTask, listContracts } from "../lib/api";
 import { useAuth } from "../contexts/AuthContext";
 
 export default function FilesPage() {
@@ -59,6 +59,14 @@ function FilesPageContent() {
   const [focusedElement, setFocusedElement] = useState<FocusedElement | null>(null);
   const [suggestedPrompt, setSuggestedPrompt] = useState<string | null>(null);
   const [createdTask, setCreatedTask] = useState<Task | null>(null);
+  // Contract selection modal state for task creation
+  const [showContractModal, setShowContractModal] = useState(false);
+  const [contracts, setContracts] = useState<ContractSummary[]>([]);
+  const [contractsLoading, setContractsLoading] = useState(false);
+  const [pendingTaskData, setPendingTaskData] = useState<{ name: string; plan: string } | null>(null);
+  // Contract selection modal state for file creation
+  const [showFileContractModal, setShowFileContractModal] = useState(false);
+  const [pendingFileData, setPendingFileData] = useState<{ name: string; body?: BodyElement[] } | null>(null);
   const pendingUpdateRef = useRef(false);
   // Track the last version we sent to detect our own updates
   const lastSentVersionRef = useRef<number | null>(null);
@@ -548,10 +556,10 @@ function FilesPageContent() {
     [fileDetail]
   );
 
-  // Create a mesh task from an element
+  // Create a mesh task from an element - shows contract selection modal
   const handleCreateTaskFromElement = useCallback(
     async (index: number, selectedText?: string) => {
-      if (!fileDetail) return;
+      if (!fileDetail || contractsLoading) return;
 
       const element = fileDetail.body[index];
       if (!element) return;
@@ -578,57 +586,98 @@ function FilesPageContent() {
       // Create a task name from the content
       const name = content.slice(0, 60) + (content.length > 60 ? "..." : "");
 
+      // Store pending task data and show contract selection modal
+      setPendingTaskData({ name, plan: content });
+      setContractsLoading(true);
+      try {
+        const response = await listContracts();
+        setContracts(response.contracts);
+        setShowContractModal(true);
+      } catch (e) {
+        console.error("Failed to load contracts:", e);
+      } finally {
+        setContractsLoading(false);
+      }
+    },
+    [fileDetail, contractsLoading]
+  );
+
+  // Create task with selected contract
+  const handleCreateTaskWithContract = useCallback(
+    async (contractId: string) => {
+      if (!pendingTaskData || !fileDetail) return;
+      setShowContractModal(false);
       try {
         const task = await createTask({
-          name,
-          plan: content,
+          contractId,
+          name: pendingTaskData.name,
+          plan: pendingTaskData.plan,
           description: `Created from ${fileDetail.name}`,
         });
         setCreatedTask(task);
+        setPendingTaskData(null);
       } catch (err) {
         console.error("Failed to create task:", err);
       }
     },
-    [fileDetail]
+    [pendingTaskData, fileDetail]
   );
 
+  // Open contract selection modal for file creation
   const handleCreate = useCallback(async () => {
-    if (creating) return;
+    if (creating || contractsLoading) return;
+    setContractsLoading(true);
+    try {
+      const response = await listContracts();
+      setContracts(response.contracts);
+      setPendingFileData({ name: `Untitled ${new Date().toLocaleDateString()}` });
+      setShowFileContractModal(true);
+    } catch (e) {
+      console.error("Failed to load contracts:", e);
+    } finally {
+      setContractsLoading(false);
+    }
+  }, [creating, contractsLoading]);
+
+  // Create file with selected contract
+  const handleCreateFileWithContract = useCallback(async (contractId: string) => {
+    if (creating || !pendingFileData) return;
+    setShowFileContractModal(false);
     setCreating(true);
     try {
       const newFile = await saveFile({
-        name: `Untitled ${new Date().toLocaleDateString()}`,
+        contractId,
+        name: pendingFileData.name,
+        body: pendingFileData.body,
         transcript: [],
       });
       if (newFile) {
+        // If there's body content, update it
+        if (pendingFileData.body && pendingFileData.body.length > 0) {
+          await editFile(newFile.id, { body: pendingFileData.body, version: newFile.version });
+        }
         navigate(`/files/${newFile.id}`);
       }
     } finally {
       setCreating(false);
+      setPendingFileData(null);
     }
-  }, [creating, saveFile, navigate]);
+  }, [creating, pendingFileData, saveFile, editFile, navigate]);
 
   const handleUploadMarkdown = useCallback(async (name: string, body: BodyElement[]) => {
-    if (creating) return;
-    setCreating(true);
+    if (creating || contractsLoading) return;
+    setContractsLoading(true);
     try {
-      const newFile = await saveFile({
-        name,
-        transcript: [],
-      });
-      if (newFile) {
-        // Update with the parsed body
-        const updated = await editFile(newFile.id, { body, version: newFile.version });
-        if (updated) {
-          navigate(`/files/${updated.id}`);
-        } else {
-          navigate(`/files/${newFile.id}`);
-        }
-      }
+      const response = await listContracts();
+      setContracts(response.contracts);
+      setPendingFileData({ name, body });
+      setShowFileContractModal(true);
+    } catch (e) {
+      console.error("Failed to load contracts:", e);
     } finally {
-      setCreating(false);
+      setContractsLoading(false);
     }
-  }, [creating, saveFile, editFile, navigate]);
+  }, [creating, contractsLoading]);
 
   // Conflict resolution handlers
   const handleConflictReload = useCallback(async () => {
@@ -808,6 +857,124 @@ function FilesPageContent() {
           </div>
         </div>
       )}
+
+      {/* Contract Selection Modal for Task Creation */}
+      {showContractModal && (
+        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+          <div className="bg-[#0d1117] border border-[#30363d] rounded-lg max-w-md w-full mx-4 max-h-[80vh] flex flex-col">
+            <div className="p-4 border-b border-[#30363d] flex justify-between items-center">
+              <h2 className="text-lg font-semibold text-white">Select Contract for Task</h2>
+              <button
+                onClick={() => {
+                  setShowContractModal(false);
+                  setPendingTaskData(null);
+                }}
+                className="text-[#8b949e] hover:text-white"
+              >
+                <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+                </svg>
+              </button>
+            </div>
+            <div className="p-4 overflow-y-auto flex-1">
+              {contracts.length === 0 ? (
+                <div className="text-center py-8">
+                  <p className="text-[#8b949e] mb-4">No contracts found. Create a contract first.</p>
+                  <button
+                    onClick={() => {
+                      setShowContractModal(false);
+                      setPendingTaskData(null);
+                      navigate("/contracts");
+                    }}
+                    className="px-4 py-2 bg-[#238636] hover:bg-[#2ea043] text-white rounded-md text-sm"
+                  >
+                    Create Contract
+                  </button>
+                </div>
+              ) : (
+                <div className="space-y-2">
+                  {contracts.map((contract) => (
+                    <button
+                      key={contract.id}
+                      onClick={() => handleCreateTaskWithContract(contract.id)}
+                      className="w-full text-left p-3 rounded-md border border-[#30363d] hover:border-[#58a6ff] hover:bg-[#161b22] transition-colors"
+                    >
+                      <div className="flex items-center justify-between">
+                        <span className="text-white font-medium">{contract.name}</span>
+                        <span className="text-xs px-2 py-0.5 rounded bg-[#21262d] text-[#8b949e]">
+                          {contract.phase}
+                        </span>
+                      </div>
+                      {contract.description && (
+                        <p className="text-sm text-[#8b949e] mt-1 line-clamp-2">{contract.description}</p>
+                      )}
+                    </button>
+                  ))}
+                </div>
+              )}
+            </div>
+          </div>
+        </div>
+      )}
+
+      {/* Contract Selection Modal for File Creation */}
+      {showFileContractModal && (
+        <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
+          <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-md w-full mx-4 max-h-[80vh] flex flex-col">
+            <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center">
+              <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff]">Select Contract for File</h2>
+              <button
+                onClick={() => {
+                  setShowFileContractModal(false);
+                  setPendingFileData(null);
+                }}
+                className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors"
+              >
+                <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
+                </svg>
+              </button>
+            </div>
+            <div className="p-4 overflow-y-auto flex-1">
+              {contracts.length === 0 ? (
+                <div className="text-center py-8">
+                  <p className="text-[#7788aa] font-mono text-xs mb-4">No contracts found. Create a contract first.</p>
+                  <button
+                    onClick={() => {
+                      setShowFileContractModal(false);
+                      setPendingFileData(null);
+                      navigate("/contracts");
+                    }}
+                    className="px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] transition-colors"
+                  >
+                    Create Contract
+                  </button>
+                </div>
+              ) : (
+                <div className="space-y-2">
+                  {contracts.map((contract) => (
+                    <button
+                      key={contract.id}
+                      onClick={() => handleCreateFileWithContract(contract.id)}
+                      className="w-full text-left p-3 border border-[rgba(117,170,252,0.15)] bg-[#0a1525] hover:border-[rgba(117,170,252,0.35)] hover:bg-[#0d1b2d] transition-colors"
+                    >
+                      <div className="flex items-center justify-between">
+                        <span className="text-[#9bc3ff] font-mono text-xs">{contract.name}</span>
+                        <span className="text-[10px] font-mono uppercase px-2 py-0.5 border border-[rgba(117,170,252,0.25)] text-[#75aafc]">
+                          {contract.phase}
+                        </span>
+                      </div>
+                      {contract.description && (
+                        <p className="text-[10px] font-mono text-[#7788aa] mt-1 line-clamp-2">{contract.description}</p>
+                      )}
+                    </button>
+                  ))}
+                </div>
+              )}
+            </div>
+          </div>
+        </div>
+      )}
     </div>
   );
 }
-- 
cgit v1.2.3

