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.
Files changed (40) hide show
  1. open_codex_ui-0.1.4.dist-info/METADATA +249 -0
  2. open_codex_ui-0.1.4.dist-info/RECORD +40 -0
  3. open_codex_ui-0.1.4.dist-info/WHEEL +5 -0
  4. open_codex_ui-0.1.4.dist-info/entry_points.txt +7 -0
  5. open_codex_ui-0.1.4.dist-info/top_level.txt +1 -0
  6. yier_web/__init__.py +6 -0
  7. yier_web/app.py +223 -0
  8. yier_web/auth.py +269 -0
  9. yier_web/cli.py +172 -0
  10. yier_web/codex/__init__.py +3 -0
  11. yier_web/codex/ipc_manager.py +1761 -0
  12. yier_web/codex/session_events.py +110 -0
  13. yier_web/codex/ws_commands.py +299 -0
  14. yier_web/config.py +651 -0
  15. yier_web/directory_picker.py +93 -0
  16. yier_web/event_stream.py +29 -0
  17. yier_web/frontend.py +204 -0
  18. yier_web/routes/__init__.py +17 -0
  19. yier_web/routes/codex.py +573 -0
  20. yier_web/routes/core.py +281 -0
  21. yier_web/schemas.py +534 -0
  22. yier_web/static/assets/CodexEmbedView-CN_-Mhe2.js +1 -0
  23. yier_web/static/assets/CodexView-wpI61iXa.js +606 -0
  24. yier_web/static/assets/LoginView-CELCom2O.js +101 -0
  25. yier_web/static/assets/api-CeihACIV.js +1099 -0
  26. yier_web/static/assets/index-BdFqJ-Kl.css +1 -0
  27. yier_web/static/assets/index-CjVNk6ja.js +183 -0
  28. yier_web/static/assets/index-mSBvq1p8.js +79 -0
  29. yier_web/static/assets/open-codex-ui-icon-DKJ1ZKj4.svg +99 -0
  30. yier_web/static/assets/primeicons-C6QP2o4f.woff2 +0 -0
  31. yier_web/static/assets/primeicons-DMOk5skT.eot +0 -0
  32. yier_web/static/assets/primeicons-Dr5RGzOO.svg +345 -0
  33. yier_web/static/assets/primeicons-MpK4pl85.ttf +0 -0
  34. yier_web/static/assets/primeicons-WjwUDZjB.woff +0 -0
  35. yier_web/static/assets/useCodexWorkspace-6QenDzvb.css +1 -0
  36. yier_web/static/assets/useCodexWorkspace-D1rCOO1x.js +1128 -0
  37. yier_web/static/brand/open-codex-ui-logo.svg +85 -0
  38. yier_web/static/favicon.ico +0 -0
  39. yier_web/static/favicon.svg +99 -0
  40. yier_web/static/index.html +24 -0
@@ -0,0 +1,281 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from typing import Any, AsyncIterator
6
+
7
+ from litestar import Controller, Request, get, post, put
8
+ from litestar.datastructures import State
9
+ from litestar.exceptions import HTTPException
10
+ from litestar.response import Response, ServerSentEvent
11
+ from litestar.response.sse import ServerSentEventMessage
12
+ from litestar.status_codes import HTTP_400_BAD_REQUEST
13
+
14
+ from yier_web.config import MCPValidationError
15
+ from yier_web.event_stream import EventStreamItem
16
+ from yier_web.schemas import (
17
+ AuthLoginRequest,
18
+ AuthSessionResponse,
19
+ HealthResponse,
20
+ LLMHealth,
21
+ MCPConfigResponse,
22
+ MCPHealth,
23
+ SaveAllowedRootsRequest,
24
+ SaveAppSettingsRequest,
25
+ SaveLLMRequest,
26
+ SaveMCPConfigRequest,
27
+ SelectDirectoryRequest,
28
+ SelectDirectoryResponse,
29
+ )
30
+
31
+ EVENT_STREAM_PING_INTERVAL_SECONDS = 15.0
32
+
33
+
34
+ def _service(state: State, name: str) -> Any:
35
+ return getattr(state, name)
36
+
37
+
38
+ def _shutdown_event(state: State) -> asyncio.Event | None:
39
+ shutdown_event = getattr(state, "shutdown_event", None)
40
+ if isinstance(shutdown_event, asyncio.Event):
41
+ return shutdown_event
42
+ return None
43
+
44
+
45
+ async def _wait_for_event_stream_item(
46
+ subscriber: asyncio.Queue[EventStreamItem],
47
+ shutdown_event: asyncio.Event | None,
48
+ timeout: float,
49
+ ) -> tuple[EventStreamItem | None, bool]:
50
+ subscriber_task = asyncio.create_task(subscriber.get())
51
+ shutdown_task: asyncio.Task[bool] | None = None
52
+ wait_tasks: list[asyncio.Task[Any]] = [subscriber_task]
53
+
54
+ if shutdown_event is not None:
55
+ shutdown_task = asyncio.create_task(shutdown_event.wait())
56
+ wait_tasks.append(shutdown_task)
57
+
58
+ try:
59
+ done, pending = await asyncio.wait(
60
+ wait_tasks,
61
+ timeout=timeout,
62
+ return_when=asyncio.FIRST_COMPLETED,
63
+ )
64
+ for task in pending:
65
+ task.cancel()
66
+ for task in pending:
67
+ try:
68
+ await task
69
+ except asyncio.CancelledError:
70
+ pass
71
+
72
+ if not done:
73
+ return None, False
74
+
75
+ if shutdown_task is not None and shutdown_task in done:
76
+ return None, True
77
+
78
+ return subscriber_task.result(), False
79
+ finally:
80
+ if not subscriber_task.done():
81
+ subscriber_task.cancel()
82
+ try:
83
+ await subscriber_task
84
+ except asyncio.CancelledError:
85
+ pass
86
+ if shutdown_task is not None and not shutdown_task.done():
87
+ shutdown_task.cancel()
88
+ try:
89
+ await shutdown_task
90
+ except asyncio.CancelledError:
91
+ pass
92
+
93
+
94
+ wait_for_event_stream_item = _wait_for_event_stream_item
95
+
96
+
97
+ class AuthController(Controller):
98
+ path = "/auth"
99
+
100
+ @get("/session")
101
+ async def get_auth_session(
102
+ self,
103
+ request: Request[Any, Any, Any],
104
+ state: State,
105
+ ) -> AuthSessionResponse:
106
+ payload = _service(state, "auth_service").session_payload(request)
107
+ return AuthSessionResponse(**payload)
108
+
109
+ @post("/login")
110
+ async def login(
111
+ self,
112
+ data: AuthLoginRequest,
113
+ request: Request[Any, Any, Any],
114
+ state: State,
115
+ ) -> Response:
116
+ auth_service = _service(state, "auth_service")
117
+ if not auth_service.verify_login_password(data.password):
118
+ raise HTTPException(status_code=401, detail="Invalid password.")
119
+ return auth_service.build_login_response(request)
120
+
121
+ @post("/logout")
122
+ async def logout(self, request: Request[Any, Any, Any], state: State) -> Response:
123
+ return _service(state, "auth_service").build_logout_response(request)
124
+
125
+
126
+ class HealthController(Controller):
127
+ @get("/health")
128
+ async def health(self, state: State) -> HealthResponse:
129
+ config_service = _service(state, "config_service")
130
+ frontend_service = _service(state, "frontend_service")
131
+ frontend = await frontend_service.get_status()
132
+ settings = config_service.load_web_settings()
133
+ runtime: dict[str, Any] = {}
134
+ failing_servers = [
135
+ name
136
+ for name, entry in runtime.items()
137
+ if entry.status in {"failed", "needs_auth", "needs_client_registration"}
138
+ ]
139
+ mcp_ready = not failing_servers
140
+ return HealthResponse(
141
+ frontend=frontend,
142
+ llm=LLMHealth(
143
+ ready=True,
144
+ detail=None,
145
+ ),
146
+ mcp=MCPHealth(
147
+ ready=mcp_ready,
148
+ detail=None
149
+ if mcp_ready
150
+ else f"Problematic MCP servers: {', '.join(failing_servers)}",
151
+ runtime=runtime,
152
+ ),
153
+ backends=config_service.build_backend_health(),
154
+ allowed_roots=settings.allowed_roots,
155
+ )
156
+
157
+
158
+ class ConfigController(Controller):
159
+ path = "/config"
160
+
161
+ @get()
162
+ async def get_config(self, state: State) -> Response:
163
+ config_service = _service(state, "config_service")
164
+ runtime: dict[str, Any] = {}
165
+ return Response(content=config_service.build_public_config(runtime).model_dump())
166
+
167
+ @put("/llm")
168
+ async def save_llm_config(self, data: SaveLLMRequest, state: State) -> Response:
169
+ config_service = _service(state, "config_service")
170
+ config_service.save_llm_settings(data)
171
+ runtime: dict[str, Any] = {}
172
+ return Response(content=config_service.build_public_config(runtime).model_dump())
173
+
174
+ @put("/roots")
175
+ async def save_allowed_roots_config(
176
+ self,
177
+ data: SaveAllowedRootsRequest,
178
+ state: State,
179
+ ) -> Response:
180
+ config_service = _service(state, "config_service")
181
+ config_service.save_allowed_roots(data.allowed_roots)
182
+ runtime: dict[str, Any] = {}
183
+ return Response(content=config_service.build_public_config(runtime).model_dump())
184
+
185
+ @put("/app")
186
+ async def save_app_config(self, data: SaveAppSettingsRequest, state: State) -> Response:
187
+ config_service = _service(state, "config_service")
188
+ config_service.save_app_settings(data)
189
+ runtime: dict[str, Any] = {}
190
+ return Response(content=config_service.build_public_config(runtime).model_dump())
191
+
192
+ @get("/mcp")
193
+ async def get_mcp_config(self, state: State) -> MCPConfigResponse:
194
+ config_service = _service(state, "config_service")
195
+ return MCPConfigResponse(
196
+ mcp_servers=config_service.load_mcp_servers(),
197
+ runtime={},
198
+ )
199
+
200
+ @put("/mcp")
201
+ async def save_mcp_config(
202
+ self,
203
+ data: SaveMCPConfigRequest,
204
+ state: State,
205
+ ) -> MCPConfigResponse:
206
+ config_service = _service(state, "config_service")
207
+ try:
208
+ config_service.save_mcp_servers(data.mcp_servers)
209
+ except MCPValidationError as exc:
210
+ raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
211
+ return MCPConfigResponse(
212
+ mcp_servers=config_service.load_mcp_servers(),
213
+ runtime={},
214
+ )
215
+
216
+ @post("/mcp/reload")
217
+ async def reload_mcp_config(self, state: State) -> MCPConfigResponse:
218
+ config_service = _service(state, "config_service")
219
+ return MCPConfigResponse(
220
+ mcp_servers=config_service.load_mcp_servers(),
221
+ runtime={},
222
+ )
223
+
224
+
225
+ class SystemController(Controller):
226
+ path = "/system"
227
+
228
+ @post("/select-directory")
229
+ async def select_directory(
230
+ self,
231
+ data: SelectDirectoryRequest,
232
+ state: State,
233
+ ) -> SelectDirectoryResponse:
234
+ selected_path = await asyncio.to_thread(
235
+ _service(state, "directory_picker_service").select_directory,
236
+ data.initial_path,
237
+ )
238
+ return SelectDirectoryResponse(
239
+ selected=bool(selected_path),
240
+ project_path=selected_path or "",
241
+ )
242
+
243
+
244
+ class EventsController(Controller):
245
+ path = "/events"
246
+
247
+ @get("/stream")
248
+ async def stream_events(self, state: State) -> ServerSentEvent:
249
+ event_broker = _service(state, "event_broker")
250
+ shutdown_event = _shutdown_event(state)
251
+
252
+ async def event_stream() -> AsyncIterator[ServerSentEventMessage]:
253
+ subscriber = event_broker.subscribe()
254
+ try:
255
+ yield ServerSentEventMessage(
256
+ event="connected",
257
+ data=json.dumps({"status": "ok"}, ensure_ascii=False),
258
+ )
259
+ while True:
260
+ item, shutting_down = await _wait_for_event_stream_item(
261
+ subscriber,
262
+ shutdown_event,
263
+ timeout=EVENT_STREAM_PING_INTERVAL_SECONDS,
264
+ )
265
+ if shutting_down:
266
+ break
267
+ if item is None:
268
+ yield ServerSentEventMessage(
269
+ event="ping",
270
+ data=json.dumps({"status": "alive"}, ensure_ascii=False),
271
+ )
272
+ continue
273
+
274
+ yield ServerSentEventMessage(
275
+ event=item.event,
276
+ data=json.dumps(item.data, ensure_ascii=False),
277
+ )
278
+ finally:
279
+ event_broker.unsubscribe(subscriber)
280
+
281
+ return ServerSentEvent(event_stream())