mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-07-26 09:02:17 +00:00
Settle the Playwright-grade reliability contract in agent-desktop-core before the Windows/Linux adapters are built, so they inherit it instead of redesigning it. Every command now observes, waits, verifies, and reports honestly instead of firing blindly. Highlights: capability-supertrait split of PlatformAdapter with not_supported() defaults; canonical role/state vocabulary with live `is --property visible`; display enumeration (`list-displays`) and honest `--screen` with scale factor; truthful Automation permission; `native_id` identity spine; window-id-first resolution; serializable `LocatorQuery` with live `find`; default-on auto-wait before every ref action; three-way `hit_test` occlusion gate; `scroll_into_view` in core; core accessible-name precedence; typed `ActionStep` delivery tier; `ProcessState` and `APP_UNRESPONSIVE`; `LaunchOptions`; baseline-diff desktop signals (`wait --event`); typed clipboard (`Text`/`Image`/`FileUrls`); mouse modifier chords and `mouse-wheel`. Hardened through a 35-reviewer pass with independent validation and a green live e2e gate (109/0), plus a head-vs-main performance comparison harness. BREAKING CHANGE: default-on auto-wait changes the timing of every previously-untouched ref-action call (bounded 5000 ms default; `--timeout-ms 0` restores single-shot). `ENVELOPE_VERSION` is now `2.1` (adds the `APP_UNRESPONSIVE` code and process state in error details). FFI ABI major is `3` (append-only struct evolution; `wait --event` is intentionally not exposed over FFI). The legacy string clipboard API is removed in favor of typed content. `key-down`/`key-up` fail closed until daemon-owned held input exists. `close-app` verifies termination and the osascript fallback path is removed. `--text` matching is subtree containment: `find --text X --first` returns the outermost matching container.
139 lines
4.3 KiB
Python
139 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def raw_string_end(source, start):
|
|
prefix_length = 2 if source.startswith("br", start) else 1
|
|
if source[start : start + prefix_length] not in ("r", "br"):
|
|
return None
|
|
if start > 0 and (source[start - 1].isalnum() or source[start - 1] == "_"):
|
|
return None
|
|
cursor = start + prefix_length
|
|
hashes = 0
|
|
while cursor < len(source) and source[cursor] == "#":
|
|
hashes += 1
|
|
cursor += 1
|
|
if cursor >= len(source) or source[cursor] != '"':
|
|
return None
|
|
marker = '"' + "#" * hashes
|
|
end = source.find(marker, cursor + 1)
|
|
return len(source) if end < 0 else end + len(marker)
|
|
|
|
|
|
def quoted_string_end(source, start):
|
|
prefix_length = 2 if source.startswith('b"', start) else 1
|
|
if source[start : start + prefix_length] not in ('"', 'b"'):
|
|
return None
|
|
if prefix_length == 2 and start > 0 and (
|
|
source[start - 1].isalnum() or source[start - 1] == "_"
|
|
):
|
|
return None
|
|
cursor = start + prefix_length
|
|
escaped = False
|
|
while cursor < len(source):
|
|
character = source[cursor]
|
|
if escaped:
|
|
escaped = False
|
|
elif character == "\\":
|
|
escaped = True
|
|
elif character == '"':
|
|
return cursor + 1
|
|
cursor += 1
|
|
return len(source)
|
|
|
|
|
|
def block_comment_end(source, start):
|
|
cursor = start + 2
|
|
depth = 1
|
|
while cursor < len(source) and depth:
|
|
if source.startswith("/*", cursor):
|
|
depth += 1
|
|
cursor += 2
|
|
elif source.startswith("*/", cursor):
|
|
depth -= 1
|
|
cursor += 2
|
|
else:
|
|
cursor += 1
|
|
return cursor
|
|
|
|
|
|
def forbidden_comments(source):
|
|
findings = []
|
|
cursor = 0
|
|
line = 1
|
|
code_on_line = False
|
|
while cursor < len(source):
|
|
raw_end = raw_string_end(source, cursor)
|
|
if raw_end is not None:
|
|
segment = source[cursor:raw_end]
|
|
line += segment.count("\n")
|
|
if "\n" in segment:
|
|
code_on_line = bool(segment.rsplit("\n", 1)[-1].strip())
|
|
else:
|
|
code_on_line = True
|
|
cursor = raw_end
|
|
continue
|
|
string_end = quoted_string_end(source, cursor)
|
|
if string_end is not None:
|
|
segment = source[cursor:string_end]
|
|
line += segment.count("\n")
|
|
if "\n" in segment:
|
|
code_on_line = bool(segment.rsplit("\n", 1)[-1].strip())
|
|
else:
|
|
code_on_line = True
|
|
cursor = string_end
|
|
continue
|
|
if source.startswith("//", cursor):
|
|
is_doc = source.startswith("///", cursor) or source.startswith("//!", cursor)
|
|
if not is_doc:
|
|
kind = "end-of-line comment" if code_on_line else "non-doc line comment"
|
|
findings.append((line, kind))
|
|
end = source.find("\n", cursor + 2)
|
|
if end < 0:
|
|
break
|
|
cursor = end
|
|
continue
|
|
if source.startswith("/*", cursor):
|
|
is_doc = source.startswith("/**", cursor) or source.startswith("/*!", cursor)
|
|
if not is_doc:
|
|
findings.append((line, "block comment"))
|
|
end = block_comment_end(source, cursor)
|
|
line += source[cursor:end].count("\n")
|
|
code_on_line = False if "\n" in source[cursor:end] else code_on_line
|
|
cursor = end
|
|
continue
|
|
character = source[cursor]
|
|
if character == "\n":
|
|
line += 1
|
|
code_on_line = False
|
|
elif not character.isspace():
|
|
code_on_line = True
|
|
cursor += 1
|
|
return findings
|
|
|
|
|
|
def check_path(path):
|
|
if not path.is_file():
|
|
return []
|
|
source = path.read_text(encoding="utf-8")
|
|
if "@generated" in "\n".join(source.splitlines()[:5]):
|
|
return []
|
|
return forbidden_comments(source)
|
|
|
|
|
|
def main():
|
|
failed = False
|
|
for raw_path in sys.stdin.buffer.read().split(b"\0"):
|
|
if not raw_path:
|
|
continue
|
|
path = Path(raw_path.decode())
|
|
for line, kind in check_path(path):
|
|
print(f"{path}:{line}: {kind}s are forbidden", file=sys.stderr)
|
|
failed = True
|
|
raise SystemExit(1 if failed else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|