Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

## Added

- `agent-tty dashboard`: a read-only, interactive **Session Dashboard** that lists your sessions and shows a live **Live View** of the selected one — watch what your agents are doing in their shells, e.g. in a tmux split. The Live View is produced by **Event Log Follow** (file-tail of `events.jsonl` → `libghostty-vt` `replayTo`/`snapshot`), so it reads the append-only **Event Log** as the source of truth and never queries the live host (ADR 0006). Master-detail UI with Tab-toggled focus (list select vs. Live View pan), 1:1 clip-top-left/letterbox plus a lossy block-glyph overview (`z`), an active/all scope toggle (`a`), and pin-on-exit (the watched session stays and freezes on its final screen with an exit badge). Requires the optional `libghostty-vt` renderer with no browser fallback, so `doctor` now reports a `dashboard` readiness capability. Interactive-only (no `--json`; fails fast on a non-interactive terminal); machine-readable session listing remains via `list --json`. See `docs/prd/session-dashboard/PRD.md` and ADR 0006 ([#109](https://github.com/coder/agent-tty/issues/109)).
- `inspect --json` now reports `host.cliVersion`, `host.rpcSocketPath`, `rendererRuntime.profile`, `rendererRuntime.booted`, `rendererRuntime.bootInFlight` (live mode), and `eventLogBytes` (both live and offline replay). All fields are optional schema additions; existing consumers are unaffected ([#104](https://github.com/coder/agent-tty/pull/104)).
- Canonical proof-bundle lock-down: a new `CanonicalBundleManifestSchema` requires `sha256` and `bytes` on every artifact, `npm run validate-bundle:canonical` (also wired through `mise run validate-bundles`) runs eight drift-detection rules plus catalog parity across the four canonical bundles, and the `linux-static` CI job now fails on bundle drift ([#104](https://github.com/coder/agent-tty/pull/104)).
- Hero Demo bundle (`dogfood/agent-uses-agent-tty/`) replaced with an external Outer Camera flow: VHS records real Codex (`gpt-5.5`) and Claude (`claude-opus-4-7`) TUIs while `agent-tty` produces the inner Neovim proof artifacts. A new `mise run demo:agent-uses-agent-tty` task regenerates and promotes the demo with pinned `vhs`/`ttyd`/`ffmpeg` ([#105](https://github.com/coder/agent-tty/pull/105)).
Expand Down
318 changes: 318 additions & 0 deletions aube-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@effect/rpc": "^0.74.0",
"@effect/sql": "^0.50.0",
"@types/node": "25.5.0",
"@types/react": "^19.2.16",
"oxfmt": "0.47.0",
"oxlint": "1.62.0",
"oxlint-tsgolint": "0.22.1",
Expand All @@ -84,8 +85,10 @@
"dependencies": {
"commander": "14.0.3",
"ghostty-web": "0.4.0",
"ink": "^7.0.5",
"node-pty": "1.1.0",
"playwright": "1.60.0",
"react": "^19.2.7",
"ulid": "3.0.2",
"zod": "4.3.6"
},
Expand Down
62 changes: 62 additions & 0 deletions src/cli/commands/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import process from 'node:process';

import type { CommandContext } from '../context.js';

import { ERROR_CODES, makeCliError } from '../../protocol/errors.js';
import {
assertDashboardRendererAvailable,
probeLibghosttyVt,
type LibghosttyVtProbe,
} from '../../renderer/readiness.js';
import type { DashboardScope } from '../../dashboard/sessionScope.js';

export interface DashboardAppOptions {
home: string;
scope: DashboardScope;
sessionId?: string;
}

export interface DashboardCommandOptions {
context: CommandContext;
all: boolean;
session?: string;
}

export interface DashboardCommandDependencies {
isInteractive?: () => boolean;
probeRenderer?: () => Promise<LibghosttyVtProbe>;
runApp?: (options: DashboardAppOptions) => Promise<void>;
}

function defaultIsInteractive(): boolean {
return process.stdout.isTTY && process.stdin.isTTY;
}

async function defaultRunApp(options: DashboardAppOptions): Promise<void> {
// Imported lazily so non-dashboard CLI paths never load the Ink/React runtime.
const { runDashboardApp } = await import('../../dashboard/app.js');
await runDashboardApp(options);
}

export async function runDashboardCommand(
options: DashboardCommandOptions,
dependencies: DashboardCommandDependencies = {},
): Promise<void> {
const isInteractive = dependencies.isInteractive ?? defaultIsInteractive;
if (!isInteractive()) {
throw makeCliError(ERROR_CODES.INVALID_INPUT, {
message:
'agent-tty dashboard requires an interactive terminal: stdin and stdout must both be a TTY. It is interactive-only and does not support --json or piped/CI use.',
});
}

const probe = await (dependencies.probeRenderer ?? probeLibghosttyVt)();
assertDashboardRendererAvailable(probe);

const runApp = dependencies.runApp ?? defaultRunApp;
await runApp({
home: options.context.home,
scope: options.all ? 'all' : 'active',
...(options.session === undefined ? {} : { sessionId: options.session }),
});
}
17 changes: 17 additions & 0 deletions src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type { CapabilityEntry } from '../../renderer/capabilities.js';
import { createPty } from '../../pty/createPty.js';
import { resolveDefaultPlaywrightBrowsersPath } from '../../renderer/browserPath.js';
import { discoverCapabilities } from '../../renderer/capabilities.js';
import { probeLibghosttyVt } from '../../renderer/readiness.js';
import {
artifactPath,
ensureArtifactsDir,
Expand Down Expand Up @@ -65,6 +66,7 @@ const DOCTOR_CHECK_LABELS: Readonly<Record<string, string>> = Object.freeze({
browser_launch: 'browser',
ghostty_web_available: 'ghostty-web',
screenshot_viable: 'screenshot',
libghostty_vt_available: 'libghostty-vt',
});

let doctorResourceSequence = 0;
Expand Down Expand Up @@ -760,6 +762,20 @@ async function runGhosttyWebAvailableCheck(): Promise<string> {
return 'WASM available';
}

async function runLibghosttyVtAvailableCheck(): Promise<DoctorCheckOutcome> {
const probe = await probeLibghosttyVt();
if (!probe.available) {
// libghostty-vt is an optional dependency; its absence makes the dashboard
// unavailable but does not break the rest of agent-tty, so skip (not fail).
return skipDoctorCheck(
probe.detail ??
probe.reason ??
'libghostty-vt optional renderer not installed',
);
}
return probe.detail ?? 'libghostty-vt available';
}

async function runScreenshotViabilityCheck(): Promise<string> {
const chromium = await getPlaywrightChromium();
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'agent-tty-doctor-'));
Expand Down Expand Up @@ -831,6 +847,7 @@ export async function runDoctorChecks(): Promise<DoctorResult> {
['browser_launch', () => runBrowserLaunchCheck()],
['ghostty_web_available', () => runGhosttyWebAvailableCheck()],
['screenshot_viable', () => runScreenshotViabilityCheck()],
['libghostty_vt_available', () => runLibghosttyVtAvailableCheck()],
]);
const allChecks = [...environment, ...renderer];
const uniqueCheckNames = new Set(allChecks.map((check) => check.name));
Expand Down
30 changes: 30 additions & 0 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Command, CommanderError } from 'commander';
import type { CommandContext } from './context.js';

import { runCreateCommand } from './commands/create.js';
import { runDashboardCommand } from './commands/dashboard.js';
import { runDestroyCommand } from './commands/destroy.js';
import { runDoctorCommand } from './commands/doctor.js';
import { runGcCommand } from './commands/gc.js';
Expand Down Expand Up @@ -674,6 +675,35 @@ async function main(): Promise<void> {
),
);

program
.command('dashboard')
.description(
'Watch what your agents are doing in their shells: a read-only, live dashboard of your sessions',
)
.option(
'--all',
'Start showing all sessions (active and terminal), not just active ones',
false,
)
.option('--session <id>', 'Preselect a session to watch on launch')
.action(
wrapAction(
'dashboard',
async (
options: { all: boolean; session?: string },
context: CommandContext,
) => {
await runDashboardCommand({
context,
all: options.all,
...(options.session === undefined
? {}
: { session: options.session }),
});
},
),
);

const recordCommand = program
.command('record')
.description('Manage recorded session artifacts');
Expand Down
Loading
Loading