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,110 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable
5
+ import inspect
6
+ import logging
7
+ from typing import Any
8
+
9
+ JsonDict = dict[str, Any]
10
+ CodexSessionEvent = JsonDict
11
+ CodexSessionEventQueue = asyncio.Queue[CodexSessionEvent]
12
+ CodexSessionEventSink = Callable[[CodexSessionEvent], Awaitable[None] | None]
13
+ Unsubscribe = Callable[[], None]
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class CodexSessionEventHub:
19
+ """Fan out Codex session events to thread subscribers and channel sinks."""
20
+
21
+ def __init__(self) -> None:
22
+ self._thread_subscribers: dict[str, set[CodexSessionEventQueue]] = {}
23
+ self._sinks: set[CodexSessionEventSink] = set()
24
+
25
+ def subscribe_thread(
26
+ self,
27
+ thread_id: str,
28
+ queue: CodexSessionEventQueue,
29
+ ) -> bool:
30
+ subscribers = self._thread_subscribers.setdefault(thread_id, set())
31
+ was_empty = not subscribers
32
+ subscribers.add(queue)
33
+ return was_empty
34
+
35
+ def unsubscribe_thread(
36
+ self,
37
+ thread_id: str,
38
+ queue: CodexSessionEventQueue,
39
+ ) -> bool:
40
+ subscribers = self._thread_subscribers.get(thread_id)
41
+ if subscribers is None or queue not in subscribers:
42
+ return False
43
+ subscribers.discard(queue)
44
+ if not subscribers:
45
+ self._thread_subscribers.pop(thread_id, None)
46
+ return True
47
+ return False
48
+
49
+ def clear_thread(self, thread_id: str) -> None:
50
+ self._thread_subscribers.pop(thread_id, None)
51
+
52
+ def clear_thread_subscribers(self) -> None:
53
+ self._thread_subscribers.clear()
54
+
55
+ def clear(self) -> None:
56
+ self._thread_subscribers.clear()
57
+ self._sinks.clear()
58
+
59
+ def add_sink(self, sink: CodexSessionEventSink) -> Unsubscribe:
60
+ self._sinks.add(sink)
61
+
62
+ def unsubscribe() -> None:
63
+ self._sinks.discard(sink)
64
+
65
+ return unsubscribe
66
+
67
+ async def publish_thread_event(
68
+ self,
69
+ thread_id: str,
70
+ event: CodexSessionEvent,
71
+ ) -> None:
72
+ for queue in list(self._thread_subscribers.get(thread_id, set())):
73
+ queue.put_nowait(event)
74
+ for sink in list(self._sinks):
75
+ await self._publish_to_sink(sink, event)
76
+
77
+ async def publish_to_thread_subscribers(
78
+ self,
79
+ thread_id: str,
80
+ event: CodexSessionEvent,
81
+ ) -> None:
82
+ for queue in list(self._thread_subscribers.get(thread_id, set())):
83
+ queue.put_nowait(event)
84
+
85
+ async def publish_to_all_thread_subscribers(
86
+ self,
87
+ event: CodexSessionEvent,
88
+ ) -> None:
89
+ for subscribers in list(self._thread_subscribers.values()):
90
+ for queue in list(subscribers):
91
+ queue.put_nowait(event)
92
+
93
+ async def publish_global_event(self, event: CodexSessionEvent) -> None:
94
+ for subscribers in list(self._thread_subscribers.values()):
95
+ for queue in list(subscribers):
96
+ queue.put_nowait(event)
97
+ for sink in list(self._sinks):
98
+ await self._publish_to_sink(sink, event)
99
+
100
+ async def _publish_to_sink(
101
+ self,
102
+ sink: CodexSessionEventSink,
103
+ event: CodexSessionEvent,
104
+ ) -> None:
105
+ try:
106
+ result = sink(event)
107
+ if inspect.isawaitable(result):
108
+ await result
109
+ except Exception as exc:
110
+ logger.warning("Codex session event sink failed: %s", exc)
@@ -0,0 +1,299 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from yier_web.codex.ipc_manager import CodexIpcManager, CodexSubscriberQueue
7
+
8
+
9
+ def _payload_text(payload: dict[str, Any], key: str) -> str:
10
+ value = payload.get(key)
11
+ return value.strip() if isinstance(value, str) else ""
12
+
13
+
14
+ def _required_payload_text(payload: dict[str, Any], key: str) -> str:
15
+ value = _payload_text(payload, key)
16
+ if not value:
17
+ raise ValueError(f"{key} is required.")
18
+ return value
19
+
20
+
21
+ def _payload_dict(payload: dict[str, Any], key: str) -> dict[str, Any] | None:
22
+ value = payload.get(key)
23
+ return dict(value) if isinstance(value, dict) else None
24
+
25
+
26
+ def _payload_dict_list(payload: dict[str, Any], key: str) -> list[dict[str, Any]]:
27
+ value = payload.get(key)
28
+ if not isinstance(value, list):
29
+ return []
30
+ return [dict(item) for item in value if isinstance(item, dict)]
31
+
32
+
33
+ def _payload_int(payload: dict[str, Any], key: str) -> int | None:
34
+ value = payload.get(key)
35
+ if isinstance(value, bool):
36
+ return None
37
+ return value if isinstance(value, int) else None
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class CodexWsCommandContext:
42
+ manager: CodexIpcManager
43
+ payload: dict[str, Any]
44
+ outbox: CodexSubscriberQueue
45
+ subscribed_thread_ids: set[str]
46
+
47
+
48
+ class CodexWsCommandStrategy:
49
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
50
+ raise NotImplementedError
51
+
52
+
53
+ async def _publish_workspace(
54
+ manager: CodexIpcManager,
55
+ outbox: CodexSubscriberQueue,
56
+ ) -> dict[str, Any]:
57
+ workspace = await manager.workspace()
58
+ payload = workspace.model_dump(mode="json")
59
+ await outbox.put(
60
+ {
61
+ "type": "workspace",
62
+ "payload": payload,
63
+ }
64
+ )
65
+ return payload
66
+
67
+
68
+ class ListThreadsCommandStrategy(CodexWsCommandStrategy):
69
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
70
+ return await _publish_workspace(context.manager, context.outbox)
71
+
72
+
73
+ class StartThreadCommandStrategy(CodexWsCommandStrategy):
74
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
75
+ result = await context.manager.start_thread(
76
+ project_path=_payload_text(context.payload, "project_path") or None,
77
+ host_id=_payload_text(context.payload, "host_id") or "local",
78
+ )
79
+ await _publish_workspace(context.manager, context.outbox)
80
+ return result
81
+
82
+
83
+ class ThreadCommandStrategy(CodexWsCommandStrategy):
84
+ def thread_id(self, context: CodexWsCommandContext) -> str:
85
+ return _required_payload_text(context.payload, "thread_id")
86
+
87
+ def host_id(self, context: CodexWsCommandContext) -> str | None:
88
+ return _payload_text(context.payload, "host_id") or None
89
+
90
+
91
+ class SubscribeThreadCommandStrategy(ThreadCommandStrategy):
92
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
93
+ thread_id = self.thread_id(context)
94
+ state = await context.manager.subscribe(
95
+ thread_id,
96
+ context.outbox,
97
+ host_id=self.host_id(context),
98
+ )
99
+ context.subscribed_thread_ids.add(thread_id)
100
+ return {"thread_id": thread_id, "state": state}
101
+
102
+
103
+ class UnsubscribeThreadCommandStrategy(ThreadCommandStrategy):
104
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
105
+ thread_id = self.thread_id(context)
106
+ await context.manager.unsubscribe(thread_id, context.outbox)
107
+ context.subscribed_thread_ids.discard(thread_id)
108
+ return {"thread_id": thread_id}
109
+
110
+
111
+ class SendPromptCommandStrategy(ThreadCommandStrategy):
112
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
113
+ thread_id = self.thread_id(context)
114
+ prompt = _payload_text(context.payload, "prompt")
115
+ attachments = _payload_dict_list(context.payload, "attachments")
116
+ if not prompt and not attachments:
117
+ raise ValueError("prompt or attachments is required.")
118
+ await context.manager.send_prompt(
119
+ thread_id,
120
+ prompt,
121
+ collaboration_mode=_payload_dict(context.payload, "collaboration_mode"),
122
+ attachments=attachments,
123
+ approval_policy=_payload_text(context.payload, "approval_policy") or None,
124
+ approvals_reviewer=_payload_text(context.payload, "approvals_reviewer")
125
+ or None,
126
+ sandbox_policy=_payload_dict(context.payload, "sandbox_policy"),
127
+ )
128
+ return {"thread_id": thread_id}
129
+
130
+
131
+ class SteerPromptCommandStrategy(ThreadCommandStrategy):
132
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
133
+ thread_id = self.thread_id(context)
134
+ prompt = _required_payload_text(context.payload, "prompt")
135
+ await context.manager.steer_prompt(thread_id, prompt)
136
+ return {"thread_id": thread_id}
137
+
138
+
139
+ class EnqueueFollowupCommandStrategy(ThreadCommandStrategy):
140
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
141
+ thread_id = self.thread_id(context)
142
+ prompt = _required_payload_text(context.payload, "prompt")
143
+ return await context.manager.enqueue_followup(thread_id, prompt)
144
+
145
+
146
+ class RemoveFollowupCommandStrategy(ThreadCommandStrategy):
147
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
148
+ thread_id = self.thread_id(context)
149
+ message_id = _required_payload_text(context.payload, "message_id")
150
+ await context.manager.remove_followup(thread_id, message_id)
151
+ return {"thread_id": thread_id, "message_id": message_id}
152
+
153
+
154
+ class InterruptTurnCommandStrategy(ThreadCommandStrategy):
155
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
156
+ thread_id = self.thread_id(context)
157
+ turn_id = _payload_text(context.payload, "turn_id") or None
158
+ interrupted = await context.manager.interrupt_turn(thread_id, turn_id)
159
+ return {"thread_id": thread_id, "forwarded": interrupted}
160
+
161
+
162
+ class CompactThreadCommandStrategy(ThreadCommandStrategy):
163
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
164
+ thread_id = self.thread_id(context)
165
+ forwarded = await context.manager.compact_thread(thread_id)
166
+ return {"thread_id": thread_id, "forwarded": forwarded}
167
+
168
+
169
+ class SetThreadGoalCommandStrategy(ThreadCommandStrategy):
170
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
171
+ thread_id = self.thread_id(context)
172
+ response = await context.manager.set_thread_goal(
173
+ thread_id,
174
+ objective=_payload_text(context.payload, "objective") or None,
175
+ status=_payload_text(context.payload, "status") or None,
176
+ token_budget=_payload_int(context.payload, "token_budget")
177
+ or _payload_int(context.payload, "tokenBudget"),
178
+ )
179
+ return {"thread_id": thread_id, **response}
180
+
181
+
182
+ class GetThreadGoalCommandStrategy(ThreadCommandStrategy):
183
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
184
+ thread_id = self.thread_id(context)
185
+ goal = await context.manager.get_thread_goal(thread_id)
186
+ return {"thread_id": thread_id, "goal": goal}
187
+
188
+
189
+ class ClearThreadGoalCommandStrategy(ThreadCommandStrategy):
190
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
191
+ thread_id = self.thread_id(context)
192
+ response = await context.manager.clear_thread_goal(thread_id)
193
+ return {"thread_id": thread_id, **response}
194
+
195
+
196
+ class SetCollaborationModeCommandStrategy(ThreadCommandStrategy):
197
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
198
+ thread_id = self.thread_id(context)
199
+ await context.manager.set_collaboration_mode(
200
+ thread_id,
201
+ _payload_dict(context.payload, "collaboration_mode"),
202
+ )
203
+ return {"thread_id": thread_id}
204
+
205
+
206
+ class SubmitUserInputResponseCommandStrategy(ThreadCommandStrategy):
207
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
208
+ thread_id = self.thread_id(context)
209
+ request_id = _required_payload_text(context.payload, "request_id")
210
+ submitted = await context.manager.submit_user_input_response(
211
+ thread_id,
212
+ request_id,
213
+ _payload_dict(context.payload, "response") or {"answers": {}},
214
+ )
215
+ return {"thread_id": thread_id, "submitted": submitted}
216
+
217
+
218
+ class RenameThreadCommandStrategy(ThreadCommandStrategy):
219
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
220
+ thread_id = self.thread_id(context)
221
+ name = _required_payload_text(context.payload, "name")
222
+ await context.manager.rename_thread(thread_id, name)
223
+ return {"thread_id": thread_id}
224
+
225
+
226
+ class ArchiveThreadCommandStrategy(ThreadCommandStrategy):
227
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
228
+ thread_id = self.thread_id(context)
229
+ await context.manager.archive_thread(thread_id)
230
+ return {"thread_id": thread_id}
231
+
232
+
233
+ class ForkThreadCommandStrategy(ThreadCommandStrategy):
234
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
235
+ thread_id = self.thread_id(context)
236
+ result = await context.manager.fork_thread(
237
+ thread_id,
238
+ host_id=self.host_id(context),
239
+ )
240
+ await _publish_workspace(context.manager, context.outbox)
241
+ return result
242
+
243
+
244
+ class UnarchiveThreadCommandStrategy(ThreadCommandStrategy):
245
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
246
+ thread_id = self.thread_id(context)
247
+ await context.manager.unarchive_thread(
248
+ thread_id,
249
+ host_id=self.host_id(context),
250
+ )
251
+ return {"thread_id": thread_id}
252
+
253
+
254
+ class ListSkillsCommandStrategy(CodexWsCommandStrategy):
255
+ async def execute(self, context: CodexWsCommandContext) -> dict[str, Any]:
256
+ thread_id = _payload_text(context.payload, "thread_id") or None
257
+ cwd = _payload_text(context.payload, "cwd") or None
258
+ force_reload = bool(
259
+ context.payload.get("force_reload") or context.payload.get("forceReload")
260
+ )
261
+ skills = await context.manager.list_skills(
262
+ thread_id=thread_id,
263
+ host_id=_payload_text(context.payload, "host_id") or None,
264
+ cwd=cwd,
265
+ force_reload=force_reload,
266
+ )
267
+ return {"skills": skills}
268
+
269
+
270
+ class CodexWsCommandStrategyFactory:
271
+ def __init__(self) -> None:
272
+ self.strategies: dict[str, CodexWsCommandStrategy] = {
273
+ "list_threads": ListThreadsCommandStrategy(),
274
+ "start_thread": StartThreadCommandStrategy(),
275
+ "subscribe_thread": SubscribeThreadCommandStrategy(),
276
+ "unsubscribe_thread": UnsubscribeThreadCommandStrategy(),
277
+ "send_prompt": SendPromptCommandStrategy(),
278
+ "steer_prompt": SteerPromptCommandStrategy(),
279
+ "enqueue_followup": EnqueueFollowupCommandStrategy(),
280
+ "remove_followup": RemoveFollowupCommandStrategy(),
281
+ "interrupt_turn": InterruptTurnCommandStrategy(),
282
+ "compact_thread": CompactThreadCommandStrategy(),
283
+ "set_thread_goal": SetThreadGoalCommandStrategy(),
284
+ "get_thread_goal": GetThreadGoalCommandStrategy(),
285
+ "clear_thread_goal": ClearThreadGoalCommandStrategy(),
286
+ "set_collaboration_mode": SetCollaborationModeCommandStrategy(),
287
+ "submit_user_input_response": SubmitUserInputResponseCommandStrategy(),
288
+ "rename_thread": RenameThreadCommandStrategy(),
289
+ "archive_thread": ArchiveThreadCommandStrategy(),
290
+ "fork_thread": ForkThreadCommandStrategy(),
291
+ "unarchive_thread": UnarchiveThreadCommandStrategy(),
292
+ "list_skills": ListSkillsCommandStrategy(),
293
+ }
294
+
295
+ def get(self, message_type: str) -> CodexWsCommandStrategy:
296
+ strategy = self.strategies.get(message_type)
297
+ if strategy is None:
298
+ raise ValueError(f"Unsupported Codex command: {message_type}")
299
+ return strategy