jupyterlab-codex-sidebar 0.1.4 → 0.1.6
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/.claude/settings.local.json +9 -0
- package/.github/workflows/unit-tests.yml +27 -0
- package/.jupyterlab-playwright.log +0 -0
- package/README.md +83 -9
- package/docs/images/codex-sidebar-screenshot.png +0 -0
- package/jupyterlab_codex/handlers.py +938 -297
- package/jupyterlab_codex/labextension/package.json +13 -3
- package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
- package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
- package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
- package/jupyterlab_codex/protocol.py +297 -0
- package/jupyterlab_codex/runner.py +58 -15
- package/jupyterlab_codex/sessions.py +582 -97
- package/lib/codexChat.d.ts +13 -0
- package/lib/codexChat.js +2506 -0
- package/lib/codexChat.js.map +1 -0
- package/lib/codexChatAttachmentDedup.d.ts +10 -0
- package/lib/codexChatAttachmentDedup.js +35 -0
- package/lib/codexChatAttachmentDedup.js.map +1 -0
- package/lib/codexChatAttachmentLimit.d.ts +18 -0
- package/lib/codexChatAttachmentLimit.js +50 -0
- package/lib/codexChatAttachmentLimit.js.map +1 -0
- package/lib/codexChatAttachmentState.d.ts +15 -0
- package/lib/codexChatAttachmentState.js +16 -0
- package/lib/codexChatAttachmentState.js.map +1 -0
- package/lib/codexChatDocumentUtils.d.ts +70 -0
- package/lib/codexChatDocumentUtils.js +506 -0
- package/lib/codexChatDocumentUtils.js.map +1 -0
- package/lib/codexChatFormatting.d.ts +11 -0
- package/lib/codexChatFormatting.js +83 -0
- package/lib/codexChatFormatting.js.map +1 -0
- package/lib/codexChatNotice.d.ts +3 -0
- package/lib/codexChatNotice.js +74 -0
- package/lib/codexChatNotice.js.map +1 -0
- package/lib/codexChatPersistence.d.ts +35 -0
- package/lib/codexChatPersistence.js +158 -0
- package/lib/codexChatPersistence.js.map +1 -0
- package/lib/codexChatPrimitives.d.ts +44 -0
- package/lib/codexChatPrimitives.js +156 -0
- package/lib/codexChatPrimitives.js.map +1 -0
- package/lib/codexChatRender.d.ts +24 -0
- package/lib/codexChatRender.js +293 -0
- package/lib/codexChatRender.js.map +1 -0
- package/lib/codexChatSessionFactory.d.ts +15 -0
- package/lib/codexChatSessionFactory.js +45 -0
- package/lib/codexChatSessionFactory.js.map +1 -0
- package/lib/codexChatSessionKey.d.ts +3 -0
- package/lib/codexChatSessionKey.js +14 -0
- package/lib/codexChatSessionKey.js.map +1 -0
- package/lib/codexChatStorage.d.ts +4 -0
- package/lib/codexChatStorage.js +37 -0
- package/lib/codexChatStorage.js.map +1 -0
- package/lib/codexSessionResolver.d.ts +12 -0
- package/lib/codexSessionResolver.js +38 -0
- package/lib/codexSessionResolver.js.map +1 -0
- package/lib/handlers/activitySummarizer.d.ts +15 -0
- package/lib/handlers/activitySummarizer.js +327 -0
- package/lib/handlers/activitySummarizer.js.map +1 -0
- package/lib/handlers/codexMessageTypes.d.ts +30 -0
- package/lib/handlers/codexMessageTypes.js +2 -0
- package/lib/handlers/codexMessageTypes.js.map +1 -0
- package/lib/handlers/codexMessageUtils.d.ts +46 -0
- package/lib/handlers/codexMessageUtils.js +144 -0
- package/lib/handlers/codexMessageUtils.js.map +1 -0
- package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
- package/lib/handlers/handleCodexSocketMessage.js +78 -0
- package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
- package/lib/handlers/sessionSyncHandler.d.ts +34 -0
- package/lib/handlers/sessionSyncHandler.js +181 -0
- package/lib/handlers/sessionSyncHandler.js.map +1 -0
- package/lib/hooks/useCodexSocket.d.ts +15 -0
- package/lib/hooks/useCodexSocket.js +84 -0
- package/lib/hooks/useCodexSocket.js.map +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/panel.d.ts +1 -11
- package/lib/panel.js +1 -2815
- package/lib/panel.js.map +1 -1
- package/lib/protocol.d.ts +235 -0
- package/lib/protocol.js +278 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +13 -3
- package/playwright.config.cjs +27 -0
- package/playwright.unit.config.cjs +19 -0
- package/pyproject.toml +1 -1
- package/release.sh +52 -14
- package/scripts/run_playwright_e2e.sh +96 -0
- package/scripts/run_playwright_freeze_repro.sh +58 -0
- package/scripts/run_playwright_queue_repro.sh +60 -0
- package/scripts/run_playwright_repro.sh +55 -0
- package/src/codexChat.tsx +3914 -0
- package/src/codexChatAttachmentDedup.ts +47 -0
- package/src/codexChatAttachmentLimit.ts +81 -0
- package/src/codexChatAttachmentState.ts +37 -0
- package/src/codexChatDocumentUtils.ts +644 -0
- package/src/codexChatFormatting.ts +94 -0
- package/src/codexChatNotice.ts +95 -0
- package/src/codexChatPersistence.ts +191 -0
- package/src/codexChatPrimitives.tsx +446 -0
- package/src/codexChatRender.tsx +376 -0
- package/src/codexChatSessionFactory.ts +79 -0
- package/src/codexChatSessionKey.ts +16 -0
- package/src/codexChatStorage.ts +36 -0
- package/src/codexSessionResolver.ts +56 -0
- package/src/handlers/activitySummarizer.ts +369 -0
- package/src/handlers/codexMessageTypes.ts +34 -0
- package/src/handlers/codexMessageUtils.ts +217 -0
- package/src/handlers/handleCodexSocketMessage.ts +204 -0
- package/src/handlers/sessionSyncHandler.ts +308 -0
- package/src/hooks/useCodexSocket.ts +109 -0
- package/src/index.ts +1 -1
- package/src/panel.tsx +1 -4184
- package/src/protocol.ts +582 -0
- package/style/index.css +480 -11
- package/test-results/.last-run.json +4 -0
- package/test.py +0 -0
- package/tests/e2e/cell-output-error-tail.spec.js +156 -0
- package/tests/e2e/codex-ui-test-helpers.js +138 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
- package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
- package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
- package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
- package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
- package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
- package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
- package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
- package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
- package/tests/e2e/mock-codex-cli-flood.py +127 -0
- package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
- package/tests/e2e/mock-codex-cli.py +95 -0
- package/tests/e2e/queue-multitab-repro.spec.js +189 -0
- package/tests/test_handlers.py +116 -0
- package/tests/test_protocol.py +169 -0
- package/tests/test_session_store_limits.py +50 -0
- package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
- package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
- package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
- package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
- package/tests/unit/codexChatLimit.spec.ts +18 -0
- package/tests/unit/codexChatNotice.spec.ts +45 -0
- package/tests/unit/codexChatPersistence.spec.ts +199 -0
- package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
- package/tests/unit/codexChatSessionKey.spec.ts +18 -0
- package/tests/unit/codexMessageUtils.spec.ts +89 -0
- package/tests/unit/codexSessionResolver.spec.ts +92 -0
- package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/webpack.config.js +6 -0
- package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
- package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
- package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
- /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Literal, Tuple
|
|
4
|
+
|
|
5
|
+
ProtocolVersion = Literal["1.0.0"]
|
|
6
|
+
|
|
7
|
+
PROTOCOL_VERSION: ProtocolVersion = "1.0.0"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProtocolParseError(ValueError):
|
|
11
|
+
"""Raised when an incoming client payload cannot be interpreted."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _coerce_string(value: Any) -> str:
|
|
15
|
+
if not isinstance(value, str):
|
|
16
|
+
return ""
|
|
17
|
+
return value.strip()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _coerce_string_untrimmed(value: Any) -> str:
|
|
21
|
+
if not isinstance(value, str):
|
|
22
|
+
return ""
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _coerce_bool(value: Any) -> bool:
|
|
27
|
+
if isinstance(value, bool):
|
|
28
|
+
return value
|
|
29
|
+
if isinstance(value, str):
|
|
30
|
+
return value.strip().lower() in {"1", "true", "y", "yes", "on"}
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_client_message(raw: Any) -> Tuple[str, Dict[str, Any]]:
|
|
35
|
+
"""
|
|
36
|
+
Parse and normalize incoming websocket payloads.
|
|
37
|
+
|
|
38
|
+
Returns (msg_type, payload) with normalized string/bool fields.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(raw, dict):
|
|
41
|
+
raise ProtocolParseError("Invalid message payload")
|
|
42
|
+
|
|
43
|
+
msg_type = raw.get("type")
|
|
44
|
+
if msg_type == "start_session":
|
|
45
|
+
return (
|
|
46
|
+
"start_session",
|
|
47
|
+
{
|
|
48
|
+
"sessionId": _coerce_string(raw.get("sessionId")),
|
|
49
|
+
"notebookPath": _coerce_string(raw.get("notebookPath")),
|
|
50
|
+
"sessionContextKey": _coerce_string(raw.get("sessionContextKey")),
|
|
51
|
+
"forceNewThread": _coerce_bool(raw.get("forceNewThread")),
|
|
52
|
+
"commandPath": _coerce_string(raw.get("commandPath")),
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if msg_type == "send":
|
|
57
|
+
return (
|
|
58
|
+
"send",
|
|
59
|
+
{
|
|
60
|
+
"sessionId": _coerce_string(raw.get("sessionId")),
|
|
61
|
+
"sessionContextKey": _coerce_string(raw.get("sessionContextKey")),
|
|
62
|
+
"content": _coerce_string(raw.get("content")),
|
|
63
|
+
"notebookPath": _coerce_string(raw.get("notebookPath")),
|
|
64
|
+
"commandPath": _coerce_string(raw.get("commandPath")),
|
|
65
|
+
"model": _coerce_string(raw.get("model")),
|
|
66
|
+
"reasoningEffort": _coerce_string(raw.get("reasoningEffort")),
|
|
67
|
+
"sandbox": _coerce_string(raw.get("sandbox")),
|
|
68
|
+
"selection": _coerce_string_untrimmed(raw.get("selection")),
|
|
69
|
+
"cellOutput": _coerce_string_untrimmed(raw.get("cellOutput")),
|
|
70
|
+
"images": raw.get("images") if isinstance(raw.get("images"), list) else [],
|
|
71
|
+
"uiSelectionPreview": raw.get("uiSelectionPreview"),
|
|
72
|
+
"uiCellOutputPreview": raw.get("uiCellOutputPreview"),
|
|
73
|
+
"selectionTruncated": _coerce_bool(raw.get("selectionTruncated")),
|
|
74
|
+
"cellOutputTruncated": _coerce_bool(raw.get("cellOutputTruncated")),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if msg_type == "delete_session":
|
|
79
|
+
return ("delete_session", {"sessionId": _coerce_string(raw.get("sessionId"))})
|
|
80
|
+
|
|
81
|
+
if msg_type == "delete_all_sessions":
|
|
82
|
+
return ("delete_all_sessions", {})
|
|
83
|
+
|
|
84
|
+
if msg_type == "cancel":
|
|
85
|
+
return ("cancel", {"runId": _coerce_string(raw.get("runId"))})
|
|
86
|
+
|
|
87
|
+
if msg_type == "end_session":
|
|
88
|
+
return ("end_session", {"sessionId": _coerce_string(raw.get("sessionId"))})
|
|
89
|
+
|
|
90
|
+
if msg_type == "refresh_rate_limits":
|
|
91
|
+
return ("refresh_rate_limits", {})
|
|
92
|
+
|
|
93
|
+
raise ProtocolParseError("Unknown message type")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_base_message(msg_type: str) -> Dict[str, Any]:
|
|
97
|
+
return {"type": msg_type, "protocolVersion": PROTOCOL_VERSION}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def build_cli_defaults_payload(
|
|
101
|
+
*,
|
|
102
|
+
model: str | None = None,
|
|
103
|
+
reasoning_effort: str | None = None,
|
|
104
|
+
available_models: list[dict[str, Any]] | None = None,
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
payload = _build_base_message("cli_defaults")
|
|
107
|
+
if model is not None:
|
|
108
|
+
payload["model"] = model
|
|
109
|
+
if reasoning_effort is not None:
|
|
110
|
+
payload["reasoningEffort"] = reasoning_effort
|
|
111
|
+
if available_models is not None:
|
|
112
|
+
payload["availableModels"] = available_models
|
|
113
|
+
return payload
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_status_payload(
|
|
117
|
+
*,
|
|
118
|
+
state: str,
|
|
119
|
+
run_id: str | None = None,
|
|
120
|
+
session_id: str | None = None,
|
|
121
|
+
session_context_key: str | None = None,
|
|
122
|
+
notebook_path: str | None = None,
|
|
123
|
+
run_mode: str | None = None,
|
|
124
|
+
paired_ok: bool | None = None,
|
|
125
|
+
paired_path: str = "",
|
|
126
|
+
paired_os_path: str = "",
|
|
127
|
+
paired_message: str = "",
|
|
128
|
+
notebook_mode: str = "",
|
|
129
|
+
effective_sandbox: str | None = None,
|
|
130
|
+
history: list[dict[str, Any]] | None = None,
|
|
131
|
+
session_resolution: str | None = None,
|
|
132
|
+
session_resolution_notice: str | None = None,
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
payload = _build_base_message("status")
|
|
135
|
+
payload["state"] = state
|
|
136
|
+
|
|
137
|
+
if run_id:
|
|
138
|
+
payload["runId"] = run_id
|
|
139
|
+
if session_id:
|
|
140
|
+
payload["sessionId"] = session_id
|
|
141
|
+
if session_context_key:
|
|
142
|
+
payload["sessionContextKey"] = session_context_key
|
|
143
|
+
if notebook_path:
|
|
144
|
+
payload["notebookPath"] = notebook_path
|
|
145
|
+
if run_mode:
|
|
146
|
+
payload["runMode"] = run_mode
|
|
147
|
+
if paired_ok is not None:
|
|
148
|
+
payload["pairedOk"] = paired_ok
|
|
149
|
+
if paired_path:
|
|
150
|
+
payload["pairedPath"] = paired_path
|
|
151
|
+
if paired_os_path:
|
|
152
|
+
payload["pairedOsPath"] = paired_os_path
|
|
153
|
+
if paired_message:
|
|
154
|
+
payload["pairedMessage"] = paired_message
|
|
155
|
+
if notebook_mode:
|
|
156
|
+
payload["notebookMode"] = notebook_mode
|
|
157
|
+
if history is not None:
|
|
158
|
+
payload["history"] = history
|
|
159
|
+
if session_resolution is not None:
|
|
160
|
+
payload["sessionResolution"] = session_resolution
|
|
161
|
+
if session_resolution_notice:
|
|
162
|
+
payload["sessionResolutionNotice"] = session_resolution_notice
|
|
163
|
+
if effective_sandbox:
|
|
164
|
+
payload["effectiveSandbox"] = effective_sandbox
|
|
165
|
+
return payload
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def build_output_payload(
|
|
169
|
+
*,
|
|
170
|
+
run_id: str,
|
|
171
|
+
session_id: str,
|
|
172
|
+
session_context_key: str,
|
|
173
|
+
notebook_path: str,
|
|
174
|
+
text: str,
|
|
175
|
+
role: str = "assistant",
|
|
176
|
+
) -> Dict[str, Any]:
|
|
177
|
+
return _build_base_message("output") | {
|
|
178
|
+
"runId": run_id,
|
|
179
|
+
"sessionId": session_id,
|
|
180
|
+
"sessionContextKey": session_context_key,
|
|
181
|
+
"notebookPath": notebook_path,
|
|
182
|
+
"text": text,
|
|
183
|
+
"role": role,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_event_payload(
|
|
188
|
+
*,
|
|
189
|
+
run_id: str,
|
|
190
|
+
session_id: str,
|
|
191
|
+
session_context_key: str,
|
|
192
|
+
notebook_path: str,
|
|
193
|
+
payload: Dict[str, Any],
|
|
194
|
+
) -> Dict[str, Any]:
|
|
195
|
+
return _build_base_message("event") | {
|
|
196
|
+
"runId": run_id,
|
|
197
|
+
"sessionId": session_id,
|
|
198
|
+
"sessionContextKey": session_context_key,
|
|
199
|
+
"notebookPath": notebook_path,
|
|
200
|
+
"payload": payload,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def build_done_payload(
|
|
205
|
+
*,
|
|
206
|
+
run_id: str,
|
|
207
|
+
session_id: str,
|
|
208
|
+
session_context_key: str,
|
|
209
|
+
notebook_path: str,
|
|
210
|
+
exit_code: int | None,
|
|
211
|
+
file_changed: bool,
|
|
212
|
+
run_mode: str,
|
|
213
|
+
paired_ok: bool,
|
|
214
|
+
paired_path: str,
|
|
215
|
+
paired_os_path: str,
|
|
216
|
+
paired_message: str,
|
|
217
|
+
notebook_mode: str,
|
|
218
|
+
cancelled: bool = False,
|
|
219
|
+
) -> Dict[str, Any]:
|
|
220
|
+
payload = _build_base_message("done") | {
|
|
221
|
+
"runId": run_id,
|
|
222
|
+
"sessionId": session_id,
|
|
223
|
+
"sessionContextKey": session_context_key,
|
|
224
|
+
"notebookPath": notebook_path,
|
|
225
|
+
"exitCode": exit_code,
|
|
226
|
+
"fileChanged": file_changed,
|
|
227
|
+
"runMode": run_mode,
|
|
228
|
+
"pairedOk": paired_ok,
|
|
229
|
+
}
|
|
230
|
+
payload["pairedPath"] = paired_path
|
|
231
|
+
payload["pairedOsPath"] = paired_os_path
|
|
232
|
+
payload["pairedMessage"] = paired_message
|
|
233
|
+
payload["notebookMode"] = notebook_mode
|
|
234
|
+
if cancelled:
|
|
235
|
+
payload["cancelled"] = True
|
|
236
|
+
return payload
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_error_payload(
|
|
240
|
+
*,
|
|
241
|
+
run_id: str | None = None,
|
|
242
|
+
session_id: str | None = None,
|
|
243
|
+
session_context_key: str = "",
|
|
244
|
+
notebook_path: str = "",
|
|
245
|
+
message: str,
|
|
246
|
+
run_mode: str | None = None,
|
|
247
|
+
suggested_command_path: str | None = None,
|
|
248
|
+
paired_ok: bool | None = None,
|
|
249
|
+
paired_path: str = "",
|
|
250
|
+
paired_os_path: str = "",
|
|
251
|
+
paired_message: str = "",
|
|
252
|
+
notebook_mode: str = "",
|
|
253
|
+
) -> Dict[str, Any]:
|
|
254
|
+
payload = _build_base_message("error")
|
|
255
|
+
if run_id:
|
|
256
|
+
payload["runId"] = run_id
|
|
257
|
+
if session_id:
|
|
258
|
+
payload["sessionId"] = session_id
|
|
259
|
+
if session_context_key:
|
|
260
|
+
payload["sessionContextKey"] = session_context_key
|
|
261
|
+
if notebook_path:
|
|
262
|
+
payload["notebookPath"] = notebook_path
|
|
263
|
+
payload["message"] = message
|
|
264
|
+
if run_mode is not None:
|
|
265
|
+
payload["runMode"] = run_mode
|
|
266
|
+
if suggested_command_path:
|
|
267
|
+
payload["suggestedCommandPath"] = suggested_command_path
|
|
268
|
+
if paired_ok is not None:
|
|
269
|
+
payload["pairedOk"] = paired_ok
|
|
270
|
+
if paired_path:
|
|
271
|
+
payload["pairedPath"] = paired_path
|
|
272
|
+
if paired_os_path:
|
|
273
|
+
payload["pairedOsPath"] = paired_os_path
|
|
274
|
+
if paired_message:
|
|
275
|
+
payload["pairedMessage"] = paired_message
|
|
276
|
+
if notebook_mode:
|
|
277
|
+
payload["notebookMode"] = notebook_mode
|
|
278
|
+
return payload
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def build_rate_limits_payload(snapshot: Any) -> Dict[str, Any]:
|
|
282
|
+
return _build_base_message("rate_limits") | {"snapshot": snapshot}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def build_delete_all_payload(
|
|
286
|
+
*,
|
|
287
|
+
ok: bool,
|
|
288
|
+
deleted_count: int,
|
|
289
|
+
failed_count: int,
|
|
290
|
+
message: str,
|
|
291
|
+
) -> Dict[str, Any]:
|
|
292
|
+
payload = _build_base_message("delete_all_sessions")
|
|
293
|
+
payload["ok"] = ok
|
|
294
|
+
payload["deletedCount"] = deleted_count
|
|
295
|
+
payload["failedCount"] = failed_count
|
|
296
|
+
payload["message"] = message
|
|
297
|
+
return payload
|
|
@@ -11,6 +11,7 @@ class CodexRunner:
|
|
|
11
11
|
def __init__(self, command: str = "codex", args: List[str] | None = None):
|
|
12
12
|
configured_command = os.environ.get("JUPYTERLAB_CODEX_COMMAND", "").strip()
|
|
13
13
|
self._command = self._resolve_command(configured_command or command)
|
|
14
|
+
self._model_catalog_cache: dict[str, tuple[float, list[dict[str, Any]]]] = {}
|
|
14
15
|
if args is not None:
|
|
15
16
|
self._raw_args = list(args)
|
|
16
17
|
self._common_args: List[str] = []
|
|
@@ -32,24 +33,24 @@ class CodexRunner:
|
|
|
32
33
|
"never",
|
|
33
34
|
"--skip-git-repo-check",
|
|
34
35
|
]
|
|
35
|
-
self._model_catalog_cache: list[dict[str, Any]] = []
|
|
36
|
-
self._model_catalog_cache_time = 0.0
|
|
37
36
|
|
|
38
|
-
async def list_available_models(
|
|
37
|
+
async def list_available_models(
|
|
38
|
+
self, command: str | None = None, force_refresh: bool = False
|
|
39
|
+
) -> list[dict[str, Any]]:
|
|
40
|
+
command_to_run = self._resolve_command((command or "").strip() or self._command)
|
|
39
41
|
now = time.monotonic()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
cached = self._model_catalog_cache.get(command_to_run)
|
|
43
|
+
if not force_refresh and cached and now - cached[0] < 600:
|
|
44
|
+
return list(cached[1])
|
|
42
45
|
|
|
43
|
-
command_to_run = self._resolve_command((command or "").strip() or self._command)
|
|
44
46
|
try:
|
|
45
47
|
models = await self._load_available_models(command_to_run)
|
|
46
48
|
except Exception:
|
|
47
|
-
return []
|
|
49
|
+
return list(cached[1]) if cached else []
|
|
48
50
|
|
|
49
51
|
if not isinstance(models, list) or not models:
|
|
50
|
-
return []
|
|
51
|
-
self._model_catalog_cache = models
|
|
52
|
-
self._model_catalog_cache_time = now
|
|
52
|
+
return list(cached[1]) if cached else []
|
|
53
|
+
self._model_catalog_cache[command_to_run] = (now, models)
|
|
53
54
|
return list(models)
|
|
54
55
|
|
|
55
56
|
async def _load_available_models(self, command_to_run: str) -> list[dict[str, Any]]:
|
|
@@ -60,9 +61,9 @@ class CodexRunner:
|
|
|
60
61
|
"stdio://",
|
|
61
62
|
stdin=asyncio.subprocess.PIPE,
|
|
62
63
|
stdout=asyncio.subprocess.PIPE,
|
|
63
|
-
stderr=asyncio.subprocess.
|
|
64
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
64
65
|
)
|
|
65
|
-
if proc.stdin is None or proc.stdout is None
|
|
66
|
+
if proc.stdin is None or proc.stdout is None:
|
|
66
67
|
raise RuntimeError("Failed to open app-server subprocess streams")
|
|
67
68
|
|
|
68
69
|
buffer = bytearray()
|
|
@@ -275,10 +276,15 @@ class CodexRunner:
|
|
|
275
276
|
sandbox: str | None = None,
|
|
276
277
|
images: List[str] | None = None,
|
|
277
278
|
command: str | None = None,
|
|
279
|
+
resume_session_id: str | None = None,
|
|
278
280
|
) -> int:
|
|
279
281
|
command_to_run = self._resolve_command((command or "").strip() or self._command)
|
|
280
282
|
args = self._args_for_options(
|
|
281
|
-
model=model,
|
|
283
|
+
model=model,
|
|
284
|
+
reasoning_effort=reasoning_effort,
|
|
285
|
+
sandbox=sandbox,
|
|
286
|
+
images=images,
|
|
287
|
+
resume_session_id=resume_session_id,
|
|
282
288
|
)
|
|
283
289
|
|
|
284
290
|
proc = await asyncio.create_subprocess_exec(
|
|
@@ -371,11 +377,13 @@ class CodexRunner:
|
|
|
371
377
|
reasoning_effort: str | None,
|
|
372
378
|
sandbox: str | None,
|
|
373
379
|
images: List[str] | None,
|
|
380
|
+
resume_session_id: str | None = None,
|
|
374
381
|
) -> List[str]:
|
|
375
382
|
requested_model = (model or "").strip()
|
|
376
383
|
requested_reasoning_effort = (reasoning_effort or "").strip()
|
|
377
384
|
requested_sandbox = (sandbox or "").strip()
|
|
378
385
|
requested_images = [p for p in (images or []) if isinstance(p, str) and p.strip()]
|
|
386
|
+
requested_resume_session_id = (resume_session_id or "").strip()
|
|
379
387
|
|
|
380
388
|
if self._raw_args is not None:
|
|
381
389
|
args = list(self._raw_args)
|
|
@@ -399,7 +407,17 @@ class CodexRunner:
|
|
|
399
407
|
cleaned.append(token)
|
|
400
408
|
idx += 1
|
|
401
409
|
|
|
402
|
-
|
|
410
|
+
stdin_insertion_index = cleaned.index("-") if "-" in cleaned else len(cleaned)
|
|
411
|
+
if requested_resume_session_id and "resume" not in cleaned:
|
|
412
|
+
cleaned[stdin_insertion_index:stdin_insertion_index] = [
|
|
413
|
+
"resume",
|
|
414
|
+
requested_resume_session_id,
|
|
415
|
+
]
|
|
416
|
+
options_insertion_index = (
|
|
417
|
+
cleaned.index("resume")
|
|
418
|
+
if "resume" in cleaned
|
|
419
|
+
else (cleaned.index("-") if "-" in cleaned else len(cleaned))
|
|
420
|
+
)
|
|
403
421
|
to_insert: List[str] = []
|
|
404
422
|
if requested_images:
|
|
405
423
|
to_insert.extend(["--image", *requested_images])
|
|
@@ -410,11 +428,36 @@ class CodexRunner:
|
|
|
410
428
|
if requested_reasoning_effort:
|
|
411
429
|
to_insert.extend(["-c", f'model_reasoning_effort="{requested_reasoning_effort}"'])
|
|
412
430
|
|
|
413
|
-
cleaned[
|
|
431
|
+
cleaned[options_insertion_index:options_insertion_index] = to_insert
|
|
414
432
|
return cleaned
|
|
415
433
|
|
|
416
434
|
effective_model = requested_model or self._default_model
|
|
417
435
|
effective_sandbox = requested_sandbox or self._default_sandbox
|
|
436
|
+
|
|
437
|
+
if requested_resume_session_id:
|
|
438
|
+
# `codex exec resume` does not accept `--sandbox` after `resume`.
|
|
439
|
+
# Sandbox must be passed as an `exec` option before the `resume` subcommand.
|
|
440
|
+
args: List[str] = [
|
|
441
|
+
"--ask-for-approval",
|
|
442
|
+
"never",
|
|
443
|
+
"exec",
|
|
444
|
+
"--json",
|
|
445
|
+
"--color",
|
|
446
|
+
"never",
|
|
447
|
+
"--skip-git-repo-check",
|
|
448
|
+
]
|
|
449
|
+
if effective_sandbox:
|
|
450
|
+
args.extend(["--sandbox", effective_sandbox])
|
|
451
|
+
args.append("resume")
|
|
452
|
+
if effective_model:
|
|
453
|
+
args.extend(["-m", effective_model])
|
|
454
|
+
if requested_reasoning_effort:
|
|
455
|
+
args.extend(["-c", f'model_reasoning_effort="{requested_reasoning_effort}"'])
|
|
456
|
+
if requested_images:
|
|
457
|
+
args.extend(["--image", *requested_images])
|
|
458
|
+
args.extend([requested_resume_session_id, "-"])
|
|
459
|
+
return args
|
|
460
|
+
|
|
418
461
|
args = list(self._common_args)
|
|
419
462
|
if effective_sandbox:
|
|
420
463
|
args.extend(["--sandbox", effective_sandbox])
|