From 41f2d4949f229a92da66161fdb437bab18b6ea23 Mon Sep 17 00:00:00 2001 From: onesuper Date: Thu, 9 Apr 2026 10:36:31 +0800 Subject: [PATCH] feat: include cursor position in wait change detection hasChanged now compares cursor x/y in addition to screen text, title, and fullscreen state. This allows `wait` to resolve when only the cursor moves (e.g. vim navigating without changing text), giving agents more accurate readiness signals without polling. Co-Authored-By: Claude Opus 4.6 --- src/session.test.ts | 22 ++++++++++++++++++++++ src/session.ts | 21 +++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/session.test.ts b/src/session.test.ts index f08b539..62c17e8 100644 --- a/src/session.test.ts +++ b/src/session.test.ts @@ -52,6 +52,28 @@ describe("hasChanged", () => { it("returns true when is_fullscreen changes even if screen is the same", () => { expect(hasChanged(base, { screen: "hello", title: "", is_fullscreen: true })).toBe(true); }); + + it("returns true when cursor position changes even if screen text is the same", () => { + const before = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 0, y: 0 } }; + const current = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 5, y: 0 } }; + expect(hasChanged(before, current)).toBe(true); + }); + + it("returns true when cursor row changes", () => { + const before = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 0, y: 0 } }; + const current = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 0, y: 1 } }; + expect(hasChanged(before, current)).toBe(true); + }); + + it("returns false when cursor position is the same", () => { + const before = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 3, y: 2 } }; + const current = { screen: "hello", title: "", is_fullscreen: false, cursor: { x: 3, y: 2 } }; + expect(hasChanged(before, current)).toBe(false); + }); + + it("ignores cursor when cursor is not provided (backward compatible)", () => { + expect(hasChanged(base, { screen: "hello", title: "", is_fullscreen: false })).toBe(false); + }); }); describe("Session", () => { diff --git a/src/session.ts b/src/session.ts index 1b034eb..a17b44f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -25,14 +25,20 @@ export function extractIsFullscreen(bufferNamespace: { active: { type: string } /** Check if any observable state has changed between two snapshots. */ export function hasChanged( - before: { screen: string; title: string; is_fullscreen: boolean }, - current: { screen: string; title: string; is_fullscreen: boolean } + before: { screen: string; title: string; is_fullscreen: boolean; cursor?: { x: number; y: number } }, + current: { screen: string; title: string; is_fullscreen: boolean; cursor?: { x: number; y: number } } ): boolean { - return ( + if ( current.screen !== before.screen || current.title !== before.title || current.is_fullscreen !== before.is_fullscreen - ); + ) return true; + + if (before.cursor && current.cursor) { + if (current.cursor.x !== before.cursor.x || current.cursor.y !== before.cursor.y) return true; + } + + return false; } // Special key name → escape sequence mapping @@ -213,6 +219,8 @@ export class Session { const beforeScreen = this.lastSnapshot; const beforeTitle = this._title; const beforeFullscreen = this._isFullscreen; + const buf0 = this.terminal.buffer.active; + const beforeCursor = { x: buf0.cursorX, y: buf0.cursorY }; if (this._status === "exited") { return this.snapshot(); @@ -253,9 +261,10 @@ export class Session { if (new RegExp(text).test(currentScreen)) { done(); return; } } else { // Change mode: resolve when any observable state differs from before AND has been idle + const currentCursor = { x: buf.cursorX, y: buf.cursorY }; if (hasChanged( - { screen: beforeScreen, title: beforeTitle, is_fullscreen: beforeFullscreen }, - { screen: currentScreen, title: this._title, is_fullscreen: this._isFullscreen } + { screen: beforeScreen, title: beforeTitle, is_fullscreen: beforeFullscreen, cursor: beforeCursor }, + { screen: currentScreen, title: this._title, is_fullscreen: this._isFullscreen, cursor: currentCursor } )) { if (idleTimer) clearTimeout(idleTimer); idleTimer = setTimeout(done, 100);