summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/files.tsx
blob: 6cfb3ca489f0ffb4571ec35675e8273a25a18f49 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                 


                                                        
                                                                                 
                                                        

                                                                                
                                             
                                                               



                                      

                                                                                                   
                                                  

                                     






























                                                                                  

                                             
                                                                                                                   

                                                                            
                                                  
                                                                                 
                                                                                    


                                                                                    







                                                                                                              
                                         















































                                                                                        




                                        







                                                           
                              
                                      


                                                     




                                

                                       
     
                                             
 


                                             







                                                                                                  
                                     
















                                                                                              


               
                                                                   

                                                                         
                                                     


                                                     

                              


                                                         



                                                
               







                               
























                                                                  

                                      












                                                                                                  
       
      
                                                 

    












                                                      











                                                            
                                    
 

                                                              












                                                                                            
         

       
                                                     














                                                            
                                    
 

                                                              












                                                                                            
         

       
                                                     

    

































































































































































































































                                                                                            
                                                                        

                                                     
                                                  

























                                                                             




















                                                                  

                                       


                                     


                                                         
                                 



                                                     
                                 

    
                                                    
                                                

















                                                                                  


                                      


                                   


                       



                                                                                               



                                         
                               
     
                                                                
 
                                                                                         

                                             
         





                                                    
               
                                 
     
                                   
 




                                                        


                                                   
                            
                                   
     
                                                            








                                                                












                                                                                                


         
                                                                                  




                                                             


                                                   

                            
                              
                                   
     
                                             


                                                       
                            

         



                                                                          
                                                                                
                   
                                                                                                                   




                             







                                                                        

                                                             




                                                                     
                                                         

                                                       

                                                   







                                                              


                                      







                                                                       

                  






                                                                              
                                         

                                       
                                   
                                                   


             













                                                         

                                                 



                                               































                                                                                                                                                       





















































































































                                                                                                                                                                                     


          
import { useState, useCallback, useEffect, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { FileList } from "../components/files/FileList";
import { FileDetail, type FocusedElement } from "../components/files/FileDetail";
import { CliInput } from "../components/files/CliInput";
import { ConflictNotification } from "../components/files/ConflictNotification";
import { UpdateNotification } from "../components/files/UpdateNotification";
import { useFiles } from "../hooks/useFiles";
import { useVersionHistory } from "../hooks/useVersionHistory";
import {
  useFileSubscription,
  type FileUpdateEvent,
} from "../hooks/useFileSubscription";
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() {
  const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
  const navigate = useNavigate();

  // Redirect to login if not authenticated (when auth is configured)
  useEffect(() => {
    if (!authLoading && isAuthConfigured && !isAuthenticated) {
      navigate("/login");
    }
  }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);

  // Show loading while checking auth
  if (authLoading) {
    return (
      <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
        <Masthead showNav />
        <main className="flex-1 flex items-center justify-center">
          <p className="text-[#7788aa] font-mono text-sm">Loading...</p>
        </main>
      </div>
    );
  }

  // Don't render if not authenticated (will redirect)
  if (isAuthConfigured && !isAuthenticated) {
    return null;
  }

  return <FilesPageContent />;
}

function FilesPageContent() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const { files, loading, error, conflict, clearConflict, fetchFile, editFile, removeFile, saveFile } = useFiles();
  const [fileDetail, setFileDetail] = useState<FileDetailType | null>(null);
  const [detailLoading, setDetailLoading] = useState(false);
  const [creating, setCreating] = useState(false);
  const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null);
  const [remoteFileData, setRemoteFileData] = useState<FileDetailType | null>(null);
  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);
  // Track the version we just successfully saved (to ignore its WebSocket notification)
  const lastSavedVersionRef = useRef<number | null>(null);
  // Use refs for values checked in WebSocket callback to avoid stale closures
  const hasLocalChangesRef = useRef(false);
  const isActivelyEditingRef = useRef(false);
  const currentVersionRef = useRef<number | null>(null);

  // Helper functions to update refs (used only in callbacks, not for rendering)
  const updateHasLocalChanges = useCallback((value: boolean) => {
    hasLocalChangesRef.current = value;
  }, []);

  const updateIsActivelyEditing = useCallback((value: boolean) => {
    isActivelyEditingRef.current = value;
  }, []);

  // Version history
  const {
    versions,
    loading: versionsLoading,
    selectedVersion,
    loadingVersion,
    restoring,
    fetchVersion,
    restoreToVersion,
    clearSelectedVersion,
    fetchVersions,
  } = useVersionHistory({
    fileId: id || null,
    currentVersion: fileDetail?.version || 0,
  });

  // Handle version restore
  const handleRestoreVersion = useCallback(
    async (targetVersion: number) => {
      const result = await restoreToVersion(targetVersion);
      if (result) {
        currentVersionRef.current = result.version;
        setFileDetail(result);
        updateHasLocalChanges(false);
        // Refresh version list after restore
        fetchVersions();
      }
    },
    [restoreToVersion, fetchVersions, updateHasLocalChanges]
  );

  // Load file detail when URL has an id
  useEffect(() => {
    if (id) {
      setDetailLoading(true);
      updateHasLocalChanges(false);
      // Reset pending update tracking when switching files
      pendingUpdateRef.current = false;
      lastSentVersionRef.current = null;
      lastSavedVersionRef.current = null;
      currentVersionRef.current = null;
      setRemoteUpdate(null);
      setRemoteFileData(null);
      setFocusedElement(null);
      fetchFile(id).then((detail) => {
        if (detail) {
          currentVersionRef.current = detail.version;
        }
        setFileDetail(detail);
        setDetailLoading(false);
      });
    } else {
      setFileDetail(null);
      currentVersionRef.current = null;
      updateHasLocalChanges(false);
    }
  }, [id, fetchFile, updateHasLocalChanges]);

  // Handle file update events from WebSocket
  const handleFileUpdate = useCallback(
    async (event: FileUpdateEvent) => {
      // Check if this is a version we just saved - ignore it
      // This handles the case where the WebSocket arrives after the HTTP response
      if (lastSavedVersionRef.current !== null && event.version === lastSavedVersionRef.current) {
        lastSavedVersionRef.current = null;
        return;
      }

      // If we have a pending update, check if this is our own update
      if (pendingUpdateRef.current) {
        if (lastSentVersionRef.current !== null) {
          const expectedNewVersion = lastSentVersionRef.current + 1;
          if (event.version === expectedNewVersion) {
            // This is our own update - ignore it
            pendingUpdateRef.current = false;
            lastSentVersionRef.current = null;
            return;
          }
        }
        // We sent an update but received a different version - could be a race condition
        // Still ignore since we have an update in flight
        return;
      }

      // Check if this version matches what we already have
      // This catches cases where our save's WebSocket arrives late
      if (currentVersionRef.current !== null && event.version === currentVersionRef.current) {
        return;
      }

      // If no local changes and not actively editing, auto-refresh
      // Use refs to get current values (avoid stale closure issues)
      if (!hasLocalChangesRef.current && !isActivelyEditingRef.current) {
        const detail = await fetchFile(event.fileId);
        if (detail) {
          currentVersionRef.current = detail.version;
        }
        setFileDetail(detail);
      } else {
        // Fetch remote version for diff display
        const remoteData = await fetchFile(event.fileId);
        setRemoteFileData(remoteData);
        // Show notification about remote update
        setRemoteUpdate(event);
      }
    },
    [fetchFile]
  );

  // Subscribe to file updates
  useFileSubscription({
    fileId: id || null,
    onUpdate: handleFileUpdate,
  });

  const handleSelectFile = useCallback(
    (fileId: string) => {
      navigate(`/files/${fileId}`);
    },
    [navigate]
  );

  const handleBack = useCallback(() => {
    navigate("/files");
  }, [navigate]);

  const handleDelete = useCallback(
    async (fileId: string) => {
      if (confirm("Are you sure you want to delete this file?")) {
        const success = await removeFile(fileId);
        if (success && id === fileId) {
          navigate("/files");
        }
      }
    },
    [removeFile, id, navigate]
  );

  const handleSave = useCallback(
    async (fileId: string, name: string, description: string) => {
      if (!fileDetail) return;
      pendingUpdateRef.current = true;
      lastSentVersionRef.current = fileDetail.version;
      try {
        const result = await editFile(fileId, { name, description, version: fileDetail.version });
        if (result) {
          // Track the saved version to ignore its WebSocket notification
          lastSavedVersionRef.current = result.version;
          currentVersionRef.current = result.version;
          setFileDetail(result);
          updateHasLocalChanges(false);
        }
      } finally {
        pendingUpdateRef.current = false;
        lastSentVersionRef.current = null;
      }
    },
    [editFile, fileDetail, updateHasLocalChanges]
  );

  const handleBodyUpdate = useCallback(
    (body: BodyElement[], summary: string | null) => {
      if (fileDetail) {
        setFileDetail({
          ...fileDetail,
          body,
          summary,
        });
      }
    },
    [fileDetail]
  );

  const handleBodyElementUpdate = useCallback(
    async (index: number, element: BodyElement) => {
      if (fileDetail && id) {
        // Create new body array with updated element
        const newBody = [...fileDetail.body];
        newBody[index] = element;

        // Update local state immediately for responsiveness
        setFileDetail({
          ...fileDetail,
          body: newBody,
        });
        updateHasLocalChanges(true);

        // Save to backend with version for optimistic locking
        pendingUpdateRef.current = true;
        lastSentVersionRef.current = fileDetail.version;
        try {
          const result = await editFile(id, { body: newBody, version: fileDetail.version });
          if (result) {
            // Track the saved version to ignore its WebSocket notification
            lastSavedVersionRef.current = result.version;
            currentVersionRef.current = result.version;
            setFileDetail(result);
            updateHasLocalChanges(false);
          }
        } finally {
          pendingUpdateRef.current = false;
          lastSentVersionRef.current = null;
        }
      }
    },
    [fileDetail, id, editFile, updateHasLocalChanges]
  );

  const handleBodyReorder = useCallback(
    async (fromIndex: number, toIndex: number) => {
      if (fileDetail && id) {
        // Create new body array with reordered elements
        const newBody = [...fileDetail.body];
        const [movedElement] = newBody.splice(fromIndex, 1);
        newBody.splice(toIndex, 0, movedElement);

        // Update local state immediately for responsiveness
        setFileDetail({
          ...fileDetail,
          body: newBody,
        });
        updateHasLocalChanges(true);

        // Save to backend with version for optimistic locking
        pendingUpdateRef.current = true;
        lastSentVersionRef.current = fileDetail.version;
        try {
          const result = await editFile(id, { body: newBody, version: fileDetail.version });
          if (result) {
            // Track the saved version to ignore its WebSocket notification
            lastSavedVersionRef.current = result.version;
            currentVersionRef.current = result.version;
            setFileDetail(result);
            updateHasLocalChanges(false);
          }
        } finally {
          pendingUpdateRef.current = false;
          lastSentVersionRef.current = null;
        }
      }
    },
    [fileDetail, id, editFile, updateHasLocalChanges]
  );

  // Element action handlers for context menu
  const handleBodyElementDelete = useCallback(
    async (index: number) => {
      if (fileDetail && id) {
        const newBody = fileDetail.body.filter((_, i) => i !== index);

        // Update local state immediately
        setFileDetail({
          ...fileDetail,
          body: newBody,
        });
        updateHasLocalChanges(true);

        // Clear focus if deleting focused element
        if (focusedElement?.index === index) {
          setFocusedElement(null);
        } else if (focusedElement && focusedElement.index > index) {
          // Adjust focus index if deleting an element before it
          setFocusedElement({
            ...focusedElement,
            index: focusedElement.index - 1,
          });
        }

        // Save to backend
        pendingUpdateRef.current = true;
        lastSentVersionRef.current = fileDetail.version;
        try {
          const result = await editFile(id, { body: newBody, version: fileDetail.version });
          if (result) {
            lastSavedVersionRef.current = result.version;
            currentVersionRef.current = result.version;
            setFileDetail(result);
            updateHasLocalChanges(false);
          }
        } finally {
          pendingUpdateRef.current = false;
          lastSentVersionRef.current = null;
        }
      }
    },
    [fileDetail, id, editFile, updateHasLocalChanges, focusedElement]
  );

  const handleBodyElementDuplicate = useCallback(
    async (index: number) => {
      if (fileDetail && id) {
        const elementToDuplicate = fileDetail.body[index];
        if (!elementToDuplicate) return;

        const newBody = [...fileDetail.body];
        // Insert duplicate after the original
        newBody.splice(index + 1, 0, { ...elementToDuplicate });

        // Update local state immediately
        setFileDetail({
          ...fileDetail,
          body: newBody,
        });
        updateHasLocalChanges(true);

        // Adjust focus index if duplicating before focused element
        if (focusedElement && focusedElement.index > index) {
          setFocusedElement({
            ...focusedElement,
            index: focusedElement.index + 1,
          });
        }

        // Save to backend
        pendingUpdateRef.current = true;
        lastSentVersionRef.current = fileDetail.version;
        try {
          const result = await editFile(id, { body: newBody, version: fileDetail.version });
          if (result) {
            lastSavedVersionRef.current = result.version;
            currentVersionRef.current = result.version;
            setFileDetail(result);
            updateHasLocalChanges(false);
          }
        } finally {
          pendingUpdateRef.current = false;
          lastSentVersionRef.current = null;
        }
      }
    },
    [fileDetail, id, editFile, updateHasLocalChanges, focusedElement]
  );

  const handleFocusElement = useCallback((element: FocusedElement | null) => {
    setFocusedElement(element);
  }, []);

  const handleClearFocus = useCallback(() => {
    setFocusedElement(null);
  }, []);

  // Convert element to a different type
  const handleConvertElement = useCallback(
    async (index: number, toType: string) => {
      if (!fileDetail || !id) return;

      const element = fileDetail.body[index];
      if (!element) return;

      // Extract text content from current element
      let textContent = "";
      switch (element.type) {
        case "heading":
        case "paragraph":
          textContent = element.text;
          break;
        case "code":
          textContent = element.content;
          break;
        case "list":
          textContent = element.items.join("\n");
          break;
        default:
          return; // Can't convert charts/images
      }

      // Create new element based on target type
      let newElement: BodyElement;
      if (toType === "paragraph") {
        newElement = { type: "paragraph", text: textContent };
      } else if (toType === "list_unordered") {
        const items = textContent.split("\n").filter(line => line.trim());
        newElement = { type: "list", ordered: false, items };
      } else if (toType === "list_ordered") {
        const items = textContent.split("\n").filter(line => line.trim());
        newElement = { type: "list", ordered: true, items };
      } else if (toType === "code") {
        newElement = { type: "code", content: textContent };
      } else if (toType.startsWith("heading_")) {
        const level = parseInt(toType.replace("heading_", ""), 10) as 1 | 2 | 3 | 4 | 5 | 6;
        newElement = { type: "heading", level, text: textContent };
      } else {
        return; // Unknown type
      }

      const newBody = [...fileDetail.body];
      newBody[index] = newElement;

      // Update local state
      setFileDetail({ ...fileDetail, body: newBody });
      updateHasLocalChanges(true);

      // Update focus if this element was focused
      if (focusedElement?.index === index) {
        setFocusedElement({
          index,
          type: newElement.type,
          preview: textContent.slice(0, 50) + (textContent.length > 50 ? "..." : ""),
        });
      }

      // Save to backend
      pendingUpdateRef.current = true;
      lastSentVersionRef.current = fileDetail.version;
      try {
        const result = await editFile(id, { body: newBody, version: fileDetail.version });
        if (result) {
          lastSavedVersionRef.current = result.version;
          currentVersionRef.current = result.version;
          setFileDetail(result);
          updateHasLocalChanges(false);
        }
      } finally {
        pendingUpdateRef.current = false;
        lastSentVersionRef.current = null;
      }
    },
    [fileDetail, id, editFile, updateHasLocalChanges, focusedElement]
  );

  // Generate from element - focus on it and pre-fill a prompt
  const handleGenerateFromElement = useCallback(
    (index: number, action: string) => {
      if (!fileDetail) return;

      const element = fileDetail.body[index];
      if (!element) return;

      // Get preview text
      let preview = "";
      switch (element.type) {
        case "heading":
        case "paragraph":
          preview = element.text.slice(0, 50);
          break;
        case "code":
          preview = element.content.slice(0, 50);
          break;
        case "list":
          preview = element.items[0]?.slice(0, 40) || "";
          break;
        default:
          preview = "Element";
      }

      // Focus on the element
      setFocusedElement({
        index,
        type: element.type,
        preview: preview + (preview.length >= 50 ? "..." : ""),
      });

      // Set suggested prompt based on action
      let prompt = "";
      switch (action) {
        case "elaborate":
          prompt = "Elaborate and expand on this content";
          break;
        case "summarize":
          prompt = "Summarize this content";
          break;
        case "extract_actions":
          prompt = "Extract action items from this content";
          break;
      }
      setSuggestedPrompt(prompt);
    },
    [fileDetail]
  );

  // Create a mesh task from an element - shows contract selection modal
  const handleCreateTaskFromElement = useCallback(
    async (index: number, selectedText?: string) => {
      if (!fileDetail || contractsLoading) return;

      const element = fileDetail.body[index];
      if (!element) return;

      // Get the content to use as task plan
      let content = selectedText || "";
      if (!content) {
        switch (element.type) {
          case "heading":
          case "paragraph":
            content = element.text;
            break;
          case "code":
            content = element.content;
            break;
          case "list":
            content = element.items.join("\n");
            break;
          default:
            content = "Task from file element";
        }
      }

      // 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({
          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);
      }
    },
    [pendingTaskData, fileDetail]
  );

  // Open contract selection modal for file creation
  const handleCreate = useCallback(async () => {
    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({
        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, pendingFileData, saveFile, editFile, navigate]);

  const handleUploadMarkdown = useCallback(async (name: string, body: BodyElement[]) => {
    if (creating || contractsLoading) return;
    setContractsLoading(true);
    try {
      const response = await listContracts();
      setContracts(response.contracts);
      setPendingFileData({ name, body });
      setShowFileContractModal(true);
    } catch (e) {
      console.error("Failed to load contracts:", e);
    } finally {
      setContractsLoading(false);
    }
  }, [creating, contractsLoading]);

  // Conflict resolution handlers
  const handleConflictReload = useCallback(async () => {
    if (id) {
      clearConflict();
      const detail = await fetchFile(id);
      if (detail) {
        currentVersionRef.current = detail.version;
      }
      setFileDetail(detail);
      updateHasLocalChanges(false);
    }
  }, [id, clearConflict, fetchFile, updateHasLocalChanges]);

  const handleConflictForceOverwrite = useCallback(async () => {
    if (id && fileDetail) {
      clearConflict();
      // Fetch latest version first
      const latest = await fetchFile(id);
      if (latest) {
        // Retry with latest version
        pendingUpdateRef.current = true;
        lastSentVersionRef.current = latest.version;
        try {
          const result = await editFile(id, { body: fileDetail.body, version: latest.version });
          if (result) {
            // Track the saved version to ignore its WebSocket notification
            lastSavedVersionRef.current = result.version;
            currentVersionRef.current = result.version;
            setFileDetail(result);
            updateHasLocalChanges(false);
          }
        } finally {
          pendingUpdateRef.current = false;
          lastSentVersionRef.current = null;
        }
      }
    }
  }, [id, fileDetail, clearConflict, fetchFile, editFile, updateHasLocalChanges]);

  // Remote update handlers
  const handleRemoteUpdateRefresh = useCallback(async () => {
    if (id) {
      const detail = await fetchFile(id);
      if (detail) {
        currentVersionRef.current = detail.version;
      }
      setFileDetail(detail);
      setRemoteUpdate(null);
      setRemoteFileData(null);
      updateHasLocalChanges(false);
    }
  }, [id, fetchFile, updateHasLocalChanges]);

  const handleRemoteUpdateDismiss = useCallback(() => {
    setRemoteUpdate(null);
    setRemoteFileData(null);
  }, []);

  return (
    <div className="relative z-10 h-screen flex flex-col overflow-hidden">
      <Masthead showTicker={false} showNav />

      <main className="flex-1 p-4 md:p-6 min-h-0 overflow-hidden flex flex-col">
        {error && (
          <div className="mb-4 p-3 border border-red-400/50 bg-red-400/10 text-red-400 font-mono text-sm shrink-0">
            {error}
          </div>
        )}

        {id && fileDetail ? (
          <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
            <div className="flex-1 min-h-0 overflow-hidden">
              <FileDetail
                file={fileDetail}
                loading={detailLoading}
                onBack={handleBack}
                onSave={handleSave}
                onDelete={handleDelete}
                onBodyElementUpdate={handleBodyElementUpdate}
                onBodyReorder={handleBodyReorder}
                onBodyElementDelete={handleBodyElementDelete}
                onBodyElementDuplicate={handleBodyElementDuplicate}
                onConvertElement={handleConvertElement}
                onGenerateFromElement={handleGenerateFromElement}
                onCreateTaskFromElement={handleCreateTaskFromElement}
                onEditingChange={updateIsActivelyEditing}
                hasPendingRemoteUpdate={!!remoteUpdate}
                onOverwrite={handleRemoteUpdateDismiss}
                focusedElement={focusedElement}
                onFocusElement={handleFocusElement}
                versions={versions}
                versionsLoading={versionsLoading}
                selectedVersion={selectedVersion}
                loadingVersion={loadingVersion}
                restoring={restoring}
                onSelectVersion={fetchVersion}
                onRestoreVersion={handleRestoreVersion}
                onClearVersionSelection={clearSelectedVersion}
              />
            </div>
            <div className="shrink-0">
              <CliInput
                fileId={id}
                onUpdate={handleBodyUpdate}
                focusedElement={focusedElement}
                onClearFocus={handleClearFocus}
                suggestedPrompt={suggestedPrompt}
                onClearSuggestedPrompt={() => setSuggestedPrompt(null)}
              />
            </div>
          </div>
        ) : id && detailLoading ? (
          <div className="panel h-full flex items-center justify-center">
            <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
          </div>
        ) : (
          <FileList
            files={files}
            loading={loading || creating}
            onSelect={handleSelectFile}
            onDelete={handleDelete}
            onCreate={handleCreate}
            onUploadMarkdown={handleUploadMarkdown}
          />
        )}
      </main>

      {/* Conflict notification */}
      {conflict?.hasConflict && (
        <ConflictNotification
          onReload={handleConflictReload}
          onForceOverwrite={handleConflictForceOverwrite}
          onDismiss={clearConflict}
        />
      )}

      {/* Remote update notification */}
      {remoteUpdate && (
        <UpdateNotification
          updatedBy={remoteUpdate.updatedBy}
          localBody={fileDetail?.body || []}
          remoteBody={remoteFileData?.body || []}
          onRefresh={handleRemoteUpdateRefresh}
          onDismiss={handleRemoteUpdateDismiss}
        />
      )}

      {/* Task created notification */}
      {createdTask && (
        <div className="fixed bottom-4 right-4 z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] p-4 shadow-lg max-w-sm">
          <div className="flex items-start gap-3">
            <span className="text-[#75aafc] text-lg">@</span>
            <div className="flex-1">
              <p className="font-mono text-xs text-[#9bc3ff] mb-1">Task created</p>
              <p className="font-mono text-sm text-white truncate mb-3">
                {createdTask.name}
              </p>
              <div className="flex gap-2">
                <button
                  onClick={() => {
                    navigate(`/mesh/${createdTask.id}`);
                    setCreatedTask(null);
                  }}
                  className="px-3 py-1 font-mono text-xs text-[#0a1628] bg-[#75aafc] hover:bg-[#9bc3ff] transition-colors"
                >
                  Go to task
                </button>
                <button
                  onClick={() => setCreatedTask(null)}
                  className="px-3 py-1 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.3)] hover:border-[#75aafc] transition-colors"
                >
                  Dismiss
                </button>
              </div>
            </div>
          </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>
  );
}