python-codex 0.2.3__py3-none-any.whl → 0.2.5__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.
- pycodex/context.py +4 -18
- pycodex/interactive_session.py +15 -1
- pycodex/model.py +153 -11
- pycodex/model_metadata.py +31 -0
- pycodex/prompts/models.json +334 -0
- pycodex/tools/unified_exec_manager.py +32 -1
- pycodex/utils/compactor.py +5 -87
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/METADATA +16 -4
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/RECORD +14 -13
- workspace_server/app.py +57 -1
- workspace_server/workspace.html +44 -1
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/WHEEL +0 -0
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/entry_points.txt +0 -0
- {python_codex-0.2.3.dist-info → python_codex-0.2.5.dist-info}/licenses/LICENSE +0 -0
pycodex/utils/compactor.py
CHANGED
|
@@ -22,6 +22,7 @@ Include:
|
|
|
22
22
|
- Important context, constraints, or user preferences
|
|
23
23
|
- What remains to be done (clear next steps)
|
|
24
24
|
- Any critical data, examples, or references needed to continue
|
|
25
|
+
- Preserve concise verbatim excerpts of the latest user request and active constraints in their original language, and write the summary in the user's primary language.
|
|
25
26
|
|
|
26
27
|
Be concise, structured, and focused on helping the next LLM seamlessly continue the work."""
|
|
27
28
|
|
|
@@ -31,15 +32,10 @@ SUMMARY_PREFIX = (
|
|
|
31
32
|
"that were used by that language model. Use this to build on the work that "
|
|
32
33
|
"has already been done and avoid duplicating work. Here is the summary "
|
|
33
34
|
"produced by the other language model, use the information in this summary "
|
|
34
|
-
"to assist with your own analysis
|
|
35
|
+
"to assist with your own analysis. Continue the current task directly; do "
|
|
36
|
+
"not merely repeat or acknowledge the handoff:"
|
|
35
37
|
)
|
|
36
38
|
|
|
37
|
-
COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000
|
|
38
|
-
_APPROX_CHARS_PER_TOKEN = 4
|
|
39
|
-
_SUBAGENT_NOTIFICATION_PREFIX = "<subagent_notification>\n"
|
|
40
|
-
_EXEC_COMMAND_COMPLETED_PREFIX = "<exec_command_completed>\n"
|
|
41
|
-
|
|
42
|
-
|
|
43
39
|
@dataclass(frozen=True)
|
|
44
40
|
class CompactResult:
|
|
45
41
|
history: 'typing.Tuple[ConversationItem, ...]'
|
|
@@ -67,8 +63,7 @@ def compact(
|
|
|
67
63
|
history: 'typing.Sequence[ConversationItem]',
|
|
68
64
|
) -> 'typing.Tuple[ConversationItem, ...]':
|
|
69
65
|
summary_text = _build_summary_message(_last_assistant_message(history))
|
|
70
|
-
|
|
71
|
-
return build_compacted_history(user_messages, summary_text)
|
|
66
|
+
return build_compacted_history(summary_text)
|
|
72
67
|
|
|
73
68
|
|
|
74
69
|
async def compact_agent(
|
|
@@ -159,53 +154,10 @@ def prune_oldest_tool_response(
|
|
|
159
154
|
)
|
|
160
155
|
|
|
161
156
|
|
|
162
|
-
def collect_user_messages(
|
|
163
|
-
history: 'typing.Sequence[ConversationItem]',
|
|
164
|
-
) -> 'typing.Tuple[str, ...]':
|
|
165
|
-
compact_prompt = _normalize_for_compare(DEFAULT_COMPACT_PROMPT)
|
|
166
|
-
collected: 'typing.List[str]' = []
|
|
167
|
-
for item in history:
|
|
168
|
-
if not isinstance(item, UserMessage):
|
|
169
|
-
continue
|
|
170
|
-
if is_summary_message(item.text):
|
|
171
|
-
continue
|
|
172
|
-
if _normalize_for_compare(item.text) == compact_prompt:
|
|
173
|
-
continue
|
|
174
|
-
if _is_synthetic_user_message(item.text):
|
|
175
|
-
continue
|
|
176
|
-
collected.append(item.text)
|
|
177
|
-
return tuple(collected)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def is_summary_message(message: 'str') -> 'bool':
|
|
181
|
-
return message.startswith(f"{SUMMARY_PREFIX}\n")
|
|
182
|
-
|
|
183
|
-
|
|
184
157
|
def build_compacted_history(
|
|
185
|
-
user_messages: 'typing.Sequence[str]',
|
|
186
158
|
summary_text: 'str',
|
|
187
|
-
max_tokens: 'int' = COMPACT_USER_MESSAGE_MAX_TOKENS,
|
|
188
159
|
) -> 'typing.Tuple[ConversationItem, ...]':
|
|
189
|
-
|
|
190
|
-
if max_tokens > 0:
|
|
191
|
-
remaining = max_tokens
|
|
192
|
-
for message in reversed(tuple(user_messages)):
|
|
193
|
-
if remaining <= 0:
|
|
194
|
-
break
|
|
195
|
-
tokens = _approx_token_count(message)
|
|
196
|
-
if tokens <= remaining:
|
|
197
|
-
selected_messages.append(message)
|
|
198
|
-
remaining -= tokens
|
|
199
|
-
continue
|
|
200
|
-
selected_messages.append(_truncate_text_to_tokens(message, remaining))
|
|
201
|
-
break
|
|
202
|
-
selected_messages.reverse()
|
|
203
|
-
|
|
204
|
-
compacted: 'typing.List[ConversationItem]' = [
|
|
205
|
-
UserMessage(text=message) for message in selected_messages
|
|
206
|
-
]
|
|
207
|
-
compacted.append(UserMessage(text=summary_text or _build_summary_message(None)))
|
|
208
|
-
return tuple(compacted)
|
|
160
|
+
return (UserMessage(text=summary_text or _build_summary_message(None)),)
|
|
209
161
|
|
|
210
162
|
|
|
211
163
|
def _last_assistant_message(
|
|
@@ -222,46 +174,12 @@ def _build_summary_message(summary_text: 'typing.Union[str, None]') -> 'str':
|
|
|
222
174
|
return f"{SUMMARY_PREFIX}\n{normalized}"
|
|
223
175
|
|
|
224
176
|
|
|
225
|
-
def _approx_token_count(text: 'str') -> 'int':
|
|
226
|
-
if not text:
|
|
227
|
-
return 0
|
|
228
|
-
return max(1, (len(text) + _APPROX_CHARS_PER_TOKEN - 1) // _APPROX_CHARS_PER_TOKEN)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _truncate_text_to_tokens(text: 'str', max_tokens: 'int') -> 'str':
|
|
232
|
-
if max_tokens <= 0:
|
|
233
|
-
return ""
|
|
234
|
-
max_chars = max(max_tokens, 1) * _APPROX_CHARS_PER_TOKEN
|
|
235
|
-
if len(text) <= max_chars:
|
|
236
|
-
return text
|
|
237
|
-
|
|
238
|
-
removed_tokens = _approx_token_count(text[max_chars:])
|
|
239
|
-
suffix = f"\n...[{removed_tokens} tokens truncated]..."
|
|
240
|
-
available = max_chars - len(suffix)
|
|
241
|
-
if available <= 0:
|
|
242
|
-
return suffix.lstrip()
|
|
243
|
-
return text[:available].rstrip() + suffix
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def _normalize_for_compare(text: 'str') -> 'str':
|
|
247
|
-
return "\n".join(line.rstrip() for line in text.strip().splitlines()).strip()
|
|
248
|
-
|
|
249
|
-
|
|
250
177
|
def _pluralize(noun: 'str', count: 'int') -> 'str':
|
|
251
178
|
if count == 1:
|
|
252
179
|
return noun
|
|
253
180
|
return f"{noun}s"
|
|
254
181
|
|
|
255
182
|
|
|
256
|
-
def _is_synthetic_user_message(text: 'str') -> 'bool':
|
|
257
|
-
return text.startswith(
|
|
258
|
-
(
|
|
259
|
-
_SUBAGENT_NOTIFICATION_PREFIX,
|
|
260
|
-
_EXEC_COMMAND_COMPLETED_PREFIX,
|
|
261
|
-
)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
|
|
265
183
|
def _is_context_length_error(message: 'str') -> 'bool':
|
|
266
184
|
lower = message.lower()
|
|
267
185
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: A minimal Python extraction of Codex's main agent loop
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.6.2
|
|
@@ -179,7 +179,8 @@ Current behavior:
|
|
|
179
179
|
- interactive mode shows a compact event stream for user-visible phases such as
|
|
180
180
|
tool execution and model follow-up after tool results
|
|
181
181
|
- assistant text is printed from streaming deltas directly
|
|
182
|
-
- interactive mode supports `/history`, `/title`, `/model`, `/resume`,
|
|
182
|
+
- interactive mode supports `/history`, `/title`, `/model`, `/resume`, `/compact`,
|
|
183
|
+
and `/fork`
|
|
183
184
|
- `/model <name>` switches the model used by later turns in the current
|
|
184
185
|
interactive session; `/model` shows the current model and available choices
|
|
185
186
|
- `/resume` with no argument lists the currently resumable sessions by their
|
|
@@ -189,9 +190,15 @@ Current behavior:
|
|
|
189
190
|
- `/compact` synthesizes a local handoff summary, replaces the in-memory
|
|
190
191
|
conversation history with the compacted view, and appends a compacted-history
|
|
191
192
|
entry to the rollout so later `/resume` sees the same state
|
|
193
|
+
- `/fork` generates a new model session id while preserving the current history,
|
|
194
|
+
rollout, and workspace tab
|
|
192
195
|
- `model_auto_compact_token_limit = <tokens>` in `config.toml` enables the same
|
|
193
196
|
compaction path automatically when the latest reported usage reaches that
|
|
194
197
|
threshold before a follow-up sampling request or the next user turn
|
|
198
|
+
- `service_tier = "fast"` enables Fast mode for models whose vendored metadata
|
|
199
|
+
advertises the `priority` service tier; pycodex follows upstream Codex by
|
|
200
|
+
sending `service_tier = "priority"` on the Responses request, while
|
|
201
|
+
`service_tier = "default"` or unsupported tiers are omitted
|
|
195
202
|
- if a model request fails with `context_length_exceeded`, pycodex now treats
|
|
196
203
|
the provider-reported requested token count as a failed-request usage sample,
|
|
197
204
|
triggers the same compact path immediately, and retries the request once; if
|
|
@@ -219,8 +226,13 @@ Current behavior:
|
|
|
219
226
|
`delete(name)` controls; omitted names become `workspace-1`, `workspace-2`,
|
|
220
227
|
etc. If `board` is omitted when adding a workspace, pycodex assigns a random
|
|
221
228
|
writable `/tmp/pcws-*.html` board path. Add/delete actions and later
|
|
222
|
-
session-state saves refresh the JSON file.
|
|
223
|
-
|
|
229
|
+
session-state saves refresh the JSON file. Board HTML can reference local
|
|
230
|
+
images beside the board (including nested paths) with relative URLs; only
|
|
231
|
+
`image/*` files contained by the board directory are served.
|
|
232
|
+
Assistant Markdown supports KaTeX formulas with `$...$`, `$$...$$`,
|
|
233
|
+
`\(...\)`, and `\[...\]` delimiters.
|
|
234
|
+
`--password <value>` enables a password-only login page for workspace pages,
|
|
235
|
+
APIs, and websocket connections.
|
|
224
236
|
- steer is enabled by default in interactive mode: normal input goes into the
|
|
225
237
|
runtime steer path, the current request stops at the next safe boundary, and
|
|
226
238
|
later steer text is appended to the next model request's `input` in order;
|
|
@@ -3,12 +3,13 @@ pycodex/agent.py,sha256=HKzWVFRag_Le8LVR1qqfdfVzk23bRsXkykZV8Zq2hLE,21659
|
|
|
3
3
|
pycodex/cli.py,sha256=1wTlt3ccgQqczrWBV3IjUVE0_L2QUlfPZEf6GB-tGZY,20983
|
|
4
4
|
pycodex/collaboration.py,sha256=yQ6pBD-R3ZWR4_FAYQFoS7KF0m4LLD42otXIbPqw2ys,641
|
|
5
5
|
pycodex/compat.py,sha256=l35JE0vGAOCn9NWbWbqwGURGk83HXddXQ5wJIfcG41o,3254
|
|
6
|
-
pycodex/context.py,sha256=
|
|
6
|
+
pycodex/context.py,sha256=NRCJpYpEm76cAK1akid0BvQ-Z0mAvw1ZNboVxjPtm6U,26219
|
|
7
7
|
pycodex/doctor.py,sha256=De3M4hRBJq8ZeqsUJgHz0vitqrH18YugrEnz7oHhTdQ,10572
|
|
8
8
|
pycodex/feishu_card.py,sha256=De6pM--3MfhgGo-WcWfhm-fop5UtzjwKrZ4gI2Lls3w,26198
|
|
9
9
|
pycodex/feishu_link.py,sha256=DDQCYXQXgo_2SpXPyWlK8KYcumNO1-sqFuQDsEMQpTg,16427
|
|
10
|
-
pycodex/interactive_session.py,sha256=
|
|
11
|
-
pycodex/model.py,sha256=
|
|
10
|
+
pycodex/interactive_session.py,sha256=WzOD06d43ckF7SPer12JKzUPIK42Djw4kEjFRdM6_Lw,16420
|
|
11
|
+
pycodex/model.py,sha256=2euikRqEuemeCv0gyyITCyuTdEOTK9yT50HIzd75Vtg,41808
|
|
12
|
+
pycodex/model_metadata.py,sha256=7fHntKSUXhG5eASJi4yw4jU_IyzGbYBp-WE6af19ATA,877
|
|
12
13
|
pycodex/portable.py,sha256=gxl2E2h5uZJbasMEPPs-nyALFPIvX79T2ZYsu6vXZrg,15656
|
|
13
14
|
pycodex/portable_server.py,sha256=6I3pQkWj3e_SFlDXY2mGdCPns1w_3PSxByBV9wv5epI,7331
|
|
14
15
|
pycodex/protocol.py,sha256=4qiEcBQc3d3RZqsIKBjwORsHsQa78cqwvNljtnIuNbM,10795
|
|
@@ -17,7 +18,7 @@ pycodex/runtime_services.py,sha256=6PQMI4MM8F9imijaVKBIz_0ADtoKDcdYs6vQ-c4frtQ,1
|
|
|
17
18
|
pycodex/prompts/collaboration_default.md,sha256=MBTmPuMubeWfZgIeFVj49wwnwD4n_o3fVYAbgWKwu6Q,955
|
|
18
19
|
pycodex/prompts/collaboration_plan.md,sha256=IzjQAA5oHJz-3FmJdOjsJ4LHq6LW1tlEYMoy09n0HKk,8777
|
|
19
20
|
pycodex/prompts/default_base_instructions.md,sha256=D65mcj6bo4CDvVom-D9cbJRJVNquo0NghKt164_fRsg,20923
|
|
20
|
-
pycodex/prompts/models.json,sha256=
|
|
21
|
+
pycodex/prompts/models.json,sha256=Q3xyyqb6_c8_b3RN-0r0x2rpa-RUq5nxiCBtMdE8cZU,458204
|
|
21
22
|
pycodex/prompts/permissions/approval_policy/never.md,sha256=QceTG6wjkaJARjYr0HYV1aPnPcpGcrkRUW-smWRr6MQ,120
|
|
22
23
|
pycodex/prompts/permissions/approval_policy/on_failure.md,sha256=dfJjpXkpO6_ANdCKxbVJ8o4vyLxevrJWfKsGHTqtbkc,289
|
|
23
24
|
pycodex/prompts/permissions/approval_policy/on_request.md,sha256=hVQalzh0FAdkKzw5u-N4H7-LtC9ijVDlYsh3OKsZKzo,3661
|
|
@@ -46,7 +47,7 @@ pycodex/tools/send_input_tool.py,sha256=vRg-f7LI2Sr01j1LeCPKeXto_ROQZ7bcUC5GIjx9
|
|
|
46
47
|
pycodex/tools/shell_command_tool.py,sha256=wUw4lw8VLGIQ-7BIgyEsI0oqjj-Yr-MZn-_VDrnimAw,4202
|
|
47
48
|
pycodex/tools/shell_tool.py,sha256=1m-Tcbn3His4ggyK5ec8Mkg6ihopqTvBd9F6fJlm6m4,4054
|
|
48
49
|
pycodex/tools/spawn_agent_tool.py,sha256=7Me4Frjz7_fTpjMMFCvNmDHPOK2G1qTfXUr2Ugs61bU,6194
|
|
49
|
-
pycodex/tools/unified_exec_manager.py,sha256=
|
|
50
|
+
pycodex/tools/unified_exec_manager.py,sha256=YM0mKGygkprV51iAgt-A2YXJCG8Fyg3wuGGHktXuuQM,13957
|
|
50
51
|
pycodex/tools/update_plan_tool.py,sha256=UsChtCBqI1RnVnPQbByPmD2LMZXMoCwfe6NpGqP8qu0,3174
|
|
51
52
|
pycodex/tools/view_image_tool.py,sha256=2Xu5Vx7djVpz7-IV-LKrDKJjvVJc9AND-MFXp3CWplg,3892
|
|
52
53
|
pycodex/tools/wait_agent_tool.py,sha256=0Uj9-IrXe2dSvOtOMq-RAc0XzaidwFH5q7Mri3BXWyM,3135
|
|
@@ -55,7 +56,7 @@ pycodex/tools/web_search_tool.py,sha256=mhiK_G6VC6K6-01KctmIpsr-BURDF8L4ZJyY6IFG
|
|
|
55
56
|
pycodex/tools/write_stdin_tool.py,sha256=K--XMp-AyEdp8yQ9EseYHsK3QrGrK4AgzOyvUk4swmw,3608
|
|
56
57
|
pycodex/utils/__init__.py,sha256=p3jaERPxkimNDhmMIsm4glvKiazf3UMv1X-qoH5Zl3U,963
|
|
57
58
|
pycodex/utils/async_bridge.py,sha256=d21Pjim-nsQbSG5pJddd0WaQ03CzA3w3TINWDmmjWbg,1815
|
|
58
|
-
pycodex/utils/compactor.py,sha256=
|
|
59
|
+
pycodex/utils/compactor.py,sha256=REmxBha4UH7EODDhd9oKl0GsVsqujHCJ_2LUKR_R1qY,6524
|
|
59
60
|
pycodex/utils/debug.py,sha256=JeEB5JfzYfbdG0fXlrWFmXyR1ts86fKsI_97IqgF6R0,296
|
|
60
61
|
pycodex/utils/dotenv.py,sha256=rGKmurHjm7GdP4giyjHBPpSPv2Oi45qBqDB6HG3CnfA,1866
|
|
61
62
|
pycodex/utils/get_env.py,sha256=5fNhcNhujOakWV6AS66rGW3jEA68WGpuE4YVXJZFE6U,7427
|
|
@@ -79,12 +80,12 @@ responses_server/tools/custom_adapter.py,sha256=LxO7ldydvR-GWachDz8GKC0Q8KGGFoFP
|
|
|
79
80
|
responses_server/tools/web_search.py,sha256=pm4ZUiHUfxc0bGY1kEvt-BCzDrZIyP24xzPUcga2ul0,8908
|
|
80
81
|
workspace_server/__init__.py,sha256=PRM4ONODb6hxjBUHkBD4TprE_pkTCIlT9sfftK5ic6M,866
|
|
81
82
|
workspace_server/__main__.py,sha256=9SRp-Yw7ShGxc6DhSIXcDLKgGEdAVm3oBZ59rBOPjT0,62
|
|
82
|
-
workspace_server/app.py,sha256=
|
|
83
|
-
workspace_server/workspace.html,sha256=
|
|
83
|
+
workspace_server/app.py,sha256=20lTENFBlercCzFmtAR4fIeMvqIZr8Gq_J2VotmNPck,56858
|
|
84
|
+
workspace_server/workspace.html,sha256=xEcb5tVFM_5Ol6VFp1KzT3D95D8CBse5YyUkAxHugsM,42109
|
|
84
85
|
workspace_server/workspaces.html,sha256=GuYlFiOm1RJINrCKncClHt24D5i7XNABiP9C0xGxBA0,15679
|
|
85
86
|
workspace_server/workspaces.py,sha256=Z-zoj0xgkOOP2CwbQl3vAI6TwLIDUInJKyobs0Jt3ps,19962
|
|
86
|
-
python_codex-0.2.
|
|
87
|
-
python_codex-0.2.
|
|
88
|
-
python_codex-0.2.
|
|
89
|
-
python_codex-0.2.
|
|
90
|
-
python_codex-0.2.
|
|
87
|
+
python_codex-0.2.5.dist-info/METADATA,sha256=EbfCi71Or5nayWpWlbT5jnE1rEcmxmj-wvXxG6UOaZE,18852
|
|
88
|
+
python_codex-0.2.5.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
89
|
+
python_codex-0.2.5.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
|
|
90
|
+
python_codex-0.2.5.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
|
|
91
|
+
python_codex-0.2.5.dist-info/RECORD,,
|
workspace_server/app.py
CHANGED
|
@@ -2,6 +2,7 @@ import argparse
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import html
|
|
4
4
|
import json
|
|
5
|
+
import mimetypes
|
|
5
6
|
import os
|
|
6
7
|
import secrets
|
|
7
8
|
import threading
|
|
@@ -13,7 +14,13 @@ except ImportError: # pragma: no cover - Python 3.6 compatibility
|
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
|
|
16
|
-
from fastapi.responses import
|
|
17
|
+
from fastapi.responses import (
|
|
18
|
+
FileResponse,
|
|
19
|
+
HTMLResponse,
|
|
20
|
+
JSONResponse,
|
|
21
|
+
RedirectResponse,
|
|
22
|
+
Response,
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
from pycodex.cli import build_agent, build_cli_queue, build_model, configure_loguru
|
|
19
26
|
from pycodex.interactive_session import run_interactive_session
|
|
@@ -968,6 +975,14 @@ def create_multi_workspace_app(
|
|
|
968
975
|
return
|
|
969
976
|
await _websocket_session_handler(entry.manager, websocket)
|
|
970
977
|
|
|
978
|
+
@app.api_route(
|
|
979
|
+
"/w/{workspace_id}/{asset_path:path}",
|
|
980
|
+
methods=["GET", "HEAD"],
|
|
981
|
+
)
|
|
982
|
+
async def workspace_board_asset(workspace_id: str, asset_path: str) -> Response:
|
|
983
|
+
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
984
|
+
return _board_asset_response(entry.definition.board_path, asset_path)
|
|
985
|
+
|
|
971
986
|
return app
|
|
972
987
|
|
|
973
988
|
|
|
@@ -1181,6 +1196,10 @@ def _install_workspace_routes(
|
|
|
1181
1196
|
return
|
|
1182
1197
|
await _websocket_session_handler(manager, websocket)
|
|
1183
1198
|
|
|
1199
|
+
@app.api_route("/{asset_path:path}", methods=["GET", "HEAD"])
|
|
1200
|
+
async def board_asset(asset_path: str) -> Response:
|
|
1201
|
+
return _board_asset_response(board_path, asset_path)
|
|
1202
|
+
|
|
1184
1203
|
|
|
1185
1204
|
def _board_status_response(board_path: "typing.Union[Path, None]") -> JSONResponse:
|
|
1186
1205
|
if board_path is None or not board_path.is_file():
|
|
@@ -1388,6 +1407,43 @@ def _board_response(board_path: "typing.Union[Path, None]") -> Response:
|
|
|
1388
1407
|
return _html_response(board_path.read_text(encoding="utf-8", errors="replace"))
|
|
1389
1408
|
|
|
1390
1409
|
|
|
1410
|
+
def _board_asset_response(
|
|
1411
|
+
board_path: "typing.Union[Path, None]",
|
|
1412
|
+
asset_path: str,
|
|
1413
|
+
) -> Response:
|
|
1414
|
+
media_type, unused_encoding = mimetypes.guess_type(str(asset_path or ""))
|
|
1415
|
+
del unused_encoding
|
|
1416
|
+
if board_path is None or not media_type or not media_type.startswith("image/"):
|
|
1417
|
+
raise HTTPException(status_code=404, detail="board image not found")
|
|
1418
|
+
|
|
1419
|
+
resolved_asset = None
|
|
1420
|
+
try:
|
|
1421
|
+
board_directory = board_path.parent.resolve()
|
|
1422
|
+
resolved_asset = (board_directory / asset_path).resolve()
|
|
1423
|
+
within_board_directory = (
|
|
1424
|
+
os.path.commonpath([str(board_directory), str(resolved_asset)])
|
|
1425
|
+
== str(board_directory)
|
|
1426
|
+
)
|
|
1427
|
+
except (OSError, RuntimeError, ValueError):
|
|
1428
|
+
within_board_directory = False
|
|
1429
|
+
|
|
1430
|
+
if (
|
|
1431
|
+
not within_board_directory
|
|
1432
|
+
or resolved_asset is None
|
|
1433
|
+
or not resolved_asset.is_file()
|
|
1434
|
+
):
|
|
1435
|
+
raise HTTPException(status_code=404, detail="board image not found")
|
|
1436
|
+
|
|
1437
|
+
return FileResponse(
|
|
1438
|
+
str(resolved_asset),
|
|
1439
|
+
media_type=media_type,
|
|
1440
|
+
headers={
|
|
1441
|
+
"Cache-Control": "no-cache",
|
|
1442
|
+
"X-Content-Type-Options": "nosniff",
|
|
1443
|
+
},
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
|
|
1391
1447
|
def _workspace_entry_or_404(
|
|
1392
1448
|
registry: 'WorkspaceRegistry',
|
|
1393
1449
|
workspace_id: str,
|
workspace_server/workspace.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__WORKSPACE_TITLE__</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
10
|
color-scheme: light;
|
|
@@ -431,6 +432,14 @@
|
|
|
431
432
|
.markdown a:hover {
|
|
432
433
|
text-decoration: underline;
|
|
433
434
|
}
|
|
435
|
+
.markdown .katex-display {
|
|
436
|
+
margin: 0 0 var(--space-4);
|
|
437
|
+
overflow-x: auto;
|
|
438
|
+
overflow-y: hidden;
|
|
439
|
+
}
|
|
440
|
+
.markdown .katex-display:last-child {
|
|
441
|
+
margin-bottom: 0;
|
|
442
|
+
}
|
|
434
443
|
.composer {
|
|
435
444
|
position: relative;
|
|
436
445
|
min-width: 0;
|
|
@@ -641,6 +650,8 @@
|
|
|
641
650
|
</div>
|
|
642
651
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
|
643
652
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
|
|
653
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
|
|
654
|
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
|
|
644
655
|
<script>
|
|
645
656
|
const log = document.getElementById("log");
|
|
646
657
|
const spinner = document.getElementById("spinner");
|
|
@@ -672,6 +683,12 @@
|
|
|
672
683
|
const markdownRenderer = window.markdownit
|
|
673
684
|
? window.markdownit({html: false, linkify: true, breaks: false})
|
|
674
685
|
: null;
|
|
686
|
+
const mathDelimiterPlaceholders = [
|
|
687
|
+
["\\[", "\uE000"],
|
|
688
|
+
["\\]", "\uE001"],
|
|
689
|
+
["\\(", "\uE002"],
|
|
690
|
+
["\\)", "\uE003"],
|
|
691
|
+
];
|
|
675
692
|
|
|
676
693
|
function relativeUrl(path) {
|
|
677
694
|
return new URL(path, window.location.href).toString();
|
|
@@ -708,11 +725,37 @@
|
|
|
708
725
|
root.textContent = source || "";
|
|
709
726
|
return;
|
|
710
727
|
}
|
|
711
|
-
|
|
728
|
+
let markdownSource = String(source || "");
|
|
729
|
+
mathDelimiterPlaceholders.forEach(([delimiter, placeholder]) => {
|
|
730
|
+
markdownSource = markdownSource.split(delimiter).join(placeholder);
|
|
731
|
+
});
|
|
732
|
+
const dirtyHtml = markdownRenderer.render(markdownSource);
|
|
712
733
|
root.innerHTML = window.DOMPurify.sanitize(dirtyHtml, {
|
|
713
734
|
USE_PROFILES: {html: true},
|
|
714
735
|
ADD_ATTR: ["target", "rel"],
|
|
715
736
|
});
|
|
737
|
+
const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
738
|
+
let textNode = textWalker.nextNode();
|
|
739
|
+
while (textNode) {
|
|
740
|
+
let value = textNode.nodeValue || "";
|
|
741
|
+
mathDelimiterPlaceholders.forEach(([delimiter, placeholder]) => {
|
|
742
|
+
value = value.split(placeholder).join(delimiter);
|
|
743
|
+
});
|
|
744
|
+
textNode.nodeValue = value;
|
|
745
|
+
textNode = textWalker.nextNode();
|
|
746
|
+
}
|
|
747
|
+
if (window.renderMathInElement) {
|
|
748
|
+
window.renderMathInElement(root, {
|
|
749
|
+
delimiters: [
|
|
750
|
+
{left: "$$", right: "$$", display: true},
|
|
751
|
+
{left: "\\[", right: "\\]", display: true},
|
|
752
|
+
{left: "\\(", right: "\\)", display: false},
|
|
753
|
+
{left: "$", right: "$", display: false},
|
|
754
|
+
],
|
|
755
|
+
throwOnError: false,
|
|
756
|
+
trust: false,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
716
759
|
root.querySelectorAll("a").forEach((anchor) => {
|
|
717
760
|
anchor.target = "_blank";
|
|
718
761
|
anchor.rel = "noopener noreferrer";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|