browser-workspace 0.4.2 → 0.4.5

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 (107) hide show
  1. package/README.md +15 -1
  2. package/SKILL.md +22 -21
  3. package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
  4. package/package.json +1 -1
  5. package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
  6. package/platforms/chatgpt/tests/test_contract.py +7 -0
  7. package/pyproject.toml +6 -2
  8. package/session_daemon.py +91 -3
  9. package/src/browser_workspace.egg-info/PKG-INFO +2 -1
  10. package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
  11. package/src/browser_workspace.egg-info/entry_points.txt +2 -0
  12. package/src/browser_workspace.egg-info/requires.txt +1 -0
  13. package/src/browser_workspace.egg-info/top_level.txt +1 -1
  14. package/src/browser_workspace_app/__init__.py +1 -0
  15. package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
  16. package/src/browser_workspace_app/cli.py +236 -0
  17. package/src/browser_workspace_app/extension/manifest.json +26 -0
  18. package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
  19. package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
  20. package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
  21. package/src/browser_workspace_app/extension/workspace.html +8 -0
  22. package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
  23. package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
  24. package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
  25. package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
  26. package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
  27. package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
  28. package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
  29. package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
  30. package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
  31. package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
  32. package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
  33. package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
  34. package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
  35. package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
  36. package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
  37. package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
  38. package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
  39. package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
  40. package/src/browser_workspace_app/managed_chrome.py +244 -0
  41. package/src/browser_workspace_app/node/admin.mjs +44 -0
  42. package/src/browser_workspace_app/package.json +36 -0
  43. package/src/browser_workspace_app/platform_runner.py +204 -0
  44. package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
  45. package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
  46. package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
  47. package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
  48. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
  49. package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
  50. package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
  51. package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
  52. package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
  53. package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
  54. package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
  55. package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
  56. package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
  57. package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
  58. package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
  59. package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
  60. package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
  61. package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
  62. package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
  63. package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
  64. package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
  65. package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
  66. package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
  67. package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
  68. package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
  69. package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
  70. package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
  71. package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
  72. package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
  73. package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
  74. package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
  75. package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
  76. package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
  77. package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
  78. package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
  79. package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
  80. package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
  81. package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
  82. package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
  83. package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
  84. package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
  85. package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
  86. package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
  87. package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
  88. package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
  89. package/src/browser_workspace_app/pyproject.toml +33 -0
  90. package/src/browser_workspace_app/session_client.py +107 -0
  91. package/src/browser_workspace_app/session_daemon.py +431 -0
  92. package/src/browser_harness/__init__.py +0 -2
  93. package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
  94. package/src/browser_harness/_ipc.py +0 -201
  95. package/src/browser_harness/admin.py +0 -1588
  96. package/src/browser_harness/auth.py +0 -546
  97. package/src/browser_harness/daemon.py +0 -894
  98. package/src/browser_harness/helpers.py +0 -668
  99. package/src/browser_harness/macos.py +0 -131
  100. package/src/browser_harness/mcp_cli.py +0 -16
  101. package/src/browser_harness/paths.py +0 -49
  102. package/src/browser_harness/recorder.py +0 -343
  103. package/src/browser_harness/run.py +0 -413
  104. package/src/browser_harness/telemetry.py +0 -308
  105. package/src/browser_harness/video-template.html +0 -970
  106. package/src/browser_harness/video.py +0 -749
  107. package/src/browser_harness/video_render.py +0 -524
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+ import argparse, json, os, sys
4
+ from pathlib import Path
5
+ ROOT=Path(__file__).resolve().parent
6
+ VENV_PY=ROOT/'.venv'/'bin'/'python'
7
+ if VENV_PY.exists() and Path(sys.executable).resolve()!=VENV_PY.resolve() and os.environ.get('BROWSER_WORKSPACE_VENV_REEXEC')!='1':
8
+ env=dict(os.environ); env['BROWSER_WORKSPACE_VENV_REEXEC']='1'
9
+ os.execve(str(VENV_PY),[str(VENV_PY),str(Path(__file__).resolve()),*sys.argv[1:]],env)
10
+ sys.path.insert(0,str(ROOT))
11
+ from session_client import SOCKET_PATH, ensure_daemon
12
+ from platform_runner import run_platform_action
13
+
14
+ def emit(x): print(json.dumps(x,ensure_ascii=False))
15
+
16
+ def status():
17
+ daemon=ensure_daemon()
18
+ helper=(ROOT/'node'/'admin.mjs').resolve()
19
+ return {
20
+ 'root': str(ROOT.resolve()),
21
+ 'cli': str(Path(__file__).resolve()),
22
+ 'admin_helper': str(helper),
23
+ 'managed_chrome': daemon.get('managed_chrome') if daemon else None,
24
+ 'daemon': {
25
+ 'running': bool(daemon and daemon.get('ok')),
26
+ 'pid': daemon.get('pid') if daemon else None,
27
+ 'session_count': daemon.get('session_count') if daemon else None,
28
+ 'socket': str(SOCKET_PATH),
29
+ },
30
+ }
31
+
32
+ def main():
33
+ fmt=argparse.RawDescriptionHelpFormatter
34
+ p=argparse.ArgumentParser(
35
+ prog='browser-workspace',
36
+ description='Control the shared Browser Workspace Chrome, browser sessions, and platform automations.',
37
+ epilog='''Examples:
38
+ browser-workspace status
39
+ browser-workspace session start --url https://example.com
40
+ browser-workspace session list
41
+ browser-workspace session exec SESSION_ID --code-file script.py
42
+ browser-workspace session stop SESSION_ID
43
+ browser-workspace session stop --all
44
+ browser-workspace platform profile wechat-channels
45
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
46
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
47
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
48
+ ''',
49
+ formatter_class=fmt,
50
+ )
51
+ sp=p.add_subparsers(dest='group',required=True,title='commands',metavar='COMMAND')
52
+
53
+ sp.add_parser(
54
+ 'status',
55
+ help='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
56
+ description='Show Browser Workspace runtime, managed Chrome, daemon, and active-session status.',
57
+ epilog='Example:\n browser-workspace status',
58
+ formatter_class=fmt,
59
+ )
60
+
61
+ session=sp.add_parser(
62
+ 'session',
63
+ help='Create, inspect, execute code in, and stop browser sessions.',
64
+ description='Manage Browser Workspace sessions.',
65
+ epilog='''Examples:
66
+ browser-workspace session start --url https://example.com
67
+ browser-workspace session list
68
+ browser-workspace session exec SESSION_ID --code-file script.py
69
+ cat script.py | browser-workspace session exec SESSION_ID
70
+ browser-workspace session stop SESSION_ID
71
+ browser-workspace session stop --all
72
+ ''',
73
+ formatter_class=fmt,
74
+ )
75
+ ssp=session.add_subparsers(dest='cmd',required=True,title='session commands',metavar='COMMAND')
76
+
77
+ start=ssp.add_parser(
78
+ 'start',
79
+ help='Start a browser session, optionally opening a URL.',
80
+ description='Start a browser session. The shared managed Chrome is reused; this does not launch a separate browser per session.',
81
+ epilog='''Examples:
82
+ browser-workspace session start
83
+ browser-workspace session start --url https://example.com
84
+ ''',
85
+ formatter_class=fmt,
86
+ )
87
+ start.add_argument('--workspace', help=argparse.SUPPRESS)
88
+ start.add_argument('--url', metavar='URL', help='URL to open when the session starts.')
89
+
90
+ ssp.add_parser(
91
+ 'list',
92
+ help='List currently active Browser Workspace sessions.',
93
+ description='List currently active Browser Workspace sessions and their target tabs.',
94
+ epilog='Example:\n browser-workspace session list',
95
+ formatter_class=fmt,
96
+ )
97
+
98
+ ex=ssp.add_parser(
99
+ 'exec',
100
+ help='Execute Browser Workspace Python helpers inside an existing session.',
101
+ description='Execute Python using Browser Workspace helpers in an existing session. Read code from --code-file or stdin.',
102
+ epilog='''Examples:
103
+ browser-workspace session exec SESSION_ID --code-file script.py
104
+
105
+ browser-workspace session exec SESSION_ID <<'PY'
106
+ print(page_info())
107
+ print(snapshot(interactive_only=False))
108
+ PY
109
+ ''',
110
+ formatter_class=fmt,
111
+ )
112
+ ex.add_argument('session_id', metavar='SESSION_ID', help='Session ID returned by "session start" or "session list".')
113
+ ex.add_argument('--code-file', metavar='PATH', help='Read Python code from this file instead of stdin.')
114
+
115
+ stop=ssp.add_parser(
116
+ 'stop',
117
+ help='Stop one session or all sessions and release their Browser Workspace tabs.',
118
+ description='Stop one Browser Workspace session, or all sessions with --all.',
119
+ epilog='''Examples:
120
+ browser-workspace session stop SESSION_ID
121
+ browser-workspace session stop --all
122
+ ''',
123
+ formatter_class=fmt,
124
+ )
125
+ stop.add_argument('session_id', nargs='?', metavar='SESSION_ID', help='Session ID to stop.')
126
+ stop.add_argument('--all', action='store_true', dest='stop_all', help='Stop all active Browser Workspace sessions.')
127
+
128
+ platform=sp.add_parser(
129
+ 'platform',
130
+ help='Inspect and run reusable website-specific platform automations.',
131
+ description='Inspect and run reusable Browser Workspace platform automations.',
132
+ epilog='''Examples:
133
+ browser-workspace platform profile wechat-channels
134
+ browser-workspace platform source wechat-channels post --config post.json
135
+ browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
136
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
137
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
138
+ browser-workspace session stop SESSION_ID
139
+
140
+ One-shot mode (no orchestrator self-heal):
141
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
142
+ ''',
143
+ formatter_class=fmt,
144
+ )
145
+ psp=platform.add_subparsers(dest='pcmd',required=True,title='platform commands',metavar='COMMAND')
146
+
147
+ profile=psp.add_parser(
148
+ 'profile',
149
+ help='Show a platform manifest/profile: fields, media rules, and capabilities.',
150
+ description='Show the declared Browser Workspace profile for a platform.',
151
+ epilog='Example:\n browser-workspace platform profile wechat-channels',
152
+ formatter_class=fmt,
153
+ )
154
+ profile.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
155
+
156
+ source=psp.add_parser(
157
+ 'source',
158
+ help='Print the fully prepared Python source for one platform action without running it.',
159
+ description='Render the action source after injecting the optional JSON config path. No browser action is executed.',
160
+ epilog='''Examples:
161
+ browser-workspace platform source wechat-channels post --config post.json
162
+ browser-workspace platform source wechat-channels manage --config status.json
163
+ ''',
164
+ formatter_class=fmt,
165
+ )
166
+ source.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
167
+ source.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
168
+ source.add_argument('--config', metavar='PATH', help='JSON configuration file used to prepare the action.')
169
+
170
+ run=psp.add_parser(
171
+ 'run',
172
+ help='Run one platform action in Browser Workspace.',
173
+ description='Run a declared platform action. A caller-owned --session-id is required by default so an orchestrator can inspect failures, self-heal in the same browser state, write repairs back to platform code, and close the session only after completion. --auto-session is an explicit one-shot mode without orchestrator self-heal.',
174
+ epilog='''Examples:
175
+ browser-workspace session start --url https://channels.weixin.qq.com/platform/post/list
176
+ browser-workspace platform run wechat-channels manage --session-id SESSION_ID --config status.json
177
+ browser-workspace platform run wechat-channels post --session-id SESSION_ID --config post.json
178
+ browser-workspace session stop SESSION_ID
179
+
180
+ One-shot mode (no orchestrator self-heal):
181
+ browser-workspace platform run wechat-channels manage --auto-session --config status.json
182
+ ''',
183
+ formatter_class=fmt,
184
+ )
185
+ run.add_argument('platform', metavar='PLATFORM', help='Platform name, for example "wechat-channels".')
186
+ run.add_argument('action', metavar='ACTION', help='Declared action name, for example "post" or "manage".')
187
+ run.add_argument('--config', metavar='PATH', help='JSON configuration file for the platform action.')
188
+ run.add_argument('--url', metavar='URL', help='Optional initial URL for a temporary session.')
189
+ session_mode = run.add_mutually_exclusive_group()
190
+ session_mode.add_argument('--session-id', metavar='SESSION_ID', help='Required by default. Reuse a caller-owned session so failures can be inspected and self-healed before the session is closed.')
191
+ session_mode.add_argument('--auto-session', action='store_true', help='Explicit one-shot mode: create and clean up a temporary session automatically. No orchestrator self-heal is available after failure.')
192
+ args=p.parse_args()
193
+ if args.group=='status': emit(status()); return
194
+ if args.group=='session' and args.cmd=='list':
195
+ from session_client import request
196
+ emit(request({'op':'list'})); return
197
+ if args.group=='session' and args.cmd=='start':
198
+ from session_client import request
199
+ emit(request({'op':'start','workspace':args.workspace,'url':args.url})); return
200
+ if args.group=='session' and args.cmd=='exec':
201
+ from session_client import request
202
+ if args.code_file: code=Path(args.code_file).read_text()
203
+ elif not sys.stdin.isatty(): code=sys.stdin.read()
204
+ else: p.error('session exec reads Python from stdin (use a heredoc) or --code-file')
205
+ r=request({'op':'exec','session_id':args.session_id,'code':code}); emit(r); raise SystemExit(0 if r.get('ok',False) else 1)
206
+ if args.group=='session' and args.cmd=='stop':
207
+ from session_client import request
208
+ if args.stop_all:
209
+ if args.session_id: p.error('session stop accepts either SESSION_ID or --all, not both')
210
+ emit(request({'op':'stop-all'})); return
211
+ if not args.session_id: p.error('session stop requires SESSION_ID or --all')
212
+ emit(request({'op':'stop','session_id':args.session_id})); return
213
+ if args.group=='platform' and args.pcmd=='source':
214
+ from platform_runner import action_path, prepare_action
215
+ path=action_path(args.platform,args.action); config=Path(args.config) if args.config else None
216
+ sys.stdout.write(prepare_action(path,config)); return
217
+ if args.group=='platform' and args.pcmd=='profile':
218
+ from platform_runner import platform_profile
219
+ emit(platform_profile(args.platform)); return
220
+ if args.group=='platform' and args.pcmd=='run':
221
+ if not args.session_id and not args.auto_session:
222
+ emit({
223
+ 'ok': False,
224
+ 'error': 'session_id_required',
225
+ 'message': 'platform run requires --session-id by default so the orchestrator can self-heal failures in the same browser session.',
226
+ 'suggested_workflow': [
227
+ 'browser-workspace session start --url <URL>',
228
+ 'browser-workspace platform run <PLATFORM> <ACTION> --session-id SESSION_ID [--config FILE]',
229
+ 'inspect/self-heal failures in the same SESSION_ID and write successful repairs back to platform code',
230
+ 'browser-workspace session stop SESSION_ID',
231
+ ],
232
+ 'auto_session': 'Use --auto-session only for a one-shot run. It creates and closes its own session and does not provide orchestrator self-heal.',
233
+ })
234
+ raise SystemExit(2)
235
+ r=run_platform_action(args.platform,args.action,config_path=args.config,url=args.url,session_id=args.session_id,auto_session=args.auto_session); emit(r); raise SystemExit(0 if (r.get('result') or {}).get('ok',False) else 1)
236
+ if __name__=='__main__': main()
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Browser Workspace",
4
+ "version": "0.3.1",
5
+ "description": "Own named Chrome tab-group workspaces and lazy reusable tab pools for browser agents.",
6
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7NU4eqCIkv793eZGTna5ntNKx42cj7wFWD0b+tEhGWHgXuYMDKXHuvtf1249vozaGGpDPFtH3d5uk0QNCl5yFA+kdL5RgTyg5qIpdFT4NET5Y8HSALxSjy27U4+Lf7XwYkQdSuClK7S9KxUI7OE9bloiDaSWbs9XjEBoeUGALXgqsIMgBVbyQ2RuMUo9mGpTreA+Tp2ATivU5ZnwmOuhJzniefbyAST/U6LY4VK/7jijnHLbfmTmCyi3xHWD2b8l9A7IsDBR1eRk6XY0YqZlC/H2wSmcUUQjhsgpiQK32iwwyNFspKgH4ReRPoO6RSuatUS8+J+/xevwCBF5eBq1QIDAQAB",
7
+ "permissions": [
8
+ "storage",
9
+ "tabs",
10
+ "tabGroups",
11
+ "alarms",
12
+ "webNavigation",
13
+ "contextMenus"
14
+ ],
15
+ "host_permissions": [
16
+ "http://*/*",
17
+ "https://*/*"
18
+ ],
19
+ "background": {
20
+ "service_worker": "service-worker.mjs",
21
+ "type": "module"
22
+ },
23
+ "action": {
24
+ "default_title": "Browser Workspace"
25
+ }
26
+ }
@@ -0,0 +1,126 @@
1
+ import { WorkspaceManager } from "./workspace-manager.mjs";
2
+
3
+ const manager = new WorkspaceManager(chrome);
4
+ const RECLAIM_ALARM = "browser-workspace.reclaim-stale";
5
+ const closingWindowIds = new Set();
6
+
7
+ chrome.alarms.create(RECLAIM_ALARM, { periodInMinutes: 5 });
8
+
9
+ manager.restoreConfiguredWorkspaces().catch((error) => {
10
+ console.warn("Browser Workspace failed to restore configured workspaces", error);
11
+ });
12
+
13
+ const RELEASE_ALL_MENU_ID = "browser-workspace.release-all";
14
+
15
+ async function ensureContextMenus() {
16
+ try {
17
+ await chrome.contextMenus.remove(RELEASE_ALL_MENU_ID);
18
+ } catch {
19
+ // Menu does not exist yet.
20
+ }
21
+ chrome.contextMenus.create({
22
+ id: RELEASE_ALL_MENU_ID,
23
+ title: "Release all tabs",
24
+ contexts: ["action"],
25
+ });
26
+ }
27
+
28
+ chrome.runtime.onInstalled.addListener(() => {
29
+ ensureContextMenus().catch((error) => {
30
+ console.warn("Browser Workspace failed to create extension menu", error);
31
+ });
32
+ });
33
+
34
+ chrome.runtime.onStartup.addListener(() => {
35
+ ensureContextMenus().catch((error) => {
36
+ console.warn("Browser Workspace failed to create extension menu", error);
37
+ });
38
+ });
39
+
40
+ ensureContextMenus().catch((error) => {
41
+ console.warn("Browser Workspace failed to initialize extension menu", error);
42
+ });
43
+
44
+ chrome.contextMenus.onClicked.addListener((info) => {
45
+ if (info.menuItemId !== RELEASE_ALL_MENU_ID) return;
46
+ manager.releaseAll().catch((error) => {
47
+ console.warn("Browser Workspace failed to release all tabs", error);
48
+ });
49
+ });
50
+
51
+ globalThis.browserWorkspaceManagerRpc = async (request) => {
52
+ try {
53
+ return { ok: true, result: await manager.rpc(request) };
54
+ } catch (error) {
55
+ return {
56
+ ok: false,
57
+ error: {
58
+ message: error?.message || String(error),
59
+ name: error?.name || "Error",
60
+ },
61
+ };
62
+ }
63
+ };
64
+
65
+ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
66
+ globalThis.browserWorkspaceManagerRpc(request).then(sendResponse);
67
+ return true;
68
+ });
69
+
70
+ chrome.tabs.onCreated.addListener((tab) => {
71
+ manager.inheritWorkspaceForCreatedTab(tab).catch((error) => {
72
+ console.warn("Browser Workspace failed to inherit child tab group", error);
73
+ });
74
+
75
+ setTimeout(() => {
76
+ manager.reconcileCreatedTab(tab.id).catch((error) => {
77
+ if (!/No tab with id|Invalid tab ID/i.test(String(error?.message || error))) {
78
+ console.warn("Browser Workspace failed delayed created-tab reconciliation", error);
79
+ }
80
+ });
81
+ }, 150);
82
+ });
83
+
84
+ chrome.webNavigation.onCreatedNavigationTarget.addListener((details) => {
85
+ manager.inheritNavigationTarget(details).catch((error) => {
86
+ console.warn("Browser Workspace failed to inherit navigation target", error);
87
+ });
88
+ });
89
+
90
+ chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
91
+ if (!Object.prototype.hasOwnProperty.call(changeInfo, "groupId")) return;
92
+ manager.inheritWorkspaceForCreatedTab(tab).catch((error) => {
93
+ console.warn("Browser Workspace failed to reconcile tab group ownership", error);
94
+ });
95
+ });
96
+
97
+ chrome.tabs.onActivated.addListener((activeInfo) => {
98
+ manager.noteActivatedTab(activeInfo).catch((error) => {
99
+ console.warn("Browser Workspace failed to record active tab context", error);
100
+ });
101
+ });
102
+
103
+ chrome.tabs.onRemoved.addListener((_tabId, removeInfo) => {
104
+ if (!removeInfo?.isWindowClosing) return;
105
+ closingWindowIds.add(removeInfo.windowId);
106
+ setTimeout(() => closingWindowIds.delete(removeInfo.windowId), 1000);
107
+ });
108
+
109
+ chrome.tabGroups.onRemoved.addListener((group) => {
110
+ // Chrome also emits onRemoved while moving a group between windows, and
111
+ // while a whole window is closing. Defer one turn so a moved group can
112
+ // reappear, and preserve persisted workspaces across normal window closes.
113
+ setTimeout(() => {
114
+ if (closingWindowIds.has(group.windowId)) return;
115
+ manager.removeDeletedGroup(group).catch((error) => {
116
+ console.warn("Browser Workspace failed to remove deleted workspace", error);
117
+ });
118
+ }, 100);
119
+ });
120
+
121
+ chrome.alarms.onAlarm.addListener((alarm) => {
122
+ if (alarm.name !== RECLAIM_ALARM) return;
123
+ manager.reclaimStaleTabs().catch((error) => {
124
+ console.warn("Browser Workspace failed to reclaim stale tabs", error);
125
+ });
126
+ });