diff --git a/AGENTS.md b/AGENTS.md index 2b333997c..a3a9bbd9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -276,9 +276,9 @@ This repo encodes invariants as self-declaring gates. The correct response to a - Commit messages and PR titles should use conventional prefixes such as `feat:`, `fix:`, `chore:`, `perf:`, `refactor:`, `docs:`, `test:`, `build:`, or `ci:` as appropriate. - Do not use bracketed automation prefixes such as `[codex]` or similar bot tags in commit messages or PR titles. - Open a ready-for-review PR by default. Use a draft PR only when the user explicitly asks for one or the work is intentionally incomplete. -- PR body must be short and include: - - `## Summary`: lead with benefits and reviewer-relevant outcomes. Prefer a compact before/after when it makes the improvement clearer. Include the issue closed by the PR using `Closes #123` when applicable. - - `## Validation`: answer this prompt in concise prose: "How did you verify the change, and what passed or changed on screen?" Prefer evidence over command dumps; mention the relevant check category or scenario, and include screenshots when visual/UI behavior is relevant. +- PR body must be short, reviewer-oriented, and include: + - `## Summary`: describe the user/API behavior, not the implementation file tour. Lead with what changed for operators, clients, command authors, or platform behavior. Use a compact before/after when it clarifies the workflow or bug fix. For new or changed public APIs, include 1-3 concrete CLI/Node/MCP examples that reviewers can scan. Include `Closes #123` when applicable. + - `## Validation`: summarize meaningful evidence in concise prose or bullets. Prefer scenario names, manual device/browser evidence, changed screenshots, CI status, and notable failures/retries with their outcome. Avoid command accounting for routine local gates; mention an exact command only when it is unusual, manually reproducible evidence, or necessary to explain a residual risk. For docs-only changes, say why runtime validation is not applicable instead of writing a command checklist. - Call out real tradeoffs, known gaps, or follow-ups explicitly; omit boilerplate when there are none. - Include touched-file count and note if scope expanded beyond initial command family. diff --git a/CONTEXT.md b/CONTEXT.md index 38add6c7b..2763f99af 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -72,8 +72,9 @@ The perfect-shape refactor is complete and merged. Its end-state: (`src/core/command-descriptor/registry.ts`) is the single declaration site from which the capability matrix, daemon command registry, batch allowlist, timeout policy, MCP exposure list, and capability-checked CLI command list are *derived* by parity-tested projection. The command catalog - still owns identity, command families still own surface metadata/CLI schema, and the Node client - surface remains a deferred derivation target. One + still owns identity, command families still own surface metadata/CLI schema, and system command + facets now project their simple Node client command methods; the rest of the Node client surface + remains a deferred derivation target. One `PlatformPlugin` per platform family (`src/core/platform-plugin/`) stops core/daemon from branching on platform, with the Apple plugin the first instance. See [ADR 0008](docs/adr/0008-command-descriptor-registry.md). diff --git a/scripts/integration-progress-model.ts b/scripts/integration-progress-model.ts index 299d78a7d..1b4c4511f 100644 --- a/scripts/integration-progress-model.ts +++ b/scripts/integration-progress-model.ts @@ -152,7 +152,7 @@ function summarizeProviderScenarioFlagCoverage(files) { ['hideTouches', 'recording without touch overlays'], ['intervalMs', 'repeated press interval'], ['delayMs', 'typing/fill delay'], - ['durationMs', 'scroll and gesture duration'], + ['durationMs', 'scroll, gesture, and TV remote duration'], ['holdMs', 'press hold duration'], ['jitterPx', 'press jitter'], ['pixels', 'scroll distance'], diff --git a/src/__tests__/cli-grammar.test.ts b/src/__tests__/cli-grammar.test.ts index 5b62e5868..a2b9e42e9 100644 --- a/src/__tests__/cli-grammar.test.ts +++ b/src/__tests__/cli-grammar.test.ts @@ -164,7 +164,7 @@ test('is grammar explains the predicate/selector-key collision on invalid predic assert.equal(err.code, 'INVALID_ARGS'); assert.match( err.message, - /is requires predicate: visible\|hidden\|exists\|editable\|selected\|text/, + /is requires predicate: visible\|hidden\|exists\|editable\|selected\|focused\|text/, ); assert.match(err.details?.hint ?? '', /is /); assert.match(err.details?.hint ?? '', /visible=true/); diff --git a/src/__tests__/contracts-schema-public.test.ts b/src/__tests__/contracts-schema-public.test.ts index b22506e68..be88f7bcd 100644 --- a/src/__tests__/contracts-schema-public.test.ts +++ b/src/__tests__/contracts-schema-public.test.ts @@ -12,6 +12,7 @@ import type { BackCommandResult, HomeCommandResult, RotateCommandResult, + TvRemoteCommandResult, } from '../contracts/navigation.ts'; import type { ViewportCommandResult } from '../contracts/viewport.ts'; import { centerOfRect, defaultHintForCode, normalizeError } from '../sdk/contracts.ts'; @@ -166,6 +167,13 @@ test('command result contracts are assignable to command result map', () => { } satisfies RotateCommandResult; const rotateFromMap: CommandResult<'rotate'> = rotate; + const tvRemote = { + action: 'tv-remote', + button: 'select', + message: 'Pressed TV remote select', + } satisfies TvRemoteCommandResult; + const tvRemoteFromMap: CommandResult<'tv-remote'> = tvRemote; + const clipboard = { action: 'write', textLength: 11, @@ -187,6 +195,7 @@ test('command result contracts are assignable to command result map', () => { assert.equal(homeFromMap.action, 'home'); assert.equal(appSwitcherFromMap.action, 'app-switcher'); assert.equal(rotateFromMap.orientation, 'portrait'); + assert.equal(tvRemoteFromMap.button, 'select'); assert.equal(clipboardFromMap.action === 'write' ? clipboardFromMap.textLength : -1, 11); assert.equal( appstateFromMap.platform === 'android' ? appstateFromMap.package : '', diff --git a/src/__tests__/runtime-public.test.ts b/src/__tests__/runtime-public.test.ts index 7c8559438..6aace159d 100644 --- a/src/__tests__/runtime-public.test.ts +++ b/src/__tests__/runtime-public.test.ts @@ -265,6 +265,7 @@ test('internal backend, commands, and io modules are usable', () => { assert.equal(typeof commands.system.settings, 'function'); assert.equal(typeof commands.system.alert, 'function'); assert.equal(typeof commands.system.appSwitcher, 'function'); + assert.equal(typeof commands.system.tvRemote, 'function'); assert.equal(typeof commands.admin.devices, 'function'); assert.equal(typeof commands.admin.install, 'function'); assert.equal(typeof commands.recording.record, 'function'); diff --git a/src/backend.ts b/src/backend.ts index e58e6db11..b619a2ce2 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -9,6 +9,7 @@ import type { ClickButton } from './core/click-button.ts'; import type { DeviceRotation } from './core/device-rotation.ts'; import type { ScrollDirection } from './core/scroll-gesture.ts'; import type { SessionSurface } from './core/session-surface.ts'; +import type { TvRemoteButton } from './core/tv-remote.ts'; import type { RecordingExportQuality } from './core/recording-export-quality.ts'; import type { SnapshotDiagnosticsSummary } from './snapshot-diagnostics.ts'; import type { @@ -87,6 +88,11 @@ export type BackendBackOptions = { mode?: BackMode; }; +export type BackendTvRemoteOptions = { + button: TvRemoteButton; + durationMs?: number; +}; + export type BackendKeyboardOptions = { action: 'status' | 'get' | 'dismiss' | 'enter' | 'return'; }; @@ -491,6 +497,10 @@ export type AgentDeviceBackend = { options?: BackendBackOptions, ): Promise; pressHome?(context: BackendCommandContext): Promise; + pressTvRemote?( + context: BackendCommandContext, + options: BackendTvRemoteOptions, + ): Promise; rotate?( context: BackendCommandContext, orientation: BackendDeviceOrientation, diff --git a/src/cli/parser/__tests__/args-parse-interaction.test.ts b/src/cli/parser/__tests__/args-parse-interaction.test.ts index 1942f09b5..dd3c0b2e9 100644 --- a/src/cli/parser/__tests__/args-parse-interaction.test.ts +++ b/src/cli/parser/__tests__/args-parse-interaction.test.ts @@ -26,6 +26,21 @@ test('parseArgs accepts keyboard subcommands', () => { assert.deepEqual(enter.positionals, ['enter']); }); +test('parseArgs accepts tv-remote', () => { + const remote = parseArgs(['tv-remote', 'press', 'select', '--duration-ms', '250'], { + strictFlags: true, + }); + assert.equal(remote.command, 'tv-remote'); + assert.deepEqual(remote.positionals, ['press', 'select']); + assert.equal(remote.flags.durationMs, 250); + + const longpress = parseArgs(['tv-remote', 'longpress', 'select'], { + strictFlags: true, + }); + assert.equal(longpress.command, 'tv-remote'); + assert.deepEqual(longpress.positionals, ['longpress', 'select']); +}); + test('parseArgs accepts scroll pixel distance and duration flags', () => { const parsed = parseArgs(['scroll', 'down', '--pixels', '240', '--duration-ms', '50'], { strictFlags: true, diff --git a/src/cli/parser/__tests__/cli-help-command-usage.test.ts b/src/cli/parser/__tests__/cli-help-command-usage.test.ts index 355a12cf8..242bafab3 100644 --- a/src/cli/parser/__tests__/cli-help-command-usage.test.ts +++ b/src/cli/parser/__tests__/cli-help-command-usage.test.ts @@ -35,6 +35,14 @@ test('usageForCommand resolves longpress help', async () => { assert.match(help ?? '', /agent-device longpress \[durationMs\]/); }); +test('usageForCommand documents tv-remote longpress preset', async () => { + const help = await usageForCommand('tv-remote'); + assert.equal(help === null, false); + assert.match(help ?? '', /agent-device tv-remote \[press\|longpress\]/); + assert.match(help ?? '', /--duration-ms /); + assert.match(help ?? '', /Use longpress for a 500ms held remote button/); +}); + test('usageForCommand supports legacy long-press alias', async () => { const help = await usageForCommand('long-press'); assert.equal(help === null, false); diff --git a/src/cli/parser/__tests__/cli-help-topics.test.ts b/src/cli/parser/__tests__/cli-help-topics.test.ts index d3c302a31..cef6b4cbe 100644 --- a/src/cli/parser/__tests__/cli-help-topics.test.ts +++ b/src/cli/parser/__tests__/cli-help-topics.test.ts @@ -155,6 +155,10 @@ test('usage includes agent workflows, config, environment, and examples footers' usageText, /agent-device help debugging\s+Use when logs, network, audio, perf memory, traces, alerts, or diagnostics matter/, ); + assert.match( + usageText, + /agent-device help tv\s+Use when navigating Android TV or tvOS focus-first surfaces/, + ); assert.match( usageText, /agent-device help react-devtools\s+Use when inspecting components, props\/state\/hooks, renders, or profiles/, @@ -296,6 +300,20 @@ test('usageForCommand resolves workflow help topic', async () => { assert.doesNotMatch(help, /agent-device react-devtools profile/); }); +test('usageForCommand resolves tv help topic', async () => { + const help = await usageForCommand('tv'); + if (help === null) throw new Error('Expected tv help text'); + assert.match(help, /agent-device help tv/); + assert.match(help, /agent-device tv-remote press down/); + assert.match(help, /agent-device screenshot \.\/tv-focus\.png --overlay-refs/); + assert.match(help, /tv-remote longpress select/); + assert.match(help, /tv-remote press select --duration-ms 500/); + assert.match(help, /longpress is CLI sugar for --duration-ms 500/); + assert.match(help, /ok, center, and enter are input aliases for select/); + assert.match(help, /do not switch to raw adb keyevent/); + assert.match(help, /Use --platform ios --target tv/); +}); + test('usageForCommand resolves web help topic', async () => { const help = await usageForCommand('web'); if (help === null) throw new Error('Expected web help text'); diff --git a/src/cli/parser/cli-help.ts b/src/cli/parser/cli-help.ts index c148300cd..4f38d4003 100644 --- a/src/cli/parser/cli-help.ts +++ b/src/cli/parser/cli-help.ts @@ -19,6 +19,10 @@ const AGENT_WORKFLOWS = [ description: 'Use when logs, network, audio, perf memory, traces, alerts, or diagnostics matter', }, + { + label: 'agent-device help tv', + description: 'Use when navigating Android TV or tvOS focus-first surfaces', + }, { label: 'agent-device help react-native', description: 'Use when the target app is React Native, Expo, or a dev client', @@ -94,6 +98,7 @@ const AGENT_QUICKSTART_LINES = [ 'Direct proxy: run agent-device connect proxy --daemon-base-url before using a shared Mac proxy. Device leases are automatic on open and expire after five minutes of inactivity.', 'Batch JSON steps use "command" and structured "input"; legacy "positionals"/"flags" steps still run in CLI but are deprecated until the next major version.', 'Navigation: app-owned back uses back; system back uses back --system.', + 'TV targets are focus-first: read help tv; use tv-remote press up|down|left|right|select to move D-pad/remote focus, tv-remote longpress