From a2148d4e3117cdda2e1d0a8e3df289bfe04789a3 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 30 Apr 2026 12:12:48 +0100 Subject: feat(document-mode): folder layout v2, glow on pending, inline formatting, autosave fix (#107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Autosave bug fix (top priority) The 250ms debounce on the localStorage draft write was racing the unmount cleanup: typing then navigating within 250ms cleared the pending timer *before* it flushed, which is exactly when we needed the draft saved. Drafts are now written synchronously on every keystroke. localStorage .setItem on a small string is sub-millisecond — the debounce was a premature optimisation. ## Sidebar v2 (document-directives.tsx) - Tasks now live in a `tasks/` subfolder inside each directive folder (orchestrator, completion, and started step tasks). The pinned `.md` document remains at the top of the directive folder. - Status circles moved to the RIGHT side only (previously rendered on both sides, which the user found noisy). - New `StatusDot` component composes the status colour with two optional modifiers: a "live" pulse when the orchestrator is running, and a GLOW (amber ring + pulse) when there is a pending user question for that directive or task. The glow is sourced from the existing SupervisorQuestionsContext, indexed by `directiveId` and `taskId`. - New `TaskIcon` (terminal) and `CompletionIcon` (PR-bracket) so orchestrator/step/completion entries look distinct from the .md file. ## Inline formatting in the editor (DocumentEditor.tsx) - New `MarkdownShortcutPlugin` (scoped to TEXT_FORMAT_TRANSFORMERS only) so typing `**foo**`, `*foo*`, `` `foo` ``, `~~foo~~` auto-formats inline. Block-level shortcuts (# heading, - list) are intentionally excluded so the document shape (H1 / goal / StepsBlock / trailing para) stays intact. - New `FloatingFormatToolbar` appears above any non-collapsed selection inside the goal paragraph, with B / I / U / S / buttons that dispatch FORMAT_TEXT_COMMAND. Buttons highlight when the corresponding format is active. Standard ⌘B / ⌘I / ⌘U keyboard shortcuts also work via the existing RichTextPlugin. - Round-trip via a small inline-only markdown serializer/parser so formatting persists across saves. Supported markers: `\``code\``, `***bold-italic***`, `**bold**`, `*italic* / _italic_`, `~~strike~~`. Underline survives within the editor session (toolbar / shortcut) but has no markdown syntax so it does not round-trip — by design. - No backend schema change: `directive.goal` is still a TEXT column, it just contains inline markdown now. Co-authored-by: Claude Opus 4.7 (1M context) --- makima/frontend/src/routes/document-directives.tsx | 241 +++++++++++++++------ 1 file changed, 175 insertions(+), 66 deletions(-) (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index 687d86f..aba3613 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams, useSearchParams } from "react-router"; import { Masthead } from "../components/Masthead"; import { useDirective, useDirectives } from "../hooks/useDirectives"; import { useAuth } from "../contexts/AuthContext"; +import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext"; import { DocumentEditor } from "../components/directives/DocumentEditor"; import { DocumentTaskStream } from "../components/directives/DocumentTaskStream"; import type { @@ -87,6 +88,40 @@ function FileIcon() { ); } +/** Terminal/prompt icon for orchestrator and step tasks. */ +function TaskIcon() { + return ( + + + + + ); +} + +/** PR-bracket icon for the completion task. */ +function CompletionIcon() { + return ( + + + + + + + ); +} + function PinIcon() { return ( void; selection: SidebarSelection | null; onSelect: (sel: SidebarSelection) => void; + /** Set of task ids that currently have pending user questions. */ + pendingTaskIds: Set; + /** Whether any pending question is associated with this directive. */ + hasPendingForDirective: boolean; }) { const dotColor = STATUS_DOT[directive.status] ?? STATUS_DOT.draft; const fileName = `${slugify(directive.title, directive.id.slice(0, 8))}.md`; @@ -177,6 +219,10 @@ function DirectiveFolder({ // Collect the tasks to surface in the folder body. const tasks = useMemo(() => collectTasks(detailed, directive), [detailed, directive]); + const orchestratorRunning = !!directive.orchestratorTaskId; + // Tasks subfolder open state — independent of the directive folder. + const [tasksOpen, setTasksOpen] = useState(true); + return (
@@ -229,57 +265,113 @@ function DirectiveFolder({ - {tasks.length === 0 ? ( -
  • - No tasks yet -
  • - ) : ( - tasks.map((t) => { - const isSelected = - selection?.directiveId === directive.id && - selection?.taskId === t.taskId; - const tdot = STEP_STATUS_DOT[t.status] ?? STEP_STATUS_DOT.pending; - const live = - t.status === "running" || t.kind === "orchestrator-active"; - return ( -
  • - -
  • - ); - }) - )} + {/* tasks/ subfolder — collapsible, contains orchestrator/completion/steps. */} +
  • + + + {tasksOpen && ( +
      + {tasks.length === 0 ? ( +
    • + No tasks yet +
    • + ) : ( + tasks.map((t) => { + const isSelected = + selection?.directiveId === directive.id && + selection?.taskId === t.taskId; + const tdot = STEP_STATUS_DOT[t.status] ?? STEP_STATUS_DOT.pending; + const live = + t.status === "running" || t.kind === "orchestrator-active"; + const glow = pendingTaskIds.has(t.taskId); + const Icon = + t.kind === "completion" ? CompletionIcon : TaskIcon; + return ( +
    • + +
    • + ); + }) + )} +
    + )} +
  • )}
    ); } +/** + * Right-side status indicator. Composes the colored status dot with optional + * "live" pulse (orchestrator running) and "glow" attention ring (pending user + * question waiting on a response). + */ +function StatusDot({ + color, + live, + glow, + status, +}: { + color: string; + live: boolean; + glow: boolean; + status: string; +}) { + // The glow is a soft amber ring pulsed via box-shadow. Keep it subtle so it + // doesn't fight the live pulse for attention when both are present. + const ring = glow + ? "shadow-[0_0_0_2px_rgba(251,191,36,0.45),0_0_8px_2px_rgba(251,191,36,0.55)] animate-pulse" + : ""; + const livePulse = live && !glow ? "animate-pulse" : ""; + const title = glow + ? `${status} — needs response` + : live + ? `${status} — running` + : `status: ${status}`; + return ( + + ); +} + interface FolderTaskRow { taskId: string; label: string; @@ -342,6 +434,21 @@ interface SidebarProps { } function DocumentSidebar({ directives, loading, selection, onSelect }: SidebarProps) { + // Pending user questions — drives the "glow" attention ring. We split into + // two indices so the directive folder header glows whenever ANY of its + // tasks has a pending question, while individual task rows glow only for + // their own question. + const { pendingQuestions } = useSupervisorQuestions(); + const { directivesWithPending, tasksWithPending } = useMemo(() => { + const dirs = new Set(); + const tasks = new Set(); + for (const q of pendingQuestions) { + if (q.directiveId) dirs.add(q.directiveId); + if (q.taskId) tasks.add(q.taskId); + } + return { directivesWithPending: dirs, tasksWithPending: tasks }; + }, [pendingQuestions]); + // Sort active first, then idle, then paused, then archived. const sorted = useMemo(() => { const order: Record = { @@ -421,6 +528,8 @@ function DocumentSidebar({ directives, loading, selection, onSelect }: SidebarPr onToggle={() => toggleOpen(d.id)} selection={selection} onSelect={onSelect} + pendingTaskIds={tasksWithPending} + hasPendingForDirective={directivesWithPending.has(d.id)} /> )) )} -- cgit v1.2.3