summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/document-directives.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-08 11:29:56 +0100
committerGitHub <noreply@github.com>2026-05-08 11:29:56 +0100
commite00be74c8b575c725829677aadeb755ee81454d0 (patch)
tree6804ba099978b32f94e3436bb373c2b0bd07b84d /makima/frontend/src/routes/document-directives.tsx
parentd7048aaef8ffa483c63a765d2d35ae01389e331f (diff)
downloadsoryu-e00be74c8b575c725829677aadeb755ee81454d0.tar.gz
soryu-e00be74c8b575c725829677aadeb755ee81454d0.zip
feat(directives): unified contracts surface — backbone (#128)
This is the backbone PR for the unified directive workflow. A directive holds a sequence of contracts; each contract is a spec body whose execution drives tasks in the directive's shared worktree. Lifecycle (Lock & Start, queue scheduler, drag-reorder) lands in follow-ups. What's in this PR: - Migration adds `position` (queue order) and `merge_mode` (shared|own_pr) columns to directive_documents. The actual table rename is deferred — the legacy `contracts` table from the old contracts system still exists, and the rename collision waits for Phase 5 to drop legacy contracts. - Repository: list orders by position; create assigns next-position; update accepts merge_mode; new reorder_directive_document_position shifts siblings inside a transaction. - HTTP: endpoints aliased under /api/v1/directives/{id}/contracts and /api/v1/contracts/{id}/... with a new /contracts/{id}/reorder. - Frontend: api types renamed `DirectiveContract*` (avoiding the legacy `Contract` type collision); document-directives.tsx imports via aliases so the rest of the file is untouched. Internal struct + table names stay `DirectiveDocument` / `directive_documents` until the legacy contracts cleanup. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/frontend/src/routes/document-directives.tsx')
-rw-r--r--makima/frontend/src/routes/document-directives.tsx56
1 files changed, 28 insertions, 28 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 1714aed..d589dac 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -7,16 +7,16 @@ import { DocumentEditor } from "../components/directives/DocumentEditor";
import {
type DirectiveSummary,
type DirectiveStatus,
- type DirectiveDocument,
- type DirectiveDocumentStatus,
+ type DirectiveContract as Contract,
+ type DirectiveContractStatus as ContractStatus,
type DirectiveStep,
type Task,
- type DocumentTasksResponse,
- listDirectiveDocuments,
- createDirectiveDocument,
- getDirectiveDocument,
- updateDirectiveDocument,
- listDirectiveDocumentTasks,
+ type DirectiveContractTasksResponse as ContractTasksResponse,
+ listDirectiveContracts as listContracts,
+ createDirectiveContract as createContract,
+ getDirectiveContract as getContract,
+ updateDirectiveContract as updateContract,
+ listDirectiveContractTasks as listContractTasks,
createDirectiveTask,
startDirective,
pauseDirective,
@@ -43,7 +43,7 @@ const STATUS_DOT: Record<DirectiveStatus, string> = {
// Per-document status palette. Active/draft documents use the same bright
// green-ish accent as a running directive; shipped/archived use a muted blue.
-const DOC_STATUS_DOT: Record<DirectiveDocumentStatus, string> = {
+const DOC_STATUS_DOT: Record<ContractStatus, string> = {
draft: "bg-[#556677]",
active: "bg-green-400",
shipped: "bg-[#75aafc]",
@@ -66,7 +66,7 @@ const DOC_STATUS_DOT: Record<DirectiveDocumentStatus, string> = {
// rather than just an id slice). Accepts either a DirectiveSummary or a full
// DirectiveWithSteps — only `title` is read.
function fileLabel(
- doc: DirectiveDocument,
+ doc: Contract,
directive: { title: string },
): string {
const docTitle = doc.title.trim();
@@ -172,7 +172,7 @@ interface DirectiveFolderProps {
/** Called when the user clicks the folder header itself (after toggle). */
onHeaderClick: () => void;
selection: SidebarSelection | null;
- onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void;
+ onSelectDocument: (directiveId: string, doc: Contract) => void;
onCreateDocument: (directive: DirectiveSummary) => Promise<void>;
/** Open the inline "+ New ephemeral task" form for this directive. */
onCreateEphemeralTask: (directive: DirectiveSummary) => void;
@@ -210,7 +210,7 @@ function DirectiveFolder({
// Documents fetched lazily on open. We deliberately scope the fetch to the
// open-state so closed folders don't pay the network cost on initial render.
- const [docs, setDocs] = useState<DirectiveDocument[] | null>(null);
+ const [docs, setDocs] = useState<Contract[] | null>(null);
const [docsLoading, setDocsLoading] = useState(false);
const [docsError, setDocsError] = useState<string | null>(null);
@@ -224,7 +224,7 @@ function DirectiveFolder({
setDocsLoading(true);
setDocsError(null);
try {
- const list = await listDirectiveDocuments(directive.id);
+ const list = await listContracts(directive.id);
setDocs(list);
} catch (e) {
setDocsError(e instanceof Error ? e.message : "Failed to load documents");
@@ -242,8 +242,8 @@ function DirectiveFolder({
// Split the documents into the two visual groups. Memoised so we don't
// recompute on every render.
const { activeDocs, shippedDocs } = useMemo(() => {
- const active: DirectiveDocument[] = [];
- const shipped: DirectiveDocument[] = [];
+ const active: Contract[] = [];
+ const shipped: Contract[] = [];
for (const d of docs ?? []) {
if (d.status === "shipped" || d.status === "archived") {
shipped.push(d);
@@ -447,7 +447,7 @@ function DirectiveFolder({
// =============================================================================
interface DocumentRowProps {
- doc: DirectiveDocument;
+ doc: Contract;
directive: DirectiveSummary;
selected: boolean;
onSelect: () => void;
@@ -547,7 +547,7 @@ function DocumentTasksFolder({
onSelectTask,
}: DocumentTasksFolderProps) {
const [open, setOpen] = useState(defaultOpen);
- const [data, setData] = useState<DocumentTasksResponse | null>(null);
+ const [data, setData] = useState<ContractTasksResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -561,7 +561,7 @@ function DocumentTasksFolder({
setLoading(true);
setError(null);
try {
- const res = await listDirectiveDocumentTasks(documentId);
+ const res = await listContractTasks(documentId);
setData(res);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load tasks");
@@ -770,7 +770,7 @@ interface SidebarProps {
directives: DirectiveSummary[];
loading: boolean;
selection: SidebarSelection | null;
- onSelectDocument: (directiveId: string, doc: DirectiveDocument) => void;
+ onSelectDocument: (directiveId: string, doc: Contract) => void;
onSelectDirective: (directiveId: string) => void;
onCreateDocument: (directive: DirectiveSummary) => Promise<void>;
onCreateContract: () => void;
@@ -894,8 +894,8 @@ function DocumentSidebar({
// =============================================================================
// Editor shell — wraps DocumentEditor and handles the "no document selected"
// and loading states. Two modes:
-// 1) documentId selected → fetch the DirectiveDocument and edit doc.body via
-// updateDirectiveDocument (the call that auto-reactivates a shipped doc).
+// 1) documentId selected → fetch the Contract and edit doc.body via
+// updateContract (the call that auto-reactivates a shipped doc).
// 2) no documentId (legacy fallback, kept for the "select a directive but
// not a document" transitional case) → edit directive.goal as before.
// =============================================================================
@@ -918,7 +918,7 @@ function EditorShell({
const documentId = selection?.documentId ?? null;
// We deliberately don't pull `updateGoal` here — in the multi-document
- // world, edits flow through updateDirectiveDocument (which auto-reactivates
+ // world, edits flow through updateContract (which auto-reactivates
// a shipped doc when its body changes). The legacy directive.goal is
// unused on this surface.
const {
@@ -932,7 +932,7 @@ function EditorShell({
// Document fetch — only when documentId is selected. Refetched whenever the
// id changes; not polled (the document stream is too low-traffic to warrant
// background refresh in this iteration).
- const [doc, setDoc] = useState<DirectiveDocument | null>(null);
+ const [doc, setDoc] = useState<Contract | null>(null);
const [docLoading, setDocLoading] = useState(false);
const [docError, setDocError] = useState<string | null>(null);
@@ -946,7 +946,7 @@ function EditorShell({
let cancelled = false;
setDocLoading(true);
setDocError(null);
- getDirectiveDocument(documentId)
+ getContract(documentId)
.then((d) => {
if (cancelled) return;
setDoc(d);
@@ -970,7 +970,7 @@ function EditorShell({
const onUpdateDocumentBody = useCallback(
async (body: string) => {
if (!documentId) return;
- const updated = await updateDirectiveDocument(documentId, { body });
+ const updated = await updateContract(documentId, { body });
setDoc(updated);
// Tell the sidebar to refetch the directive's document list so the
// status chip flips from `shipped` back to `active` (and any title
@@ -1230,7 +1230,7 @@ export default function DocumentDirectivesPage() {
lastResolvedRef.current = routeDirectiveId;
let cancelled = false;
- listDirectiveDocuments(routeDirectiveId)
+ listContracts(routeDirectiveId)
.then((list) => {
if (cancelled) return;
// Prefer the first 'active' doc; fall back to the first 'draft'.
@@ -1259,7 +1259,7 @@ export default function DocumentDirectivesPage() {
}, [routeDirectiveId, selection?.documentId, selection?.taskId, setSearchParams]);
const handleSelectDocument = useCallback(
- (directiveId: string, doc: DirectiveDocument) => {
+ (directiveId: string, doc: Contract) => {
navigate(`/directives/${directiveId}?document=${doc.id}`);
},
[navigate],
@@ -1278,7 +1278,7 @@ export default function DocumentDirectivesPage() {
const handleCreateDocument = useCallback(
async (directive: DirectiveSummary) => {
- const created = await createDirectiveDocument(directive.id, {
+ const created = await createContract(directive.id, {
title: "",
body: "",
});