From 8e2bbcab1a7b3b9005803d7ce3bfce7fa483a4d7 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 16 May 2026 18:04:17 +0100 Subject: fix(directives): tasks folder visibility, circular auto-save timer, unlock-to-edit (#133) * feat: soryu - makima: Fix tasks/ folder visibility and rename for multi-contract directives * feat: soryu - makima: Replace auto-save countdown text/bar with a circular timer * feat: soryu - makima: Require Unlock before editing a locked contract body --- .../src/components/directives/DocumentEditor.tsx | 282 +++++++++++++++++---- makima/frontend/src/routes/document-directives.tsx | 54 +++- makima/frontend/tsconfig.tsbuildinfo | 2 +- 3 files changed, 283 insertions(+), 55 deletions(-) diff --git a/makima/frontend/src/components/directives/DocumentEditor.tsx b/makima/frontend/src/components/directives/DocumentEditor.tsx index 661665c..0d6a391 100644 --- a/makima/frontend/src/components/directives/DocumentEditor.tsx +++ b/makima/frontend/src/components/directives/DocumentEditor.tsx @@ -69,8 +69,6 @@ import { */ const COUNTDOWN_FRESH_MS = 60_000; const COUNTDOWN_RUNNING_MS = 10_000; -/** The countdown bar only appears once we're inside this many ms from firing. */ -const BAR_VISIBLE_MS = 10_000; const SAVED_TOAST_MS = 1200; /** * Drafts are written synchronously to localStorage on every keystroke. We used @@ -722,9 +720,66 @@ function EditorContextMenu({ // Countdown bar // ============================================================================= +/** + * Compact circular countdown indicator. The ring fills (clockwise from 12 + * o'clock) as `progress` approaches 1. Uses `currentColor` so the parent's + * text tone drives both the rest of the row and the ring. + */ +function CircularTimer({ + progress, + size = 14, + stroke = 2, + className, + title, +}: { + progress: number; + size?: number; + stroke?: number; + className?: string; + title?: string; +}) { + const r = (size - stroke) / 2; + const c = 2 * Math.PI * r; + const dash = Math.max(0, Math.min(1, progress)) * c; + return ( + + {title ? {title} : null} + + + + ); +} + interface SaveCountdownBarProps { state: "idle" | "dirty" | "pending" | "saving" | "saved" | "error"; remainingMs: number; + /** Total countdown window (varies by orchestrator state). Used to compute + * the circular timer's fill ratio. */ + countdownMs: number; liveStart: boolean; orchestratorRunning: boolean; draftSavedAt: number | null; @@ -736,6 +791,7 @@ interface SaveCountdownBarProps { function SaveCountdownBar({ state, remainingMs, + countdownMs, liveStart, orchestratorRunning, draftSavedAt, @@ -747,46 +803,65 @@ function SaveCountdownBar({ // observe save state at all times — and to have a "Save now" button they // can hit without waiting for the countdown. - let label: string; - let progressPct = 0; let tone = "border-[rgba(117,170,252,0.3)] text-[#9bc3ff]"; + if (state === "saving" || state === "saved") { + tone = "border-emerald-700 text-emerald-300"; + } else if (state === "error") { + tone = "border-red-700 text-red-300"; + } else if (state === "idle") { + tone = "border-[rgba(117,170,252,0.2)] text-[#7788aa]"; + } + // Left-side primary indicator. For `pending` and `saving`, the circular + // timer is the visual; the text next to it is short and neutral. For + // every other state, we render plain text only. + let primary: React.ReactNode; if (state === "pending") { + const ringProgress = + countdownMs > 0 + ? Math.max(0, Math.min(1, 1 - remainingMs / countdownMs)) + : 0; const seconds = Math.max(0, Math.ceil(remainingMs / 1000)); - // Show ticking countdown in the last 10s, otherwise a quieter label. - if (remainingMs <= BAR_VISIBLE_MS) { + const tipBase = orchestratorRunning + ? `Replans in ${seconds}s — Esc/Undo cancels` + : `Auto-saves in ${seconds}s — Esc/Undo cancels`; + const ringLabel = orchestratorRunning ? "Replan" : "Auto-save"; + primary = ( + + + {ringLabel} + + ); + } else if (state === "saving") { + primary = ( + + + Saving… + + ); + } else { + let label: string; + if (state === "dirty") { label = orchestratorRunning - ? `Replanning in ${seconds}s — Esc/Undo cancels.` - : `Saving in ${seconds}s — Esc/Undo cancels.`; - progressPct = Math.max( - 0, - Math.min(100, (1 - remainingMs / BAR_VISIBLE_MS) * 100), - ); + ? "Unsaved changes — saving will replan the contract." + : "Unsaved changes."; + } else if (state === "saved") { + label = "Saved"; + } else if (state === "error") { + label = "Save failed — try again."; } else { - label = "Unsaved changes — auto-save soon."; - progressPct = 0; + label = "Up to date."; } - } else if (state === "dirty") { - label = orchestratorRunning - ? "Unsaved changes — saving will replan the contract." - : "Unsaved changes."; - progressPct = 0; - } 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 if (state === "error") { - label = "Save failed — try again."; - progressPct = 100; - tone = "border-red-700 text-red-300"; - } else { - label = "Up to date."; - progressPct = 0; - tone = "border-[rgba(117,170,252,0.2)] text-[#7788aa]"; + primary = ( + {label} + ); } // Right-side "Draft saved Xs ago" stamp — re-renders on a 1Hz ticker so @@ -801,14 +876,8 @@ function SaveCountdownBar({ className={`shrink-0 border-t border-dashed ${tone} bg-[#0a1628]`} data-makima-countdown={state} > -
-
-
- {label} + {primary} {draftLabel && ( Promise | void; onCreatePR: () => Promise | void; onPickUpOrders: () => Promise | unknown; + /** + * Whether the body content is currently editable. Locked (non-`draft`) + * contracts pass `false`, which puts Lexical into read-only mode, disables + * the autosave state machine, and surfaces the "Unlock to edit" affordance. + */ + editable: boolean; + /** + * Invoked when the user clicks the inline "Unlock" affordance shown above + * the body for non-`draft` contracts. The caller is expected to flip the + * contract status back to `draft` (typically via `unlockDirectiveContract`) + * and refresh the document so this component receives `editable=true`. + */ + onRequestUnlock: () => void | Promise; } type SaveState = "idle" | "dirty" | "pending" | "saving" | "saved" | "error"; @@ -890,8 +972,14 @@ export function DocumentEditor({ onCleanup, onCreatePR, onPickUpOrders, + editable, + onRequestUnlock, }: DocumentEditorProps) { // ---- Lexical config ---------------------------------------------------- + // NOTE: `editable` is only read by Lexical at composer mount time. Since + // we re-key the composer by `documentId` (not status), we ALSO mirror the + // `editable` prop into the live editor via below on + // every change. const initialConfig = useMemo( () => ({ // Re-key the composer when the directive id changes so we get a clean @@ -904,8 +992,9 @@ export function DocumentEditor({ }, nodes: [HeadingNode, ListNode, ListItemNode, StepsBlockNode], theme: editorTheme, - editable: true, + editable, }), + // eslint-disable-next-line react-hooks/exhaustive-deps [documentId], ); @@ -1148,6 +1237,12 @@ export function DocumentEditor({ const handleGoalChange = useCallback( (goal: string) => { + // When the body is locked (contract is not in `draft`), Lexical's + // setEditable(false) blocks user input, but the OnChangePlugin can + // still fire for programmatic seeds. Ignore everything in that case + // so we don't kick the autosave state machine. + if (!editable) return; + pendingGoalRef.current = goal; // 1. Always persist work-in-progress to localStorage IMMEDIATELY so @@ -1185,9 +1280,21 @@ export function DocumentEditor({ setSaveState("dirty"); } }, - [body, documentId, liveStart, saveState, startOrExtendCountdown], + [body, documentId, editable, liveStart, saveState, startOrExtendCountdown], ); + // When the body becomes locked (contract status leaves `draft`), tear + // down any in-flight autosave: timers off, state back to idle. The + // existing per-keystroke localStorage draft remains untouched — it'll + // re-hydrate the editor if/when the user unlocks again. + useEffect(() => { + if (!editable) { + cancelTimers(); + setSaveState("idle"); + setRemainingMs(countdownMs); + } + }, [editable, countdownMs]); + // ---- Right-click context menu ----------------------------------------- const [menu, setMenu] = useState<{ x: number; y: number } | null>(null); @@ -1196,6 +1303,35 @@ export function DocumentEditor({ setMenu({ x: e.clientX, y: e.clientY }); }, []); + // ---- Unlock affordance state ----------------------------------------- + // When the contract is locked (non-`draft`), the body shows an inline + // banner with an Unlock button that calls back into the parent. We + // surface any error from the unlock API inline rather than throwing — + // mirrors the inline error pattern in ContractHeader. + const [unlockBusy, setUnlockBusy] = useState(false); + const [unlockError, setUnlockError] = useState(null); + + // Clear any stale unlock error once the body becomes editable again. + useEffect(() => { + if (editable) { + setUnlockError(null); + setUnlockBusy(false); + } + }, [editable]); + + const handleUnlockClick = useCallback(async () => { + if (unlockBusy) return; + setUnlockBusy(true); + setUnlockError(null); + try { + await onRequestUnlock(); + } catch (e) { + setUnlockError(e instanceof Error ? e.message : "Unlock failed"); + } finally { + setUnlockBusy(false); + } + }, [onRequestUnlock, unlockBusy]); + // ---- Render ------------------------------------------------------------ return (
@@ -1203,12 +1339,19 @@ export function DocumentEditor({ {/* Capture the editor ref via a tiny inline plugin */} + {/* Keep Lexical's editable flag in sync with the parent prop. */} + { pendingGoalRef.current = draft; + // Don't kick the autosave state machine if the body is + // currently locked — the lock-watching useEffect would just + // tear it back down on the next render, and we don't want + // a flash of "pending" state under a locked banner. + if (!editable) return; if (liveStart) { startOrExtendCountdown(); } else { @@ -1228,10 +1371,33 @@ export function DocumentEditor({
+ {!editable && ( +
+ + Locked — unlock to edit. + + {unlockError && ( + + {unlockError} + + )} + +
+ )} } - 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" + // When locked, dim the text and switch the caret to the + // default arrow so the body visually reads as read-only. + // Lexical's setEditable(false) already blocks input; this + // is the cosmetic layer. + style={!editable ? { cursor: "default" } : undefined} + className={`outline-none font-mono text-[13px] leading-relaxed [&_.makima-doc-h1]:text-[24px] [&_.makima-doc-h1]:font-medium [&_.makima-doc-h1]:mb-3 [&_.makima-doc-h1]:tracking-tight [&_.makima-doc-paragraph]:my-2 [&_.makima-doc-paragraph]:text-[13px] relative ${ + editable + ? "text-[#dbe7ff] [&_.makima-doc-h1]:text-white [&_.makima-doc-paragraph]:text-[#c0d0e0]" + : "text-[#7a8aa0] [&_.makima-doc-h1]:text-[#a8b8d0] [&_.makima-doc-paragraph]:text-[#7a8aa0] select-text" + }`} /> } ErrorBoundary={LexicalErrorBoundary} @@ -1254,6 +1429,7 @@ export function DocumentEditor({ { + editor.setEditable(editable); + }, [editor, editable]); + 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/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index 06e427a..479dcd8 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -266,6 +266,11 @@ function DirectiveFolder({ return { activeDocs: active, shippedDocs: shipped }; }, [docs]); + // When a directive owns more than one contract, the two `tasks/` folders + // would otherwise be ambiguous. We pass this down to DocumentTasksFolder + // so it can rename itself to `tasks - /` for clarity. + const multipleContracts = activeDocs.length + shippedDocs.length > 1; + const handleCreate = useCallback(async () => { if (creating) return; setCreating(true); @@ -433,6 +438,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + contractLabel={fileLabel(doc, directive)} + multipleContracts={multipleContracts} />
))} @@ -477,6 +484,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + contractLabel={fileLabel(doc, directive)} + multipleContracts={multipleContracts} />
))} @@ -627,6 +636,13 @@ interface DocumentTasksFolderProps { selectedTaskId: string | null; /** Click handler for step/task rows — navigates to the live transcript. */ onSelectTask: (directiveId: string, taskId: string) => void; + /** Human-readable contract label (already resolved via fileLabel). Used to + * disambiguate multiple tasks/ folders under the same directive. */ + contractLabel: string; + /** True when the parent directive owns more than one contract — drives the + * `tasks - /` rename so the two sibling tasks/ folders are + * distinguishable. Single-contract directives keep the plain `tasks/`. */ + multipleContracts: boolean; } function DocumentTasksFolder({ @@ -637,6 +653,8 @@ function DocumentTasksFolder({ refreshNonce, selectedTaskId, onSelectTask, + contractLabel, + multipleContracts, }: DocumentTasksFolderProps) { const [open, setOpen] = useState(defaultOpen); const [data, setData] = useState(null); @@ -672,24 +690,29 @@ function DocumentTasksFolder({ const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0); - // Don't render the folder at all if we've fetched and the document has - // no tasks. This is the cleanest visual: a draft document just shows up - // as a single row with no children. The empty-folder check is gated on - // a successful fetch so we don't flash "no tasks/" rows during loading. - if (data && total === 0 && !loading && !error) { - return null; - } + // Folder always renders (even when empty) so the user can click into a + // fresh contract's tasks/ folder and see it stay visible. The empty state + // shows a muted "no tasks yet" placeholder inside the open body — same + // visual weight as the existing "Loading tasks…" / error placeholders. + // + // When the parent directive owns multiple contracts, both tasks/ folders + // are disambiguated as `tasks - /` so the user can tell + // them apart. Single-contract directives keep the plain `tasks/` label. + const headerLabel = multipleContracts + ? `tasks - ${contractLabel}/` + : "tasks/"; return (
)} + {data && total === 0 && !loading && !error && ( +
+ no tasks yet +
+ )} {data?.steps.map((step) => ( { await pickUpOrders(); }} + // Locked-and-started (`active`), `queued`, `shipped`, and + // `archived` contracts must be unlocked before edits are + // accepted. Only `draft` is freely editable; everything else + // shows the in-editor Unlock affordance. + editable={doc.status === "draft"} + onRequestUnlock={async () => { + const updated = await unlockDirectiveContract(doc.id); + setDoc(updated); + onDocumentChanged(); + }} />
); diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index a520296..acdfa37 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/quickswitcher.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/documenttaskstream.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/exec-redirect.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/routes/tmp.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/quickswitcher.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/directives/doglist.tsx","./src/components/directives/directivecontextmenu.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/documenteditor.tsx","./src/components/directives/documenttaskstream.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/daemons.tsx","./src/routes/directives.tsx","./src/routes/document-directives.tsx","./src/routes/exec-redirect.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/routes/tmp.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file -- cgit v1.2.3