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 @@ -17,6 +17,7 @@
- Session-status policy is now centralized in a single module that classifies every `SessionStatus` as active, terminal, commandable, live-host eligible, offline-replay eligible, collectable, and destroyed; lifecycle, inspect, gc, wait, host, and command-state checks share the same predicates. CLI JSON, protocol schemas, event-log behavior, and artifact formats are unchanged ([#67](https://github.com/coder/agent-tty/pull/67)).
- Persisted event-log validation, JSONL parsing, and contiguous-sequence checks for live hydration, offline replay, and `record export` now flow through a shared codec at `src/storage/eventLogCodec.ts`. Validation errors use neutral event-log wording; missing-log policy remains caller-specific (offline replay treats missing logs as empty; `record export` still surfaces missing logs as an error) ([#68](https://github.com/coder/agent-tty/pull/68)).
- Snapshot result construction and artifact persistence are now shared between live host RPCs and offline replay through `src/snapshot/capture.ts`. Snapshot artifact filenames, JSON formatting (two-space indent + trailing newline), manifest metadata, and `rendererBackend` reporting are unchanged ([#69](https://github.com/coder/agent-tty/pull/69)).
- Screenshot capture, validation, and artifact persistence are now shared between live host RPCs and offline replay through `src/screenshot/capture.ts`. The shared seam owns temporary-file allocation, renderer screenshot invocation, allowlisted public result construction, Zod validation before any rename or manifest append, manifest metadata creation, and temp-file cleanup on failure. Screenshot filenames, final PNG paths, manifest entry shape (`profileName`, `cols`, `rows`, `pngSizeBytes`, `cursorVisible`, `rendererBackend`, `pixelWidth`, `pixelHeight`, `sha256`, `renderProfileHash`), and `screenshot --json` output are unchanged ([#78](https://github.com/coder/agent-tty/pull/78)).
- Waited-run completion bookkeeping (sentinel scanning, postamble sanitization, waiter resolution, timeout, exit, `run_complete` appends) moved out of `hostMain` into a dedicated `RunCompletionCoordinator`. No public CLI, protocol, or event-log changes ([#70](https://github.com/coder/agent-tty/pull/70)).
- Refreshed contributor and agent guidance in `AGENTS.md` with an outcome-first operating contract, validation guidance, and grouped project invariants ([#46](https://github.com/coder/agent-tty/pull/46)). Added in-repo agent skills under `.agents/skills/` (`diagnose`, `tdd`, `triage`, `improve-codebase-architecture`, `grill-with-docs`, `to-issues`, `to-prd`) plus matching links from `.claude/skills/` and `.mux/skills/` ([#65](https://github.com/coder/agent-tty/pull/65)).

Expand Down
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ A persisted JSON artifact containing exactly the **Snapshot Result** returned to
The operation that derives a **Snapshot Result** from a **Semantic Snapshot** and records the matching **Snapshot Artifact**.
_Avoid_: Renderer capture

**Screenshot Result**:
A public screenshot payload returned to a caller describing the rendered PNG of a **Session** at a captured event-log sequence.

**Screenshot Artifact**:
A persisted PNG file plus its manifest entry recording the **Screenshot Result** for a **Session** at a captured event-log sequence.

**Screenshot Capture**:
The operation that invokes the renderer to produce the PNG, builds the **Screenshot Result**, and records the matching **Screenshot Artifact**.
_Avoid_: Renderer capture

**Release Prep Workflow**:
The maintainer-facing process for choosing the next release version and preparing release changes for review before they land on the default branch.

Expand All @@ -90,6 +100,8 @@ The tag-triggered automation that validates, packages, and publishes a release a
- An **Offline Replay Eligible Session** is reconstructed from its persisted **Event Log** and manifest.
- A **Snapshot Result** is derived from exactly one **Semantic Snapshot**.
- A **Snapshot Artifact** contains exactly the **Snapshot Result** emitted to the caller.
- A **Screenshot Capture** produces exactly one **Screenshot Result** and exactly one **Screenshot Artifact** for the same captured event-log sequence.
- A **Screenshot Artifact** is the persisted PNG plus its manifest entry that the **Screenshot Result** describes to the caller.
- A **Command Target** is exactly one **Commandable Session** selected by an input or control command.
- A **Commandable Session** can accept a **Waited Run**.
- A **Render Wait** may include text, regex, cursor, or **Screen Stability** conditions.
Expand Down
95 changes: 95 additions & 0 deletions dogfood/issue-64-share-screenshot-capture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Issue 64 — Share screenshot capture and artifact persistence

This bundle compares screenshot output from the running session (live) and
exited session (offline replay) paths, before and after the refactor that
moves both paths through the shared `captureScreenshotResult(...)` helper in
`src/screenshot/capture.ts`.

## How it was generated

The same `commands.sh` was run twice:

1. **before**: against parent commit `b2d5068` (`refactor: share render wait
matching (#76)`) in a temporary `git worktree`. This still has the
inline screenshot logic in `src/host/hostMain.ts` and a duplicate
implementation in `src/cli/commands/screenshot.ts`.
2. **after**: against this branch, where both paths call the new
`captureScreenshotResult(...)` helper.

Each run uses an isolated absolute `AGENT_TTY_HOME` (recorded in
`<label>/agent-tty-home.txt`) and writes its outputs under
`<label>/`.

The script is idempotent and safe to re-run:

```sh
bash dogfood/issue-64-share-screenshot-capture/commands.sh after
# or, in a worktree of the parent commit:
bash dogfood/issue-64-share-screenshot-capture/commands.sh before
```

## Scenarios captured

For both `before/` and `after/` directories:

| File | Description |
| ---------------------------------------- | --------------------------------------------------- |
| `03-screenshot-live-default.json` | Live (running) screenshot, default cursor |
| `04-screenshot-live-show-cursor.json` | Live (running) screenshot with `--show-cursor` |
| `12-screenshot-offline-default.json` | Offline (exited) screenshot, default cursor |
| `13-screenshot-offline-show-cursor.json` | Offline (exited) screenshot with `--show-cursor` |
| `screenshot-live-default.png` | Live default-cursor PNG |
| `screenshot-live-show-cursor.png` | Live show-cursor PNG |
| `screenshot-offline-default.png` | Offline default-cursor PNG |
| `screenshot-offline-show-cursor.png` | Offline show-cursor PNG |
| `manifest-live.json` | Artifact manifest after live session screenshots |
| `manifest-offline.json` | Artifact manifest after offline session screenshots |
| `sha256-summary.txt` | One-line-per-PNG SHA-256 summary |
| `transcript.txt` | Full command transcript |

Default profile is `reference-dark` (the only profile this refactor needs to
preserve).

## Parity verdict

| Comparison | Result |
| ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------- |
| `sha256-summary.txt` (live + offline, with and without cursor) | **identical** |
| All four screenshot result envelopes, excluding session IDs and the absolute `artifactPath` (which embeds the temp `AGENT_TTY_HOME`) | **identical fields** |
| Artifact manifest entries, excluding ULID `id`, `sessionId`, and `createdAt` timestamps | **identical metadata** |
| All four PNG files, byte-for-byte (`cmp -s`) | **identical bytes** |

Specifically:

- Live default and live show-cursor PNGs both hash to
`a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb` in both
before and after.
- Offline default and offline show-cursor PNGs both hash to
`993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d` in both
before and after.
- All result envelopes preserve `profileName=reference-dark`,
`cols=80`, `rows=24`, `pngSizeBytes`, `pixelWidth=640`, `pixelHeight=384`,
`cursorVisible`, `rendererBackend=ghostty-web`, and `renderProfileHash`.

## Reviewer reproduction

To independently verify these comparisons:

```sh
node -e "
const fs = require('node:fs');
function strip(p) {
const e = JSON.parse(fs.readFileSync(p, 'utf8'));
const r = e.result; delete r.sessionId; delete r.artifactPath;
delete e.timestamp;
return e;
}
for (const f of ['03-screenshot-live-default','04-screenshot-live-show-cursor','12-screenshot-offline-default','13-screenshot-offline-show-cursor']) {
const a = strip(\`before/\${f}.json\`);
const b = strip(\`after/\${f}.json\`);
console.log(f, JSON.stringify(a)===JSON.stringify(b) ? 'IDENTICAL' : 'DIFFER');
}
"
cmp -s before/screenshot-live-default.png after/screenshot-live-default.png && echo "live-default PNG IDENTICAL"
cmp -s before/screenshot-offline-default.png after/screenshot-offline-default.png && echo "offline-default PNG IDENTICAL"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ok": true,
"command": "create",
"timestamp": "2026-04-30T07:53:01.428Z",
"result": {
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"createdAt": "2026-04-30T07:53:00.604Z",
"cols": 80,
"rows": 24,
"shell": "/bin/bash"
}
}
13 changes: 13 additions & 0 deletions dogfood/issue-64-share-screenshot-capture/after/02-wait-live.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"ok": true,
"command": "wait",
"timestamp": "2026-04-30T07:53:03.114Z",
"result": {
"matched": true,
"timedOut": false,
"matchedText": "live screenshot fixture",
"cursorRow": 1,
"cursorCol": 0,
"capturedAtSeq": 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ok": true,
"command": "screenshot",
"timestamp": "2026-04-30T07:53:04.412Z",
"result": {
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"capturedAtSeq": 0,
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"artifactPath": "/tmp/tmp.p97cQahYjP/sessions/01KQEP0YDSTXK6A7EB7ABAK8SS/artifacts/screenshot-0-reference-dark.png",
"pngSizeBytes": 3790,
"cursorVisible": false,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"sha256": "a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb",
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ok": true,
"command": "screenshot",
"timestamp": "2026-04-30T07:53:05.670Z",
"result": {
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"capturedAtSeq": 0,
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"artifactPath": "/tmp/tmp.p97cQahYjP/sessions/01KQEP0YDSTXK6A7EB7ABAK8SS/artifacts/screenshot-0-reference-dark.png",
"pngSizeBytes": 3790,
"cursorVisible": true,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"sha256": "a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb",
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ok": true,
"command": "create",
"timestamp": "2026-04-30T07:53:07.504Z",
"result": {
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"createdAt": "2026-04-30T07:53:06.784Z",
"cols": 80,
"rows": 24,
"shell": "/bin/bash"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ok": true,
"command": "wait",
"timestamp": "2026-04-30T07:53:08.644Z",
"result": {
"timedOut": false,
"exitCode": 0
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ok": true,
"command": "screenshot",
"timestamp": "2026-04-30T07:53:10.283Z",
"result": {
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"capturedAtSeq": 1,
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"artifactPath": "/tmp/tmp.p97cQahYjP/sessions/01KQEP14EXSSGJX9RRSTAVYGK2/artifacts/screenshot-1-reference-dark.png",
"pngSizeBytes": 3749,
"cursorVisible": false,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"sha256": "993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d",
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ok": true,
"command": "screenshot",
"timestamp": "2026-04-30T07:53:16.766Z",
"result": {
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"capturedAtSeq": 1,
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"artifactPath": "/tmp/tmp.p97cQahYjP/sessions/01KQEP14EXSSGJX9RRSTAVYGK2/artifacts/screenshot-1-reference-dark.png",
"pngSizeBytes": 3749,
"cursorVisible": true,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"sha256": "993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d",
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ok": true,
"command": "destroy",
"timestamp": "2026-04-30T07:53:24.099Z",
"result": {
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"destroyed": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ok": true,
"command": "destroy",
"timestamp": "2026-04-30T07:53:23.059Z",
"result": {
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"destroyed": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tmp/tmp.p97cQahYjP
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
01KQEP0YDSTXK6A7EB7ABAK8SS
46 changes: 46 additions & 0 deletions dogfood/issue-64-share-screenshot-capture/after/manifest-live.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"version": 1,
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"artifacts": [
{
"id": "01KQEP124PSDQBCK39NM2JRG0Q",
"kind": "screenshot",
"filename": "screenshot-0-reference-dark.png",
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"capturedAtSeq": 0,
"createdAt": "2026-04-30T07:53:04.406Z",
"sha256": "a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb",
"metadata": {
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"pngSizeBytes": 3790,
"cursorVisible": false,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
},
{
"id": "01KQEP13C2VTX38AMZV3F92STZ",
"kind": "screenshot",
"filename": "screenshot-0-reference-dark.png",
"sessionId": "01KQEP0YDSTXK6A7EB7ABAK8SS",
"capturedAtSeq": 0,
"createdAt": "2026-04-30T07:53:05.667Z",
"sha256": "a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb",
"metadata": {
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"pngSizeBytes": 3790,
"cursorVisible": true,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"version": 1,
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"artifacts": [
{
"id": "01KQEP17V1X05S77G8CX5X7VPE",
"kind": "screenshot",
"filename": "screenshot-1-reference-dark.png",
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"capturedAtSeq": 1,
"createdAt": "2026-04-30T07:53:10.241Z",
"sha256": "993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d",
"metadata": {
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"pngSizeBytes": 3749,
"cursorVisible": false,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
},
{
"id": "01KQEP1E5KMKCHYK9BEZJ6TD0G",
"kind": "screenshot",
"filename": "screenshot-1-reference-dark.png",
"sessionId": "01KQEP14EXSSGJX9RRSTAVYGK2",
"capturedAtSeq": 1,
"createdAt": "2026-04-30T07:53:16.723Z",
"sha256": "993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d",
"metadata": {
"profileName": "reference-dark",
"cols": 80,
"rows": 24,
"pngSizeBytes": 3749,
"cursorVisible": true,
"rendererBackend": "ghostty-web",
"pixelWidth": 640,
"pixelHeight": 384,
"renderProfileHash": "8ffed6af301ec7c0e6b69599c3be0d1d12096f9fcdfc59d0bbb4cc474d64c53d"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
01KQEP14EXSSGJX9RRSTAVYGK2
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
screenshot-live-default.png:
a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb
screenshot-live-show-cursor.png:
a658e720e18b2943fd6203e8947a9fe4e60fa0759cb5d0f9c74668abfe267ceb
screenshot-offline-default.png:
993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d
screenshot-offline-show-cursor.png:
993fb5f9fe5b9ec7ed456b4005ec40478f5ee6bc3c6d5a65f6dbc78fbd60406d
Loading
Loading