summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/pnpm-lock.yaml6
-rw-r--r--makima/frontend/src/hooks/useDirectiveSubscription.ts180
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts80
-rw-r--r--makima/frontend/src/routes/document-directives.tsx38
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
5 files changed, 299 insertions, 7 deletions
diff --git a/makima/frontend/pnpm-lock.yaml b/makima/frontend/pnpm-lock.yaml
index 2dfbf67..b9d2ecd 100644
--- a/makima/frontend/pnpm-lock.yaml
+++ b/makima/frontend/pnpm-lock.yaml
@@ -1676,7 +1676,7 @@ packages:
dependencies:
baseline-browser-mapping: 2.10.24
caniuse-lite: 1.0.30001791
- electron-to-chromium: 1.5.344
+ electron-to-chromium: 1.5.345
node-releases: 2.0.38
update-browserslist-db: 1.2.3(browserslist@4.28.2)
dev: true
@@ -1865,8 +1865,8 @@ packages:
engines: {node: '>=8'}
dev: true
- /electron-to-chromium@1.5.344:
- resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==}
+ /electron-to-chromium@1.5.345:
+ resolution: {integrity: sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==}
dev: true
/enhanced-resolve@5.21.0:
diff --git a/makima/frontend/src/hooks/useDirectiveSubscription.ts b/makima/frontend/src/hooks/useDirectiveSubscription.ts
new file mode 100644
index 0000000..2efaa2b
--- /dev/null
+++ b/makima/frontend/src/hooks/useDirectiveSubscription.ts
@@ -0,0 +1,180 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { TASK_SUBSCRIBE_ENDPOINT } from "../lib/api";
+
+/**
+ * The set of directive update event kinds the server may emit. Mirrors the
+ * server-side `DirectiveUpdateNotification.kind` field that the backend
+ * broadcasts on the directive_updates channel.
+ */
+export type DirectiveUpdateKind =
+ | "created"
+ | "updated"
+ | "deleted"
+ | "step_created"
+ | "step_updated"
+ | "step_deleted";
+
+export interface DirectiveUpdateEvent {
+ directiveId: string;
+ kind: DirectiveUpdateKind;
+ stepId?: string;
+ version: number;
+}
+
+interface UseDirectiveSubscriptionOptions {
+ /**
+ * Whether the subscription is active. Defaults to true. Set false to pause
+ * the connection (e.g. when the user is not on the doc-view page).
+ */
+ enabled?: boolean;
+ onUpdate?: (event: DirectiveUpdateEvent) => void;
+ onError?: (error: string) => void;
+}
+
+/**
+ * Thin WebSocket hook that subscribes to the directive_updates broadcast on
+ * the existing task subscription endpoint. Auto-reconnects with the same 3s
+ * backoff used by `useTaskSubscription`. Mirrors that hook's conventions:
+ * single connection per hook instance, callbacks stored in a ref so changing
+ * them never tears the socket down, lazy connect when `enabled` is true, and
+ * an explicit `unsubscribeDirectives` on cleanup so the server stops sending.
+ */
+export function useDirectiveSubscription(
+ options: UseDirectiveSubscriptionOptions = {}
+) {
+ const { enabled = true, onUpdate, onError } = options;
+
+ const [connected, setConnected] = useState(false);
+ const wsRef = useRef<WebSocket | null>(null);
+ const reconnectTimeoutRef = useRef<number | null>(null);
+ // Track whether we want to be subscribed across reconnects.
+ const subscribedRef = useRef(false);
+
+ // Stable callbacks ref so consumers can pass new function identities each
+ // render without churning the WebSocket.
+ const callbacksRef = useRef({ onUpdate, onError });
+ useEffect(() => {
+ callbacksRef.current = { onUpdate, onError };
+ }, [onUpdate, onError]);
+
+ const connect = useCallback(() => {
+ const currentState = wsRef.current?.readyState;
+ if (
+ currentState === WebSocket.OPEN ||
+ currentState === WebSocket.CONNECTING
+ ) {
+ return;
+ }
+ if (wsRef.current && currentState === WebSocket.CLOSING) {
+ wsRef.current = null;
+ }
+
+ try {
+ const ws = new WebSocket(TASK_SUBSCRIBE_ENDPOINT);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setConnected(true);
+ if (subscribedRef.current) {
+ ws.send(JSON.stringify({ type: "subscribeDirectives" }));
+ }
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const message = JSON.parse(event.data);
+ switch (message.type) {
+ case "directiveUpdated":
+ callbacksRef.current.onUpdate?.({
+ directiveId: message.directiveId,
+ kind: message.kind as DirectiveUpdateKind,
+ stepId: message.stepId,
+ version: message.version,
+ });
+ break;
+ case "error":
+ callbacksRef.current.onError?.(message.message);
+ break;
+ // Acknowledgements — no-op
+ case "directivesSubscribed":
+ case "directivesUnsubscribed":
+ break;
+ }
+ } catch (e) {
+ console.error("Failed to parse directive subscription message:", e);
+ }
+ };
+
+ ws.onerror = () => {
+ callbacksRef.current.onError?.("WebSocket connection error");
+ };
+
+ ws.onclose = () => {
+ setConnected(false);
+ wsRef.current = null;
+
+ // Reconnect with the same 3s backoff used elsewhere if we still want
+ // to be subscribed.
+ if (subscribedRef.current) {
+ reconnectTimeoutRef.current = window.setTimeout(() => {
+ connect();
+ }, 3000);
+ }
+ };
+ } catch (e) {
+ callbacksRef.current.onError?.(
+ e instanceof Error ? e.message : "Failed to connect"
+ );
+ }
+ }, []);
+
+ // Drive (un)subscription based on `enabled`.
+ useEffect(() => {
+ if (enabled) {
+ subscribedRef.current = true;
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify({ type: "subscribeDirectives" }));
+ } else {
+ connect();
+ }
+ } else {
+ // Tell the server we're done, then close — no pending reconnect.
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
+ wsRef.current.send(JSON.stringify({ type: "unsubscribeDirectives" }));
+ }
+ subscribedRef.current = false;
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ reconnectTimeoutRef.current = null;
+ }
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ }
+ }, [enabled, connect]);
+
+ // Cleanup on unmount.
+ useEffect(() => {
+ return () => {
+ subscribedRef.current = false;
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+ if (wsRef.current) {
+ if (wsRef.current.readyState === WebSocket.OPEN) {
+ try {
+ wsRef.current.send(
+ JSON.stringify({ type: "unsubscribeDirectives" })
+ );
+ } catch {
+ /* socket already going down */
+ }
+ }
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+ };
+ }, []);
+
+ return { connected };
+}
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
index 898f671..2bfb63a 100644
--- a/makima/frontend/src/hooks/useDirectives.ts
+++ b/makima/frontend/src/hooks/useDirectives.ts
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback, useRef } from "react";
import {
type DirectiveSummary,
type DirectiveWithSteps,
@@ -23,6 +23,45 @@ import {
pickUpOrders as pickUpOrdersApi,
createDirectivePR,
} from "../lib/api";
+import type { DirectiveUpdateEvent } from "./useDirectiveSubscription";
+
+// =============================================================================
+// In-module event bus for directive updates.
+//
+// The actual WebSocket is opened once at the page level (see
+// `document-directives.tsx`) via `useDirectiveSubscription`. That hook calls
+// `dispatchDirectiveUpdate(event)` for each `directiveUpdated` message, which
+// fans the event out to every `useDirectives()` and `useDirective(id)` listener
+// in the tree. This keeps the connection count bounded to one regardless of
+// how many directive hooks are mounted.
+// =============================================================================
+
+type DirectiveUpdateListener = (event: DirectiveUpdateEvent) => void;
+
+const directiveUpdateListeners = new Set<DirectiveUpdateListener>();
+
+function subscribeDirectiveUpdates(
+ listener: DirectiveUpdateListener,
+): () => void {
+ directiveUpdateListeners.add(listener);
+ return () => {
+ directiveUpdateListeners.delete(listener);
+ };
+}
+
+/**
+ * Dispatch a directive update event to all listeners. Called from the
+ * page-level `useDirectiveSubscription` hook.
+ */
+export function dispatchDirectiveUpdate(event: DirectiveUpdateEvent): void {
+ for (const listener of directiveUpdateListeners) {
+ try {
+ listener(event);
+ } catch (e) {
+ console.error("Directive update listener threw:", e);
+ }
+ }
+}
export function useDirectives() {
const [directives, setDirectives] = useState<DirectiveSummary[]>([]);
@@ -46,6 +85,29 @@ export function useDirectives() {
refresh();
}, [refresh]);
+ // Live updates: refresh the list when any directive changes. Debounced so a
+ // burst of events (e.g. a directive transition that also creates a step task)
+ // collapses into a single fetch.
+ const debounceTimerRef = useRef<number | null>(null);
+ useEffect(() => {
+ const unsubscribe = subscribeDirectiveUpdates(() => {
+ if (debounceTimerRef.current != null) {
+ clearTimeout(debounceTimerRef.current);
+ }
+ debounceTimerRef.current = window.setTimeout(() => {
+ debounceTimerRef.current = null;
+ refresh();
+ }, 250);
+ });
+ return () => {
+ if (debounceTimerRef.current != null) {
+ clearTimeout(debounceTimerRef.current);
+ debounceTimerRef.current = null;
+ }
+ unsubscribe();
+ };
+ }, [refresh]);
+
const create = useCallback(async (req: CreateDirectiveRequest) => {
const d = await createDirective(req);
await refresh();
@@ -100,7 +162,9 @@ export function useDirective(id: string | undefined) {
refresh();
}, [id]); // eslint-disable-line react-hooks/exhaustive-deps
- // Auto-poll while directive is active, has an orchestrator task, or has a completion task
+ // Auto-poll while directive is active, has an orchestrator task, or has a completion task.
+ // Kept alongside the live subscription as a safety net for missed events
+ // (e.g. brief disconnects, lagged broadcast channel).
useEffect(() => {
if (!directive) return;
const needsPolling =
@@ -113,6 +177,18 @@ export function useDirective(id: string | undefined) {
return () => clearInterval(interval);
}, [directive?.status, directive?.orchestratorTaskId, directive?.completionTaskId, silentRefresh]);
+ // Live updates: silent-refresh when an event for THIS directive arrives.
+ // Includes step_* events whose directiveId matches. We don't surface a
+ // loading state here so the editor pane doesn't flash.
+ useEffect(() => {
+ if (!id) return;
+ const unsubscribe = subscribeDirectiveUpdates((event) => {
+ if (event.directiveId !== id) return;
+ silentRefresh();
+ });
+ return unsubscribe;
+ }, [id, silentRefresh]);
+
const update = useCallback(async (req: UpdateDirectiveRequest) => {
if (!id) return;
await updateDirective(id, req);
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx
index ffd2a8b..4afc52d 100644
--- a/makima/frontend/src/routes/document-directives.tsx
+++ b/makima/frontend/src/routes/document-directives.tsx
@@ -1,7 +1,13 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { Masthead } from "../components/Masthead";
-import { useDirective, useDirectives } from "../hooks/useDirectives";
+import {
+ dispatchDirectiveUpdate,
+ useDirective,
+ useDirectives,
+} from "../hooks/useDirectives";
+import { useDirectiveSubscription } from "../hooks/useDirectiveSubscription";
+import type { DirectiveUpdateEvent } from "../hooks/useDirectiveSubscription";
import { useAuth } from "../contexts/AuthContext";
import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext";
import { DocumentEditor } from "../components/directives/DocumentEditor";
@@ -1135,6 +1141,36 @@ export default function DocumentDirectivesPage() {
const closeContextMenu = useCallback(() => setContextMenu(null), []);
+ // Single page-level WebSocket for directive updates. Fans events out via the
+ // in-module event bus so every `useDirective(id)` and `useDirectives()` hook
+ // mounted under this page reacts without opening additional sockets.
+ const handleDirectiveUpdate = useCallback(
+ (event: DirectiveUpdateEvent) => {
+ // Let directive hooks (list + detail) reconcile their own state.
+ dispatchDirectiveUpdate(event);
+
+ // Always nudge the sidebar list so status dots / orchestrator pulses
+ // reflect the change immediately (this is debounced inside useDirectives).
+ refreshList();
+
+ // If the currently-selected directive was deleted, navigate back to the
+ // list so we don't get stuck on a "Document not found" screen.
+ if (
+ event.kind === "deleted" &&
+ selectedId &&
+ event.directiveId === selectedId
+ ) {
+ navigate("/directives");
+ }
+ },
+ [navigate, refreshList, selectedId],
+ );
+
+ useDirectiveSubscription({
+ enabled: isAuthenticated || !isAuthConfigured,
+ onUpdate: handleDirectiveUpdate,
+ });
+
if (authLoading) {
return (
<div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 56c723a..f4341f6 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/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
+{"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/usedirectivesubscription.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