pythinker-code 2.0.0__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.
- pythinker_code/CHANGELOG.md +16 -0
- pythinker_code/__init__.py +0 -0
- pythinker_code/__main__.py +92 -0
- pythinker_code/acp/AGENTS.md +93 -0
- pythinker_code/acp/__init__.py +13 -0
- pythinker_code/acp/convert.py +128 -0
- pythinker_code/acp/host.py +298 -0
- pythinker_code/acp/mcp.py +46 -0
- pythinker_code/acp/server.py +497 -0
- pythinker_code/acp/session.py +496 -0
- pythinker_code/acp/tools.py +167 -0
- pythinker_code/acp/types.py +13 -0
- pythinker_code/acp/version.py +45 -0
- pythinker_code/agents/default/agent.yaml +36 -0
- pythinker_code/agents/default/coder.yaml +25 -0
- pythinker_code/agents/default/explore.yaml +46 -0
- pythinker_code/agents/default/plan.yaml +30 -0
- pythinker_code/agents/default/system.md +164 -0
- pythinker_code/agents/okabe/agent.yaml +22 -0
- pythinker_code/agentspec.py +163 -0
- pythinker_code/app.py +820 -0
- pythinker_code/approval_runtime/__init__.py +29 -0
- pythinker_code/approval_runtime/models.py +42 -0
- pythinker_code/approval_runtime/runtime.py +235 -0
- pythinker_code/auth/__init__.py +25 -0
- pythinker_code/auth/anthropic_direct.py +207 -0
- pythinker_code/auth/deepseek.py +192 -0
- pythinker_code/auth/lm_studio.py +418 -0
- pythinker_code/auth/minimax.py +203 -0
- pythinker_code/auth/oauth.py +1122 -0
- pythinker_code/auth/ollama.py +293 -0
- pythinker_code/auth/openai.py +771 -0
- pythinker_code/auth/opencode_go.py +203 -0
- pythinker_code/auth/openrouter.py +225 -0
- pythinker_code/auth/platforms.py +466 -0
- pythinker_code/background/__init__.py +36 -0
- pythinker_code/background/agent_runner.py +231 -0
- pythinker_code/background/ids.py +19 -0
- pythinker_code/background/manager.py +650 -0
- pythinker_code/background/models.py +105 -0
- pythinker_code/background/store.py +237 -0
- pythinker_code/background/summary.py +66 -0
- pythinker_code/background/worker.py +209 -0
- pythinker_code/cli/__init__.py +1326 -0
- pythinker_code/cli/__main__.py +19 -0
- pythinker_code/cli/_lazy_group.py +238 -0
- pythinker_code/cli/export.py +322 -0
- pythinker_code/cli/info.py +62 -0
- pythinker_code/cli/mcp.py +349 -0
- pythinker_code/cli/plugin.py +351 -0
- pythinker_code/cli/toad.py +74 -0
- pythinker_code/cli/vis.py +38 -0
- pythinker_code/cli/web.py +80 -0
- pythinker_code/config.py +453 -0
- pythinker_code/constant.py +33 -0
- pythinker_code/exception.py +43 -0
- pythinker_code/hooks/__init__.py +4 -0
- pythinker_code/hooks/config.py +34 -0
- pythinker_code/hooks/engine.py +371 -0
- pythinker_code/hooks/events.py +190 -0
- pythinker_code/hooks/runner.py +89 -0
- pythinker_code/llm.py +412 -0
- pythinker_code/metadata.py +79 -0
- pythinker_code/notifications/__init__.py +33 -0
- pythinker_code/notifications/llm.py +77 -0
- pythinker_code/notifications/manager.py +145 -0
- pythinker_code/notifications/models.py +50 -0
- pythinker_code/notifications/notifier.py +41 -0
- pythinker_code/notifications/store.py +118 -0
- pythinker_code/notifications/wire.py +21 -0
- pythinker_code/plugin/__init__.py +124 -0
- pythinker_code/plugin/manager.py +153 -0
- pythinker_code/plugin/tool.py +173 -0
- pythinker_code/prompts/__init__.py +6 -0
- pythinker_code/prompts/compact.md +73 -0
- pythinker_code/prompts/init.md +21 -0
- pythinker_code/py.typed +0 -0
- pythinker_code/session.py +319 -0
- pythinker_code/session_fork.py +325 -0
- pythinker_code/session_state.py +132 -0
- pythinker_code/share.py +14 -0
- pythinker_code/skill/__init__.py +727 -0
- pythinker_code/skill/flow/__init__.py +99 -0
- pythinker_code/skill/flow/d2.py +482 -0
- pythinker_code/skill/flow/mermaid.py +266 -0
- pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
- pythinker_code/skills/skill-creator/SKILL.md +367 -0
- pythinker_code/soul/__init__.py +304 -0
- pythinker_code/soul/agent.py +520 -0
- pythinker_code/soul/approval.py +267 -0
- pythinker_code/soul/btw.py +214 -0
- pythinker_code/soul/compaction.py +189 -0
- pythinker_code/soul/context.py +339 -0
- pythinker_code/soul/denwarenji.py +39 -0
- pythinker_code/soul/dynamic_injection.py +84 -0
- pythinker_code/soul/dynamic_injections/__init__.py +0 -0
- pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
- pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
- pythinker_code/soul/message.py +92 -0
- pythinker_code/soul/pythinkersoul.py +1613 -0
- pythinker_code/soul/slash.py +340 -0
- pythinker_code/soul/toolset.py +788 -0
- pythinker_code/subagents/__init__.py +21 -0
- pythinker_code/subagents/builder.py +42 -0
- pythinker_code/subagents/core.py +86 -0
- pythinker_code/subagents/git_context.py +172 -0
- pythinker_code/subagents/models.py +54 -0
- pythinker_code/subagents/output.py +71 -0
- pythinker_code/subagents/registry.py +28 -0
- pythinker_code/subagents/runner.py +428 -0
- pythinker_code/subagents/store.py +196 -0
- pythinker_code/telemetry/__init__.py +211 -0
- pythinker_code/telemetry/config.py +54 -0
- pythinker_code/telemetry/crash.py +157 -0
- pythinker_code/telemetry/metrics.py +208 -0
- pythinker_code/telemetry/otel.py +240 -0
- pythinker_code/telemetry/sentry.py +167 -0
- pythinker_code/telemetry/sink.py +189 -0
- pythinker_code/tools/AGENTS.md +6 -0
- pythinker_code/tools/__init__.py +105 -0
- pythinker_code/tools/agent/__init__.py +277 -0
- pythinker_code/tools/agent/description.md +41 -0
- pythinker_code/tools/ask_user/__init__.py +159 -0
- pythinker_code/tools/ask_user/description.md +19 -0
- pythinker_code/tools/background/__init__.py +318 -0
- pythinker_code/tools/background/list.md +10 -0
- pythinker_code/tools/background/output.md +11 -0
- pythinker_code/tools/background/stop.md +8 -0
- pythinker_code/tools/display.py +46 -0
- pythinker_code/tools/dmail/__init__.py +38 -0
- pythinker_code/tools/dmail/dmail.md +17 -0
- pythinker_code/tools/file/__init__.py +30 -0
- pythinker_code/tools/file/glob.md +17 -0
- pythinker_code/tools/file/glob.py +160 -0
- pythinker_code/tools/file/grep.md +6 -0
- pythinker_code/tools/file/grep_local.py +589 -0
- pythinker_code/tools/file/plan_mode.py +45 -0
- pythinker_code/tools/file/read.md +16 -0
- pythinker_code/tools/file/read.py +300 -0
- pythinker_code/tools/file/read_media.md +24 -0
- pythinker_code/tools/file/read_media.py +217 -0
- pythinker_code/tools/file/replace.md +7 -0
- pythinker_code/tools/file/replace.py +195 -0
- pythinker_code/tools/file/utils.py +257 -0
- pythinker_code/tools/file/write.md +5 -0
- pythinker_code/tools/file/write.py +177 -0
- pythinker_code/tools/plan/__init__.py +327 -0
- pythinker_code/tools/plan/description.md +29 -0
- pythinker_code/tools/plan/enter.py +190 -0
- pythinker_code/tools/plan/enter_description.md +35 -0
- pythinker_code/tools/plan/heroes.py +277 -0
- pythinker_code/tools/shell/__init__.py +253 -0
- pythinker_code/tools/shell/bash.md +35 -0
- pythinker_code/tools/shell/powershell.md +30 -0
- pythinker_code/tools/test.py +55 -0
- pythinker_code/tools/think/__init__.py +21 -0
- pythinker_code/tools/think/think.md +1 -0
- pythinker_code/tools/todo/__init__.py +168 -0
- pythinker_code/tools/todo/set_todo_list.md +23 -0
- pythinker_code/tools/utils.py +199 -0
- pythinker_code/tools/web/__init__.py +4 -0
- pythinker_code/tools/web/fetch.md +1 -0
- pythinker_code/tools/web/fetch.py +189 -0
- pythinker_code/tools/web/search.md +1 -0
- pythinker_code/tools/web/search.py +163 -0
- pythinker_code/ui/__init__.py +0 -0
- pythinker_code/ui/acp/__init__.py +99 -0
- pythinker_code/ui/print/__init__.py +474 -0
- pythinker_code/ui/print/visualize.py +185 -0
- pythinker_code/ui/shell/__init__.py +1696 -0
- pythinker_code/ui/shell/console.py +109 -0
- pythinker_code/ui/shell/debug.py +190 -0
- pythinker_code/ui/shell/echo.py +17 -0
- pythinker_code/ui/shell/export_import.py +117 -0
- pythinker_code/ui/shell/keyboard.py +300 -0
- pythinker_code/ui/shell/mcp_status.py +113 -0
- pythinker_code/ui/shell/model_picker.py +318 -0
- pythinker_code/ui/shell/oauth.py +272 -0
- pythinker_code/ui/shell/placeholders.py +531 -0
- pythinker_code/ui/shell/prompt.py +2278 -0
- pythinker_code/ui/shell/replay.py +215 -0
- pythinker_code/ui/shell/session_picker.py +227 -0
- pythinker_code/ui/shell/setup.py +212 -0
- pythinker_code/ui/shell/slash.py +898 -0
- pythinker_code/ui/shell/startup.py +32 -0
- pythinker_code/ui/shell/task_browser.py +486 -0
- pythinker_code/ui/shell/update.py +350 -0
- pythinker_code/ui/shell/usage.py +291 -0
- pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
- pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
- pythinker_code/ui/shell/usage_adapters/base.py +72 -0
- pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
- pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
- pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
- pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
- pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
- pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
- pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
- pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
- pythinker_code/ui/shell/usage_render.py +150 -0
- pythinker_code/ui/shell/visualize/__init__.py +165 -0
- pythinker_code/ui/shell/visualize/_approval_panel.py +505 -0
- pythinker_code/ui/shell/visualize/_blocks.py +629 -0
- pythinker_code/ui/shell/visualize/_btw_panel.py +224 -0
- pythinker_code/ui/shell/visualize/_input_router.py +48 -0
- pythinker_code/ui/shell/visualize/_interactive.py +523 -0
- pythinker_code/ui/shell/visualize/_live_view.py +826 -0
- pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
- pythinker_code/ui/theme.py +241 -0
- pythinker_code/usage_ratelimit_cache.py +175 -0
- pythinker_code/utils/__init__.py +0 -0
- pythinker_code/utils/aiohttp.py +24 -0
- pythinker_code/utils/aioqueue.py +72 -0
- pythinker_code/utils/broadcast.py +37 -0
- pythinker_code/utils/changelog.py +108 -0
- pythinker_code/utils/clipboard.py +246 -0
- pythinker_code/utils/datetime.py +64 -0
- pythinker_code/utils/diff.py +135 -0
- pythinker_code/utils/editor.py +91 -0
- pythinker_code/utils/environment.py +73 -0
- pythinker_code/utils/envvar.py +22 -0
- pythinker_code/utils/export.py +696 -0
- pythinker_code/utils/file_filter.py +375 -0
- pythinker_code/utils/frontmatter.py +70 -0
- pythinker_code/utils/io.py +27 -0
- pythinker_code/utils/logging.py +146 -0
- pythinker_code/utils/media_tags.py +29 -0
- pythinker_code/utils/message.py +24 -0
- pythinker_code/utils/path.py +199 -0
- pythinker_code/utils/proctitle.py +33 -0
- pythinker_code/utils/proxy.py +31 -0
- pythinker_code/utils/pyinstaller.py +45 -0
- pythinker_code/utils/rich/__init__.py +33 -0
- pythinker_code/utils/rich/columns.py +99 -0
- pythinker_code/utils/rich/diff_render.py +481 -0
- pythinker_code/utils/rich/markdown.py +900 -0
- pythinker_code/utils/rich/markdown_sample.md +108 -0
- pythinker_code/utils/rich/markdown_sample_short.md +2 -0
- pythinker_code/utils/rich/syntax.py +114 -0
- pythinker_code/utils/sensitive.py +54 -0
- pythinker_code/utils/server.py +121 -0
- pythinker_code/utils/signals.py +43 -0
- pythinker_code/utils/slashcmd.py +124 -0
- pythinker_code/utils/string.py +41 -0
- pythinker_code/utils/subprocess_env.py +73 -0
- pythinker_code/utils/term.py +168 -0
- pythinker_code/utils/typing.py +20 -0
- pythinker_code/vis/__init__.py +0 -0
- pythinker_code/vis/api/__init__.py +5 -0
- pythinker_code/vis/api/sessions.py +687 -0
- pythinker_code/vis/api/statistics.py +209 -0
- pythinker_code/vis/api/system.py +19 -0
- pythinker_code/vis/app.py +175 -0
- pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-D2MTYyJz.js +1 -0
- pythinker_code/vis/static/assets/index-CezafTt_.js +185 -0
- pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
- pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/vis/static/index.html +17 -0
- pythinker_code/web/__init__.py +5 -0
- pythinker_code/web/api/__init__.py +15 -0
- pythinker_code/web/api/config.py +208 -0
- pythinker_code/web/api/open_in.py +199 -0
- pythinker_code/web/api/sessions.py +1225 -0
- pythinker_code/web/app.py +451 -0
- pythinker_code/web/auth.py +191 -0
- pythinker_code/web/models.py +98 -0
- pythinker_code/web/runner/__init__.py +5 -0
- pythinker_code/web/runner/messages.py +57 -0
- pythinker_code/web/runner/process.py +754 -0
- pythinker_code/web/runner/worker.py +97 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- pythinker_code/web/static/assets/_baseUniq--dyU3g5v.js +1 -0
- pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
- pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
- pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
- pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
- pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
- pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
- pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
- pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
- pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
- pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
- pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
- pythinker_code/web/static/assets/arc-DkMjLpYa.js +1 -0
- pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-CHWVaGo9.js +36 -0
- pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
- pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
- pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
- pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
- pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
- pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
- pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
- pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
- pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
- pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
- pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
- pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
- pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
- pythinker_code/web/static/assets/blockDiagram-VD42YOAC-DzdKe497.js +122 -0
- pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
- pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
- pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
- pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-D84Blotg.js +10 -0
- pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
- pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
- pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- pythinker_code/web/static/assets/channel-CllSjjdl.js +1 -0
- pythinker_code/web/static/assets/chunk-4BX2VUAB-C9w8wleE.js +1 -0
- pythinker_code/web/static/assets/chunk-55IACEB6-YlYJ8HnF.js +1 -0
- pythinker_code/web/static/assets/chunk-B4BG7PRW-Bwtz_AHU.js +165 -0
- pythinker_code/web/static/assets/chunk-DI55MBZ5-BbEHkl8h.js +220 -0
- pythinker_code/web/static/assets/chunk-FMBD7UC4-BKPbvjLC.js +15 -0
- pythinker_code/web/static/assets/chunk-QN33PNHL-D73dQvKf.js +1 -0
- pythinker_code/web/static/assets/chunk-QZHKN3VN-zGiLKes_.js +1 -0
- pythinker_code/web/static/assets/chunk-TZMSLE5B-LHJCi2fy.js +1 -0
- pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-vX27iZwa.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-vX27iZwa.js +1 -0
- pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
- pythinker_code/web/static/assets/clone-DYBkaPm2.js +1 -0
- pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
- pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
- pythinker_code/web/static/assets/code-block-IT6T5CEO-NtKViZGl.js +2 -0
- pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
- pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
- pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
- pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
- pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
- pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-DialjZpd.js +1 -0
- pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
- pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
- pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
- pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
- pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
- pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
- pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
- pythinker_code/web/static/assets/cytoscape.esm-C_Fzpdck.js +321 -0
- pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
- pythinker_code/web/static/assets/dagre-6UL2VRFP-DfuvkZZ7.js +4 -0
- pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
- pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
- pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
- pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
- pythinker_code/web/static/assets/diagram-PSM6KHXK-DLGctX3v.js +24 -0
- pythinker_code/web/static/assets/diagram-QEK2KX5R-DnxN6S0F.js +43 -0
- pythinker_code/web/static/assets/diagram-S2PKOQOG-Caq_Set9.js +24 -0
- pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
- pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
- pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
- pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
- pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
- pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
- pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
- pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
- pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
- pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
- pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-BgTfALoK.js +60 -0
- pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
- pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
- pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
- pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
- pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
- pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
- pythinker_code/web/static/assets/flowDiagram-NV44I4VS-QjW_fnGi.js +162 -0
- pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
- pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
- pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
- pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-fqi8JFof.js +267 -0
- pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
- pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
- pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
- pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
- pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
- pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
- pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
- pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-i7o6VQ8x.js +65 -0
- pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
- pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
- pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
- pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
- pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
- pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
- pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
- pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
- pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
- pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
- pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
- pythinker_code/web/static/assets/graph-C0vZK2pT.js +1 -0
- pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
- pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
- pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
- pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
- pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
- pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
- pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
- pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
- pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
- pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
- pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
- pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
- pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
- pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
- pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
- pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
- pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
- pythinker_code/web/static/assets/index-BYCCk6-K.js +153 -0
- pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
- pythinker_code/web/static/assets/index-Cpy4G3uJ.js +2 -0
- pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
- pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
- pythinker_code/web/static/assets/index-DdIkp80K.js +5 -0
- pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-BMPpeZSM.js +2 -0
- pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
- pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
- pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
- pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
- pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
- pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
- pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-DAM7gngo.js +139 -0
- pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
- pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
- pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
- pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
- pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
- pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
- pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
- pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
- pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
- pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-ChpBpV0k.js +89 -0
- pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
- pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
- pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
- pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
- pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
- pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
- pythinker_code/web/static/assets/layout-C3Jp1gKO.js +1 -0
- pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
- pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
- pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
- pythinker_code/web/static/assets/linear-BGHtL1N4.js +1 -0
- pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
- pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
- pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
- pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
- pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
- pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
- pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
- pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
- pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
- pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
- pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
- pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
- pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
- pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
- pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
- pythinker_code/web/static/assets/mermaid-VLURNSYL-C_HW6koB.js +465 -0
- pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
- pythinker_code/web/static/assets/mermaid.core-CnT4VrPC.js +191 -0
- pythinker_code/web/static/assets/min-Dn5VRVmX.js +1 -0
- pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
- pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
- pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-x8EwhfIt.js +68 -0
- pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
- pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
- pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
- pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
- pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
- pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
- pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
- pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
- pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
- pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
- pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
- pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
- pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
- pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
- pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
- pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
- pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
- pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
- pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
- pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
- pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
- pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
- pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-DgxBKGz2.js +30 -0
- pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
- pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
- pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
- pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
- pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
- pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
- pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
- pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
- pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
- pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
- pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
- pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
- pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
- pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
- pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
- pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
- pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
- pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
- pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
- pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DSpe_dqk.js +7 -0
- pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
- pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
- pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
- pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
- pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
- pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
- pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
- pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
- pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-8o9hozL-.js +64 -0
- pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
- pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
- pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
- pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
- pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
- pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-BLOSUixH.js +10 -0
- pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
- pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
- pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
- pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
- pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
- pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
- pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-6F2G8JTU.js +145 -0
- pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
- pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
- pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
- pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
- pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
- pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
- pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
- pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
- pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
- pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
- pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
- pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
- pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
- pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
- pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
- pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DP8xP0iJ.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-1l6-EZNX.js +1 -0
- pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
- pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
- pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
- pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
- pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
- pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
- pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
- pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
- pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
- pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
- pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
- pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
- pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-DMgruDfK.js +61 -0
- pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
- pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
- pythinker_code/web/static/assets/treemap-KMMF4GRG-Bo9ZkrAK.js +128 -0
- pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
- pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
- pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
- pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
- pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
- pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
- pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
- pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
- pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
- pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
- pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
- pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
- pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
- pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
- pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
- pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
- pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
- pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
- pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
- pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
- pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
- pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
- pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
- pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
- pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
- pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
- pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
- pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
- pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
- pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
- pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
- pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-GeF2johi.js +7 -0
- pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
- pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
- pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
- pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
- pythinker_code/web/static/brand/arctecture.webp +0 -0
- pythinker_code/web/static/brand/bimi-logo.svg +46 -0
- pythinker_code/web/static/brand/favicon.ico +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
- pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
- pythinker_code/web/static/brand/icon-192.png +0 -0
- pythinker_code/web/static/brand/icon-512.png +0 -0
- pythinker_code/web/static/brand/icon.svg +1 -0
- pythinker_code/web/static/brand/logo.png +0 -0
- pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
- pythinker_code/web/static/brand/robots.txt +4 -0
- pythinker_code/web/static/index.html +15 -0
- pythinker_code/web/static/logo.png +0 -0
- pythinker_code/web/store/__init__.py +1 -0
- pythinker_code/web/store/sessions.py +432 -0
- pythinker_code/wire/__init__.py +148 -0
- pythinker_code/wire/file.py +151 -0
- pythinker_code/wire/jsonrpc.py +263 -0
- pythinker_code/wire/protocol.py +2 -0
- pythinker_code/wire/root_hub.py +27 -0
- pythinker_code/wire/serde.py +26 -0
- pythinker_code/wire/server.py +1069 -0
- pythinker_code/wire/types.py +698 -0
- pythinker_code-2.0.0.dist-info/METADATA +660 -0
- pythinker_code-2.0.0.dist-info/RECORD +731 -0
- pythinker_code-2.0.0.dist-info/WHEEL +4 -0
- pythinker_code-2.0.0.dist-info/entry_points.txt +4 -0
- pythinker_code-2.0.0.dist-info/licenses/LICENSE +202 -0
- pythinker_code-2.0.0.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""Session process management for Pythinker CLI web interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import base64
|
|
7
|
+
import contextlib
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import mimetypes
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from uuid import UUID, uuid4
|
|
18
|
+
|
|
19
|
+
from PIL import Image
|
|
20
|
+
from PIL.Image import Image as PILImage
|
|
21
|
+
from pydantic import TypeAdapter
|
|
22
|
+
from pythinker_core.message import ContentPart, ImageURLPart, TextPart
|
|
23
|
+
from starlette.websockets import WebSocket, WebSocketState
|
|
24
|
+
|
|
25
|
+
from pythinker_code.config import load_config
|
|
26
|
+
from pythinker_code.llm import ModelCapability
|
|
27
|
+
from pythinker_code.utils.logging import logger
|
|
28
|
+
from pythinker_code.utils.subprocess_env import get_clean_env
|
|
29
|
+
from pythinker_code.web.models import (
|
|
30
|
+
SessionNoticeEvent,
|
|
31
|
+
SessionNoticePayload,
|
|
32
|
+
SessionState,
|
|
33
|
+
SessionStatus,
|
|
34
|
+
)
|
|
35
|
+
from pythinker_code.web.runner.messages import new_session_status_message
|
|
36
|
+
from pythinker_code.web.store.sessions import load_session_by_id
|
|
37
|
+
from pythinker_code.wire.jsonrpc import (
|
|
38
|
+
JSONRPCCancelMessage,
|
|
39
|
+
JSONRPCErrorObject,
|
|
40
|
+
JSONRPCErrorResponse,
|
|
41
|
+
JSONRPCEventMessage,
|
|
42
|
+
JSONRPCInMessage,
|
|
43
|
+
JSONRPCInMessageAdapter,
|
|
44
|
+
JSONRPCOutMessage,
|
|
45
|
+
JSONRPCPromptMessage,
|
|
46
|
+
JSONRPCRequestMessage,
|
|
47
|
+
JSONRPCSuccessResponse,
|
|
48
|
+
)
|
|
49
|
+
from pythinker_code.wire.serde import deserialize_wire_message
|
|
50
|
+
|
|
51
|
+
JSONRPCOutMessageAdapter = TypeAdapter[JSONRPCOutMessage](JSONRPCOutMessage)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SessionProcess:
|
|
55
|
+
"""Manages a single session's PythinkerCLI subprocess.
|
|
56
|
+
|
|
57
|
+
Handles:
|
|
58
|
+
- Starting/stopping the subprocess
|
|
59
|
+
- Reading from stdout (wire messages from PythinkerCLI)
|
|
60
|
+
- Writing to stdin (user input to PythinkerCLI)
|
|
61
|
+
- Broadcasting messages to connected WebSockets
|
|
62
|
+
|
|
63
|
+
Concurrency model:
|
|
64
|
+
- `SessionProcess` is the long-lived container for a `session_id`.
|
|
65
|
+
It may outlive worker restarts.
|
|
66
|
+
- Liveness vs busy are separate:
|
|
67
|
+
- `is_alive` / `is_running`: worker subprocess exists and has not exited.
|
|
68
|
+
- `is_busy`: there is at least one in-flight prompt id.
|
|
69
|
+
- WebSocket fanout supports "join while running":
|
|
70
|
+
- New clients replay `wire.jsonl` history first.
|
|
71
|
+
- Live messages during replay are buffered per-WS and flushed afterwards.
|
|
72
|
+
|
|
73
|
+
Locks:
|
|
74
|
+
- `_lock` guards worker lifecycle and busy state.
|
|
75
|
+
- `_ws_lock` guards WebSocket state.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, session_id: UUID) -> None:
|
|
79
|
+
"""Initialize a session process."""
|
|
80
|
+
self.session_id = session_id
|
|
81
|
+
self._in_flight_prompt_ids: set[str] = set()
|
|
82
|
+
self._status_seq = 0
|
|
83
|
+
self._worker_id: str | None = None
|
|
84
|
+
self._status = SessionStatus(
|
|
85
|
+
session_id=self.session_id,
|
|
86
|
+
state="stopped",
|
|
87
|
+
seq=self._status_seq,
|
|
88
|
+
worker_id=self._worker_id,
|
|
89
|
+
reason=None,
|
|
90
|
+
detail=None,
|
|
91
|
+
updated_at=datetime.now(UTC),
|
|
92
|
+
)
|
|
93
|
+
self._process: asyncio.subprocess.Process | None = None
|
|
94
|
+
self._websockets: set[WebSocket] = set()
|
|
95
|
+
self._websocket_count = 0
|
|
96
|
+
self._replay_buffers: dict[WebSocket, list[str]] = {}
|
|
97
|
+
self._read_task: asyncio.Task[None] | None = None
|
|
98
|
+
self._expecting_exit = False
|
|
99
|
+
self._lock = asyncio.Lock()
|
|
100
|
+
self._ws_lock = asyncio.Lock()
|
|
101
|
+
self._sent_files: set[str] = set()
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_alive(self) -> bool:
|
|
105
|
+
"""Whether the worker subprocess exists and has not exited."""
|
|
106
|
+
process = self._process
|
|
107
|
+
return process is not None and process.returncode is None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_running(self) -> bool:
|
|
111
|
+
"""Backward-compatible name: indicates worker liveness."""
|
|
112
|
+
return self.is_alive
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_busy(self) -> bool:
|
|
116
|
+
"""Whether the session is currently processing a prompt."""
|
|
117
|
+
return len(self._in_flight_prompt_ids) > 0
|
|
118
|
+
|
|
119
|
+
def clear_in_flight(self) -> None:
|
|
120
|
+
"""Clear stale in-flight prompt IDs (e.g. after an error)."""
|
|
121
|
+
self._in_flight_prompt_ids.clear()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def status(self) -> SessionStatus:
|
|
125
|
+
"""Current runtime status snapshot."""
|
|
126
|
+
return self._status
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def websocket_count(self) -> int:
|
|
130
|
+
"""Get the number of connected WebSockets."""
|
|
131
|
+
return self._websocket_count
|
|
132
|
+
|
|
133
|
+
async def send_status_snapshot(self, ws: WebSocket) -> None:
|
|
134
|
+
"""Send the current status snapshot to a specific WebSocket."""
|
|
135
|
+
await ws.send_text(new_session_status_message(self._status).model_dump_json())
|
|
136
|
+
|
|
137
|
+
def _build_status(
|
|
138
|
+
self,
|
|
139
|
+
state: SessionState,
|
|
140
|
+
reason: str | None,
|
|
141
|
+
detail: str | None,
|
|
142
|
+
) -> SessionStatus | None:
|
|
143
|
+
"""Build a new status object if different from current."""
|
|
144
|
+
current = self._status
|
|
145
|
+
if (
|
|
146
|
+
current.state == state
|
|
147
|
+
and current.reason == reason
|
|
148
|
+
and current.detail == detail
|
|
149
|
+
and current.worker_id == self._worker_id
|
|
150
|
+
):
|
|
151
|
+
return None
|
|
152
|
+
self._status_seq += 1
|
|
153
|
+
status = SessionStatus(
|
|
154
|
+
session_id=self.session_id,
|
|
155
|
+
state=state,
|
|
156
|
+
seq=self._status_seq,
|
|
157
|
+
worker_id=self._worker_id,
|
|
158
|
+
reason=reason,
|
|
159
|
+
detail=detail,
|
|
160
|
+
updated_at=datetime.now(UTC),
|
|
161
|
+
)
|
|
162
|
+
self._status = status
|
|
163
|
+
return status
|
|
164
|
+
|
|
165
|
+
async def _emit_status(
|
|
166
|
+
self,
|
|
167
|
+
state: SessionState,
|
|
168
|
+
*,
|
|
169
|
+
reason: str | None = None,
|
|
170
|
+
detail: str | None = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Emit a status update if different from current."""
|
|
173
|
+
status = self._build_status(state, reason, detail)
|
|
174
|
+
if status is None:
|
|
175
|
+
return
|
|
176
|
+
await self._broadcast(new_session_status_message(status).model_dump_json())
|
|
177
|
+
|
|
178
|
+
async def start(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
reason: str | None = None,
|
|
182
|
+
detail: str | None = None,
|
|
183
|
+
restart_started_at: float | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Start the PythinkerCLI subprocess."""
|
|
186
|
+
async with self._lock:
|
|
187
|
+
if self.is_alive:
|
|
188
|
+
if self._read_task is None or self._read_task.done():
|
|
189
|
+
self._read_task = asyncio.create_task(self._read_loop())
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
self._in_flight_prompt_ids.clear()
|
|
193
|
+
self._expecting_exit = False
|
|
194
|
+
self._worker_id = str(uuid4())
|
|
195
|
+
|
|
196
|
+
# 16MB buffer for large messages (e.g., base64-encoded images)
|
|
197
|
+
STREAM_LIMIT = 16 * 1024 * 1024
|
|
198
|
+
|
|
199
|
+
if getattr(sys, "frozen", False):
|
|
200
|
+
worker_cmd = [sys.executable, "__web-worker", str(self.session_id)]
|
|
201
|
+
else:
|
|
202
|
+
worker_cmd = [
|
|
203
|
+
sys.executable,
|
|
204
|
+
"-m",
|
|
205
|
+
"pythinker_code.web.runner.worker",
|
|
206
|
+
str(self.session_id),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
self._process = await asyncio.create_subprocess_exec(
|
|
210
|
+
*worker_cmd,
|
|
211
|
+
stdin=asyncio.subprocess.PIPE,
|
|
212
|
+
stdout=asyncio.subprocess.PIPE,
|
|
213
|
+
stderr=asyncio.subprocess.PIPE,
|
|
214
|
+
limit=STREAM_LIMIT,
|
|
215
|
+
env=get_clean_env(),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
self._read_task = asyncio.create_task(self._read_loop())
|
|
219
|
+
if restart_started_at is not None:
|
|
220
|
+
elapsed_ms = int((time.perf_counter() - restart_started_at) * 1000)
|
|
221
|
+
detail = f"restart_ms={elapsed_ms}"
|
|
222
|
+
await self._emit_status("idle", reason=reason or "start", detail=detail)
|
|
223
|
+
await self._emit_restart_notice(reason=reason, restart_ms=elapsed_ms)
|
|
224
|
+
else:
|
|
225
|
+
await self._emit_status("idle", reason=reason or "start", detail=None)
|
|
226
|
+
|
|
227
|
+
async def stop(self) -> None:
|
|
228
|
+
"""Stop the session: terminate worker and close all WebSockets."""
|
|
229
|
+
await self.stop_worker(reason="stop")
|
|
230
|
+
await self._close_all_websockets()
|
|
231
|
+
|
|
232
|
+
async def stop_worker(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
reason: str | None = None,
|
|
236
|
+
emit_status: bool = True,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Stop only the worker subprocess, keeping WebSockets connected."""
|
|
239
|
+
async with self._lock:
|
|
240
|
+
self._expecting_exit = True
|
|
241
|
+
if self._process is not None:
|
|
242
|
+
if self._process.returncode is None:
|
|
243
|
+
self._process.terminate()
|
|
244
|
+
try:
|
|
245
|
+
await asyncio.wait_for(self._process.wait(), timeout=10.0)
|
|
246
|
+
except TimeoutError:
|
|
247
|
+
self._process.kill()
|
|
248
|
+
await self._process.wait()
|
|
249
|
+
self._process = None
|
|
250
|
+
|
|
251
|
+
if self._read_task is not None:
|
|
252
|
+
self._read_task.cancel()
|
|
253
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
254
|
+
await self._read_task
|
|
255
|
+
self._read_task = None
|
|
256
|
+
|
|
257
|
+
self._in_flight_prompt_ids.clear()
|
|
258
|
+
self._worker_id = None
|
|
259
|
+
self._expecting_exit = False
|
|
260
|
+
if emit_status:
|
|
261
|
+
await self._emit_status("stopped", reason=reason or "stop")
|
|
262
|
+
|
|
263
|
+
async def restart_worker(self, *, reason: str | None = None) -> None:
|
|
264
|
+
"""Restart the worker subprocess without disconnecting WebSockets."""
|
|
265
|
+
started_at = time.perf_counter()
|
|
266
|
+
await self._emit_status("restarting", reason=reason or "restart")
|
|
267
|
+
await self.stop_worker(reason="restart", emit_status=False)
|
|
268
|
+
await self.start(reason=reason or "restart", restart_started_at=started_at)
|
|
269
|
+
|
|
270
|
+
async def _emit_restart_notice(self, *, reason: str | None, restart_ms: int) -> None:
|
|
271
|
+
"""Emit a restart notice to all WebSockets."""
|
|
272
|
+
label = "Session restarted"
|
|
273
|
+
if reason == "config_update":
|
|
274
|
+
label = "Session restarted due to config update"
|
|
275
|
+
payload = SessionNoticePayload(
|
|
276
|
+
text=f"{label} · {restart_ms}ms",
|
|
277
|
+
kind="restart",
|
|
278
|
+
reason=reason,
|
|
279
|
+
restart_ms=restart_ms,
|
|
280
|
+
)
|
|
281
|
+
event = SessionNoticeEvent(payload=payload)
|
|
282
|
+
await self._broadcast(
|
|
283
|
+
json.dumps(
|
|
284
|
+
{
|
|
285
|
+
"jsonrpc": "2.0",
|
|
286
|
+
"method": "event",
|
|
287
|
+
"params": event.model_dump(mode="json"),
|
|
288
|
+
},
|
|
289
|
+
ensure_ascii=False,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def _read_loop(self) -> None:
|
|
294
|
+
"""Read messages from subprocess stdout and broadcast to WebSockets."""
|
|
295
|
+
assert self._process is not None
|
|
296
|
+
assert self._process.stdout is not None
|
|
297
|
+
assert self._process.stderr is not None
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
while True:
|
|
301
|
+
line = await self._process.stdout.readline()
|
|
302
|
+
if not line:
|
|
303
|
+
if self._process.stdout.at_eof():
|
|
304
|
+
if self._expecting_exit:
|
|
305
|
+
break
|
|
306
|
+
stderr = await self._process.stderr.read()
|
|
307
|
+
if not stderr:
|
|
308
|
+
stderr = b"No stderr"
|
|
309
|
+
# Clear in-flight IDs before broadcasting so that
|
|
310
|
+
# is_busy is already False when the frontend reacts
|
|
311
|
+
# to the error and sends a new prompt.
|
|
312
|
+
self._in_flight_prompt_ids.clear()
|
|
313
|
+
await self._broadcast(
|
|
314
|
+
JSONRPCErrorResponse(
|
|
315
|
+
id=str(uuid4()),
|
|
316
|
+
error=JSONRPCErrorObject(
|
|
317
|
+
code=self._process.returncode or -1,
|
|
318
|
+
message=stderr.decode("utf-8"),
|
|
319
|
+
),
|
|
320
|
+
).model_dump_json()
|
|
321
|
+
)
|
|
322
|
+
logger.warning(
|
|
323
|
+
f"Process exited with {self._process.returncode}: "
|
|
324
|
+
f"{stderr.decode('utf-8')}"
|
|
325
|
+
)
|
|
326
|
+
await self._emit_status(
|
|
327
|
+
"error",
|
|
328
|
+
reason="process_exit",
|
|
329
|
+
detail=stderr.decode("utf-8"),
|
|
330
|
+
)
|
|
331
|
+
break
|
|
332
|
+
else:
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
await self._broadcast(line.decode("utf-8").rstrip("\n"))
|
|
336
|
+
|
|
337
|
+
# Handle out message
|
|
338
|
+
try:
|
|
339
|
+
msg = json.loads(line)
|
|
340
|
+
match msg.get("method"):
|
|
341
|
+
case "event":
|
|
342
|
+
msg["params"] = deserialize_wire_message(msg["params"])
|
|
343
|
+
await self._handle_out_message(JSONRPCEventMessage.model_validate(msg))
|
|
344
|
+
case "request":
|
|
345
|
+
msg["params"] = deserialize_wire_message(msg["params"])
|
|
346
|
+
await self._handle_out_message(
|
|
347
|
+
JSONRPCRequestMessage.model_validate(msg)
|
|
348
|
+
)
|
|
349
|
+
case _:
|
|
350
|
+
if msg.get("error"):
|
|
351
|
+
await self._handle_out_message(
|
|
352
|
+
JSONRPCErrorResponse.model_validate(msg)
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
await self._handle_out_message(
|
|
356
|
+
JSONRPCSuccessResponse.model_validate(msg)
|
|
357
|
+
)
|
|
358
|
+
except json.JSONDecodeError:
|
|
359
|
+
logger.error(f"Invalid JSONRPC out message: {line}")
|
|
360
|
+
|
|
361
|
+
except asyncio.CancelledError:
|
|
362
|
+
raise
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.warning(f"Unexpected error in read loop: {e.__class__.__name__} {e}")
|
|
365
|
+
self._in_flight_prompt_ids.clear()
|
|
366
|
+
await self._emit_status("error", reason="read_loop_error", detail=str(e))
|
|
367
|
+
|
|
368
|
+
async def _handle_out_message(self, message: JSONRPCOutMessage) -> None:
|
|
369
|
+
"""Handle outbound message from worker."""
|
|
370
|
+
match message:
|
|
371
|
+
case JSONRPCSuccessResponse():
|
|
372
|
+
was_busy = self.is_busy
|
|
373
|
+
if message.id in self._in_flight_prompt_ids:
|
|
374
|
+
self._in_flight_prompt_ids.remove(message.id)
|
|
375
|
+
if was_busy and not self.is_busy:
|
|
376
|
+
await self._emit_status("idle", reason="prompt_complete")
|
|
377
|
+
case JSONRPCErrorResponse():
|
|
378
|
+
was_busy = self.is_busy
|
|
379
|
+
if message.id in self._in_flight_prompt_ids:
|
|
380
|
+
self._in_flight_prompt_ids.remove(message.id)
|
|
381
|
+
if was_busy and not self.is_busy:
|
|
382
|
+
await self._emit_status("idle", reason="prompt_error")
|
|
383
|
+
case _:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
async def _encode_uploaded_files(self) -> AsyncGenerator[ContentPart]:
|
|
387
|
+
"""Encode uploaded files for sending to the model."""
|
|
388
|
+
session = load_session_by_id(self.session_id)
|
|
389
|
+
assert session is not None
|
|
390
|
+
|
|
391
|
+
uploads_dir = session.pythinker_code_session.dir / "uploads"
|
|
392
|
+
if not uploads_dir.exists():
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
# Load .sent marker left by fork to avoid re-sending inherited files.
|
|
396
|
+
# The marker is kept (not deleted) so it survives process restarts.
|
|
397
|
+
sent_marker = uploads_dir / ".sent"
|
|
398
|
+
if sent_marker.exists():
|
|
399
|
+
try:
|
|
400
|
+
already_sent = json.loads(sent_marker.read_text(encoding="utf-8"))
|
|
401
|
+
self._sent_files.update(already_sent)
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
all_files = sorted(
|
|
406
|
+
(f for f in uploads_dir.iterdir() if f.name != ".sent"),
|
|
407
|
+
key=lambda x: x.name,
|
|
408
|
+
)
|
|
409
|
+
files = [f for f in all_files if f.name not in self._sent_files]
|
|
410
|
+
|
|
411
|
+
if not files:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
# Build file list with paths and mime types
|
|
415
|
+
file_infos: list[tuple[Path, str]] = []
|
|
416
|
+
for file in files:
|
|
417
|
+
mime_type, _ = mimetypes.guess_type(file.name)
|
|
418
|
+
file_infos.append((file, mime_type or "application/octet-stream"))
|
|
419
|
+
|
|
420
|
+
# Output file list summary
|
|
421
|
+
file_list_lines = ["<uploaded_files>"]
|
|
422
|
+
for idx, (file, _) in enumerate(file_infos, start=1):
|
|
423
|
+
file_list_lines.append(f"{idx}. {file}")
|
|
424
|
+
file_list_lines.append("</uploaded_files>")
|
|
425
|
+
yield TextPart(text="\n".join(file_list_lines) + "\n\n")
|
|
426
|
+
|
|
427
|
+
# Text file extensions
|
|
428
|
+
text_extensions = {
|
|
429
|
+
".txt",
|
|
430
|
+
".md",
|
|
431
|
+
".json",
|
|
432
|
+
".yaml",
|
|
433
|
+
".yml",
|
|
434
|
+
".xml",
|
|
435
|
+
".html",
|
|
436
|
+
".css",
|
|
437
|
+
".js",
|
|
438
|
+
".ts",
|
|
439
|
+
".py",
|
|
440
|
+
".sh",
|
|
441
|
+
".csv",
|
|
442
|
+
".log",
|
|
443
|
+
".rst",
|
|
444
|
+
".toml",
|
|
445
|
+
".ini",
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Check model capabilities
|
|
449
|
+
config = load_config()
|
|
450
|
+
capabilities: set[ModelCapability] = set()
|
|
451
|
+
if config.default_model:
|
|
452
|
+
capabilities = config.models[config.default_model].capabilities or set()
|
|
453
|
+
is_vision = "image_in" in capabilities
|
|
454
|
+
is_video_in = "video_in" in capabilities
|
|
455
|
+
|
|
456
|
+
# Process each file
|
|
457
|
+
for file, mime_type in file_infos:
|
|
458
|
+
file_path = str(file)
|
|
459
|
+
ext = file.suffix.lower()
|
|
460
|
+
|
|
461
|
+
if is_vision and mime_type.startswith("image/"):
|
|
462
|
+
try:
|
|
463
|
+
content = file.read_bytes()
|
|
464
|
+
with Image.open(io.BytesIO(content)) as img:
|
|
465
|
+
pil_img: PILImage = img
|
|
466
|
+
width, height = pil_img.size
|
|
467
|
+
max_side = max(width, height)
|
|
468
|
+
if max_side > 4096:
|
|
469
|
+
scale = 4096 / max_side
|
|
470
|
+
new_size = (int(width * scale), int(height * scale))
|
|
471
|
+
pil_img = pil_img.resize( # pyright: ignore[reportUnknownMemberType]
|
|
472
|
+
new_size
|
|
473
|
+
)
|
|
474
|
+
buffer = io.BytesIO()
|
|
475
|
+
pil_img.save(buffer, format="PNG")
|
|
476
|
+
encoded = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
477
|
+
tag = f'<image path="{file_path}" content_type="{mime_type}">'
|
|
478
|
+
yield TextPart(text=tag)
|
|
479
|
+
yield ImageURLPart(
|
|
480
|
+
image_url=ImageURLPart.ImageURL(url=f"data:image/png;base64,{encoded}")
|
|
481
|
+
)
|
|
482
|
+
yield TextPart(text="</image>\n\n")
|
|
483
|
+
except Exception:
|
|
484
|
+
# Skip files that fail to encode - don't block the upload
|
|
485
|
+
pass
|
|
486
|
+
elif is_video_in and mime_type.startswith("video/"):
|
|
487
|
+
# For video files, emit a <video> tag for frontend display but don't embed content.
|
|
488
|
+
# The agent will use ReadMediaFile tool to read it, which handles video uploads
|
|
489
|
+
# properly.
|
|
490
|
+
yield TextPart(text=f'<video path="{file_path}" content_type="{mime_type}">')
|
|
491
|
+
yield TextPart(text="</video>\n\n")
|
|
492
|
+
elif ext in text_extensions or mime_type.startswith("text/"):
|
|
493
|
+
try:
|
|
494
|
+
content = file.read_bytes()
|
|
495
|
+
text_content = content.decode("utf-8", errors="replace")
|
|
496
|
+
yield TextPart(text=f'<document path="{file_path}" content_type="{mime_type}">')
|
|
497
|
+
yield TextPart(text=text_content)
|
|
498
|
+
yield TextPart(text="</document>\n\n")
|
|
499
|
+
except Exception:
|
|
500
|
+
# Skip files that fail to decode - don't block the upload
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
# Mark files as sent
|
|
504
|
+
for file in files:
|
|
505
|
+
self._sent_files.add(file.name)
|
|
506
|
+
|
|
507
|
+
async def _handle_in_message(self, message: JSONRPCInMessage) -> str | None:
|
|
508
|
+
"""Handle inbound message to worker, encoding uploaded files."""
|
|
509
|
+
match message:
|
|
510
|
+
case JSONRPCPromptMessage():
|
|
511
|
+
user_input: list[ContentPart] = []
|
|
512
|
+
async for part in self._encode_uploaded_files():
|
|
513
|
+
user_input.append(part)
|
|
514
|
+
# Special marker for file-only uploads
|
|
515
|
+
if isinstance(message.params.user_input, str):
|
|
516
|
+
if message.params.user_input != "PYTHINKER_FILE_UPLOAD_WITHOUT_MESSAGE":
|
|
517
|
+
user_input.append(TextPart(text=message.params.user_input))
|
|
518
|
+
else:
|
|
519
|
+
user_input += message.params.user_input
|
|
520
|
+
return json.dumps(
|
|
521
|
+
{
|
|
522
|
+
"jsonrpc": "2.0",
|
|
523
|
+
"method": "prompt",
|
|
524
|
+
"id": message.id,
|
|
525
|
+
"params": {
|
|
526
|
+
"user_input": [part.model_dump(mode="json") for part in user_input],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
ensure_ascii=False,
|
|
530
|
+
)
|
|
531
|
+
case _:
|
|
532
|
+
return None
|
|
533
|
+
return None
|
|
534
|
+
|
|
535
|
+
async def _broadcast(self, message: str) -> None:
|
|
536
|
+
"""Broadcast a message to all connected WebSockets."""
|
|
537
|
+
disconnected: set[WebSocket] = set()
|
|
538
|
+
|
|
539
|
+
async with self._ws_lock:
|
|
540
|
+
websockets = list(self._websockets)
|
|
541
|
+
to_send: list[WebSocket] = []
|
|
542
|
+
for ws in websockets:
|
|
543
|
+
buffer = self._replay_buffers.get(ws)
|
|
544
|
+
if buffer is not None:
|
|
545
|
+
buffer.append(message)
|
|
546
|
+
else:
|
|
547
|
+
to_send.append(ws)
|
|
548
|
+
|
|
549
|
+
for ws in to_send:
|
|
550
|
+
try:
|
|
551
|
+
if ws.client_state == WebSocketState.CONNECTED:
|
|
552
|
+
await ws.send_text(message)
|
|
553
|
+
else:
|
|
554
|
+
disconnected.add(ws)
|
|
555
|
+
except Exception as e:
|
|
556
|
+
logger.warning(f"websocket failed: {e.__class__.__name__} {e}")
|
|
557
|
+
disconnected.add(ws)
|
|
558
|
+
|
|
559
|
+
if disconnected:
|
|
560
|
+
async with self._ws_lock:
|
|
561
|
+
self._websockets -= disconnected
|
|
562
|
+
self._websocket_count = len(self._websockets)
|
|
563
|
+
for ws in disconnected:
|
|
564
|
+
self._replay_buffers.pop(ws, None)
|
|
565
|
+
logger.debug(
|
|
566
|
+
f"Broadcast: removed {len(disconnected)} disconnected ws, "
|
|
567
|
+
f"remaining={self._websocket_count}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
async def add_websocket_and_begin_replay(self, ws: WebSocket) -> None:
|
|
571
|
+
"""Atomically attach a WebSocket and enter replay mode for it."""
|
|
572
|
+
async with self._ws_lock:
|
|
573
|
+
if ws not in self._websockets:
|
|
574
|
+
self._websockets.add(ws)
|
|
575
|
+
self._websocket_count = len(self._websockets)
|
|
576
|
+
self._replay_buffers.setdefault(ws, [])
|
|
577
|
+
logger.debug(f"WebSocket added (replay mode), count={self._websocket_count}")
|
|
578
|
+
|
|
579
|
+
async def end_replay(self, ws: WebSocket) -> None:
|
|
580
|
+
"""Flush buffered live messages for a websocket after history replay."""
|
|
581
|
+
while True:
|
|
582
|
+
async with self._ws_lock:
|
|
583
|
+
buffer = self._replay_buffers.get(ws)
|
|
584
|
+
if buffer is None:
|
|
585
|
+
return
|
|
586
|
+
if not buffer:
|
|
587
|
+
self._replay_buffers.pop(ws, None)
|
|
588
|
+
return
|
|
589
|
+
chunk = buffer.copy()
|
|
590
|
+
buffer.clear()
|
|
591
|
+
|
|
592
|
+
if ws.client_state != WebSocketState.CONNECTED:
|
|
593
|
+
logger.warning("end_replay: ws not connected, cleaning up replay buffer")
|
|
594
|
+
async with self._ws_lock:
|
|
595
|
+
self._replay_buffers.pop(ws, None)
|
|
596
|
+
return
|
|
597
|
+
for message in chunk:
|
|
598
|
+
try:
|
|
599
|
+
await ws.send_text(message)
|
|
600
|
+
except Exception as e:
|
|
601
|
+
# Send failed — pop the replay buffer so _broadcast()
|
|
602
|
+
# sends directly (or detects disconnect) on the next call.
|
|
603
|
+
# Do NOT remove ws from _websockets here; let _broadcast()
|
|
604
|
+
# or session_stream's finally block handle cleanup.
|
|
605
|
+
logger.warning(f"end_replay: send_text failed during buffer flush: {e}")
|
|
606
|
+
async with self._ws_lock:
|
|
607
|
+
self._replay_buffers.pop(ws, None)
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
async def _close_all_websockets(self) -> None:
|
|
611
|
+
"""Close all connected WebSockets."""
|
|
612
|
+
async with self._ws_lock:
|
|
613
|
+
websockets = list(self._websockets)
|
|
614
|
+
self._websockets.clear()
|
|
615
|
+
self._websocket_count = 0
|
|
616
|
+
self._replay_buffers.clear()
|
|
617
|
+
|
|
618
|
+
for ws in websockets:
|
|
619
|
+
try:
|
|
620
|
+
if ws.client_state == WebSocketState.CONNECTED:
|
|
621
|
+
await ws.close(code=1001, reason="Session process exited")
|
|
622
|
+
except Exception:
|
|
623
|
+
# Ignore errors closing already-disconnected WebSockets
|
|
624
|
+
pass
|
|
625
|
+
|
|
626
|
+
async def remove_websocket(self, ws: WebSocket) -> None:
|
|
627
|
+
"""Remove a WebSocket connection from this session."""
|
|
628
|
+
async with self._ws_lock:
|
|
629
|
+
if ws in self._websockets:
|
|
630
|
+
self._websockets.discard(ws)
|
|
631
|
+
self._websocket_count = len(self._websockets)
|
|
632
|
+
logger.debug(f"WebSocket removed, count={self._websocket_count}")
|
|
633
|
+
self._replay_buffers.pop(ws, None)
|
|
634
|
+
|
|
635
|
+
async def send_message(self, message: str) -> None:
|
|
636
|
+
"""Send a message to the subprocess stdin."""
|
|
637
|
+
await self.start()
|
|
638
|
+
process = self._process
|
|
639
|
+
assert process is not None
|
|
640
|
+
assert process.stdin is not None
|
|
641
|
+
|
|
642
|
+
# Handle in message
|
|
643
|
+
try:
|
|
644
|
+
in_message = JSONRPCInMessageAdapter.validate_json(message)
|
|
645
|
+
if isinstance(in_message, JSONRPCPromptMessage):
|
|
646
|
+
was_busy = self.is_busy
|
|
647
|
+
self._in_flight_prompt_ids.add(in_message.id)
|
|
648
|
+
if not was_busy:
|
|
649
|
+
await self._emit_status("busy", reason="prompt")
|
|
650
|
+
elif isinstance(in_message, JSONRPCCancelMessage) and not self.is_busy:
|
|
651
|
+
# If not busy, return success to avoid errors
|
|
652
|
+
await self._broadcast(
|
|
653
|
+
JSONRPCSuccessResponse(id=in_message.id, result={}).model_dump_json()
|
|
654
|
+
)
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
new_message = await self._handle_in_message(in_message)
|
|
658
|
+
if new_message is not None:
|
|
659
|
+
message = new_message
|
|
660
|
+
except ValueError as e:
|
|
661
|
+
logger.error(f"{e.__class__.__name__} {e}: Invalid JSONRPC in message: {message}")
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
process.stdin.write((message + "\n").encode("utf-8"))
|
|
665
|
+
await process.stdin.drain()
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class PythinkerCLIRunner:
|
|
669
|
+
"""Manages multiple session processes."""
|
|
670
|
+
|
|
671
|
+
def __init__(self) -> None:
|
|
672
|
+
"""Initialize the runner."""
|
|
673
|
+
self._sessions: dict[UUID, SessionProcess] = {}
|
|
674
|
+
self._lock = asyncio.Lock()
|
|
675
|
+
|
|
676
|
+
def start(self) -> None:
|
|
677
|
+
"""Start the runner (no-op, sessions started on demand)."""
|
|
678
|
+
pass
|
|
679
|
+
|
|
680
|
+
async def stop(self) -> None:
|
|
681
|
+
"""Stop all running sessions."""
|
|
682
|
+
tasks: list[asyncio.Task[None]] = []
|
|
683
|
+
for session in self._sessions.values():
|
|
684
|
+
if session.is_running:
|
|
685
|
+
tasks.append(asyncio.create_task(session.stop()))
|
|
686
|
+
if tasks:
|
|
687
|
+
_, pending = await asyncio.wait(tasks, timeout=5.0)
|
|
688
|
+
for t in pending:
|
|
689
|
+
t.cancel()
|
|
690
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
691
|
+
await t
|
|
692
|
+
|
|
693
|
+
async def get_or_create_session(self, session_id: UUID) -> SessionProcess:
|
|
694
|
+
"""Get or create a session process."""
|
|
695
|
+
async with self._lock:
|
|
696
|
+
if session_id not in self._sessions:
|
|
697
|
+
self._sessions[session_id] = SessionProcess(session_id)
|
|
698
|
+
return self._sessions[session_id]
|
|
699
|
+
|
|
700
|
+
def get_session(self, session_id: UUID) -> SessionProcess | None:
|
|
701
|
+
"""Get a session process if it exists."""
|
|
702
|
+
return self._sessions.get(session_id)
|
|
703
|
+
|
|
704
|
+
async def detach_websocket(self, ws: WebSocket, session_id: UUID) -> None:
|
|
705
|
+
"""Detach a WebSocket from a session."""
|
|
706
|
+
async with self._lock:
|
|
707
|
+
session = self._sessions.get(session_id)
|
|
708
|
+
if session:
|
|
709
|
+
await session.remove_websocket(ws)
|
|
710
|
+
|
|
711
|
+
async def restart_running_workers(
|
|
712
|
+
self,
|
|
713
|
+
*,
|
|
714
|
+
reason: str,
|
|
715
|
+
force: bool,
|
|
716
|
+
) -> RestartWorkersSummary:
|
|
717
|
+
"""Restart all running workers to apply global config updates.
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
reason: Reason for the restart (e.g., "config_update")
|
|
721
|
+
force: If True, also restart busy sessions (may interrupt prompts)
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Summary of restarted and skipped sessions
|
|
725
|
+
"""
|
|
726
|
+
async with self._lock:
|
|
727
|
+
running = [(sid, proc) for sid, proc in self._sessions.items() if proc.is_running]
|
|
728
|
+
|
|
729
|
+
restarted: list[UUID] = []
|
|
730
|
+
skipped_busy: list[UUID] = []
|
|
731
|
+
tasks: list[asyncio.Task[None]] = []
|
|
732
|
+
|
|
733
|
+
for session_id, proc in running:
|
|
734
|
+
if proc.is_busy and not force:
|
|
735
|
+
skipped_busy.append(session_id)
|
|
736
|
+
continue
|
|
737
|
+
restarted.append(session_id)
|
|
738
|
+
tasks.append(asyncio.create_task(proc.restart_worker(reason=reason)))
|
|
739
|
+
|
|
740
|
+
if tasks:
|
|
741
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
742
|
+
|
|
743
|
+
return RestartWorkersSummary(
|
|
744
|
+
restarted_session_ids=restarted,
|
|
745
|
+
skipped_busy_session_ids=skipped_busy,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@dataclass(slots=True)
|
|
750
|
+
class RestartWorkersSummary:
|
|
751
|
+
"""Summary of a restart_running_workers operation."""
|
|
752
|
+
|
|
753
|
+
restarted_session_ids: list[UUID]
|
|
754
|
+
skipped_busy_session_ids: list[UUID]
|