python-codex 0.3.2__py3-none-any.whl → 0.3.3__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.
- {python_codex-0.3.2.dist-info → python_codex-0.3.3.dist-info}/METADATA +47 -2
- {python_codex-0.3.2.dist-info → python_codex-0.3.3.dist-info}/RECORD +16 -10
- responses_server/stream_router.py +17 -13
- workspace_server/app.py +203 -10
- workspace_server/content_views.LICENSE.txt +470 -0
- workspace_server/content_views.css +1 -0
- workspace_server/content_views.mjs +25 -0
- workspace_server/editor.css +1 -0
- workspace_server/editor.mjs +1260 -0
- workspace_server/workspace.html +118 -62
- workspace_server/workspace_views.py +533 -0
- workspace_server/workspaces.html +2 -2
- workspace_server/workspaces.py +4 -0
- {python_codex-0.3.2.dist-info → python_codex-0.3.3.dist-info}/WHEEL +0 -0
- {python_codex-0.3.2.dist-info → python_codex-0.3.3.dist-info}/entry_points.txt +0 -0
- {python_codex-0.3.2.dist-info → python_codex-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-codex
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -25,6 +25,8 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
English README. Chinese version: `README_ZH.md`
|
|
27
27
|
|
|
28
|
+
0.3.3 workspace viewers and reasoning stream fix: [release notes](docs/RELEASE_0.3.3.md).
|
|
29
|
+
|
|
28
30
|
0.3.2 startup fixes: [release notes](docs/RELEASE_0.3.2.md).
|
|
29
31
|
|
|
30
32
|
0.3.0 release preparation and Python API migration: `docs/RELEASE_0.3.0.md`.
|
|
@@ -264,6 +266,45 @@ Current behavior:
|
|
|
264
266
|
- with an argv prompt or piped stdin, run a single turn
|
|
265
267
|
- `pycodex-ws` starts the standalone browser workspace manager and serves each
|
|
266
268
|
workspace with a board pane and a pycodex session pane
|
|
269
|
+
- the left pane is the board, containing a live HTML Canvas tab and closable read-only file/diff tabs that
|
|
270
|
+
scroll together in one toolbar; a mouse wheel scrolls overflowing tab strips
|
|
271
|
+
horizontally.
|
|
272
|
+
Click Markdown file links such as `[source](src/app.py:42)` or
|
|
273
|
+
`[source](/data/project/app.py#L42)` to open a view. Git-style diff links use
|
|
274
|
+
`[changes](diff:HEAD:src/app.py)` for working changes,
|
|
275
|
+
`[versions](diff:HEAD~1..HEAD:src/app.py)` for two revisions, and
|
|
276
|
+
`[files](diff:old.py..new.py)` for two local files. Append `#L42` or `#R42`
|
|
277
|
+
to a diff link to select a 1-based line on the left or right.
|
|
278
|
+
Views open when the user clicks; model replies alone do not create tabs.
|
|
279
|
+
Reopening the same file or comparison selects its existing tab in that
|
|
280
|
+
browser, including when the line changes. Other pages keep their selection.
|
|
281
|
+
Tabs show filenames first, adding a short directory suffix
|
|
282
|
+
for duplicate names. Git versions use short commit IDs in a secondary label.
|
|
283
|
+
Long labels use the same width limit as session tabs, shortening the secondary
|
|
284
|
+
information before the filename;
|
|
285
|
+
hover titles retain the complete paths and revisions. Text,
|
|
286
|
+
Markdown, HTML, common raster images and text diffs are supported. Monaco
|
|
287
|
+
provides syntax highlighting, find, folding and character-level diffs.
|
|
288
|
+
Its code and styles load only when a code or diff view opens, so Canvas,
|
|
289
|
+
Markdown and HTML previews do not load the editor. Viewer assets use gzip
|
|
290
|
+
when supported and revalidate cached content with ETags.
|
|
291
|
+
Narrow content panes automatically use a unified diff; targeting the left
|
|
292
|
+
side keeps both sides visible. Markdown and HTML links default to previews;
|
|
293
|
+
line targets open their source. Clicking a link without a line returns that
|
|
294
|
+
same tab to preview. Manual tab switches preserve the chosen mode and reading state.
|
|
295
|
+
All views remain
|
|
296
|
+
read-only, with text selection and copying available. Local files update
|
|
297
|
+
automatically, including both sides of file diffs and the working side of
|
|
298
|
+
Git diffs; Git revisions stay fixed. Updates keep the active editor and
|
|
299
|
+
reading state without replaying an earlier line request.
|
|
300
|
+
Canvas is for explanations and custom visuals; source and diffs use links.
|
|
301
|
+
`[Canvas](workspace://canvas)` returns to the live page; an HTML file link opens
|
|
302
|
+
a preview tab, and `page.html:1` opens its source. File links work even with an empty `--toolset`.
|
|
303
|
+
Canvas and HTML previews share the same origin and script permissions;
|
|
304
|
+
the browser loads their resources with the workspace login cookie.
|
|
305
|
+
Open views survive reloads and server restarts. See
|
|
306
|
+
[workspace content views](docs/RUNTIME.md#workspace-content-views) for the
|
|
307
|
+
complete syntax and limits.
|
|
267
308
|
- interactive mode exits through `/exit`, `/quit`, Ctrl+D on an empty prompt,
|
|
268
309
|
or a single Ctrl+C; accepted work finishes before cleanup and normal exit.
|
|
269
310
|
A `[closing]` message explains the wait. Press Ctrl+C again while closing
|
|
@@ -366,7 +407,7 @@ Current behavior:
|
|
|
366
407
|
time when it expires.
|
|
367
408
|
- while a background command or clock is pending, the idle status is
|
|
368
409
|
`idle: sleeping`
|
|
369
|
-
- only the active workspace tab shows its close button; closing it restores
|
|
410
|
+
- only the active workspace session tab shows its close button; closing it restores
|
|
370
411
|
the selected tab's conversation, draft, and scroll position immediately
|
|
371
412
|
after its snapshot loads
|
|
372
413
|
- `--vllm-endpoint http://host:port` automatically launches a local
|
|
@@ -387,6 +428,10 @@ Current behavior:
|
|
|
387
428
|
reasoning, retries the same downstream request once, and only then emits
|
|
388
429
|
`response.failed` with `type = "model_output_invalid"` if the retry is still
|
|
389
430
|
reasoning-only
|
|
431
|
+
- compat reasoning fragments are forwarded immediately as
|
|
432
|
+
`response.reasoning_text.delta`, including thinking from `/v1/messages`.
|
|
433
|
+
Continuous reasoning keeps the client's read idle timeout from expiring
|
|
434
|
+
while it waits for assistant text; only finalized reasoning items enter history
|
|
390
435
|
- malformed JSON in ordinary downstream function-call arguments is detected by
|
|
391
436
|
`responses_server` and returned as `response.failed` with `type = "server_error"`;
|
|
392
437
|
the client's existing `stream_max_retries` policy retries the current request
|
|
@@ -73,20 +73,26 @@ responses_server/messages_api.py,sha256=ialAYTMmN0y-uxQB3Y51ukq-kvkSVO84WVLXaqLs
|
|
|
73
73
|
responses_server/payload_processors.py,sha256=dqPwqHD-FhyiIwjzFEMf2wCtQDxkRyerZzbdqnH1aDE,3561
|
|
74
74
|
responses_server/server.py,sha256=k4DbsOYkaHHcy38uF9u_QXahJC1cplKsTu-MdC3DCIs,2286
|
|
75
75
|
responses_server/session_store.py,sha256=nwGxQL6nUVKnDVeDHz8IzqNQ7dWk8Ybf0URekOHdxTo,1133
|
|
76
|
-
responses_server/stream_router.py,sha256=
|
|
76
|
+
responses_server/stream_router.py,sha256=TQX2-8uOGad3TBJnRu9SOtDaHBHjYwE-WERYz6rcuo8,46274
|
|
77
77
|
responses_server/trajectory_dump.py,sha256=5_Wy6tslszrqx3j1JeP_boyTbzIicKy8No84PzMnmHk,5111
|
|
78
78
|
responses_server/tools/__init__.py,sha256=ivsBSEy0SBUhY-Uea5v1XMLXShkwHdCVl0id-1FwdZg,150
|
|
79
79
|
responses_server/tools/custom_adapter.py,sha256=SnIiNUdomff3APtTriURAC9YCeEdhCDjzGMEYusfUdk,8340
|
|
80
80
|
responses_server/tools/web_search.py,sha256=pnKH9JyTScRehWpZS3HpXRvjWsQvC0QZqxjEOCT4ndg,8936
|
|
81
81
|
workspace_server/__init__.py,sha256=PRM4ONODb6hxjBUHkBD4TprE_pkTCIlT9sfftK5ic6M,866
|
|
82
82
|
workspace_server/__main__.py,sha256=876xZTqhPeDBD2GKKr8pKX33Af8PhFTosAI8bYQsFgQ,61
|
|
83
|
-
workspace_server/app.py,sha256=
|
|
83
|
+
workspace_server/app.py,sha256=aoo5vq5AciEHe3r5gcgt2B349uuWLY6fbFYbOQ2CRAE,67164
|
|
84
|
+
workspace_server/content_views.LICENSE.txt,sha256=PUOceZvrZI9c2LeLVh6wJNv5wwALKoJ8jOOfq-05WmQ,64183
|
|
85
|
+
workspace_server/content_views.css,sha256=n1yPrOZbzNS0phv2JGwaYbK8t81MI2kKe9rc3sY2TRI,1662
|
|
86
|
+
workspace_server/content_views.mjs,sha256=8sbYqAgwYrAE9SMd3bO6jv6DlHwWru1gboID8h2oTLE,16241
|
|
87
|
+
workspace_server/editor.css,sha256=4qBe2ZLqloAFQ2YNyJYvsJelvTHIIQCxpfg4EVTubk8,346450
|
|
88
|
+
workspace_server/editor.mjs,sha256=oZpBsmnA56VIogpbg_NDg8cs4bIQZkuUCtXPJnVLook,4110455
|
|
84
89
|
workspace_server/owned_process.py,sha256=j1cJ89Nm7gS91Zj62kYKabFbb9qhbU7-q1YDsWcVcn8,895
|
|
85
|
-
workspace_server/workspace.html,sha256=
|
|
86
|
-
workspace_server/
|
|
87
|
-
workspace_server/workspaces.
|
|
88
|
-
|
|
89
|
-
python_codex-0.3.
|
|
90
|
-
python_codex-0.3.
|
|
91
|
-
python_codex-0.3.
|
|
92
|
-
python_codex-0.3.
|
|
90
|
+
workspace_server/workspace.html,sha256=r1cBihjnyt8cxjI9xGqTVBs3zboMcoYHemgvwNMpN2A,74728
|
|
91
|
+
workspace_server/workspace_views.py,sha256=acV-SS3Uw-Qy5AI_KV16AowB1R3cbJN4Okk6CDbpPzw,22172
|
|
92
|
+
workspace_server/workspaces.html,sha256=72WEextEEUZeAYCmZ5UcWbg1m7voFezxgL6j48koRuU,15794
|
|
93
|
+
workspace_server/workspaces.py,sha256=hMvGyZtuFshGV3l56AMhGJN0vDOvvVj8iv9-VAJJHhw,22524
|
|
94
|
+
python_codex-0.3.3.dist-info/METADATA,sha256=ucgUraJ7rlCeUstF-FlAE8qiURNuqF-qOw6xKMbYF7M,42715
|
|
95
|
+
python_codex-0.3.3.dist-info/WHEEL,sha256=KGYbc1zXlYddvwxnNty23BeaKzh7YuoSIvIMO4jEhvw,87
|
|
96
|
+
python_codex-0.3.3.dist-info/entry_points.txt,sha256=vkV2UWCtEKvQNMJuPNjt8HyBKiwp83JyqBatrBNGDp8,80
|
|
97
|
+
python_codex-0.3.3.dist-info/licenses/LICENSE,sha256=0X8ifk312hYAORM4hlzg8wVSEXYKNmiPgWlB1YIy2Nw,10926
|
|
98
|
+
python_codex-0.3.3.dist-info/RECORD,,
|
|
@@ -894,19 +894,23 @@ class StreamRouter:
|
|
|
894
894
|
|
|
895
895
|
reasoning = delta.get("reasoning")
|
|
896
896
|
reasoning_content = delta.get("reasoning_content")
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
897
|
+
reasoning_deltas = [reasoning]
|
|
898
|
+
# Some chat providers expose the same delta under both aliases.
|
|
899
|
+
if reasoning_content != reasoning:
|
|
900
|
+
reasoning_deltas.append(reasoning_content)
|
|
901
|
+
for reasoning_delta in reasoning_deltas:
|
|
902
|
+
if isinstance(reasoning_delta, str) and reasoning_delta:
|
|
903
|
+
reasoning_parts.append(reasoning_delta)
|
|
904
|
+
events.append(
|
|
905
|
+
(
|
|
906
|
+
"response.reasoning_text.delta",
|
|
907
|
+
{
|
|
908
|
+
"type": "response.reasoning_text.delta",
|
|
909
|
+
"content_index": 0,
|
|
910
|
+
"delta": reasoning_delta,
|
|
911
|
+
},
|
|
912
|
+
)
|
|
913
|
+
)
|
|
910
914
|
|
|
911
915
|
content = delta.get("content")
|
|
912
916
|
if isinstance(content, str) and content:
|
workspace_server/app.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import asyncio
|
|
3
|
+
import gzip
|
|
4
|
+
import hashlib
|
|
3
5
|
import html
|
|
4
6
|
import json
|
|
5
7
|
import mimetypes
|
|
6
8
|
import os
|
|
7
9
|
import secrets
|
|
10
|
+
import subprocess
|
|
8
11
|
import sys
|
|
9
12
|
import threading
|
|
10
13
|
from dataclasses import asdict, fields, is_dataclass
|
|
@@ -63,6 +66,7 @@ from pycodex.utils.event_helpers import (
|
|
|
63
66
|
shorten_title,
|
|
64
67
|
)
|
|
65
68
|
|
|
69
|
+
from .workspace_views import WorkspaceViewOpener, WorkspaceViewStore
|
|
66
70
|
from .workspaces import (
|
|
67
71
|
WorkspaceDefinition,
|
|
68
72
|
WorkspaceEntry,
|
|
@@ -122,7 +126,7 @@ def build_parser() -> "argparse.ArgumentParser":
|
|
|
122
126
|
"--toolset",
|
|
123
127
|
nargs="*",
|
|
124
128
|
default=None,
|
|
125
|
-
help="Builtin tool names for all sessions
|
|
129
|
+
help="Builtin tool names for all sessions. An empty list disables all tools.",
|
|
126
130
|
)
|
|
127
131
|
parser.add_argument(
|
|
128
132
|
"--timeout-seconds",
|
|
@@ -815,6 +819,7 @@ def create_app(
|
|
|
815
819
|
session_source: "typing.Union[WorkspaceSessionManager, SessionFactory]",
|
|
816
820
|
board_path: "typing.Union[Path, None]",
|
|
817
821
|
password: "typing.Union[str, None]" = None,
|
|
822
|
+
work_dir: "typing.Union[Path, None]" = None,
|
|
818
823
|
) -> FastAPI:
|
|
819
824
|
manager = (
|
|
820
825
|
session_source
|
|
@@ -823,7 +828,7 @@ def create_app(
|
|
|
823
828
|
)
|
|
824
829
|
app = _create_lifespan_app(manager.start, manager.close)
|
|
825
830
|
auth_token = _install_auth(app, password)
|
|
826
|
-
_install_workspace_routes(app, manager, board_path)
|
|
831
|
+
_install_workspace_routes(app, manager, board_path, Path(work_dir or Path.cwd()).resolve())
|
|
827
832
|
app.state.workspace_auth_token = auth_token
|
|
828
833
|
return app
|
|
829
834
|
|
|
@@ -834,6 +839,7 @@ def create_multi_workspace_app(
|
|
|
834
839
|
) -> FastAPI:
|
|
835
840
|
app = _create_lifespan_app(registry.start, registry.close)
|
|
836
841
|
auth_token = _install_auth(app, password)
|
|
842
|
+
view_asset_cache = {}
|
|
837
843
|
|
|
838
844
|
@app.get("/")
|
|
839
845
|
async def index() -> Response:
|
|
@@ -899,6 +905,45 @@ def create_multi_workspace_app(
|
|
|
899
905
|
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
900
906
|
return _board_status_response(entry.definition.board_path)
|
|
901
907
|
|
|
908
|
+
@app.get("/w/{workspace_id}/api/views")
|
|
909
|
+
def workspace_views(workspace_id: str) -> JSONResponse:
|
|
910
|
+
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
911
|
+
return _views_response(entry.manager)
|
|
912
|
+
|
|
913
|
+
@app.post("/w/{workspace_id}/api/views/open")
|
|
914
|
+
def workspace_open_view(
|
|
915
|
+
workspace_id: str, payload: "typing.Dict[str, object]"
|
|
916
|
+
) -> JSONResponse:
|
|
917
|
+
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
918
|
+
return _open_view_response(entry.manager, entry.definition.work_dir, payload)
|
|
919
|
+
|
|
920
|
+
@app.get("/w/{workspace_id}/api/views/{view_id}")
|
|
921
|
+
def workspace_view(workspace_id: str, view_id: str) -> JSONResponse:
|
|
922
|
+
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
923
|
+
return _view_response(entry.manager, view_id)
|
|
924
|
+
|
|
925
|
+
@app.delete("/w/{workspace_id}/api/views/{view_id}")
|
|
926
|
+
def workspace_close_view(workspace_id: str, view_id: str) -> JSONResponse:
|
|
927
|
+
entry = _workspace_entry_or_404(registry, workspace_id)
|
|
928
|
+
return _close_view_response(entry.manager, view_id)
|
|
929
|
+
|
|
930
|
+
@app.api_route(
|
|
931
|
+
"/w/{workspace_id}/assets/content_views.mjs", methods=["GET", "HEAD"]
|
|
932
|
+
)
|
|
933
|
+
@app.api_route(
|
|
934
|
+
"/w/{workspace_id}/assets/content_views.css", methods=["GET", "HEAD"]
|
|
935
|
+
)
|
|
936
|
+
@app.api_route(
|
|
937
|
+
"/w/{workspace_id}/assets/content_views.LICENSE.txt", methods=["GET", "HEAD"]
|
|
938
|
+
)
|
|
939
|
+
@app.api_route("/w/{workspace_id}/assets/editor.mjs", methods=["GET", "HEAD"])
|
|
940
|
+
@app.api_route("/w/{workspace_id}/assets/editor.css", methods=["GET", "HEAD"])
|
|
941
|
+
def workspace_view_asset(workspace_id: str, request: Request) -> Response:
|
|
942
|
+
_workspace_entry_or_404(registry, workspace_id)
|
|
943
|
+
return _view_asset_response(
|
|
944
|
+
request.url.path.rsplit("/", 1)[-1], request, view_asset_cache
|
|
945
|
+
)
|
|
946
|
+
|
|
902
947
|
@app.get("/w/{workspace_id}/ws/session")
|
|
903
948
|
async def workspace_websocket_backend_hint(workspace_id: str) -> JSONResponse:
|
|
904
949
|
_workspace_entry_or_404(registry, workspace_id)
|
|
@@ -1139,10 +1184,13 @@ def _install_workspace_routes(
|
|
|
1139
1184
|
app: FastAPI,
|
|
1140
1185
|
manager: WorkspaceSessionManager,
|
|
1141
1186
|
board_path: "typing.Union[Path, None]",
|
|
1187
|
+
work_dir: Path,
|
|
1142
1188
|
) -> None:
|
|
1189
|
+
view_asset_cache = {}
|
|
1190
|
+
|
|
1143
1191
|
@app.get("/")
|
|
1144
1192
|
async def index() -> HTMLResponse:
|
|
1145
|
-
return _html_response(_render_workspace_shell(board_path))
|
|
1193
|
+
return _html_response(_render_workspace_shell(board_path, work_dir=work_dir))
|
|
1146
1194
|
|
|
1147
1195
|
@app.get("/favicon.ico")
|
|
1148
1196
|
async def favicon() -> Response:
|
|
@@ -1156,6 +1204,32 @@ def _install_workspace_routes(
|
|
|
1156
1204
|
async def board_status() -> JSONResponse:
|
|
1157
1205
|
return _board_status_response(board_path)
|
|
1158
1206
|
|
|
1207
|
+
@app.get("/api/views")
|
|
1208
|
+
def views() -> JSONResponse:
|
|
1209
|
+
return _views_response(manager)
|
|
1210
|
+
|
|
1211
|
+
@app.post("/api/views/open")
|
|
1212
|
+
def open_view(payload: "typing.Dict[str, object]") -> JSONResponse:
|
|
1213
|
+
return _open_view_response(manager, work_dir, payload)
|
|
1214
|
+
|
|
1215
|
+
@app.get("/api/views/{view_id}")
|
|
1216
|
+
def view(view_id: str) -> JSONResponse:
|
|
1217
|
+
return _view_response(manager, view_id)
|
|
1218
|
+
|
|
1219
|
+
@app.delete("/api/views/{view_id}")
|
|
1220
|
+
def close_view(view_id: str) -> JSONResponse:
|
|
1221
|
+
return _close_view_response(manager, view_id)
|
|
1222
|
+
|
|
1223
|
+
@app.api_route("/assets/content_views.mjs", methods=["GET", "HEAD"])
|
|
1224
|
+
@app.api_route("/assets/content_views.css", methods=["GET", "HEAD"])
|
|
1225
|
+
@app.api_route("/assets/content_views.LICENSE.txt", methods=["GET", "HEAD"])
|
|
1226
|
+
@app.api_route("/assets/editor.mjs", methods=["GET", "HEAD"])
|
|
1227
|
+
@app.api_route("/assets/editor.css", methods=["GET", "HEAD"])
|
|
1228
|
+
def view_asset(request: Request) -> Response:
|
|
1229
|
+
return _view_asset_response(
|
|
1230
|
+
request.url.path.rsplit("/", 1)[-1], request, view_asset_cache
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1159
1233
|
@app.get("/ws/session")
|
|
1160
1234
|
async def websocket_backend_hint() -> JSONResponse:
|
|
1161
1235
|
return _websocket_backend_hint_response()
|
|
@@ -1199,6 +1273,117 @@ def _install_workspace_routes(
|
|
|
1199
1273
|
return _board_asset_response(board_path, asset_path)
|
|
1200
1274
|
|
|
1201
1275
|
|
|
1276
|
+
def _views_response(manager: WorkspaceSessionManager) -> JSONResponse:
|
|
1277
|
+
try:
|
|
1278
|
+
views = manager.views.list_views()
|
|
1279
|
+
except (OSError, ValueError) as error:
|
|
1280
|
+
raise HTTPException(status_code=400, detail=str(error))
|
|
1281
|
+
return JSONResponse({"views": views}, headers={"Cache-Control": "no-store"})
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def _open_view_response(
|
|
1285
|
+
manager: WorkspaceSessionManager, work_dir: Path, payload: "typing.Dict[str, object]"
|
|
1286
|
+
) -> JSONResponse:
|
|
1287
|
+
try:
|
|
1288
|
+
view = WorkspaceViewOpener(manager.views, work_dir).open(payload.get("href"))
|
|
1289
|
+
views = manager.views.list_views()
|
|
1290
|
+
except (OSError, ValueError, subprocess.TimeoutExpired) as error:
|
|
1291
|
+
raise HTTPException(status_code=400, detail=str(error))
|
|
1292
|
+
return JSONResponse(
|
|
1293
|
+
{"view": view, "views": views}, headers={"Cache-Control": "no-store"}
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _view_response(manager: WorkspaceSessionManager, view_id: str) -> JSONResponse:
|
|
1298
|
+
try:
|
|
1299
|
+
view = manager.views.get(view_id)
|
|
1300
|
+
except KeyError:
|
|
1301
|
+
raise HTTPException(status_code=404, detail="view not found")
|
|
1302
|
+
except (OSError, ValueError) as error:
|
|
1303
|
+
raise HTTPException(status_code=400, detail=str(error))
|
|
1304
|
+
return JSONResponse(view, headers={"Cache-Control": "no-store"})
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _close_view_response(
|
|
1308
|
+
manager: WorkspaceSessionManager, view_id: str
|
|
1309
|
+
) -> JSONResponse:
|
|
1310
|
+
try:
|
|
1311
|
+
manager.views.close(view_id)
|
|
1312
|
+
except KeyError:
|
|
1313
|
+
raise HTTPException(status_code=404, detail="view not found")
|
|
1314
|
+
return JSONResponse({"ok": True})
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def _view_asset_encoding(accept_encoding: str) -> str:
|
|
1318
|
+
qualities = {}
|
|
1319
|
+
for entry in accept_encoding.lower().split(","):
|
|
1320
|
+
name, *parameters = entry.strip().split(";")
|
|
1321
|
+
quality = 1.0
|
|
1322
|
+
for parameter in parameters:
|
|
1323
|
+
key, _, value = parameter.strip().partition("=")
|
|
1324
|
+
if key != "q":
|
|
1325
|
+
continue
|
|
1326
|
+
try:
|
|
1327
|
+
quality = float(value)
|
|
1328
|
+
except ValueError as exc:
|
|
1329
|
+
raise HTTPException(
|
|
1330
|
+
status_code=400, detail="invalid encoding quality"
|
|
1331
|
+
) from exc
|
|
1332
|
+
if not 0 <= quality <= 1:
|
|
1333
|
+
raise HTTPException(status_code=400, detail="invalid encoding quality")
|
|
1334
|
+
qualities[name.strip()] = quality
|
|
1335
|
+
gzip_quality = qualities.get("gzip", qualities.get("*", 0))
|
|
1336
|
+
if gzip_quality > 0 and gzip_quality >= qualities.get("identity", 0):
|
|
1337
|
+
return "gzip"
|
|
1338
|
+
if qualities.get("identity", 0 if qualities.get("*") == 0 else 1) > 0:
|
|
1339
|
+
return "identity"
|
|
1340
|
+
raise HTTPException(status_code=406, detail="no acceptable asset encoding")
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _view_asset_response(asset_name: str, request: Request, cache: dict) -> Response:
|
|
1344
|
+
media_types = {
|
|
1345
|
+
"content_views.mjs": "text/javascript",
|
|
1346
|
+
"content_views.css": "text/css",
|
|
1347
|
+
"content_views.LICENSE.txt": "text/plain",
|
|
1348
|
+
"editor.mjs": "text/javascript",
|
|
1349
|
+
"editor.css": "text/css",
|
|
1350
|
+
}
|
|
1351
|
+
if asset_name not in media_types:
|
|
1352
|
+
raise HTTPException(status_code=404, detail="asset not found")
|
|
1353
|
+
encoding = _view_asset_encoding(request.headers.get("accept-encoding", ""))
|
|
1354
|
+
path = Path(__file__).with_name(asset_name)
|
|
1355
|
+
stat = path.stat()
|
|
1356
|
+
stamp = (stat.st_mtime_ns, stat.st_size)
|
|
1357
|
+
key = (asset_name, encoding)
|
|
1358
|
+
cached = cache.get(key)
|
|
1359
|
+
if cached is None or cached[0] != stamp:
|
|
1360
|
+
body = path.read_bytes()
|
|
1361
|
+
if encoding == "gzip":
|
|
1362
|
+
body = gzip.compress(body, compresslevel=6)
|
|
1363
|
+
etag = '"' + hashlib.sha256(body).hexdigest() + '"'
|
|
1364
|
+
cached = cache[key] = (stamp, body, etag)
|
|
1365
|
+
_, body, etag = cached
|
|
1366
|
+
headers = {
|
|
1367
|
+
"Cache-Control": "no-cache",
|
|
1368
|
+
"X-Content-Type-Options": "nosniff",
|
|
1369
|
+
"Vary": "Accept-Encoding",
|
|
1370
|
+
"ETag": etag,
|
|
1371
|
+
}
|
|
1372
|
+
if encoding == "gzip":
|
|
1373
|
+
headers["Content-Encoding"] = "gzip"
|
|
1374
|
+
for candidate in request.headers.get("if-none-match", "").split(","):
|
|
1375
|
+
candidate = candidate.strip()
|
|
1376
|
+
if candidate.startswith("W/"):
|
|
1377
|
+
candidate = candidate[2:]
|
|
1378
|
+
if candidate in ("*", etag):
|
|
1379
|
+
return Response(status_code=304, headers=headers)
|
|
1380
|
+
headers["Content-Length"] = str(len(body))
|
|
1381
|
+
return Response(
|
|
1382
|
+
b"" if request.method == "HEAD" else body,
|
|
1383
|
+
media_type=media_types[asset_name], headers=headers,
|
|
1384
|
+
)
|
|
1385
|
+
|
|
1386
|
+
|
|
1202
1387
|
def _board_status_response(board_path: "typing.Union[Path, None]") -> JSONResponse:
|
|
1203
1388
|
if board_path is None or not board_path.is_file():
|
|
1204
1389
|
return JSONResponse({"exists": False})
|
|
@@ -1560,6 +1745,8 @@ def _build_workspace_entry(
|
|
|
1560
1745
|
args: "argparse.Namespace",
|
|
1561
1746
|
persist_callback: "typing.Union[typing.Callable[[], None], None]" = None,
|
|
1562
1747
|
) -> "WorkspaceEntry":
|
|
1748
|
+
views = WorkspaceViewStore(definition.board_path)
|
|
1749
|
+
|
|
1563
1750
|
def build_session() -> "WorkspaceInteractiveSession":
|
|
1564
1751
|
model = build_model(
|
|
1565
1752
|
config_path=args.config,
|
|
@@ -1575,7 +1762,7 @@ def _build_workspace_entry(
|
|
|
1575
1762
|
profile=args.profile,
|
|
1576
1763
|
system_prompt=args.system_prompt,
|
|
1577
1764
|
extra_contextual_user_messages=(
|
|
1578
|
-
[
|
|
1765
|
+
[_workspace_context_text(definition.board_path, definition.work_dir)]
|
|
1579
1766
|
if definition.board_path is not None
|
|
1580
1767
|
else []
|
|
1581
1768
|
),
|
|
@@ -1598,18 +1785,24 @@ def _build_workspace_entry(
|
|
|
1598
1785
|
session_factory,
|
|
1599
1786
|
definition.board_path,
|
|
1600
1787
|
persist_callback=persist_callback,
|
|
1788
|
+
views=views,
|
|
1601
1789
|
),
|
|
1602
1790
|
)
|
|
1603
1791
|
|
|
1604
1792
|
|
|
1605
|
-
def
|
|
1793
|
+
def _workspace_context_text(
|
|
1606
1794
|
board_path: Path,
|
|
1607
|
-
work_dir: "typing.Union[Path, None]"
|
|
1795
|
+
work_dir: "typing.Union[Path, None]",
|
|
1608
1796
|
) -> str:
|
|
1609
1797
|
return (
|
|
1610
|
-
"Current workspace
|
|
1611
|
-
"Changes you make to this file are shown to the user in real time. "
|
|
1612
|
-
"You can create or modify this file anytime
|
|
1798
|
+
"Current workspace Canvas file: {0}. "
|
|
1799
|
+
"Changes you make to this HTML file are shown to the user in real time. "
|
|
1800
|
+
"You can create or modify this file anytime.\n\n"
|
|
1801
|
+
"Replies and Canvas HTML can use extended links to open files and diffs "
|
|
1802
|
+
"in the board: git:REV:path, diff:REV:path, diff:BASE..HEAD:path, "
|
|
1803
|
+
"and diff:left..right. Append #L42 to select a file or left-side line, "
|
|
1804
|
+
"or #R42 for the right side of a diff. "
|
|
1805
|
+
"Use workspace://canvas to return to the live Canvas."
|
|
1613
1806
|
).format(_format_board_path_for_prompt(board_path, work_dir=work_dir))
|
|
1614
1807
|
|
|
1615
1808
|
|
|
@@ -1662,7 +1855,7 @@ def _render_empty_board() -> str:
|
|
|
1662
1855
|
|
|
1663
1856
|
def _render_missing_board(board_path: Path) -> str:
|
|
1664
1857
|
return _render_board_placeholder(
|
|
1665
|
-
"
|
|
1858
|
+
"Canvas pending",
|
|
1666
1859
|
"Your work will appear here",
|
|
1667
1860
|
"Ask pycodex to create a page, a report, or a visual. "
|
|
1668
1861
|
"This canvas updates as you work.",
|