diff --git a/packages/mobile/src/hooks/useSession.ts b/packages/mobile/src/hooks/useSession.ts index 82c355b69..7bdf901ca 100644 --- a/packages/mobile/src/hooks/useSession.ts +++ b/packages/mobile/src/hooks/useSession.ts @@ -5,6 +5,11 @@ import type { DashboardSession } from "../types"; const POLL_INTERVAL = 5_000; +interface UseSessionOptions { + /** Set to false to disable polling. Defaults to true. */ + enabled?: boolean; +} + interface UseSessionResult { session: DashboardSession | null; loading: boolean; @@ -12,7 +17,8 @@ interface UseSessionResult { refresh: () => void; } -export function useSession(id: string): UseSessionResult { +export function useSession(id: string, options?: UseSessionOptions): UseSessionResult { + const enabled = options?.enabled ?? true; const { fetchSession } = useBackend(); const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); @@ -51,6 +57,12 @@ export function useSession(id: string): UseSessionResult { }, []); useEffect(() => { + if (!enabled) { + setSession(null); + setLoading(false); + return; + } + startPolling(); const handleAppState = (nextState: AppStateStatus) => { @@ -69,12 +81,13 @@ export function useSession(id: string): UseSessionResult { stopPolling(); sub.remove(); }; - }, [startPolling, stopPolling]); + }, [enabled, startPolling, stopPolling]); const refresh = useCallback(() => { + if (!enabled) return; setLoading(true); doFetch(); - }, [doFetch]); + }, [enabled, doFetch]); return { session, loading, error, refresh }; } diff --git a/packages/mobile/src/screens/OrchestratorScreen.tsx b/packages/mobile/src/screens/OrchestratorScreen.tsx index 62ebfb7ea..8d386a221 100644 --- a/packages/mobile/src/screens/OrchestratorScreen.tsx +++ b/packages/mobile/src/screens/OrchestratorScreen.tsx @@ -36,7 +36,7 @@ function getZoneCounts(sessions: DashboardSession[]) { export default function OrchestratorScreen({ navigation }: Props) { const { sessions, orchestratorId, loading, error, refresh } = useSessions(); const { sendMessage, terminalWsUrl } = useBackend(); - const { session: orchSession } = useSession(orchestratorId ?? ""); + const { session: orchSession } = useSession(orchestratorId ?? "", { enabled: !!orchestratorId }); const [message, setMessage] = useState(""); const [sending, setSending] = useState(false); diff --git a/packages/mobile/src/terminal/terminal-html.ts b/packages/mobile/src/terminal/terminal-html.ts index 00b3283b3..62f162462 100644 --- a/packages/mobile/src/terminal/terminal-html.ts +++ b/packages/mobile/src/terminal/terminal-html.ts @@ -148,10 +148,11 @@ export const TERMINAL_HTML = ` }); // XDA handler: respond to CSI > q with XTerm identity (enables tmux clipboard) - term.onData(function (data) { - if (data === '\\x1b[>q') { - term.write('\\x1bP>|XTerm(370)\\x1b\\\\'); - } + // Must use parser.registerCsiHandler — onData only captures outgoing user input, + // not incoming data from the WebSocket. The XDA query arrives via ws → term.write(). + term.parser.registerCsiHandler({ prefix: '>', final: 'q' }, function () { + term.write('\\x1bP>|XTerm(370)\\x1b\\\\'); + return true; }); document.addEventListener('message', function (evt) { handleRNMessage(evt.data); });