diff --git a/CHANGELOG.md b/CHANGELOG.md index 40867193..1b069149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Update UI automation guard guidance to point at `debug_continue` when paused. - Fix tool loading bugs in static tool registration. - Fix xcodemake command argument corruption when project directory path appears as substring in non-path arguments. +- Fix snapshot_ui warning state being isolated per UI automation tool, causing false warnings. ## [1.16.0] - 2025-12-30 - Remove dynamic tool discovery (`discover_tools`) and `XCODEBUILDMCP_DYNAMIC_TOOLS`. Use `XCODEBUILDMCP_ENABLED_WORKFLOWS` to limit startup tool registration. diff --git a/src/mcp/tools/ui-automation/long_press.ts b/src/mcp/tools/ui-automation/long_press.ts index 11dc7182..2ca289b5 100644 --- a/src/mcp/tools/ui-automation/long_press.ts +++ b/src/mcp/tools/ui-automation/long_press.ts @@ -29,6 +29,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { getSnapshotUiWarning } from './shared/snapshot-ui-state.ts'; // Define schema as ZodObject const longPressSchema = z.object({ @@ -99,7 +100,7 @@ export async function long_pressLogic( await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const coordinateWarning = getCoordinateWarning(simulatorId); + const coordinateWarning = getSnapshotUiWarning(simulatorId); const message = `Long press at (${x}, ${y}) for ${duration}ms simulated successfully.`; const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n'); @@ -153,30 +154,6 @@ export default { }), }; -// Session tracking for snapshot_ui warnings -interface DescribeUISession { - timestamp: number; - simulatorId: string; -} - -const snapshotUiTimestamps = new Map(); -const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds - -function getCoordinateWarning(simulatorId: string): string | null { - const session = snapshotUiTimestamps.get(simulatorId); - if (!session) { - return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; - } - - const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { - const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; - } - - return null; -} - // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], diff --git a/src/mcp/tools/ui-automation/shared/snapshot-ui-state.ts b/src/mcp/tools/ui-automation/shared/snapshot-ui-state.ts new file mode 100644 index 00000000..cd0fa28c --- /dev/null +++ b/src/mcp/tools/ui-automation/shared/snapshot-ui-state.ts @@ -0,0 +1,22 @@ +const SNAPSHOT_UI_WARNING_TIMEOUT_MS = 60000; // 60 seconds + +const snapshotUiTimestamps = new Map(); + +export function recordSnapshotUiCall(simulatorId: string): void { + snapshotUiTimestamps.set(simulatorId, Date.now()); +} + +export function getSnapshotUiWarning(simulatorId: string): string | null { + const timestamp = snapshotUiTimestamps.get(simulatorId); + if (!timestamp) { + return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; + } + + const timeSinceDescribe = Date.now() - timestamp; + if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT_MS) { + const secondsAgo = Math.round(timeSinceDescribe / 1000); + return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; + } + + return null; +} diff --git a/src/mcp/tools/ui-automation/snapshot_ui.ts b/src/mcp/tools/ui-automation/snapshot_ui.ts index 040e05d4..c75e62d5 100644 --- a/src/mcp/tools/ui-automation/snapshot_ui.ts +++ b/src/mcp/tools/ui-automation/snapshot_ui.ts @@ -17,6 +17,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { recordSnapshotUiCall } from './shared/snapshot-ui-state.ts'; // Define schema as ZodObject const snapshotUiSchema = z.object({ @@ -34,16 +35,6 @@ export interface AxeHelpers { const LOG_PREFIX = '[AXe]'; -// Session tracking for snapshot_ui warnings (shared across UI tools) -const snapshotUiTimestamps = new Map(); - -function recordSnapshotUICall(simulatorId: string): void { - snapshotUiTimestamps.set(simulatorId, { - timestamp: Date.now(), - simulatorId, - }); -} - /** * Core business logic for snapshot_ui functionality */ @@ -80,7 +71,7 @@ export async function snapshot_uiLogic( ); // Record the snapshot_ui call for warning system - recordSnapshotUICall(simulatorId); + recordSnapshotUiCall(simulatorId); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); const response: ToolResponse = { diff --git a/src/mcp/tools/ui-automation/swipe.ts b/src/mcp/tools/ui-automation/swipe.ts index f332670d..3941af43 100644 --- a/src/mcp/tools/ui-automation/swipe.ts +++ b/src/mcp/tools/ui-automation/swipe.ts @@ -23,6 +23,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { getSnapshotUiWarning } from './shared/snapshot-ui-state.ts'; // Define schema as ZodObject const swipeSchema = z.object({ @@ -119,7 +120,7 @@ export async function swipeLogic( await executeAxeCommand(commandArgs, simulatorId, 'swipe', executor, axeHelpers); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const coordinateWarning = getCoordinateWarning(simulatorId); + const coordinateWarning = getSnapshotUiWarning(simulatorId); const message = `Swipe from (${x1}, ${y1}) to (${x2}, ${y2})${optionsText} simulated successfully.`; const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n'); @@ -170,30 +171,6 @@ export default { }), }; -// Session tracking for snapshot_ui warnings -interface DescribeUISession { - timestamp: number; - simulatorId: string; -} - -const snapshotUiTimestamps = new Map(); -const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds - -function getCoordinateWarning(simulatorId: string): string | null { - const session = snapshotUiTimestamps.get(simulatorId); - if (!session) { - return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; - } - - const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { - const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; - } - - return null; -} - // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[], diff --git a/src/mcp/tools/ui-automation/tap.ts b/src/mcp/tools/ui-automation/tap.ts index 061ec077..b53b8904 100644 --- a/src/mcp/tools/ui-automation/tap.ts +++ b/src/mcp/tools/ui-automation/tap.ts @@ -17,6 +17,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { getSnapshotUiWarning } from './shared/snapshot-ui-state.ts'; export interface AxeHelpers { getAxePath: () => string | null; @@ -90,25 +91,6 @@ const publicSchemaObject = z.strictObject(baseTapSchema.omit({ simulatorId: true const LOG_PREFIX = '[AXe]'; -// Session tracking for snapshot_ui warnings (shared across UI tools) -const snapshotUiTimestamps = new Map(); -const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds - -function getCoordinateWarning(simulatorId: string): string | null { - const session = snapshotUiTimestamps.get(simulatorId); - if (!session) { - return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; - } - - const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { - const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; - } - - return null; -} - export async function tapLogic( params: TapParams, executor: CommandExecutor, @@ -167,7 +149,7 @@ export async function tapLogic( await executeAxeCommand(commandArgs, simulatorId, 'tap', executor, axeHelpers); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const coordinateWarning = usesCoordinates ? getCoordinateWarning(simulatorId) : null; + const coordinateWarning = usesCoordinates ? getSnapshotUiWarning(simulatorId) : null; const message = `${actionDescription} simulated successfully.`; const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n'); diff --git a/src/mcp/tools/ui-automation/touch.ts b/src/mcp/tools/ui-automation/touch.ts index 8f638120..c7c89596 100644 --- a/src/mcp/tools/ui-automation/touch.ts +++ b/src/mcp/tools/ui-automation/touch.ts @@ -24,6 +24,7 @@ import { createSessionAwareTool, getSessionAwareToolSchemaShape, } from '../../../utils/typed-tool-factory.ts'; +import { getSnapshotUiWarning } from './shared/snapshot-ui-state.ts'; // Define schema as ZodObject const touchSchema = z.object({ @@ -95,7 +96,7 @@ export async function touchLogic( await executeAxeCommand(commandArgs, simulatorId, 'touch', executor, axeHelpers); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); - const coordinateWarning = getCoordinateWarning(simulatorId); + const coordinateWarning = getSnapshotUiWarning(simulatorId); const message = `Touch event (${actionText}) at (${x}, ${y}) executed successfully.`; const warnings = [guard.warningText, coordinateWarning].filter(Boolean).join('\n\n'); @@ -147,30 +148,6 @@ export default { }), }; -// Session tracking for snapshot_ui warnings -interface DescribeUISession { - timestamp: number; - simulatorId: string; -} - -const snapshotUiTimestamps = new Map(); -const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds - -function getCoordinateWarning(simulatorId: string): string | null { - const session = snapshotUiTimestamps.get(simulatorId); - if (!session) { - return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; - } - - const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { - const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; - } - - return null; -} - // Helper function for executing axe commands (inlined from src/tools/axe/index.ts) async function executeAxeCommand( commandArgs: string[],