This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/types.ts
T
Miguel Palhas 1bb464be4b refactor(maestro): flatten web UI to a single host
Session tree nests by working directory only; the daemon chips in the
header become a session tally, and the spawn form loses its host picker.
API calls move from /api/daemons/:id/* to /api/dirs, /api/recent-dirs,
/api/gitroot, and /api/info.
2026-08-22 10:14:10 +01:00

162 lines
5.4 KiB
TypeScript

// Mirror of `maestro-protocol` JSON shapes. Keep in sync with
// crates/maestro-protocol/src/lib.rs.
export type Uuid = string;
export type IsoTimestamp = string;
export type SessionStatus = "Running" | "Dead";
/** Daemon-inferred activity dimension, orthogonal to `SessionStatus` (NAP-340).
* Absent on payloads from daemons that predate the field. */
export type ActivityState = "Active" | "Idle" | "AwaitingInput";
export interface Session {
id: Uuid;
cwd: string;
command: string;
env: Record<string, string>;
metadata: Record<string, unknown>;
status: SessionStatus;
created_at: IsoTimestamp;
/** Current foreground command in the pane (e.g. `claude`, `bash`), as the
* daemon's activity watcher observed it. Absent until observed (NAP-340). */
foreground?: string | null;
/** Inferred activity. Defaults to `Idle` on the wire when omitted. */
activity?: ActivityState;
/** Wall-clock of the most recent observed PTY output (NAP-340). */
last_activity_at?: IsoTimestamp;
/** The pane's *current* working directory, as the daemon's activity watcher
* observed it (`#{pane_current_path}`) — distinct from `cwd` (the spawn dir).
* Drives the directory-based session tree. Absent until first observed. */
current_cwd?: string | null;
}
/** One directory's immediate subdirectories (NAP-329). */
export interface DirListing {
/** Resolved absolute directory that was listed (after ~/relative expansion). */
path: string;
/** Immediate subdirectory names, sorted case-insensitively. */
entries: string[];
/** True when the listing was capped server-side. */
truncated?: boolean;
}
/** A spawn-form command preset advertised by the daemon (#22/#26). The `custom`
* preset has an empty `command` and reveals the free-text command input. */
export interface SpawnPreset {
id: string;
label: string;
command: string;
}
/** `GET /api/info` — the daemon's identity plus its spawn presets (#26).
* Mirrors the daemon-side `DaemonInfo`. */
export interface DaemonInfo {
hostname: string;
version: string;
/** Home dir of the user the daemon runs as; used to prefill the spawn form's
* working directory. Empty/absent for older daemons. */
home_dir?: string;
/** Command presets (claude/opencode/zsh + custom), config-resolved. */
presets: SpawnPreset[];
}
/** One deduplicated recent working dir for the spawn form's recents picker
* (#24/#26). */
export interface RecentDir {
cwd: string;
last_command: string;
last_used: IsoTimestamp;
}
/** `GET /api/recent-dirs` — the daemon's recent working dirs (#26). */
export interface RecentDirs {
dirs: RecentDir[];
truncated?: boolean;
}
/** `GET /api/gitroot?path=` — the enclosing repo toplevel of a path, or absent
* when the path isn't inside a repo (#26). */
export interface GitRoot {
root?: string;
}
export interface WorktreeSpec {
base_repo: string;
branch: string;
cleanup_on_kill?: boolean;
}
export interface CreateSessionRequest {
cwd: string;
command: string;
env?: Record<string, string>;
metadata?: Record<string, unknown>;
worktree?: WorktreeSpec;
}
export type CatalogEvent =
| { type: "Snapshot"; sessions: Session[] }
| { type: "SessionAdded"; session: Session }
| { type: "SessionRemoved"; id: Uuid }
| { type: "SessionUpdated"; session: Session }
| { type: "Lagged"; skipped: number };
export type ClientControl =
| { type: "Resize"; cols: number; rows: number }
| { type: "Ping" }
| { type: "Pong" }
| { type: "Error"; code: string; message: string };
export interface ApiError {
error: { code: string; message: string };
}
export function sessionName(s: Session): string {
const meta = s.metadata as Record<string, unknown>;
const name = meta?.name;
if (typeof name === "string" && name.length > 0) return name;
return s.command;
}
/** The directory a session lives in: its live `current_cwd` if the daemon has
* observed one, else the spawn `cwd`. Drives the directory-based session tree. */
export function sessionCwd(s: Session): string | undefined {
if (typeof s.current_cwd === "string" && s.current_cwd) return s.current_cwd;
return s.cwd || undefined;
}
/** Collapse a daemon's home dir to `~`. `home` comes from `DaemonInfo.home_dir`;
* absent/empty leaves the path untouched. Mirrors the CLI's `abbreviate_dir`. */
export function abbreviateCwd(cwd: string, home?: string): string {
if (home && (cwd === home || cwd.startsWith(`${home}/`))) {
return `~${cwd.slice(home.length)}`;
}
return cwd;
}
/** `~/git.naps.pt/naps62/maestro` → `~/g/n/maestro`: keep the last segment
* verbatim, collapse every earlier one to its first character. Two-or-fewer
* segments are left alone (nothing to gain). Returns escaped HTML with the
* collapsed stem wrapped in `.folder-stem` so callers' CSS can dim it. Shared
* by the sidebar tree and the spawn dialog's recent-directory picker so both
* abbreviate paths identically. */
export function abbreviateFolderName(name: string): string {
const parts = name.split("/");
if (parts.length <= 2) return escapeHtml(name);
const last = parts[parts.length - 1];
const stem = parts
.slice(0, -1)
.map((p) => (p === "~" ? "~" : Array.from(p)[0] ?? ""))
.join("/");
return `<span class="folder-stem">${escapeHtml(stem)}/</span>${escapeHtml(last)}`;
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}