screenforge 0.4.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.
Files changed (64) hide show
  1. cli/__init__.py +0 -0
  2. cli/_version.py +1 -0
  3. cli/dispatch.py +266 -0
  4. cli/doctor.py +487 -0
  5. cli/modes/__init__.py +0 -0
  6. cli/modes/action.py +262 -0
  7. cli/modes/default.py +248 -0
  8. cli/modes/demo.py +162 -0
  9. cli/modes/dry_run.py +237 -0
  10. cli/modes/init.py +133 -0
  11. cli/modes/plan.py +148 -0
  12. cli/modes/workflow.py +354 -0
  13. cli/parser.py +305 -0
  14. cli/reporter.py +207 -0
  15. cli/session.py +146 -0
  16. cli/shared.py +427 -0
  17. cli/shorthand.py +90 -0
  18. cli/tool_protocol_handlers.py +446 -0
  19. common/__init__.py +0 -0
  20. common/adapters/__init__.py +21 -0
  21. common/adapters/android_adapter.py +273 -0
  22. common/adapters/base_adapter.py +24 -0
  23. common/adapters/ios_adapter.py +278 -0
  24. common/adapters/web_adapter.py +271 -0
  25. common/ai.py +277 -0
  26. common/ai_autonomous.py +273 -0
  27. common/ai_heal.py +222 -0
  28. common/cache/__init__.py +15 -0
  29. common/cache/cache_hash.py +57 -0
  30. common/cache/cache_manager.py +300 -0
  31. common/cache/cache_stats.py +133 -0
  32. common/cache/cache_storage.py +79 -0
  33. common/cache/embedding_loader.py +150 -0
  34. common/capabilities.py +121 -0
  35. common/case_memory.py +327 -0
  36. common/error_codes.py +61 -0
  37. common/exceptions.py +18 -0
  38. common/executor.py +1504 -0
  39. common/failure_diagnosis.py +138 -0
  40. common/history_manager.py +75 -0
  41. common/logs.py +168 -0
  42. common/mcp_server.py +467 -0
  43. common/preflight.py +496 -0
  44. common/progress.py +37 -0
  45. common/run_reporter.py +415 -0
  46. common/run_resume.py +149 -0
  47. common/runtime_modes.py +35 -0
  48. common/tool_protocol.py +196 -0
  49. common/visual_fallback.py +71 -0
  50. common/workflow_schema.py +150 -0
  51. config/__init__.py +0 -0
  52. config/config.py +167 -0
  53. config/env_loader.py +76 -0
  54. screenforge-0.4.0.dist-info/METADATA +43 -0
  55. screenforge-0.4.0.dist-info/RECORD +64 -0
  56. screenforge-0.4.0.dist-info/WHEEL +5 -0
  57. screenforge-0.4.0.dist-info/entry_points.txt +2 -0
  58. screenforge-0.4.0.dist-info/licenses/LICENSE +21 -0
  59. screenforge-0.4.0.dist-info/top_level.txt +4 -0
  60. utils/__init__.py +0 -0
  61. utils/screenshot_annotator.py +60 -0
  62. utils/utils_ios.py +195 -0
  63. utils/utils_web.py +304 -0
  64. utils/utils_xml.py +218 -0
cli/modes/action.py ADDED
@@ -0,0 +1,262 @@
1
+ """Single-action execution mode."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+
7
+ import cli.shared as _shared
8
+ from cli.reporter import (
9
+ _apply_resume_summary,
10
+ _build_action_summary,
11
+ _build_inline_action_data,
12
+ _build_reporter,
13
+ _emit_run_started,
14
+ )
15
+ from cli.shared import (
16
+ _capture_ui_state,
17
+ _connect_adapter,
18
+ _ensure_executor_runtime,
19
+ _ensure_history_manager,
20
+ _ensure_ui_compressors,
21
+ _SharedAdapterManager,
22
+ current_url,
23
+ get_initial_header,
24
+ log,
25
+ save_to_disk,
26
+ )
27
+ from common.failure_diagnosis import diagnose
28
+ from common.runtime_modes import MODE_RUN
29
+
30
+
31
+ def build_failure_payload(
32
+ *,
33
+ action_name: str,
34
+ platform: str,
35
+ assertion_failed: bool,
36
+ error_code: str,
37
+ locator_value: str,
38
+ ui_tree: dict,
39
+ current_url: str,
40
+ ) -> dict:
41
+ """Assemble the --action --json failure payload.
42
+
43
+ A failed assertion is a verdict: keep the original bare shape (no diagnosis,
44
+ no page, no retry bait). An engine_error is where the agent needs help, so
45
+ enrich it with the did-you-mean diagnosis, the current ui_tree, and the URL.
46
+
47
+ Note: ui_tree, element_count, and current_url are emitted ONLY for
48
+ engine_error — the assertion_failed path returns the bare verdict above.
49
+ """
50
+ payload = {
51
+ "ok": False,
52
+ "action": action_name,
53
+ "platform": platform,
54
+ "result": "assertion_failed" if assertion_failed else "engine_error",
55
+ "assertion_failed": assertion_failed,
56
+ "error": (
57
+ f"Assertion failed: {action_name}"
58
+ if assertion_failed
59
+ else f"Action failed: {action_name}"
60
+ ),
61
+ }
62
+ if assertion_failed:
63
+ return payload
64
+
65
+ diag = diagnose(
66
+ error_code=error_code or "E037",
67
+ locator_value=locator_value or "",
68
+ ui_elements=ui_tree.get("ui_elements", []) or [],
69
+ )
70
+ payload.update(diag.to_dict())
71
+ payload["ui_tree"] = ui_tree
72
+ payload["element_count"] = len(ui_tree.get("ui_elements", []) or [])
73
+ payload["current_url"] = current_url
74
+ return payload
75
+
76
+
77
+ def run_action_default_mode(
78
+ args,
79
+ output_script_path: str,
80
+ resume_context: dict,
81
+ shared_adapter_manager: _SharedAdapterManager | None = None,
82
+ ) -> int:
83
+ adapter = None
84
+ owns_adapter = False
85
+ json_mode = args.json
86
+ reporter = _build_reporter(args, output_script_path, MODE_RUN, json_output=False)
87
+ exit_code = 1
88
+ final_status = "failed"
89
+ final_error = ""
90
+ steps_executed = 0
91
+ _emit_run_started(reporter, args, output_script_path, MODE_RUN)
92
+ _apply_resume_summary(reporter, resume_context)
93
+
94
+ try:
95
+ action_data = _build_inline_action_data(args)
96
+ action_summary = _build_action_summary(args, action_data, executed_steps=0)
97
+ reporter.update_control_summary(
98
+ control_kind="action",
99
+ control_label=action_summary["action_name"],
100
+ source_ref="inline://action",
101
+ action=action_summary["action"],
102
+ locator_type=action_summary["locator_type"],
103
+ locator_value=action_summary["locator_value"],
104
+ extra_value=action_summary["extra_value"],
105
+ )
106
+ reporter.emit_event(
107
+ "action_loaded",
108
+ action_name=action_summary["action_name"],
109
+ action=action_summary["action"],
110
+ locator_type=action_summary["locator_type"],
111
+ locator_value=action_summary["locator_value"],
112
+ )
113
+
114
+ try:
115
+ if shared_adapter_manager:
116
+ adapter = shared_adapter_manager.get_or_create(args.platform, args.env)
117
+ else:
118
+ adapter = _connect_adapter(args, reporter)
119
+ owns_adapter = True
120
+ device = adapter.driver
121
+ except Exception as e:
122
+ final_error = str(e)
123
+ reporter.emit_event("startup_failed", platform=args.platform, error=str(e))
124
+ log.error(f"❌ [Error] {args.platform} connection failed: {e}")
125
+ return 1
126
+
127
+ _ensure_history_manager()
128
+ _ensure_executor_runtime()
129
+
130
+ if os.path.exists(output_script_path):
131
+ with open(output_script_path, "r", encoding="utf-8") as f:
132
+ existing_lines = f.readlines()
133
+ history_manager = _shared.StepHistoryManager(initial_content=existing_lines)
134
+ log.info(f"📎 [System] Append mode: continuing on existing script ({output_script_path})")
135
+ else:
136
+ # Name the test after the action so the file is discoverable.
137
+ header = get_initial_header(label=action_data.get("name") or None)
138
+ history_manager = _shared.StepHistoryManager(initial_content=header)
139
+ save_to_disk(output_script_path, header)
140
+
141
+ if shared_adapter_manager:
142
+ # Reuse the session's shared executor so a `ref @N` action resolves
143
+ # against the cache a prior inspect_ui populated (same MCP session).
144
+ executor = shared_adapter_manager.get_executor(args.platform, args.env)
145
+ else:
146
+ executor = _shared.UIExecutor(device, platform=args.platform)
147
+
148
+ reporter.emit_event(
149
+ "step_started",
150
+ step=1,
151
+ source="action",
152
+ step_name=action_data["name"],
153
+ )
154
+ result = executor.execute_and_record(action_data)
155
+ if not result.get("success"):
156
+ assertion_failed = bool(result.get("assertion_failed"))
157
+ if assertion_failed:
158
+ final_error = f"Assertion failed: {action_data['name']}"
159
+ else:
160
+ final_error = f"Action failed: {action_data['name']}"
161
+ reporter.emit_event(
162
+ "action_executed",
163
+ step=1,
164
+ success=False,
165
+ action_description=action_data["name"],
166
+ )
167
+ log.error(f"❌ [Action] Failed: {action_data['name']}")
168
+ if json_mode:
169
+ # Distinguish a verification verdict (assertion_failed=true: the
170
+ # SUT did not meet the assertion — do NOT retry) from a real
171
+ # engine error. Only engine errors get the did-you-mean
172
+ # diagnosis + current ui_tree (the agent needs the page to
173
+ # recover); a verdict stays a bare result so we never bait a
174
+ # retry on a legitimately-failed assertion.
175
+ # Capture the URL at the moment of failure, BEFORE compress_web_dom
176
+ # traverses the DOM (a mid-redirect click could otherwise settle the
177
+ # navigation and report the post-redirect URL).
178
+ page_url = current_url(adapter, args.platform)
179
+ ui_tree = {}
180
+ if not assertion_failed:
181
+ _ensure_ui_compressors()
182
+ try:
183
+ ui_json, _ = _capture_ui_state(args, adapter, reporter, 1)
184
+ ui_tree = json.loads(ui_json)
185
+ except Exception as e:
186
+ # Degrade (connection lost / non-JSON) but don't hide it:
187
+ # a silent empty tree yields empty candidates with no signal.
188
+ log.warning(f"⚠️ [Action] Failed to capture post-failure UI state: {e}")
189
+ ui_tree = {}
190
+ error_code = result.get("error_code", "") or ""
191
+ if not error_code and not assertion_failed:
192
+ log.warning("⚠️ [Action] executor returned no error_code; defaulting to E037")
193
+ payload = build_failure_payload(
194
+ action_name=action_data["name"],
195
+ platform=args.platform,
196
+ assertion_failed=assertion_failed,
197
+ error_code=error_code,
198
+ locator_value=getattr(args, "locator_value", "") or "",
199
+ ui_tree=ui_tree,
200
+ current_url=page_url,
201
+ )
202
+ json.dump(payload, sys.stdout, ensure_ascii=False)
203
+ sys.stdout.write("\n")
204
+ sys.stdout.flush()
205
+ return 1
206
+
207
+ history_manager.add_step(result["code_lines"], result["action_description"])
208
+ save_to_disk(output_script_path, history_manager.get_current_file_content())
209
+ reporter.emit_event(
210
+ "action_executed",
211
+ step=1,
212
+ success=True,
213
+ action_description=result["action_description"],
214
+ )
215
+ steps_executed = 1
216
+ reporter.update_summary(
217
+ action_summary=_build_action_summary(
218
+ args, action_data, executed_steps=steps_executed
219
+ )
220
+ )
221
+ reporter.update_control_summary(executed_steps=steps_executed)
222
+ final_status = "success"
223
+ exit_code = 0
224
+
225
+ if json_mode:
226
+ _ensure_ui_compressors()
227
+ ui_json, _ = _capture_ui_state(args, adapter, reporter, 1)
228
+ try:
229
+ ui_tree = json.loads(ui_json)
230
+ except (json.JSONDecodeError, TypeError):
231
+ ui_tree = {}
232
+ json.dump({
233
+ "ok": True,
234
+ "action": action_data["name"],
235
+ "platform": args.platform,
236
+ "ui_tree": ui_tree,
237
+ "element_count": len(ui_tree.get("ui_elements", [])),
238
+ "output_script": output_script_path,
239
+ "current_url": current_url(adapter, args.platform),
240
+ }, sys.stdout, ensure_ascii=False)
241
+ sys.stdout.write("\n")
242
+ sys.stdout.flush()
243
+
244
+ return 0
245
+ except Exception as e:
246
+ final_error = str(e)
247
+ reporter.emit_event("action_run_failed", error=str(e))
248
+ log.error(f"❌ [Action] Failed: {e}")
249
+ return 1
250
+ finally:
251
+ reporter.finalize(
252
+ status=final_status,
253
+ exit_code=exit_code,
254
+ steps_executed=steps_executed,
255
+ last_error=final_error,
256
+ )
257
+ log.info(f"🏁 Done. Generated script saved to: {output_script_path}")
258
+ if owns_adapter and adapter:
259
+ try:
260
+ adapter.teardown()
261
+ except Exception as e:
262
+ log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
cli/modes/default.py ADDED
@@ -0,0 +1,248 @@
1
+ """Default autonomous brain loop mode."""
2
+
3
+ import cli.shared as _shared
4
+ from cli.reporter import (
5
+ _apply_resume_summary,
6
+ _build_reporter,
7
+ _emit_run_started,
8
+ )
9
+ from cli.shared import (
10
+ _capture_ui_state,
11
+ _connect_adapter,
12
+ _ensure_executor_runtime,
13
+ _ensure_history_manager,
14
+ _ensure_runtime_classes,
15
+ get_initial_header,
16
+ log,
17
+ save_to_disk,
18
+ )
19
+ from common.runtime_modes import MODE_RUN
20
+
21
+
22
+ def run_default_mode(
23
+ args,
24
+ output_script_path: str,
25
+ context_content: str,
26
+ resume_context: dict,
27
+ ) -> int:
28
+ adapter = None
29
+ reporter = _build_reporter(args, output_script_path, MODE_RUN)
30
+ exit_code = 1
31
+ final_status = "failed"
32
+ final_error = ""
33
+ steps_executed = 0
34
+ _emit_run_started(reporter, args, output_script_path, MODE_RUN)
35
+ _apply_resume_summary(reporter, resume_context)
36
+
37
+ try:
38
+ try:
39
+ adapter = _connect_adapter(args, reporter)
40
+ device = adapter.driver
41
+ except Exception as e:
42
+ final_error = str(e)
43
+ reporter.emit_event("startup_failed", platform=args.platform, error=str(e))
44
+ log.error(f"❌ [Error] {args.platform} connection failed: {e}")
45
+ return 1
46
+
47
+ _ensure_history_manager()
48
+ _ensure_executor_runtime()
49
+ header = get_initial_header(label=str(getattr(args, "goal", "")).strip() or None)
50
+ history_manager = _shared.StepHistoryManager(initial_content=header)
51
+ save_to_disk(output_script_path, header)
52
+
53
+ _ensure_runtime_classes()
54
+ brain = _shared.AutonomousBrain()
55
+ executor = _shared.UIExecutor(device, platform=args.platform)
56
+
57
+ step_count = 0
58
+ last_error = ""
59
+ consecutive_failures = 0
60
+ last_ui_json = ""
61
+
62
+ while step_count < args.max_steps:
63
+ step_count += 1
64
+ steps_executed = step_count
65
+ log.info(f"\n--- 🔄 Step {step_count} ---")
66
+ reporter.emit_event("step_started", step=step_count)
67
+
68
+ ui_json, screenshot_base64 = _capture_ui_state(
69
+ args, adapter, reporter, step_count
70
+ )
71
+ current_history = history_manager.get_history()
72
+
73
+ if last_ui_json == ui_json and step_count > 1 and not last_error:
74
+ last_error = "[System Warning] The previous action was executed but the page UI did not change. Possible causes: invalid input, unchecked checkbox, or invisible overlay. Do NOT repeat the same action — try a different strategy."
75
+ log.warning("[E020] UI stagnation detected: action executed but page did not change. Possible causes: invalid input, unchecked checkbox, or invisible overlay.")
76
+
77
+ last_ui_json = ui_json
78
+
79
+ decision_data = brain.get_next_autonomous_action(
80
+ goal=args.goal,
81
+ context=context_content,
82
+ ui_json=ui_json,
83
+ history=current_history,
84
+ platform=args.platform,
85
+ last_error=last_error,
86
+ screenshot_base64=screenshot_base64,
87
+ )
88
+
89
+ status = decision_data.get("status")
90
+ action_data = decision_data.get("result", {})
91
+ last_error = ""
92
+ reporter.emit_event(
93
+ "decision_received",
94
+ step=step_count,
95
+ status=status,
96
+ action=action_data.get("action", ""),
97
+ locator_type=action_data.get("locator_type", ""),
98
+ locator_value=action_data.get("locator_value", ""),
99
+ )
100
+
101
+ if status == "success":
102
+ if action_data and action_data.get("action"):
103
+ log.info(
104
+ "🔍 Final action/assertion detected with success status, executing..."
105
+ )
106
+ result = executor.execute_and_record(action_data)
107
+ if result.get("success"):
108
+ history_manager.add_step(
109
+ result["code_lines"], result["action_description"]
110
+ )
111
+ save_to_disk(
112
+ output_script_path,
113
+ history_manager.get_current_file_content(),
114
+ )
115
+ log.info(f"✅ Final action succeeded: {result['action_description']}")
116
+ reporter.emit_event(
117
+ "action_executed",
118
+ step=step_count,
119
+ success=True,
120
+ action_description=result["action_description"],
121
+ )
122
+ else:
123
+ final_error = "Final action/assertion failed — task verification did not pass"
124
+ reporter.emit_event(
125
+ "action_executed",
126
+ step=step_count,
127
+ success=False,
128
+ action_description="final_action_failed",
129
+ )
130
+ log.error("❌ Final action/assertion failed — task verification did not pass")
131
+ return 1
132
+
133
+ final_status = "success"
134
+ exit_code = 0
135
+ log.info("🎉 [Agent] All goals and assertions achieved!")
136
+ return 0
137
+
138
+ if status == "failed":
139
+ final_error = "Task cannot continue — AI determined failure"
140
+ log.warning("⚠️ [Agent] Task cannot continue — AI determined failure.")
141
+ return 1
142
+
143
+ if status == "running":
144
+ if not action_data:
145
+ last_error = "Model returned 'running' status but provided no action. Please provide a concrete action."
146
+ consecutive_failures += 1
147
+ reporter.emit_event(
148
+ "action_executed",
149
+ step=step_count,
150
+ success=False,
151
+ action_description="missing_action",
152
+ )
153
+ elif action_data.get("action") == "not_found":
154
+ # The model explicitly reports the target is absent from the
155
+ # UI tree and needs visual verification. This is not an
156
+ # engine error and must NOT count toward the circuit breaker
157
+ # (otherwise two not_found in a row trips it before vision is
158
+ # ever tried, since max_retries defaults to 2).
159
+ last_error = (
160
+ "Model reported the target element is not in the current "
161
+ "UI tree (action='not_found'). Re-run with --vision to let "
162
+ "the visual model locate it, or try a different strategy/"
163
+ "scroll to bring it into view."
164
+ )
165
+ log.warning(
166
+ "[Agent] Model returned 'not_found' — target absent from UI "
167
+ "tree. Suggesting visual fallback (no circuit-breaker penalty)."
168
+ )
169
+ reporter.emit_event(
170
+ "action_executed",
171
+ step=step_count,
172
+ success=False,
173
+ action_description="not_found",
174
+ )
175
+ continue
176
+ else:
177
+ result = executor.execute_and_record(action_data)
178
+ if result.get("success"):
179
+ consecutive_failures = 0
180
+ history_manager.add_step(
181
+ result["code_lines"], result["action_description"]
182
+ )
183
+ save_to_disk(
184
+ output_script_path,
185
+ history_manager.get_current_file_content(),
186
+ )
187
+ log.info(f"Action succeeded: {result['action_description']}")
188
+ reporter.emit_event(
189
+ "action_executed",
190
+ step=step_count,
191
+ success=True,
192
+ action_description=result["action_description"],
193
+ )
194
+ else:
195
+ consecutive_failures += 1
196
+ action_repr = f"{action_data.get('action')} - {action_data.get('locator_type')}={action_data.get('locator_value')}"
197
+ last_error = f"Action [{action_repr}] failed — element not found or not interactable on the current page."
198
+ reporter.emit_event(
199
+ "action_executed",
200
+ step=step_count,
201
+ success=False,
202
+ action_description=action_repr,
203
+ )
204
+ log.warning(
205
+ f"[E021] Step failed (attempt {consecutive_failures}/{args.max_retries}). Self-heal engine will retry with adjusted strategy."
206
+ )
207
+
208
+ if consecutive_failures >= args.max_retries:
209
+ action_repr = f"{action_data.get('action')} - {action_data.get('locator_type')}={action_data.get('locator_value')}"
210
+ final_error = f"Circuit breaker triggered after {args.max_retries} consecutive failures on: {action_repr}"
211
+ log.error(
212
+ f"[E022] Circuit breaker triggered: {args.max_retries} consecutive failures on [{action_repr}]. "
213
+ f"Last error: element not found or not interactable. "
214
+ f"Fix: run 'screenforge --action click --platform {args.platform} --dry-run ...' to inspect current page state, "
215
+ f"or adjust your workflow/locator strategy."
216
+ )
217
+ return 1
218
+ continue
219
+
220
+ final_error = f"Unknown decision status: {status}"
221
+ log.error(f"[E023] Unknown decision status '{status}' returned by AI brain. This is likely a model parsing issue. Fix: check MODEL_NAME supports structured JSON output")
222
+ return 1
223
+
224
+ final_error = f"Max step limit reached ({args.max_steps} steps)"
225
+ log.warning(
226
+ f"[E024] Exploration exceeded max step limit ({args.max_steps}). Possible infinite loop. Fix: increase --max_steps or refine your --goal to be more specific."
227
+ )
228
+ return 1
229
+
230
+ except KeyboardInterrupt:
231
+ final_error = "Interrupted by user (KeyboardInterrupt)"
232
+ reporter.emit_event("interrupted", reason="KeyboardInterrupt")
233
+ log.warning("\n⚠️ Interrupted — safely aborting...")
234
+ return 1
235
+
236
+ finally:
237
+ reporter.finalize(
238
+ status=final_status,
239
+ exit_code=exit_code,
240
+ steps_executed=steps_executed,
241
+ last_error=final_error,
242
+ )
243
+ log.info(f"🏁 Done. Generated script saved to: {output_script_path}")
244
+ if adapter:
245
+ try:
246
+ adapter.teardown()
247
+ except Exception as e:
248
+ log.warning(f"⚠️ [Warning] Cleanup failed: {e}")
cli/modes/demo.py ADDED
@@ -0,0 +1,162 @@
1
+ """Demo mode — showcases ScreenForge's full flow without API key or device."""
2
+
3
+ import sys
4
+ import time
5
+
6
+ from cli._version import __version__
7
+
8
+ DEMO_SCENARIO = {
9
+ "title": "Login Flow Test",
10
+ "url": "https://demo.screenforge.dev",
11
+ "steps": [
12
+ {
13
+ "description": "Navigate to login page",
14
+ "action": "goto",
15
+ "target": "https://demo.screenforge.dev/login",
16
+ "ui_tree_snippet": (
17
+ '<div id="app">\n'
18
+ ' <nav>\n'
19
+ ' <a href="/login">Sign In</a>\n'
20
+ ' <a href="/register">Sign Up</a>\n'
21
+ ' </nav>\n'
22
+ '</div>'
23
+ ),
24
+ "generated_code": 'page.goto("https://demo.screenforge.dev/login")',
25
+ },
26
+ {
27
+ "description": "Enter username",
28
+ "action": "input",
29
+ "target": "#email → 'user@example.com'",
30
+ "ui_tree_snippet": (
31
+ '<form id="login-form">\n'
32
+ ' <input id="email" type="email" placeholder="Email" />\n'
33
+ ' <input id="password" type="password" placeholder="Password" />\n'
34
+ ' <button type="submit">Sign In</button>\n'
35
+ '</form>'
36
+ ),
37
+ "generated_code": (
38
+ 'page.locator("#email").first.fill("user@example.com")'
39
+ ),
40
+ },
41
+ {
42
+ "description": "Enter password",
43
+ "action": "input",
44
+ "target": "#password → '••••••••'",
45
+ "ui_tree_snippet": None,
46
+ "generated_code": 'page.locator("#password").first.fill("password123")',
47
+ },
48
+ {
49
+ "description": "Click sign in button",
50
+ "action": "click",
51
+ "target": "text='Sign In'",
52
+ "ui_tree_snippet": None,
53
+ "generated_code": "page.get_by_text('Sign In').first.click()",
54
+ },
55
+ {
56
+ "description": "Verify dashboard loaded",
57
+ "action": "assert_exist",
58
+ "target": "text='Welcome back'",
59
+ "ui_tree_snippet": (
60
+ '<main>\n'
61
+ ' <h1>Welcome back</h1>\n'
62
+ ' <div class="dashboard">...</div>\n'
63
+ '</main>'
64
+ ),
65
+ "generated_code": (
66
+ 'expect(page.get_by_text("Welcome back").first).to_be_visible()'
67
+ ),
68
+ },
69
+ ],
70
+ }
71
+
72
+ PYTEST_OUTPUT = '''import allure
73
+ import pytest
74
+ from playwright.sync_api import Page, expect
75
+
76
+
77
+ @allure.feature("Authentication")
78
+ @allure.story("Login Flow")
79
+ class TestLogin:
80
+ @allure.step("Navigate to login page")
81
+ def test_login_success(self, page: Page):
82
+ page.goto("https://demo.screenforge.dev/login")
83
+
84
+ with allure.step("Enter credentials"):
85
+ page.locator("#email").first.fill("user@example.com")
86
+ page.locator("#password").first.fill("password123")
87
+
88
+ with allure.step("Submit login form"):
89
+ page.get_by_text('Sign In').first.click()
90
+
91
+ with allure.step("Verify dashboard loaded"):
92
+ expect(page.get_by_text("Welcome back").first).to_be_visible()
93
+ '''
94
+
95
+
96
+ def _print_slow(text: str, delay: float = 0.02) -> None:
97
+ for char in text:
98
+ sys.stdout.write(char)
99
+ sys.stdout.flush()
100
+ time.sleep(delay)
101
+ sys.stdout.write("\n")
102
+
103
+
104
+ def _print_step_header(step_num: int, total: int, description: str) -> None:
105
+ print(f"\n [{step_num}/{total}] {description}")
106
+
107
+
108
+ def run_demo_mode() -> int:
109
+ scenario = DEMO_SCENARIO
110
+ steps = scenario["steps"]
111
+
112
+ print()
113
+ print(f" screenforge v{__version__} — demo mode")
114
+ print(f" Scenario: {scenario['title']}")
115
+ print(f" Target: {scenario['url']} (simulated)")
116
+ print(" " + "-" * 50)
117
+ time.sleep(0.5)
118
+
119
+ print("\n Connecting to browser...", end="")
120
+ time.sleep(0.3)
121
+ print(" OK (simulated)")
122
+
123
+ for i, step in enumerate(steps, 1):
124
+ _print_step_header(i, len(steps), step["description"])
125
+ time.sleep(0.3)
126
+
127
+ if step["ui_tree_snippet"]:
128
+ print(" inspect_ui:")
129
+ for line in step["ui_tree_snippet"].split("\n"):
130
+ print(f" {line}")
131
+ time.sleep(0.2)
132
+
133
+ print(f" action: {step['action']} → {step['target']}")
134
+ time.sleep(0.2)
135
+
136
+ print(f" codegen: {step['generated_code']}")
137
+ time.sleep(0.3)
138
+
139
+ print("\n " + "-" * 50)
140
+ print(" Generating pytest script...\n")
141
+ time.sleep(0.4)
142
+
143
+ for line in PYTEST_OUTPUT.strip().split("\n"):
144
+ _print_slow(f" {line}", delay=0.008)
145
+
146
+ print("\n " + "-" * 50)
147
+ print(" Demo complete!")
148
+ print()
149
+ print(" What just happened:")
150
+ print(" 1. ScreenForge inspected the page structure (inspect_ui)")
151
+ print(" 2. AI analyzed the DOM and chose actions")
152
+ print(" 3. Actions were executed step by step")
153
+ print(" 4. A production-ready pytest script was generated")
154
+ print()
155
+ print(" To do this for real:")
156
+ print(" export OPENAI_API_KEY=sk-...")
157
+ print(" screenforge --action click --platform web --locator-type text --locator-value 'Login'")
158
+ print()
159
+ print(" Star us on GitHub if this was useful!")
160
+ print()
161
+
162
+ return 0