From 80085c7cfa9d679ed3e3fd54a7d55fa8ab1addef Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 1 May 2026 18:06:38 +0100 Subject: feat(doc-mode): unified surface — ephemeral tasks, tmp/, /exec redirect, palette, SWR (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phases 1-3 of the unified-surface plan, bundled per the user's request. The directive document/folder UI is now the canonical place to interact with makima; legacy /exec is subsumed by routing redirects, /contracts is already hidden in nav, and orphan tasks surface under a top-level tmp/ pseudo-folder. Phase 4 (porting contract-only features) was confirmed out of scope; Phase 5 (deleting the contracts code) is a follow-up. ## Backend - New migration `20260501000000_archive_existing_contracts.sql` flips every legacy contract to `archived` so /contracts is read-only history. - New endpoint `POST /api/v1/directives/{id}/tasks` creates an ephemeral task — `directive_id` set, `directive_step_id` NULL, repo/branch inherited from the directive. Reuses `create_task_for_owner`. - New endpoint `GET /api/v1/directives/{id}/tasks` lists ephemeral tasks attached to a directive (drives the per-folder ephemeral group). - `GET /api/v1/mesh/tasks?orphan=true` returns top-level tasks with no `directive_id` AND no `parent_task_id` — backs the sidebar's tmp/. - New repo helpers `list_ephemeral_directive_tasks_for_owner` and `list_orphan_tasks_for_owner`. - The existing `mesh_merge` endpoints are reused as-is for ephemeral task merge (no new merge logic needed). ## Frontend ### Sticky composer + auto-scroll fix (`DocumentTaskStream.tsx`) - Sticky comment composer pinned to viewport bottom; padding compensates so the last entry isn't hidden behind it. - `autoScroll` now resumes when the user scrolls back within 80px of the bottom (previously stuck off forever after a single scroll-up). - Floating "↓ Jump to latest" chip when the user has scrolled away. - Action header strip: explicit Stop / Send / Open-in-task-page + conditional "Merge to base ↗" button on ephemeral terminal tasks. - Module-level cache of historical entries by taskId so re-selecting a task you've viewed renders instantly while a fresh fetch runs. ### Sidebar (`document-directives.tsx`) - Top-level `tmp/` folder: orphan tasks, polled every 5s. - Per-directive `tasks/` subfolder now also surfaces ephemeral tasks (lazily fetched on folder open) with a distinct asterisk-on-terminal icon (`EphemeralTaskIcon`). - Inline hover-action chips on each directive folder header: Start / Pause / PR / +New task. Right-click menu still works as a power-user fallback. - "Now executing" amber strip in the editor pane: surfaces the live orchestrator/completion/running-step task with a one-click jump. - Inline `+ New task` modal (name + plan); on submit calls `createDirectiveTask` and navigates into the freshly-spawned task. - New `EphemeralAwareTaskStream` wrapper passes `ephemeral` and `status` to `DocumentTaskStream` so the merge button only shows when the selected task is genuinely an ephemeral spinoff in a terminal state. Step-spawned tasks merge via the directive's PR completion. ### SWR cache (`useDirectives.ts`) - Module-level `listCache` and per-id `detailCache` (mirrors the pattern in `useUserSettings.ts`). Mounting the hook renders the cache value immediately if present and kicks a background refresh; subscribers see the new value when it lands. Cuts perceived navigation latency to near-zero on warm cache hits. ### QuickSwitcher (`QuickSwitcher.tsx`, new) - IntelliJ-style double-Shift command palette mounted at app root. - Listens at the document level for two `Shift` keydowns within 300ms with no other key in between; ignores while focus is in an input/textarea so capitalising letters doesn't pop the palette. - Searches across directives + their tasks (orchestrator/completion/ steps/ephemerals) + orphan tmp tasks. Fuzzy matches on title. - Eagerly loads task details for the first 20 directives on open so searches don't block on per-directive fetches. ### Routing (`main.tsx` + `exec-redirect.tsx` + `tmp.tsx`) - New `ExecRedirect` wrapper at `/exec/:id`: when documentMode is on AND the task has a `directiveId`, replaces the URL with `/directives/?task=`. Otherwise renders the legacy `MeshPage` as before. - New `/tmp/:taskId` route renders `DocumentTaskStream` standalone for orphan tasks, with the masthead and a `tmp / ` breadcrumb. Co-authored-by: Claude Opus 4.7 (1M context) --- makima/frontend/src/lib/api.ts | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'makima/frontend/src/lib/api.ts') diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 3fcd728..4d664cf 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3509,6 +3509,65 @@ export async function newDirectiveDraft( return res.json(); } +/** + * Request body for spawning an ephemeral task under a directive. The task is + * not part of the DAG — it lives alongside the directive's planned steps but + * the orchestrator ignores it. + */ +export interface CreateDirectiveTaskRequest { + name: string; + plan: string; + /** Override the directive's repository_url; defaults to the directive's. */ + repositoryUrl?: string; + /** Override the directive's base_branch; defaults to the directive's. */ + baseBranch?: string; +} + +/** + * List ephemeral tasks (directive_id set, directive_step_id NULL) attached + * to a directive. Backs the "spinoff" group inside each directive folder + * in the document-mode sidebar. + */ +export async function listDirectiveEphemeralTasks( + directiveId: string, +): Promise { + const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/tasks`); + if (!res.ok) { + throw new Error(`Failed to list directive tasks: ${res.statusText}`); + } + return res.json(); +} + +export async function createDirectiveTask( + directiveId: string, + req: CreateDirectiveTaskRequest, +): Promise { + const res = await authFetch( + `${API_BASE}/api/v1/directives/${directiveId}/tasks`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(req), + }, + ); + if (!res.ok) { + throw new Error(`Failed to create directive task: ${res.statusText}`); + } + return res.json(); +} + +/** + * List tasks not attached to any directive (and not subtasks). Backs the + * `tmp/` pseudo-folder in the document-mode sidebar. + */ +export async function listOrphanTasks(): Promise { + const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks?orphan=true`); + if (!res.ok) { + throw new Error(`Failed to list orphan tasks: ${res.statusText}`); + } + return res.json(); +} + export async function createDirectivePR(id: string): Promise { const res = await authFetch(`${API_BASE}/api/v1/directives/${id}/create-pr`, { method: "POST" }); if (!res.ok) throw new Error(`Failed to create PR: ${res.statusText}`); -- cgit v1.2.3