summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 02:19:42 +0000
committerGitHub <noreply@github.com>2026-01-25 02:19:42 +0000
commit03ab90836707954277597dc21fd8035019d8e221 (patch)
tree061a880c6ea2cd3bee2fa80137a2e7e3bf3ec6fb
parent2003544969e5b7248ecd242b5cec50b324fa751b (diff)
downloadsoryu-03ab90836707954277597dc21fd8035019d8e221.tar.gz
soryu-03ab90836707954277597dc21fd8035019d8e221.zip
Move files tab and file pages to be accessible via contracts (#28)
* feat: remove Files from top-level navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update file links to use contract-scoped routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add contract context to FileDetail component - Add contractId, contractName, and onContractClick props to FileDetailProps - Update breadcrumb navigation to show contract name with path separator when viewing file within a contract context - Fall back to "Back to list" when no contract context is provided - This enables the FileDetail component to be used within the /contracts/:contractId/files/:fileId route Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update routes to nest files under contracts - Add react-router-dom for client-side routing - Create ContractList component to list all contracts - Create ContractDetail component with tabs (overview, files, tasks, repos) - Create FileDetail component to view individual files - Configure routes: - /contracts - list all contracts - /contracts/:id - view contract details with Files tab - /contracts/:contractId/files/:fileId - view file in contract context - Remove standalone file routes (/files, /files/:id) Files are now only accessible through their parent contract. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * Task completion checkpoint * feat: Add contract-scoped file route /contracts/:id/files/:fileId - Create ContractFilePage component for viewing files within contract context - Add route for /contracts/:id/files/:fileId in main.tsx - Update handleFileSelect in contracts.tsx to navigate to contract-scoped file URL - File viewer now has "Back to Contract" navigation instead of standalone /files This allows files accessed from a contract to maintain context and return to the contract page when going back. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/contract-file.tsx659
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
3 files changed, 669 insertions, 1 deletions
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 0464495..383b732 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -17,6 +17,7 @@ import MeshPage from "./routes/mesh";
import HistoryPage from "./routes/history";
import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
+import ContractFilePage from "./routes/contract-file";
import TemplatesPage from "./routes/templates";
createRoot(document.getElementById("root")!).render(
@@ -71,6 +72,14 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
+ path="/contracts/:id/files/:fileId"
+ element={
+ <ProtectedRoute>
+ <ContractFilePage />
+ </ProtectedRoute>
+ }
+ />
+ <Route
path="/workflow"
element={
<ProtectedRoute>
diff --git a/makima/frontend/src/routes/contract-file.tsx b/makima/frontend/src/routes/contract-file.tsx
new file mode 100644
index 0000000..9ed25ed
--- /dev/null
+++ b/makima/frontend/src/routes/contract-file.tsx
@@ -0,0 +1,659 @@
+import { useEffect, useState, useCallback, useRef } from "react";
+import { useParams, useNavigate } from "react-router";
+import { useAuth } from "../contexts/AuthContext";
+import { Masthead } from "../components/Masthead";
+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 } from "../lib/api";
+
+/**
+ * ContractFilePage - Wrapper for viewing files within a contract context
+ *
+ * This component handles the /contracts/:contractId/files/:fileId route,
+ * providing navigation back to the contract and rendering the file detail view.
+ */
+export default function ContractFilePage() {
+ const { id: contractId, fileId } = useParams<{ id: string; fileId: string }>();
+ 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;
+ }
+
+ // Render the file page with contract context
+ return <ContractAwareFilesPage contractId={contractId} fileId={fileId} />;
+}
+
+// A version of the files page aware of contract context
+function ContractAwareFilesPage({
+ contractId,
+ fileId,
+}: {
+ contractId?: string;
+ fileId?: string;
+}) {
+ const navigate = useNavigate();
+ const { error, conflict, clearConflict, fetchFile, editFile, removeFile } = useFiles();
+ const [fileDetail, setFileDetail] = useState<FileDetailType | null>(null);
+ const [detailLoading, setDetailLoading] = 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 pendingUpdateRef = useRef(false);
+ const lastSentVersionRef = useRef<number | null>(null);
+ const lastSavedVersionRef = useRef<number | null>(null);
+ const hasLocalChangesRef = useRef(false);
+ const isActivelyEditingRef = useRef(false);
+ const currentVersionRef = useRef<number | null>(null);
+
+ // Handle back navigation - go to contract detail instead of /files
+ const handleBack = useCallback(() => {
+ if (contractId) {
+ navigate(`/contracts/${contractId}`);
+ } else {
+ navigate("/contracts");
+ }
+ }, [contractId, navigate]);
+
+ 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: fileId || null,
+ currentVersion: fileDetail?.version || 0,
+ });
+
+ const handleRestoreVersion = useCallback(
+ async (targetVersion: number) => {
+ const result = await restoreToVersion(targetVersion);
+ if (result) {
+ currentVersionRef.current = result.version;
+ setFileDetail(result);
+ updateHasLocalChanges(false);
+ fetchVersions();
+ }
+ },
+ [restoreToVersion, fetchVersions, updateHasLocalChanges]
+ );
+
+ // Load file detail when fileId is provided
+ useEffect(() => {
+ if (fileId) {
+ setDetailLoading(true);
+ updateHasLocalChanges(false);
+ pendingUpdateRef.current = false;
+ lastSentVersionRef.current = null;
+ lastSavedVersionRef.current = null;
+ currentVersionRef.current = null;
+ setRemoteUpdate(null);
+ setRemoteFileData(null);
+ setFocusedElement(null);
+ fetchFile(fileId).then((detail) => {
+ if (detail) {
+ currentVersionRef.current = detail.version;
+ }
+ setFileDetail(detail);
+ setDetailLoading(false);
+ });
+ } else {
+ setFileDetail(null);
+ currentVersionRef.current = null;
+ updateHasLocalChanges(false);
+ }
+ }, [fileId, fetchFile, updateHasLocalChanges]);
+
+ // Handle file update events from WebSocket
+ const handleFileUpdate = useCallback(
+ async (event: FileUpdateEvent) => {
+ if (lastSavedVersionRef.current !== null && event.version === lastSavedVersionRef.current) {
+ lastSavedVersionRef.current = null;
+ return;
+ }
+
+ if (pendingUpdateRef.current) {
+ if (lastSentVersionRef.current !== null) {
+ const expectedNewVersion = lastSentVersionRef.current + 1;
+ if (event.version === expectedNewVersion) {
+ pendingUpdateRef.current = false;
+ lastSentVersionRef.current = null;
+ return;
+ }
+ }
+ return;
+ }
+
+ if (currentVersionRef.current !== null && event.version === currentVersionRef.current) {
+ return;
+ }
+
+ if (!hasLocalChangesRef.current && !isActivelyEditingRef.current) {
+ const detail = await fetchFile(event.fileId);
+ if (detail) {
+ currentVersionRef.current = detail.version;
+ }
+ setFileDetail(detail);
+ } else {
+ const remoteData = await fetchFile(event.fileId);
+ setRemoteFileData(remoteData);
+ setRemoteUpdate(event);
+ }
+ },
+ [fetchFile]
+ );
+
+ useFileSubscription({
+ fileId: fileId || null,
+ onUpdate: handleFileUpdate,
+ });
+
+ const handleDelete = useCallback(
+ async (id: string) => {
+ if (confirm("Are you sure you want to delete this file?")) {
+ const success = await removeFile(id);
+ if (success && fileId === id) {
+ handleBack();
+ }
+ }
+ },
+ [removeFile, fileId, handleBack]
+ );
+
+ const handleSave = useCallback(
+ async (id: string, name: string, description: string) => {
+ if (!fileDetail) return;
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(id, { name, description, version: fileDetail.version });
+ if (result) {
+ 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 && fileId) {
+ const newBody = [...fileDetail.body];
+ newBody[index] = element;
+
+ setFileDetail({
+ ...fileDetail,
+ body: newBody,
+ });
+ updateHasLocalChanges(true);
+
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(fileId, { 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, fileId, editFile, updateHasLocalChanges]
+ );
+
+ const handleBodyReorder = useCallback(
+ async (fromIndex: number, toIndex: number) => {
+ if (fileDetail && fileId) {
+ const newBody = [...fileDetail.body];
+ const [movedElement] = newBody.splice(fromIndex, 1);
+ newBody.splice(toIndex, 0, movedElement);
+
+ setFileDetail({
+ ...fileDetail,
+ body: newBody,
+ });
+ updateHasLocalChanges(true);
+
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(fileId, { 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, fileId, editFile, updateHasLocalChanges]
+ );
+
+ const handleBodyElementDelete = useCallback(
+ async (index: number) => {
+ if (fileDetail && fileId) {
+ const newBody = fileDetail.body.filter((_, i) => i !== index);
+
+ setFileDetail({
+ ...fileDetail,
+ body: newBody,
+ });
+ updateHasLocalChanges(true);
+
+ if (focusedElement?.index === index) {
+ setFocusedElement(null);
+ } else if (focusedElement && focusedElement.index > index) {
+ setFocusedElement({
+ ...focusedElement,
+ index: focusedElement.index - 1,
+ });
+ }
+
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(fileId, { 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, fileId, editFile, updateHasLocalChanges, focusedElement]
+ );
+
+ const handleBodyElementDuplicate = useCallback(
+ async (index: number) => {
+ if (fileDetail && fileId) {
+ const elementToDuplicate = fileDetail.body[index];
+ if (!elementToDuplicate) return;
+
+ const newBody = [...fileDetail.body];
+ newBody.splice(index + 1, 0, { ...elementToDuplicate });
+
+ setFileDetail({
+ ...fileDetail,
+ body: newBody,
+ });
+ updateHasLocalChanges(true);
+
+ if (focusedElement && focusedElement.index > index) {
+ setFocusedElement({
+ ...focusedElement,
+ index: focusedElement.index + 1,
+ });
+ }
+
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(fileId, { 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, fileId, editFile, updateHasLocalChanges, focusedElement]
+ );
+
+ const handleFocusElement = useCallback((element: FocusedElement | null) => {
+ setFocusedElement(element);
+ }, []);
+
+ const handleClearFocus = useCallback(() => {
+ setFocusedElement(null);
+ }, []);
+
+ const handleConvertElement = useCallback(
+ async (index: number, toType: string) => {
+ if (!fileDetail || !fileId) return;
+
+ const element = fileDetail.body[index];
+ if (!element) return;
+
+ 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;
+ }
+
+ 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;
+ }
+
+ const newBody = [...fileDetail.body];
+ newBody[index] = newElement;
+
+ setFileDetail({ ...fileDetail, body: newBody });
+ updateHasLocalChanges(true);
+
+ if (focusedElement?.index === index) {
+ setFocusedElement({
+ index,
+ type: newElement.type,
+ preview: textContent.slice(0, 50) + (textContent.length > 50 ? "..." : ""),
+ });
+ }
+
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = fileDetail.version;
+ try {
+ const result = await editFile(fileId, { 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, fileId, editFile, updateHasLocalChanges, focusedElement]
+ );
+
+ const handleGenerateFromElement = useCallback(
+ (index: number, action: string) => {
+ if (!fileDetail) return;
+
+ const element = fileDetail.body[index];
+ if (!element) return;
+
+ 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";
+ }
+
+ setFocusedElement({
+ index,
+ type: element.type,
+ preview: preview + (preview.length >= 50 ? "..." : ""),
+ });
+
+ 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]
+ );
+
+ // Conflict resolution handlers
+ const handleConflictReload = useCallback(async () => {
+ if (fileId) {
+ clearConflict();
+ const detail = await fetchFile(fileId);
+ if (detail) {
+ currentVersionRef.current = detail.version;
+ }
+ setFileDetail(detail);
+ updateHasLocalChanges(false);
+ }
+ }, [fileId, clearConflict, fetchFile, updateHasLocalChanges]);
+
+ const handleConflictForceOverwrite = useCallback(async () => {
+ if (fileId && fileDetail) {
+ clearConflict();
+ const latest = await fetchFile(fileId);
+ if (latest) {
+ pendingUpdateRef.current = true;
+ lastSentVersionRef.current = latest.version;
+ try {
+ const result = await editFile(fileId, { body: fileDetail.body, version: latest.version });
+ if (result) {
+ lastSavedVersionRef.current = result.version;
+ currentVersionRef.current = result.version;
+ setFileDetail(result);
+ updateHasLocalChanges(false);
+ }
+ } finally {
+ pendingUpdateRef.current = false;
+ lastSentVersionRef.current = null;
+ }
+ }
+ }
+ }, [fileId, fileDetail, clearConflict, fetchFile, editFile, updateHasLocalChanges]);
+
+ const handleRemoteUpdateRefresh = useCallback(async () => {
+ if (fileId) {
+ const detail = await fetchFile(fileId);
+ if (detail) {
+ currentVersionRef.current = detail.version;
+ }
+ setFileDetail(detail);
+ setRemoteUpdate(null);
+ setRemoteFileData(null);
+ updateHasLocalChanges(false);
+ }
+ }, [fileId, 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>
+ )}
+
+ {fileId && 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}
+ 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={fileId}
+ onUpdate={handleBodyUpdate}
+ focusedElement={focusedElement}
+ onClearFocus={handleClearFocus}
+ suggestedPrompt={suggestedPrompt}
+ onClearSuggestedPrompt={() => setSuggestedPrompt(null)}
+ />
+ </div>
+ </div>
+ ) : fileId && detailLoading ? (
+ <div className="panel h-full flex items-center justify-center">
+ <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
+ </div>
+ ) : (
+ <div className="panel h-full flex items-center justify-center">
+ <div className="text-center">
+ <p className="font-mono text-sm text-[#555] mb-4">
+ File not found
+ </p>
+ <button
+ onClick={handleBack}
+ className="px-4 py-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
+ >
+ &larr; Back to Contract
+ </button>
+ </div>
+ </div>
+ )}
+ </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}
+ />
+ )}
+ </div>
+ );
+}
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 4974688..034501a 100644
--- a/makima/frontend/tsconfig.tsbuildinfo
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/autopilotpanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/autopilotpanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file