summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/src/components/directives/DocumentEditor.tsx664
-rw-r--r--makima/frontend/src/components/directives/StepsBlockNode.tsx281
-rw-r--r--makima/frontend/src/routes/document-directives.tsx235
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
4 files changed, 1120 insertions, 62 deletions
diff --git a/makima/frontend/src/components/directives/DocumentEditor.tsx b/makima/frontend/src/components/directives/DocumentEditor.tsx
new file mode 100644
index 0000000..40fccf1
--- /dev/null
+++ b/makima/frontend/src/components/directives/DocumentEditor.tsx
@@ -0,0 +1,664 @@
+/**
+ * DocumentEditor — the Lexical-based document body for Document Mode.
+ *
+ * Layout (top to bottom):
+ * - Read-only H1 with the directive's title.
+ * - Editable paragraph with the directive's goal. Editing the goal triggers
+ * a 3-second countdown bar at the bottom of the editor; if the timer
+ * expires, we call updateGoal(). Esc / ⌘Z cancels; further typing extends.
+ * - A custom non-editable StepsBlock decorator node showing each step.
+ *
+ * Right-click anywhere in the editor opens a custom context menu offering
+ * the three directive-level actions: Clean Up, Update PR, Plan Orders.
+ *
+ * Live updates:
+ * - The directive prop is updated by the parent's useDirective polling.
+ * - StepsBlockContextProvider wraps the LexicalComposer so that the steps
+ * block (a Lexical DecoratorNode) sees fresh data on every render
+ * without us having to mutate the editor state.
+ */
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ $createParagraphNode,
+ $createTextNode,
+ $getRoot,
+ $isElementNode,
+ COMMAND_PRIORITY_LOW,
+ KEY_ESCAPE_COMMAND,
+ UNDO_COMMAND,
+ type LexicalEditor,
+} from "lexical";
+import { $createHeadingNode, HeadingNode } from "@lexical/rich-text";
+import { ListNode, ListItemNode } from "@lexical/list";
+import { LexicalComposer } from "@lexical/react/LexicalComposer";
+import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
+import { ContentEditable } from "@lexical/react/LexicalContentEditable";
+import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
+import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import type { DirectiveWithSteps } from "../../lib/api";
+import {
+ $createStepsBlockNode,
+ $isStepsBlockNode,
+ StepsBlockNode,
+ StepsBlockContextProvider,
+} from "./StepsBlockNode";
+
+// =============================================================================
+// Constants
+// =============================================================================
+
+const SAVE_COUNTDOWN_MS = 3000;
+const SAVED_TOAST_MS = 1200;
+
+// =============================================================================
+// Editor theme — minimal, just enough so the rich-text plugin has something to
+// hang class names on. We rely on our own typography otherwise.
+// =============================================================================
+
+const editorTheme = {
+ paragraph: "makima-doc-paragraph",
+ heading: {
+ h1: "makima-doc-h1",
+ h2: "makima-doc-h2",
+ },
+ text: {
+ bold: "font-bold",
+ italic: "italic",
+ underline: "underline",
+ },
+};
+
+// =============================================================================
+// Plugins
+// =============================================================================
+
+/**
+ * (Re)builds the editor's root content from the directive whenever the
+ * directive ID changes. We keep this controlled so that switching documents
+ * resets the editor cleanly.
+ *
+ * We deliberately do NOT re-seed on every directive update — only on id
+ * change — so the user's in-flight goal edits aren't trampled by a poll that
+ * happens mid-keystroke.
+ */
+function SeedContentPlugin({
+ directive,
+}: {
+ directive: DirectiveWithSteps;
+}) {
+ const [editor] = useLexicalComposerContext();
+ const seededIdRef = useRef<string | null>(null);
+
+ useEffect(() => {
+ if (seededIdRef.current === directive.id) return;
+ seededIdRef.current = directive.id;
+
+ editor.update(
+ () => {
+ const root = $getRoot();
+ root.clear();
+
+ // H1: title (read-only — see ReadOnlyTitlePlugin).
+ const heading = $createHeadingNode("h1");
+ heading.append($createTextNode(directive.title));
+ root.append(heading);
+
+ // Paragraph: goal (editable).
+ const goalPara = $createParagraphNode();
+ if (directive.goal.length > 0) {
+ goalPara.append($createTextNode(directive.goal));
+ }
+ root.append(goalPara);
+
+ // Steps block (decorator — non-editable).
+ root.append($createStepsBlockNode());
+
+ // Trailing empty paragraph so the cursor has somewhere to land below
+ // the steps block.
+ root.append($createParagraphNode());
+ },
+ { tag: "history-merge" },
+ );
+ }, [editor, directive.id, directive.title, directive.goal]);
+
+ return null;
+}
+
+/**
+ * Prevents edits to the H1 (title) node. The title is meant to feel like a
+ * file name — clicking it shows a caret, but typing is no-op'd. We watch the
+ * editor's update stream and, if the H1's text drifts from the seed value,
+ * revert it on the next microtask so the user sees the change get rejected
+ * rather than silently swallowed.
+ */
+function ReadOnlyTitlePlugin({ title }: { title: string }) {
+ const [editor] = useLexicalComposerContext();
+
+ useEffect(() => {
+ let reverting = false;
+ return editor.registerUpdateListener(({ editorState }) => {
+ if (reverting) return;
+ editorState.read(() => {
+ const root = $getRoot();
+ const first = root.getFirstChild();
+ if (!first || first.getType() !== "heading") return;
+ const text = first.getTextContent();
+ if (text === title) return;
+ reverting = true;
+ queueMicrotask(() => {
+ editor.update(
+ () => {
+ const r = $getRoot();
+ const h = r.getFirstChild();
+ if (!h || h.getType() !== "heading") {
+ reverting = false;
+ return;
+ }
+ if ($isElementNode(h) && h.getTextContent() !== title) {
+ h.getChildren().forEach((c) => c.remove());
+ if (title.length > 0) {
+ h.append($createTextNode(title));
+ }
+ }
+ reverting = false;
+ },
+ { tag: "history-merge" },
+ );
+ });
+ });
+ });
+ }, [editor, title]);
+
+ return null;
+}
+
+/**
+ * Watches the goal paragraph (the second top-level child) and reports its
+ * current text to the parent on every change. Kept separate from the
+ * countdown bar so the bar is purely a UI concern.
+ */
+function GoalChangePlugin({
+ onGoalChange,
+}: {
+ onGoalChange: (goal: string) => void;
+}) {
+ return (
+ <OnChangePlugin
+ ignoreSelectionChange
+ onChange={(editorState) => {
+ editorState.read(() => {
+ const root = $getRoot();
+ const children = root.getChildren();
+ // The goal lives at index 1 (after the H1 title).
+ const goalNode = children[1];
+ if (!goalNode) return;
+ onGoalChange(goalNode.getTextContent());
+ });
+ }}
+ />
+ );
+}
+
+/**
+ * Wires Lexical commands into UI callbacks. We forward Esc presses (used to
+ * cancel the countdown) and Undo (⌘/Ctrl-Z) without intercepting them.
+ */
+function CountdownKeyBridge({
+ onEsc,
+ onUndo,
+}: {
+ onEsc: () => void;
+ onUndo: () => void;
+}) {
+ const [editor] = useLexicalComposerContext();
+ useEffect(() => {
+ const unEsc = editor.registerCommand(
+ KEY_ESCAPE_COMMAND,
+ () => {
+ onEsc();
+ return false; // don't consume; let other handlers run
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+ const unUndo = editor.registerCommand(
+ UNDO_COMMAND,
+ () => {
+ onUndo();
+ return false;
+ },
+ COMMAND_PRIORITY_LOW,
+ );
+ return () => {
+ unEsc();
+ unUndo();
+ };
+ }, [editor, onEsc, onUndo]);
+ return null;
+}
+
+// =============================================================================
+// Right-click context menu
+// =============================================================================
+
+interface EditorContextMenuProps {
+ x: number;
+ y: number;
+ onClose: () => void;
+ onCleanup: () => void;
+ onUpdatePR: () => void;
+ onPlanOrders: () => void;
+}
+
+function EditorContextMenu({
+ x,
+ y,
+ onClose,
+ onCleanup,
+ onUpdatePR,
+ onPlanOrders,
+}: EditorContextMenuProps) {
+ const ref = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ const handleClick = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) onClose();
+ };
+ const handleKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ };
+ document.addEventListener("mousedown", handleClick);
+ document.addEventListener("keydown", handleKey);
+ return () => {
+ document.removeEventListener("mousedown", handleClick);
+ document.removeEventListener("keydown", handleKey);
+ };
+ }, [onClose]);
+
+ // Clamp into viewport.
+ useEffect(() => {
+ if (!ref.current) return;
+ const rect = ref.current.getBoundingClientRect();
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+ if (rect.right > vw) ref.current.style.left = `${x - rect.width}px`;
+ if (rect.bottom > vh) ref.current.style.top = `${y - rect.height}px`;
+ }, [x, y]);
+
+ const item =
+ "w-full px-3 py-1.5 text-left text-xs font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] flex items-center gap-2";
+
+ return (
+ <div
+ ref={ref}
+ className="fixed z-50 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] shadow-lg min-w-[180px]"
+ style={{ left: x, top: y }}
+ >
+ <div className="px-3 py-1.5 text-[10px] font-mono text-[#556677] uppercase border-b border-[rgba(117,170,252,0.2)]">
+ Document
+ </div>
+ <button
+ type="button"
+ className={item}
+ onClick={() => {
+ onCleanup();
+ onClose();
+ }}
+ >
+ <span className="text-[#75aafc]">⎚</span>
+ Clean Up
+ </button>
+ <button
+ type="button"
+ className={item}
+ onClick={() => {
+ onUpdatePR();
+ onClose();
+ }}
+ >
+ <span className="text-[#75aafc]">↗</span>
+ Update PR
+ </button>
+ <button
+ type="button"
+ className={item}
+ onClick={() => {
+ onPlanOrders();
+ onClose();
+ }}
+ >
+ <span className="text-[#c084fc]">◆</span>
+ Plan Orders
+ </button>
+ </div>
+ );
+}
+
+// =============================================================================
+// Countdown bar
+// =============================================================================
+
+interface SaveCountdownBarProps {
+ state: "idle" | "pending" | "saving" | "saved" | "error";
+ remainingMs: number;
+ totalMs: number;
+ onCancel: () => void;
+}
+
+function SaveCountdownBar({
+ state,
+ remainingMs,
+ totalMs,
+ onCancel,
+}: SaveCountdownBarProps) {
+ if (state === "idle") return null;
+
+ let label: string;
+ let progressPct = 0;
+ let tone = "border-[rgba(117,170,252,0.3)] text-[#9bc3ff]";
+
+ if (state === "pending") {
+ const seconds = Math.max(0, Math.ceil(remainingMs / 1000));
+ label = `Saving goal in ${seconds}s — press Esc or Undo to cancel.`;
+ progressPct = Math.max(0, Math.min(100, (1 - remainingMs / totalMs) * 100));
+ } else if (state === "saving") {
+ label = "Saving…";
+ progressPct = 100;
+ tone = "border-emerald-700 text-emerald-300";
+ } else if (state === "saved") {
+ label = "Saved";
+ progressPct = 100;
+ tone = "border-emerald-700 text-emerald-300";
+ } else {
+ label = "Save failed — try again.";
+ progressPct = 100;
+ tone = "border-red-700 text-red-300";
+ }
+
+ return (
+ <div
+ className={`shrink-0 border-t border-dashed ${tone} bg-[#0a1628]`}
+ data-makima-countdown={state}
+ >
+ <div className="h-0.5 bg-[#10203a]">
+ <div
+ className="h-full bg-[#75aafc] transition-[width] duration-100 ease-linear"
+ style={{ width: `${progressPct}%` }}
+ />
+ </div>
+ <div className="flex items-center justify-between px-4 py-1.5">
+ <span className="text-[10px] font-mono">{label}</span>
+ {state === "pending" && (
+ <button
+ type="button"
+ onClick={onCancel}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-2 py-0.5"
+ >
+ Cancel
+ </button>
+ )}
+ </div>
+ </div>
+ );
+}
+
+// =============================================================================
+// Main component
+// =============================================================================
+
+export interface DocumentEditorProps {
+ directive: DirectiveWithSteps;
+ onUpdateGoal: (goal: string) => Promise<void> | void;
+ onCleanup: () => Promise<void> | void;
+ onCreatePR: () => Promise<void> | void;
+ onPickUpOrders: () => Promise<unknown> | unknown;
+}
+
+type SaveState = "idle" | "pending" | "saving" | "saved" | "error";
+
+export function DocumentEditor({
+ directive,
+ onUpdateGoal,
+ onCleanup,
+ onCreatePR,
+ onPickUpOrders,
+}: DocumentEditorProps) {
+ // ---- Lexical config ----------------------------------------------------
+ const initialConfig = useMemo(
+ () => ({
+ // Re-key the composer when the directive id changes so we get a clean
+ // editor state per document. We do this via the `key` prop on
+ // <LexicalComposer> below as well.
+ namespace: `makima-doc-${directive.id}`,
+ onError: (err: Error) => {
+ // eslint-disable-next-line no-console
+ console.error("[DocumentEditor]", err);
+ },
+ nodes: [HeadingNode, ListNode, ListItemNode, StepsBlockNode],
+ theme: editorTheme,
+ editable: true,
+ }),
+ [directive.id],
+ );
+
+ // ---- Goal auto-save state machine --------------------------------------
+ const [saveState, setSaveState] = useState<SaveState>("idle");
+ const [remainingMs, setRemainingMs] = useState(SAVE_COUNTDOWN_MS);
+ const pendingGoalRef = useRef<string>(directive.goal);
+ const timerRef = useRef<number | null>(null);
+ const tickRef = useRef<number | null>(null);
+ const deadlineRef = useRef<number>(0);
+ const editorRef = useRef<LexicalEditor | null>(null);
+
+ // Reset state when switching directives.
+ useEffect(() => {
+ pendingGoalRef.current = directive.goal;
+ cancelTimers();
+ setSaveState("idle");
+ setRemainingMs(SAVE_COUNTDOWN_MS);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [directive.id]);
+
+ // If the persisted goal updated externally and matches the pending goal,
+ // settle the bar.
+ useEffect(() => {
+ if (saveState === "pending" && pendingGoalRef.current === directive.goal) {
+ cancelTimers();
+ setSaveState("idle");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [directive.goal]);
+
+ function cancelTimers() {
+ if (timerRef.current != null) {
+ window.clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ if (tickRef.current != null) {
+ window.clearInterval(tickRef.current);
+ tickRef.current = null;
+ }
+ }
+
+ const fireSave = useCallback(async () => {
+ const next = pendingGoalRef.current;
+ cancelTimers();
+ setSaveState("saving");
+ try {
+ await onUpdateGoal(next);
+ setSaveState("saved");
+ window.setTimeout(() => {
+ // Only fade if no new edit has reopened a pending state in the meantime.
+ setSaveState((s) => (s === "saved" ? "idle" : s));
+ }, SAVED_TOAST_MS);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error("Failed to save goal", e);
+ setSaveState("error");
+ window.setTimeout(() => {
+ setSaveState((s) => (s === "error" ? "idle" : s));
+ }, 2500);
+ }
+ }, [onUpdateGoal]);
+
+ const startOrExtendCountdown = useCallback(() => {
+ cancelTimers();
+ deadlineRef.current = Date.now() + SAVE_COUNTDOWN_MS;
+ setSaveState("pending");
+ setRemainingMs(SAVE_COUNTDOWN_MS);
+ tickRef.current = window.setInterval(() => {
+ const remaining = Math.max(0, deadlineRef.current - Date.now());
+ setRemainingMs(remaining);
+ if (remaining <= 0 && tickRef.current != null) {
+ window.clearInterval(tickRef.current);
+ tickRef.current = null;
+ }
+ }, 100);
+ timerRef.current = window.setTimeout(() => {
+ void fireSave();
+ }, SAVE_COUNTDOWN_MS);
+ }, [fireSave]);
+
+ const cancelCountdown = useCallback(() => {
+ if (saveState !== "pending") return;
+ cancelTimers();
+ pendingGoalRef.current = directive.goal; // reset pending edit
+ setSaveState("idle");
+ setRemainingMs(SAVE_COUNTDOWN_MS);
+ // Also revert the editor's goal paragraph back to the persisted value, so
+ // the user sees the rollback.
+ const editor = editorRef.current;
+ if (editor) {
+ editor.update(
+ () => {
+ const root = $getRoot();
+ const goalNode = root.getChildren()[1];
+ if (!goalNode || !$isElementNode(goalNode)) return;
+ goalNode.getChildren().forEach((c) => c.remove());
+ if (directive.goal.length > 0) {
+ goalNode.append($createTextNode(directive.goal));
+ }
+ },
+ { tag: "history-merge" },
+ );
+ }
+ }, [directive.goal, saveState]);
+
+ // Cleanup on unmount.
+ useEffect(() => {
+ return cancelTimers;
+ }, []);
+
+ const handleGoalChange = useCallback(
+ (goal: string) => {
+ pendingGoalRef.current = goal;
+ if (goal === directive.goal) {
+ // Edit reverted — cancel the countdown (if any).
+ if (saveState === "pending") {
+ cancelTimers();
+ setSaveState("idle");
+ }
+ return;
+ }
+ startOrExtendCountdown();
+ },
+ [directive.goal, saveState, startOrExtendCountdown],
+ );
+
+ // ---- Right-click context menu -----------------------------------------
+ const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
+
+ const handleContextMenu = useCallback((e: React.MouseEvent) => {
+ e.preventDefault();
+ setMenu({ x: e.clientX, y: e.clientY });
+ }, []);
+
+ // ---- Render ------------------------------------------------------------
+ return (
+ <div className="flex flex-col h-full overflow-hidden">
+ <StepsBlockContextProvider value={{ directive }}>
+ <LexicalComposer key={directive.id} initialConfig={initialConfig}>
+ {/* Capture the editor ref via a tiny inline plugin */}
+ <EditorRefCapture editorRef={editorRef} />
+ <SeedContentPlugin directive={directive} />
+ <ReadOnlyTitlePlugin title={directive.title} />
+ <HistoryPlugin />
+ <GoalChangePlugin onGoalChange={handleGoalChange} />
+ <CountdownKeyBridge onEsc={cancelCountdown} onUndo={cancelCountdown} />
+
+ <div
+ className="flex-1 overflow-auto"
+ onContextMenu={handleContextMenu}
+ >
+ <div className="max-w-3xl mx-auto px-8 py-10">
+ <RichTextPlugin
+ contentEditable={
+ <ContentEditable
+ aria-placeholder="Describe the directive's goal…"
+ placeholder={
+ <div className="pointer-events-none absolute text-[#445566] font-mono text-[13px] mt-2">
+ Describe the directive's goal…
+ </div>
+ }
+ className="outline-none font-mono text-[13px] leading-relaxed text-[#dbe7ff] [&_.makima-doc-h1]:text-[24px] [&_.makima-doc-h1]:font-medium [&_.makima-doc-h1]:text-white [&_.makima-doc-h1]:mb-3 [&_.makima-doc-h1]:tracking-tight [&_.makima-doc-paragraph]:my-2 [&_.makima-doc-paragraph]:text-[13px] [&_.makima-doc-paragraph]:text-[#c0d0e0] relative"
+ />
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+ </div>
+ </div>
+ </LexicalComposer>
+ </StepsBlockContextProvider>
+
+ <SaveCountdownBar
+ state={saveState}
+ remainingMs={remainingMs}
+ totalMs={SAVE_COUNTDOWN_MS}
+ onCancel={cancelCountdown}
+ />
+
+ {menu && (
+ <EditorContextMenu
+ x={menu.x}
+ y={menu.y}
+ onClose={() => setMenu(null)}
+ onCleanup={() => {
+ void onCleanup();
+ }}
+ onUpdatePR={() => {
+ void onCreatePR();
+ }}
+ onPlanOrders={() => {
+ void onPickUpOrders();
+ }}
+ />
+ )}
+ </div>
+ );
+}
+
+/**
+ * Tiny plugin that stashes the LexicalEditor instance on a ref so the parent
+ * component can issue updates from outside (e.g. to revert the goal on cancel).
+ */
+function EditorRefCapture({
+ editorRef,
+}: {
+ editorRef: React.MutableRefObject<LexicalEditor | null>;
+}) {
+ const [editor] = useLexicalComposerContext();
+ useEffect(() => {
+ editorRef.current = editor;
+ return () => {
+ if (editorRef.current === editor) {
+ editorRef.current = null;
+ }
+ };
+ }, [editor, editorRef]);
+ return null;
+}
+
+// Re-export the steps-block helpers so consumers can include the node class
+// in their own initial configs if needed.
+export { $createStepsBlockNode, $isStepsBlockNode, StepsBlockNode };
diff --git a/makima/frontend/src/components/directives/StepsBlockNode.tsx b/makima/frontend/src/components/directives/StepsBlockNode.tsx
new file mode 100644
index 0000000..ab3d7da
--- /dev/null
+++ b/makima/frontend/src/components/directives/StepsBlockNode.tsx
@@ -0,0 +1,281 @@
+/**
+ * StepsBlockNode — a Lexical DecoratorNode that renders the directive's steps
+ * as an in-document, non-editable diagram.
+ *
+ * The actual data (steps, orchestratorTaskId, etc.) does NOT live on the node
+ * itself — that would require us to dispatch a Lexical update on every poll,
+ * which is wasteful and fights against Lexical's content-equality model. The
+ * node is a marker that says "render the steps block here", and the React
+ * component pulls live data from a context provided by DocumentEditor. So when
+ * `useDirective` polls and produces new steps, the StepsBlock re-renders
+ * automatically without touching the editor state at all.
+ */
+import {
+ DecoratorNode,
+ type LexicalNode,
+ type NodeKey,
+ type SerializedLexicalNode,
+ type Spread,
+} from "lexical";
+import { createContext, useContext, type JSX } from "react";
+import type { DirectiveStep, DirectiveWithSteps, StepStatus } from "../../lib/api";
+
+// =============================================================================
+// Context provided by DocumentEditor — the StepsBlock reads live directive data
+// =============================================================================
+
+interface StepsBlockContextValue {
+ directive: DirectiveWithSteps | null;
+}
+
+const StepsBlockContext = createContext<StepsBlockContextValue>({ directive: null });
+
+export const StepsBlockContextProvider = StepsBlockContext.Provider;
+
+// =============================================================================
+// Status palette (matches StepNode.tsx for consistency)
+// =============================================================================
+
+const STATUS_COLORS: Record<StepStatus, { bg: string; border: string; text: string; pill: string }> = {
+ pending: {
+ bg: "bg-[#1a2540]",
+ border: "border-[#2a3a5a]",
+ text: "text-[#7788aa]",
+ pill: "bg-[#0f1a30] text-[#7788aa] border-[#2a3a5a]",
+ },
+ ready: {
+ bg: "bg-[#2a2a10]",
+ border: "border-[#4a4a20]",
+ text: "text-yellow-400",
+ pill: "bg-[#1a1a08] text-yellow-300 border-[#4a4a20]",
+ },
+ running: {
+ bg: "bg-[#0a2a1a]",
+ border: "border-[#1a5a3a]",
+ text: "text-green-400",
+ pill: "bg-[#062014] text-green-300 border-[#1a5a3a]",
+ },
+ completed: {
+ bg: "bg-[#0a2a2a]",
+ border: "border-[#1a5a5a]",
+ text: "text-emerald-400",
+ pill: "bg-[#062424] text-emerald-300 border-[#1a5a5a]",
+ },
+ failed: {
+ bg: "bg-[#2a1a1a]",
+ border: "border-[#5a2a2a]",
+ text: "text-red-400",
+ pill: "bg-[#241010] text-red-300 border-[#5a2a2a]",
+ },
+ skipped: {
+ bg: "bg-[#1a1a2a]",
+ border: "border-[#2a2a4a]",
+ text: "text-[#7788aa]",
+ pill: "bg-[#101020] text-[#7788aa] border-[#2a2a4a]",
+ },
+};
+
+const STATUS_LABEL: Record<StepStatus, string> = {
+ pending: "PENDING",
+ ready: "READY",
+ running: "RUNNING",
+ completed: "DONE",
+ failed: "FAILED",
+ skipped: "SKIP",
+};
+
+// =============================================================================
+// React component rendered inside the editor body
+// =============================================================================
+
+function StepCard({ step }: { step: DirectiveStep }) {
+ const colors = STATUS_COLORS[step.status] ?? STATUS_COLORS.pending;
+ const label = STATUS_LABEL[step.status] ?? step.status.toUpperCase();
+ return (
+ <div
+ className={`${colors.bg} border ${colors.border} rounded px-3 py-2 flex items-start gap-3`}
+ >
+ <span
+ className="text-[10px] font-mono text-[#556677] shrink-0 w-5 text-right"
+ aria-hidden
+ >
+ {String(step.orderIndex + 1).padStart(2, "0")}
+ </span>
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center justify-between gap-2">
+ <span className="text-[12px] font-mono text-white truncate">
+ {step.name}
+ </span>
+ <span
+ className={`text-[9px] font-mono uppercase tracking-wide border rounded px-1.5 py-0.5 shrink-0 ${colors.pill}`}
+ >
+ {label}
+ </span>
+ </div>
+ {step.description && (
+ <p className="text-[10px] font-mono text-[#7788aa] truncate mt-0.5">
+ {step.description}
+ </p>
+ )}
+ </div>
+ </div>
+ );
+}
+
+function StepsBlock(): JSX.Element {
+ const { directive } = useContext(StepsBlockContext);
+
+ // While the directive is loading or absent, render a quiet placeholder so the
+ // editor body still has something visible — but make sure it has the same
+ // outline as the loaded view so the document doesn't reflow.
+ if (!directive) {
+ return (
+ <div
+ contentEditable={false}
+ className="my-3 border border-dashed border-[rgba(117,170,252,0.2)] rounded px-3 py-4 select-none"
+ >
+ <div className="text-[10px] font-mono text-[#556677] uppercase tracking-wide">
+ steps · loading
+ </div>
+ </div>
+ );
+ }
+
+ const steps = [...directive.steps].sort((a, b) => a.orderIndex - b.orderIndex);
+ const isOrchestratorRunning = !!directive.orchestratorTaskId;
+ const completed = steps.filter((s) => s.status === "completed").length;
+ const total = steps.length;
+ const caption = isOrchestratorRunning
+ ? "makima is editing this document"
+ : total === 0
+ ? "no steps yet"
+ : total === 1
+ ? "1 step"
+ : `${total} steps`;
+
+ return (
+ <div
+ // contentEditable={false} keeps Lexical from treating this as editable
+ // content — the user can't put a caret inside it.
+ contentEditable={false}
+ // Use a small data attribute so external CSS / tests can target it.
+ data-makima-block="steps"
+ className="my-3 border border-[rgba(117,170,252,0.2)] rounded bg-[#091428] select-none"
+ >
+ {/* Caption */}
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-dashed border-[rgba(117,170,252,0.15)]">
+ <div className="flex items-center gap-1.5">
+ {isOrchestratorRunning && (
+ <span
+ className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse"
+ aria-hidden
+ />
+ )}
+ <span className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide">
+ {caption}
+ </span>
+ </div>
+ {total > 0 && (
+ <span className="text-[10px] font-mono text-[#556677]">
+ {completed}/{total} done
+ </span>
+ )}
+ </div>
+
+ {/* Step diagram */}
+ {steps.length === 0 ? (
+ <div className="px-3 py-4 text-[11px] font-mono text-[#556677] italic">
+ {isOrchestratorRunning
+ ? "Planner is generating steps…"
+ : "No steps yet — start the directive or plan orders to populate."}
+ </div>
+ ) : (
+ <ol className="px-3 py-3 flex flex-col gap-1.5">
+ {steps.map((step, idx) => (
+ <li key={step.id} className="relative">
+ <StepCard step={step} />
+ {idx < steps.length - 1 && (
+ <div
+ className="absolute left-[18px] -bottom-1 h-1 w-px bg-[rgba(117,170,252,0.2)]"
+ aria-hidden
+ />
+ )}
+ </li>
+ ))}
+ </ol>
+ )}
+ </div>
+ );
+}
+
+// =============================================================================
+// Lexical decorator node
+// =============================================================================
+
+export type SerializedStepsBlockNode = Spread<
+ { /* No fields — the block is a marker; live data comes from context. */ },
+ SerializedLexicalNode
+>;
+
+export class StepsBlockNode extends DecoratorNode<JSX.Element> {
+ static getType(): string {
+ return "makima-steps-block";
+ }
+
+ static clone(node: StepsBlockNode): StepsBlockNode {
+ return new StepsBlockNode(node.__key);
+ }
+
+ constructor(key?: NodeKey) {
+ super(key);
+ }
+
+ createDOM(): HTMLElement {
+ const el = document.createElement("div");
+ el.className = "makima-steps-block-host";
+ return el;
+ }
+
+ updateDOM(): false {
+ return false;
+ }
+
+ static importJSON(_serializedNode: SerializedStepsBlockNode): StepsBlockNode {
+ return $createStepsBlockNode();
+ }
+
+ exportJSON(): SerializedStepsBlockNode {
+ return {
+ type: StepsBlockNode.getType(),
+ version: 1,
+ };
+ }
+
+ isInline(): boolean {
+ return false;
+ }
+
+ isIsolated(): boolean {
+ // Isolated decorator nodes can't be partially selected — the user can only
+ // click into them, not drag a selection into them. That's what we want.
+ return true;
+ }
+
+ isKeyboardSelectable(): boolean {
+ return true;
+ }
+
+ decorate(): JSX.Element {
+ return <StepsBlock />;
+ }
+}
+
+export function $createStepsBlockNode(): StepsBlockNode {
+ return new StepsBlockNode();
+}
+
+export function $isStepsBlockNode(
+ node: LexicalNode | null | undefined,
+): node is StepsBlockNode {
+ return node instanceof StepsBlockNode;
+}
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index 81ca584..42e6a69 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -1,8 +1,9 @@
-import { useEffect, useMemo } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { Masthead } from "../components/Masthead";
import { useDirective, useDirectives } from "../hooks/useDirectives";
import { useAuth } from "../contexts/AuthContext";
+import { DocumentEditor } from "../components/directives/DocumentEditor";
import type { DirectiveSummary, DirectiveStatus } from "../lib/api";
// Status dot color, matching the existing tabular UI's badge palette so the
@@ -16,6 +17,27 @@ const STATUS_DOT: Record<DirectiveStatus, string> = {
};
// =============================================================================
+// Sidebar grouping — group directives by lifecycle stage so the file tree
+// reads like a folder per status. We collapse the noisy ones (Archived) by
+// default and keep Active / Idle expanded.
+// =============================================================================
+
+type SidebarGroup = "active" | "idle" | "archived";
+
+const GROUP_LABEL: Record<SidebarGroup, string> = {
+ active: "active",
+ idle: "idle",
+ archived: "archived",
+};
+
+function bucketOf(status: DirectiveStatus): SidebarGroup {
+ if (status === "active" || status === "paused") return "active";
+ if (status === "archived") return "archived";
+ // draft + idle land in the idle bucket (i.e. "not currently running").
+ return "idle";
+}
+
+// =============================================================================
// Sidebar icons (inline SVG, no new deps)
// =============================================================================
@@ -70,6 +92,20 @@ function FileIcon() {
);
}
+function Caret({ open }: { open: boolean }) {
+ return (
+ <svg
+ viewBox="0 0 8 8"
+ width={8}
+ height={8}
+ className={`shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
+ aria-hidden
+ >
+ <path d="M2 1l4 3-4 3z" fill="#7788aa" />
+ </svg>
+ );
+}
+
// =============================================================================
// Sidebar
// =============================================================================
@@ -82,6 +118,35 @@ interface SidebarProps {
}
function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarProps) {
+ const groups: Record<SidebarGroup, DirectiveSummary[]> = useMemo(() => {
+ const out: Record<SidebarGroup, DirectiveSummary[]> = {
+ active: [],
+ idle: [],
+ archived: [],
+ };
+ for (const d of directives) {
+ out[bucketOf(d.status)].push(d);
+ }
+ // Sort each group alphabetically so it feels like a stable file tree.
+ (Object.keys(out) as SidebarGroup[]).forEach((k) => {
+ out[k].sort((a, b) =>
+ a.title.localeCompare(b.title, undefined, { sensitivity: "base" }),
+ );
+ });
+ return out;
+ }, [directives]);
+
+ // Default-collapsed state per folder. Archived is collapsed by default
+ // (it's history); the other two are open so users see their work.
+ const [openGroups, setOpenGroups] = useState<Record<SidebarGroup, boolean>>({
+ active: true,
+ idle: true,
+ archived: false,
+ });
+
+ const toggleGroup = (g: SidebarGroup) =>
+ setOpenGroups((prev) => ({ ...prev, [g]: !prev[g] }));
+
return (
<div className="flex flex-col h-full">
{/* Sidebar header */}
@@ -94,14 +159,14 @@ function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarP
</span>
</div>
- {/* "Folder" header */}
+ {/* Top-level "directives/" folder */}
<div className="flex items-center gap-1.5 px-3 py-1.5 text-[11px] font-mono text-[#9bc3ff]">
<FolderIcon open />
<span>directives/</span>
</div>
- {/* Document list */}
- <div className="flex-1 overflow-y-auto">
+ {/* Body */}
+ <div className="flex-1 overflow-y-auto pb-4">
{loading && directives.length === 0 ? (
<div className="px-3 py-6 text-center text-[#556677] font-mono text-[11px]">
Loading...
@@ -111,40 +176,74 @@ function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarP
No directives yet
</div>
) : (
- <ul className="py-1">
- {directives.map((d) => {
- const isSelected = d.id === selectedId;
- const dot = STATUS_DOT[d.status] ?? STATUS_DOT.draft;
- // Slugify the title to look more like a file name in a tree.
- const slug = d.title
- .trim()
- .replace(/\s+/g, "-")
- .replace(/[^a-zA-Z0-9._-]/g, "")
- .toLowerCase();
- const fileName = slug.length > 0 ? `${slug}.md` : `${d.id.slice(0, 8)}.md`;
- return (
- <li key={d.id}>
- <button
- type="button"
- onClick={() => onSelect(d.id)}
- title={d.title}
- className={`w-full text-left flex items-center gap-1.5 pl-6 pr-3 py-1 font-mono text-[11px] transition-colors ${
- isSelected
- ? "bg-[rgba(117,170,252,0.12)] text-white"
- : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)]"
- }`}
- >
- <FileIcon />
- <span
- className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
- aria-hidden
- />
- <span className="truncate">{fileName}</span>
- </button>
- </li>
- );
- })}
- </ul>
+ (Object.keys(groups) as SidebarGroup[]).map((group) => {
+ const list = groups[group];
+ if (list.length === 0) return null;
+ const open = openGroups[group];
+ return (
+ <div key={group} className="select-none">
+ {/* Group header (sub-folder) */}
+ <button
+ type="button"
+ onClick={() => toggleGroup(group)}
+ className="w-full flex items-center gap-1.5 pl-4 pr-3 py-1 font-mono text-[11px] text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.05)]"
+ >
+ <Caret open={open} />
+ <FolderIcon open={open} />
+ <span>{GROUP_LABEL[group]}/</span>
+ <span className="ml-auto text-[10px] text-[#556677]">
+ {list.length}
+ </span>
+ </button>
+
+ {/* Files inside the group */}
+ {open && (
+ <ul className="py-0.5">
+ {list.map((d) => {
+ const isSelected = d.id === selectedId;
+ const dot = STATUS_DOT[d.status] ?? STATUS_DOT.draft;
+ const slug = d.title
+ .trim()
+ .replace(/\s+/g, "-")
+ .replace(/[^a-zA-Z0-9._-]/g, "")
+ .toLowerCase();
+ const fileName =
+ slug.length > 0 ? `${slug}.md` : `${d.id.slice(0, 8)}.md`;
+ const orchestratorRunning = !!d.orchestratorTaskId;
+ return (
+ <li key={d.id}>
+ <button
+ type="button"
+ onClick={() => onSelect(d.id)}
+ title={d.title}
+ className={`w-full text-left flex items-center gap-1.5 pl-9 pr-3 py-1 font-mono text-[11px] transition-colors ${
+ isSelected
+ ? "bg-[rgba(117,170,252,0.12)] text-white border-l-2 border-[#75aafc]"
+ : "text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.06)] border-l-2 border-transparent"
+ }`}
+ >
+ <FileIcon />
+ <span
+ className={`inline-block w-1.5 h-1.5 rounded-full shrink-0 ${dot}`}
+ aria-hidden
+ />
+ <span className="truncate flex-1">{fileName}</span>
+ {orchestratorRunning && (
+ <span
+ className="inline-block w-1.5 h-1.5 rounded-full shrink-0 bg-yellow-400 animate-pulse"
+ title="Orchestrator running"
+ aria-label="Orchestrator running"
+ />
+ )}
+ </button>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ </div>
+ );
+ })
)}
</div>
</div>
@@ -152,7 +251,8 @@ function DocumentSidebar({ directives, loading, selectedId, onSelect }: SidebarP
}
// =============================================================================
-// Editor shell (placeholder — actual Lexical body lands in the next step)
+// Editor shell — wraps DocumentEditor and handles the "no document selected"
+// and loading states.
// =============================================================================
interface EditorShellProps {
@@ -162,7 +262,14 @@ interface EditorShellProps {
}
function EditorShell({ selectedId, hasDirectives, listLoading }: EditorShellProps) {
- const { directive, loading } = useDirective(selectedId);
+ const {
+ directive,
+ loading,
+ updateGoal,
+ cleanup,
+ createPR,
+ pickUpOrders,
+ } = useDirective(selectedId);
if (!selectedId) {
return (
@@ -196,24 +303,37 @@ function EditorShell({ selectedId, hasDirectives, listLoading }: EditorShellProp
return (
<div className="flex-1 flex flex-col h-full overflow-hidden">
- {/* Document header */}
- <div className="px-6 py-4 border-b border-dashed border-[rgba(117,170,252,0.2)]">
- <div className="flex items-center gap-2 mb-1 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
+ {/* Document header — breadcrumb-like, mirrors a code editor's tab bar */}
+ <div className="px-6 py-3 border-b border-dashed border-[rgba(117,170,252,0.2)]">
+ <div className="flex items-center gap-2 text-[10px] font-mono uppercase tracking-wide text-[#7788aa]">
<FileIcon />
<span>directives /</span>
<span className="text-[#9bc3ff]">{directive.id.slice(0, 8)}</span>
+ {!!directive.orchestratorTaskId && (
+ <span className="ml-2 inline-flex items-center gap-1 text-yellow-400">
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" />
+ orchestrator running
+ </span>
+ )}
</div>
- <h1 className="text-[18px] font-mono text-white truncate">
- {directive.title}
- </h1>
</div>
- {/* Placeholder editor body — Lexical rich text comes in the next step. */}
- <div className="flex-1 overflow-auto">
- <div className="max-w-3xl mx-auto px-6 py-10 font-mono text-[12px] text-[#556677]">
- Document editor coming soon&hellip;
- </div>
- </div>
+ {/* Lexical editor body */}
+ <DocumentEditor
+ directive={directive}
+ onUpdateGoal={async (goal) => {
+ await updateGoal(goal);
+ }}
+ onCleanup={async () => {
+ await cleanup();
+ }}
+ onCreatePR={async () => {
+ await createPR();
+ }}
+ onPickUpOrders={async () => {
+ await pickUpOrders();
+ }}
+ />
</div>
);
}
@@ -234,13 +354,6 @@ export default function DocumentDirectivesPage() {
}
}, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
- const sortedDirectives = useMemo(() => {
- // Stable alphabetical order by title — feels right for a "file tree".
- return [...directives].sort((a, b) =>
- a.title.localeCompare(b.title, undefined, { sensitivity: "base" }),
- );
- }, [directives]);
-
if (authLoading) {
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
@@ -262,14 +375,14 @@ export default function DocumentDirectivesPage() {
{/* Left: file-tree sidebar */}
<div className="w-[240px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col bg-[#091428]">
<DocumentSidebar
- directives={sortedDirectives}
+ directives={directives}
loading={listLoading}
selectedId={selectedId ?? null}
onSelect={(id) => navigate(`/directives/${id}`)}
/>
</div>
- {/* Right: empty editor shell */}
+ {/* Right: Lexical editor */}
<EditorShell
selectedId={selectedId}
hasDirectives={directives.length > 0}
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index c078688..56c723a 100644
--- a/makima/frontend/tsconfig.tsbuildinfo
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/doglist.tsx","./src/components/directives/directivecontextmenu.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/taskslideoutpanel.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/ordercontextmenu.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usedogs.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useusersettings.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemons.tsx","./src/routes/directives.tsx","./src/routes/document-directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/doglist.tsx","./src/components/directives/directivecontextmenu.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/documenteditor.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/stepsblocknode.tsx","./src/components/directives/taskslideoutpanel.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/ordercontextmenu.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usedogs.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useusersettings.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemons.tsx","./src/routes/directives.tsx","./src/routes/document-directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file