Skip to content

gui_cmd

gui_cmd

jarvis gui — start and open the local graphical interface.

Functions

clear_frontend_state

clear_frontend_state(pid: int, launch_token: str | None) -> None

Deregister a frontend, but only if it still owns the file.

Source code in src/openjarvis/cli/gui_cmd.py
def clear_frontend_state(pid: int, launch_token: str | None) -> None:
    """Deregister a frontend, but only if it still owns the file."""
    with _frontend_state_lock():
        state = _read_frontend_state()
        if state.get("pid") == pid and state.get("launch_token") == launch_token:
            _FRONTEND_STATE_FILE.unlink(missing_ok=True)

stop_frontend

stop_frontend(*, state: dict | None = None) -> int | None

Stop the recorded frontend, optionally using a port check's exact snapshot.

Unknown ownership keeps the saved state and raises so a later call can retry. Returns the launch PID stopped, or None for a stale registration.

Source code in src/openjarvis/cli/gui_cmd.py
def stop_frontend(*, state: dict | None = None) -> int | None:
    """Stop the recorded frontend, optionally using a port check's exact snapshot.

    Unknown ownership keeps the saved state and raises so a later call can
    retry. Returns the launch PID stopped, or None for a stale registration.
    """
    state = _read_frontend_state() if state is None else state
    pid = state.get("pid")
    if pid is None:
        return None
    stopped = _terminate_frontend(state)
    clear_frontend_state(pid, state.get("launch_token"))
    return pid if stopped else None

gui

gui(frontend_port: int, api_port: int, no_server: bool, no_browser: bool) -> None

Start the browser-based graphical mode in the default browser.

This command is intended for source checkouts. For an installed desktop application, launch OpenJarvis from the operating system menu instead.

Source code in src/openjarvis/cli/gui_cmd.py
@click.command()
@click.option(
    "--frontend-port", default=5173, show_default=True, type=click.IntRange(1, 65535)
)
@click.option(
    "--api-port", default=8000, show_default=True, type=click.IntRange(1, 65535)
)
@click.option("--no-server", is_flag=True, help="Do not start the API server.")
@click.option("--no-browser", is_flag=True, help="Only start the frontend.")
def gui(frontend_port: int, api_port: int, no_server: bool, no_browser: bool) -> None:
    """Start the browser-based graphical mode in the default browser.

    This command is intended for source checkouts. For an installed desktop
    application, launch OpenJarvis from the operating system menu instead.
    """
    console = Console(stderr=True)
    frontend = _frontend_dir()
    if frontend is None:
        raise click.ClickException(
            "The graphical frontend is not available in this installation. "
            "Download the OpenJarvis desktop app or run this command "
            "from a source checkout."
        )
    npm = shutil.which("npm") or shutil.which("npm.cmd")
    if npm is None:
        raise click.ClickException(
            "Node.js/npm is required for graphical mode. Install Node.js 22 or newer."
        )
    _check_frontend_port(frontend_port)
    _ensure_frontend_dependencies(frontend, npm)

    # `jarvis start` exits non-zero when a daemon is already registered, so
    # without this a perfectly healthy API turned into the misleading "Could
    # not start the OpenJarvis API server." Adopt it instead.
    if not no_server and _api_healthy(api_port):
        if not _daemon_pid_alive(api_port, require_ready=True):
            raise click.ClickException(
                f"API port {api_port} is already serving an unregistered service. "
                "Stop it or choose another port. Use --no-server only when you "
                "intend to use an externally managed OpenJarvis backend."
            )
        console.print(
            f"[green]Reusing the API server already running on port {api_port}.[/green]"
        )
        no_server = True

    launched_api = not no_server
    if launched_api:
        uv = shutil.which("uv")
        if uv is None:
            raise click.ClickException(
                "uv is required to start the API with desktop dependencies. "
                "Install uv or use --no-server with an already-running API."
            )
        server = subprocess.run(
            [
                uv,
                "run",
                "--extra",
                "desktop",
                "jarvis",
                "start",
                "--port",
                str(api_port),
            ],
            cwd=frontend.parent,
            check=False,
        )
        if server.returncode != 0:
            raise click.ClickException("Could not start the OpenJarvis API server.")

    env = os.environ.copy()
    # Let browser requests use Vite's same-origin proxy at any frontend port.
    # VITE_API_URL is exposed to browser code, so clear an inherited override.
    env["VITE_API_URL"] = ""
    env["OPENJARVIS_VITE_PROXY_TARGET"] = f"http://127.0.0.1:{api_port}"
    spawn_kwargs = {}
    if sys.platform == "win32":
        spawn_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
    else:
        spawn_kwargs["start_new_session"] = True
    process = subprocess.Popen(
        [
            npm,
            "run",
            "dev",
            "--",
            "--host",
            "127.0.0.1",
            "--port",
            str(frontend_port),
            "--strictPort",
        ],
        cwd=frontend,
        env=env,
        **spawn_kwargs,
    )
    owner = _FrontendOwner(process, frontend_port)
    shutdown = owner.shutdown
    handlers = {}
    atexit.register(shutdown)
    try:
        for signum in (signal.SIGTERM, getattr(signal, "SIGBREAK", None)):
            if signum is not None:
                with contextlib.suppress(OSError, ValueError):
                    previous = signal.getsignal(signum)
                    signal.signal(signum, lambda *_: sys.exit(1))
                    handlers[signum] = previous
        owner.register()
        if not _wait_for_port(
            process, "127.0.0.1", frontend_port, on_poll=owner.refresh
        ):
            raise click.ClickException(
                "The graphical frontend did not start on the requested port."
            )
        owner.refresh()
        url = f"http://127.0.0.1:{frontend_port}"
        console.print(f"[green]OpenJarvis graphical mode is ready:[/green] {url}")
        if not no_browser:
            webbrowser.open(url)

        watch_api = _api_healthy(api_port)
        failures = 0
        while True:
            owner.refresh()
            try:
                process.wait(timeout=_API_POLL_SECONDS)
                break
            except subprocess.TimeoutExpired:
                pass
            healthy = _api_healthy(api_port)
            if healthy:
                watch_api = True
                failures = 0
                continue
            if not watch_api and not launched_api:
                continue
            failures += 1
            if failures < _API_FAILURES_BEFORE_DOWN or _daemon_pid_alive(api_port):
                continue
            console.print(
                "[red]The API server stopped or failed to start; "
                "shutting down the frontend.[/red]\n"
                f"  Check {DEFAULT_CONFIG_DIR / 'server.log'}, "
                f"then run 'jarvis gui' again."
            )
            raise SystemExit(1)
    except KeyboardInterrupt:
        pass
    finally:
        atexit.unregister(shutdown)
        for signum, previous in handlers.items():
            with contextlib.suppress(OSError, ValueError):
                signal.signal(signum, previous)
        shutdown()