From 2dafe938f41edbb8ceb7c6a3655c9533bb50e47d Mon Sep 17 00:00:00 2001
From: soryu <soryu@soryu.co>
Date: Thu, 30 Apr 2026 15:48:26 +0100
Subject: fix(doc-mode): autosave robustness, draft→active flip, save-now,
 sidebar context menus (#108)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Stage 1 of the planned doc-mode revamp — bug fixes + UX polish ahead of the
larger contract-revisioning architecture work.

## Backend: 'draft' included in goal-update status flip

repository::update_directive_goal previously flipped only idle/paused → active
on a goal save, leaving 'draft' alone. That meant brand-new directives got
their goal persisted on save but never spawned a planner — exactly the
"orchestrator never runs" report. Extended the CASE clause so 'draft' also
flips to 'active' on save. The status remains visible to users; this just
makes the implicit "first goal save = start" behaviour work end-to-end.

## Autosave robustness (DocumentEditor.tsx)

The synchronous-write fix from the previous PR was correct in principle but
not visible enough for users to confirm it was working, and could still drop
the very last edit on an abrupt tab close. This change:

- Adds beforeunload / pagehide / visibilitychange handlers that synchronously
  flush pendingGoalRef → localStorage (skipping if it matches the persisted
  value). Backed by a persistedGoalRef that tracks directive.goal in real
  time so the handler doesn't capture a stale closure.
- Tracks the timestamp of every successful draft write (draftSavedAt) and
  surfaces it as a "Draft saved Ns ago" stamp in the bar — refreshed on a
  1Hz ticker so users can SEE the autosave is alive.
- Logs a console.warn on localStorage write failure (was silently swallowed)
  so quota / storage-disabled environments are diagnosable.

## Always-visible save bar + Save now button

The bar now renders in every state (was hiding when idle/pending-with-time-
remaining). Idle shows a quiet "Up to date." Pending outside the last 10s
shows "Unsaved changes — auto-save soon." Save now is always present;
disabled only when truly idle.

## EXEC and CONTRACTS hidden in document mode

NavStrip filters Contracts and Exec links when settings.documentModeEnabled
is true. Those areas are subsumed by the directive-document interface; the
nav strip stops surfacing them so document mode users have one canonical
place to work.

## Right-click context menus on sidebar

Right-clicking a directive folder header opens DirectiveContextMenu with
start / pause / archive / delete / Go-to-PR — same component the legacy
list page uses. Right-clicking a task row inside the tasks/ subfolder opens
a smaller TaskContextMenu with Interrupt (for orchestrator/completion/
running steps) and Mark complete / failed / skipped (for step rows).
Step lifecycle calls require the directive_step.id, so FolderTaskRow now
carries stepId alongside taskId.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 makima/frontend/src/components/NavStrip.tsx | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

(limited to 'makima/frontend/src/components/NavStrip.tsx')

diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 17013ac..a6e483d 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -1,5 +1,6 @@
 import { useAuth } from "../contexts/AuthContext";
 import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext";
+import { useUserSettings } from "../hooks/useUserSettings";
 import { RewriteLink } from "./RewriteLink";
 
 interface NavLink {
@@ -7,14 +8,30 @@ interface NavLink {
   href: string;
   requiresAuth?: boolean;
   external?: boolean;
+  /**
+   * When true the link is hidden once the user has flipped on the
+   * document-mode UI — those areas (Exec, Contracts) are subsumed by the
+   * directive-document interface and surfacing them just creates noise.
+   */
+  hideInDocumentMode?: boolean;
 }
 
 const NAV_LINKS: NavLink[] = [
   { label: "Listen", href: "/listen" },
   { label: "Directives", href: "/directives", requiresAuth: true },
   { label: "Orders", href: "/orders", requiresAuth: true },
-  { label: "Contracts", href: "/contracts", requiresAuth: true },
-  { label: "Exec", href: "/exec", requiresAuth: true },
+  {
+    label: "Contracts",
+    href: "/contracts",
+    requiresAuth: true,
+    hideInDocumentMode: true,
+  },
+  {
+    label: "Exec",
+    href: "/exec",
+    requiresAuth: true,
+    hideInDocumentMode: true,
+  },
   { label: "Daemons", href: "/daemons", requiresAuth: true },
   { label: "History", href: "/history", requiresAuth: true },
 ];
@@ -22,6 +39,8 @@ const NAV_LINKS: NavLink[] = [
 export function NavStrip() {
   const { isAuthenticated, isAuthConfigured, signOut, user } = useAuth();
   const { pendingQuestions } = useSupervisorQuestions();
+  const { settings } = useUserSettings();
+  const documentMode = settings?.documentModeEnabled ?? false;
   const directiveQuestionCount = pendingQuestions.filter(q => q.directiveId).length;
 
   const handleSignOut = async () => {
@@ -41,7 +60,9 @@ export function NavStrip() {
         NAV//
       </span>
       <div className="flex flex-wrap gap-2 items-center flex-1">
-        {NAV_LINKS.map((link) => (
+        {NAV_LINKS.filter(
+          (link) => !(documentMode && link.hideInDocumentMode),
+        ).map((link) => (
           <span key={link.label} className="relative inline-flex items-center">
             <RewriteLink
               to={link.href}
-- 
cgit v1.2.3

