mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 08:02:02 +00:00
The phone can now hand a pane a photo or file, read what an agent changed, answer its prompts with a tap, and keep its link across leaving a pane. Gateway and protocol: - `Open::Upload` puts a file (up to 20 MB) in the directory the desktop keeps pasted images in, under a name a shell takes unquoted, and answers with its path. Panes on this machine only for now. - `Open::Diff` answers with the working tree's changes against HEAD and its untracked files, for the directory a pane is in. - Both are one-shot streams; an older gateway drops them unanswered, which the client reports as "update tty7". App: - Attach button in the message box: the uploaded path goes into the draft. - Changes sheet from a pane's menu: a block per file, lines coloured. - Settings → Key bar: remove, reorder and add keys, from a catalog or written out (`/compact\r`, `^C`), across pages; reset to the default. - An agent's numbered choices show as buttons while it waits. - Pinch to zoom, tap a link to open it, find in the scrollback, a Copy button for text a program copies (OSC 52), and an optional Face ID lock. - Swipe from the left edge to go back; the iOS WebView has no page stack. - One workspace at a time on a machine, picked from a row of chips. - Agent avatars in their own colours. - Each screen's watch is its own: a second watch no longer ends the first, which showed as "Can't reach" and a reconnect after leaving a pane. - Dialing no longer holds the session lock, so Forget works while a machine is still connecting; its confirmation is an in-app sheet, since `window.confirm` shows nothing in the iOS WebView. - iOS 15 minimum, as App Store Connect will require from April 2027. - `scripts/testflight.sh` archives, signs for the App Store at export (the team has no devices for a development profile) and uploads. Co-authored-by: Claude <noreply@anthropic.com>
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
// The frontend's view of the Rust side: typed wrappers over its commands, and
|
|
// the shapes that cross the bridge. The wire types mirror tty7-mobile-proto;
|
|
// its tests pin the tags these match on.
|
|
|
|
import { Channel, invoke } from "@tauri-apps/api/core";
|
|
|
|
export interface Host {
|
|
id: string;
|
|
name: string;
|
|
relay?: string | null;
|
|
addrs: string[];
|
|
}
|
|
|
|
export type AgentStatus = "idle" | "working" | "waiting" | "done";
|
|
|
|
export interface AgentView {
|
|
kind: string;
|
|
status: AgentStatus;
|
|
message?: string | null;
|
|
}
|
|
|
|
export interface PaneView {
|
|
id: number;
|
|
title: string;
|
|
cwd?: string | null;
|
|
agent?: AgentView | null;
|
|
}
|
|
|
|
export interface TabView {
|
|
id: string;
|
|
name: string;
|
|
hibernated: boolean;
|
|
panes: PaneView[];
|
|
}
|
|
|
|
export interface WorkspaceView {
|
|
id: string;
|
|
name: string;
|
|
tabs: TabView[];
|
|
}
|
|
|
|
export interface Tree {
|
|
host: string;
|
|
workspaces: WorkspaceView[];
|
|
/** Machines the desktop is linked to over SSH. Absent from older gateways. */
|
|
remotes?: RemoteView[];
|
|
}
|
|
|
|
export interface RemoteView {
|
|
key: string;
|
|
name: string;
|
|
connected: boolean;
|
|
error?: string | null;
|
|
/** Up, but its first read has not come back: workspaces unknown, not absent. */
|
|
pending?: boolean;
|
|
workspaces: WorkspaceView[];
|
|
}
|
|
|
|
export interface LinkInfo {
|
|
path: "direct" | "relay" | "connecting";
|
|
rtt_ms: number;
|
|
}
|
|
|
|
export type TreeMsg =
|
|
| { type: "tree"; tree: Tree }
|
|
| { type: "link"; link: LinkInfo }
|
|
| { type: "error"; message: string }
|
|
| { type: "closed" };
|
|
|
|
export type PaneEvent =
|
|
| { type: "size"; cols: number; rows: number }
|
|
| { type: "cwd"; path: string }
|
|
| { type: "agent"; agent: AgentView | null }
|
|
| { type: "exited"; code: number | null }
|
|
| { type: "error"; message: string }
|
|
/** Whether the pane runs at this phone's size; `refused` says why not. */
|
|
| { type: "lease"; held: boolean; refused?: string | null };
|
|
|
|
export const hosts = () => invoke<Host[]>("hosts");
|
|
|
|
export const pair = (code: string, deviceName: string) =>
|
|
invoke<Host>("pair", { code, deviceName });
|
|
|
|
export const forget = (hostId: string) => invoke<void>("forget", { hostId });
|
|
|
|
/** Sets the style of what the page does not draw: status bar, keyboard. */
|
|
export const appearance = (style: "system" | "light" | "dark") => invoke<void>("appearance", { style });
|
|
|
|
export function watch(hostId: string, onMsg: (msg: TreeMsg) => void) {
|
|
const onEvent = new Channel<TreeMsg>();
|
|
onEvent.onmessage = onMsg;
|
|
return invoke<number>("watch", { hostId, onEvent });
|
|
}
|
|
|
|
/** Ends a watch by the id `watch` resolved with. */
|
|
export const unwatch = (watch: number) => invoke<void>("unwatch", { watch });
|
|
|
|
export const refresh = (watch: number) => invoke<void>("refresh", { watch });
|
|
|
|
/** Output arrives as ArrayBuffers of raw terminal bytes, events as objects. */
|
|
/** `machine` is a remote's key, or null for the paired machine itself. */
|
|
export function paneOpen(
|
|
hostId: string,
|
|
machine: string | null,
|
|
paneId: number,
|
|
onOutput: (bytes: Uint8Array) => void,
|
|
onEvent: (event: PaneEvent) => void,
|
|
) {
|
|
const channel = new Channel<ArrayBuffer | PaneEvent>();
|
|
channel.onmessage = (msg) => {
|
|
if (msg instanceof ArrayBuffer) onOutput(new Uint8Array(msg));
|
|
else onEvent(msg);
|
|
};
|
|
return invoke<number>("pane_open", { hostId, machine, paneId, onOutput: channel });
|
|
}
|
|
|
|
export interface TabCreated {
|
|
tab_id: string;
|
|
pane_id: number;
|
|
}
|
|
|
|
export const tabNew = (
|
|
hostId: string,
|
|
machine: string | null,
|
|
workspaceId: string,
|
|
cwd: string | null,
|
|
size: { cols: number; rows: number } | null,
|
|
) => invoke<TabCreated>("tab_new", { hostId, machine, workspaceId, cwd, size });
|
|
|
|
export const paneInput = (handle: number, data: string) =>
|
|
invoke<void>("pane_input", { handle, data });
|
|
|
|
/** Run the pane at `size`, the phone's grid; `null` gives it back. */
|
|
export const paneLease = (handle: number, size: { cols: number; rows: number } | null) =>
|
|
invoke<void>("pane_lease", { handle, size });
|
|
|
|
/** The largest file the gateway takes (`tty7_mobile_proto::MAX_UPLOAD`),
|
|
* checked here too so a big video is refused before it is read in. */
|
|
export const MAX_UPLOAD = 20 * 2 ** 20;
|
|
|
|
/** Sends a file to the machine a pane runs on; resolves with its path there.
|
|
* The bytes go as the raw body; the rest rides in headers, encoded, since a
|
|
* header is ASCII and a file name need not be. */
|
|
export const upload = (hostId: string, machine: string | null, name: string, bytes: Uint8Array) =>
|
|
invoke<string>("upload", bytes, {
|
|
headers: {
|
|
"x-host": encodeURIComponent(hostId),
|
|
"x-name": encodeURIComponent(name),
|
|
"x-machine": encodeURIComponent(machine ?? ""),
|
|
},
|
|
});
|
|
|
|
export interface Diff {
|
|
root: string;
|
|
patch: string;
|
|
untracked: string[];
|
|
truncated: boolean;
|
|
}
|
|
|
|
/** What has changed in the repository `cwd` is in. */
|
|
export const diff = (hostId: string, machine: string | null, cwd: string) =>
|
|
invoke<Diff>("diff", { hostId, machine, cwd });
|
|
|
|
export const paneClose = (handle: number) => invoke<void>("pane_close", { handle });
|