From 08d2ea6a685e248146b30f70421b853ff404b9ad Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 2 May 2026 16:11:05 +0100 Subject: feat(doc-mode): + New contract sidebar header + + New ephemeral task per folder (#121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unified surface didn't expose any way to create a new contract or a new ephemeral task — the only paths were `/directives/?document=...` or right-click context menus that no longer existed in the documents-per- directive sidebar restructure. Two new affordances: * **Sidebar header gains a "+ New" button** that opens NewContractModal (title + goal + optional repository_url). On submit calls `useDirectives.create` and navigates the user into the new directive. Header label updated from "Documents" to "Contracts" so the button's intent reads naturally. * **Each open directive folder gets a "+ New ephemeral task" button** alongside the existing "+ New document" affordance. Opens NewEphemeralTaskModal (name + plan), calls the existing `createDirectiveTask` API, navigates the user into the new task's transcript at `?task=`. NewContractModal and NewEphemeralTaskModal are local to the page — mirror the styling of the older NewTaskModal that lived here pre- restructure. ⌘/Ctrl+Enter submits. Co-authored-by: Claude Opus 4.7 (1M context) --- makima/frontend/src/routes/document-directives.tsx | 335 ++++++++++++++++++++- 1 file changed, 330 insertions(+), 5 deletions(-) (limited to 'makima/frontend/src') diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index b583bef..427122c 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -17,6 +17,7 @@ import { getDirectiveDocument, updateDirectiveDocument, listDirectiveDocumentTasks, + createDirectiveTask, } from "../lib/api"; // Status dot color, matching the existing tabular UI's badge palette so the @@ -174,6 +175,8 @@ interface DirectiveFolderProps { selection: SidebarSelection | null; onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void; onCreateDocument: (directive: DirectiveSummary) => Promise; + /** Open the inline "+ New ephemeral task" form for this directive. */ + onCreateEphemeralTask: (directive: DirectiveSummary) => void; /** * Document refresh trigger — bumped externally so the folder refetches its * document list after a create/update happens elsewhere. Primarily used so @@ -190,6 +193,7 @@ function DirectiveFolder({ selection, onSelectDocument, onCreateDocument, + onCreateEphemeralTask, refreshNonce, }: DirectiveFolderProps) { const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft; @@ -326,6 +330,19 @@ function DirectiveFolder({ New document + {/* + New ephemeral task — sibling affordance for spawning a + one-off task under this directive that's NOT part of the + DAG. Useful for sidebar scratch work, debugging, etc. */} + + {activeDocs.length === 0 && !docsLoading && (
no active documents @@ -668,6 +685,8 @@ interface SidebarProps { onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void; onSelectDirective: (directiveId: string) => void; onCreateDocument: (directive: DirectiveSummary) => Promise; + onCreateContract: () => void; + onCreateEphemeralTask: (directive: DirectiveSummary) => void; refreshNonce: number; } @@ -678,6 +697,8 @@ function DocumentSidebar({ onSelectDocument, onSelectDirective, onCreateDocument, + onCreateContract, + onCreateEphemeralTask, refreshNonce, }: SidebarProps) { const groups: Record = useMemo(() => { @@ -725,13 +746,23 @@ function DocumentSidebar({ return (
{/* Sidebar header */} -
+
- Documents - - - {directives.length} + Contracts +
+ + {directives.length} + + +
{/* Top-level "directives/" folder */} @@ -784,6 +815,7 @@ function DocumentSidebar({ selection={selection} onSelectDocument={onSelectDocument} onCreateDocument={onCreateDocument} + onCreateEphemeralTask={onCreateEphemeralTask} refreshNonce={refreshNonce} /> ))} @@ -1152,6 +1184,38 @@ export default function DocumentDirectivesPage() { [bumpRefresh, navigate], ); + // Modal state for the two new creation surfaces in the sidebar: + // * + New contract → opens NewContractModal, calls useDirectives.create + // * + New ephemeral task (per directive) → opens NewEphemeralTaskModal + const { create: createDirective } = useDirectives(); + const [showNewContract, setShowNewContract] = useState(false); + const [newEphemeralFor, setNewEphemeralFor] = useState(null); + + const handleSubmitNewContract = useCallback( + async (title: string, goal: string, repositoryUrl: string) => { + const d = await createDirective({ + title, + goal, + repositoryUrl: repositoryUrl.length > 0 ? repositoryUrl : undefined, + }); + setShowNewContract(false); + navigate(`/directives/${d.id}`); + }, + [createDirective, navigate], + ); + + const handleSubmitNewEphemeral = useCallback( + async (name: string, plan: string) => { + if (!newEphemeralFor) return; + const task = await createDirectiveTask(newEphemeralFor.id, { name, plan }); + const target = newEphemeralFor.id; + setNewEphemeralFor(null); + bumpRefresh(); + navigate(`/directives/${target}?task=${task.id}`); + }, + [newEphemeralFor, bumpRefresh, navigate], + ); + if (authLoading) { return (
@@ -1183,6 +1247,8 @@ export default function DocumentDirectivesPage() { onSelectDocument={handleSelectDocument} onSelectDirective={handleSelectDirective} onCreateDocument={handleCreateDocument} + onCreateContract={() => setShowNewContract(true)} + onCreateEphemeralTask={(d) => setNewEphemeralFor(d)} refreshNonce={refreshNonce} />
@@ -1195,6 +1261,265 @@ export default function DocumentDirectivesPage() { onDocumentChanged={bumpRefresh} /> + + {showNewContract && ( + setShowNewContract(false)} + onSubmit={handleSubmitNewContract} + /> + )} + {newEphemeralFor && ( + setNewEphemeralFor(null)} + onSubmit={handleSubmitNewEphemeral} + /> + )} +
+ ); +} + +/** + * Modal for creating a new directive (= "contract" in the doc-mode UI). + * Title + goal are required; repository_url is optional. On submit calls + * useDirectives.create and navigates the user into the new directive. + */ +function NewContractModal({ + onClose, + onSubmit, +}: { + onClose: () => void; + onSubmit: (title: string, goal: string, repositoryUrl: string) => Promise; +}) { + const [title, setTitle] = useState(""); + const [goal, setGoal] = useState(""); + const [repo, setRepo] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [err, setErr] = useState(null); + const titleRef = useRef(null); + + useEffect(() => { + titleRef.current?.focus(); + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [onClose]); + + const submit = async (e: React.FormEvent) => { + e.preventDefault(); + const t = title.trim(); + const g = goal.trim(); + if (!t || !g || submitting) return; + setSubmitting(true); + setErr(null); + try { + await onSubmit(t, g, repo.trim()); + } catch (caught) { + setErr(caught instanceof Error ? caught.message : String(caught)); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
e.stopPropagation()} + className="w-[520px] max-w-[90vw] bg-[#0a1628] border border-[rgba(117,170,252,0.4)] shadow-xl flex flex-col" + > +
+

+ New contract +

+
+
+
+ + setTitle(e.target.value)} + placeholder="e.g. Migrate auth to Supabase" + className="w-full bg-transparent border border-[rgba(117,170,252,0.2)] focus:border-[#75aafc] outline-none px-3 py-2 text-[13px] text-[#dbe7ff] placeholder-[#445566]" + /> +
+
+ +