Skip to content

feat(screen-hash): stable content hash on snapshot and render-wait results#127

Merged
ThomasK33 merged 4 commits into
mainfrom
feat/screen-hash
Jun 6, 2026
Merged

feat(screen-hash): stable content hash on snapshot and render-wait results#127
ThomasK33 merged 4 commits into
mainfrom
feat/screen-hash

Conversation

@ThomasK33
Copy link
Copy Markdown
Member

Summary

Adds an optional screenHash to snapshot and render-wait results — a lowercase 64-char SHA-256 of the canonical visible-screen text — giving a caller (typically an AI coding agent) a stable token to tell whether the rendered screen content actually changed between two observations, without diffing full text. Unlike the event-log sequence it does not advance on cursor moves or no-op repaints; unlike the screenshot pixel sha256 it is the semantic (text) counterpart.

Implements the screen-hash PRD (docs/prd/screen-hash/PRD.md). Closes #125.

What's included

screenHash field

  • Optional screenHash on the structured + text snapshot results and on render-wait results, validated by the existing (now consolidated, single-source) Sha256HexSchema.
  • Hash = SHA-256 (UTF-8) of visibleLines[].text joined by \n — visible screen only (no scrollback, cursor, or styles).
  • One shared canonical-visible-text helper (src/renderer/canonicalScreen.ts) feeds the Screen Hash, the host Screen Stability compare, and the text Render Wait matcher, so the three can never disagree about "the screen".

Key decisions (settled during planning)

  • Hash any observed snapshot: matched live waits, snapshot captures, and the offline host-unreachable matched:false fallback all carry it; only results that observed no snapshot (live timeout, consecutive-failure giveup, replay-error throw) omit it — so "absent hash ⇔ no screen observed" holds literally.
  • Batch: matched batch wait-step records carry it too.
  • Renderer convergence: both backends are aligned on one canonical screen form (exactly rows lines; full grapheme clusters; interior blank cells as spaces; ASCII-only trailing trim) so the hash is renderer-independent. This intentionally changes the default ghostty-web stability/text-wait comparand on grapheme / interior-gap / non-ASCII-trailing screens — a deliberate, narrow change pinned by characterization + cross-backend tests (see PRD §Out of Scope and user-story feat: Week 7 — contract ratification, test locks, proof bundles, and docs sync #10).

CI / test hardening (separate commit ci: …, independent of the feature)

  • test:integration, test:e2e, and the combined test now retry transient failures (--retry=2); test:unit stays strict. Browser/host/PTY e2e tests can flake under machine load (a screenshot-render/RPC hiccup that passes on retry and in isolation) — this keeps such flakes from spuriously failing CI while a genuine failure still fails all three attempts. Documented in CONTRIBUTING + CI.

Verification

  • Green locally: typecheck, lint, oxfmt, unit (1334 tests), build, workflow-lint (zizmor).
  • The cross-backend equality test and the CLI envelope integration test are host/native-dependent (need a live PTY host + the optional @coder/libghostty-vt-node addon) and run in CI; the convergence's pure decode logic is unit-tested via exported twins (assembleCanonicalLine / stripTrailingAsciiSpaces).

Notes for reviewers

  • screenHash lives on the result, not on SemanticSnapshot (the snapshot shape is unchanged).
  • The deliberate cells[] (blank = '') vs visibleLines[].text (blank = ' ', spacer dropped) asymmetry is documented inline in ghosttyWeb/backend.ts; the hash sources visibleLines[].text only.
  • No ScreenshotResult change — screenshots keep only their pixel sha256.

🤖 Generated with Claude Code

ThomasK33 and others added 4 commits June 5, 2026 20:14
Capture the screenHash design from the grill-with-docs pass: a stable digest of normalized visible screen text, computed from the same canonical visible text as the Screen Stability check and text Render Wait matching (unified, no behavior change), and distinct from the screenshot pixel sha256.

Change-Id: I73d19ebe921f316bff9dab166c8ba756f0cdd3fe
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Product requirements for an optional screenHash field on snapshot and render-wait results: a stable content change-token computed from the canonical visible text and unified with the Screen Stability compare, with no behavior change. Produced via the to-prd flow.

Change-Id: Icbd22c29b3785476ee13ed62c2c100cdc45d7c22
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Add an optional `screenHash` field — a lowercase 64-char SHA-256 of the
canonical visible-screen text (`visibleLines[].text` joined by `\n`) — to
snapshot results (structured and text) and to render-wait results that observed
a Semantic Snapshot. It gives a caller a stable content-change token that is
unaffected by cursor motion or no-op repaints.

- Extract one shared canonical-visible-text helper (src/renderer/canonicalScreen.ts)
  and route the Screen Hash, the host Screen Stability compare, and the text
  Render Wait matcher through it; add a UTF-8-pinned sha256Hex util and
  consolidate the duplicate Sha256HexSchema into one exported definition.
- Hash any observed snapshot: matched live waits, snapshot captures, and the
  offline host-unreachable matched:false fallback carry it; live timeouts,
  consecutive-failure giveups, and replay errors omit it.
- Carry the hash on matched batch wait-step records.
- Converge both renderer backends on one canonical screen form so the hash is
  renderer-independent: ghostty-web now decodes full grapheme clusters, keeps
  interior blank cells as spaces, and right-trims ASCII spaces only;
  libghostty-vt pads visible lines to exactly `rows`. This intentionally aligns
  the default ghostty-web stability/text-wait comparand on grapheme /
  interior-gap / non-ASCII-trailing screens, pinned by characterization and
  cross-backend tests.

Closes #125.

Change-Id: I698af5dbf8f66c70f49661712652b24d70415f0a
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Integration and e2e tests drive real PTY hosts and headless-browser renderers,
which can transiently fail under machine load (e.g. a screenshot render or host
RPC hiccup) even when the code is correct. Pass `--retry=2` to the
`test:integration`, `test:e2e`, and combined `test` scripts so a flaky attempt
is retried in place instead of failing the sharded CI job; a genuine failure
still fails all three attempts. `test:unit` stays strict so the unit gate keeps
catching real unit flakes. Document the policy in CONTRIBUTING and CI.

Change-Id: I7696b26b9a5b97102b4543a6bcf485ce30ea4be4
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 merged commit 0dcbb9f into main Jun 6, 2026
13 checks passed
@ThomasK33 ThomasK33 deleted the feat/screen-hash branch June 6, 2026 11:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

screenHash: a stable content hash on snapshot and wait results

1 participant