feat(desktop): add daemon status dialog to built-in daemon settings

Add a "Full status" button in the desktop daemon section that runs
`paseo daemon status` via Electron IPC and displays the raw output
in a modal.

Also fixes Commander.js argv parsing when the CLI is invoked via
Electron's ELECTRON_RUN_AS_NODE — Commander auto-detects the Electron
environment and skips only 1 argv element instead of 2, causing
"unknown command" errors. Fixed by explicitly passing { from: "node" }.
This commit is contained in:
Mohamed Boudra
2026-04-03 18:49:59 +07:00
parent f924d33076
commit 4ca04fc661
5 changed files with 106 additions and 1 deletions
@@ -14,6 +14,7 @@ import {
Copy,
FileText,
Smartphone,
Activity,
} from "lucide-react-native";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Button } from "@/components/ui/button";
@@ -22,6 +23,7 @@ import { confirmDialog } from "@/utils/confirm-dialog";
import { openExternalUrl } from "@/utils/open-external-url";
import { getLocalDaemonVersion, isVersionMismatch } from "@/desktop/updates/desktop-updates";
import {
getCliDaemonStatus,
getCliSymlinkInstructions,
getDesktopDaemonLogs,
getDesktopDaemonPairing,
@@ -62,6 +64,9 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
const [cliSymlinkInstructions, setCliSymlinkInstructions] =
useState<CliSymlinkInstructions | null>(null);
const [pairingStatusMessage, setPairingStatusMessage] = useState<string | null>(null);
const [cliStatusOutput, setCliStatusOutput] = useState<string | null>(null);
const [isCliStatusModalOpen, setIsCliStatusModalOpen] = useState(false);
const [isLoadingCliStatus, setIsLoadingCliStatus] = useState(false);
const loadDaemonData = useCallback(() => {
if (!showSection) {
@@ -315,6 +320,33 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
});
}, [pairingOffer?.url]);
const handleOpenCliStatus = useCallback(async () => {
setIsLoadingCliStatus(true);
try {
setCliStatusOutput(await getCliDaemonStatus());
setIsCliStatusModalOpen(true);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setCliStatusOutput(`Failed to fetch daemon status: ${message}`);
setIsCliStatusModalOpen(true);
} finally {
setIsLoadingCliStatus(false);
}
}, []);
const handleCopyCliStatus = useCallback(() => {
if (!cliStatusOutput) {
return;
}
void Clipboard.setStringAsync(cliStatusOutput)
.then(() => {
Alert.alert("Copied", "Status copied to clipboard.");
})
.catch((error) => {
console.error("[Settings] Failed to copy daemon status", error);
});
}, [cliStatusOutput]);
if (!showSection) {
return null;
}
@@ -458,6 +490,23 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
Pair device
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Full status</Text>
<Text style={styles.hintText}>
Runs `paseo daemon status` and shows the output.
</Text>
</View>
<Button
variant="outline"
size="sm"
leftIcon={<Activity size={theme.iconSize.sm} color={theme.colors.foreground} />}
onPress={() => void handleOpenCliStatus()}
disabled={isLoadingCliStatus}
>
{isLoadingCliStatus ? "Loading..." : "View status"}
</Button>
</View>
</View>
{daemonVersionMismatch ? (
@@ -524,6 +573,28 @@ export function LocalDaemonSection({ appVersion, showLifecycleControls }: LocalD
</Text>
</View>
</AdaptiveModalSheet>
<AdaptiveModalSheet
visible={isCliStatusModalOpen}
onClose={() => setIsCliStatusModalOpen(false)}
title="Daemon status"
testID="daemon-cli-status-dialog"
snapPoints={["60%", "85%"]}
>
<View style={styles.modalBody}>
<Text style={styles.logOutput} selectable>
{cliStatusOutput ?? ""}
</Text>
<View style={styles.modalActions}>
<Button variant="outline" size="sm" onPress={() => setIsCliStatusModalOpen(false)}>
Close
</Button>
<Button size="sm" onPress={handleCopyCliStatus}>
Copy
</Button>
</View>
</View>
</AdaptiveModalSheet>
</View>
);
}
@@ -162,6 +162,14 @@ export async function getCliSymlinkInstructions(): Promise<CliSymlinkInstruction
return parseCliSymlinkInstructions(await invokeDesktopCommand("cli_symlink_instructions"));
}
export async function getCliDaemonStatus(): Promise<string> {
const raw = await invokeDesktopCommand<unknown>("cli_daemon_status");
if (typeof raw !== "string") {
throw new Error("Unexpected CLI daemon status response.");
}
return raw;
}
export type LocalTransportEventUnlisten = () => void;
export type LocalTransportEventHandler = (payload: LocalTransportEventPayload) => void;
+1 -1
View File
@@ -4,4 +4,4 @@ const program = createCli();
if (process.argv.length <= 2) {
process.argv.push("onboard");
}
program.parse();
program.parse(process.argv, { from: "node" });
@@ -21,6 +21,7 @@ import {
createNodeEntrypointInvocation,
resolveDaemonRunnerEntrypoint,
runCliJsonCommand,
runCliTextCommand,
} from "./runtime-paths.js";
const DAEMON_LOG_FILENAME = "daemon.log";
@@ -398,6 +399,10 @@ function getDaemonLogs(): DesktopDaemonLogs {
};
}
function getCliDaemonStatus(): string {
return runCliTextCommand(["daemon", "status"]);
}
async function getDaemonPairing(): Promise<DesktopPairingOffer> {
const status = resolveStatus();
if (status.status !== "running") {
@@ -521,6 +526,7 @@ export function createDaemonCommandHandlers(): Record<string, DesktopCommandHand
desktop_daemon_logs: () => getDaemonLogs(),
desktop_daemon_pairing: () => getDaemonPairing(),
cli_symlink_instructions: () => getCliSymlinkInstructions(),
cli_daemon_status: () => getCliDaemonStatus(),
write_attachment_base64: (args) => writeAttachmentBase64(args ?? {}),
copy_attachment_file: (args) => copyAttachmentFileToManagedStorage(args ?? {}),
read_file_base64: (args) => readManagedFileBase64(args ?? {}),
@@ -246,6 +246,26 @@ export function runCliPassthroughCommand(args: string[]): number {
return result.signal ? 1 : 0;
}
export function runCliTextCommand(args: string[]): string {
const invocation = createCliInvocation(args);
const result = spawnSync(invocation.command, invocation.args, {
env: invocation.env,
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
throw new Error(stderr.length > 0 ? stderr : `CLI command failed with exit code ${result.status}`);
}
return typeof result.stdout === "string" ? result.stdout.trimEnd() : "";
}
export function runCliJsonCommand(args: string[]): unknown {
const invocation = createCliInvocation(args);
const result = spawnSync(invocation.command, invocation.args, {