openplot 1.0.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.
openplot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """OpenPlot: Visual plot debugger."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1 @@
1
+ """API helpers and schemas for OpenPlot."""
@@ -0,0 +1,132 @@
1
+ """Typed request payloads for the OpenPlot FastAPI server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Literal
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+ from ..models import FixRunner
10
+
11
+
12
+ class PlotModePathSuggestionsRequest(BaseModel):
13
+ selection_type: Literal["data", "script"] = "data"
14
+ query: str = ""
15
+ workspace_id: str | None = None
16
+
17
+
18
+ class PlotModeSelectPathsRequest(BaseModel):
19
+ selection_type: Literal["data", "script"]
20
+ paths: list[str]
21
+ workspace_id: str | None = None
22
+
23
+
24
+ class PlotModeChatRequest(BaseModel):
25
+ message: str = ""
26
+ workspace_id: str | None = None
27
+ runner: FixRunner | None = None
28
+ model: str | None = None
29
+ variant: str | None = None
30
+
31
+
32
+ class PlotModeSettingsRequest(BaseModel):
33
+ execution_mode: Literal["quick", "autonomous"]
34
+ workspace_id: str | None = None
35
+
36
+
37
+ class PlotModeQuestionAnswerItemRequest(BaseModel):
38
+ question_id: str
39
+ option_ids: list[str] = []
40
+ text: str | None = None
41
+
42
+
43
+ class PlotModeQuestionAnswerRequest(BaseModel):
44
+ question_set_id: str
45
+ answers: list[PlotModeQuestionAnswerItemRequest] = []
46
+ workspace_id: str | None = None
47
+ runner: FixRunner | None = None
48
+ model: str | None = None
49
+ variant: str | None = None
50
+
51
+
52
+ class PlotModeTabularHintRegionRequest(BaseModel):
53
+ sheet_id: str
54
+ row_start: int
55
+ row_end: int
56
+ col_start: int
57
+ col_end: int
58
+
59
+
60
+ class PlotModeTabularHintRequest(BaseModel):
61
+ selector_id: str
62
+ regions: list[PlotModeTabularHintRegionRequest] = []
63
+ workspace_id: str | None = None
64
+ sheet_id: str | None = None
65
+ row_start: int | None = None
66
+ row_end: int | None = None
67
+ col_start: int | None = None
68
+ col_end: int | None = None
69
+ note: str | None = None
70
+ runner: FixRunner | None = None
71
+ model: str | None = None
72
+ variant: str | None = None
73
+
74
+
75
+ class PlotModeFinalizeRequest(BaseModel):
76
+ model_config = ConfigDict(extra="allow")
77
+ workspace_id: str | None = None
78
+
79
+
80
+ class RenameSessionRequest(BaseModel):
81
+ workspace_name: str | None = None
82
+ name: str | None = None
83
+
84
+
85
+ class RenameBranchRequest(BaseModel):
86
+ name: str | None = None
87
+ branch_name: str | None = None
88
+
89
+
90
+ class PreferencesRequest(BaseModel):
91
+ fix_runner: FixRunner | None = None
92
+ fix_model: str | None = None
93
+ fix_variant: str | None = None
94
+
95
+
96
+ class PythonInterpreterRequest(BaseModel):
97
+ mode: Literal["builtin", "manual", "auto"] = "builtin"
98
+ path: str | None = None
99
+
100
+
101
+ class RunnerInstallRequest(BaseModel):
102
+ runner: FixRunner
103
+
104
+
105
+ class RunnerAuthLaunchRequest(BaseModel):
106
+ runner: FixRunner
107
+
108
+
109
+ class OpenExternalUrlRequest(BaseModel):
110
+ url: str
111
+
112
+
113
+ class StartFixJobRequest(BaseModel):
114
+ session_id: str | None = None
115
+ runner: FixRunner | None = None
116
+ model: str = ""
117
+ variant: str | None = None
118
+
119
+
120
+ class CheckoutVersionRequest(BaseModel):
121
+ version_id: str = ""
122
+ branch_id: str | None = None
123
+
124
+
125
+ class AnnotationUpdateRequest(BaseModel):
126
+ feedback: str | None = None
127
+ status: str | None = None
128
+
129
+
130
+ class SubmitScriptRequest(BaseModel):
131
+ code: str = ""
132
+ annotation_id: str | None = None
openplot/cli.py ADDED
@@ -0,0 +1,139 @@
1
+ """CLI entry point for OpenPlot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import webbrowser
7
+ from pathlib import Path
8
+
9
+ import click
10
+ import uvicorn
11
+
12
+ DEFAULT_SERVE_PORT = 17623
13
+
14
+
15
+ @click.group()
16
+ @click.version_option(package_name="openplot")
17
+ def main() -> None:
18
+ """The agentic plotting "IDE" built for everyone."""
19
+
20
+
21
+ @main.command()
22
+ @click.argument("file", required=False)
23
+ @click.option(
24
+ "--port",
25
+ "-p",
26
+ default=DEFAULT_SERVE_PORT,
27
+ help=(f"Port to serve on (default: {DEFAULT_SERVE_PORT}; use 0 to auto-pick)."),
28
+ )
29
+ @click.option("--host", "-h", default="127.0.0.1", help="Host to bind to.")
30
+ @click.option("--no-browser", is_flag=True, help="Don't auto-open the browser.")
31
+ def serve(file: str | None, port: int, host: str, no_browser: bool) -> None:
32
+ """Start the OpenPlot server for FILE or by restoring a workspace.
33
+
34
+ FILE can be a Python script (.py).
35
+ If FILE is omitted, OpenPlot restores the most recently updated workspace.
36
+ If no workspace exists, OpenPlot starts in plot mode.
37
+ """
38
+ from .server import (
39
+ create_app,
40
+ init_session_from_script,
41
+ set_workspace_dir,
42
+ write_port_file,
43
+ )
44
+
45
+ if file is not None:
46
+ file_path = Path(file).expanduser().resolve()
47
+ if not file_path.exists():
48
+ click.secho(f"Error: File not found: {file_path}", fg="red")
49
+ sys.exit(1)
50
+
51
+ ext = file_path.suffix.lower()
52
+ if ext != ".py":
53
+ click.secho(
54
+ f"Error: OpenPlot serve only accepts Python scripts (.py), got: {file_path.name}",
55
+ fg="red",
56
+ )
57
+ sys.exit(1)
58
+
59
+ click.echo(f"Executing {file_path.name} ...")
60
+ result = init_session_from_script(file_path)
61
+ if not result.success:
62
+ click.secho(f"Error: {result.error}", fg="red")
63
+ if result.stderr:
64
+ click.echo(result.stderr)
65
+ sys.exit(1)
66
+ click.echo(f"Detected output: {result.plot_path} ({result.plot_type})")
67
+ else:
68
+ set_workspace_dir(Path.cwd())
69
+ click.echo(
70
+ "Starting OpenPlot. The web UI will restore the most recently updated workspace or start a new plot workspace."
71
+ )
72
+
73
+ # Pick a free port if 0.
74
+ if port == 0:
75
+ import socket
76
+
77
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
78
+ s.bind(("", 0))
79
+ port = s.getsockname()[1]
80
+
81
+ write_port_file(port)
82
+
83
+ url = f"http://{host}:{port}"
84
+ click.echo(f"OpenPlot server running at {url}")
85
+
86
+ if not no_browser:
87
+ # Delay browser open slightly so the server is ready.
88
+ import threading
89
+
90
+ threading.Timer(1.0, lambda: webbrowser.open(url)).start()
91
+
92
+ app = create_app()
93
+ uvicorn.run(app, host=host, port=port, log_level="warning")
94
+
95
+
96
+ @main.command()
97
+ @click.argument("file", required=False)
98
+ @click.option(
99
+ "--port",
100
+ "-p",
101
+ default=DEFAULT_SERVE_PORT,
102
+ help=(f"Port to serve on (default: {DEFAULT_SERVE_PORT}; use 0 to auto-pick)."),
103
+ )
104
+ @click.option("--host", "-h", default="127.0.0.1", help="Host to bind to.")
105
+ def desktop(file: str | None, port: int, host: str) -> None:
106
+ """Launch OpenPlot in a native desktop window."""
107
+ from .desktop import launch_desktop
108
+
109
+ launch_desktop(file, host=host, port=port)
110
+
111
+
112
+ @main.command()
113
+ @click.option(
114
+ "--server-url",
115
+ default=None,
116
+ help=(
117
+ "OpenPlot backend URL to proxy (default: auto-discover from "
118
+ "OPENPLOT_SERVER_URL or ~/.openplot/port)."
119
+ ),
120
+ )
121
+ def mcp(server_url: str | None) -> None:
122
+ """Launch the MCP stdio server (for agent integration).
123
+
124
+ This connects to a running OpenPlot server to proxy tool calls.
125
+ """
126
+ from .mcp_server import BackendError, discover_server_url, run_mcp_stdio
127
+
128
+ try:
129
+ resolved_url = discover_server_url(server_url)
130
+ except BackendError as exc:
131
+ click.secho(f"Error: {exc}", fg="red", err=True)
132
+ sys.exit(1)
133
+
134
+ click.echo(f"Starting OpenPlot MCP server (backend: {resolved_url})", err=True)
135
+ run_mcp_stdio(resolved_url)
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
openplot/desktop.py ADDED
@@ -0,0 +1,437 @@
1
+ """Desktop launcher for OpenPlot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import importlib
7
+ import json
8
+ import os
9
+ import socket
10
+ import sys
11
+ import threading
12
+ import time
13
+ from contextlib import suppress
14
+ from pathlib import Path
15
+
16
+ import click
17
+ import uvicorn
18
+
19
+ DEFAULT_DESKTOP_PORT = 17623
20
+ _ALLOWED_SUFFIXES = {".py", ".svg", ".png", ".jpg", ".jpeg", ".pdf"}
21
+ _DESKTOP_FILE_DROP_EVENT = "openplot-desktop-file-drop"
22
+
23
+
24
+ def _strip_macos_process_serial_arg() -> None:
25
+ """Remove Finder's process serial argument before Click parses argv."""
26
+ if sys.platform != "darwin":
27
+ return
28
+ if len(sys.argv) < 2:
29
+ return
30
+ if sys.argv[1].startswith("-psn_"):
31
+ del sys.argv[1]
32
+
33
+
34
+ def _pick_free_port() -> int:
35
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
36
+ sock.bind(("", 0))
37
+ return int(sock.getsockname()[1])
38
+
39
+
40
+ def _configure_linux_qt_runtime() -> None:
41
+ if not sys.platform.startswith("linux"):
42
+ return
43
+
44
+ os.environ.setdefault("PYWEBVIEW_GUI", "qt")
45
+ os.environ.setdefault("QT_API", "pyqt6")
46
+ os.environ.setdefault("QT_QPA_PLATFORM", "xcb")
47
+ os.environ.setdefault("QT_OPENGL", "software")
48
+ os.environ.setdefault("QT_QUICK_BACKEND", "software")
49
+ os.environ.setdefault("LIBGL_ALWAYS_SOFTWARE", "1")
50
+ os.environ.setdefault("QTWEBENGINE_DISABLE_SANDBOX", "1")
51
+
52
+ chromium_default = (
53
+ "--disable-gpu --disable-gpu-compositing --disable-features=Vulkan --no-sandbox"
54
+ )
55
+ existing_chromium_flags = os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", "").strip()
56
+ if existing_chromium_flags:
57
+ os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = (
58
+ f"{existing_chromium_flags} {chromium_default}"
59
+ )
60
+ else:
61
+ os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = chromium_default
62
+
63
+ appdir = os.environ.get("APPDIR", "").strip()
64
+ if appdir:
65
+ bundled_xkb = Path(appdir) / "usr" / "share" / "X11" / "xkb"
66
+ if bundled_xkb.is_dir():
67
+ os.environ.setdefault("XKB_CONFIG_ROOT", str(bundled_xkb))
68
+
69
+
70
+ def _resolve_input_file(raw_file: str | None) -> Path | None:
71
+ if not raw_file:
72
+ return None
73
+
74
+ path = Path(raw_file).expanduser().resolve()
75
+
76
+ if not path.exists():
77
+ raise click.ClickException(f"File not found: {path}")
78
+
79
+ ext = path.suffix.lower()
80
+ if ext not in _ALLOWED_SUFFIXES:
81
+ supported = ", ".join(sorted(_ALLOWED_SUFFIXES))
82
+ raise click.ClickException(
83
+ f"Unsupported file type '{ext}'. Expected one of: {supported}"
84
+ )
85
+ return path
86
+
87
+
88
+ def _desktop_file_drop_script(paths: list[str]) -> str:
89
+ payload = json.dumps({"paths": paths})
90
+ return (
91
+ "window.dispatchEvent("
92
+ f"new CustomEvent({_DESKTOP_FILE_DROP_EVENT!r}, {{ detail: {payload} }})"
93
+ ");"
94
+ )
95
+
96
+
97
+ def _desktop_dropped_file_paths(event: object) -> list[str]:
98
+ if not isinstance(event, dict):
99
+ return []
100
+
101
+ data_transfer = event.get("dataTransfer")
102
+ if not isinstance(data_transfer, dict):
103
+ return []
104
+
105
+ raw_files = data_transfer.get("files")
106
+ if not isinstance(raw_files, list):
107
+ return []
108
+
109
+ paths: list[str] = []
110
+ seen: set[str] = set()
111
+ for item in raw_files:
112
+ if not isinstance(item, dict):
113
+ continue
114
+ raw_path = item.get("pywebviewFullPath")
115
+ if not isinstance(raw_path, str):
116
+ continue
117
+ normalized = raw_path.strip()
118
+ if not normalized or normalized in seen:
119
+ continue
120
+ seen.add(normalized)
121
+ paths.append(normalized)
122
+ return paths
123
+
124
+
125
+ def _bind_macos_file_drop_bridge(window: object) -> None:
126
+ if sys.platform != "darwin":
127
+ return
128
+
129
+ try:
130
+ from webview.dom import DOMEventHandler
131
+ except ImportError:
132
+ return
133
+
134
+ document = getattr(getattr(window, "dom", None), "document", None)
135
+ if document is None:
136
+ return
137
+
138
+ def _ignore_drag(_event: object) -> None:
139
+ return
140
+
141
+ def _on_drop(event: object) -> None:
142
+ paths = _desktop_dropped_file_paths(event)
143
+ if not paths:
144
+ return
145
+ evaluate_js = getattr(window, "evaluate_js", None)
146
+ if not callable(evaluate_js):
147
+ return
148
+ with suppress(Exception):
149
+ evaluate_js(_desktop_file_drop_script(paths))
150
+
151
+ document.events.dragenter += DOMEventHandler(_ignore_drag, True, True)
152
+ document.events.dragover += DOMEventHandler(_ignore_drag, True, True, debounce=500)
153
+ document.events.drop += DOMEventHandler(_on_drop, True, True)
154
+
155
+
156
+ def _restore_stdio_for_windowed_app() -> None:
157
+ """Restore sys.stdin/stdout/stderr from OS handles.
158
+
159
+ PyInstaller windowed apps (console=False) set these to None, but when a
160
+ parent process spawns us with pipes the OS-level handles are valid.
161
+ Re-wrapping them lets stdio-based transports (MCP, click.echo) work.
162
+ """
163
+ if sys.platform == "win32":
164
+ _restore_stdio_win32()
165
+ else:
166
+ _restore_stdio_posix()
167
+
168
+
169
+ def _restore_stdio_posix() -> None:
170
+ for fd, mode, attr in ((0, "r", "stdin"), (1, "w", "stdout"), (2, "w", "stderr")):
171
+ if getattr(sys, attr) is None:
172
+ try:
173
+ setattr(sys, attr, os.fdopen(fd, mode, closefd=False))
174
+ except OSError:
175
+ pass
176
+
177
+
178
+ def _restore_stdio_win32() -> None:
179
+ """Restore stdio on Windows using the Win32 standard handles.
180
+
181
+ In a GUI-subsystem process (console=False) the C-runtime file descriptors
182
+ 0/1/2 may not be mapped, but the Win32 standard handles that the parent
183
+ provided via STARTUPINFO *are* present. We use GetStdHandle -> msvcrt
184
+ open_osfhandle -> os.fdopen to reconnect them.
185
+ """
186
+ try:
187
+ import ctypes
188
+ import msvcrt
189
+ except ImportError:
190
+ return
191
+
192
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
193
+ INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
194
+
195
+ STD_HANDLES = (
196
+ (-10, 0, "r", "stdin"), # STD_INPUT_HANDLE
197
+ (-11, 1, "w", "stdout"), # STD_OUTPUT_HANDLE
198
+ (-12, 2, "w", "stderr"), # STD_ERROR_HANDLE
199
+ )
200
+
201
+ for std_id, fallback_fd, mode, attr in STD_HANDLES:
202
+ if getattr(sys, attr) is not None:
203
+ continue
204
+
205
+ handle = kernel32.GetStdHandle(std_id)
206
+ if handle in (0, INVALID_HANDLE_VALUE, None):
207
+ continue
208
+
209
+ try:
210
+ fd = msvcrt.open_osfhandle(handle, 0)
211
+ except OSError:
212
+ fd = fallback_fd
213
+
214
+ try:
215
+ setattr(sys, attr, os.fdopen(fd, mode, closefd=False))
216
+ except OSError:
217
+ pass
218
+
219
+
220
+ def _run_internal_script_execution(
221
+ *,
222
+ script_path: str,
223
+ work_dir: str | None,
224
+ capture_dir: str | None,
225
+ ) -> int:
226
+ from openplot.executor import execute_script_inline
227
+
228
+ _restore_stdio_for_windowed_app()
229
+
230
+ result = execute_script_inline(
231
+ script_path,
232
+ work_dir=work_dir,
233
+ capture_dir=capture_dir,
234
+ )
235
+ click.echo(
236
+ json.dumps(
237
+ {
238
+ "type": "openplot_internal_script_result",
239
+ "success": result.success,
240
+ "plot_path": result.plot_path,
241
+ "plot_type": result.plot_type,
242
+ "stdout": result.stdout,
243
+ "stderr": result.stderr,
244
+ "returncode": result.returncode,
245
+ "duration_s": result.duration_s,
246
+ "error": result.error,
247
+ }
248
+ )
249
+ )
250
+
251
+ if result.returncode != 0:
252
+ return result.returncode
253
+ return 0
254
+
255
+
256
+ def _run_internal_mcp_stdio(server_url: str | None) -> int:
257
+ from openplot.mcp_server import BackendError, discover_server_url, run_mcp_stdio
258
+
259
+ _restore_stdio_for_windowed_app()
260
+
261
+ try:
262
+ resolved_url = discover_server_url(server_url)
263
+ except BackendError as exc:
264
+ click.echo(f"Error: {exc}", err=True)
265
+ return 1
266
+
267
+ run_mcp_stdio(resolved_url)
268
+ return 0
269
+
270
+
271
+ def launch_desktop(
272
+ file: str | None,
273
+ *,
274
+ host: str = "127.0.0.1",
275
+ port: int = DEFAULT_DESKTOP_PORT,
276
+ ) -> None:
277
+ """Launch OpenPlot in a native webview window."""
278
+ file_path = _resolve_input_file(file)
279
+ if file_path is not None and file_path.suffix.lower() != ".py":
280
+ raise click.ClickException(
281
+ f"OpenPlot desktop only accepts Python scripts (.py), got: {file_path.name}"
282
+ )
283
+
284
+ _configure_linux_qt_runtime()
285
+
286
+ try:
287
+ webview = importlib.import_module("webview")
288
+ except ModuleNotFoundError as exc:
289
+ raise click.ClickException(
290
+ "Desktop mode requires 'pywebview'. Install with: uv sync --extra desktop"
291
+ ) from exc
292
+
293
+ from openplot.server import (
294
+ create_app,
295
+ init_session_from_script,
296
+ set_workspace_dir,
297
+ write_port_file,
298
+ )
299
+
300
+ if file_path is None:
301
+ set_workspace_dir(Path.home())
302
+ else:
303
+ set_workspace_dir(file_path.parent)
304
+
305
+ click.echo(f"Executing {file_path.name} ...")
306
+ result = init_session_from_script(file_path)
307
+ if not result.success:
308
+ details = result.error or "Failed to execute script"
309
+ if result.stderr:
310
+ details = f"{details}\n{result.stderr.strip()}"
311
+ raise click.ClickException(details)
312
+
313
+ if port == 0:
314
+ port = _pick_free_port()
315
+
316
+ write_port_file(port)
317
+ url = f"http://{host}:{port}"
318
+
319
+ app = create_app()
320
+ config = uvicorn.Config(app, host=host, port=port, log_level="warning")
321
+ uvicorn_server = uvicorn.Server(config)
322
+ server_thread = threading.Thread(
323
+ target=uvicorn_server.run,
324
+ name="openplot-uvicorn",
325
+ daemon=True,
326
+ )
327
+ server_thread.start()
328
+
329
+ started_deadline = time.monotonic() + 15.0
330
+ while not uvicorn_server.started and server_thread.is_alive():
331
+ if time.monotonic() >= started_deadline:
332
+ break
333
+ time.sleep(0.05)
334
+
335
+ shutdown_once = threading.Event()
336
+
337
+ def _shutdown_server() -> None:
338
+ if shutdown_once.is_set():
339
+ return
340
+ shutdown_once.set()
341
+ uvicorn_server.should_exit = True
342
+ server_thread.join(timeout=8.0)
343
+
344
+ if not uvicorn_server.started:
345
+ _shutdown_server()
346
+ raise click.ClickException("Failed to start local OpenPlot backend")
347
+
348
+ click.echo(f"OpenPlot desktop running at {url}")
349
+
350
+ atexit.register(_shutdown_server)
351
+
352
+ window = webview.create_window(
353
+ "OpenPlot",
354
+ url=url,
355
+ width=1440,
356
+ height=920,
357
+ min_size=(980, 700),
358
+ zoomable=True,
359
+ )
360
+ window.events.closed += _shutdown_server
361
+
362
+ try:
363
+ webview.start(_bind_macos_file_drop_bridge, window, debug=False)
364
+ finally:
365
+ _shutdown_server()
366
+ with suppress(Exception):
367
+ atexit.unregister(_shutdown_server)
368
+
369
+
370
+ @click.command()
371
+ @click.argument("file", required=False)
372
+ @click.option(
373
+ "--port",
374
+ "-p",
375
+ default=DEFAULT_DESKTOP_PORT,
376
+ help=(f"Port to serve on (default: {DEFAULT_DESKTOP_PORT}; use 0 to auto-pick)."),
377
+ )
378
+ @click.option("--host", "-h", default="127.0.0.1", help="Host to bind to.")
379
+ @click.option(
380
+ "--internal-execute-script",
381
+ "internal_execute_script",
382
+ type=click.Path(path_type=str),
383
+ hidden=True,
384
+ )
385
+ @click.option(
386
+ "--internal-work-dir",
387
+ "internal_work_dir",
388
+ type=click.Path(path_type=str),
389
+ hidden=True,
390
+ )
391
+ @click.option(
392
+ "--internal-capture-dir",
393
+ "internal_capture_dir",
394
+ type=click.Path(path_type=str),
395
+ hidden=True,
396
+ )
397
+ @click.option(
398
+ "--internal-run-mcp",
399
+ "internal_run_mcp",
400
+ is_flag=True,
401
+ hidden=True,
402
+ )
403
+ @click.option(
404
+ "--internal-mcp-server-url",
405
+ "internal_mcp_server_url",
406
+ type=str,
407
+ hidden=True,
408
+ )
409
+ def main(
410
+ file: str | None,
411
+ port: int,
412
+ host: str,
413
+ internal_execute_script: str | None,
414
+ internal_work_dir: str | None,
415
+ internal_capture_dir: str | None,
416
+ internal_run_mcp: bool,
417
+ internal_mcp_server_url: str | None,
418
+ ) -> None:
419
+ """Launch OpenPlot in a native desktop window."""
420
+ if internal_execute_script is not None:
421
+ raise SystemExit(
422
+ _run_internal_script_execution(
423
+ script_path=internal_execute_script,
424
+ work_dir=internal_work_dir,
425
+ capture_dir=internal_capture_dir,
426
+ )
427
+ )
428
+
429
+ if internal_run_mcp:
430
+ raise SystemExit(_run_internal_mcp_stdio(internal_mcp_server_url))
431
+
432
+ launch_desktop(file, host=host, port=port)
433
+
434
+
435
+ if __name__ == "__main__":
436
+ _strip_macos_process_serial_arg()
437
+ main()