androidctl 0.1.0__py3-none-any.whl
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.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- androidctld/waits/matcher.py +41 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""Command capability validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
from androidctld.actions.focused_input_admissibility import (
|
|
8
|
+
blocked_by_group_fields,
|
|
9
|
+
focus_mismatch_fields,
|
|
10
|
+
keyboard_blocker_allows_public_type,
|
|
11
|
+
keyboard_blocker_allows_semantic_type,
|
|
12
|
+
public_focused_input_ref,
|
|
13
|
+
public_node_is_focused_input,
|
|
14
|
+
semantic_focused_input_ref,
|
|
15
|
+
semantic_node_is_focused_input,
|
|
16
|
+
)
|
|
17
|
+
from androidctld.commands.command_models import (
|
|
18
|
+
ActionCommand,
|
|
19
|
+
FocusCommand,
|
|
20
|
+
GlobalCommand,
|
|
21
|
+
LongTapCommand,
|
|
22
|
+
OpenCommand,
|
|
23
|
+
RefBoundActionCommand,
|
|
24
|
+
ScreenshotCommand,
|
|
25
|
+
ScrollCommand,
|
|
26
|
+
SubmitCommand,
|
|
27
|
+
TapCommand,
|
|
28
|
+
TypeCommand,
|
|
29
|
+
is_ref_bound_action_command,
|
|
30
|
+
)
|
|
31
|
+
from androidctld.commands.open_targets import (
|
|
32
|
+
OpenAppTarget,
|
|
33
|
+
OpenUrlTarget,
|
|
34
|
+
validate_open_target,
|
|
35
|
+
)
|
|
36
|
+
from androidctld.device.action_models import (
|
|
37
|
+
GlobalActionRequest,
|
|
38
|
+
LaunchAppActionRequest,
|
|
39
|
+
LongTapActionRequest,
|
|
40
|
+
NodeActionRequest,
|
|
41
|
+
OpenUrlActionRequest,
|
|
42
|
+
ScrollActionRequest,
|
|
43
|
+
SwipeActionRequest,
|
|
44
|
+
TapActionRequest,
|
|
45
|
+
TypeActionRequest,
|
|
46
|
+
required_action_kind_for_request,
|
|
47
|
+
)
|
|
48
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
49
|
+
from androidctld.protocol import CommandKind
|
|
50
|
+
from androidctld.refs.models import NodeHandle
|
|
51
|
+
from androidctld.refs.repair import failed_repair_decision, ref_repair_error
|
|
52
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
53
|
+
from androidctld.runtime.screen_state import (
|
|
54
|
+
current_artifacts,
|
|
55
|
+
current_compiled_screen,
|
|
56
|
+
current_public_screen,
|
|
57
|
+
)
|
|
58
|
+
from androidctld.semantics.compiler import CompiledScreen, SemanticNode
|
|
59
|
+
from androidctld.semantics.public_models import PublicNode, PublicScreen
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BoundRefNode(NamedTuple):
|
|
63
|
+
node: PublicNode
|
|
64
|
+
group_name: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BoundSemanticNode(NamedTuple):
|
|
68
|
+
node: SemanticNode
|
|
69
|
+
blocking_group: str | None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ensure_command_supported(
|
|
73
|
+
session: WorkspaceRuntime, command: ActionCommand | ScreenshotCommand
|
|
74
|
+
) -> None:
|
|
75
|
+
capabilities = session.device_capabilities
|
|
76
|
+
if capabilities is None:
|
|
77
|
+
return
|
|
78
|
+
if isinstance(command, ScreenshotCommand):
|
|
79
|
+
if capabilities.supports_screenshot:
|
|
80
|
+
return
|
|
81
|
+
raise unsupported_command_capability(
|
|
82
|
+
command=command.kind,
|
|
83
|
+
missing_capabilities=["supportsScreenshot"],
|
|
84
|
+
)
|
|
85
|
+
if isinstance(command, SubmitCommand):
|
|
86
|
+
return
|
|
87
|
+
required_action_kind = required_action_kind_for(command)
|
|
88
|
+
if capabilities.supports_action(required_action_kind):
|
|
89
|
+
return
|
|
90
|
+
raise unsupported_command_capability(
|
|
91
|
+
command=command.kind,
|
|
92
|
+
missing_action_kinds=_public_missing_action_kinds(
|
|
93
|
+
command=command.kind,
|
|
94
|
+
missing_action_kinds=[required_action_kind],
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ensure_action_request_supported(
|
|
100
|
+
session: WorkspaceRuntime,
|
|
101
|
+
*,
|
|
102
|
+
command: CommandKind,
|
|
103
|
+
request: (
|
|
104
|
+
TapActionRequest
|
|
105
|
+
| LongTapActionRequest
|
|
106
|
+
| TypeActionRequest
|
|
107
|
+
| NodeActionRequest
|
|
108
|
+
| ScrollActionRequest
|
|
109
|
+
| SwipeActionRequest
|
|
110
|
+
| GlobalActionRequest
|
|
111
|
+
| LaunchAppActionRequest
|
|
112
|
+
| OpenUrlActionRequest
|
|
113
|
+
),
|
|
114
|
+
) -> None:
|
|
115
|
+
capabilities = session.device_capabilities
|
|
116
|
+
if capabilities is None:
|
|
117
|
+
return
|
|
118
|
+
required_action_kind = required_action_kind_for(request)
|
|
119
|
+
if capabilities.supports_action(required_action_kind):
|
|
120
|
+
return
|
|
121
|
+
raise unsupported_command_capability(
|
|
122
|
+
command=command,
|
|
123
|
+
missing_action_kinds=_public_missing_action_kinds(
|
|
124
|
+
command=command,
|
|
125
|
+
missing_action_kinds=[required_action_kind],
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_ref_action(
|
|
131
|
+
session: WorkspaceRuntime,
|
|
132
|
+
command: RefBoundActionCommand,
|
|
133
|
+
) -> BoundRefNode | None:
|
|
134
|
+
if isinstance(command, SubmitCommand):
|
|
135
|
+
return None
|
|
136
|
+
screen = current_public_screen(session)
|
|
137
|
+
if screen is None:
|
|
138
|
+
raise DaemonError(
|
|
139
|
+
code=DaemonErrorCode.SCREEN_NOT_READY,
|
|
140
|
+
message="screen is not ready yet",
|
|
141
|
+
retryable=False,
|
|
142
|
+
details={"workspaceRoot": session.workspace_root.as_posix()},
|
|
143
|
+
http_status=200,
|
|
144
|
+
)
|
|
145
|
+
if command.source_screen_id != screen.screen_id:
|
|
146
|
+
return None
|
|
147
|
+
bound = _find_bound_ref_node(screen, command.ref)
|
|
148
|
+
if bound is None:
|
|
149
|
+
raise DaemonError(
|
|
150
|
+
code=DaemonErrorCode.REF_RESOLUTION_FAILED,
|
|
151
|
+
message="ref does not exist on the current screen",
|
|
152
|
+
retryable=False,
|
|
153
|
+
details={"ref": command.ref},
|
|
154
|
+
http_status=200,
|
|
155
|
+
)
|
|
156
|
+
_ensure_not_blocked(screen, bound, action=command.kind.value)
|
|
157
|
+
_ensure_action_exposed(bound.node, command.kind.value)
|
|
158
|
+
if isinstance(command, ScrollCommand):
|
|
159
|
+
_ensure_scroll_direction_exposed(bound.node, command.direction)
|
|
160
|
+
if isinstance(command, (FocusCommand, TypeCommand)):
|
|
161
|
+
_ensure_input_capable(bound.node, action=command.kind.value)
|
|
162
|
+
if isinstance(command, TypeCommand):
|
|
163
|
+
_ensure_matching_focused_input(screen, node=bound.node)
|
|
164
|
+
return bound
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def validate_action_semantics(
|
|
168
|
+
session: WorkspaceRuntime,
|
|
169
|
+
command: ActionCommand,
|
|
170
|
+
) -> None:
|
|
171
|
+
if not is_ref_bound_action_command(command):
|
|
172
|
+
return
|
|
173
|
+
validate_ref_action(session, command)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def validate_resolved_ref_action(
|
|
177
|
+
session: WorkspaceRuntime,
|
|
178
|
+
command: RefBoundActionCommand,
|
|
179
|
+
request_handle: NodeHandle | None,
|
|
180
|
+
) -> None:
|
|
181
|
+
if isinstance(command, SubmitCommand):
|
|
182
|
+
return
|
|
183
|
+
screen = current_public_screen(session)
|
|
184
|
+
if screen is None or request_handle is None:
|
|
185
|
+
return
|
|
186
|
+
if command.source_screen_id == screen.screen_id:
|
|
187
|
+
return
|
|
188
|
+
compiled_screen = current_compiled_screen(session)
|
|
189
|
+
if compiled_screen is None:
|
|
190
|
+
raise DaemonError(
|
|
191
|
+
code=DaemonErrorCode.SCREEN_NOT_READY,
|
|
192
|
+
message="screen is not ready yet",
|
|
193
|
+
retryable=False,
|
|
194
|
+
details={"workspaceRoot": session.workspace_root.as_posix()},
|
|
195
|
+
http_status=200,
|
|
196
|
+
)
|
|
197
|
+
bound = _find_repaired_target(
|
|
198
|
+
session,
|
|
199
|
+
compiled_screen,
|
|
200
|
+
request_handle,
|
|
201
|
+
command.ref,
|
|
202
|
+
source_screen_id=command.source_screen_id,
|
|
203
|
+
)
|
|
204
|
+
_ensure_not_blocked_semantic(
|
|
205
|
+
bound,
|
|
206
|
+
action=command.kind.value,
|
|
207
|
+
compiled_screen=compiled_screen,
|
|
208
|
+
)
|
|
209
|
+
_ensure_action_exposed_semantic(bound.node, action=command.kind.value)
|
|
210
|
+
if isinstance(command, ScrollCommand):
|
|
211
|
+
_ensure_scroll_direction_exposed_semantic(bound.node, command.direction)
|
|
212
|
+
if isinstance(command, (FocusCommand, TypeCommand)):
|
|
213
|
+
_ensure_input_capable_semantic(bound.node, action=command.kind.value)
|
|
214
|
+
if isinstance(command, TypeCommand):
|
|
215
|
+
_ensure_matching_focused_input_semantic(
|
|
216
|
+
compiled_screen,
|
|
217
|
+
target=bound.node,
|
|
218
|
+
ref=command.ref,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def required_action_kind_for(
|
|
223
|
+
command: (
|
|
224
|
+
ActionCommand
|
|
225
|
+
| TapActionRequest
|
|
226
|
+
| LongTapActionRequest
|
|
227
|
+
| TypeActionRequest
|
|
228
|
+
| NodeActionRequest
|
|
229
|
+
| ScrollActionRequest
|
|
230
|
+
| SwipeActionRequest
|
|
231
|
+
| GlobalActionRequest
|
|
232
|
+
| LaunchAppActionRequest
|
|
233
|
+
| OpenUrlActionRequest
|
|
234
|
+
| OpenAppTarget
|
|
235
|
+
| OpenUrlTarget
|
|
236
|
+
),
|
|
237
|
+
) -> str:
|
|
238
|
+
if isinstance(command, (OpenAppTarget, OpenUrlTarget)):
|
|
239
|
+
return validate_open_target(command).required_action_kind
|
|
240
|
+
if isinstance(
|
|
241
|
+
command,
|
|
242
|
+
(
|
|
243
|
+
TapActionRequest,
|
|
244
|
+
LongTapActionRequest,
|
|
245
|
+
TypeActionRequest,
|
|
246
|
+
NodeActionRequest,
|
|
247
|
+
ScrollActionRequest,
|
|
248
|
+
SwipeActionRequest,
|
|
249
|
+
GlobalActionRequest,
|
|
250
|
+
LaunchAppActionRequest,
|
|
251
|
+
OpenUrlActionRequest,
|
|
252
|
+
),
|
|
253
|
+
):
|
|
254
|
+
return required_action_kind_for_request(command)
|
|
255
|
+
if isinstance(command, OpenCommand):
|
|
256
|
+
return required_action_kind_for(command.target)
|
|
257
|
+
if isinstance(command, TapCommand):
|
|
258
|
+
return "tap"
|
|
259
|
+
if isinstance(command, LongTapCommand):
|
|
260
|
+
return "longTap"
|
|
261
|
+
if isinstance(command, TypeCommand):
|
|
262
|
+
return "type"
|
|
263
|
+
if isinstance(command, (FocusCommand, SubmitCommand)):
|
|
264
|
+
return "node"
|
|
265
|
+
if isinstance(command, ScrollCommand):
|
|
266
|
+
return "scroll"
|
|
267
|
+
if isinstance(command, GlobalCommand):
|
|
268
|
+
return "global"
|
|
269
|
+
raise TypeError(f"unsupported action capability input: {type(command)!r}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def unsupported_command_capability(
|
|
273
|
+
*,
|
|
274
|
+
command: CommandKind,
|
|
275
|
+
missing_capabilities: list[str] | None = None,
|
|
276
|
+
missing_action_kinds: list[str] | None = None,
|
|
277
|
+
) -> DaemonError:
|
|
278
|
+
missing_capabilities = missing_capabilities or []
|
|
279
|
+
missing_action_kinds = missing_action_kinds or []
|
|
280
|
+
return DaemonError(
|
|
281
|
+
code=DaemonErrorCode.DEVICE_AGENT_CAPABILITY_MISMATCH,
|
|
282
|
+
message=f"{command.value} is not supported by the connected device agent",
|
|
283
|
+
retryable=False,
|
|
284
|
+
details={
|
|
285
|
+
"command": command.value,
|
|
286
|
+
"missingCapabilities": missing_capabilities,
|
|
287
|
+
"missingActionKinds": missing_action_kinds,
|
|
288
|
+
},
|
|
289
|
+
http_status=200,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _public_missing_action_kinds(
|
|
294
|
+
*, command: CommandKind, missing_action_kinds: list[str]
|
|
295
|
+
) -> list[str]:
|
|
296
|
+
if command is CommandKind.FOCUS:
|
|
297
|
+
return [
|
|
298
|
+
"focus" if action_kind == "node" else action_kind
|
|
299
|
+
for action_kind in missing_action_kinds
|
|
300
|
+
]
|
|
301
|
+
if command is CommandKind.SUBMIT:
|
|
302
|
+
return [
|
|
303
|
+
"submit" if action_kind == "node" else action_kind
|
|
304
|
+
for action_kind in missing_action_kinds
|
|
305
|
+
]
|
|
306
|
+
return missing_action_kinds
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _find_bound_ref_node(screen: PublicScreen, ref: str) -> BoundRefNode | None:
|
|
310
|
+
for group in screen.groups:
|
|
311
|
+
for node in group.nodes:
|
|
312
|
+
if node.ref == ref:
|
|
313
|
+
return BoundRefNode(node=node, group_name=group.name)
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _ensure_not_blocked(
|
|
318
|
+
screen: PublicScreen,
|
|
319
|
+
bound: BoundRefNode,
|
|
320
|
+
*,
|
|
321
|
+
action: str,
|
|
322
|
+
) -> None:
|
|
323
|
+
blocking_group = screen.surface.blocking_group
|
|
324
|
+
if blocking_group is None or bound.group_name == blocking_group:
|
|
325
|
+
return
|
|
326
|
+
if keyboard_blocker_allows_public_type(
|
|
327
|
+
blocking_group=blocking_group,
|
|
328
|
+
action=action,
|
|
329
|
+
screen=screen,
|
|
330
|
+
node=bound.node,
|
|
331
|
+
):
|
|
332
|
+
return
|
|
333
|
+
raise DaemonError(
|
|
334
|
+
code=DaemonErrorCode.TARGET_BLOCKED,
|
|
335
|
+
message="target is blocked on the current screen",
|
|
336
|
+
retryable=False,
|
|
337
|
+
details=blocked_by_group_fields(
|
|
338
|
+
blocking_group=blocking_group,
|
|
339
|
+
ref=bound.node.ref,
|
|
340
|
+
),
|
|
341
|
+
http_status=200,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _ensure_not_blocked_semantic(
|
|
346
|
+
bound: BoundSemanticNode,
|
|
347
|
+
*,
|
|
348
|
+
action: str,
|
|
349
|
+
compiled_screen: CompiledScreen,
|
|
350
|
+
) -> None:
|
|
351
|
+
blocking_group = bound.blocking_group
|
|
352
|
+
if blocking_group is None or bound.node.group == blocking_group:
|
|
353
|
+
return
|
|
354
|
+
if keyboard_blocker_allows_semantic_type(
|
|
355
|
+
blocking_group=blocking_group,
|
|
356
|
+
action=action,
|
|
357
|
+
screen=compiled_screen,
|
|
358
|
+
node=bound.node,
|
|
359
|
+
):
|
|
360
|
+
return
|
|
361
|
+
raise DaemonError(
|
|
362
|
+
code=DaemonErrorCode.TARGET_BLOCKED,
|
|
363
|
+
message="target is blocked on the current screen",
|
|
364
|
+
retryable=False,
|
|
365
|
+
details=blocked_by_group_fields(
|
|
366
|
+
blocking_group=blocking_group,
|
|
367
|
+
ref=bound.node.ref or None,
|
|
368
|
+
),
|
|
369
|
+
http_status=200,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _ensure_action_exposed(node: PublicNode, action: str) -> None:
|
|
374
|
+
if action in node.actions:
|
|
375
|
+
return
|
|
376
|
+
raise DaemonError(
|
|
377
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
378
|
+
message=f"{action} is not available for the requested target",
|
|
379
|
+
retryable=False,
|
|
380
|
+
details={
|
|
381
|
+
"reason": "action_not_exposed",
|
|
382
|
+
"ref": node.ref,
|
|
383
|
+
"action": action,
|
|
384
|
+
},
|
|
385
|
+
http_status=200,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _ensure_action_exposed_semantic(node: SemanticNode, *, action: str) -> None:
|
|
390
|
+
if action in node.actions:
|
|
391
|
+
return
|
|
392
|
+
raise DaemonError(
|
|
393
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
394
|
+
message=f"{action} is not available for the requested target",
|
|
395
|
+
retryable=False,
|
|
396
|
+
details={
|
|
397
|
+
"reason": "action_not_exposed",
|
|
398
|
+
"ref": node.ref or None,
|
|
399
|
+
"action": action,
|
|
400
|
+
},
|
|
401
|
+
http_status=200,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _ensure_scroll_direction_exposed(node: PublicNode, direction: str) -> None:
|
|
406
|
+
if direction in node.scroll_directions:
|
|
407
|
+
return
|
|
408
|
+
raise DaemonError(
|
|
409
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
410
|
+
message=(
|
|
411
|
+
f"scroll direction '{direction}' is not available for the requested target"
|
|
412
|
+
),
|
|
413
|
+
retryable=False,
|
|
414
|
+
details={
|
|
415
|
+
"reason": "scroll_direction_not_exposed",
|
|
416
|
+
"ref": node.ref,
|
|
417
|
+
"direction": direction,
|
|
418
|
+
"scrollDirections": list(node.scroll_directions),
|
|
419
|
+
},
|
|
420
|
+
http_status=200,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _ensure_scroll_direction_exposed_semantic(
|
|
425
|
+
node: SemanticNode, direction: str
|
|
426
|
+
) -> None:
|
|
427
|
+
if direction in node.scroll_directions:
|
|
428
|
+
return
|
|
429
|
+
raise DaemonError(
|
|
430
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
431
|
+
message=(
|
|
432
|
+
f"scroll direction '{direction}' is not available for the requested target"
|
|
433
|
+
),
|
|
434
|
+
retryable=False,
|
|
435
|
+
details={
|
|
436
|
+
"reason": "scroll_direction_not_exposed",
|
|
437
|
+
"ref": node.ref or None,
|
|
438
|
+
"direction": direction,
|
|
439
|
+
"scrollDirections": list(node.scroll_directions),
|
|
440
|
+
},
|
|
441
|
+
http_status=200,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _ensure_input_capable(node: PublicNode, *, action: str) -> None:
|
|
446
|
+
if node.role == "input":
|
|
447
|
+
return
|
|
448
|
+
raise DaemonError(
|
|
449
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
450
|
+
message=f"{action} requires an input-capable target",
|
|
451
|
+
retryable=False,
|
|
452
|
+
details={
|
|
453
|
+
"reason": "not_input_capable",
|
|
454
|
+
"ref": node.ref,
|
|
455
|
+
"action": action,
|
|
456
|
+
"role": node.role,
|
|
457
|
+
},
|
|
458
|
+
http_status=200,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _ensure_input_capable_semantic(node: SemanticNode, *, action: str) -> None:
|
|
463
|
+
if node.role == "input":
|
|
464
|
+
return
|
|
465
|
+
raise DaemonError(
|
|
466
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
467
|
+
message=f"{action} requires an input-capable target",
|
|
468
|
+
retryable=False,
|
|
469
|
+
details={
|
|
470
|
+
"reason": "not_input_capable",
|
|
471
|
+
"ref": node.ref or None,
|
|
472
|
+
"action": action,
|
|
473
|
+
"role": node.role,
|
|
474
|
+
},
|
|
475
|
+
http_status=200,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _ensure_matching_focused_input(screen: PublicScreen, *, node: PublicNode) -> None:
|
|
480
|
+
if public_node_is_focused_input(screen, node):
|
|
481
|
+
return
|
|
482
|
+
raise DaemonError(
|
|
483
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
484
|
+
message="target is not the current focused input",
|
|
485
|
+
retryable=False,
|
|
486
|
+
details=focus_mismatch_fields(
|
|
487
|
+
ref=node.ref,
|
|
488
|
+
focused_input_ref=public_focused_input_ref(screen),
|
|
489
|
+
),
|
|
490
|
+
http_status=200,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _ensure_matching_focused_input_semantic(
|
|
495
|
+
screen: CompiledScreen,
|
|
496
|
+
*,
|
|
497
|
+
target: SemanticNode,
|
|
498
|
+
ref: str,
|
|
499
|
+
) -> None:
|
|
500
|
+
if semantic_node_is_focused_input(screen, target):
|
|
501
|
+
return
|
|
502
|
+
raise DaemonError(
|
|
503
|
+
code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
|
|
504
|
+
message="target is not the current focused input",
|
|
505
|
+
retryable=False,
|
|
506
|
+
details=focus_mismatch_fields(
|
|
507
|
+
ref=ref,
|
|
508
|
+
focused_input_ref=semantic_focused_input_ref(screen),
|
|
509
|
+
),
|
|
510
|
+
http_status=200,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _find_repaired_target(
|
|
515
|
+
session: WorkspaceRuntime,
|
|
516
|
+
screen: CompiledScreen,
|
|
517
|
+
handle: NodeHandle,
|
|
518
|
+
ref: str,
|
|
519
|
+
*,
|
|
520
|
+
source_screen_id: str,
|
|
521
|
+
) -> BoundSemanticNode:
|
|
522
|
+
for node in _compiled_screen_nodes(screen):
|
|
523
|
+
if node.raw_rid == handle.rid:
|
|
524
|
+
return BoundSemanticNode(node=node, blocking_group=screen.blocking_group)
|
|
525
|
+
raise ref_repair_error(
|
|
526
|
+
failed_repair_decision(ref=ref, source_screen_id=source_screen_id),
|
|
527
|
+
public_screen=current_public_screen(session),
|
|
528
|
+
artifacts=current_artifacts(session),
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _compiled_screen_nodes(screen: CompiledScreen) -> tuple[SemanticNode, ...]:
|
|
533
|
+
return (
|
|
534
|
+
tuple(screen.targets)
|
|
535
|
+
+ tuple(screen.dialog)
|
|
536
|
+
+ tuple(screen.keyboard)
|
|
537
|
+
+ tuple(screen.system)
|
|
538
|
+
+ tuple(screen.context)
|
|
539
|
+
)
|