python-codex 0.3.2__tar.gz → 0.3.3__tar.gz

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 (158) hide show
  1. python_codex-0.3.3/.gitattributes +2 -0
  2. {python_codex-0.3.2 → python_codex-0.3.3}/AGENTS.md +5 -2
  3. {python_codex-0.3.2 → python_codex-0.3.3}/PKG-INFO +47 -2
  4. {python_codex-0.3.2 → python_codex-0.3.3}/README.md +46 -1
  5. {python_codex-0.3.2 → python_codex-0.3.3}/README_ZH.md +29 -1
  6. python_codex-0.3.3/docs/RELEASE_0.3.3.md +43 -0
  7. {python_codex-0.3.2 → python_codex-0.3.3}/docs/RUNTIME.md +291 -2
  8. {python_codex-0.3.2 → python_codex-0.3.3}/docs/responses_server/README.md +21 -3
  9. {python_codex-0.3.2 → python_codex-0.3.3}/packages/pycodex-ws/pyproject.toml +2 -2
  10. {python_codex-0.3.2 → python_codex-0.3.3}/pyproject.toml +8 -2
  11. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/stream_router.py +17 -13
  12. {python_codex-0.3.2 → python_codex-0.3.3}/tests/TESTS.md +40 -0
  13. {python_codex-0.3.2 → python_codex-0.3.3}/tests/responses_server/fake_chat_completions_server.py +14 -1
  14. {python_codex-0.3.2 → python_codex-0.3.3}/tests/responses_server/test_server.py +39 -2
  15. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_model.py +121 -0
  16. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_workspace_server.py +2 -2
  17. python_codex-0.3.3/tests/test_workspace_views.py +948 -0
  18. python_codex-0.3.3/tests/workspace_content_views.mjs +919 -0
  19. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/app.py +203 -10
  20. python_codex-0.3.3/workspace_server/content_views.LICENSE.txt +470 -0
  21. python_codex-0.3.3/workspace_server/content_views.css +1 -0
  22. python_codex-0.3.3/workspace_server/content_views.mjs +25 -0
  23. python_codex-0.3.3/workspace_server/editor.css +1 -0
  24. python_codex-0.3.3/workspace_server/editor.mjs +1260 -0
  25. python_codex-0.3.3/workspace_server/frontend/.gitignore +1 -0
  26. python_codex-0.3.3/workspace_server/frontend/build.mjs +98 -0
  27. python_codex-0.3.3/workspace_server/frontend/content_views.css +83 -0
  28. python_codex-0.3.3/workspace_server/frontend/content_views.mjs +668 -0
  29. python_codex-0.3.3/workspace_server/frontend/editor.css +50 -0
  30. python_codex-0.3.3/workspace_server/frontend/editor.mjs +342 -0
  31. python_codex-0.3.3/workspace_server/frontend/package-lock.json +541 -0
  32. python_codex-0.3.3/workspace_server/frontend/package.json +17 -0
  33. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/workspace.html +118 -62
  34. python_codex-0.3.3/workspace_server/workspace_views.py +533 -0
  35. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/workspaces.html +2 -2
  36. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/workspaces.py +4 -0
  37. {python_codex-0.3.2 → python_codex-0.3.3}/.github/workflows/publish.yml +0 -0
  38. {python_codex-0.3.2 → python_codex-0.3.3}/.github/workflows/test.yml +0 -0
  39. {python_codex-0.3.2 → python_codex-0.3.3}/.gitignore +0 -0
  40. {python_codex-0.3.2 → python_codex-0.3.3}/LICENSE +0 -0
  41. {python_codex-0.3.2 → python_codex-0.3.3}/docs/ALIGNMENT.md +0 -0
  42. {python_codex-0.3.2 → python_codex-0.3.3}/docs/CONTEXT.md +0 -0
  43. {python_codex-0.3.2 → python_codex-0.3.3}/docs/RELEASE_0.3.0.md +0 -0
  44. {python_codex-0.3.2 → python_codex-0.3.3}/docs/RELEASE_0.3.2.md +0 -0
  45. {python_codex-0.3.2 → python_codex-0.3.3}/packages/pycodex-ws/README.md +0 -0
  46. {python_codex-0.3.2 → python_codex-0.3.3}/packages/pycodex-ws/pycodex_ws_meta/__init__.py +0 -0
  47. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/__init__.py +0 -0
  48. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/__main__.py +0 -0
  49. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/agent.py +0 -0
  50. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/bootstrap.py +0 -0
  51. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/cli.py +0 -0
  52. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/compat.py +0 -0
  53. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/context.py +0 -0
  54. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/doctor.py +0 -0
  55. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/events.py +0 -0
  56. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/feishu_card.py +0 -0
  57. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/feishu_link.py +0 -0
  58. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/model.py +0 -0
  59. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/model_metadata.py +0 -0
  60. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/portable.py +0 -0
  61. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/portable_server.py +0 -0
  62. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/default_base_instructions.md +0 -0
  63. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/models.json +0 -0
  64. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
  65. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
  66. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
  67. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
  68. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
  69. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
  70. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
  71. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
  72. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/protocol.py +0 -0
  73. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/runtime.py +0 -0
  74. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/runtime_services.py +0 -0
  75. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/__init__.py +0 -0
  76. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/agent_tool_schemas.py +0 -0
  77. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/apply_patch_tool.py +0 -0
  78. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/base_tool.py +0 -0
  79. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/clock_tool.py +0 -0
  80. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/close_agent_tool.py +0 -0
  81. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/code_mode_manager.py +0 -0
  82. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/exec_command_tool.py +0 -0
  83. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/exec_runtime.js +0 -0
  84. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/exec_tool.py +0 -0
  85. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/grep_files_tool.py +0 -0
  86. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/ipython_tool.py +0 -0
  87. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/list_dir_tool.py +0 -0
  88. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/read_file_tool.py +0 -0
  89. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/request_permissions_tool.py +0 -0
  90. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/request_user_input_tool.py +0 -0
  91. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/resume_agent_tool.py +0 -0
  92. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/send_input_tool.py +0 -0
  93. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/shell_command_tool.py +0 -0
  94. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/shell_tool.py +0 -0
  95. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/spawn_agent_tool.py +0 -0
  96. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/unified_exec_manager.py +0 -0
  97. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/update_plan_tool.py +0 -0
  98. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/view_image_tool.py +0 -0
  99. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/wait_agent_tool.py +0 -0
  100. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/wait_tool.py +0 -0
  101. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/web_search_tool.py +0 -0
  102. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/tools/write_stdin_tool.py +0 -0
  103. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/__init__.py +0 -0
  104. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/async_bridge.py +0 -0
  105. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/compactor.py +0 -0
  106. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/debug.py +0 -0
  107. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/dotenv.py +0 -0
  108. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/event_helpers.py +0 -0
  109. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/get_env.py +0 -0
  110. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/image_utils.py +0 -0
  111. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/random_ids.py +0 -0
  112. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/session_persist.py +0 -0
  113. {python_codex-0.3.2 → python_codex-0.3.3}/pycodex/utils/truncation.py +0 -0
  114. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/__init__.py +0 -0
  115. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/__main__.py +0 -0
  116. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/app.py +0 -0
  117. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/config.py +0 -0
  118. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/messages_api.py +0 -0
  119. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/payload_processors.py +0 -0
  120. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/server.py +0 -0
  121. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/session_store.py +0 -0
  122. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/tools/__init__.py +0 -0
  123. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/tools/custom_adapter.py +0 -0
  124. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/tools/web_search.py +0 -0
  125. {python_codex-0.3.2 → python_codex-0.3.3}/responses_server/trajectory_dump.py +0 -0
  126. {python_codex-0.3.2 → python_codex-0.3.3}/tests/__init__.py +0 -0
  127. {python_codex-0.3.2 → python_codex-0.3.3}/tests/compare_context_requests.py +0 -0
  128. {python_codex-0.3.2 → python_codex-0.3.3}/tests/compare_steer_request_bodies.py +0 -0
  129. {python_codex-0.3.2 → python_codex-0.3.3}/tests/compare_tool_schemas.py +0 -0
  130. {python_codex-0.3.2 → python_codex-0.3.3}/tests/conftest.py +0 -0
  131. {python_codex-0.3.2 → python_codex-0.3.3}/tests/fake_responses_server.py +0 -0
  132. {python_codex-0.3.2 → python_codex-0.3.3}/tests/fakes.py +0 -0
  133. {python_codex-0.3.2 → python_codex-0.3.3}/tests/responses_server/test_trajectory_dump.py +0 -0
  134. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_agent.py +0 -0
  135. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_agent_state.py +0 -0
  136. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_async_bridge.py +0 -0
  137. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_builtin_tools.py +0 -0
  138. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_cli.py +0 -0
  139. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_compactor.py +0 -0
  140. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_context.py +0 -0
  141. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_doctor.py +0 -0
  142. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_events.py +0 -0
  143. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_fake_responses_server.py +0 -0
  144. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_feishu_card.py +0 -0
  145. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_feishu_link.py +0 -0
  146. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_feishu_oauth.py +0 -0
  147. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_ipython_tool.py +0 -0
  148. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_owned_workspace_server.py +0 -0
  149. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_packaging.py +0 -0
  150. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_portable.py +0 -0
  151. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_py36_syntax.py +0 -0
  152. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_runtime_state.py +0 -0
  153. {python_codex-0.3.2 → python_codex-0.3.3}/tests/test_session_backend.py +0 -0
  154. {python_codex-0.3.2 → python_codex-0.3.3}/tools/feishu_oauth.py +0 -0
  155. {python_codex-0.3.2 → python_codex-0.3.3}/tools/sync_model_prompts.py +0 -0
  156. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/__init__.py +0 -0
  157. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/__main__.py +0 -0
  158. {python_codex-0.3.2 → python_codex-0.3.3}/workspace_server/owned_process.py +0 -0
@@ -0,0 +1,2 @@
1
+ # The generated Monaco bundle preserves significant whitespace in template literals.
2
+ workspace_server/editor.mjs -whitespace
@@ -29,7 +29,7 @@
29
29
  - 保持文件结构简洁:会话命令放在 `runtime.py`,CLI 启动、终端 I/O 和输入循环统一放在 `cli.py`;事件、纯文本 `visualize()`、有状态 `render(display)` 及展示状态/提示规则放在 `events.py`,通用无状态文本/颜色/摘要 helpers 放在 `utils/event_helpers.py`,helpers 不反向依赖事件或前端,不维护展示 handler mapping。每个前端实例独占 `EventDisplay`,事件本身保持冻结且不持有展示状态;CLI 只执行 log/status/prompt 回调,飞书卡片复用同一套无色 render,仅保留会话快照和卡片布局适配。飞书灰框按排队/开始/完成边界保留最近完成的回复,新一轮标记 `(*last turn)`、完成后替换;临时活动/问题单独展示,不把事件日志累积进回复正文,不恢复工具/流式/重试等逐事件展示分支。Web/飞书不接收 ANSI 文本。测试按行为归属维护,展示规则在事件层验证,前端只留必要的输入/展示/传输集成回归。
30
30
  - Runtime 统一通过 `start()` / `close()` 启停,不恢复公开 `shutdown()` / `run_forever()` 双入口或独立关闭 Future。`Agent.accepts_input` 是本地接纳开关,`runtime.accepts_input` 只读取它;Runtime 关闭时把该开关置为 false,后台唤醒因此停止,已接收请求继续排空。关闭不入队特殊请求,不引入 `begin_shutdown` / `draining`。worker 排空后在命令锁内清理子 Agent 和连接,最后由 `Agent.shutdown()` 一次性清理工具。取消关闭调用方不能取消 worker/turn,Web 线程清理异常也必须 stop/join,不重复关闭底层会话。
31
31
  - 交互 CLI 的 Ctrl+C 在 prompt_toolkit 输入边界通过 `c-c` / `<sigint>` 按键绑定以 `EOFError` 正常退出,兼容不支持 `interrupt_exception` 构造参数的 3.0.36,和 `/exit` 共用 `runtime.close()`;不要让输入子 Task 抛出的 `KeyboardInterrupt` 逃到 asyncio runner 后触发全体 Task 清理。单次 Ctrl+C 发起正常退出,接纳关闭事件展示 `[closing]` 等待提示,已有模型/工具/队列继续自然结束,不借此恢复 turn cancellation。关闭等待期间再次 Ctrl+C 是显式强退:CLI 的进程级 SIGINT handler 以状态 130 立即退出,不再进入 asyncio 清理等待;这不改变 Runtime 的正常排空语义,也不保证外部工具进程停止。旧版 prompt_toolkit 会在输入结束时重置 SIGINT,因此关闭边界要重新安装 handler;正常结束或清理报错后恢复原 SIGINT handler。
32
- - Relative images in board HTML resolve to HTTP siblings such as `/w/<workspace-id>/<image>` (or `/<image>` in single-workspace mode), not to `/board/...`. Keep the image fallback routes after concrete workspace/API routes, resolve files from the board's directory, and reject non-`image/*` files or paths that escape that directory.
32
+ - Relative images in board HTML resolve to HTTP siblings such as `/w/<workspace-id>/<image>` (or `/<image>` in single-workspace mode), not to `/board/...`. Keep the image fallback routes after concrete workspace/API routes, resolve files from the board's directory, and reject non-`image/*` files or paths that escape that directory. Canvas and HTML file previews use ordinary same-origin iframes with the same script permissions; the browser sends login cookies when loading resources. Do not copy cookies into HTML or add image-fetch/data-URL preprocessing.
33
33
  - 交互 CLI 的事件流展示优先表达用户可感知的阶段(例如工具开始/完成、模型回看工具结果),不要直接把内部 `iteration` 计数暴露成主要状态文案;`iterations` 应继续保留在 `TurnResult` 等程序化结果里。
34
34
  - 在交互 CLI 里,`stream_error` 表示当前 Responses stream attempt 失败且模型客户端可能马上自动重试;不要在这个事件上 `finish_stream()` 输出当前 assistant delta buffer,否则第一次失败 attempt 的文本和重试成功后的最终回复会重复显示。真正 fatal 的失败仍由 `turn_failed` 走通用 flush,保留 partial 输出。
35
35
  - prompt/context 相关逻辑统一放在 `pycodex/context.py`:`AgentLoop` 只维护真实会话历史;每轮请求前由 `ContextManager` 注入 base instructions、developer message、`AGENTS.md` 指令和 `<environment_context>`,且这些注入项不写回 history。
@@ -49,7 +49,10 @@
49
49
  - Collaboration mode 已按用户要求彻底移除:不保留模式枚举、ContextConfig/ToolContext 模式字段、协作提示模板、`build_agent` 的 `session_mode` 参数或模式门控;不要为了 upstream 对齐重新引入。移除模式不代表启用 `request_user_input`,它的默认不可用返回单独保留。普通 CLI/Web 交互、`update_plan` 和 subagent 与该模式无关,继续保留。
50
50
  - `request_user_input` 保留默认工具注册及参数声明,但调用固定返回 `request_user_input is unavailable in Default mode`,不收集答案、不触发 `input_requested`,有交互 handler 也不启用。工具本身无状态,不注入 request manager,不为此恢复模式系统或增加启用开关。通用 runtime 问答/权限服务继续保留。`ToolResult.success` 仅是本地执行状态,不能序列化到 Responses input;恢复旧 rollout 读到 `success` 时也不得重新发送该字段。
51
51
  - 当前 `exec` / `wait` 已有一个最小 code-mode 实现:底层通过 Node 子进程运行 JavaScript,自带 `text` / `image` / `store` / `load` / `exit` / `notify` / `yield_control` helper,并允许通过 `tools.<name>(...)` 调回本地已注册工具;`web_search` 当前作为 Responses API provider-native tool declaration 暴露给模型,不经过本地 ToolRegistry 执行。
52
- - 除明确记录的 pycodex 扩展(当前为 `clock`)外,我们支持的 tools 必须和原版 Codex 内置 tools 一一对应:名称、定位、参数形状、交互模型、输出语义都应尽量对齐;不要混合多个原版工具的语义做一个“折中工具”。
52
+ - 除明确记录的 pycodex 扩展 `clock` 外,我们支持的 tools 必须和原版 Codex 内置 tools 一一对应:名称、定位、参数形状、交互模型、输出语义都应尽量对齐;不要混合多个原版工具的语义做一个“折中工具”。
53
+ - Workspace 文件/diff 由用户点击 Markdown 链接打开,不再注册 `workspace_display`。普通绝对/相对文件路径兼容 `:行号` / `#L行号`,相对路径基于 workspace cwd;Git 文件用 `git:REV:path`,比较用 `diff:REV:path`、`diff:BASE..HEAD:path`、`diff:REV:path..REV:path` 或 `diff:left..right`,diff 行号用 `#L` / `#R`。Git 路径沿用仓库根目录语义,显式 `./` / `../` 基于 workspace cwd;路径特殊字符编码,比较端点文件名中的字面 `..` 编码成 `%2E%2E`。点击经 `POST api/views/open`,独立于 Agent/Runtime,不产生模型消息或工具结果,空 `--toolset` 仍可使用。同一来源或有序比较复用标签的 id、顺序和阅读状态;身份按规范化路径或 Git 仓库/解析后版本/路径判定,标题保留完整路径和版本。本地文件跟随磁盘,Git 内容固定;来源、最新内容、打开列表及 reveal counter 原子保存在 Board 同名 `.pycodex-views.json`。旧记录无来源时保持固定快照。只有发起点击的页面消费返回的 selection/reveal 并切换焦点,轮询只更新列表与内容,不跟随其他页面打开;关闭不修改源文件或会话。
54
+ - Workspace 代码与 diff 查看器使用只读 Monaco;源码和固定依赖在 `workspace_server/frontend/`,修改后用该目录的 `npm ci` / `npm run build` 更新生成的 JS/CSS 和许可证,不手改构建产物。首屏 `content_views.mjs` / CSS 只含轻量控制层,首次打开源码/diff 才并行加载独立的同源 `editor.mjs` / CSS,等待两者完成后创建编辑器;Canvas/Markdown/HTML/图片预览不加载 Monaco。编辑器保留全部语言高亮及查看器需要的功能,JSON 直接注册 tokenizer 和语言配置,避免语言服务入口间接带回全量功能。diff worker 从内嵌 Blob URL 启动,字体内嵌编辑器 CSS,随 wheel 分发,Python 启动不依赖 Node。静态资源按 Accept-Encoding 协商 gzip,压缩结果按应用实例缓存并随文件变化失效,使用 Vary/ETag/304;不对会话流加压缩,不在 wheel 重复保存压缩副本。`MonacoEnvironment.getWorker` 是库要求的全局 host hook,其余查看器状态按实例维护。只保留活动视图的编辑器和 model,切换时先保存阅读状态再隐藏/销毁;diff 按内容容器宽度调整原实例布局。沿用视图列表的两秒轮询检查已记录路径,按内容 revision 失效缓存;内容更新复用编辑器/model 并保留阅读状态,不切换标签或手机面板。标签按 VS Code 的文件名/次要说明方式展示:优先显示 basename,同名不同路径才补最短可区分目录,Git 版本用 7 位 commit ID 作为次要说明,同一路径比较只显示一次文件名。宽度上限复用会话标签规则,次要说明先截断,完整标题继续用于悬停、存储及 API 回复。内容/会话标签栏将普通竖向鼠标滚轮转换成横向滚动,保留原生横向、Shift/Ctrl 手势。
55
+ - Workspace 整个左侧展示区称为看板,实时 HTML 首标签称为 Canvas;内部 `board` id、配置字段及路由名保持不变。装配提示沿用自然语言介绍 Canvas 文件的位置、修改实时可见及可随时创建/修改,再简要说明回复和 Canvas 中可用的扩展链接、行号后缀和返回 Canvas 的链接;不规定展示策略,也不加入冗长实现细节。`workspace://canvas` 返回现有实时页面;Markdown 和 HTML(含 `.htm`)文件链接默认预览,带行号打开源码,再次点击无行号链接在同一标签回到预览。普通刷新和手动切换保留本页所选模式及各自阅读位置,不重放旧定位;行号和模式不参与标签身份/标题,其他页面的打开不改变本页模式。Canvas 是常驻的同源 HTML iframe,普通 HTML 预览沿用同源权限及浏览器原生资源加载;预览的父页面消息只用于滚动位置恢复并校验 frame 和文档代数,无变化时保留当前实例,切换时释放。Canvas 普通 HTML 导航保持不变,仅接管显式 git/diff/Canvas URI。异步打开/创建/更新/diff 完成时只应用当前请求;Markdown/HTML diff 始终显示代码。窄屏定位 diff 左侧时保留双栏,聚焦/定位右侧恢复自动布局。
53
56
  - 代码风格上不要使用 `*` 定义 keyword-only 参数;接口默认允许位置参数。
54
57
  - runtime 包和会在 import 阶段执行的测试辅助代码需要保持 Python 3.6.2 语法兼容;不要引入 walrus `:=`、`match` 等仅 3.8+/3.10+ 可解析的新语法,哪怕分支在运行时不会走到。
55
58
  - 本仓库使用 `uv`;本地默认没有预装测试依赖,开始工作前先跑 `uv sync --dev`,验证用 `uv run pytest`。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-codex
3
- Version: 0.3.2
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
@@ -2,6 +2,8 @@
2
2
 
3
3
  English README. Chinese version: `README_ZH.md`
4
4
 
5
+ 0.3.3 workspace viewers and reasoning stream fix: [release notes](docs/RELEASE_0.3.3.md).
6
+
5
7
  0.3.2 startup fixes: [release notes](docs/RELEASE_0.3.2.md).
6
8
 
7
9
  0.3.0 release preparation and Python API migration: `docs/RELEASE_0.3.0.md`.
@@ -241,6 +243,45 @@ Current behavior:
241
243
  - with an argv prompt or piped stdin, run a single turn
242
244
  - `pycodex-ws` starts the standalone browser workspace manager and serves each
243
245
  workspace with a board pane and a pycodex session pane
246
+ - the left pane is the board, containing a live HTML Canvas tab and closable read-only file/diff tabs that
247
+ scroll together in one toolbar; a mouse wheel scrolls overflowing tab strips
248
+ horizontally.
249
+ Click Markdown file links such as `[source](src/app.py:42)` or
250
+ `[source](/data/project/app.py#L42)` to open a view. Git-style diff links use
251
+ `[changes](diff:HEAD:src/app.py)` for working changes,
252
+ `[versions](diff:HEAD~1..HEAD:src/app.py)` for two revisions, and
253
+ `[files](diff:old.py..new.py)` for two local files. Append `#L42` or `#R42`
254
+ to a diff link to select a 1-based line on the left or right.
255
+ Views open when the user clicks; model replies alone do not create tabs.
256
+ Reopening the same file or comparison selects its existing tab in that
257
+ browser, including when the line changes. Other pages keep their selection.
258
+ Tabs show filenames first, adding a short directory suffix
259
+ for duplicate names. Git versions use short commit IDs in a secondary label.
260
+ Long labels use the same width limit as session tabs, shortening the secondary
261
+ information before the filename;
262
+ hover titles retain the complete paths and revisions. Text,
263
+ Markdown, HTML, common raster images and text diffs are supported. Monaco
264
+ provides syntax highlighting, find, folding and character-level diffs.
265
+ Its code and styles load only when a code or diff view opens, so Canvas,
266
+ Markdown and HTML previews do not load the editor. Viewer assets use gzip
267
+ when supported and revalidate cached content with ETags.
268
+ Narrow content panes automatically use a unified diff; targeting the left
269
+ side keeps both sides visible. Markdown and HTML links default to previews;
270
+ line targets open their source. Clicking a link without a line returns that
271
+ same tab to preview. Manual tab switches preserve the chosen mode and reading state.
272
+ All views remain
273
+ read-only, with text selection and copying available. Local files update
274
+ automatically, including both sides of file diffs and the working side of
275
+ Git diffs; Git revisions stay fixed. Updates keep the active editor and
276
+ reading state without replaying an earlier line request.
277
+ Canvas is for explanations and custom visuals; source and diffs use links.
278
+ `[Canvas](workspace://canvas)` returns to the live page; an HTML file link opens
279
+ a preview tab, and `page.html:1` opens its source. File links work even with an empty `--toolset`.
280
+ Canvas and HTML previews share the same origin and script permissions;
281
+ the browser loads their resources with the workspace login cookie.
282
+ Open views survive reloads and server restarts. See
283
+ [workspace content views](docs/RUNTIME.md#workspace-content-views) for the
284
+ complete syntax and limits.
244
285
  - interactive mode exits through `/exit`, `/quit`, Ctrl+D on an empty prompt,
245
286
  or a single Ctrl+C; accepted work finishes before cleanup and normal exit.
246
287
  A `[closing]` message explains the wait. Press Ctrl+C again while closing
@@ -343,7 +384,7 @@ Current behavior:
343
384
  time when it expires.
344
385
  - while a background command or clock is pending, the idle status is
345
386
  `idle: sleeping`
346
- - only the active workspace tab shows its close button; closing it restores
387
+ - only the active workspace session tab shows its close button; closing it restores
347
388
  the selected tab's conversation, draft, and scroll position immediately
348
389
  after its snapshot loads
349
390
  - `--vllm-endpoint http://host:port` automatically launches a local
@@ -364,6 +405,10 @@ Current behavior:
364
405
  reasoning, retries the same downstream request once, and only then emits
365
406
  `response.failed` with `type = "model_output_invalid"` if the retry is still
366
407
  reasoning-only
408
+ - compat reasoning fragments are forwarded immediately as
409
+ `response.reasoning_text.delta`, including thinking from `/v1/messages`.
410
+ Continuous reasoning keeps the client's read idle timeout from expiring
411
+ while it waits for assistant text; only finalized reasoning items enter history
367
412
  - malformed JSON in ordinary downstream function-call arguments is detected by
368
413
  `responses_server` and returned as `response.failed` with `type = "server_error"`;
369
414
  the client's existing `stream_max_retries` policy retries the current request
@@ -2,6 +2,8 @@
2
2
 
3
3
  中文 README。English version: `README.md`
4
4
 
5
+ 0.3.3 看板文件/diff 查看器与推理流修复:[发布说明](docs/RELEASE_0.3.3.md)。
6
+
5
7
  0.3.2 启动修复:[发布说明](docs/RELEASE_0.3.2.md)。
6
8
 
7
9
  0.3.0 发版准备与 Python API 迁移说明:`docs/RELEASE_0.3.0.md`。
@@ -260,8 +262,34 @@ pycodex doctor
260
262
  - 当前默认工具集由上游对齐子集和 pycodex 的 `clock` 扩展组成:`shell`、`shell_command`、`exec_command`、`write_stdin`、`clock`、`exec`、`wait`、`web_search`、`update_plan`、`request_user_input`、`request_permissions`、`spawn_agent`、`send_input`、`resume_agent`、`wait_agent`、`close_agent`、`apply_patch`、`grep_files`、`read_file`、`list_dir`、`view_image`
261
263
  - `clock(period_m)` 为当前 Agent session 设置一个周期计时器,传 `null` 取消;每次回复后重新计时,到期后用包含当前时区时间的 `<clock_tick>` 消息唤醒 Agent
262
264
  - 后台命令或 clock 正在等待时,空闲状态统一显示为 `idle: sleeping`
263
- - workspace 只在当前活动 tab 上显示关闭按钮
265
+ - workspace 整个左侧展示区称为看板,包含实时 HTML Canvas 首标签和只读文件/diff 标签;
266
+ 所有标签在同一行共同滚动,溢出时可用鼠标滚轮横向滚动。点击普通 Markdown 文件链接
267
+ `[源码](src/app.py:42)` 或 `[源码](/data/project/app.py#L42)` 即可打开视图。
268
+ diff 使用 Git 风格链接:`[工作区改动](diff:HEAD:src/app.py)`、
269
+ `[两个版本](diff:HEAD~1..HEAD:src/app.py)`、`[两个文件](diff:old.py..new.py)`。
270
+ 只有用户点击后才创建或复用标签,模型输出链接本身不会打开文件。
271
+ 再次打开同一文件或比较会切换到已有标签,保持原有顺序;其他页面的焦点不变。
272
+ diff 后加 `#L42` / `#R42` 可居中并选中左/右侧第 42 行,行号从 1 开始。
273
+ 行号不参与标签身份,改变行号仍复用已有标签;普通刷新不会重放旧定位。
274
+ 标签优先显示文件名,同名文件补充可区分的短目录;Git 版本用短 commit ID 作为次要说明。
275
+ 长标题沿用会话标签的宽度上限,优先截断次要说明,悬停标题保留完整路径和 Git revision。
276
+ 支持文本、Markdown、HTML、PNG/JPEG/GIF/WebP 和文本 diff。
277
+ 代码与 diff 使用 Monaco,支持语法高亮、查找、折叠、字符级差异和差异跳转;
278
+ 编辑器代码和样式在首次打开源码或 diff 时才加载,Canvas、Markdown/HTML 预览
279
+ 不加载 Monaco。查看器静态资源支持 gzip 传输及 ETag 缓存校验。
280
+ 内容区变窄时自动切换单栏 diff,定位左侧行号时保留双栏以显示目标。
281
+ Markdown 和 HTML 不带行号时默认预览,带行号时显示源码;再次点击无行号链接会在
282
+ 同一标签回到预览,手动切换标签保留所选模式和阅读位置。用户可以选中复制,不能在查看器中修改源文件。
283
+ 本地文件自动更新,包括文件 diff 两侧和 Git diff 的工作区一侧;Git 提交版本保持固定。
284
+ 更新复用当前编辑器并保留阅读状态。Canvas 用于导读和自定义图示,源码和 diff 通过链接查看;
285
+ `[Canvas](workspace://canvas)` 返回实时页面,HTML 文件链接打开预览标签,`page.html:1` 查看源码。
286
+ Canvas 与 HTML 预览使用相同的同源权限,资源请求由浏览器自动携带工作区登录 cookie。
287
+ 空 `--toolset` 不影响用户点击文件链接。
288
+ 用户可关闭标签,打开列表和视图在刷新及服务重启
289
+ 后恢复,完整语法和大小限制见 [workspace content views](docs/RUNTIME.md#workspace-content-views)。
290
+ - workspace 右侧会话区只在当前活动 tab 上显示关闭按钮
264
291
  - `--vllm-endpoint http://host:port` 会自动拉起一个本地 `responses_server` compat 层;当 path 为空时会内部补 `/v1`,继续把 `/responses` 请求转到下游 `/v1/chat/completions`。这条本地 compat 路径始终使用标准 Responses request shape,即使所选模型的 metadata 打开了 `responses_lite` 也不会切换成 lite wire format;启动时还会读取下游 `/v1/models`,自动使用返回列表中的最后一个 model id。当前对 `model_provider = "vllm"` 已补上 reasoning 兼容:会把 chat chunk 里的 `reasoning` / `reasoning_content` 翻回 Responses `reasoning` item,并把历史里的 `reasoning` item 回放成下游 assistant message 的 `reasoning` 字段;同时会向 vLLM 请求 streaming usage,并在最终 `response.completed.response.usage` 中回传
292
+ - compat 层会立即将 reasoning 分片转发为 `response.reasoning_text.delta`,也适用于 `/v1/messages` 的 thinking 流。持续到达的思考分片可避免等待正文期间触发客户端读空闲超时;只有最终确认的 reasoning item 才进入历史
265
293
  - `pycodex doctor` 会检查配置、`.env`、API key、DNS、TCP/TLS,以及可选的 live Responses API 请求
266
294
 
267
295
  它目前主要用于:
@@ -0,0 +1,43 @@
1
+ # 0.3.3 release notes
2
+
3
+ Both `python-codex` and `pycodex-ws` use version **0.3.3**. The workspace
4
+ metapackage depends on exactly `python-codex==0.3.3`; publish the core package
5
+ first.
6
+
7
+ ## Workspace file and diff viewers
8
+
9
+ - Open file, Git revision, and diff links from replies or Canvas in persistent
10
+ board tabs. Source and diff views use read-only Monaco editors with line
11
+ navigation and syntax highlighting.
12
+ - Preview Markdown, HTML, and images directly. Add a line number to a Markdown
13
+ or HTML link to open its source; click the link without a line number to return
14
+ to the preview in the same tab. `workspace://canvas` returns to Canvas.
15
+ - Local files refresh as they change, while Git views retain their resolved
16
+ revision. Each browser page keeps its own active tab, preview mode, and reading
17
+ position. Opening a view does not submit a message to the agent.
18
+ - Load the editor only when opening source or a diff. The wheel includes editor
19
+ assets, fonts, and the diff worker, so installed workspaces do not need Node.
20
+ Static assets support cached gzip responses and conditional requests.
21
+
22
+ ## Reasoning stream fix
23
+
24
+ Chat-completions compatibility mode now forwards `reasoning` and
25
+ `reasoning_content` increments immediately as `response.reasoning_text.delta`
26
+ events. Long reasoning phases continue to send data to the client, avoiding
27
+ stream idle timeouts while the backend is still generating.
28
+
29
+ Repeated reasoning aliases are deduplicated. Final reasoning items and the
30
+ existing retry behavior for responses containing only reasoning are preserved.
31
+ Regression coverage includes a client request whose reasoning phase lasts
32
+ longer than its configured stream idle timeout.
33
+
34
+ ## Upgrade
35
+
36
+ ```bash
37
+ python3 -m pip install --upgrade pycodex-ws==0.3.3
38
+ python3 -m pycodex
39
+ python3 -m workspace_server --help
40
+ ```
41
+
42
+ Installing `pycodex-ws==0.3.3` also installs `python-codex==0.3.3`. The import
43
+ path and both console commands remain unchanged.
@@ -163,7 +163,7 @@ under `Diagram source`. Loading or syntax errors leave the source visible.
163
163
  A render that finishes after its conversation view has been replaced is discarded;
164
164
  completed diagrams keep the view at the bottom only if it is already there.
165
165
 
166
- Closing the active workspace tab uses the same browser transition as selecting
166
+ Closing the active workspace session tab uses the same browser transition as selecting
167
167
  another tab: reset that tab's render signature, restore its draft and scroll
168
168
  position, and request its snapshot immediately. A pending poll for the previous
169
169
  tab is aborted; late responses or errors cannot overwrite the selected tab.
@@ -249,7 +249,7 @@ own; unlinking a Feishu card only detaches that frontend. Web submits directly
249
249
  to the backend, including across its worker-thread boundary: it does not run a
250
250
  CLI shell or maintain an extra prompt queue.
251
251
 
252
- Workspace state stores every open tab in order, starting when the tab is created.
252
+ Workspace state stores every open session tab in order, starting when the tab is created.
253
253
  An empty tab stores an empty title and rollout path, so restoring it creates a
254
254
  fresh empty session without writing a rollout. Manual titles and first messages
255
255
  are not prerequisites for keeping a tab across a server restart. Changes to
@@ -438,6 +438,295 @@ completed/failed status, but active execution and pending inputs take precedence
438
438
  when reporting busy. Direct calls and background invokes produce the same events;
439
439
  status observers no longer inspect raw Task results.
440
440
 
441
+ ## Workspace content views
442
+
443
+ The entire left workspace pane is the board. It starts with a permanent Canvas
444
+ tab for live HTML, followed by read-only file/diff tabs. All tabs scroll together horizontally.
445
+ The tab strips hide their native scrollbars so overflowing tabs stay vertically
446
+ aligned with the toolbar buttons while remaining scrollable.
447
+ Both content and session tab strips translate ordinary vertical mouse-wheel
448
+ input into horizontal scrolling. Horizontal trackpad gestures, Shift+wheel and
449
+ Ctrl+wheel retain their browser behavior; wheel input is consumed only when
450
+ the strip actually moves.
451
+ The file/diff toolbar beneath the tabs keeps its complete path labels in a
452
+ horizontal scroll area with no visible scrollbar. It uses the same wheel
453
+ behavior as the tab strips; the action buttons stay outside the scrolling area.
454
+ Canvas keeps its existing live HTML and image routes. Clicking a Markdown file
455
+ or diff link selects the existing tab for that target, or appends a new read-only
456
+ tab when the target is not already open. Rendering a link does not open a view.
457
+ Reusing a tab keeps its ID, order and reading state. On mobile, selecting a view reveals the content pane. Switching
458
+ tabs preserves their scroll positions; closing a view removes that tab
459
+ without changing a source file, the Canvas or a conversation.
460
+
461
+ Content tabs share the session tabs' rounded styling and show a close button
462
+ only on the active content tab. They occupy the same 44-pixel toolbar as the
463
+ workspace back link and mobile pane switch. The Canvas tab's hover title labels
464
+ `Workspace`, `Current dir`, and `Canvas file` on separate lines.
465
+ Diff views start with the two file labels; the opening time, read-only status
466
+ and whether the view follows files are available in the tab's hover title.
467
+ Stored titles, API responses and hover titles retain complete paths and both Git revisions, for example
468
+ `COMMIT_A:src/A.py <-> COMMIT_B:src/A.py` for full commit IDs. A symbolic or
469
+ abbreviated revision also includes its resolved ID, such as `HEAD (COMMIT):src/A.py`,
470
+ so comparisons opened before and after a branch moves remain distinguishable.
471
+ Visible labels follow VS Code's filename/description layout: the basename is
472
+ the main label, with no leading directory. When open files share a name but
473
+ have different paths, a secondary label adds the shortest directory suffix that
474
+ distinguishes them. For example, two `src/app.py` files in different packages
475
+ can show `app.py` with `client/src` and `server/src`. Opening or closing a
476
+ duplicate updates these descriptions without changing tab identity or focus.
477
+ Git versions use seven-character commit IDs in the secondary label; a comparison
478
+ of the same path shows its filename once, followed by `abc1234 ↔ def5678` or
479
+ `abc1234 ↔ working tree`. Different comparison paths retain both filenames and
480
+ any needed directory descriptions. This is browser display formatting only;
481
+ source identity, stored titles and API responses retain their full information.
482
+ Content tabs share the session tabs' 180-pixel maximum width. Secondary text
483
+ shrinks and ends in an ellipsis before the filename loses space; a filename
484
+ that alone exceeds the available width also uses an ellipsis. The complete
485
+ title remains available on hover and as the tab's accessible label.
486
+
487
+ Workspace assembly introduces the Canvas file, explains that edits are shown
488
+ in real time and that the model can create or modify it anytime. It then describes
489
+ the extended file/diff links supported in replies and Canvas HTML, including
490
+ optional line anchors and the link back to the live Canvas.
491
+ Detailed syntax and viewer behavior are documented below. There is no `workspace_display` tool.
492
+ Link clicks use the browser's view API, independently of Agent/Runtime, and add
493
+ no messages or tool results to the conversation. They also work with an empty
494
+ `--toolset`. Internal `board` IDs, configuration keys and HTTP routes keep their
495
+ existing names.
496
+
497
+ Ordinary Markdown file links accept absolute or workspace-relative paths.
498
+ Use Git-style expressions for comparisons:
499
+
500
+ | Markdown link | View |
501
+ | --- | --- |
502
+ | `[source](src/app.py)` | Working file |
503
+ | `[line](src/app.py:42)` or `[line](/data/project/app.py#L42)` | File, line 42 |
504
+ | `[document](README.md)` or `[page](report.html)` | Markdown or HTML preview |
505
+ | `[source](README.md:1)` or `[source](report.html#L1)` | Markdown or HTML source, line 1 |
506
+ | `[version](git:HEAD:src/app.py#L42)` | Git file at a fixed revision, line 42 |
507
+ | `[changes](diff:HEAD:src/app.py)` | HEAD versus the working file |
508
+ | `[versions](diff:HEAD~1..HEAD:src/app.py)` | Two revisions of the same file |
509
+ | `[rename](diff:HEAD~1:old.py..HEAD:new.py)` | Two Git files with different paths |
510
+ | `[files](diff:old.py..new.py)` | Two local files |
511
+ | `[left](diff:HEAD:src/app.py#L12)` | Diff, left line 12 |
512
+ | `[right](diff:HEAD:src/app.py#R42)` | Diff, right line 42 |
513
+ | `[Canvas](workspace://canvas)` | The existing live HTML Canvas |
514
+
515
+ `..` compares the two specified snapshots directly, like `git diff A..B`.
516
+ It does not calculate a merge base. Line numbers start at 1 and select the
517
+ whole line. A diff uses `#L` for its left side and `#R` for its right side;
518
+ plain file links use `:line` or `#Lline`. Zero, negative or out-of-range lines,
519
+ images and missing target files are errors and do not change the selection.
520
+ Line targets do not affect a tab's identity or title.
521
+
522
+ These links work in chat Markdown, rendered Markdown file views and, for the
523
+ explicit `git:`, `diff:` and `workspace://canvas` forms, Canvas HTML anchors.
524
+ Canvas's ordinary HTML navigation keeps its existing behavior. Linking the
525
+ Canvas HTML file path opens a preview tab; adding a line number opens its source.
526
+ `workspace://canvas` selects the existing live page. External URLs, `mailto:` links and plain `#anchor` links keep their browser
527
+ behavior. The renderer preserves recognized view destinations through sanitizing
528
+ without permitting executable URL protocols.
529
+
530
+ Percent-encode literal special characters in paths: space as `%20`, `#` as `%23`,
531
+ `?` as `%3F`, `%` as `%25`, and a literal colon as `%3A`. For example,
532
+ `[source](src/a%20%23%25.py)` opens `src/a #%.py`. In a diff endpoint, encode
533
+ a literal `..` within a filename as `%2E%2E` so it is not a comparison separator.
534
+ Parent-directory components such as `diff:../old.py..../new.py` work directly.
535
+ Links are limited to 8192 characters; malformed percent encoding and control
536
+ characters are rejected.
537
+
538
+ File identities use normalized absolute paths, so relative/absolute spellings
539
+ of the same path reuse a tab. Comparisons use the ordered pair of their sources;
540
+ reversing the sides opens a separate view. Git identities include the repository,
541
+ resolved revision and repository-relative path. Equivalent link forms share
542
+ a tab, while distinct commits remain distinct even when their file contents
543
+ match. Without a line target, a missing file can still select its existing tab
544
+ with the last readable contents and an error. A line request reads current content to
545
+ validate the line before changing the selection.
546
+
547
+ Local paths resolve from the workspace working directory; absolute paths and
548
+ percent-encoded paths with spaces are accepted. Backslashes are preserved for
549
+ Windows paths. Git `revision:path` operands follow Git's object syntax and
550
+ identify blobs; their paths start at the repository root, or at the workspace
551
+ directory when explicitly prefixed with `./` or `../`. One revision compares the
552
+ Git file with its working counterpart; two revisions compare the same repository
553
+ path in both revisions. A file missing on just one side is displayed as empty, including
554
+ an explicitly selected new working file. Missing on both sides is an error.
555
+ Other protocols, multiple comparisons and arbitrary Git flags are rejected.
556
+ Links are parsed into internal operations, never passed to a shell; Git
557
+ content is read by object ID without running diff drivers or text conversions.
558
+
559
+ Local file views follow changes on disk. Both sides of `diff:A..B` update, while
560
+ `diff:REV:file` updates only its working file. Git operands are resolved
561
+ once when opening the view, so a later commit or branch move does not change
562
+ the pinned Git contents. This also applies to comparisons between two revisions.
563
+ Opening the moved reference again resolves its new version and creates a separate
564
+ comparison when that version is not already open.
565
+
566
+ Text is decoded as UTF-8 with replacement for invalid bytes. Markdown and HTML
567
+ files default to previews. Markdown uses the existing sanitized renderer; HTML
568
+ (`.html` / `.htm`, case-insensitive) renders its markup, styles and scripts in an
569
+ ordinary same-origin iframe, just like Canvas. Canvas is the persistent iframe
570
+ for the configured live HTML file; other HTML tabs render their current file or
571
+ Git snapshot through `srcdoc`. Both use the browser's normal resource loading
572
+ and automatically send the workspace login cookie. HTML scripts have the same
573
+ access to the parent page and authenticated workspace APIs as Canvas scripts;
574
+ the login cookie remains `HttpOnly` and is never copied into the HTML.
575
+ Preview host messages only save and restore scrolling, and are checked against the
576
+ active frame and current document generation. Unchanged HTML keeps its current
577
+ frame and interactive state; content changes reload it and restore scrolling.
578
+ Switching away destroys the frame, and returning rebuilds it at the saved position.
579
+ Relative asset URLs still use the workspace HTTP base and existing Board image
580
+ routes; filesystem image paths are not implicitly converted into asset URLs.
581
+ Image `src`, `srcset`, CSS resource URLs and HTML `<base>` URLs keep their native
582
+ browser behavior. The viewer does not fetch or convert HTML images to data URLs.
583
+ The existing Board image routes continue to enforce directory and MIME boundaries.
584
+ Code and SVG files are shown as source, and
585
+ PNG/JPEG/GIF/WebP files are stored as image data URLs. Code and text diffs use
586
+ Monaco with filename-based syntax highlighting, line numbers, native
587
+ find, code folding, selection/copying and optional line wrapping. Unknown
588
+ extensions use plain text. Both editor state and DOM editing are read-only;
589
+ find has no replace controls, and diff views disable the editing gutter menu,
590
+ with no accept/reject or revert controls.
591
+ The editor uses textarea input (`editContext: false`) so `domReadOnly` sets the
592
+ native `readonly` attribute and touching code does not open the mobile keyboard.
593
+ The separate find field remains editable.
594
+ Opening Markdown or HTML with a line number switches that tab to its source
595
+ editor, so line numbers refer to the actual file. An explicit link without a
596
+ line switches the same tab back to preview. Source and preview keep separate
597
+ reading positions. Ordinary content refreshes and manual tab switches preserve
598
+ the current presentation and do not replay line requests. Presentation choices
599
+ belong to that browser page; another page's opens do not change them.
600
+ Markdown/HTML comparisons always use the code diff editor.
601
+
602
+ Monaco's diff worker computes line and character changes from the two complete
603
+ documents, including leading and trailing whitespace changes.
604
+ The native diff editor keeps both sides aligned when
605
+ scrolling or opening find. Arrow buttons and `Alt+Up`/`Alt+Down` navigate
606
+ changes. A `ResizeObserver` on the content container selects split mode at
607
+ 720 pixels or wider and unified mode below that, including splitter changes
608
+ without a window resize. A line request on the original (left) side keeps split
609
+ mode even below that width so the selected line remains visible. Focusing or
610
+ targeting the modified (right) editor restores automatic layout. This setting
611
+ is preserved when switching tabs. Layout changes reuse the same editors and text models.
612
+ Long documents keep their full searchable text while Monaco renders only the
613
+ visible region.
614
+
615
+ Only the active code view owns an editor (or a pair of editors for a split
616
+ diff). Switching tabs captures Monaco's serializable view state plus find and
617
+ wrapping options, then disposes the editor, text models, event subscriptions and
618
+ observer. Capture happens before hiding the pane so scroll positions remain
619
+ available. Returning restores that reading state; closing the tab also drops its
620
+ caches. Late module/editor completions
621
+ cannot replace a newly selected view. Markdown and image previews retain their
622
+ existing rendering and scroll state.
623
+ Content updates reuse that active editor and its models. Monaco's existing worker
624
+ computes text edits, which are applied without flushing the models so folding and
625
+ find decorations survive; the editor restores its reading state afterward.
626
+ Find stays open and editable. Superseded updates and updates to disposed editors
627
+ are discarded. Updates to
628
+ background tabs invalidate their cached content; selecting them fetches the
629
+ latest version. A refresh keeps the current content visible while loading and
630
+ does not select a tab or switch the mobile pane.
631
+ Explicit line requests wait for current file contents and diff computation,
632
+ then select the line, reveal it and focus the target editor. A newer selection
633
+ or a tab switch invalidates an older pending request. Ordinary content refreshes
634
+ and manual tab switches do not replay an earlier line request; another click
635
+ can explicitly reveal even the same line again.
636
+
637
+ Files are limited to 2 MiB each, text to 20,000 lines and raster images to
638
+ 20 megapixels. View storage is limited to 64 MiB per workspace; exceeding a
639
+ limit when opening a view is reported in the browser and does not create or replace a tab.
640
+ If a live file becomes unreadable or invalid, its last readable content remains
641
+ visible with an error. A deleted file view also keeps its last content; a
642
+ deleted side of a diff becomes empty and is marked missing. Recreating or fixing
643
+ the file resumes updates. A storage write or total-budget failure is reported
644
+ to the browser and leaves the previous persisted views intact.
645
+
646
+ `workspace_server/workspace_views.py` owns `WorkspaceViewOpener` and `WorkspaceViewStore`.
647
+ It saves open views, explicit file/Git sources, the latest contents, optional
648
+ line targets and the monotonic reveal counter
649
+ atomically in the Canvas file's sibling `.pycodex-views.json` file, separately from
650
+ conversation state. Sources are checked again after a server restart.
651
+ Earlier saved snapshots without source information remain fixed; reopening
652
+ their file or diff creates a view with explicit sources. Such snapshots are not
653
+ matched by title alone. Saved titles are reconstructed from their full document
654
+ labels without guessing source paths. The store is shared across that workspace's
655
+ session threads; closing a tab is also reflected in other connected browsers.
656
+ Selected-tab state is local to each browser/workspace. A click focuses its target
657
+ only in that page; other pages can discover the tab without changing their
658
+ selection. A new page defaults to Canvas, or restores its saved selection.
659
+
660
+ `POST api/views/open` accepts `{"href": "<file path or URI>"}` and returns
661
+ `{"view": <full snapshot>, "views": <metadata list>}`. Relative local paths use the
662
+ workspace's `work_dir`; single-workspace `create_app(..., work_dir=...)` defaults
663
+ to the process's working directory. The other routes use view IDs:
664
+ `GET api/views` lists metadata, including content/reveal revisions, a live/fixed
665
+ flag and optional `selection` (`document`: zero-based side, `line`: one-based line);
666
+ `GET api/views/<id>` returns the current contents and
667
+ `DELETE api/views/<id>` closes the view. These routes use the existing
668
+ workspace authentication and namespace in both single- and multi-workspace
669
+ apps. The browser polls
670
+ the list every two seconds. The store checks only recorded file paths, using
671
+ file identity, size and modification/change times to detect writes and atomic
672
+ replacements; unchanged files are not reread. No recursive scan or additional
673
+ watcher process is needed. Content/error changes advance the view revision and
674
+ invalidate browser caches. Stale poll/content responses cannot replace a
675
+ newer revision or a different selected view.
676
+ Each successful click advances the target's `reveal_revision`. Only the initiating
677
+ page uses that open response to select a tab and apply its optional line target;
678
+ polling never follows another page's reveal. Newer clicks, manual selection,
679
+ closing a tab or stopping the page invalidate an outstanding open request.
680
+ Late success/failure responses and polls cannot override the user's latest choice.
681
+ Reveals keep the current editor when possible and do not reorder tabs. The
682
+ counter survives closing its latest target and restarting the server. File
683
+ refreshes change only the content `revision` and therefore do not move focus.
684
+ The JS/CSS assets and third-party licenses ship in the wheel and are served
685
+ through concrete routes before the Board image fallback. `content_views.mjs`
686
+ and `content_views.css` contain only the lightweight tabs and preview controller.
687
+ The first code or diff view loads the separate same-origin `editor.mjs` and
688
+ `editor.css` in parallel, and waits for both before creating an editor. Canvas,
689
+ Markdown, HTML and image previews do not request either editor asset. Each
690
+ display retains its loading promise and stylesheet for later code views;
691
+ switching away during loading still invalidates the pending editor creation.
692
+
693
+ The editor bundle includes all language highlighters and its editor/diff worker,
694
+ but registers only the features used by the read-only viewer. JSON uses its
695
+ tokenizer and language configuration directly: the language-service entry point
696
+ would also import the full editor feature set even with diagnostics disabled.
697
+ The worker starts from an embedded Blob URL and the editor CSS embeds the icon
698
+ font. `MonacoEnvironment.getWorker` is the library-required global host hook;
699
+ other viewer state belongs to each content display. No editor, font, worker or
700
+ language package is fetched from a CDN. Python installation and `pycodex-ws`
701
+ startup do not require Node.
702
+
703
+ Only these static viewer assets use negotiated gzip responses. Each application
704
+ instance caches the requested identity/gzip representations, invalidating them
705
+ when the corresponding file's modification time or size changes. Responses use
706
+ `Vary: Accept-Encoding`, representation-specific ETags and `Cache-Control: no-cache`;
707
+ matching conditional requests return 304. HEAD reports the same representation
708
+ headers without a body. Compression does not buffer session traffic, and the
709
+ wheel contains one copy of each asset, without precompressed duplicates. The
710
+ package's Python requirements are unchanged. Monaco does not officially support
711
+ mobile browsers; viewport and touch checks supplement, but do not establish,
712
+ support across real mobile browsers.
713
+
714
+ Frontend sources and exact dependency versions live in
715
+ `workspace_server/frontend/`, with `package-lock.json` fixing transitive
716
+ dependencies. After changing those sources, rebuild the generated assets using
717
+ Node 18+ from the repository root:
718
+
719
+ ```bash
720
+ npm --prefix workspace_server/frontend ci
721
+ npm --prefix workspace_server/frontend run build
722
+ ```
723
+
724
+ The build collects licenses and third-party notices for bundled packages and stages every output
725
+ before replacing the public JS entry. For an isolated preview, pass a staging
726
+ directory with `npm --prefix workspace_server/frontend run build -- /tmp/workspace-assets`.
727
+ The wheel contains the generated assets, without frontend sources, `node_modules`
728
+ or the build toolchain.
729
+
441
730
  ## Observation
442
731
 
443
732
  Runtime events are frozen dataclasses defined together in `pycodex/events.py`,