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.
- package/README.md +50 -13
- package/dist/app/app.d.ts +13 -0
- package/dist/app/app.js +158 -8
- package/dist/app/cli/install.js +2 -2
- package/dist/app/commands/command-controller.d.ts +4 -0
- package/dist/app/commands/command-controller.js +11 -0
- package/dist/app/commands/command-git-actions.d.ts +25 -0
- package/dist/app/commands/command-git-actions.js +381 -0
- package/dist/app/commands/command-host.d.ts +4 -0
- package/dist/app/commands/command-host.js +5 -2
- package/dist/app/commands/command-model-actions.d.ts +1 -0
- package/dist/app/commands/command-model-actions.js +57 -35
- package/dist/app/commands/command-navigation-actions.d.ts +1 -0
- package/dist/app/commands/command-navigation-actions.js +16 -3
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +15 -1
- package/dist/app/commands/command-session-actions.js +9 -3
- package/dist/app/commands/reload-context-inventory.d.ts +11 -0
- package/dist/app/commands/reload-context-inventory.js +70 -0
- package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
- package/dist/app/extensions/extension-actions-controller.js +6 -0
- package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
- package/dist/app/extensions/subagent-catalog-state.js +23 -0
- package/dist/app/input/autocomplete-controller.js +61 -34
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/input/input-controller.d.ts +2 -1
- package/dist/app/input/input-controller.js +7 -2
- package/dist/app/input/prompt-enhancer-controller.js +53 -35
- package/dist/app/input/voice-controller.d.ts +41 -45
- package/dist/app/input/voice-controller.js +351 -419
- package/dist/app/model/model-usage-controller.js +17 -7
- package/dist/app/model/model-usage-status.d.ts +4 -1
- package/dist/app/model/model-usage-status.js +270 -46
- package/dist/app/popup/menu-items-controller.d.ts +13 -3
- package/dist/app/popup/menu-items-controller.js +37 -21
- package/dist/app/popup/popup-action-controller.d.ts +12 -2
- package/dist/app/popup/popup-action-controller.js +77 -24
- package/dist/app/popup/popup-menu-controller.d.ts +36 -14
- package/dist/app/popup/popup-menu-controller.js +239 -69
- package/dist/app/rendering/dcp-stats.d.ts +9 -4
- package/dist/app/rendering/dcp-stats.js +40 -425
- package/dist/app/rendering/editor-panels.js +10 -4
- package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
- package/dist/app/rendering/popup-menu-renderer.js +42 -37
- package/dist/app/rendering/render-controller.js +23 -2
- package/dist/app/rendering/status-line-renderer.d.ts +5 -0
- package/dist/app/rendering/status-line-renderer.js +41 -5
- package/dist/app/rendering/tab-line-renderer.js +26 -20
- package/dist/app/runtime.d.ts +12 -1
- package/dist/app/runtime.js +120 -12
- package/dist/app/screen/mouse-controller.d.ts +2 -0
- package/dist/app/screen/mouse-controller.js +17 -7
- package/dist/app/screen/status-controller.d.ts +4 -0
- package/dist/app/screen/status-controller.js +5 -0
- package/dist/app/session/lazy-session-manager.js +34 -0
- package/dist/app/session/session-event-controller.d.ts +1 -0
- package/dist/app/session/session-event-controller.js +10 -1
- package/dist/app/session/session-history.d.ts +1 -0
- package/dist/app/session/session-history.js +12 -1
- package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
- package/dist/app/session/session-lifecycle-controller.js +16 -1
- package/dist/app/session/tabs-controller.d.ts +26 -1
- package/dist/app/session/tabs-controller.js +454 -147
- package/dist/app/subagents/subagents-files.js +60 -1
- package/dist/app/subagents/subagents-model.d.ts +1 -0
- package/dist/app/subagents/subagents-model.js +18 -2
- package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
- package/dist/app/subagents/subagents-widget-controller.js +6 -0
- package/dist/app/types.d.ts +16 -1
- package/dist/app/workspace/workspace-actions-controller.js +10 -2
- package/dist/app/workspace/workspace-undo.d.ts +1 -0
- package/dist/app/workspace/workspace-undo.js +1 -0
- package/dist/bundled-extensions/question/index.js +9 -1
- package/dist/bundled-extensions/question/remote.d.ts +4 -0
- package/dist/bundled-extensions/question/remote.js +33 -0
- package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
- package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
- package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
- package/dist/bundled-extensions/telegram-connector/config.js +87 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
- package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
- package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
- package/dist/bundled-extensions/telegram-connector/index.js +195 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
- package/dist/bundled-extensions/terminal-bell/index.js +0 -76
- package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
- package/dist/bundled-extensions/workspace-undo/index.js +191 -0
- package/dist/config.d.ts +21 -2
- package/dist/config.js +159 -32
- package/dist/default-pix-config.js +25 -5
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
- package/dist/schemas/pi-tools-suite-schema.js +54 -83
- package/dist/schemas/pix-schema.d.ts +19 -2
- package/dist/schemas/pix-schema.js +47 -5
- package/dist/schemas/tasks-schema.d.ts +18 -0
- package/dist/schemas/tasks-schema.js +41 -0
- package/docs/concurrency.md +9 -1
- package/docs/desktop-mvp.md +22 -6
- package/docs/desktop-task-manager.md +148 -84
- package/docs/release.md +30 -6
- package/external/pi-tools-suite/README.md +197 -84
- package/external/pi-tools-suite/docs/evals.md +1 -1
- package/external/pi-tools-suite/docs/session-recovery.md +47 -14
- package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
- package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
- package/external/pi-tools-suite/package.json +6 -1
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
- package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
- package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
- package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
- package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
- package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
- package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
- package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
- package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
- package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
- package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
- package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
- package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
- package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
- package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
- package/external/pi-tools-suite/src/config.ts +55 -1
- package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
- package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
- package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
- package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
- package/external/pi-tools-suite/src/context-inventory.ts +99 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
- package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
- package/external/pi-tools-suite/src/dcp/config.ts +114 -17
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
- package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
- package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
- package/external/pi-tools-suite/src/dcp/index.ts +338 -66
- package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
- package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
- package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
- package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
- package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
- package/external/pi-tools-suite/src/dcp/state.ts +9 -0
- package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
- package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
- package/external/pi-tools-suite/src/index.ts +7 -1
- package/external/pi-tools-suite/src/lib/project.ts +36 -1
- package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
- package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
- package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
- package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
- package/external/pi-tools-suite/src/todo/index.ts +21 -0
- package/external/pi-tools-suite/src/todo/todo.ts +19 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
- package/package.json +10 -9
- package/schemas/pi-tools-suite.json +466 -287
- package/schemas/pix.json +129 -11
- package/schemas/tasks.json +131 -0
- package/skills/simplify/SKILL.md +33 -5
- package/docs/desktop-markdown-media.md +0 -77
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
- package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
- package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
- package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
- package/skills/playwright-cli/SKILL.md +0 -420
- package/skills/playwright-cli/references/element-attributes.md +0 -23
- package/skills/playwright-cli/references/playwright-tests.md +0 -50
- package/skills/playwright-cli/references/request-mocking.md +0 -87
- package/skills/playwright-cli/references/running-code.md +0 -241
- package/skills/playwright-cli/references/session-management.md +0 -273
- package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
- package/skills/playwright-cli/references/storage-state.md +0 -290
- package/skills/playwright-cli/references/test-generation.md +0 -142
- package/skills/playwright-cli/references/tracing.md +0 -154
- package/skills/playwright-cli/references/video-recording.md +0 -147
- package/skills/spec-lite/SKILL.md +0 -140
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
- /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}")
|