diff --git a/packages/contracts/src/tv-remote-runtime.ts b/packages/contracts/src/tv-remote-runtime.ts index 88922013bf..5a98695e57 100644 --- a/packages/contracts/src/tv-remote-runtime.ts +++ b/packages/contracts/src/tv-remote-runtime.ts @@ -1,6 +1,6 @@ import type { Interactor, RunnerContext } from './interactor-types.ts'; import type { TvRemoteButton } from './tv-remote.ts'; -import type { RuntimeOperationFact } from './platform-runtime.ts'; +import type { RuntimeOperationFact, RuntimeOperationUnavailability } from './platform-runtime.ts'; import type { SnapshotRuntimeExecution } from './snapshot-runtime.ts'; /** @@ -30,9 +30,29 @@ export type TvRemoteRuntimeOperationFacts = Readonly<{ }>; export function tvRemoteRuntimeOperationFacts( - input: Readonly<{ tvRemote: RuntimeOperationFact }>, + input: Readonly<{ + tvRemote: RuntimeOperationFact; + buttonSupport?: Readonly<{ + supported: readonly TvRemoteButton[]; + unsupported: RuntimeOperationUnavailability; + }>; + }>, ): TvRemoteRuntimeOperationFacts { - return Object.freeze({ tvRemote: input.tvRemote }); + const { tvRemote, buttonSupport } = input; + return Object.freeze({ + tvRemote: + tvRemote.available && buttonSupport + ? Object.freeze({ + available: true as const, + subjects: Object.freeze( + Object.fromEntries( + buttonSupport.supported.map((button) => [button, { available: true as const }]), + ), + ), + unlisted: buttonSupport.unsupported, + }) + : tvRemote, + }); } /** How a fail-closed refusal names the TV remote to the caller, matching the command. */ diff --git a/packages/contracts/src/tv-remote.test.ts b/packages/contracts/src/tv-remote.test.ts index 5ca94b22ce..a1c5c8de27 100644 --- a/packages/contracts/src/tv-remote.test.ts +++ b/packages/contracts/src/tv-remote.test.ts @@ -3,13 +3,18 @@ import { test } from 'vitest'; import { parseTvRemoteButton, TV_REMOTE_BUTTONS, + tvRemoteButtonsForPlatform, + toAndroidTvRemoteKeyevent, + toAppleTvRemoteButton, toVegaTvRemoteKey, tvRemoteDurationMode, } from './tv-remote.ts'; test('Vega TV remote mapping covers the shared button contract', () => { assert.deepEqual( - Object.fromEntries(TV_REMOTE_BUTTONS.map((button) => [button, toVegaTvRemoteKey(button)])), + Object.fromEntries( + tvRemoteButtonsForPlatform('vega').map((button) => [button, toVegaTvRemoteKey(button)]), + ), { up: 'KEY_UP', down: 'KEY_DOWN', @@ -37,3 +42,76 @@ test('shared select aliases normalize before reaching a platform adapter', () => 'select', ]); }); + +test('Android TV hardware buttons map to their KeyEvent codes', () => { + const expected = { + 'volume-up': '24', + 'volume-down': '25', + mute: '164', + power: '26', + sleep: '223', + wake: '224', + 'media-play-pause': '85', + 'media-next': '87', + 'media-previous': '88', + 'media-rewind': '89', + 'media-fast-forward': '90', + 'channel-up': '166', + 'channel-down': '167', + 'last-channel': '229', + guide: '172', + info: '165', + captions: '175', + 'tv-input': '178', + 'color-red': '183', + 'color-green': '184', + 'color-yellow': '185', + 'color-blue': '186', + }; + for (const [button, code] of Object.entries(expected)) { + assert.equal(toAndroidTvRemoteKeyevent(parseTvRemoteButton(button)), code); + } + assert.equal(TV_REMOTE_BUTTONS.length, 30); +}); + +test('raw Android codes and named TV buttons normalize before dispatch', () => { + assert.equal(parseTvRemoteButton('VOLUME-UP'), 'volume-up'); + assert.equal(parseTvRemoteButton('ANDROID-KEYCODE:300'), 'android-keycode:300'); + assert.equal( + toAndroidTvRemoteKeyevent(parseTvRemoteButton('android-keycode:2147483647')), + '2147483647', + ); +}); + +test.each([ + undefined, + 'volume-sideways', + 'android-keycode:', + 'android-keycode:0', + 'android-keycode:-1', + 'android-keycode:01', + 'android-keycode:1.5', + 'android-keycode:1e2', + 'android-keycode:2147483648', + 'android-keycode:99999999999', + 'android-keycode:300\n', +])('rejects invalid TV button %j', (button) => { + assert.throws(() => parseTvRemoteButton(button), { code: 'INVALID_ARGS' }); +}); + +test('Apple and Vega maps refuse Android-only buttons instead of passing an absent key', () => { + for (const button of ['volume-up', 'android-keycode:300'] as const) { + assert.throws(() => toAppleTvRemoteButton(button), { code: 'UNSUPPORTED_OPERATION' }); + assert.throws(() => toVegaTvRemoteKey(button), { code: 'UNSUPPORTED_OPERATION' }); + } + assert.deepEqual(tvRemoteButtonsForPlatform('apple'), [ + 'up', + 'down', + 'left', + 'right', + 'select', + 'menu', + 'home', + 'back', + ]); +}); diff --git a/packages/contracts/src/tv-remote.ts b/packages/contracts/src/tv-remote.ts index c341731aec..b3b6955b76 100644 --- a/packages/contracts/src/tv-remote.ts +++ b/packages/contracts/src/tv-remote.ts @@ -5,8 +5,8 @@ type TvRemoteDurationMode = 'exact' | 'longpress'; type TvRemoteButtonDefinition = { aliases: readonly string[]; androidKeyevent: string; - appleRemoteButton: string; - vegaKey: string; + appleRemoteButton?: string; + vegaKey?: string; }; const TV_REMOTE_DURATION_MODE = { @@ -64,20 +64,56 @@ const TV_REMOTE_BUTTON_DEFINITIONS = { appleRemoteButton: 'menu', vegaKey: 'KEY_BACK', }, + 'volume-up': { aliases: [], androidKeyevent: '24' }, + 'volume-down': { aliases: [], androidKeyevent: '25' }, + mute: { aliases: [], androidKeyevent: '164' }, + power: { aliases: [], androidKeyevent: '26' }, + sleep: { aliases: [], androidKeyevent: '223' }, + wake: { aliases: [], androidKeyevent: '224' }, + 'media-play-pause': { aliases: [], androidKeyevent: '85' }, + 'media-next': { aliases: [], androidKeyevent: '87' }, + 'media-previous': { aliases: [], androidKeyevent: '88' }, + 'media-rewind': { aliases: [], androidKeyevent: '89' }, + 'media-fast-forward': { aliases: [], androidKeyevent: '90' }, + 'channel-up': { aliases: [], androidKeyevent: '166' }, + 'channel-down': { aliases: [], androidKeyevent: '167' }, + 'last-channel': { aliases: [], androidKeyevent: '229' }, + guide: { aliases: [], androidKeyevent: '172' }, + info: { aliases: [], androidKeyevent: '165' }, + captions: { aliases: [], androidKeyevent: '175' }, + 'tv-input': { aliases: [], androidKeyevent: '178' }, + 'color-red': { aliases: [], androidKeyevent: '183' }, + 'color-green': { aliases: [], androidKeyevent: '184' }, + 'color-yellow': { aliases: [], androidKeyevent: '185' }, + 'color-blue': { aliases: [], androidKeyevent: '186' }, } as const satisfies Record; export const TV_REMOTE_BUTTONS = Object.keys( TV_REMOTE_BUTTON_DEFINITIONS, ) as (keyof typeof TV_REMOTE_BUTTON_DEFINITIONS)[]; -export const TV_REMOTE_BUTTON_USAGE = `<${TV_REMOTE_BUTTONS.join('|')}>`; +const ANDROID_KEYCODE_PREFIX = 'android-keycode:'; +const MAX_ANDROID_KEYCODE = 2_147_483_647; -export type TvRemoteButton = (typeof TV_REMOTE_BUTTONS)[number]; +/** Canonical decimal Android keycodes in the positive Java int range. */ +export const TV_REMOTE_ANDROID_KEYCODE_PATTERN = + /^android-keycode:(?:[1-9][0-9]{0,8}|1[0-9]{9}|20[0-9]{8}|21[0-3][0-9]{7}|214[0-6][0-9]{6}|2147[0-3][0-9]{5}|21474[0-7][0-9]{4}|214748[0-2][0-9]{3}|2147483[0-5][0-9]{2}|21474836[0-3][0-9]|214748364[0-7])(?![\s\S])/; -export type AppleTvRemoteButton = - (typeof TV_REMOTE_BUTTON_DEFINITIONS)[TvRemoteButton]['appleRemoteButton']; +export const TV_REMOTE_BUTTON_USAGE = `<${TV_REMOTE_BUTTONS.join('|')}|android-keycode:>`; -export type VegaTvRemoteKey = (typeof TV_REMOTE_BUTTON_DEFINITIONS)[TvRemoteButton]['vegaKey']; +type NamedTvRemoteButton = (typeof TV_REMOTE_BUTTONS)[number]; + +export type TvRemoteButton = NamedTvRemoteButton | `android-keycode:${number}`; + +export type AppleTvRemoteButton = Extract< + (typeof TV_REMOTE_BUTTON_DEFINITIONS)[NamedTvRemoteButton], + { appleRemoteButton: string } +>['appleRemoteButton']; + +export type VegaTvRemoteKey = Extract< + (typeof TV_REMOTE_BUTTON_DEFINITIONS)[NamedTvRemoteButton], + { vegaKey: string } +>['vegaKey']; export function parseTvRemoteButton(value: string | undefined): TvRemoteButton { const normalized = value?.toLowerCase(); @@ -86,24 +122,57 @@ export function parseTvRemoteButton(value: string | undefined): TvRemoteButton { if (canonical) return canonical; throw new AppError( 'INVALID_ARGS', - `tv-remote button must be one of: ${TV_REMOTE_BUTTONS.join(', ')}.`, + `tv-remote button must be one of: ${TV_REMOTE_BUTTONS.join(', ')}, or android-keycode: with n from 1 to ${MAX_ANDROID_KEYCODE}.`, ); } -function isTvRemoteButton(value: unknown): value is TvRemoteButton { - return typeof value === 'string' && TV_REMOTE_BUTTONS.includes(value as TvRemoteButton); +export function isTvRemoteButton(value: unknown): value is TvRemoteButton { + return isNamedTvRemoteButton(value) || isAndroidKeycode(value); +} + +function isNamedTvRemoteButton(value: unknown): value is NamedTvRemoteButton { + return typeof value === 'string' && Object.hasOwn(TV_REMOTE_BUTTON_DEFINITIONS, value); +} + +function isAndroidKeycode(value: unknown): value is `android-keycode:${number}` { + return typeof value === 'string' && TV_REMOTE_ANDROID_KEYCODE_PATTERN.test(value); +} + +export function tvRemoteButtonsForPlatform(platform: 'apple' | 'vega'): readonly TvRemoteButton[] { + const key = platform === 'apple' ? 'appleRemoteButton' : 'vegaKey'; + return TV_REMOTE_BUTTONS.filter((button) => key in TV_REMOTE_BUTTON_DEFINITIONS[button]); } export function toAppleTvRemoteButton(button: TvRemoteButton): AppleTvRemoteButton { - return TV_REMOTE_BUTTON_DEFINITIONS[button].appleRemoteButton; + const definition = isNamedTvRemoteButton(button) + ? TV_REMOTE_BUTTON_DEFINITIONS[button] + : undefined; + if (definition && 'appleRemoteButton' in definition) return definition.appleRemoteButton; + throw unsupportedTvRemoteButton(button, 'tvOS'); } export function toAndroidTvRemoteKeyevent(button: TvRemoteButton): string { - return TV_REMOTE_BUTTON_DEFINITIONS[button].androidKeyevent; + return isNamedTvRemoteButton(button) + ? TV_REMOTE_BUTTON_DEFINITIONS[button].androidKeyevent + : button.slice(ANDROID_KEYCODE_PREFIX.length); } export function toVegaTvRemoteKey(button: TvRemoteButton): VegaTvRemoteKey { - return TV_REMOTE_BUTTON_DEFINITIONS[button].vegaKey; + const definition = isNamedTvRemoteButton(button) + ? TV_REMOTE_BUTTON_DEFINITIONS[button] + : undefined; + if (definition && 'vegaKey' in definition) return definition.vegaKey; + throw unsupportedTvRemoteButton(button, 'Vega OS'); +} + +function unsupportedTvRemoteButton(button: TvRemoteButton, platform: string): AppError { + return new AppError( + 'UNSUPPORTED_OPERATION', + `TV remote button ${button} is not supported on ${platform}.`, + { + reason: 'unsupported-platform-leaf', + }, + ); } export function tvRemoteDurationMode( @@ -112,7 +181,7 @@ export function tvRemoteDurationMode( return TV_REMOTE_DURATION_MODE[platform]; } -const TV_REMOTE_BUTTON_ALIAS_MAP = new Map( +const TV_REMOTE_BUTTON_ALIAS_MAP = new Map( TV_REMOTE_BUTTONS.flatMap((button) => TV_REMOTE_BUTTON_DEFINITIONS[button].aliases.map((alias) => [alias, button] as const), ), diff --git a/packages/daemon/src/execution/__tests__/tv-remote-runtime.test.ts b/packages/daemon/src/execution/__tests__/tv-remote-runtime.test.ts index d0cb543738..fd4e9f20af 100644 --- a/packages/daemon/src/execution/__tests__/tv-remote-runtime.test.ts +++ b/packages/daemon/src/execution/__tests__/tv-remote-runtime.test.ts @@ -254,3 +254,37 @@ test('request router joins tv-remote admission to execution and ref invalidation expect(harness.bind).toHaveBeenCalledTimes(1); expect(harness.tvRemote).toHaveBeenCalledTimes(1); }); + +test.each(['volume-up', 'android-keycode:300'])( + 'refuses unsupported button %s before binding a runtime that supports navigation', + async (button) => { + const harness = runtimeHarness({ + available: true, + subjects: { up: available }, + unlisted: { + available: false, + reason: 'unsupported-platform-leaf', + hint: 'Navigation buttons only.', + }, + }); + const resolved = await resolveBoundTvRemoteRuntime({ + platformServices: daemonPlatformServicesFixture(), + device: vegaVvd, + positionals: [button], + inspectFacts: harness.inspectFacts, + bindDevice: harness.bindDevice, + }); + expect(resolved).toMatchObject({ + ok: false, + response: { + error: { + code: 'UNSUPPORTED_OPERATION', + details: { reason: 'unsupported-platform-leaf' }, + hint: 'Navigation buttons only.', + }, + }, + }); + expect(harness.bindDevice).not.toHaveBeenCalled(); + expect(harness.tvRemote).not.toHaveBeenCalled(); + }, +); diff --git a/packages/daemon/src/execution/tv-remote-runtime.ts b/packages/daemon/src/execution/tv-remote-runtime.ts index 552370b6fc..7d65f52336 100644 --- a/packages/daemon/src/execution/tv-remote-runtime.ts +++ b/packages/daemon/src/execution/tv-remote-runtime.ts @@ -56,6 +56,7 @@ export async function resolveBoundTvRemoteRuntime( return await resolveBoundGenericRuntime( { command: 'tv-remote', + subject: button, device: params.device, use: tvRemoteRuntimeUse, inspectFacts: params.inspectFacts, diff --git a/packages/platform-apple/src/navigation/runtime.test.ts b/packages/platform-apple/src/navigation/runtime.test.ts new file mode 100644 index 0000000000..d79c4eb186 --- /dev/null +++ b/packages/platform-apple/src/navigation/runtime.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest'; +import { runtimeSubjectFact } from '@agent-device/contracts/platform-runtime'; +import { TV_REMOTE_BUTTONS } from '@agent-device/contracts/tv-remote'; +import { appleNavigationFacts } from './runtime.ts'; + +test('tvOS admits its navigation buttons and refuses Android-only buttons', () => { + const fact = appleNavigationFacts({ + id: 'tvos-remote-facts', + name: 'Apple TV', + platform: 'apple', + appleOs: 'tvos', + kind: 'simulator', + target: 'tv', + booted: true, + }).tvRemote; + const supported = new Set(['up', 'down', 'left', 'right', 'select', 'menu', 'home', 'back']); + for (const button of [...TV_REMOTE_BUTTONS, 'android-keycode:300']) { + expect(runtimeSubjectFact(fact, button).available).toBe(supported.has(button)); + } +}); + +test.each(['simulator', 'device'] as const)('iOS %s refuses all TV remote buttons', (kind) => { + const fact = appleNavigationFacts({ + id: 'ios-remote-facts', + name: 'iPhone', + platform: 'apple', + appleOs: 'ios', + kind, + target: 'mobile', + booted: true, + }).tvRemote; + expect(runtimeSubjectFact(fact, 'volume-up')).toMatchObject({ available: false }); +}); diff --git a/packages/platform-apple/src/navigation/runtime.ts b/packages/platform-apple/src/navigation/runtime.ts index dba50102a3..787b45835d 100644 --- a/packages/platform-apple/src/navigation/runtime.ts +++ b/packages/platform-apple/src/navigation/runtime.ts @@ -6,6 +6,7 @@ import type { PlatformRuntimeHost } from '@agent-device/contracts/platform-runti import type { RuntimeOperationFact } from '@agent-device/contracts/platform-runtime'; import { systemButtonRuntimeOperationFacts } from '@agent-device/contracts/system-button-runtime'; import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote-runtime'; +import { tvRemoteButtonsForPlatform } from '@agent-device/contracts/tv-remote'; import { hasAppleActionButton, isTvOsDevice, @@ -173,7 +174,17 @@ export function appleNavigationFacts(device: DeviceInfo) { actionButton: appleActionButtonFact(device), }), ...orientationRuntimeOperationFacts({ orientation: appleOrientationFact(device) }), - ...tvRemoteRuntimeOperationFacts({ tvRemote: appleTvRemoteFact(device) }), + ...tvRemoteRuntimeOperationFacts({ + tvRemote: appleTvRemoteFact(device), + buttonSupport: { + supported: tvRemoteButtonsForPlatform('apple'), + unsupported: { + available: false, + reason: 'unsupported-platform-leaf', + hint: 'This TV remote button is not supported on tvOS.', + }, + }, + }), ...keyboardRuntimeOperationFacts({ unsupported: keyboardCellUnavailable, status: appleKeyboardStatusFact(device), diff --git a/packages/platform-vega/src/runtime.test.ts b/packages/platform-vega/src/runtime.test.ts index 14727c4e07..a1affc76d3 100644 --- a/packages/platform-vega/src/runtime.test.ts +++ b/packages/platform-vega/src/runtime.test.ts @@ -1,4 +1,5 @@ import { expect, test } from 'vitest'; +import { runtimeSubjectFact } from '@agent-device/contracts/platform-runtime'; import type { DeviceBinding } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeHost, @@ -165,6 +166,17 @@ test.each([ expect(binding.facts.operations[operation].available).toBe(supported); expect(binding.operations[operation]).toBeTypeOf(supported ? 'function' : 'undefined'); } + if (supported) { + expect(runtimeSubjectFact(binding.facts.operations.tvRemote, 'select')).toEqual({ + available: true, + }); + for (const button of ['volume-up', 'android-keycode:300']) { + expect(runtimeSubjectFact(binding.facts.operations.tvRemote, button)).toMatchObject({ + available: false, + reason: 'unsupported-platform-leaf', + }); + } + } expect(binding.facts.operations.setOrientation).toMatchObject({ available: false, reason: 'unsupported-platform-leaf', diff --git a/packages/platform-vega/src/runtime.ts b/packages/platform-vega/src/runtime.ts index 2236f8ff4e..77902ba05b 100644 --- a/packages/platform-vega/src/runtime.ts +++ b/packages/platform-vega/src/runtime.ts @@ -20,6 +20,7 @@ import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/p import { TARGET_AUTHORED_DRAG_UNSUPPORTED_HINT } from '@agent-device/contracts/gesture-admission'; import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime'; import { tvRemoteRuntimeOperationFacts } from '@agent-device/contracts/tv-remote-runtime'; +import { tvRemoteButtonsForPlatform } from '@agent-device/contracts/tv-remote'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { bindLocalDirectApplicationLifecycle } from '@agent-device/contracts/application-lifecycle-interaction'; @@ -240,6 +241,14 @@ function vegaFacts(device: DeviceInfo): RuntimeFacts }), ...tvRemoteRuntimeOperationFacts({ tvRemote: supported ? lifecycleAvailable : tvRemoteUnavailable, + buttonSupport: { + supported: tvRemoteButtonsForPlatform('vega'), + unsupported: { + available: false, + reason: 'unsupported-platform-leaf', + hint: 'This TV remote button is not supported on Vega OS.', + }, + }, }), // Two gesture tiers keep the retired closures' own wording instead of this owner's generic // one; the rest had no retired closure and now refuse at admission rather than inside the diff --git a/packages/session-journal/src/__tests__/session-event-action.test.ts b/packages/session-journal/src/__tests__/session-event-action.test.ts index 4f3be01054..d2b3d348c8 100644 --- a/packages/session-journal/src/__tests__/session-event-action.test.ts +++ b/packages/session-journal/src/__tests__/session-event-action.test.ts @@ -19,6 +19,25 @@ function action( }; } +test.each(['volume-down', 'android-keycode:300'])( + 'TV action events retain button %s in summaries and details', + (button) => { + const press = action('tv-remote', { button, durationMs: 500 }); + assert.equal(buildActionSummary(press), `Pressed TV remote ${button}`); + assert.equal(buildActionDetails(press).button, button); + assert.equal(buildActionDetails(press).durationMs, 500); + }, +); + +test.each(['volume-sideways', 'android-keycode:2147483648'])( + 'TV action events omit invalid button %s', + (button) => { + const press = action('tv-remote', { button }); + assert.equal(buildActionSummary(press), 'Used TV remote'); + assert.equal(buildActionDetails(press).button, undefined); + }, +); + test('scroll action events describe direction and distance', () => { const byPixels = action('scroll', { direction: 'down', diff --git a/packages/session-journal/src/session-event-action-presentation.ts b/packages/session-journal/src/session-event-action-presentation.ts index 461050cab3..25b5612e4a 100644 --- a/packages/session-journal/src/session-event-action-presentation.ts +++ b/packages/session-journal/src/session-event-action-presentation.ts @@ -1,7 +1,7 @@ import type { SessionAction } from '@agent-device/contracts/session'; import { DEVICE_ROTATIONS, FOLD_POSES, TELEPHONY_ACTIONS } from '@agent-device/contracts/device'; import { BACK_MODES } from '@agent-device/contracts/back-mode'; -import { TV_REMOTE_BUTTONS } from '@agent-device/contracts/tv-remote'; +import { isTvRemoteButton } from '@agent-device/contracts/tv-remote'; import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { isKeyboardAction } from './keyboard-actions.ts'; import { @@ -105,7 +105,7 @@ export function buildStructuredActionDetails(action: SessionAction): Record): Record): string { - const button = readEnum(result.button, TV_REMOTE_BUTTONS); + const button = isTvRemoteButton(result.button) ? result.button : undefined; return button ? `Pressed TV remote ${button}` : 'Used TV remote'; } diff --git a/src/commands/command-contract.ts b/src/commands/command-contract.ts index 8456ec0776..a1b8faa6c3 100644 --- a/src/commands/command-contract.ts +++ b/src/commands/command-contract.ts @@ -3,6 +3,7 @@ import type { InputAudienceMap } from './input-audience.ts'; export type JsonSchema = { type?: string | readonly string[]; description?: string; + pattern?: string; properties?: Record; required?: readonly string[]; additionalProperties?: boolean | JsonSchema; diff --git a/src/commands/schema/cli-help-command-usage.test.ts b/src/commands/schema/cli-help-command-usage.test.ts index ff4d7aaecd..1bf9fa584a 100644 --- a/src/commands/schema/cli-help-command-usage.test.ts +++ b/src/commands/schema/cli-help-command-usage.test.ts @@ -58,7 +58,7 @@ test('usageForCommand documents tv-remote longpress preset', async () => { assert.equal(help === null, false); assert.match(help ?? '', /agent-device tv-remote \[press\|longpress\]/); assert.match(help ?? '', /--duration-ms /); - assert.match(help ?? '', /longpress holds for 500ms by default/); + assert.match(help ?? '', /longpress selects the 500ms preset/); }); test('usageForCommand supports legacy long-press alias', async () => { diff --git a/src/commands/schema/cli-help.ts b/src/commands/schema/cli-help.ts index 84f2fbd017..3521534e33 100644 --- a/src/commands/schema/cli-help.ts +++ b/src/commands/schema/cli-help.ts @@ -258,9 +258,10 @@ Vega OS: Snapshot, screenshot, selectors, install, touch/text/gesture, logs, and performance commands remain unsupported until their Vega backends are implemented. Buttons: - tv-remote press up|down|left|right|select|menu|home|back - tv-remote longpress select - tv-remote press select --duration-ms 500 + Examples: tv-remote longpress select; tv-remote press select --duration-ms 500. Syntax: tv-remote [press|longpress]