diff --git a/v2/src/lib/adapters/agent-bridge.ts b/v2/src/lib/adapters/agent-bridge.ts index 07ed88c..c261bef 100644 --- a/v2/src/lib/adapters/agent-bridge.ts +++ b/v2/src/lib/adapters/agent-bridge.ts @@ -11,13 +11,21 @@ export interface AgentQueryOptions { max_turns?: number; max_budget_usd?: number; resume_session_id?: string; + remote_machine_id?: string; } export async function queryAgent(options: AgentQueryOptions): Promise { + if (options.remote_machine_id) { + const { remote_machine_id: machineId, ...agentOptions } = options; + return invoke('remote_agent_query', { machineId, options: agentOptions }); + } return invoke('agent_query', { options }); } -export async function stopAgent(sessionId: string): Promise { +export async function stopAgent(sessionId: string, remoteMachineId?: string): Promise { + if (remoteMachineId) { + return invoke('remote_agent_stop', { machineId: remoteMachineId, sessionId }); + } return invoke('agent_stop', { sessionId }); } diff --git a/v2/src/lib/adapters/pty-bridge.ts b/v2/src/lib/adapters/pty-bridge.ts index 2888919..1018c37 100644 --- a/v2/src/lib/adapters/pty-bridge.ts +++ b/v2/src/lib/adapters/pty-bridge.ts @@ -7,21 +7,35 @@ export interface PtyOptions { args?: string[]; cols?: number; rows?: number; + remote_machine_id?: string; } export async function spawnPty(options: PtyOptions): Promise { + if (options.remote_machine_id) { + const { remote_machine_id: machineId, ...ptyOptions } = options; + return invoke('remote_pty_spawn', { machineId, options: ptyOptions }); + } return invoke('pty_spawn', { options }); } -export async function writePty(id: string, data: string): Promise { +export async function writePty(id: string, data: string, remoteMachineId?: string): Promise { + if (remoteMachineId) { + return invoke('remote_pty_write', { machineId: remoteMachineId, id, data }); + } return invoke('pty_write', { id, data }); } -export async function resizePty(id: string, cols: number, rows: number): Promise { +export async function resizePty(id: string, cols: number, rows: number, remoteMachineId?: string): Promise { + if (remoteMachineId) { + return invoke('remote_pty_resize', { machineId: remoteMachineId, id, cols, rows }); + } return invoke('pty_resize', { id, cols, rows }); } -export async function killPty(id: string): Promise { +export async function killPty(id: string, remoteMachineId?: string): Promise { + if (remoteMachineId) { + return invoke('remote_pty_kill', { machineId: remoteMachineId, id }); + } return invoke('pty_kill', { id }); } diff --git a/v2/src/lib/adapters/remote-bridge.ts b/v2/src/lib/adapters/remote-bridge.ts new file mode 100644 index 0000000..79c1ca6 --- /dev/null +++ b/v2/src/lib/adapters/remote-bridge.ts @@ -0,0 +1,122 @@ +// Remote Machine Bridge — Tauri IPC adapter for multi-machine management + +import { invoke } from '@tauri-apps/api/core'; +import { listen, type UnlistenFn } from '@tauri-apps/api/event'; + +export interface RemoteMachineConfig { + label: string; + url: string; + token: string; + auto_connect: boolean; +} + +export interface RemoteMachineInfo { + id: string; + label: string; + url: string; + status: string; + auto_connect: boolean; +} + +// --- Machine management --- + +export async function listRemoteMachines(): Promise { + return invoke('remote_list'); +} + +export async function addRemoteMachine(config: RemoteMachineConfig): Promise { + return invoke('remote_add', { config }); +} + +export async function removeRemoteMachine(machineId: string): Promise { + return invoke('remote_remove', { machineId }); +} + +export async function connectRemoteMachine(machineId: string): Promise { + return invoke('remote_connect', { machineId }); +} + +export async function disconnectRemoteMachine(machineId: string): Promise { + return invoke('remote_disconnect', { machineId }); +} + +// --- Remote event listeners --- + +export interface RemoteSidecarMessage { + machineId: string; + sessionId?: string; + event?: Record; +} + +export interface RemotePtyData { + machineId: string; + sessionId?: string; + data?: string; +} + +export interface RemotePtyExit { + machineId: string; + sessionId?: string; +} + +export interface RemoteMachineEvent { + machineId: string; + payload?: unknown; + error?: unknown; +} + +export async function onRemoteSidecarMessage( + callback: (msg: RemoteSidecarMessage) => void, +): Promise { + return listen('remote-sidecar-message', (event) => { + callback(event.payload); + }); +} + +export async function onRemotePtyData( + callback: (msg: RemotePtyData) => void, +): Promise { + return listen('remote-pty-data', (event) => { + callback(event.payload); + }); +} + +export async function onRemotePtyExit( + callback: (msg: RemotePtyExit) => void, +): Promise { + return listen('remote-pty-exit', (event) => { + callback(event.payload); + }); +} + +export async function onRemoteMachineReady( + callback: (msg: RemoteMachineEvent) => void, +): Promise { + return listen('remote-machine-ready', (event) => { + callback(event.payload); + }); +} + +export async function onRemoteMachineDisconnected( + callback: (msg: RemoteMachineEvent) => void, +): Promise { + return listen('remote-machine-disconnected', (event) => { + callback(event.payload); + }); +} + +export async function onRemoteStateSync( + callback: (msg: RemoteMachineEvent) => void, +): Promise { + return listen('remote-state-sync', (event) => { + callback(event.payload); + }); +} + +export async function onRemoteError( + callback: (msg: RemoteMachineEvent) => void, +): Promise { + return listen('remote-error', (event) => { + callback(event.payload); + }); +} diff --git a/v2/src/lib/components/Settings/SettingsDialog.svelte b/v2/src/lib/components/Settings/SettingsDialog.svelte index e7ec34c..e50b5b3 100644 --- a/v2/src/lib/components/Settings/SettingsDialog.svelte +++ b/v2/src/lib/components/Settings/SettingsDialog.svelte @@ -4,6 +4,14 @@ import { notify } from '../../stores/notifications.svelte'; import { getCurrentFlavor, setFlavor } from '../../stores/theme.svelte'; import { ALL_FLAVORS, FLAVOR_LABELS, type CatppuccinFlavor } from '../../styles/themes'; + import { + getMachines, + addMachine, + removeMachine, + connectMachine, + disconnectMachine, + loadMachines, + } from '../../stores/machines.svelte'; interface Props { open: boolean; @@ -17,12 +25,21 @@ let maxPanes = $state('4'); let themeFlavor = $state('mocha'); + // Machine form state + let newMachineLabel = $state(''); + let newMachineUrl = $state(''); + let newMachineToken = $state(''); + let newMachineAutoConnect = $state(false); + + let remoteMachines = $derived(getMachines()); + onMount(async () => { try { defaultShell = (await getSetting('default_shell')) ?? ''; defaultCwd = (await getSetting('default_cwd')) ?? ''; maxPanes = (await getSetting('max_panes')) ?? '4'; themeFlavor = getCurrentFlavor(); + await loadMachines(); } catch { // Use defaults } @@ -41,6 +58,49 @@ } } + async function handleAddMachine() { + if (!newMachineLabel || !newMachineUrl || !newMachineToken) { + notify('error', 'Label, URL, and token are required'); + return; + } + try { + await addMachine({ + label: newMachineLabel, + url: newMachineUrl, + token: newMachineToken, + auto_connect: newMachineAutoConnect, + }); + newMachineLabel = ''; + newMachineUrl = ''; + newMachineToken = ''; + newMachineAutoConnect = false; + notify('success', 'Machine added'); + } catch (e) { + notify('error', `Failed to add machine: ${e}`); + } + } + + async function handleRemoveMachine(id: string) { + try { + await removeMachine(id); + notify('success', 'Machine removed'); + } catch (e) { + notify('error', `Failed to remove machine: ${e}`); + } + } + + async function handleToggleConnection(id: string, status: string) { + try { + if (status === 'connected') { + await disconnectMachine(id); + } else { + await connectMachine(id); + } + } catch (e) { + notify('error', `Connection error: ${e}`); + } + } + function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape') onClose(); } @@ -81,6 +141,58 @@ Catppuccin color scheme. New terminals use the updated theme. + +
+

Remote Machines

+ + {#if remoteMachines.length > 0} +
+ {#each remoteMachines as machine (machine.id)} +
+
+ {machine.label} + {machine.url} + + {machine.status} + +
+
+ + +
+
+ {/each} +
+ {:else} +

No remote machines configured.

+ {/if} + +
+ + + + + +