Build App / compute-version (pull_request) Successful in 4s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-linux (pull_request) Successful in 5m14s
Build App / build-windows (pull_request) Successful in 5m56s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
The pane could only ever watch a browser something else had published. This opens one: a URL and a viewport, launched inside the container and bound so the pane picks it up. Two uses, one action — a sign-in page, where the callback listener is *in* the container and the loop closes with no host round trip and no auth bridge, and a dev server on container loopback, which is how you watch a UI Claude is building. Reachable from both places the question comes up: "Open a page…" in the Browser tab, and an "In container" button on the terminal's URL prompt. Verified first, because it decided the design: a second client cannot join a bound browser. `chromium.connect()` against the published endpoint times out in every URL form (`ws+unix://…`, with and without the trailing path) — that socket speaks the dashboard's own transport, not the public connect protocol. Whoever launches is therefore the only process that can drive, so the helper is resident and holds the handle, and live resize applies to pages we opened and never to `@playwright/mcp`'s. Those take `--viewport-size` / `PLAYWRIGHT_MCP_VIEWPORT_SIZE` at launch, which the docs now say. The viewport is the interesting half. Resizing the *window* does nothing to the page — the viewer is a CDP screencast, so a bigger window is the same pixels drawn larger, which is why pages have been looking like they were rendered small. `page.setViewportSize()` genuinely reflows: measured against a `@media (max-width: 900px)` rule, it fires at 800×600 and clears at 1440×900. Match-window mode pushes the pop-out's settled size into it, debounced by generation counter because a drag emits `Resized` continuously and each one costs a container exec. Control is a polled JSON file in /tmp: no port, no second listener, nothing added to the proxy's surface, and URLs travel as argv to `node` so no shell ever parses one. A re-open with a helper already up navigates instead of relaunching — otherwise the second page would throw away the session the first one just signed into. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
327 lines
18 KiB
TypeScript
327 lines
18 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo } from "./types";
|
|
|
|
// Docker
|
|
export const checkDocker = () => invoke<boolean>("check_docker");
|
|
export const checkImageExists = () => invoke<boolean>("check_image_exists");
|
|
export const buildImage = () => invoke<void>("build_image");
|
|
export const getContainerInfo = (projectId: string) =>
|
|
invoke<ContainerInfo | null>("get_container_info", { projectId });
|
|
export const listSiblingContainers = () =>
|
|
invoke<SiblingContainer[]>("list_sibling_containers");
|
|
|
|
// Projects
|
|
export const listProjects = () => invoke<Project[]>("list_projects");
|
|
export const addProject = (name: string, paths: ProjectPath[]) =>
|
|
invoke<Project>("add_project", { name, paths });
|
|
export const removeProject = (projectId: string) =>
|
|
invoke<void>("remove_project", { projectId });
|
|
export const updateProject = (project: Project) =>
|
|
invoke<Project>("update_project", { project });
|
|
export const startProjectContainer = (projectId: string) =>
|
|
invoke<Project>("start_project_container", { projectId });
|
|
export const stopProjectContainer = (projectId: string) =>
|
|
invoke<void>("stop_project_container", { projectId });
|
|
export const rebuildProjectContainer = (projectId: string) =>
|
|
invoke<Project>("rebuild_project_container", { projectId });
|
|
export const reconcileProjectStatuses = () =>
|
|
invoke<Project[]>("reconcile_project_statuses");
|
|
|
|
// Settings
|
|
export const getSettings = () => invoke<AppSettings>("get_settings");
|
|
export const updateSettings = (settings: AppSettings) =>
|
|
invoke<AppSettings>("update_settings", { settings });
|
|
export const pullImage = (imageName: string) =>
|
|
invoke<void>("pull_image", { imageName });
|
|
export const detectAwsConfig = () =>
|
|
invoke<string | null>("detect_aws_config");
|
|
export const listAwsProfiles = () =>
|
|
invoke<string[]>("list_aws_profiles");
|
|
/** Check a corporate CA path and report what would be installed. Never
|
|
* rejects for a bad path — the reason comes back in `error`. */
|
|
export const inspectCaCertPath = (path: string) =>
|
|
invoke<CaCertInfo>("inspect_ca_cert_path", { path });
|
|
export const detectHostTimezone = () =>
|
|
invoke<string>("detect_host_timezone");
|
|
|
|
// AWS
|
|
export const awsSsoRefresh = (projectId: string) =>
|
|
invoke<void>("aws_sso_refresh", { projectId });
|
|
|
|
// Terminal
|
|
export const openTerminalSession = (projectId: string, sessionId: string, sessionType?: string, sessionName?: string) =>
|
|
invoke<void>("open_terminal_session", { projectId, sessionId, sessionType, sessionName });
|
|
export const terminalInput = (sessionId: string, data: number[]) =>
|
|
invoke<void>("terminal_input", { sessionId, data });
|
|
export const terminalResize = (sessionId: string, cols: number, rows: number) =>
|
|
invoke<void>("terminal_resize", { sessionId, cols, rows });
|
|
export const closeTerminalSession = (sessionId: string) =>
|
|
invoke<void>("close_terminal_session", { sessionId });
|
|
export const pasteImageToTerminal = (sessionId: string, imageData: number[]) =>
|
|
invoke<string>("paste_image_to_terminal", { sessionId, imageData });
|
|
export const uploadHostFileToTerminal = (sessionId: string, hostPath: string) =>
|
|
invoke<string>("upload_host_file_to_terminal", { sessionId, hostPath });
|
|
export const startAudioBridge = (sessionId: string) =>
|
|
invoke<void>("start_audio_bridge", { sessionId });
|
|
export const sendAudioData = (sessionId: string, data: number[]) =>
|
|
invoke<void>("send_audio_data", { sessionId, data });
|
|
export const stopAudioBridge = (sessionId: string) =>
|
|
invoke<void>("stop_audio_bridge", { sessionId });
|
|
|
|
// Files
|
|
export const listContainerFiles = (projectId: string, path: string) =>
|
|
invoke<FileEntry[]>("list_container_files", { projectId, path });
|
|
export const downloadContainerFile = (projectId: string, containerPath: string, hostPath: string) =>
|
|
invoke<void>("download_container_file", { projectId, containerPath, hostPath });
|
|
export const downloadContainerBackup = (projectId: string, hostPath: string, containerPath?: string) =>
|
|
invoke<number>("download_container_backup", { projectId, hostPath, containerPath });
|
|
export const uploadFileToContainer = (projectId: string, hostPath: string, containerDir: string) =>
|
|
invoke<void>("upload_file_to_container", { projectId, hostPath, containerDir });
|
|
|
|
// Updates
|
|
export const getAppVersion = () => invoke<string>("get_app_version");
|
|
export const checkForUpdates = () =>
|
|
invoke<UpdateInfo | null>("check_for_updates");
|
|
export const checkImageUpdate = () =>
|
|
invoke<ImageUpdateInfo | null>("check_image_update");
|
|
|
|
// Help
|
|
export const getHelpContent = () => invoke<string>("get_help_content");
|
|
|
|
// Web Terminal
|
|
export const startWebTerminal = () =>
|
|
invoke<WebTerminalInfo>("start_web_terminal");
|
|
export const stopWebTerminal = () =>
|
|
invoke<void>("stop_web_terminal");
|
|
export const getWebTerminalStatus = () =>
|
|
invoke<WebTerminalInfo>("get_web_terminal_status");
|
|
export const regenerateWebTerminalToken = () =>
|
|
invoke<WebTerminalInfo>("regenerate_web_terminal_token");
|
|
|
|
// STT
|
|
export const getSttStatus = () => invoke<SttStatus>("get_stt_status");
|
|
export const startStt = () => invoke<SttStatus>("start_stt");
|
|
export const stopStt = () => invoke<void>("stop_stt");
|
|
export const buildSttImage = () => invoke<void>("build_stt_image");
|
|
export const pullSttImage = () => invoke<void>("pull_stt_image");
|
|
export const transcribeAudio = (audioData: number[]) =>
|
|
invoke<string>("transcribe_audio", { audioData });
|
|
|
|
// Model gateway (LiteLLM)
|
|
export const getGatewayStatus = () => invoke<GatewayStatus>("get_gateway_status");
|
|
export const startGateway = () => invoke<GatewayStatus>("start_gateway");
|
|
export const stopGateway = () => invoke<void>("stop_gateway");
|
|
export const checkGatewayHealth = () => invoke<boolean>("check_gateway_health");
|
|
export const buildGatewayImage = () => invoke<void>("build_gateway_image");
|
|
export const pullGatewayImage = () => invoke<void>("pull_gateway_image");
|
|
/** Write-only: the provider API key is never read back out of the keychain. */
|
|
export const setGatewayApiKey = (apiKey: string) =>
|
|
invoke<void>("set_gateway_api_key", { apiKey });
|
|
export const clearGatewayApiKey = () => invoke<void>("clear_gateway_api_key");
|
|
export const getGatewayAuthToken = () => invoke<string>("get_gateway_auth_token");
|
|
export const regenerateGatewayAuthToken = () =>
|
|
invoke<string>("regenerate_gateway_auth_token");
|
|
|
|
// Docker install helper
|
|
export const detectInstallOptions = () =>
|
|
invoke<InstallOptions>("detect_install_options");
|
|
export const runDockerInstall = () => invoke<void>("run_docker_install");
|
|
|
|
// Container introspection — sessions
|
|
export const listClaudeSessions = (projectId: string) =>
|
|
invoke<ClaudeSession[]>("list_claude_sessions", { projectId });
|
|
export const resumeSessionCommand = (projectId: string, sessionId: string) =>
|
|
invoke<string>("resume_session_command", { projectId, sessionId });
|
|
|
|
// Container introspection — capabilities
|
|
export const listContainerCapabilities = (projectId: string) =>
|
|
invoke<ContainerCapabilities>("list_container_capabilities", { projectId });
|
|
|
|
// Container introspection — scheduler
|
|
export const listScheduledTasks = (projectId: string) =>
|
|
invoke<ScheduledTask[]>("list_scheduled_tasks", { projectId });
|
|
/** Returns the new task's id. */
|
|
export const addScheduledTask = (projectId: string, input: ScheduledTaskInput) =>
|
|
invoke<string>("add_scheduled_task", { projectId, ...input });
|
|
/** Edit = add + remove, so this returns a *new* task id (see the Rust doc). */
|
|
export const updateScheduledTask = (
|
|
projectId: string,
|
|
taskId: string,
|
|
input: ScheduledTaskInput,
|
|
enabled: boolean,
|
|
) => invoke<string>("update_scheduled_task", { projectId, taskId, enabled, ...input });
|
|
export const getScheduledTaskLog = (projectId: string, taskId: string, tailLines?: number) =>
|
|
invoke<string>("get_scheduled_task_log", { projectId, taskId, tailLines });
|
|
export const setScheduledTaskEnabled = (projectId: string, taskId: string, enabled: boolean) =>
|
|
invoke<string>("set_scheduled_task_enabled", { projectId, taskId, enabled });
|
|
export const runScheduledTaskNow = (projectId: string, taskId: string) =>
|
|
invoke<string>("run_scheduled_task_now", { projectId, taskId });
|
|
export const removeScheduledTask = (projectId: string, taskId: string) =>
|
|
invoke<string>("remove_scheduled_task", { projectId, taskId });
|
|
export const getSchedulerNotifications = (projectId: string) =>
|
|
invoke<SchedulerNotification[]>("get_scheduler_notifications", { projectId });
|
|
export const clearSchedulerNotifications = (projectId: string) =>
|
|
invoke<void>("clear_scheduler_notifications", { projectId });
|
|
|
|
// Auth bridge — mirrors container loopback listeners onto host loopback so
|
|
// browser OAuth logins started inside the container can complete.
|
|
export const setAuthBridgeEnabled = (projectId: string, enabled: boolean) =>
|
|
invoke<AuthBridgeStatus>("set_auth_bridge_enabled", { projectId, enabled });
|
|
export const getAuthBridgeStatus = (projectId: string) =>
|
|
invoke<AuthBridgeStatus>("get_auth_bridge_status", { projectId });
|
|
|
|
// Browser view — watch and take over the browser Claude drives with Playwright
|
|
// inside the container. Off by default, per project. Enabling probes the
|
|
// container, starts the Playwright dashboard in it, and puts a token-gated
|
|
// listener on the host's loopback in front of it; the returned `url` is the
|
|
// only way in, and it is never reachable off the machine.
|
|
export const setBrowserViewEnabled = (projectId: string, enabled: boolean) =>
|
|
invoke<BrowserViewStatus>("set_browser_view_enabled", { projectId, enabled });
|
|
export const getBrowserViewStatus = (projectId: string) =>
|
|
invoke<BrowserViewStatus>("get_browser_view_status", { projectId });
|
|
/** Probe for Playwright without starting anything — used to re-check after installing it. */
|
|
export const checkBrowserViewSupport = (projectId: string) =>
|
|
invoke<PlaywrightDetection>("check_browser_view_support", { projectId });
|
|
/**
|
|
* Install `playwright` + `@playwright/cli` into the container's `/workspace`.
|
|
*
|
|
* A container mutation, so it only ever runs from an explicit click. Progress
|
|
* streams on the existing `container-progress` event; the result carries a
|
|
* fresh probe. Browsers are a separate action — see below.
|
|
*/
|
|
export const installBrowserViewSupport = (projectId: string) =>
|
|
invoke<BrowserSetupOutcome>("install_browser_view_support", { projectId });
|
|
/**
|
|
* Install a browser and the apt libraries it needs, then verify it launches.
|
|
* `chromium` is Playwright's own build; `chrome` is the channel
|
|
* `@playwright/mcp` asks for. Hundreds of MB — never call this implicitly.
|
|
*/
|
|
export const installBrowserViewBrowser = (
|
|
projectId: string,
|
|
browser: BrowserInstallTarget,
|
|
) => invoke<BrowserSetupOutcome>("install_browser_view_browser", { projectId, browser });
|
|
|
|
/**
|
|
* Detach the live view into its own OS window, or raise it if already open.
|
|
*
|
|
* Window-only: the viewer, the proxy and the container are untouched, so
|
|
* popping out and back costs nothing. The window loads the same token-bearing
|
|
* loopback URL as the pane, and has no IPC access.
|
|
*/
|
|
export const openBrowserViewPopout = (projectId: string, alwaysOnTop: boolean) =>
|
|
invoke<void>("open_browser_view_popout", { projectId, alwaysOnTop });
|
|
/** Close the pop-out and put the view back in the tab. No-op if it isn't open. */
|
|
export const closeBrowserViewPopout = (projectId: string) =>
|
|
invoke<void>("close_browser_view_popout", { projectId });
|
|
/**
|
|
* Whether the pop-out is open and whether it is pinned, read from the window.
|
|
*
|
|
* Asked on every mount: the pane is unmounted whenever another Project Home
|
|
* sub-tab is selected, while the window carries on — so neither fact can live
|
|
* in component state and survive.
|
|
*/
|
|
export const getBrowserViewPopoutState = (projectId: string) =>
|
|
invoke<BrowserViewPopoutState>("get_browser_view_popout_state", { projectId });
|
|
/**
|
|
* Open a URL in a browser *inside* the container, published so the pane shows it.
|
|
*
|
|
* The same action serves an auth URL — the OAuth callback listener is in the
|
|
* container too, so the loop closes without the host — and a dev server on
|
|
* container loopback, which is how you watch a UI Claude is building. Only
|
|
* http/https; the backend rejects anything else.
|
|
*/
|
|
export const openPageInContainerBrowser = (
|
|
projectId: string,
|
|
url: string,
|
|
width: number,
|
|
height: number,
|
|
) => invoke<BrowserPageState>("open_page_in_container_browser", { projectId, url, width, height });
|
|
/** Resize that page. Real reflow, not a scaled screencast — see BrowserTab. */
|
|
export const setContainerPageViewport = (projectId: string, width: number, height: number) =>
|
|
invoke<void>("set_container_page_viewport", { projectId, width, height });
|
|
export const getContainerPageState = (projectId: string) =>
|
|
invoke<BrowserPageState>("get_container_page_state", { projectId });
|
|
export const closeContainerPage = (projectId: string) =>
|
|
invoke<void>("close_container_page", { projectId });
|
|
|
|
/**
|
|
* Make the page track the pop-out window's size as it is dragged.
|
|
*
|
|
* Only affects a page this app opened: a bound browser admits no second client,
|
|
* so one `@playwright/mcp` launched keeps the viewport it was given.
|
|
*/
|
|
export const setBrowserViewMatchWindow = (projectId: string, enabled: boolean) =>
|
|
invoke<void>("set_browser_view_match_window", { projectId, enabled });
|
|
export const getBrowserViewMatchWindow = (projectId: string) =>
|
|
invoke<boolean>("get_browser_view_match_window", { projectId });
|
|
|
|
/** Pin the pop-out above other windows — the point of popping it out at all. */
|
|
export const setBrowserViewPopoutAlwaysOnTop = (projectId: string, onTop: boolean) =>
|
|
invoke<void>("set_browser_view_popout_always_on_top", { projectId, onTop });
|
|
|
|
// Shared Claude Code auth token — one `claude setup-token` run authenticates
|
|
// every Anthropic-backend project. The token itself is never exposed here: it
|
|
// lives in the OS keychain and is injected as a container env var.
|
|
//
|
|
// `acquireClaudeToken` borrows the given project's running container to run the
|
|
// login (temporarily enabling its auth bridge), and streams progress on the
|
|
// `claude-token-progress` and `claude-token-output` events. It resolves only
|
|
// once the whole flow finishes, so call it without awaiting the UI on it.
|
|
//
|
|
// Partway through, `claude setup-token` prints a sign-in URL and then waits at
|
|
// a "Paste code here" prompt: the user signs in, copies the code shown by
|
|
// Anthropic, and it is delivered with `submitClaudeTokenCode`. One flow at a
|
|
// time — a second `acquireClaudeToken` call rejects while one is in progress.
|
|
export const acquireClaudeToken = (projectId: string) =>
|
|
invoke<void>("acquire_claude_token", { projectId });
|
|
export const submitClaudeTokenCode = (code: string) =>
|
|
invoke<void>("submit_claude_token_code", { code });
|
|
/** Abort an in-flight acquisition and release the single-flight guard. No-op if nothing is running. */
|
|
export const cancelClaudeToken = () => invoke<void>("cancel_claude_token");
|
|
export const hasClaudeToken = () => invoke<boolean>("has_claude_token");
|
|
/** Revoke the shared token. Also rewrites any snapshot image that still has it
|
|
* baked into its env — see `ClearTokenOutcome` for what may be left behind. */
|
|
export const clearClaudeToken = () =>
|
|
invoke<ClearTokenOutcome>("clear_claude_token");
|
|
|
|
// Container base-image migration — move a project onto the current base image
|
|
// without deleting its volumes. Reset is the destructive alternative: it wipes
|
|
// ~/.claude, the OAuth credential, installed skills and every transcript.
|
|
//
|
|
// Flow: getContainerStaleness (read-only, ~6s — two filesystem probes, so call
|
|
// it on demand rather than polling) → migrateProjectToBase → the project sits
|
|
// in "awaiting-confirmation" while the user tries it → confirmMigration or
|
|
// rollbackMigration.
|
|
//
|
|
// Rollback restores the **system layer only**. Both named volumes are untouched
|
|
// throughout, so anything written to $HOME during the migrated session — a new
|
|
// login, new skills, new transcripts — survives a rollback.
|
|
//
|
|
// Progress arrives on the existing `container-progress` event.
|
|
|
|
/** Read-only. Runs two container/image filesystem probes; not for polling. */
|
|
export const getContainerStaleness = (projectId: string) =>
|
|
invoke<ContainerStaleness>("get_container_staleness", { projectId });
|
|
|
|
/** Runs the whole migration and resolves with its report. Long-running — the
|
|
* apt replay alone was measured at ~70s for 8 packages. Calling it again while
|
|
* a migration is `interrupted` resumes that one instead of starting a new one. */
|
|
export const migrateProjectToBase = (projectId: string, options: MigrationOptions) =>
|
|
invoke<MigrationReport>("migrate_project_to_base", { projectId, options });
|
|
|
|
/** Accept the migration: drops the rollback tag and the staged payload, and
|
|
* clears the record. Idempotent. */
|
|
export const confirmMigration = (projectId: string) =>
|
|
invoke<void>("confirm_migration", { projectId });
|
|
|
|
/** Undo the migration: recreates the container from its pre-migration image.
|
|
* Fails if the migration kept no rollback image (`keep_rollback: false`). */
|
|
export const rollbackMigration = (projectId: string) =>
|
|
invoke<void>("rollback_migration", { projectId });
|
|
|
|
/** The persisted record, or null when no migration is in flight. Worth calling
|
|
* after `reconcileProjectStatuses` at startup: a migration interrupted by an
|
|
* app crash shows up here as phase "interrupted". */
|
|
export const getMigrationState = (projectId: string) =>
|
|
invoke<MigrationState | null>("get_migration_state", { projectId });
|