Replace the freeform `metadata.label` session grouping with nesting derived
from each session's working directory. The daemon's activity watcher already
polls each tmux pane every tick, so it now also captures
`#{pane_current_path}` and surfaces it as a new `Session.current_cwd`
(serde-default, back-compat); `cwd` stays the spawn dir for worktree
inference, spawn-similar, and recents.
Web catalog builds the tree from `current_cwd ?? cwd`, collapses the home
dir to `~`, and folds single-child folder chains so `~/git/maestro` reads as
one node. CLI swaps its LABEL column for an abbreviated DIR; the spawn forms
(web + TUI) and `--label` flag are removed.
The pane format stays space-delimited (parser splits the first three tokens
and keeps the remainder as the path) so a cwd containing spaces survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
4.8 KiB
TypeScript
154 lines
4.8 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;
|
|
daemon_id: string;
|
|
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;
|
|
}
|
|
|
|
export interface DaemonInfo {
|
|
daemon_id: string;
|
|
hostname: string;
|
|
version: string;
|
|
daemon_url: 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;
|
|
}
|
|
|
|
/** One directory's immediate subdirectories on a daemon's host (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/web/info` — the connected daemon's identity plus its spawn presets
|
|
* (#26). Mirrors the daemon-side `WebInfo`. */
|
|
export interface WebInfo {
|
|
daemon_id: string;
|
|
hostname: string;
|
|
version: string;
|
|
daemon_url: 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/daemons/:id/recent-dirs` — a host's recent working dirs (#26). */
|
|
export interface RecentDirs {
|
|
dirs: RecentDir[];
|
|
truncated?: boolean;
|
|
}
|
|
|
|
/** `GET /api/daemons/:id/gitroot?path=` — the enclosing repo toplevel of a path
|
|
* on a host, 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 {
|
|
daemon_id: string;
|
|
cwd: string;
|
|
command: string;
|
|
env?: Record<string, string>;
|
|
metadata?: Record<string, unknown>;
|
|
worktree?: WorktreeSpec;
|
|
}
|
|
|
|
export type RegistryEvent =
|
|
| {
|
|
type: "Snapshot";
|
|
daemons: DaemonInfo[];
|
|
sessions: Session[];
|
|
}
|
|
| { type: "DaemonAdded"; daemon: DaemonInfo }
|
|
| { type: "DaemonRemoved"; daemon_id: string }
|
|
| { type: "SessionAdded"; session: Session }
|
|
| { type: "SessionRemoved"; daemon_id: string; 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;
|
|
}
|