browser-workspace 0.4.5 → 0.4.9

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 (68) hide show
  1. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  2. package/package.json +1 -1
  3. package/platform_runner.py +11 -2
  4. package/platforms/chatgpt/actions/_lifecycle_common.py +73 -20
  5. package/platforms/chatgpt/actions/_new_turn.py +1 -0
  6. package/platforms/chatgpt/actions/_readiness.py +29 -3
  7. package/platforms/chatgpt/actions/_submit_thread.py +20 -3
  8. package/platforms/chatgpt/actions/_thread_turn.py +31 -2
  9. package/platforms/chatgpt/tests/test_contract.py +107 -1
  10. package/platforms/iyf/AIRPLAY.md +25 -0
  11. package/platforms/iyf/actions/_find.py +18 -0
  12. package/platforms/iyf/actions/_open.py +10 -0
  13. package/platforms/iyf/manifest.yaml +11 -0
  14. package/platforms/iyf/safari-airplay +57 -0
  15. package/pyproject.toml +2 -2
  16. package/session_client.py +10 -0
  17. package/src/browser_workspace.egg-info/PKG-INFO +2 -2
  18. package/src/browser_workspace.egg-info/SOURCES.txt +53 -46
  19. package/src/browser_workspace.egg-info/requires.txt +1 -1
  20. package/src/browser_workspace_app/package.json +1 -1
  21. package/src/browser_workspace_app/pyproject.toml +2 -2
  22. package/src/browser_workspace_app/session_client.py +10 -0
  23. package/src/browser_workspace_app/platform_runner.py +0 -204
  24. package/src/browser_workspace_app/platforms/chatgpt/README.md +0 -60
  25. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +0 -58
  26. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +0 -119
  27. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +0 -80
  28. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +0 -85
  29. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +0 -33
  30. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +0 -353
  31. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +0 -52
  32. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +0 -129
  33. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +0 -591
  34. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +0 -362
  35. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +0 -34
  36. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +0 -183
  37. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +0 -49
  38. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +0 -425
  39. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +0 -7
  40. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +0 -10
  41. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +0 -10
  42. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +0 -18
  43. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +0 -11
  44. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +0 -9
  45. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +0 -33
  46. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +0 -83
  47. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +0 -32
  48. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +0 -210
  49. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +0 -21
  50. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +0 -25
  51. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +0 -24
  52. package/src/browser_workspace_app/platforms/tiktok/README.md +0 -10
  53. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +0 -180
  54. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +0 -99
  55. package/src/browser_workspace_app/platforms/wechat-channels/README.md +0 -10
  56. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +0 -163
  57. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +0 -130
  58. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +0 -231
  59. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +0 -124
  60. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +0 -36
  61. package/src/browser_workspace_app/platforms/xhs/README.md +0 -10
  62. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +0 -355
  63. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +0 -281
  64. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +0 -107
  65. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +0 -34
  66. package/src/browser_workspace_app/platforms/youtube/README.md +0 -10
  67. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +0 -103
  68. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +0 -94
@@ -1,591 +0,0 @@
1
- # Canonical one-shot ChatGPT Temporary Chat submission mechanics.
2
- # Lifecycle/barrier ownership belongs to the caller (Agents Relay).
3
- """Bounded polling helpers for ChatGPT submit readiness."""
4
- from __future__ import annotations
5
-
6
- import time
7
- from collections.abc import Callable, Mapping
8
- from urllib.parse import urlencode
9
-
10
-
11
- def wait_until_stable(
12
- read_state: Callable[[], Mapping[str, object]],
13
- ready: Callable[[Mapping[str, object]], bool],
14
- *,
15
- timeout: float,
16
- interval: float = 0.25,
17
- stable_samples: int = 2,
18
- phase: str,
19
- ) -> Mapping[str, object]:
20
- """Return only after the observable state is ready for stable_samples polls."""
21
- deadline = time.monotonic() + timeout
22
- stable = 0
23
- last_state: Mapping[str, object] = {}
24
- while time.monotonic() < deadline:
25
- last_state = read_state()
26
- if ready(last_state):
27
- stable += 1
28
- if stable >= stable_samples:
29
- return last_state
30
- else:
31
- stable = 0
32
- time.sleep(interval)
33
- raise RuntimeError(f"Temporary Chat {phase} was not observed ready")
34
-
35
-
36
- def actionable_temporary_chat_candidates(candidates):
37
- return [
38
- candidate for candidate in candidates
39
- if candidate.get("visible")
40
- and not candidate.get("disabled")
41
- and candidate.get("pointerEvents") != "none"
42
- ]
43
-
44
-
45
- def temporary_chat_entry_url(prompt=""):
46
- """Open Temporary Chat and let ChatGPT prefill the prompt from the URL."""
47
- query = {"temporary-chat": "true"}
48
- if prompt:
49
- query["prompt"] = prompt
50
- return "https://chatgpt.com/?" + urlencode(query)
51
-
52
-
53
-
54
- def temporary_chat_enabled_state(url, candidates):
55
- """Recognize Temporary Chat from stable URL state with semantic UI fallback."""
56
- if "temporary-chat=true" in (url or ""):
57
- return True
58
- return any(
59
- "turn off" in f"{candidate.get('label') or ''} {candidate.get('text') or ''}".lower()
60
- for candidate in candidates
61
- if candidate.get("visible")
62
- and not candidate.get("disabled")
63
- and candidate.get("pointerEvents") != "none"
64
- )
65
-
66
-
67
- def prompt_text_matches(observed, expected):
68
- """Verify long composer/user-turn text despite ProseMirror whitespace reshaping."""
69
- import re
70
- normalize = lambda value: re.sub(r"\s+", " ", value or "").strip()
71
- observed_n = normalize(observed)
72
- expected_n = normalize(expected)
73
- if expected_n in observed_n:
74
- return True
75
- if not expected_n:
76
- return not observed_n
77
- span = min(256, max(48, len(expected_n) // 8))
78
- return (
79
- len(observed_n) >= int(len(expected_n) * 0.9)
80
- and expected_n[:span] in observed_n
81
- and expected_n[-span:] in observed_n
82
- )
83
-
84
-
85
- def submission_receipt(turns, before_count, prompt, composer_text):
86
- """Return a transport receipt after click without requiring one DOM message shape."""
87
- for turn in turns[before_count:]:
88
- if prompt_text_matches(turn.get("text"), prompt):
89
- return {"verified_by": "user-turn", "turn": turn}
90
- if not (composer_text or "").strip():
91
- return {
92
- "verified_by": "composer-cleared",
93
- "turn": {"id": None, "text": prompt},
94
- }
95
- return None
96
-
97
-
98
- import atexit
99
- import json
100
- import os
101
- import re
102
- import time
103
-
104
-
105
-
106
- CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
107
- _OWNED_TABS = []
108
- _KEEP_OWNED_TAB_OPEN = True
109
- _SUBMISSION_SUCCEEDED = False
110
-
111
-
112
-
113
- def _new_owned_tab(url):
114
- # Respect Browser Harness workspace adapters. Record both target + workspace
115
- # tab id so cleanup remains scoped to exactly this worker's own lease even
116
- # if ChatGPT navigation invalidates the original target mapping.
117
- target_id = new_tab(url)
118
- tabs = [tab for tab in list_tabs(include_chrome=False) if tab.get("targetId") == target_id]
119
- if len(tabs) != 1:
120
- raise RuntimeError(f"Owned Browser Workspace target {target_id!r} was not mapped to one tab")
121
- _OWNED_TABS.append({
122
- "targetId": target_id,
123
- "tabId": tabs[0].get("tabId"),
124
- })
125
- return target_id
126
-
127
-
128
- def _close_owned_tabs():
129
- # close-policy=never preserves an owned tab only after a verified submit.
130
- # Any failure before that point must release this worker's own lease.
131
- if _KEEP_OWNED_TAB_OPEN and _SUBMISSION_SUCCEEDED:
132
- return
133
- while _OWNED_TABS:
134
- owned = _OWNED_TABS.pop()
135
- try:
136
- close_tab(owned["targetId"])
137
- continue
138
- except Exception:
139
- pass
140
- try:
141
- current = current_tab()
142
- if owned.get("tabId") is not None and current.get("tabId") == owned["tabId"]:
143
- close_tab()
144
- except Exception:
145
- pass
146
-
147
-
148
- atexit.register(_close_owned_tabs)
149
-
150
-
151
- def _composer():
152
- selector = js("""(() => {
153
- const selectors = [
154
- '#prompt-textarea',
155
- '[contenteditable="true"][data-composer-markdown]',
156
- '[contenteditable="true"][data-lexical-editor="true"]',
157
- 'textarea'
158
- ];
159
- for (const selector of selectors) {
160
- const e=document.querySelector(selector);
161
- if (!e || e.id === 'pending-home-input' || e.disabled) continue;
162
- const r=e.getBoundingClientRect();
163
- if (r.width>0 && r.height>0) return selector;
164
- }
165
- const visible = [...document.querySelectorAll('[contenteditable="true"]')]
166
- .filter(e => {
167
- if (e.id === 'pending-home-input' || e.disabled) return false;
168
- const r=e.getBoundingClientRect();
169
- return r.width>0 && r.height>0;
170
- });
171
- return visible.length === 1 ? '[contenteditable="true"]' : null;
172
- })()""")
173
- if not selector:
174
- raise RuntimeError("ChatGPT composer was not observed")
175
- return selector
176
-
177
- def _composer_text(selector):
178
- return js(f"""(() => {{
179
- const e=document.querySelector({json.dumps(selector)});
180
- if (!e) return '';
181
- return e.getAttribute('contenteditable') === 'true'
182
- ? (e.innerText || '')
183
- : (e.value || '');
184
- }})()""") or ""
185
-
186
-
187
- def _wait_for_composer(timeout=30):
188
- def read_selector():
189
- try:
190
- return _composer()
191
- except RuntimeError:
192
- return None
193
-
194
- return wait_until_stable(
195
- lambda: {"selector": read_selector()},
196
- lambda state: bool(state["selector"]),
197
- timeout=timeout,
198
- phase="composer readiness",
199
- )["selector"]
200
-
201
-
202
- def _same_text(observed, expected):
203
- normalize = lambda value: re.sub(r"\\s+", " ", value or "").strip()
204
- return normalize(expected) in normalize(observed)
205
-
206
-
207
- def _normalized_text(value):
208
- return re.sub(r"\s+", " ", value or "").strip()
209
-
210
-
211
- def _temporary_chat_candidates():
212
- return js(r"""(() => [...document.querySelectorAll('button')].map((b, index) => {
213
- const label=(b.getAttribute('aria-label') || '').trim();
214
- const text=(b.innerText || '').trim();
215
- const semantic=/temporary/i.test(label + ' ' + text) && /chat/i.test(label + ' ' + text);
216
- if (!semantic) return null;
217
- const r=b.getBoundingClientRect();
218
- const style=getComputedStyle(b);
219
- return {
220
- index,
221
- label,
222
- text,
223
- visible:r.width > 0 && r.height > 0 && style.visibility !== 'hidden' && style.display !== 'none',
224
- disabled:!!b.disabled || b.getAttribute('aria-disabled') === 'true',
225
- pointerEvents:style.pointerEvents,
226
- };
227
- }).filter(Boolean))()""") or []
228
-
229
-
230
- def _temporary_chat_enabled():
231
- return temporary_chat_enabled_state(
232
- page_info().get("url", ""),
233
- _temporary_chat_candidates(),
234
- )
235
-
236
-
237
- def _click_temporary_chat_toggle(timeout=20):
238
- deadline = time.time() + timeout
239
- last_candidates = []
240
- while time.time() < deadline:
241
- if _temporary_chat_enabled():
242
- return "already-enabled"
243
- last_candidates = _temporary_chat_candidates()
244
- actionable = actionable_temporary_chat_candidates(last_candidates)
245
- if len(actionable) == 1:
246
- clicked = js(f"""(() => {{
247
- const b=[...document.querySelectorAll('button')][{int(actionable[0]["index"])}];
248
- if (!b) return false;
249
- const r=b.getBoundingClientRect();
250
- const style=getComputedStyle(b);
251
- if (!(r.width > 0 && r.height > 0) || b.disabled ||
252
- b.getAttribute('aria-disabled') === 'true' || style.pointerEvents === 'none') return false;
253
- b.click();
254
- return true;
255
- }})()""")
256
- if clicked:
257
- return "clicked"
258
- elif len(actionable) > 1:
259
- labels = [candidate.get("label") or candidate.get("text") or "<unnamed>" for candidate in actionable]
260
- raise RuntimeError(f"Temporary Chat toggle is ambiguous: {len(actionable)} actionable controls {labels}")
261
- time.sleep(.25)
262
- raise RuntimeError(
263
- "Temporary Chat toggle was not actionable after "
264
- f"{timeout}s; semantic candidates={last_candidates}"
265
- )
266
-
267
-
268
- def _attachment_names():
269
- return js(r"""(() => [...document.querySelectorAll('button[aria-label^="Remove file"]')]
270
- .map(b => (b.getAttribute('aria-label') || '').replace(/^Remove file\s+\d+:\s*/, ''))
271
- .filter(Boolean))()""") or []
272
-
273
-
274
- def _attachment_state():
275
- return js(r"""(() => {
276
- const names = [...document.querySelectorAll('button[aria-label^="Remove "]')]
277
- .map(b => (b.getAttribute('aria-label') || '')
278
- .replace(/^Remove file\s+\d+:\s*/, '')
279
- .replace(/^Remove\s+/, ''))
280
- .filter(Boolean);
281
- return {names, pending: false};
282
- })()""") or {"names": [], "pending": False}
283
-
284
-
285
- def _send_state():
286
- return js(r"""(() => {
287
- const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"],button[aria-label="Send"]');
288
- return {present: !!b, enabled: !!b && !b.disabled && b.getAttribute('aria-disabled') !== 'true'};
289
- })()""") or {"present": False, "enabled": False}
290
-
291
-
292
- def _wait_for_attachments(expected, timeout=45):
293
- expected = set(expected)
294
- wait_until_stable(
295
- _attachment_state,
296
- lambda state: expected.issubset(state["names"]) and not state["pending"],
297
- timeout=timeout,
298
- phase="attachment upload readiness",
299
- )
300
-
301
-
302
- def _upload_files(paths):
303
- if not paths:
304
- return []
305
-
306
- def file_input_selector():
307
- return js("""(() => {
308
- const selectors = [
309
- '#upload-files',
310
- '#upload-media',
311
- 'input[name="upload-media"]',
312
- 'input[type="file"]:not([accept])',
313
- 'input[type="file"]'
314
- ];
315
- for (const s of selectors) if (document.querySelector(s)) return s;
316
- return null;
317
- })()""")
318
-
319
- selector = file_input_selector()
320
- if not selector:
321
- opened = js("""(() => {
322
- const b=[...document.querySelectorAll('button')].find(b =>
323
- (b.getAttribute('aria-label') || '').trim() === 'Add files and more');
324
- if (!b) return false;
325
- b.click();
326
- return true;
327
- })()""")
328
- if opened:
329
- deadline = time.time() + 5
330
- while time.time() < deadline and not selector:
331
- selector = file_input_selector()
332
- if not selector:
333
- time.sleep(.1)
334
- if not selector:
335
- raise RuntimeError("ChatGPT file input was not observed")
336
- for path in paths:
337
- if not os.path.isfile(path):
338
- raise RuntimeError(f"attachment does not exist: {path}")
339
- upload_file(selector, path)
340
- expected = [os.path.basename(path) for path in paths]
341
- if expected:
342
- # Presence is a selection signal; upload readiness is checked separately.
343
- wait_until_stable(
344
- _attachment_state,
345
- lambda state: set(expected).issubset(state["names"]),
346
- timeout=30,
347
- phase="attachment presence",
348
- )
349
- _wait_for_attachments(expected)
350
- return expected
351
-
352
-
353
- def _user_turns():
354
- return js("""(() => [...document.querySelectorAll('[data-message-author-role="user"]')]
355
- .map(e => ({text:e.innerText.trim(), id:e.getAttribute('data-message-id') || ''}))
356
- .filter(e => e.text))()""") or []
357
-
358
-
359
- def _click_send():
360
- ok = js("""(() => {
361
- const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"],button[aria-label="Send"]');
362
- if (!b || b.disabled || b.getAttribute('aria-disabled') === 'true') return false;
363
- b.click(); return true;
364
- })()""")
365
- if not ok:
366
- raise RuntimeError("Temporary Chat send readiness changed before click")
367
-
368
-
369
- def _wait_for_send_ready(selector, prompt, attachments):
370
- expected = set(attachments)
371
-
372
- def read_state():
373
- try:
374
- live_selector = _composer()
375
- except RuntimeError:
376
- live_selector = selector
377
- return {
378
- "selector": live_selector,
379
- "text": _composer_text(live_selector),
380
- "attachments": _attachment_state(),
381
- "send": _send_state(),
382
- }
383
-
384
- return wait_until_stable(
385
- read_state,
386
- lambda state: (
387
- prompt_text_matches(state["text"], prompt)
388
- and expected.issubset(state["attachments"]["names"])
389
- and state["send"]["enabled"]
390
- ),
391
- timeout=45 if expected else 15,
392
- phase="send readiness",
393
- )
394
-
395
-
396
- def _wait_user_turn(before_count, prompt, selector, timeout=20):
397
- deadline = time.time() + timeout
398
- cleared_polls = 0
399
- last_error = None
400
- while time.time() < deadline:
401
- try:
402
- receipt = submission_receipt(
403
- _user_turns(),
404
- before_count,
405
- prompt,
406
- _composer_text(selector),
407
- )
408
- last_error = None
409
- except RuntimeError as exc:
410
- # The Browser Harness/CDP daemon can transiently time out while
411
- # ChatGPT is committing a navigation/message update. Receipt
412
- # observation is read-only, so retry within the existing bound.
413
- last_error = exc
414
- time.sleep(.25)
415
- continue
416
- if receipt:
417
- if receipt["verified_by"] == "user-turn":
418
- return receipt
419
- cleared_polls += 1
420
- if cleared_polls >= 2:
421
- return receipt
422
- else:
423
- cleared_polls = 0
424
- time.sleep(.25)
425
- suffix = f"; last observation error: {last_error}" if last_error else ""
426
- raise RuntimeError(
427
- "ChatGPT submission verification did not observe an accepted submission" + suffix
428
- )
429
-
430
-
431
- def _diagnostic_thread_id():
432
- match = re.search(r"/c/([^/?#]+)", page_info().get("url", ""))
433
- return match.group(1) if match else None
434
-
435
-
436
-
437
-
438
- def _fill_prompt_if_needed(prompt, mode_label, timeout=30):
439
- deadline = time.time() + timeout
440
- last_error = None
441
- while time.time() < deadline:
442
- try:
443
- selector = _wait_for_composer(timeout=min(5, max(1, deadline - time.time())))
444
- except RuntimeError as exc:
445
- # Hydration and navigation can temporarily remove the composer.
446
- # Continue the outer readiness window rather than aborting on the first poll.
447
- last_error = exc
448
- time.sleep(.25)
449
- continue
450
- if prompt_text_matches(_composer_text(selector), prompt):
451
- return selector
452
-
453
- is_contenteditable = bool(js(f"""(() => {{
454
- const e=document.querySelector({json.dumps(selector)});
455
- return !!e && e.getAttribute('contenteditable') === 'true';
456
- }})()"""))
457
- try:
458
- if is_contenteditable:
459
- cleared = js(f"""(() => {{
460
- const e=document.querySelector({json.dumps(selector)});
461
- if (!e) return false;
462
- e.focus();
463
- const sel=window.getSelection();
464
- const range=document.createRange();
465
- range.selectNodeContents(e);
466
- sel.removeAllRanges();
467
- sel.addRange(range);
468
- document.execCommand('delete', false, null);
469
- return true;
470
- }})()""")
471
- if not cleared:
472
- raise RuntimeError(
473
- f"{mode_label} contenteditable composer could not be cleared"
474
- )
475
- for offset in range(0, len(prompt), 256):
476
- chunk = prompt[offset:offset + 256]
477
- inserted = js(f"""(() => {{
478
- const e=document.querySelector({json.dumps(selector)});
479
- if (!e) return false;
480
- e.focus();
481
- const ok=document.execCommand(
482
- 'insertText', false, {json.dumps(chunk)}
483
- );
484
- e.dispatchEvent(new InputEvent('input', {{
485
- bubbles:true,
486
- inputType:'insertText',
487
- data:{json.dumps(chunk)}
488
- }}));
489
- return ok;
490
- }})()""")
491
- if not inserted:
492
- raise RuntimeError(
493
- f"{mode_label} composer rejected prompt chunk at offset {offset}"
494
- )
495
- else:
496
- fill_input(selector, prompt, clear_first=True)
497
- return selector
498
- except RuntimeError as exc:
499
- # ChatGPT can replace textarea with ProseMirror during hydration.
500
- # Re-resolve the live composer instead of keeping a stale selector.
501
- last_error = exc
502
- time.sleep(.25)
503
-
504
- suffix = f"; last fill error: {last_error}" if last_error else ""
505
- raise RuntimeError(f"{mode_label} composer could not be filled{suffix}")
506
-
507
-
508
- def _live_composer_state():
509
- try:
510
- selector = _composer()
511
- except RuntimeError:
512
- return {"selector": None, "text": ""}
513
- return {"selector": selector, "text": _composer_text(selector)}
514
-
515
-
516
- def _submit_current_page(prompt, *, temporary, mode_label):
517
- _fill_prompt_if_needed(prompt, mode_label)
518
- composer_state = wait_until_stable(
519
- _live_composer_state,
520
- lambda state: (
521
- bool(state["selector"])
522
- and prompt_text_matches(state["text"], prompt)
523
- ),
524
- timeout=30,
525
- phase=f"{mode_label} composer readiness",
526
- )
527
- selector = composer_state["selector"]
528
-
529
- attachments = _upload_files(CFG.get("file", []))
530
- before_count = len(_user_turns())
531
-
532
- send_state = _wait_for_send_ready(selector, prompt, attachments)
533
- if isinstance(send_state, dict) and send_state.get("selector"):
534
- selector = send_state["selector"]
535
-
536
- _click_send()
537
- receipt = _wait_user_turn(before_count, prompt, selector)
538
- global _SUBMISSION_SUCCEEDED
539
- _SUBMISSION_SUCCEEDED = True
540
- owned = _OWNED_TABS[-1] if _OWNED_TABS else {}
541
- return {
542
- "operation": "submit",
543
- "status": "submitted",
544
- "temporary": temporary,
545
- "attachments": attachments,
546
- "diagnostic_thread_id": _diagnostic_thread_id(),
547
- "user_message_id": receipt["turn"].get("id") or None,
548
- "owned_tab_id": owned.get("tabId"),
549
- "owned_target_id": owned.get("targetId"),
550
- "verified_by": receipt["verified_by"],
551
- "verified": True,
552
- }
553
-
554
- def _wait_for_release():
555
- release_file = CFG.get("release_file")
556
- if not release_file:
557
- raise RuntimeError("worker release file was not configured")
558
- if CFG.get("close_policy", "after-start") == "never":
559
- raise SystemExit(0)
560
- while not os.path.exists(release_file):
561
- time.sleep(.1)
562
-
563
-
564
- prompt = CFG["prompt"]
565
- _new_owned_tab(temporary_chat_entry_url(prompt))
566
- wait_for_load()
567
-
568
- _click_temporary_chat_toggle()
569
- deadline = time.time() + 20
570
- while time.time() < deadline:
571
- if _temporary_chat_enabled():
572
- break
573
- time.sleep(.25)
574
- else:
575
- raise RuntimeError("Temporary Chat activation was not observed")
576
-
577
- try:
578
- result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
579
- except RuntimeError as exc:
580
- # A URL-prefilled Temporary Chat can retain the pre-hydration home input
581
- # without ever mounting the active composer. No send occurred, so retry
582
- # once on a clean Temporary Chat page without URL prompt prefill.
583
- if "composer readiness was not observed ready" not in str(exc):
584
- raise
585
- goto_url(temporary_chat_entry_url())
586
- wait_for_load()
587
- _click_temporary_chat_toggle()
588
- if not _temporary_chat_enabled():
589
- raise RuntimeError("Temporary Chat mode lost during composer recovery") from exc
590
- result = _submit_current_page(prompt, temporary=True, mode_label="Temporary Chat")
591
- print(json.dumps(result, ensure_ascii=False), flush=True)