fix: harden AI subtitle fix response parsing

This commit is contained in:
2026-03-08 16:01:40 -07:00
parent 8e319a417d
commit 93cd688625
22 changed files with 641 additions and 55 deletions

View File

@@ -21,13 +21,17 @@ import { WindowGeometry } from '../types';
export type GeometryChangeCallback = (geometry: WindowGeometry) => void;
export type WindowFoundCallback = (geometry: WindowGeometry) => void;
export type WindowLostCallback = () => void;
export type WindowFocusChangeCallback = (focused: boolean) => void;
export abstract class BaseWindowTracker {
protected currentGeometry: WindowGeometry | null = null;
protected windowFound: boolean = false;
protected focusKnown: boolean = false;
protected windowFocused: boolean = false;
public onGeometryChange: GeometryChangeCallback | null = null;
public onWindowFound: WindowFoundCallback | null = null;
public onWindowLost: WindowLostCallback | null = null;
public onWindowFocusChange: WindowFocusChangeCallback | null = null;
abstract start(): void;
abstract stop(): void;
@@ -40,6 +44,19 @@ export abstract class BaseWindowTracker {
return this.windowFound;
}
isFocused(): boolean {
return this.focusKnown ? this.windowFocused : this.windowFound;
}
protected updateFocus(focused: boolean): void {
const changed = !this.focusKnown || this.windowFocused !== focused;
this.focusKnown = true;
this.windowFocused = focused;
if (changed) {
this.onWindowFocusChange?.(focused);
}
}
protected updateGeometry(newGeometry: WindowGeometry | null): void {
if (newGeometry) {
if (!this.windowFound) {
@@ -58,6 +75,12 @@ export abstract class BaseWindowTracker {
if (this.onGeometryChange) this.onGeometryChange(newGeometry);
}
} else {
const focusChanged = this.focusKnown && this.windowFocused;
this.focusKnown = false;
this.windowFocused = false;
if (focusChanged) {
this.onWindowFocusChange?.(false);
}
if (this.windowFound) {
this.windowFound = false;
this.currentGeometry = null;

View File

@@ -22,9 +22,56 @@ import * as fs from 'fs';
import * as os from 'os';
import { BaseWindowTracker } from './base-tracker';
import { createLogger } from '../logger';
import type { WindowGeometry } from '../types';
const log = createLogger('tracker').child('macos');
export interface MacOSHelperWindowState {
geometry: WindowGeometry;
focused: boolean;
}
export function parseMacOSHelperOutput(result: string): MacOSHelperWindowState | null {
const trimmed = result.trim();
if (!trimmed || trimmed === 'not-found') {
return null;
}
const parts = trimmed.split(',');
if (parts.length !== 4 && parts.length !== 5) {
return null;
}
const x = parseInt(parts[0]!, 10);
const y = parseInt(parts[1]!, 10);
const width = parseInt(parts[2]!, 10);
const height = parseInt(parts[3]!, 10);
if (
!Number.isFinite(x) ||
!Number.isFinite(y) ||
!Number.isFinite(width) ||
!Number.isFinite(height) ||
width <= 0 ||
height <= 0
) {
return null;
}
const focusedRaw = parts[4]?.trim().toLowerCase();
const focused =
focusedRaw === undefined ? true : focusedRaw === '1' || focusedRaw === 'true';
return {
geometry: {
x,
y,
width,
height,
},
focused,
};
}
export class MacOSWindowTracker extends BaseWindowTracker {
private pollInterval: ReturnType<typeof setInterval> | null = null;
private pollInFlight = false;
@@ -173,33 +220,12 @@ export class MacOSWindowTracker extends BaseWindowTracker {
return;
}
const result = (stdout || '').trim();
if (result && result !== 'not-found') {
const parts = result.split(',');
if (parts.length === 4) {
const x = parseInt(parts[0]!, 10);
const y = parseInt(parts[1]!, 10);
const width = parseInt(parts[2]!, 10);
const height = parseInt(parts[3]!, 10);
if (
Number.isFinite(x) &&
Number.isFinite(y) &&
Number.isFinite(width) &&
Number.isFinite(height) &&
width > 0 &&
height > 0
) {
this.updateGeometry({
x,
y,
width,
height,
});
this.pollInFlight = false;
return;
}
}
const parsed = parseMacOSHelperOutput(stdout || '');
if (parsed) {
this.updateFocus(parsed.focused);
this.updateGeometry(parsed.geometry);
this.pollInFlight = false;
return;
}
this.updateGeometry(null);

View File

@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { parseX11WindowGeometry, parseX11WindowPid, X11WindowTracker } from './x11-tracker';
import { parseMacOSHelperOutput } from './macos-tracker';
test('parseX11WindowGeometry parses xwininfo output', () => {
const geometry = parseX11WindowGeometry(`
@@ -52,3 +53,27 @@ Height: 360`;
release();
await new Promise((resolve) => setTimeout(resolve, 0));
});
test('parseMacOSHelperOutput parses geometry and focused state', () => {
assert.deepEqual(parseMacOSHelperOutput('120,240,1280,720,1'), {
geometry: {
x: 120,
y: 240,
width: 1280,
height: 720,
},
focused: true,
});
});
test('parseMacOSHelperOutput tolerates unfocused helper output', () => {
assert.deepEqual(parseMacOSHelperOutput('120,240,1280,720,0'), {
geometry: {
x: 120,
y: 240,
width: 1280,
height: 720,
},
focused: false,
});
});