367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
// The JSON the watch app reads and sends. This is the contract between the gateway and the
|
|
// watch's Codable models (watch/WristCode/API/Models.swift). docs/api.md describes it for humans.
|
|
// Change both sides together.
|
|
|
|
import type { Phase } from "./awareness.ts";
|
|
import type { ApprovalDecision } from "./t3/types.ts";
|
|
import type { HostStatus } from "./t3/host.ts";
|
|
|
|
export interface HostSummary {
|
|
id: string;
|
|
label: string;
|
|
status: HostStatus;
|
|
error: string | null;
|
|
}
|
|
|
|
/**
|
|
* How a route reaches its host, guessed from the URL: `public` for HTTPS, `vpn` when the hostname
|
|
* says vpn, and `direct` for the rest.
|
|
*/
|
|
export type RouteKind = "direct" | "vpn" | "public";
|
|
|
|
export interface Route {
|
|
url: string;
|
|
kind: RouteKind;
|
|
/** From the latest connect. `untried` when an earlier route answered first. */
|
|
result: "ok" | "failed" | "untried";
|
|
/** Why it failed, such as `refused` or `no answer in 5 s`. */
|
|
error: string | null;
|
|
checkedAt: string | null;
|
|
}
|
|
|
|
/** One host as the status screens show it. */
|
|
export interface HostStatusDetail extends HostSummary {
|
|
/** The route in use. Null unless online. */
|
|
activeUrl: string | null;
|
|
/** Preferred first, the order the gateway tries them. */
|
|
routes: Route[];
|
|
onlineSince: string | null;
|
|
lastOnlineAt: string | null;
|
|
/** When the gateway tries again, while it waits to retry. */
|
|
nextRetryAt: string | null;
|
|
tokenExpiresAt: string | null;
|
|
serverVersion: string | null;
|
|
}
|
|
|
|
/** What Apple answered for one push. */
|
|
export interface PushResult {
|
|
ok: boolean;
|
|
at: string;
|
|
/** Apple's HTTP status, or 0 when the push never reached Apple. */
|
|
status: number;
|
|
/** Apple's reason, such as `BadDeviceToken`, or why the gateway could not send. */
|
|
reason: string | null;
|
|
}
|
|
|
|
export interface StatusResponse {
|
|
hosts: HostStatusDetail[];
|
|
gateway: {
|
|
startedAt: string;
|
|
/** False when the gateway has no APNs key, so nothing can be pushed. */
|
|
pushConfigured: boolean;
|
|
lastPush: PushResult | null;
|
|
};
|
|
/** The watch asking. */
|
|
device: {
|
|
name: string;
|
|
pairedAt: string;
|
|
/** True once the watch has sent its APNs token. */
|
|
pushRegistered: boolean;
|
|
apnsEnvironment: "sandbox" | "production" | null;
|
|
notifications: NotificationPrefs;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Which pushes a watch wants. `needsMe` covers approvals and questions on your own threads, and
|
|
* `subagents` covers the same from subagents. Host problems and test pushes always go out.
|
|
*/
|
|
export interface NotificationPrefs {
|
|
needsMe: boolean;
|
|
finished: boolean;
|
|
failed: boolean;
|
|
subagents: boolean;
|
|
}
|
|
|
|
/** Something a thread is waiting on: an approval, or questions for the user. */
|
|
export interface PendingRequest {
|
|
id: string;
|
|
/** `command`, `file-change`, `permission`, `user_input`, ... */
|
|
kind: string;
|
|
/** What the agent wants to do, such as the command line. Null when the server gave none. */
|
|
prompt: string | null;
|
|
reason: string | null;
|
|
/** Approval choices, in the server's order. Empty for questions. `warning` flags a risky choice. */
|
|
options: Array<{ decision: ApprovalDecision; label: string; warning?: string }>;
|
|
/**
|
|
* On `file-change` approvals: the files the agent wants to change. Counts and preview are null
|
|
* and empty when the server sends no diff before approval, which is the usual case.
|
|
*/
|
|
files: PendingFile[];
|
|
/** Questions for the user. Empty for approvals. */
|
|
questions: Array<{
|
|
id: string;
|
|
header: string;
|
|
question: string;
|
|
options: Array<{ label: string; value: string }>;
|
|
allowCustomAnswer: boolean;
|
|
/** When true, answer with an array of option values. */
|
|
multiSelect: boolean;
|
|
}>;
|
|
}
|
|
|
|
export interface PendingFile {
|
|
path: string;
|
|
name: string;
|
|
additions: number | null;
|
|
deletions: number | null;
|
|
/** The first few changed lines, each starting with `+` or `-`. */
|
|
preview: string[];
|
|
}
|
|
|
|
export interface FeedItem {
|
|
hostId: string;
|
|
hostLabel: string;
|
|
threadId: string;
|
|
projectId: string;
|
|
projectTitle: string;
|
|
title: string;
|
|
phase: Phase;
|
|
updatedAt: string;
|
|
/** The agent's latest message, trimmed for the watch. Null until the gateway has read it. */
|
|
lastMessage: string | null;
|
|
/** What a working thread is doing right now. Null when nothing is in progress or not read yet. */
|
|
activity: Activity | null;
|
|
/** What the latest run changed, in total. Null when it changed no files or is not read yet. */
|
|
changes: ChangeTotals | null;
|
|
/** When a snoozed thread comes back to Recent. Null when it is not snoozed. */
|
|
snoozedUntil: string | null;
|
|
request: PendingRequest | null;
|
|
/**
|
|
* Set when `request` comes from a subagent this thread delegated to, rather than the thread
|
|
* itself. Answer the request on that thread. `phase` is then the subagent's.
|
|
*/
|
|
via: { hostId: string; threadId: string; title: string } | null;
|
|
}
|
|
|
|
/** One step a thread is taking now. */
|
|
export interface Activity {
|
|
kind: "command" | "edit" | "search" | "web" | "subagent" | "thinking" | "tool";
|
|
/** The command line, file name, search, subagent title or tool name, trimmed. */
|
|
text: string;
|
|
/** When the step started, for its elapsed time. */
|
|
startedAt: string | null;
|
|
}
|
|
|
|
export interface ChangeTotals {
|
|
files: number;
|
|
additions: number;
|
|
deletions: number;
|
|
}
|
|
|
|
/** A file the latest run changed. `name` is the last part of `path`. */
|
|
export interface ChangedFile {
|
|
path: string;
|
|
name: string;
|
|
additions: number;
|
|
deletions: number;
|
|
}
|
|
|
|
/** A thread search hit: the thread, and where the words matched when it was inside a message. */
|
|
export interface SearchHit {
|
|
thread: ThreadRow;
|
|
projectTitle: string;
|
|
hostLabel: string;
|
|
/** Text around the match in a message, or null when only the title matched. */
|
|
snippet: string | null;
|
|
}
|
|
|
|
export interface SearchResponse {
|
|
hits: SearchHit[];
|
|
/** Labels of hosts that were offline or failed, so their threads were not searched. */
|
|
unsearched: string[];
|
|
}
|
|
|
|
/** A thread in a list: a project's threads, or a thread's subagents. */
|
|
export interface ThreadRow {
|
|
hostId: string;
|
|
threadId: string;
|
|
title: string;
|
|
/** Null for a thread that never ran or was stopped. */
|
|
phase: Phase | null;
|
|
settled: boolean;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface ThreadListResponse {
|
|
items: ThreadRow[];
|
|
}
|
|
|
|
export interface HistoryResponse {
|
|
/** Oldest first, just before the messages the watch already has. */
|
|
messages: Message[];
|
|
/** Pass to the history route for the page before these. Null at the start of the thread. */
|
|
earlierCursor: string | null;
|
|
}
|
|
|
|
/** Counts for the watch face complication. Cheap: no thread pages are read. */
|
|
export interface Summary {
|
|
needsYou: number;
|
|
working: number;
|
|
/** The thread that has waited longest for you, if any. */
|
|
next: {
|
|
hostId: string;
|
|
threadId: string;
|
|
title: string;
|
|
hostLabel: string;
|
|
/** The command it wants to run or its first question, trimmed. Null until the gateway has read it. */
|
|
request: string | null;
|
|
} | null;
|
|
/** The working thread updated most recently, with what it is doing. Null when nothing is working. */
|
|
current: { hostId: string; threadId: string; title: string; activity: Activity | null } | null;
|
|
hostsOffline: number;
|
|
}
|
|
|
|
export interface Feed {
|
|
/** Waiting for approval or input. Oldest request first. */
|
|
needsYou: FeedItem[];
|
|
/** Starting or running. Most recently updated first. */
|
|
working: FeedItem[];
|
|
/** Completed or failed and not yet settled. Most recent first, at most 10. */
|
|
recent: FeedItem[];
|
|
/** How many threads Recent holds in all. More than `recent.length` means `GET /v1/recent` has the rest. */
|
|
recentTotal: number;
|
|
/** Finished threads snoozed for later, listed by `GET /v1/snoozed`. */
|
|
snoozedTotal: number;
|
|
hosts: HostSummary[];
|
|
}
|
|
|
|
export interface RecentResponse {
|
|
/** Every unsettled finished thread, most recent first. */
|
|
items: FeedItem[];
|
|
}
|
|
|
|
export interface SettleRecentResponse {
|
|
settled: number;
|
|
/** Threads T3 refused to settle, or whose host was offline. */
|
|
failed: number;
|
|
}
|
|
|
|
export interface Message {
|
|
id: string;
|
|
role: "user" | "assistant" | "system";
|
|
text: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface ThreadDetail {
|
|
item: FeedItem;
|
|
/** Oldest first, at most 20. */
|
|
messages: Message[];
|
|
/** True while a run is active, so Stop makes sense. */
|
|
canStop: boolean;
|
|
/** True when the thread runs in approval-required mode, asking before each action. */
|
|
askFirst: boolean;
|
|
/** Pass to the history route for older messages. Null when `messages` starts the thread. */
|
|
earlierCursor: string | null;
|
|
/** Subagents and delegated threads this thread started, newest first. */
|
|
subagents: ThreadRow[];
|
|
/** The files the latest run changed, most lines changed first. */
|
|
changedFiles: ChangedFile[];
|
|
}
|
|
|
|
export interface SnoozeRequest {
|
|
/** When the thread comes back, in the future and at most 30 days away. */
|
|
until: string;
|
|
}
|
|
|
|
export interface ThreadModeRequest {
|
|
askFirst: boolean;
|
|
}
|
|
|
|
export interface ProjectsResponse {
|
|
hosts: Array<
|
|
HostSummary & {
|
|
projects: Array<{
|
|
id: string;
|
|
title: string;
|
|
/** The model a new thread will use, such as `claude-opus-5-5`. Null when none is configured. */
|
|
model: string | null;
|
|
/** The provider instance `model` belongs to, matching an entry in `providers`. */
|
|
instanceId: string | null;
|
|
}>;
|
|
/** The providers on this host that can start a thread now, each with its models. */
|
|
providers: AgentProvider[];
|
|
}
|
|
>;
|
|
}
|
|
|
|
export interface AgentProvider {
|
|
instanceId: string;
|
|
/** Such as `Claude` or `Codex`. */
|
|
name: string;
|
|
/** False when the provider cannot run in approval-required mode. */
|
|
canAskFirst: boolean;
|
|
models: Array<{ slug: string; name: string }>;
|
|
}
|
|
|
|
export interface PairRequest {
|
|
code: string;
|
|
deviceName: string;
|
|
}
|
|
|
|
export interface PairResponse {
|
|
deviceId: string;
|
|
deviceToken: string;
|
|
}
|
|
|
|
export interface PushRegistration {
|
|
apnsToken: string;
|
|
apnsEnvironment: "sandbox" | "production";
|
|
}
|
|
|
|
export interface SendMessageRequest {
|
|
text: string;
|
|
}
|
|
|
|
export type RespondRequest =
|
|
| { requestId: string; decision: ApprovalDecision }
|
|
/** Per question id: an option value, a typed answer, or for multi-select questions an array of option values. */
|
|
| { requestId: string; answers: Record<string, string | string[]> };
|
|
|
|
export interface NewThreadRequest {
|
|
hostId: string;
|
|
projectId: string;
|
|
text: string;
|
|
/** A model from the host's `providers`. Absent uses the project's default. */
|
|
model?: { instanceId: string; slug: string };
|
|
/** Starts the thread in approval-required mode. Absent or false uses the host's default mode. */
|
|
askFirst?: boolean;
|
|
}
|
|
|
|
export interface NewThreadResponse {
|
|
hostId: string;
|
|
threadId: string;
|
|
}
|
|
|
|
export interface ErrorResponse {
|
|
error: string;
|
|
}
|
|
|
|
/** Custom keys on every push notification, beside `aps`. */
|
|
export interface PushPayload {
|
|
/** Absent on `test` pushes. */
|
|
hostId?: string;
|
|
/** Absent on `host` pushes, which are about a host rather than a thread. */
|
|
threadId?: string;
|
|
requestId?: string;
|
|
/**
|
|
* On `input` pushes that ask one single-choice question: its options, so the notification can
|
|
* offer each as a button. `allowCustomAnswer` adds a spoken answer.
|
|
*/
|
|
question?: { id: string; options: Array<{ label: string; value: string }>; allowCustomAnswer: boolean };
|
|
}
|
|
|
|
/** Notification categories the watch registers actions for. */
|
|
export type PushCategory = "approval" | "finished" | "failed" | "input" | "host" | "test";
|