pi-ui-extend 1.0.41 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ import time
8
+
9
+
10
+ def fail(message: str) -> None:
11
+ print(message, file=sys.stderr)
12
+ raise SystemExit(1)
13
+
14
+
15
+ def parse_options(values: list[str]) -> dict[str, object]:
16
+ result: dict[str, object] = {}
17
+ index = 0
18
+ while index < len(values):
19
+ key = values[index]
20
+ if not key.startswith("--"):
21
+ fail(f"unexpected argument: {key}")
22
+ name = key[2:]
23
+ if name in {"all", "print-pid"}:
24
+ result[name] = True
25
+ index += 1
26
+ continue
27
+ if index + 1 >= len(values):
28
+ fail(f"missing value for {key}")
29
+ if name in result:
30
+ fail(f"duplicate option: {key}")
31
+ result[name] = values[index + 1]
32
+ index += 2
33
+ return result
34
+
35
+
36
+ def require_option(options: dict[str, object], name: str) -> str:
37
+ value = options.get(name)
38
+ if not isinstance(value, str) or not value:
39
+ fail(f"missing --{name}")
40
+ return value
41
+
42
+
43
+ try:
44
+ import pyatspi # type: ignore
45
+ except Exception as error:
46
+ if len(sys.argv) > 1 and sys.argv[1] == "doctor":
47
+ fail(f"AT-SPI Python bindings are unavailable: {error}")
48
+ raise
49
+
50
+
51
+ def process_id(accessible) -> int | None:
52
+ for name in ("get_process_id", "getProcessId"):
53
+ method = getattr(accessible, name, None)
54
+ if callable(method):
55
+ try:
56
+ value = int(method())
57
+ return value if value > 0 else None
58
+ except Exception:
59
+ pass
60
+ return None
61
+
62
+
63
+ def role_name(accessible) -> str:
64
+ try:
65
+ return str(accessible.getRoleName() or "unknown")
66
+ except Exception:
67
+ return "unknown"
68
+
69
+
70
+ def normalize_role(value: str) -> str:
71
+ return "".join(character.lower() for character in value if character.isalnum())
72
+
73
+
74
+ def children(accessible) -> list[object]:
75
+ result: list[object] = []
76
+ try:
77
+ count = int(accessible.childCount)
78
+ for index in range(max(0, count)):
79
+ try:
80
+ child = accessible.getChildAtIndex(index)
81
+ if child is not None:
82
+ result.append(child)
83
+ except Exception:
84
+ continue
85
+ except Exception:
86
+ try:
87
+ result.extend(list(accessible))
88
+ except Exception:
89
+ pass
90
+ return result
91
+
92
+
93
+ def state_contains(accessible, state) -> bool:
94
+ try:
95
+ return bool(accessible.getState().contains(state))
96
+ except Exception:
97
+ return False
98
+
99
+
100
+ def accessible_attributes(accessible) -> dict[str, str]:
101
+ attributes: dict[str, str] = {}
102
+ try:
103
+ if accessible.name:
104
+ attributes["title"] = str(accessible.name)
105
+ except Exception:
106
+ pass
107
+ try:
108
+ if accessible.description:
109
+ attributes["description"] = str(accessible.description)
110
+ except Exception:
111
+ pass
112
+ try:
113
+ for raw in accessible.getAttributes() or []:
114
+ if ":" not in raw:
115
+ continue
116
+ key, value = raw.split(":", 1)
117
+ if key in {"id", "automation-id", "accessible-id"} and value:
118
+ attributes["identifier"] = value
119
+ break
120
+ except Exception:
121
+ pass
122
+
123
+ protected_state = getattr(pyatspi, "STATE_PROTECTED", None)
124
+ if protected_state is not None and state_contains(accessible, protected_state):
125
+ attributes["value"] = "<redacted>"
126
+ else:
127
+ try:
128
+ text = accessible.queryText()
129
+ value = text.getText(0, -1)
130
+ if value:
131
+ attributes["value"] = str(value).replace("\r", " ").replace("\n", " ")
132
+ except Exception:
133
+ try:
134
+ value_iface = accessible.queryValue()
135
+ numeric = float(value_iface.currentValue)
136
+ attributes["value"] = str(int(numeric)) if numeric.is_integer() else str(numeric)
137
+ except Exception:
138
+ pass
139
+
140
+ if "value" not in attributes and hasattr(pyatspi, "STATE_CHECKED"):
141
+ try:
142
+ states = accessible.getState()
143
+ if states.contains(pyatspi.STATE_CHECKED):
144
+ attributes["value"] = "1"
145
+ elif normalize_role(role_name(accessible)) in {"checkbox", "checkmenuitem"}:
146
+ attributes["value"] = "0"
147
+ except Exception:
148
+ pass
149
+
150
+ attributes["enabled"] = str(state_contains(accessible, pyatspi.STATE_ENABLED)).lower()
151
+ attributes["focused"] = str(state_contains(accessible, pyatspi.STATE_FOCUSED)).lower()
152
+ attributes["selected"] = str(state_contains(accessible, pyatspi.STATE_SELECTED)).lower()
153
+ return attributes
154
+
155
+
156
+ def frame(accessible) -> dict[str, int]:
157
+ try:
158
+ extents = accessible.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
159
+ return {
160
+ "x": int(extents.x),
161
+ "y": int(extents.y),
162
+ "width": int(extents.width),
163
+ "height": int(extents.height),
164
+ }
165
+ except Exception:
166
+ return {"x": 0, "y": 0, "width": 0, "height": 0}
167
+
168
+
169
+ def record(accessible, path_value: str) -> dict[str, object]:
170
+ return {
171
+ "path": path_value,
172
+ "role": role_name(accessible),
173
+ "attributes": accessible_attributes(accessible),
174
+ "frame": frame(accessible),
175
+ }
176
+
177
+
178
+ def walk(root, depth: int, limit: int) -> list[tuple[object, str]]:
179
+ result: list[tuple[object, str]] = []
180
+
181
+ def visit(accessible, path_value: str, level: int) -> None:
182
+ if len(result) >= limit:
183
+ return
184
+ result.append((accessible, path_value))
185
+ if level >= depth:
186
+ return
187
+ for index, child in enumerate(children(accessible)):
188
+ visit(child, f"{path_value}/{index}", level + 1)
189
+ if len(result) >= limit:
190
+ return
191
+
192
+ visit(root, "0", 0)
193
+ return result
194
+
195
+
196
+ def application_windows(application) -> list[object]:
197
+ direct = children(application)
198
+ windows = []
199
+ for item in direct:
200
+ role = normalize_role(role_name(item))
201
+ if role in {"frame", "dialog", "window", "application"}:
202
+ windows.append(item)
203
+ return windows or direct or [application]
204
+
205
+
206
+ def candidate_process_matches(pid: int | None, options: dict[str, object]) -> bool:
207
+ if pid is None:
208
+ return not any(key in options for key in ("pid", "pgid"))
209
+ if "pid" in options and pid != int(require_option(options, "pid")):
210
+ return False
211
+ if "pgid" in options:
212
+ try:
213
+ if os.getpgid(pid) != int(require_option(options, "pgid")):
214
+ return False
215
+ except (ProcessLookupError, PermissionError, OSError):
216
+ return False
217
+ return True
218
+
219
+
220
+ def find_window(options: dict[str, object]):
221
+ desktop = pyatspi.Registry.getDesktop(0)
222
+ requested_app = str(options.get("app", "")).lower()
223
+ requested_title = str(options.get("title", ""))
224
+ matches: list[tuple[object, int | None]] = []
225
+ for application in children(desktop):
226
+ pid = process_id(application)
227
+ if not candidate_process_matches(pid, options):
228
+ continue
229
+ try:
230
+ app_name = str(application.name or "")
231
+ except Exception:
232
+ app_name = ""
233
+ for window in application_windows(application):
234
+ try:
235
+ window_name = str(window.name or "")
236
+ except Exception:
237
+ window_name = ""
238
+ if requested_title and requested_title.lower() not in window_name.lower():
239
+ continue
240
+ if requested_app and requested_app not in app_name.lower() and requested_app not in window_name.lower():
241
+ continue
242
+ matches.append((window, pid))
243
+ if not matches:
244
+ return None
245
+ if len(matches) > 1 and not any(key in options for key in ("pid", "pgid", "title")):
246
+ fail(f"ambiguous app selector ({len(matches)} windows); use --pid")
247
+ return matches[0]
248
+
249
+
250
+ def wait_window(options: dict[str, object], timeout: float):
251
+ deadline = time.monotonic() + timeout
252
+ while time.monotonic() < deadline:
253
+ candidate = find_window(options)
254
+ if candidate is not None:
255
+ return candidate
256
+ time.sleep(0.1)
257
+ fail("timed out waiting for application window")
258
+
259
+
260
+ def focus_window(window) -> None:
261
+ try:
262
+ if window.queryComponent().grabFocus():
263
+ return
264
+ except Exception:
265
+ pass
266
+ try:
267
+ window.grabFocus()
268
+ return
269
+ except Exception:
270
+ fail("AT-SPI could not focus the target window")
271
+
272
+
273
+ def element_by_path(window, path_value: str):
274
+ parts = [part for part in path_value.split("/") if part]
275
+ current = window
276
+ start = 1 if parts and parts[0] == "0" else 0
277
+ for raw in parts[start:]:
278
+ try:
279
+ index = int(raw)
280
+ except ValueError:
281
+ fail("invalid element path")
282
+ available = children(current)
283
+ if index < 0 or index >= len(available):
284
+ fail("element path does not exist")
285
+ current = available[index]
286
+ return current
287
+
288
+
289
+ def find_element(window, options: dict[str, object]):
290
+ if "path" in options:
291
+ return element_by_path(window, require_option(options, "path"))
292
+ match = require_option(options, "match").lower()
293
+ requested_role = normalize_role(str(options.get("role", "")).replace("AX", ""))
294
+ occurrence = int(str(options.get("occurrence", "1")))
295
+ seen = 0
296
+ for accessible, _ in walk(window, 20, 1000):
297
+ attributes = accessible_attributes(accessible)
298
+ haystack = " ".join(attributes.values()).lower()
299
+ if match not in haystack:
300
+ continue
301
+ if requested_role and normalize_role(role_name(accessible)) != requested_role:
302
+ continue
303
+ seen += 1
304
+ if seen == occurrence:
305
+ return accessible
306
+ fail("no accessibility element matches the supplied selector")
307
+
308
+
309
+ def format_record(accessible, path_value: str) -> str:
310
+ value = record(accessible, path_value)
311
+ parts = [path_value, str(value["role"])]
312
+ for key, item in value["attributes"].items():
313
+ safe = str(item).replace("\r", " ").replace("\n", " ").replace('"', '\\"')
314
+ if safe:
315
+ parts.append(f'{key}="{safe}"')
316
+ rectangle = value["frame"]
317
+ parts.append(f'frame={rectangle["x"]},{rectangle["y"]},{rectangle["width"]}x{rectangle["height"]}')
318
+ return " ".join(parts)
319
+
320
+
321
+ def click_element(accessible) -> None:
322
+ try:
323
+ actions = accessible.queryAction()
324
+ for index in range(actions.nActions):
325
+ name = str(actions.getName(index) or "").lower()
326
+ if name in {"click", "press", "activate", "open", "toggle"}:
327
+ if actions.doAction(index):
328
+ return
329
+ if actions.nActions > 0 and actions.doAction(0):
330
+ return
331
+ except Exception:
332
+ pass
333
+ rectangle = frame(accessible)
334
+ if rectangle["width"] <= 0 or rectangle["height"] <= 0:
335
+ fail("element has no AT-SPI action or visible frame")
336
+ x = rectangle["x"] + rectangle["width"] // 2
337
+ y = rectangle["y"] + rectangle["height"] // 2
338
+ try:
339
+ pyatspi.Registry.generateMouseEvent(x, y, "b1c")
340
+ except Exception as error:
341
+ fail(f"AT-SPI click fallback failed: {error}")
342
+
343
+
344
+ def set_value(accessible, value: str) -> None:
345
+ try:
346
+ accessible.queryEditableText().setTextContents(value)
347
+ return
348
+ except Exception:
349
+ pass
350
+ try:
351
+ value_iface = accessible.queryValue()
352
+ value_iface.currentValue = float(value)
353
+ return
354
+ except Exception:
355
+ fail("element does not expose editable text or numeric AT-SPI value")
356
+
357
+
358
+ def xdotool() -> str | None:
359
+ return shutil.which("xdotool")
360
+
361
+
362
+ def keyboard_backend_available() -> bool:
363
+ tool = xdotool()
364
+ if tool is None:
365
+ return False
366
+ try:
367
+ result = subprocess.run(
368
+ [tool, "getactivewindow"],
369
+ stdin=subprocess.DEVNULL,
370
+ stdout=subprocess.PIPE,
371
+ stderr=subprocess.PIPE,
372
+ text=True,
373
+ timeout=3,
374
+ )
375
+ return result.returncode == 0
376
+ except (OSError, subprocess.SubprocessError):
377
+ return False
378
+
379
+
380
+ def screenshot_backend() -> tuple[str, str] | None:
381
+ executable = shutil.which("gnome-screenshot")
382
+ if executable:
383
+ return executable, "gnome-screenshot"
384
+ executable = shutil.which("scrot")
385
+ if executable:
386
+ return executable, "scrot"
387
+ return None
388
+
389
+
390
+ def capture_window(window, output: str) -> None:
391
+ backend = screenshot_backend()
392
+ if backend is None:
393
+ fail("no supported Linux window screenshot producer is installed")
394
+ directory = os.path.dirname(os.path.abspath(output))
395
+ if not os.path.isdir(directory):
396
+ fail("screenshot output directory does not exist")
397
+ focus_window(window)
398
+ time.sleep(0.12)
399
+ executable, kind = backend
400
+ if kind == "gnome-screenshot":
401
+ command = [executable, "-w", "-f", output]
402
+ else:
403
+ command = [executable, "-u", output]
404
+ result = subprocess.run(command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
405
+ if result.returncode != 0 or not os.path.isfile(output) or os.path.getsize(output) < 1:
406
+ fail((result.stderr or result.stdout or "window screenshot producer failed").strip())
407
+
408
+
409
+ def type_text(window, text: str) -> None:
410
+ tool = xdotool()
411
+ if tool is None:
412
+ fail("xdotool is required for Linux keyboard input")
413
+ focus_window(window)
414
+ result = subprocess.run([tool, "type", "--delay", "0", "--clearmodifiers", text], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
415
+ if result.returncode != 0:
416
+ fail((result.stderr or "xdotool type failed").strip())
417
+
418
+
419
+ def press_key(window, key: str, modifiers: str) -> None:
420
+ tool = xdotool()
421
+ if tool is None:
422
+ fail("xdotool is required for Linux keyboard input")
423
+ names = {
424
+ "enter": "Return", "return": "Return", "escape": "Escape", "esc": "Escape",
425
+ "tab": "Tab", "backspace": "BackSpace", "delete": "Delete", "left": "Left",
426
+ "right": "Right", "up": "Up", "down": "Down", "home": "Home", "end": "End",
427
+ "pageup": "Page_Up", "pagedown": "Page_Down", "space": "space",
428
+ }
429
+ lower = key.lower()
430
+ key_name = names.get(lower, key if len(key) == 1 else None)
431
+ if key_name is None:
432
+ fail(f"unsupported key: {key}")
433
+ modifier_names = {"cmd": "super", "shift": "shift", "ctrl": "ctrl", "opt": "alt", "alt": "alt"}
434
+ prefix: list[str] = []
435
+ for raw in [item.strip().lower() for item in modifiers.split(",") if item.strip()]:
436
+ if raw == "fn":
437
+ fail("fn modifier is not supported by the Linux AT-SPI helper")
438
+ if raw not in modifier_names:
439
+ fail(f"unsupported modifier: {raw}")
440
+ prefix.append(modifier_names[raw])
441
+ chord = "+".join(prefix + [key_name])
442
+ focus_window(window)
443
+ result = subprocess.run([tool, "key", "--clearmodifiers", chord], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10)
444
+ if result.returncode != 0:
445
+ fail((result.stderr or "xdotool key failed").strip())
446
+
447
+
448
+ if len(sys.argv) < 2:
449
+ fail("missing command")
450
+ command = sys.argv[1]
451
+ options = parse_options(sys.argv[2:])
452
+
453
+ if command == "doctor":
454
+ try:
455
+ pyatspi.Registry.getDesktop(0)
456
+ except Exception as error:
457
+ fail(f"AT-SPI registry is unavailable: {error}")
458
+ print("atspi=available")
459
+ print(f'keyboard_input={"available" if keyboard_backend_available() else "unavailable"}')
460
+ backend = screenshot_backend()
461
+ print(f'screenshot={"available" if backend else "unavailable"}')
462
+ if backend:
463
+ print(f"screenshot_backend={backend[1]}")
464
+ raise SystemExit(0)
465
+
466
+ timeout = float(str(options.get("timeout", "15")))
467
+ window, pid = wait_window(options, timeout)
468
+
469
+ if command == "wait-window":
470
+ if options.get("print-pid") and pid is not None:
471
+ print(pid)
472
+ elif command == "focus":
473
+ focus_window(window)
474
+ elif command == "inspect":
475
+ depth = int(str(options.get("depth", "10")))
476
+ limit = int(str(options.get("limit", "80")))
477
+ for accessible, path_value in walk(window, depth, limit):
478
+ print(format_record(accessible, path_value))
479
+ elif command == "describe":
480
+ element = find_element(window, options)
481
+ path_value = str(options.get("path", "match"))
482
+ print(json.dumps(record(element, path_value), sort_keys=True, separators=(",", ":")))
483
+ elif command == "screenshot":
484
+ capture_window(window, require_option(options, "out"))
485
+ elif command == "click":
486
+ click_element(find_element(window, options))
487
+ elif command == "set-value":
488
+ set_value(find_element(window, options), require_option(options, "value"))
489
+ elif command == "type":
490
+ type_text(window, require_option(options, "text"))
491
+ elif command == "key":
492
+ press_key(window, require_option(options, "key"), str(options.get("modifiers", "")))
493
+ else:
494
+ fail(f"unsupported command: {command}")