open-codex-ui 0.1.4__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.
- open_codex_ui-0.1.4.dist-info/METADATA +249 -0
- open_codex_ui-0.1.4.dist-info/RECORD +40 -0
- open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
- open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
- open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
- yier_web/__init__.py +6 -0
- yier_web/app.py +223 -0
- yier_web/auth.py +269 -0
- yier_web/cli.py +172 -0
- yier_web/codex/__init__.py +3 -0
- yier_web/codex/ipc_manager.py +1761 -0
- yier_web/codex/session_events.py +110 -0
- yier_web/codex/ws_commands.py +299 -0
- yier_web/config.py +651 -0
- yier_web/directory_picker.py +93 -0
- yier_web/event_stream.py +29 -0
- yier_web/frontend.py +204 -0
- yier_web/routes/__init__.py +17 -0
- yier_web/routes/codex.py +573 -0
- yier_web/routes/core.py +281 -0
- yier_web/schemas.py +534 -0
- yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
- yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
- yier_web/static/assets/LoginView-CELCom2O.js +101 -0
- yier_web/static/assets/api-CeihACIV.js +1099 -0
- yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
- yier_web/static/assets/index-CjVNk6ja.js +183 -0
- yier_web/static/assets/index-mSBvq1p8.js +79 -0
- yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
- yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
- yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
- yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
- yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
- yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
- yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
- yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
- yier_web/static/brand/open-codex-ui-logo.svg +85 -0
- yier_web/static/favicon.ico +0 -0
- yier_web/static/favicon.svg +99 -0
- yier_web/static/index.html +24 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LocalDirectoryPickerService:
|
|
9
|
+
def select_directory(self, initial_path: str | None = None) -> str | None:
|
|
10
|
+
normalized_initial_path = self._normalize_initial_path(initial_path)
|
|
11
|
+
|
|
12
|
+
if sys.platform == "darwin":
|
|
13
|
+
handled, selected_path = self._select_directory_with_osascript(
|
|
14
|
+
normalized_initial_path
|
|
15
|
+
)
|
|
16
|
+
if handled:
|
|
17
|
+
return selected_path
|
|
18
|
+
|
|
19
|
+
return self._select_directory_with_tk(normalized_initial_path)
|
|
20
|
+
|
|
21
|
+
def _normalize_initial_path(self, initial_path: str | None) -> Path | None:
|
|
22
|
+
if not isinstance(initial_path, str) or not initial_path.strip():
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
candidate = Path(initial_path).expanduser()
|
|
26
|
+
if candidate.is_dir():
|
|
27
|
+
return candidate.resolve()
|
|
28
|
+
if candidate.exists():
|
|
29
|
+
return candidate.parent.resolve()
|
|
30
|
+
if candidate.parent.exists():
|
|
31
|
+
return candidate.parent.resolve()
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def _select_directory_with_osascript(
|
|
35
|
+
self, initial_path: Path | None
|
|
36
|
+
) -> tuple[bool, str | None]:
|
|
37
|
+
script = [
|
|
38
|
+
'set chosenFolder to choose folder with prompt "Select a project folder"'
|
|
39
|
+
]
|
|
40
|
+
if initial_path is not None:
|
|
41
|
+
escaped_path = self._escape_applescript_text(str(initial_path))
|
|
42
|
+
script = [
|
|
43
|
+
f'set chosenFolder to choose folder with prompt "Select a project folder" default location POSIX file "{escaped_path}"'
|
|
44
|
+
]
|
|
45
|
+
script.append("POSIX path of chosenFolder")
|
|
46
|
+
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
["osascript", *sum([["-e", line] for line in script], [])],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
check=False,
|
|
52
|
+
)
|
|
53
|
+
if result.returncode == 0:
|
|
54
|
+
selected_path = result.stdout.strip()
|
|
55
|
+
if not selected_path:
|
|
56
|
+
return True, None
|
|
57
|
+
return True, str(Path(selected_path).expanduser().resolve())
|
|
58
|
+
|
|
59
|
+
stderr = result.stderr.strip()
|
|
60
|
+
if "User canceled" in stderr or "(-128)" in stderr:
|
|
61
|
+
return True, None
|
|
62
|
+
return False, None
|
|
63
|
+
|
|
64
|
+
def _select_directory_with_tk(self, initial_path: Path | None) -> str | None:
|
|
65
|
+
try:
|
|
66
|
+
import tkinter as tk
|
|
67
|
+
from tkinter import filedialog
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
root = tk.Tk()
|
|
72
|
+
root.withdraw()
|
|
73
|
+
try:
|
|
74
|
+
root.attributes("-topmost", True)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
selected_path = filedialog.askdirectory(
|
|
80
|
+
initialdir=str(initial_path) if initial_path is not None else None,
|
|
81
|
+
mustexist=True,
|
|
82
|
+
title="Select a project folder",
|
|
83
|
+
parent=root,
|
|
84
|
+
)
|
|
85
|
+
finally:
|
|
86
|
+
root.destroy()
|
|
87
|
+
|
|
88
|
+
if not selected_path:
|
|
89
|
+
return None
|
|
90
|
+
return str(Path(selected_path).expanduser().resolve())
|
|
91
|
+
|
|
92
|
+
def _escape_applescript_text(self, value: str) -> str:
|
|
93
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
yier_web/event_stream.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class EventStreamItem:
|
|
10
|
+
event: str
|
|
11
|
+
data: dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EventStreamBroker:
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._subscribers: set[asyncio.Queue[EventStreamItem]] = set()
|
|
17
|
+
|
|
18
|
+
def subscribe(self) -> asyncio.Queue[EventStreamItem]:
|
|
19
|
+
queue: asyncio.Queue[EventStreamItem] = asyncio.Queue()
|
|
20
|
+
self._subscribers.add(queue)
|
|
21
|
+
return queue
|
|
22
|
+
|
|
23
|
+
def unsubscribe(self, queue: asyncio.Queue[EventStreamItem]) -> None:
|
|
24
|
+
self._subscribers.discard(queue)
|
|
25
|
+
|
|
26
|
+
async def publish(self, event: str, data: dict[str, Any]) -> None:
|
|
27
|
+
item = EventStreamItem(event=event, data=data)
|
|
28
|
+
for subscriber in list(self._subscribers):
|
|
29
|
+
await subscriber.put(item)
|
yier_web/frontend.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import mimetypes
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from litestar.connection import WebSocket
|
|
9
|
+
from litestar import Request
|
|
10
|
+
from litestar.response import Response
|
|
11
|
+
|
|
12
|
+
from yier_web.schemas import FrontendHealth
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FrontendService:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
dist_root: Path | None = None,
|
|
19
|
+
vite_origin: str = "http://127.0.0.1:5173",
|
|
20
|
+
debug: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.dist_root = (
|
|
23
|
+
dist_root or Path(__file__).resolve().parent / "static"
|
|
24
|
+
).resolve()
|
|
25
|
+
self.vite_origin = vite_origin.rstrip("/")
|
|
26
|
+
self.debug = debug
|
|
27
|
+
|
|
28
|
+
async def get_status(self) -> FrontendHealth:
|
|
29
|
+
if await self._should_proxy_to_vite():
|
|
30
|
+
return FrontendHealth(
|
|
31
|
+
ready=True, mode="proxy", detail=f"Proxying {self.vite_origin}"
|
|
32
|
+
)
|
|
33
|
+
if (self.dist_root / "index.html").exists():
|
|
34
|
+
return FrontendHealth(
|
|
35
|
+
ready=True, mode="static", detail=f"Serving {self.dist_root}"
|
|
36
|
+
)
|
|
37
|
+
return FrontendHealth(
|
|
38
|
+
ready=False,
|
|
39
|
+
mode="missing",
|
|
40
|
+
detail="Start Vite dev server or build the frontend bundle first.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def handle_request(
|
|
44
|
+
self, request: Request[Any, Any, Any], path: str
|
|
45
|
+
) -> Response:
|
|
46
|
+
if await self._should_proxy_to_vite():
|
|
47
|
+
return await self._proxy_request(request)
|
|
48
|
+
|
|
49
|
+
resolved_path = self._resolve_dist_path(path)
|
|
50
|
+
if resolved_path is not None and resolved_path.exists():
|
|
51
|
+
return self._build_static_response(resolved_path)
|
|
52
|
+
|
|
53
|
+
index_path = self.dist_root / "index.html"
|
|
54
|
+
if index_path.exists():
|
|
55
|
+
return self._build_static_response(index_path)
|
|
56
|
+
|
|
57
|
+
return Response(
|
|
58
|
+
content="Frontend is unavailable. Start `pnpm dev` in `web` or build the frontend.",
|
|
59
|
+
media_type="text/plain",
|
|
60
|
+
status_code=503,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def handle_websocket(self, socket: WebSocket, _path: str) -> None:
|
|
64
|
+
await socket.accept()
|
|
65
|
+
if await self._should_proxy_to_vite():
|
|
66
|
+
await socket.close(
|
|
67
|
+
code=1013,
|
|
68
|
+
reason="Frontend dev WebSocket is served by Vite on port 5173.",
|
|
69
|
+
)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
await socket.close(
|
|
73
|
+
code=1008,
|
|
74
|
+
reason="Frontend WebSocket is unavailable for the static bundle.",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def _should_proxy_to_vite(self) -> bool:
|
|
78
|
+
if not self.debug:
|
|
79
|
+
return False
|
|
80
|
+
return await self._vite_available()
|
|
81
|
+
|
|
82
|
+
async def _proxy_request(self, request: Request[Any, Any, Any]) -> Response:
|
|
83
|
+
request_headers = {
|
|
84
|
+
key: value
|
|
85
|
+
for key, value in request.headers.items()
|
|
86
|
+
if key.lower() not in {"host", "connection", "content-length"}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async with self._create_vite_client(
|
|
90
|
+
timeout=10.0, follow_redirects=False
|
|
91
|
+
) as client:
|
|
92
|
+
upstream = await self._proxy_to_vite(
|
|
93
|
+
client,
|
|
94
|
+
request=request,
|
|
95
|
+
headers=request_headers,
|
|
96
|
+
path=request.url.path,
|
|
97
|
+
query=request.url.query,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if self._should_fallback_to_vite_index(request, upstream.status_code):
|
|
101
|
+
upstream = await self._proxy_to_vite(
|
|
102
|
+
client,
|
|
103
|
+
request=request,
|
|
104
|
+
headers=request_headers,
|
|
105
|
+
path="/",
|
|
106
|
+
query="",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
response_headers = {
|
|
110
|
+
key: value
|
|
111
|
+
for key, value in upstream.headers.items()
|
|
112
|
+
if key.lower() not in {"connection", "content-length", "transfer-encoding"}
|
|
113
|
+
}
|
|
114
|
+
media_type = upstream.headers.get("content-type")
|
|
115
|
+
return Response(
|
|
116
|
+
content=upstream.content,
|
|
117
|
+
status_code=upstream.status_code,
|
|
118
|
+
media_type=media_type,
|
|
119
|
+
headers=response_headers,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def _proxy_to_vite(
|
|
123
|
+
self,
|
|
124
|
+
client: httpx.AsyncClient,
|
|
125
|
+
*,
|
|
126
|
+
request: Request[Any, Any, Any],
|
|
127
|
+
headers: dict[str, str],
|
|
128
|
+
path: str,
|
|
129
|
+
query: str,
|
|
130
|
+
) -> httpx.Response:
|
|
131
|
+
target_url = f"{self.vite_origin}{path}"
|
|
132
|
+
if query:
|
|
133
|
+
target_url = f"{target_url}?{query}"
|
|
134
|
+
return await client.request(
|
|
135
|
+
request.method,
|
|
136
|
+
target_url,
|
|
137
|
+
headers=headers,
|
|
138
|
+
content=await request.body(),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _should_fallback_to_vite_index(
|
|
142
|
+
self,
|
|
143
|
+
request: Request[Any, Any, Any],
|
|
144
|
+
upstream_status_code: int,
|
|
145
|
+
) -> bool:
|
|
146
|
+
if upstream_status_code != 404:
|
|
147
|
+
return False
|
|
148
|
+
if request.method.upper() not in {"GET", "HEAD"}:
|
|
149
|
+
return False
|
|
150
|
+
if Path(request.url.path).suffix:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
accept = request.headers.get("accept", "").lower()
|
|
154
|
+
return "text/html" in accept or accept in {"", "*/*"}
|
|
155
|
+
|
|
156
|
+
def _resolve_dist_path(self, path: str) -> Path | None:
|
|
157
|
+
normalized_path = path.lstrip("/")
|
|
158
|
+
|
|
159
|
+
if not normalized_path:
|
|
160
|
+
candidate = self.dist_root / "index.html"
|
|
161
|
+
return candidate if candidate.exists() else None
|
|
162
|
+
|
|
163
|
+
candidate = (self.dist_root / normalized_path).resolve()
|
|
164
|
+
try:
|
|
165
|
+
candidate.relative_to(self.dist_root.resolve())
|
|
166
|
+
except ValueError:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
if candidate.exists() and candidate.is_file():
|
|
170
|
+
return candidate
|
|
171
|
+
if Path(normalized_path).suffix:
|
|
172
|
+
return None
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
async def _vite_available(self) -> bool:
|
|
176
|
+
try:
|
|
177
|
+
async with self._create_vite_client(timeout=0.35) as client:
|
|
178
|
+
response = await client.get(self.vite_origin)
|
|
179
|
+
except httpx.HTTPError:
|
|
180
|
+
return False
|
|
181
|
+
return response.status_code < 500
|
|
182
|
+
|
|
183
|
+
def _create_vite_client(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
timeout: float,
|
|
187
|
+
follow_redirects: bool = False,
|
|
188
|
+
) -> httpx.AsyncClient:
|
|
189
|
+
return httpx.AsyncClient(
|
|
190
|
+
follow_redirects=follow_redirects,
|
|
191
|
+
timeout=timeout,
|
|
192
|
+
trust_env=False,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _build_static_response(self, path: Path) -> Response:
|
|
196
|
+
media_type, encoding = mimetypes.guess_type(path.name)
|
|
197
|
+
headers: dict[str, str] = {}
|
|
198
|
+
if encoding:
|
|
199
|
+
headers["content-encoding"] = encoding
|
|
200
|
+
return Response(
|
|
201
|
+
content=path.read_bytes(),
|
|
202
|
+
media_type=media_type or "application/octet-stream",
|
|
203
|
+
headers=headers or None,
|
|
204
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from yier_web.routes.codex import CodexController
|
|
2
|
+
from yier_web.routes.core import (
|
|
3
|
+
AuthController,
|
|
4
|
+
ConfigController,
|
|
5
|
+
EventsController,
|
|
6
|
+
HealthController,
|
|
7
|
+
SystemController,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AuthController",
|
|
12
|
+
"CodexController",
|
|
13
|
+
"ConfigController",
|
|
14
|
+
"EventsController",
|
|
15
|
+
"HealthController",
|
|
16
|
+
"SystemController",
|
|
17
|
+
]
|