diff options
| author | soryu <soryu@soryu.co> | 2026-05-01 00:26:08 +0100 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-05-01 00:26:08 +0100 |
| commit | 765bea6cbf69388b992c1ed4126e9aafcf33e7e9 (patch) | |
| tree | 3e6c937e956ebc48e066578e5a4ebb87d741c9a2 | |
| parent | dbc7a0f5edcf3099fbf91ddca4bf5b307a2f40ff (diff) | |
| parent | a16fa65345c554f0201b1f1f521ba4e4be46d9de (diff) | |
| download | soryu-makima/directive-soryu-co-soryu---makima-96b4ff64-v1777591338.tar.gz soryu-makima/directive-soryu-co-soryu---makima-96b4ff64-v1777591338.zip | |
Merge remote-tracking branch 'origin/makima/soryu-co-soryu---makima--frontend--live-update-sid-f88e8841' into makima/directive-soryu-co-soryu---makima-96b4ff64-v1777591338makima/directive-soryu-co-soryu---makima-96b4ff64-v1777591338
# Conflicts:
# makima/frontend/pnpm-lock.yaml
# makima/frontend/src/routes/document-directives.tsx
| -rw-r--r-- | makima/frontend/pnpm-lock.yaml | 6 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectiveSubscription.ts | 180 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectives.ts | 80 | ||||
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 38 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 2 |
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 |
