jupyterlab-codex-sidebar 0.1.2 → 0.1.3

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 (87) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +27 -15
  3. package/{run_jupyterlab_codex.sh → install_dev.sh} +25 -19
  4. package/jupyterlab_codex/cli_defaults.py +1 -4
  5. package/jupyterlab_codex/handlers.py +133 -8
  6. package/jupyterlab_codex/labextension/package.json +4 -2
  7. package/jupyterlab_codex/labextension/static/00b26ac825e209505639.woff2 +0 -0
  8. package/jupyterlab_codex/labextension/static/036d4e95149b69ff9bcc.woff2 +0 -0
  9. package/jupyterlab_codex/labextension/static/07d8e303ce4fc12b4bb5.ttf +0 -0
  10. package/jupyterlab_codex/labextension/static/08ce98e51b04d58945a3.ttf +0 -0
  11. package/jupyterlab_codex/labextension/static/0cdd387c9590a1a9f979.woff2 +0 -0
  12. package/jupyterlab_codex/labextension/static/0d85ae7cc30f23790a7f.ttf +0 -0
  13. package/jupyterlab_codex/labextension/static/0f60d1b897938ec918c8.woff2 +0 -0
  14. package/jupyterlab_codex/labextension/static/11e4dc8a6471ff6d6ee5.woff +0 -0
  15. package/jupyterlab_codex/labextension/static/124.ac0734653995d3fe00f2.js +1 -0
  16. package/jupyterlab_codex/labextension/static/138ac28d1663b3037e9c.ttf +0 -0
  17. package/jupyterlab_codex/labextension/static/1ae6bd7475590e97e7f1.woff +0 -0
  18. package/jupyterlab_codex/labextension/static/1c67f068fea8bb09bf09.ttf +0 -0
  19. package/jupyterlab_codex/labextension/static/1e6f9579e90e2cac37f8.ttf +0 -0
  20. package/jupyterlab_codex/labextension/static/1ece03f79f95277d57dc.ttf +0 -0
  21. package/jupyterlab_codex/labextension/static/2014c523c3210bcc1666.woff +0 -0
  22. package/jupyterlab_codex/labextension/static/30da91e84c893f875e25.woff +0 -0
  23. package/jupyterlab_codex/labextension/static/3398dd02302557a793f2.woff +0 -0
  24. package/jupyterlab_codex/labextension/static/3931dd81faed86ba021b.ttf +0 -0
  25. package/jupyterlab_codex/labextension/static/489.ef138a7e916c5b465543.js +1 -0
  26. package/jupyterlab_codex/labextension/static/500e04d54f0d51666332.ttf +0 -0
  27. package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +2 -0
  28. package/jupyterlab_codex/labextension/static/51814d270d06ff0255db.woff2 +0 -0
  29. package/jupyterlab_codex/labextension/static/5d53e70ad607c2352162.woff2 +0 -0
  30. package/jupyterlab_codex/labextension/static/5e28753be717dac97f55.woff +0 -0
  31. package/jupyterlab_codex/labextension/static/68534840bcfdd2bffb6f.ttf +0 -0
  32. package/jupyterlab_codex/labextension/static/68e8c73ef42afd3ccec5.woff2 +0 -0
  33. package/jupyterlab_codex/labextension/static/6ab6b62e9b62dae2c00d.woff +0 -0
  34. package/jupyterlab_codex/labextension/static/6b47c40166b6dbe21a5d.woff2 +0 -0
  35. package/jupyterlab_codex/labextension/static/70ee1f64a20f2048c219.ttf +0 -0
  36. package/jupyterlab_codex/labextension/static/71d517d67827787cfabd.woff2 +0 -0
  37. package/jupyterlab_codex/labextension/static/73d591271b1604960cb1.woff2 +0 -0
  38. package/jupyterlab_codex/labextension/static/74444efd593c005e3f45.woff2 +0 -0
  39. package/jupyterlab_codex/labextension/static/7af58c5ec8f132a2ddde.woff2 +0 -0
  40. package/jupyterlab_codex/labextension/static/850c0af5c2238497feba.woff +0 -0
  41. package/jupyterlab_codex/labextension/static/8a8d244581371912b8f3.woff +0 -0
  42. package/jupyterlab_codex/labextension/static/9163df9c7122432e6495.ttf +0 -0
  43. package/jupyterlab_codex/labextension/static/91ee67500cc0129aa0ac.woff +0 -0
  44. package/jupyterlab_codex/labextension/static/95b6d2f1a50173bfedb8.ttf +0 -0
  45. package/jupyterlab_codex/labextension/static/972.d43137b7438a053eeb72.js +1 -0
  46. package/jupyterlab_codex/labextension/static/97479ca6cce906abc961.woff2 +0 -0
  47. package/jupyterlab_codex/labextension/static/99cd42a3c072d918f2f4.woff2 +0 -0
  48. package/jupyterlab_codex/labextension/static/99f9c6750b489c9462bf.woff +0 -0
  49. package/jupyterlab_codex/labextension/static/9be7ceb88004ab8ad124.woff +0 -0
  50. package/jupyterlab_codex/labextension/static/a4af7d414440a1c17908.woff2 +0 -0
  51. package/jupyterlab_codex/labextension/static/a6b2099fb555c60e3a0d.ttf +0 -0
  52. package/jupyterlab_codex/labextension/static/a6f7ec0d846ac7ad975a.woff +0 -0
  53. package/jupyterlab_codex/labextension/static/c2342cd8b869e01752a9.woff2 +0 -0
  54. package/jupyterlab_codex/labextension/static/c6368d87e8a1a3a5d337.woff +0 -0
  55. package/jupyterlab_codex/labextension/static/c647367d1dd4e1624687.ttf +0 -0
  56. package/jupyterlab_codex/labextension/static/c76c5d696297d51b9cb1.woff +0 -0
  57. package/jupyterlab_codex/labextension/static/c943cc986384f59e86be.woff +0 -0
  58. package/jupyterlab_codex/labextension/static/d0332f52868370fd83ae.ttf +0 -0
  59. package/jupyterlab_codex/labextension/static/d04c54219f9eaec6d4d4.woff2 +0 -0
  60. package/jupyterlab_codex/labextension/static/d96cdf2b3bdd4d64a8fd.woff +0 -0
  61. package/jupyterlab_codex/labextension/static/dc47344dbb6cb5b655c8.woff2 +0 -0
  62. package/jupyterlab_codex/labextension/static/de7701e42cf1f4cf0b76.woff2 +0 -0
  63. package/jupyterlab_codex/labextension/static/e14fed02b1aba7ce9f5a.woff +0 -0
  64. package/jupyterlab_codex/labextension/static/e99ae51144bf1232efcc.woff2 +0 -0
  65. package/jupyterlab_codex/labextension/static/ece03cfd83e22c212cde.woff +0 -0
  66. package/jupyterlab_codex/labextension/static/ed0b74372feefcbb9c06.ttf +0 -0
  67. package/jupyterlab_codex/labextension/static/f01f3e87d9c6a61c0c08.ttf +0 -0
  68. package/jupyterlab_codex/labextension/static/f1d6ef86f3b11a528bd5.woff +0 -0
  69. package/jupyterlab_codex/labextension/static/f36ea897e19f4a2e571d.ttf +0 -0
  70. package/jupyterlab_codex/labextension/static/f9377ab0271cda59af24.ttf +0 -0
  71. package/jupyterlab_codex/labextension/static/remoteEntry.b2fdc03a1c4582e79156.js +1 -0
  72. package/jupyterlab_codex/labextension/static/third-party-licenses.json +12 -0
  73. package/jupyterlab_codex/runner.py +169 -0
  74. package/jupyterlab_codex/sessions.py +74 -2
  75. package/lib/panel.d.ts +4 -0
  76. package/lib/panel.js +999 -231
  77. package/lib/panel.js.map +1 -1
  78. package/package.json +3 -1
  79. package/pyproject.toml +1 -1
  80. package/run_dev.sh +16 -0
  81. package/src/panel.tsx +1358 -336
  82. package/style/index.css +249 -29
  83. package/tsconfig.tsbuildinfo +1 -1
  84. package/jupyterlab_codex/labextension/static/375.d3c009ef437170530252.js +0 -2
  85. package/jupyterlab_codex/labextension/static/972.e892445dcfe89d335cf4.js +0 -1
  86. package/jupyterlab_codex/labextension/static/remoteEntry.00c43c61b0b2e7773f34.js +0 -1
  87. /package/jupyterlab_codex/labextension/static/{375.d3c009ef437170530252.js.LICENSE.txt → 504.335f3447c84ba3d74517.js.LICENSE.txt} +0 -0
package/AGENTS.md CHANGED
@@ -41,3 +41,8 @@
41
41
  - 사용자 프롬프트/출력 내용을 원문 저장하거나 네트워크로 전송하는 로직 변경
42
42
  - localStorage/IndexedDB/세션 파일에 사용자 컨텍스트를 저장하는 로직 추가
43
43
  - 외부 CLI/도구 실행 경로를 하드코딩하거나, 권한 범위를 확장하는 동작 변경
44
+
45
+ ## UI Language Policy
46
+
47
+ - User-facing UI labels, buttons, notices, dialogs, and system messages must always be displayed in English.
48
+ - Do not introduce Korean (or other non-English) UI strings in code changes.
package/README.md CHANGED
@@ -64,26 +64,25 @@ The backend runs `codex` as a local subprocess per request, streams JSONL events
64
64
  ## Install / Run
65
65
 
66
66
  ### Quick start (recommended for local development)
67
- Run:
67
+ There are two development workflows:
68
+
69
+ - `install_dev.sh` : install/link only (does not start JupyterLab)
70
+ - `run_dev.sh` : install first, then start JupyterLab
71
+
72
+ Install only:
68
73
 
69
74
  ```bash
70
- bash run_jupyterlab_codex.sh
75
+ bash install_dev.sh
71
76
  ```
72
77
 
73
- The script will:
74
- 1. install JS dependencies (`jlpm install`)
75
- 2. build frontend (`jlpm build`)
76
- 3. install Python package editable (`python -m pip install -e .`)
77
- 4. install server config snippet and enable extension
78
- 5. link labextension in `share/jupyter/labextensions/`
79
- 6. launch `jupyter lab`
80
-
81
- You can pass JupyterLab options directly:
78
+ Install + run:
82
79
 
83
80
  ```bash
84
- bash run_jupyterlab_codex.sh --ServerApp.port=8888
81
+ bash run_dev.sh --ServerApp.port=8888
85
82
  ```
86
83
 
84
+ `run_dev.sh` internally runs `install_dev.sh` first.
85
+
87
86
  ### Manual local install
88
87
  1. Build frontend
89
88
 
@@ -252,12 +251,25 @@ JupyterLab 4 우측 사이드바에서 Codex CLI(`codex exec --json`)를 채팅
252
251
 
253
252
  ## 설치/실행
254
253
  ### 빠른 실행(권장)
255
- 아래 스크립트는 “개발/로컬 실행”에 필요한 과정을 한 번에 수행합니다.
254
+ 개발용 스크립트가 분리되어 있습니다.
255
+
256
+ - `install_dev.sh` : 설치/링크만 수행 (`jupyter lab` 실행 없음)
257
+ - `run_dev.sh` : 설치 후 JupyterLab 실행
258
+
259
+ 설치만:
256
260
 
257
261
  ```bash
258
- bash run_jupyterlab_codex.sh
262
+ bash install_dev.sh
259
263
  ```
260
264
 
265
+ 설치 + 실행:
266
+
267
+ ```bash
268
+ bash run_dev.sh --ServerApp.port=8888
269
+ ```
270
+
271
+ `run_dev.sh`는 내부적으로 `install_dev.sh`를 먼저 실행한 뒤 JupyterLab을 시작합니다.
272
+
261
273
  스크립트가 하는 일(요약):
262
274
  - JS 의존성 설치(`jlpm install`) 및 빌드(`jlpm build`)
263
275
  - 파이썬 패키지 editable 설치(`python -m pip install -e .`)
@@ -268,7 +280,7 @@ bash run_jupyterlab_codex.sh
268
280
  추가로 JupyterLab 옵션을 넘기고 싶다면, 스크립트 뒤에 그대로 붙이면 됩니다.
269
281
 
270
282
  ```bash
271
- bash run_jupyterlab_codex.sh --ServerApp.port=8888
283
+ bash run_dev.sh --ServerApp.port=8888
272
284
  ```
273
285
 
274
286
  ### 수동 설치(개발/로컬)
@@ -9,10 +9,22 @@ set -euo pipefail
9
9
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
10
  cd "$ROOT_DIR"
11
11
 
12
- echo "[0/6] Checking Python/Jupyter prerequisites"
12
+ PYTHON_BIN="${PYTHON_BIN:-}"
13
+ if [ -z "$PYTHON_BIN" ]; then
14
+ if command -v python >/dev/null 2>&1; then
15
+ PYTHON_BIN="python"
16
+ elif command -v python3 >/dev/null 2>&1; then
17
+ PYTHON_BIN="python3"
18
+ else
19
+ echo "ERROR: python not found on PATH (also tried python3)."
20
+ exit 1
21
+ fi
22
+ fi
23
+
24
+ echo "[1/5] Checking Python/Jupyter prerequisites"
13
25
 
14
- if ! command -v python >/dev/null 2>&1; then
15
- echo "ERROR: python not found on PATH"
26
+ if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
27
+ echo "ERROR: $PYTHON_BIN not found on PATH"
16
28
  exit 1
17
29
  fi
18
30
 
@@ -21,9 +33,9 @@ if ! command -v node >/dev/null 2>&1; then
21
33
  exit 1
22
34
  fi
23
35
 
24
- if ! python -c 'import jupyterlab' >/dev/null 2>&1; then
36
+ if ! "$PYTHON_BIN" -c 'import jupyterlab' >/dev/null 2>&1; then
25
37
  echo "JupyterLab not found in this Python environment. Installing (jupyterlab>=4,<5; jupyter_server>=2,<3)..."
26
- python -m pip install -q "jupyterlab>=4,<5" "jupyter_server>=2,<3"
38
+ "$PYTHON_BIN" -m pip install -q "jupyterlab>=4,<5" "jupyter_server>=2,<3"
27
39
  fi
28
40
 
29
41
  JLPM="jlpm"
@@ -33,21 +45,22 @@ fi
33
45
 
34
46
  if ! command -v jupyter >/dev/null 2>&1; then
35
47
  echo "ERROR: jupyter command not found (expected after installing jupyterlab)."
36
- echo "Try: python -m pip install \"jupyterlab>=4,<5\""
48
+ echo "Try: $PYTHON_BIN -m pip install \"jupyterlab>=4,<5\""
37
49
  exit 1
38
50
  fi
39
51
 
40
- echo "[1/6] Installing JS dependencies"
52
+ echo "[2/5] Installing JS dependencies"
41
53
  $JLPM install
42
54
 
43
- echo "[2/6] Building JupyterLab extension"
55
+ echo "[3/5] Building JupyterLab extension"
44
56
  $JLPM run build
45
57
 
46
- echo "[3/6] Installing Python package (editable)"
47
- python -m pip install -e "$ROOT_DIR" --no-deps
58
+ echo "[4/5] Installing Python package (editable)"
59
+ "$PYTHON_BIN" -m pip install -e "$ROOT_DIR" --no-deps
60
+
61
+ echo "[5/5] Enabling server extension and linking labextension"
48
62
 
49
- echo "[4/6] Enabling server extension"
50
- PREFIX="${CONDA_PREFIX:-$(python -c 'import sys; print(sys.prefix)')}"
63
+ PREFIX="${CONDA_PREFIX:-$("$PYTHON_BIN" -c 'import sys; print(sys.prefix)')}"
51
64
 
52
65
  JUPYTER_CFG_DIR="$PREFIX/etc/jupyter/jupyter_server_config.d"
53
66
  mkdir -p "$JUPYTER_CFG_DIR"
@@ -58,12 +71,5 @@ jupyter server extension list | sed -n '1,120p' || true
58
71
 
59
72
  LABEXT_DIR="$PREFIX/share/jupyter/labextensions"
60
73
  mkdir -p "$LABEXT_DIR"
61
-
62
- echo "[5/6] Linking labextension into $LABEXT_DIR"
63
74
  ln -sfn "$ROOT_DIR/jupyterlab_codex/labextension" "$LABEXT_DIR/jupyterlab-codex-sidebar"
64
-
65
- echo "[6/6] Current labextension status"
66
75
  jupyter labextension list
67
-
68
- echo "Starting JupyterLab..."
69
- exec jupyter lab --no-browser --ServerApp.open_browser=False "$@"
@@ -28,9 +28,7 @@ def load_cli_defaults_for_ui() -> Dict[str, Any]:
28
28
  effective_model = env_model or (config_model.strip() if config_model else None)
29
29
  effective_reasoning = (config_reasoning or "").strip().lower() or None
30
30
 
31
- # UI only supports these levels today.
32
- if effective_reasoning not in {"low", "medium", "high", "xhigh"}:
33
- effective_reasoning = None
31
+ effective_reasoning = effective_reasoning or None
34
32
 
35
33
  return {
36
34
  "model": effective_model,
@@ -107,4 +105,3 @@ def _parse_toml_scalar(value: str) -> Any:
107
105
  # Keep this simple; config values we care about are typically plain strings.
108
106
  return value[1:-1]
109
107
  return value
110
-
@@ -54,6 +54,20 @@ def _coerce_command_path(value: Any) -> str:
54
54
  return value.strip()
55
55
 
56
56
 
57
+ def _coerce_session_context_key(value: Any) -> str:
58
+ if not isinstance(value, str):
59
+ return ""
60
+ return value.strip()
61
+
62
+
63
+ def _coerce_bool_flag(value: Any) -> bool:
64
+ if isinstance(value, bool):
65
+ return value
66
+ if isinstance(value, str):
67
+ return value.strip().lower() in {"1", "true", "y", "yes", "on"}
68
+ return False
69
+
70
+
57
71
  def _build_command_not_found_hint(requested_path: str) -> dict[str, str]:
58
72
  requested_label = requested_path or "codex"
59
73
  detected = shutil.which("codex")
@@ -85,6 +99,7 @@ class CodexWSHandler(WebSocketHandler):
85
99
  def open(self):
86
100
  self.write_message(json.dumps({"type": "status", "state": "ready"}))
87
101
  self._send_cli_defaults()
102
+ self._send_model_catalog()
88
103
  self._send_rate_limits_snapshot()
89
104
 
90
105
  async def on_message(self, message: str):
@@ -104,6 +119,14 @@ class CodexWSHandler(WebSocketHandler):
104
119
  await self._handle_send(payload)
105
120
  return
106
121
 
122
+ if msg_type == "delete_session":
123
+ self._handle_delete_session(payload)
124
+ return
125
+
126
+ if msg_type == "delete_all_sessions":
127
+ self._handle_delete_all_sessions(payload)
128
+ return
129
+
107
130
  if msg_type == "cancel":
108
131
  await self._handle_cancel(payload)
109
132
  return
@@ -129,14 +152,60 @@ class CodexWSHandler(WebSocketHandler):
129
152
  except Exception:
130
153
  return
131
154
 
155
+ def _send_model_catalog(self) -> None:
156
+ async def _send() -> None:
157
+ models = await self._runner.list_available_models()
158
+ if not models:
159
+ return
160
+ try:
161
+ self.write_message(json.dumps({"type": "cli_defaults", "availableModels": models}))
162
+ except Exception:
163
+ return
164
+
165
+ try:
166
+ asyncio.create_task(_send())
167
+ except Exception:
168
+ return
169
+
132
170
  async def _handle_start_session(self, payload: Dict[str, Any]):
133
- session_id = payload.get("sessionId") or str(uuid.uuid4())
171
+ requested_session_id = payload.get("sessionId") or ""
172
+ if not isinstance(requested_session_id, str):
173
+ requested_session_id = str(requested_session_id)
174
+ requested_session_id = requested_session_id.strip()
175
+ force_new_thread = _coerce_bool_flag(payload.get("forceNewThread"))
134
176
  notebook_path = payload.get("notebookPath", "")
177
+ if not isinstance(notebook_path, str):
178
+ notebook_path = str(notebook_path)
179
+ notebook_path = notebook_path.strip()
180
+ session_context_key = _coerce_session_context_key(payload.get("sessionContextKey"))
135
181
  notebook_os_path = self._resolve_notebook_os_path(notebook_path)
136
182
 
137
- self._store.ensure_session(session_id, notebook_path, notebook_os_path)
183
+ resolved_session_id = requested_session_id
184
+ if force_new_thread:
185
+ previous_session_id = self._store.resolve_session_for_notebook(notebook_path, notebook_os_path)
186
+ if previous_session_id and previous_session_id != resolved_session_id:
187
+ self._store.delete_session(previous_session_id)
188
+ if not resolved_session_id:
189
+ resolved_session_id = str(uuid.uuid4())
190
+ else:
191
+ resolved_session_id = self._store.resolve_session_for_notebook(notebook_path, notebook_os_path) or resolved_session_id
192
+ if not resolved_session_id:
193
+ resolved_session_id = str(uuid.uuid4())
194
+
195
+ self._store.ensure_session(resolved_session_id, notebook_path, notebook_os_path)
138
196
  if notebook_path:
139
- self._store.update_notebook_path(session_id, notebook_path, notebook_os_path)
197
+ self._store.update_notebook_path(resolved_session_id, notebook_path, notebook_os_path)
198
+
199
+ raw_history = self._store.load_messages(resolved_session_id)
200
+ history = []
201
+ for item in raw_history:
202
+ role = item.get("role")
203
+ content = item.get("content")
204
+ if role not in {"user", "assistant", "system"}:
205
+ continue
206
+ if not isinstance(content, str):
207
+ continue
208
+ history.append({"role": role, "content": content})
140
209
 
141
210
  paired_ok, paired_path, paired_os_path, paired_message = _compute_pairing_status(
142
211
  notebook_path, notebook_os_path
@@ -146,8 +215,10 @@ class CodexWSHandler(WebSocketHandler):
146
215
  {
147
216
  "type": "status",
148
217
  "state": "ready",
149
- "sessionId": session_id,
218
+ "sessionId": resolved_session_id,
150
219
  "notebookPath": notebook_path,
220
+ "sessionContextKey": session_context_key,
221
+ "history": history,
151
222
  "pairedOk": paired_ok,
152
223
  "pairedPath": paired_path,
153
224
  "pairedOsPath": paired_os_path,
@@ -157,8 +228,12 @@ class CodexWSHandler(WebSocketHandler):
157
228
  )
158
229
 
159
230
  async def _handle_send(self, payload: Dict[str, Any]):
160
- session_id = payload.get("sessionId") or str(uuid.uuid4())
231
+ session_id = payload.get("sessionId") or ""
232
+ if not isinstance(session_id, str):
233
+ session_id = str(session_id)
234
+ session_id = session_id.strip() or str(uuid.uuid4())
161
235
  content = payload.get("content", "")
236
+ session_context_key = _coerce_session_context_key(payload.get("sessionContextKey"))
162
237
  selection = payload.get("selection", "")
163
238
  cell_output = payload.get("cellOutput", "")
164
239
  images_payload = payload.get("images")
@@ -217,9 +292,9 @@ class CodexWSHandler(WebSocketHandler):
217
292
  "type": "error",
218
293
  "runId": run_id,
219
294
  "sessionId": session_id,
295
+ "sessionContextKey": session_context_key,
220
296
  "notebookPath": notebook_path,
221
- "message": paired_message
222
- or "Jupytext paired file is required for this extension.",
297
+ "message": paired_message or "Jupytext paired file is required for this extension.",
223
298
  "pairedOk": paired_ok,
224
299
  "pairedPath": paired_path,
225
300
  "pairedOsPath": paired_os_path,
@@ -234,6 +309,7 @@ class CodexWSHandler(WebSocketHandler):
234
309
  "state": "ready",
235
310
  "runId": run_id,
236
311
  "sessionId": session_id,
312
+ "sessionContextKey": session_context_key,
237
313
  "notebookPath": notebook_path,
238
314
  "pairedOk": paired_ok,
239
315
  "pairedPath": paired_path,
@@ -267,6 +343,7 @@ class CodexWSHandler(WebSocketHandler):
267
343
  "state": "running",
268
344
  "runId": run_id,
269
345
  "sessionId": session_id,
346
+ "sessionContextKey": session_context_key,
270
347
  "notebookPath": notebook_path,
271
348
  "pairedOk": paired_ok,
272
349
  "pairedPath": paired_path,
@@ -290,6 +367,7 @@ class CodexWSHandler(WebSocketHandler):
290
367
  "type": "output",
291
368
  "runId": run_id,
292
369
  "sessionId": session_id,
370
+ "sessionContextKey": session_context_key,
293
371
  "notebookPath": notebook_path,
294
372
  "text": text,
295
373
  }
@@ -306,6 +384,7 @@ class CodexWSHandler(WebSocketHandler):
306
384
  "type": "event",
307
385
  "runId": run_id,
308
386
  "sessionId": session_id,
387
+ "sessionContextKey": session_context_key,
309
388
  "notebookPath": notebook_path,
310
389
  "payload": event,
311
390
  }
@@ -348,6 +427,7 @@ class CodexWSHandler(WebSocketHandler):
348
427
  "type": "done",
349
428
  "runId": run_id,
350
429
  "sessionId": session_id,
430
+ "sessionContextKey": session_context_key,
351
431
  "notebookPath": notebook_path,
352
432
  "exitCode": exit_code,
353
433
  "fileChanged": file_changed,
@@ -365,6 +445,7 @@ class CodexWSHandler(WebSocketHandler):
365
445
  "state": "ready",
366
446
  "runId": run_id,
367
447
  "sessionId": session_id,
448
+ "sessionContextKey": session_context_key,
368
449
  "notebookPath": notebook_path,
369
450
  "pairedOk": paired_ok,
370
451
  "pairedPath": paired_path,
@@ -381,6 +462,7 @@ class CodexWSHandler(WebSocketHandler):
381
462
  "type": "done",
382
463
  "runId": run_id,
383
464
  "sessionId": session_id,
465
+ "sessionContextKey": session_context_key,
384
466
  "notebookPath": notebook_path,
385
467
  "exitCode": None,
386
468
  "cancelled": True,
@@ -399,6 +481,7 @@ class CodexWSHandler(WebSocketHandler):
399
481
  "state": "ready",
400
482
  "runId": run_id,
401
483
  "sessionId": session_id,
484
+ "sessionContextKey": session_context_key,
402
485
  "notebookPath": notebook_path,
403
486
  "pairedOk": paired_ok,
404
487
  "pairedPath": paired_path,
@@ -414,6 +497,7 @@ class CodexWSHandler(WebSocketHandler):
414
497
  "type": "error",
415
498
  "runId": run_id,
416
499
  "sessionId": session_id,
500
+ "sessionContextKey": session_context_key,
417
501
  "notebookPath": notebook_path,
418
502
  "message": hint["message"],
419
503
  "pairedOk": paired_ok,
@@ -431,6 +515,7 @@ class CodexWSHandler(WebSocketHandler):
431
515
  "state": "ready",
432
516
  "runId": run_id,
433
517
  "sessionId": session_id,
518
+ "sessionContextKey": session_context_key,
434
519
  "notebookPath": notebook_path,
435
520
  "pairedOk": paired_ok,
436
521
  "pairedPath": paired_path,
@@ -446,6 +531,7 @@ class CodexWSHandler(WebSocketHandler):
446
531
  "type": "error",
447
532
  "runId": run_id,
448
533
  "sessionId": session_id,
534
+ "sessionContextKey": session_context_key,
449
535
  "notebookPath": notebook_path,
450
536
  "message": str(exc),
451
537
  }
@@ -458,6 +544,7 @@ class CodexWSHandler(WebSocketHandler):
458
544
  "state": "ready",
459
545
  "runId": run_id,
460
546
  "sessionId": session_id,
547
+ "sessionContextKey": session_context_key,
461
548
  "notebookPath": notebook_path,
462
549
  "pairedOk": paired_ok,
463
550
  "pairedPath": paired_path,
@@ -479,8 +566,44 @@ class CodexWSHandler(WebSocketHandler):
479
566
  "task": task,
480
567
  "sessionId": session_id,
481
568
  "notebookPath": notebook_path,
569
+ "sessionContextKey": session_context_key,
482
570
  }
483
571
 
572
+ def _handle_delete_session(self, payload: Dict[str, Any]) -> None:
573
+ session_id = payload.get("sessionId")
574
+ if not isinstance(session_id, str):
575
+ session_id = str(session_id) if session_id else ""
576
+ session_id = session_id.strip()
577
+ if session_id:
578
+ self._store.delete_session(session_id)
579
+
580
+ def _handle_delete_all_sessions(self, payload: Dict[str, Any]) -> None:
581
+ del payload
582
+ response: Dict[str, Any] = {
583
+ "type": "delete_all_sessions",
584
+ "ok": False,
585
+ "deletedCount": 0,
586
+ "failedCount": 0,
587
+ "message": "Unknown error"
588
+ }
589
+ try:
590
+ deleted_count, failed_count = self._store.delete_all_sessions()
591
+ response["deletedCount"] = deleted_count
592
+ response["failedCount"] = failed_count
593
+ response["ok"] = failed_count == 0
594
+ response["message"] = (
595
+ f"Deleted {deleted_count} conversations" if deleted_count else "No conversations found to delete"
596
+ )
597
+ if failed_count:
598
+ response["message"] = f"Deleted {deleted_count} conversations, failed to delete {failed_count}"
599
+ except Exception as exc: # pragma: no cover - defensive path
600
+ response["message"] = str(exc)
601
+
602
+ try:
603
+ self.write_message(json.dumps(response))
604
+ except Exception:
605
+ return
606
+
484
607
  def _send_rate_limits_snapshot(self, force: bool = False) -> None:
485
608
  try:
486
609
  snapshot = load_latest_rate_limits(force=force)
@@ -503,6 +626,7 @@ class CodexWSHandler(WebSocketHandler):
503
626
 
504
627
  task = run_context["task"]
505
628
  task.cancel()
629
+ session_context_key = _coerce_session_context_key(run_context.get("sessionContextKey"))
506
630
  self.write_message(
507
631
  json.dumps(
508
632
  {
@@ -510,6 +634,7 @@ class CodexWSHandler(WebSocketHandler):
510
634
  "state": "ready",
511
635
  "runId": run_id,
512
636
  "sessionId": run_context["sessionId"],
637
+ "sessionContextKey": session_context_key,
513
638
  "notebookPath": run_context["notebookPath"],
514
639
  }
515
640
  )
@@ -820,7 +945,7 @@ def _sanitize_reasoning_effort(value: Any) -> str | None:
820
945
  effort = value.strip().lower()
821
946
  if not effort:
822
947
  return None
823
- if effort not in {"none", "minimal", "low", "medium", "high", "xhigh"}:
948
+ if not re.fullmatch(r"[a-z][a-z0-9._-]*", effort):
824
949
  return None
825
950
 
826
951
  return effort
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab-codex-sidebar",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "author": "ILHO AHN",
5
5
  "description": "Codex sidebar for JupyterLab",
6
6
  "license": "BSD-3-Clause",
@@ -37,7 +37,9 @@
37
37
  "@lumino/widgets": "^2.0.0",
38
38
  "dompurify": "^3.3.1",
39
39
  "highlight.js": "^11.11.1",
40
+ "katex": "^0.16.28",
40
41
  "marked": "^16.4.2",
42
+ "marked-katex-extension": "^5.1.7",
41
43
  "react": "^18.2.0",
42
44
  "react-dom": "^18.2.0"
43
45
  },
@@ -52,7 +54,7 @@
52
54
  "extension": true,
53
55
  "outputDir": "jupyterlab_codex/labextension",
54
56
  "_build": {
55
- "load": "static/remoteEntry.00c43c61b0b2e7773f34.js",
57
+ "load": "static/remoteEntry.b2fdc03a1c4582e79156.js",
56
58
  "extension": "./extension"
57
59
  }
58
60
  }
@@ -0,0 +1 @@
1
+ "use strict";(self.webpackChunkjupyterlab_codex_sidebar=self.webpackChunkjupyterlab_codex_sidebar||[]).push([[124],{8124(e,n,t){t.r(n),t.d(n,{default:()=>s});var r=t(4502);const a=/^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/,i=/^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/,l=/^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;function s(e={}){return{extensions:[d(e,o(e,!1)),(n=o(e,!0),{name:"blockKatex",level:"block",tokenizer(e,n){const t=e.match(l);if(t)return{type:"blockKatex",raw:t[0],text:t[2].trim(),displayMode:2===t[1].length}},renderer:n})]};var n}function o(e,n){return t=>r.default.renderToString(t.text,{...e,displayMode:t.displayMode})+(n?"\n":"")}function d(e,n){const t=e&&e.nonStandard,r=t?i:a;return{name:"inlineKatex",level:"inline",start(e){let n,a=e;for(;a;){if(n=a.indexOf("$"),-1===n)return;if((t?n>-1:0===n||" "===a.charAt(n-1))&&a.substring(n).match(r))return n;a=a.substring(n+1).replace(/^\$+/,"")}},tokenizer(e,n){const t=e.match(r);if(t)return{type:"inlineKatex",raw:t[0],text:t[2].trim(),displayMode:2===t[1].length}},renderer:n}}}}]);