summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/contract-file.tsx659
-rw-r--r--makima/frontend/src/routes/contracts.tsx885
-rw-r--r--makima/frontend/src/routes/document-directives.tsx16
-rw-r--r--makima/frontend/src/routes/tmp.tsx9
4 files changed, 13 insertions, 1556 deletions
diff --git a/makima/frontend/src/routes/contract-file.tsx b/makima/frontend/src/routes/contract-file.tsx
deleted file mode 100644
index 9ed25ed..0000000
--- a/makima/frontend/src/routes/contract-file.tsx
+++ /dev/null
@@ -1,659 +0,0 @@
-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/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
deleted file mode 100644
index ce9ceca..0000000
--- a/makima/frontend/src/routes/contracts.tsx
+++ /dev/null
@@ -1,885 +0,0 @@
-import { useState, useCallback, useEffect } from "react";
-import { useParams, useNavigate } from "react-router";
-import { Masthead } from "../components/Masthead";
-import { ContractList } from "../components/contracts/ContractList";
-import { ContractDetail } from "../components/contracts/ContractDetail";
-import { DirectoryInput } from "../components/mesh/DirectoryInput";
-import { useContracts } from "../hooks/useContracts";
-import { useAuth } from "../contexts/AuthContext";
-import {
- createTask,
- getDaemonDirectories,
- getRepositorySuggestions,
- listContractTypes,
-} from "../lib/api";
-import type {
- ContractWithRelations,
- ContractSummary,
- ContractPhase,
- ContractStatus,
- ContractType,
- CreateContractRequest,
- RepositorySourceType,
- DaemonDirectory,
- RepositoryHistoryEntry,
- ContractTypeTemplate,
-} from "../lib/api";
-
-export default function ContractsPage() {
- 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 <ContractsPageContent />;
-}
-
-function ContractsPageContent() {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
- const {
- contracts,
- loading,
- error,
- fetchContract,
- saveContract,
- editContract,
- removeContract,
- changePhase,
- addRemoteRepo,
- addLocalRepo,
- createManagedRepo,
- removeRepo,
- setRepoPrimary,
- } = useContracts();
-
- const [contractDetail, setContractDetail] = useState<ContractWithRelations | null>(null);
- const [detailLoading, setDetailLoading] = useState(false);
- const [isCreating, setIsCreating] = useState(false);
- const [newContractName, setNewContractName] = useState("");
- const [newContractDescription, setNewContractDescription] = useState("");
- const [contractType, setContractType] = useState<ContractType>("simple");
- const [initialPhase, setInitialPhase] = useState<ContractPhase>("plan");
- const [repoType, setRepoType] = useState<RepositorySourceType>("remote");
- const [repoName, setRepoName] = useState("");
- const [repoUrl, setRepoUrl] = useState("");
- const [repoPath, setRepoPath] = useState("");
- const [createError, setCreateError] = useState<string | null>(null);
- const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]);
- const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
- const [showRepoSuggestions, setShowRepoSuggestions] = useState(false);
- const [contractTypes, setContractTypes] = useState<ContractTypeTemplate[]>([]);
- const [contractTypesLoading, setContractTypesLoading] = useState(false);
- const [localOnly, setLocalOnly] = useState(false);
-
- // Fetch contract types when modal opens - API returns both built-in and custom templates
- useEffect(() => {
- if (isCreating) {
- setContractTypesLoading(true);
-
- listContractTypes()
- .then((res) => {
- setContractTypes(res.contractTypes);
- setContractTypesLoading(false);
- })
- .catch((err) => {
- console.error("Failed to fetch contract types:", err);
- // Fall back to built-in types
- const builtinTypes: ContractTypeTemplate[] = [
- {
- id: "simple",
- name: "Simple",
- description: "Plan \u2192 Execute: Simple workflow with a plan document",
- phases: ["plan", "execute"],
- defaultPhase: "plan",
- isBuiltin: true,
- },
- {
- id: "specification",
- name: "Specification",
- description: "Research \u2192 Specify \u2192 Plan \u2192 Execute \u2192 Review: Full specification-driven development with TDD",
- phases: ["research", "specify", "plan", "execute", "review"],
- defaultPhase: "research",
- isBuiltin: true,
- },
- {
- id: "execute",
- name: "Execute",
- description: "Execute only: Minimal workflow for immediate task execution",
- phases: ["execute"],
- defaultPhase: "execute",
- isBuiltin: true,
- },
- ];
- setContractTypes(builtinTypes);
- setContractTypesLoading(false);
- });
- }
- }, [isCreating]);
-
- // Fetch repository suggestions when modal opens and repo type changes
- useEffect(() => {
- if (isCreating && (repoType === "remote" || repoType === "local")) {
- getRepositorySuggestions(repoType, undefined, 10)
- .then((res) => {
- setRepoSuggestions(res.entries);
- setShowRepoSuggestions(res.entries.length > 0);
- })
- .catch(() => {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- });
- } else {
- setRepoSuggestions([]);
- setShowRepoSuggestions(false);
- }
- }, [isCreating, repoType]);
-
- // Apply a repository suggestion
- const applyRepoSuggestion = useCallback((suggestion: RepositoryHistoryEntry) => {
- setRepoName(suggestion.name);
- if (suggestion.repositoryUrl) {
- setRepoUrl(suggestion.repositoryUrl);
- }
- if (suggestion.localPath) {
- setRepoPath(suggestion.localPath);
- }
- setShowRepoSuggestions(false);
- }, []);
-
- // Fetch daemon directories when "local" repo type is selected
- useEffect(() => {
- if (repoType === "local" && isCreating) {
- getDaemonDirectories()
- .then((res) => setSuggestedDirectories(res.directories))
- .catch(() => setSuggestedDirectories([]));
- }
- }, [repoType, isCreating]);
-
- // Load contract detail when ID changes
- useEffect(() => {
- if (id) {
- setDetailLoading(true);
- fetchContract(id).then((contract) => {
- setContractDetail(contract);
- setDetailLoading(false);
- });
- } else {
- setContractDetail(null);
- }
- }, [id, fetchContract]);
-
- const handleSelect = useCallback(
- (contractId: string) => {
- navigate(`/contracts/${contractId}`);
- },
- [navigate]
- );
-
- const handleBack = useCallback(() => {
- navigate("/contracts");
- }, [navigate]);
-
- const handleCreate = useCallback(() => {
- setIsCreating(true);
- }, []);
-
- // Validate repository configuration
- const isRepoValid = useCallback(() => {
- if (!repoName.trim()) return false;
- if (repoType === "remote" && !repoUrl.trim()) return false;
- if (repoType === "local" && !repoPath.trim()) return false;
- return true;
- }, [repoType, repoName, repoUrl, repoPath]);
-
- const handleCreateSubmit = useCallback(async () => {
- if (!newContractName.trim()) return;
- if (!isRepoValid()) {
- setCreateError("Repository configuration is required");
- return;
- }
-
- setCreateError(null);
-
- // Get default phase from contract types or fall back to static function
- const selectedType = contractTypes.find((t) => t.id === contractType);
- const defaultPhaseForType = selectedType?.defaultPhase || (contractType === "simple" ? "plan" : "research");
- const isCustomTemplate = selectedType && !selectedType.isBuiltin;
-
- const data: CreateContractRequest = {
- name: newContractName.trim(),
- description: newContractDescription.trim() || undefined,
- // For custom templates, send templateId instead of contractType
- contractType: isCustomTemplate ? undefined : contractType,
- templateId: isCustomTemplate ? contractType : undefined,
- initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined,
- localOnly: localOnly || undefined,
- };
-
- try {
- const contract = await saveContract(data);
- if (contract) {
- // Add the repository after contract creation
- try {
- if (repoType === "remote") {
- await addRemoteRepo(contract.id, {
- name: repoName.trim(),
- repositoryUrl: repoUrl.trim(),
- isPrimary: true,
- });
- } else if (repoType === "local") {
- await addLocalRepo(contract.id, {
- name: repoName.trim(),
- localPath: repoPath.trim(),
- isPrimary: true,
- });
- } else if (repoType === "managed") {
- await createManagedRepo(contract.id, {
- name: repoName.trim(),
- isPrimary: true,
- });
- }
- } catch (repoError) {
- console.error("Failed to add repository:", repoError);
- // Still navigate to the contract - repo can be added later
- }
-
- // Clear form state
- setIsCreating(false);
- setNewContractName("");
- setNewContractDescription("");
- setContractType("simple");
- setInitialPhase("plan");
- setRepoType("remote");
- setRepoName("");
- setRepoUrl("");
- setRepoPath("");
- setLocalOnly(false);
- navigate(`/contracts/${contract.id}`);
- }
- } catch (err) {
- setCreateError(err instanceof Error ? err.message : "Failed to create contract");
- }
- }, [
- newContractName,
- newContractDescription,
- contractType,
- contractTypes,
- initialPhase,
- repoType,
- repoName,
- repoUrl,
- repoPath,
- isRepoValid,
- saveContract,
- addRemoteRepo,
- addLocalRepo,
- createManagedRepo,
- navigate,
- ]);
-
- const handleCreateCancel = useCallback(() => {
- setIsCreating(false);
- setNewContractName("");
- setNewContractDescription("");
- setContractType("simple");
- setInitialPhase("plan");
- setRepoType("remote");
- setRepoName("");
- setRepoUrl("");
- setRepoPath("");
- setLocalOnly(false);
- setCreateError(null);
- }, []);
-
- const handleUpdate = useCallback(
- async (name: string, description: string) => {
- if (contractDetail) {
- const updated = await editContract(contractDetail.id, {
- name,
- description: description || undefined,
- version: contractDetail.version,
- });
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, editContract, fetchContract]
- );
-
- const handleDelete = useCallback(async () => {
- if (contractDetail && confirm("Are you sure you want to delete this contract?")) {
- const success = await removeContract(contractDetail.id);
- if (success) {
- navigate("/contracts");
- }
- }
- }, [contractDetail, removeContract, navigate]);
-
- const handlePhaseChange = useCallback(
- async (phase: ContractPhase) => {
- if (contractDetail) {
- const updated = await changePhase(contractDetail.id, phase);
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, changePhase, fetchContract]
- );
-
- const handleStatusChange = useCallback(
- async (status: ContractStatus) => {
- if (contractDetail) {
- const updated = await editContract(contractDetail.id, {
- status,
- version: contractDetail.version,
- });
- if (updated) {
- // Refresh detail
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }
- },
- [contractDetail, editContract, fetchContract]
- );
-
- // Repository handlers
- const handleAddRemoteRepo = useCallback(
- async (name: string, url: string, isPrimary: boolean) => {
- if (contractDetail) {
- await addRemoteRepo(contractDetail.id, { name, repositoryUrl: url, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, addRemoteRepo, fetchContract]
- );
-
- const handleAddLocalRepo = useCallback(
- async (name: string, path: string, isPrimary: boolean) => {
- if (contractDetail) {
- await addLocalRepo(contractDetail.id, { name, localPath: path, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, addLocalRepo, fetchContract]
- );
-
- const handleCreateManagedRepo = useCallback(
- async (name: string, isPrimary: boolean) => {
- if (contractDetail) {
- await createManagedRepo(contractDetail.id, { name, isPrimary });
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, createManagedRepo, fetchContract]
- );
-
- const handleDeleteRepo = useCallback(
- async (repoId: string) => {
- if (contractDetail) {
- await removeRepo(contractDetail.id, repoId);
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, removeRepo, fetchContract]
- );
-
- const handleSetRepoPrimary = useCallback(
- async (repoId: string) => {
- if (contractDetail) {
- await setRepoPrimary(contractDetail.id, repoId);
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- },
- [contractDetail, setRepoPrimary, fetchContract]
- );
-
- // Refresh contract detail (used after file/task operations)
- const handleRefresh = useCallback(async () => {
- if (contractDetail) {
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- }
- }, [contractDetail, fetchContract]);
-
- // File/task navigation handlers
- const handleFileSelect = useCallback(
- (fileId: string) => {
- if (contractDetail) {
- navigate(`/contracts/${contractDetail.id}/files/${fileId}`);
- }
- },
- [navigate, contractDetail]
- );
-
- const handleTaskSelect = useCallback(
- (taskId: string) => {
- navigate(`/exec/${taskId}`);
- },
- [navigate]
- );
-
- // Create task within contract context
- const handleTaskCreate = useCallback(
- async (name: string, plan: string, repositoryUrl?: string) => {
- if (!contractDetail) return;
- try {
- // Create the task with contract_id (task is automatically associated)
- const task = await createTask({
- contractId: contractDetail.id,
- name,
- plan,
- repositoryUrl,
- });
- // Refresh contract detail to show new task
- const refreshed = await fetchContract(contractDetail.id);
- setContractDetail(refreshed);
- // Navigate to the new task
- navigate(`/exec/${task.id}`);
- } catch (e) {
- console.error("Failed to create task:", e);
- alert(e instanceof Error ? e.message : "Failed to create task");
- }
- },
- [contractDetail, fetchContract, navigate]
- );
-
- // Context menu handlers for ContractList
- const handleContextMarkComplete = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "completed", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextMarkActive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "active", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextArchive = useCallback(
- async (contract: ContractSummary) => {
- await editContract(contract.id, { status: "archived", version: contract.version });
- },
- [editContract]
- );
-
- const handleContextDelete = useCallback(
- async (contract: ContractSummary) => {
- if (confirm(`Are you sure you want to delete "${contract.name}"?`)) {
- const success = await removeContract(contract.id);
- if (success && contract.id === id) {
- navigate("/contracts");
- }
- }
- },
- [removeContract, id, navigate]
- );
-
- const handleContextGoToSupervisor = useCallback(
- (contract: ContractSummary) => {
- if (contract.supervisorTaskId) {
- navigate(`/exec/${contract.supervisorTaskId}`);
- }
- },
- [navigate]
- );
-
- return (
- <div className="relative z-10 h-screen flex flex-col overflow-hidden bg-[#0a1628]">
- <Masthead showNav />
- <main className="flex-1 flex overflow-hidden" style={{ height: "calc(100vh - 80px)" }}>
- {/* Left: Contract list */}
- <div className="w-[350px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col">
- <ContractList
- contracts={contracts}
- loading={loading}
- onSelect={handleSelect}
- onCreate={handleCreate}
- selectedId={id}
- onMarkComplete={handleContextMarkComplete}
- onMarkActive={handleContextMarkActive}
- onArchive={handleContextArchive}
- onDelete={handleContextDelete}
- onGoToSupervisor={handleContextGoToSupervisor}
- />
- </div>
-
- {/* Right: Detail or Create */}
- <div className="flex-1 overflow-hidden flex flex-col min-h-0">
- {error && (
- <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
- {error}
- </div>
- )}
-
- {/* Contract detail, creation form, or empty state */}
- <div className="flex-1 min-h-0 overflow-hidden">
- {isCreating ? (
- <div className="p-4 max-w-lg overflow-y-auto h-full bg-[#0a1628]">
- <h3 className="font-mono text-[10px] text-[#9bc3ff] uppercase tracking-wide mb-4">
- Create Contract
- </h3>
-
- {createError && (
- <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
- {createError}
- </div>
- )}
-
- <div className="space-y-4">
- {/* Contract name */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Contract Name
- </label>
- <input
- type="text"
- value={newContractName}
- onChange={(e) => setNewContractName(e.target.value)}
- placeholder="Contract name"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- autoFocus
- />
- </div>
-
- {/* Description */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Description (optional)
- </label>
- <textarea
- value={newContractDescription}
- onChange={(e) => setNewContractDescription(e.target.value)}
- placeholder="Describe what this contract is for..."
- rows={2}
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc] resize-none"
- />
- </div>
-
- {/* Contract Type */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Contract Type
- </label>
- {contractTypesLoading ? (
- <div className="flex items-center justify-center py-4">
- <span className="font-mono text-xs text-[#8b949e]">Loading contract types...</span>
- </div>
- ) : (
- <>
- <div className="flex gap-2">
- {contractTypes.map((type) => (
- <button
- key={type.id}
- type="button"
- onClick={() => {
- setContractType(type.id as ContractType);
- setInitialPhase(type.defaultPhase as ContractPhase);
- }}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- contractType === type.id
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- {type.name}
- </button>
- ))}
- </div>
- <p className="mt-1 font-mono text-xs text-[#8b949e]">
- {contractTypes.find((t) => t.id === contractType)?.description ||
- "Select a contract type"}
- </p>
- </>
- )}
- </div>
-
- {/* Starting Phase */}
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Starting Phase
- </label>
- <select
- value={initialPhase}
- onChange={(e) => setInitialPhase(e.target.value as ContractPhase)}
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- >
- {(() => {
- const template = contractTypes.find((t) => t.id === contractType);
- return (template?.phases || []).map((phase) => {
- const displayName = template?.phaseNames?.[phase] || (phase.charAt(0).toUpperCase() + phase.slice(1));
- return (
- <option key={phase} value={phase}>
- {displayName}
- </option>
- );
- });
- })()}
- </select>
- <p className="mt-1 font-mono text-xs text-[#8b949e]">
- {contractType === "simple"
- ? "Start in Plan to define what to build, or Execute if already planned"
- : "Skip earlier phases if you already have requirements defined"}
- </p>
- </div>
-
- {/* Local-Only Mode */}
- <div className="space-y-2">
- <div className="flex items-center space-x-3">
- <button
- type="button"
- onClick={() => setLocalOnly(!localOnly)}
- className={`w-5 h-5 flex items-center justify-center border transition-colors ${
- localOnly
- ? "bg-[#0f3c78] border-[#75aafc] text-[#dbe7ff]"
- : "bg-[#0d1b2d] border-[rgba(117,170,252,0.2)] text-transparent"
- }`}
- >
- {localOnly && (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="3"
- strokeLinecap="round"
- strokeLinejoin="round"
- className="w-3 h-3"
- >
- <polyline points="20 6 9 17 4 12" />
- </svg>
- )}
- </button>
- <label
- className="font-mono text-sm text-[#dbe7ff] cursor-pointer select-none"
- onClick={() => setLocalOnly(!localOnly)}
- >
- Local-Only Mode
- </label>
- </div>
- <p className="font-mono text-xs text-[#8b949e] pl-8">
- When enabled, tasks won't automatically push to remote or create PRs.
- Use patch files to export changes.
- </p>
- </div>
-
- {/* Repository Configuration */}
- <div className="border-t border-[rgba(117,170,252,0.2)] pt-4">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-3">
- Repository Configuration (Required)
- </label>
-
- {/* Repository type selector */}
- <div className="flex gap-2 mb-3">
- <button
- type="button"
- onClick={() => setRepoType("remote")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "remote"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Remote
- </button>
- <button
- type="button"
- onClick={() => setRepoType("local")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "local"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Local
- </button>
- <button
- type="button"
- onClick={() => setRepoType("managed")}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- repoType === "managed"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[rgba(117,170,252,0.2)] hover:border-[#75aafc]"
- }`}
- >
- Managed
- </button>
- </div>
-
- {/* Repository suggestions */}
- {showRepoSuggestions && repoSuggestions.length > 0 && (
- <div className="mb-3">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Recent Repositories
- </label>
- <div className="border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto">
- {repoSuggestions.map((suggestion) => (
- <button
- key={suggestion.id}
- type="button"
- onClick={() => applyRepoSuggestion(suggestion)}
- className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
- >
- <div className="flex items-center justify-between">
- <span className="text-[#9bc3ff] truncate">{suggestion.name}</span>
- <span className="text-[10px] text-[#556677] ml-2">
- {suggestion.useCount}×
- </span>
- </div>
- <div className="text-[10px] text-[#556677] truncate">
- {repoType === "local" ? suggestion.localPath : suggestion.repositoryUrl}
- </div>
- </button>
- ))}
- </div>
- </div>
- )}
-
- {/* Repository name */}
- <div className="mb-3">
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Repository Name
- </label>
- <input
- type="text"
- value={repoName}
- onChange={(e) => setRepoName(e.target.value)}
- placeholder="e.g., my-project"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- />
- </div>
-
- {/* Repository URL (for remote) */}
- {repoType === "remote" && (
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Repository URL
- </label>
- <input
- type="text"
- value={repoUrl}
- onChange={(e) => setRepoUrl(e.target.value)}
- placeholder="https://github.com/user/repo.git"
- className="w-full px-3 py-2 bg-[#0d1b2d] border border-[rgba(117,170,252,0.2)] text-[12px] font-mono text-white focus:outline-none focus:border-[#75aafc]"
- />
- </div>
- )}
-
- {/* Repository path (for local) */}
- {repoType === "local" && (
- <div>
- <label className="block text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide mb-1">
- Local Path
- </label>
- <DirectoryInput
- value={repoPath}
- onChange={setRepoPath}
- suggestions={suggestedDirectories}
- placeholder="/path/to/repository"
- />
- </div>
- )}
-
- {/* Managed description */}
- {repoType === "managed" && (
- <p className="font-mono text-xs text-[#8b949e]">
- A managed repository will be created automatically by the daemon.
- </p>
- )}
- </div>
-
- {/* Actions */}
- <div className="flex gap-2 justify-end pt-2">
- <button
- onClick={handleCreateCancel}
- className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
- >
- Cancel
- </button>
- <button
- onClick={handleCreateSubmit}
- disabled={!newContractName.trim() || !isRepoValid()}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[rgba(117,170,252,0.2)] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Create
- </button>
- </div>
- </div>
- </div>
- ) : contractDetail ? (
- <ContractDetail
- contract={contractDetail}
- loading={detailLoading}
- onBack={handleBack}
- onUpdate={handleUpdate}
- onDelete={handleDelete}
- onPhaseChange={handlePhaseChange}
- onStatusChange={handleStatusChange}
- onFileSelect={handleFileSelect}
- onTaskSelect={handleTaskSelect}
- onTaskCreate={handleTaskCreate}
- onRefresh={handleRefresh}
- onAddRemoteRepo={handleAddRemoteRepo}
- onAddLocalRepo={handleAddLocalRepo}
- onCreateManagedRepo={handleCreateManagedRepo}
- onDeleteRepo={handleDeleteRepo}
- onSetRepoPrimary={handleSetRepoPrimary}
- />
- ) : (
- <div className="panel h-full flex items-center justify-center">
- <div className="text-center">
- <p className="font-mono text-sm text-[#555] mb-4">
- Select a contract or create a new one
- </p>
- <button
- onClick={handleCreate}
- className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase"
- >
- + New Contract
- </button>
- </div>
- </div>
- )}
- </div>
- </div>
- </main>
- </div>
- );
-}
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 7b0a89b..a3ea969 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -1530,13 +1530,17 @@ export default function DocumentDirectivesPage() {
: null;
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ // h-screen + overflow-hidden so the page itself never scrolls; the
+ // sidebar and editor pane each manage their own scroll via flex-1
+ // children with overflow-y-auto. Previously we set
+ // height: calc(100vh - 80px) on <main>, which assumed an 80px masthead
+ // and quietly clipped content when the masthead was taller (or pushed
+ // the page below the viewport on shorter screens, which made the
+ // whole page scroll instead of the sidebar/editor independently).
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
- <main
- className="flex-1 flex overflow-hidden"
- style={{ height: "calc(100vh - 80px)" }}
- >
- {/* Left: file-tree sidebar */}
+ <main className="flex-1 flex min-h-0 overflow-hidden">
+ {/* Left: file-tree sidebar — independent scroll. */}
<div className="w-[260px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
<DocumentSidebar
directives={directives}
diff --git a/makima/frontend/src/routes/tmp.tsx b/makima/frontend/src/routes/tmp.tsx
index 69f13a2..c0c7365 100644
--- a/makima/frontend/src/routes/tmp.tsx
+++ b/makima/frontend/src/routes/tmp.tsx
@@ -53,7 +53,7 @@ export default function TmpTaskPage() {
if (authLoading) {
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
<main className="flex-1 flex items-center justify-center">
<p className="text-[#7788aa] font-mono text-sm">Loading...</p>
@@ -63,12 +63,9 @@ export default function TmpTaskPage() {
}
return (
- <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
+ <div className="relative z-10 h-screen flex flex-col bg-[#0a1628] overflow-hidden">
<Masthead showNav />
- <main
- className="flex-1 flex flex-col overflow-hidden"
- style={{ height: "calc(100vh - 80px)" }}
- >
+ <main className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Breadcrumb echoing the document-mode header style. */}
<div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
<div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">