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.
- package/README.md +15 -1
- package/SKILL.md +22 -21
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/package.json +1 -1
- package/platforms/chatgpt/actions/_temporary_submit.py +22 -2
- package/platforms/chatgpt/tests/test_contract.py +7 -0
- package/pyproject.toml +6 -2
- package/session_daemon.py +91 -3
- package/src/browser_workspace.egg-info/PKG-INFO +2 -1
- package/src/browser_workspace.egg-info/SOURCES.txt +84 -15
- package/src/browser_workspace.egg-info/entry_points.txt +2 -0
- package/src/browser_workspace.egg-info/requires.txt +1 -0
- package/src/browser_workspace.egg-info/top_level.txt +1 -1
- package/src/browser_workspace_app/__init__.py +1 -0
- package/src/browser_workspace_app/agent-workspace/agent_helpers.py +718 -0
- package/src/browser_workspace_app/cli.py +236 -0
- package/src/browser_workspace_app/extension/manifest.json +26 -0
- package/src/browser_workspace_app/extension/service-worker.mjs +126 -0
- package/src/browser_workspace_app/extension/workspace-manager.mjs +680 -0
- package/src/browser_workspace_app/extension/workspace-page.mjs +7 -0
- package/src/browser_workspace_app/extension/workspace.html +8 -0
- package/src/browser_workspace_app/interaction-skills/connection.md +52 -0
- package/src/browser_workspace_app/interaction-skills/cookies.md +3 -0
- package/src/browser_workspace_app/interaction-skills/cross-origin-iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dialogs.md +64 -0
- package/src/browser_workspace_app/interaction-skills/downloads.md +3 -0
- package/src/browser_workspace_app/interaction-skills/drag-and-drop.md +3 -0
- package/src/browser_workspace_app/interaction-skills/dropdowns.md +3 -0
- package/src/browser_workspace_app/interaction-skills/iframes.md +3 -0
- package/src/browser_workspace_app/interaction-skills/network-requests.md +3 -0
- package/src/browser_workspace_app/interaction-skills/print-as-pdf.md +3 -0
- package/src/browser_workspace_app/interaction-skills/profile-sync.md +90 -0
- package/src/browser_workspace_app/interaction-skills/screenshots.md +17 -0
- package/src/browser_workspace_app/interaction-skills/scrolling.md +22 -0
- package/src/browser_workspace_app/interaction-skills/shadow-dom.md +3 -0
- package/src/browser_workspace_app/interaction-skills/tabs.md +73 -0
- package/src/browser_workspace_app/interaction-skills/uploads.md +1 -0
- package/src/browser_workspace_app/interaction-skills/viewport.md +16 -0
- package/src/browser_workspace_app/licenses/browser-harness-MIT.txt +21 -0
- package/src/browser_workspace_app/managed_chrome.py +244 -0
- package/src/browser_workspace_app/node/admin.mjs +44 -0
- package/src/browser_workspace_app/package.json +36 -0
- package/src/browser_workspace_app/platform_runner.py +204 -0
- package/src/browser_workspace_app/platforms/chatgpt/README.md +60 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +58 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +119 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +80 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +85 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +33 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +353 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +52 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +129 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +591 -0
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +362 -0
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +34 -0
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +183 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +49 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +425 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +7 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +10 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +18 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +11 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +9 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +33 -0
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +83 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +32 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +210 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +21 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +25 -0
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +24 -0
- package/src/browser_workspace_app/platforms/tiktok/README.md +10 -0
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +180 -0
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +99 -0
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +10 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +163 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +130 -0
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +231 -0
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +124 -0
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +36 -0
- package/src/browser_workspace_app/platforms/xhs/README.md +10 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +355 -0
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +281 -0
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +107 -0
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +34 -0
- package/src/browser_workspace_app/platforms/youtube/README.md +10 -0
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +103 -0
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +94 -0
- package/src/browser_workspace_app/pyproject.toml +33 -0
- package/src/browser_workspace_app/session_client.py +107 -0
- package/src/browser_workspace_app/session_daemon.py +431 -0
- package/src/browser_harness/__init__.py +0 -2
- package/src/browser_harness/__pycache__/__init__.cpython-311.pyc +0 -0
- package/src/browser_harness/_ipc.py +0 -201
- package/src/browser_harness/admin.py +0 -1588
- package/src/browser_harness/auth.py +0 -546
- package/src/browser_harness/daemon.py +0 -894
- package/src/browser_harness/helpers.py +0 -668
- package/src/browser_harness/macos.py +0 -131
- package/src/browser_harness/mcp_cli.py +0 -16
- package/src/browser_harness/paths.py +0 -49
- package/src/browser_harness/recorder.py +0 -343
- package/src/browser_harness/run.py +0 -413
- package/src/browser_harness/telemetry.py +0 -308
- package/src/browser_harness/video-template.html +0 -970
- package/src/browser_harness/video.py +0 -749
- 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
|
+
});
|