pythinker-code 0.8.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 +60 -0
- pythinker_code/__init__.py +0 -0
- pythinker_code/__main__.py +97 -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 +301 -0
- pythinker_code/acp/mcp.py +46 -0
- pythinker_code/acp/server.py +497 -0
- pythinker_code/acp/session.py +502 -0
- pythinker_code/acp/tools.py +174 -0
- pythinker_code/acp/types.py +13 -0
- pythinker_code/acp/version.py +45 -0
- pythinker_code/agents/default/agent.yaml +55 -0
- pythinker_code/agents/default/code_reviewer.yaml +47 -0
- pythinker_code/agents/default/coder.yaml +42 -0
- pythinker_code/agents/default/debugger.yaml +35 -0
- pythinker_code/agents/default/explore.yaml +59 -0
- pythinker_code/agents/default/implementer.yaml +46 -0
- pythinker_code/agents/default/plan.yaml +42 -0
- pythinker_code/agents/default/review.yaml +47 -0
- pythinker_code/agents/default/security_reviewer.yaml +37 -0
- pythinker_code/agents/default/system.md +192 -0
- pythinker_code/agents/default/verifier.yaml +46 -0
- pythinker_code/agents/okabe/agent.yaml +22 -0
- pythinker_code/agentspec.py +163 -0
- pythinker_code/app.py +847 -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/github_feedback.py +228 -0
- pythinker_code/auth/lm_studio.py +418 -0
- pythinker_code/auth/minimax.py +203 -0
- pythinker_code/auth/oauth.py +1145 -0
- pythinker_code/auth/ollama.py +293 -0
- pythinker_code/auth/openai.py +783 -0
- pythinker_code/auth/opencode_go.py +203 -0
- pythinker_code/auth/openrouter.py +225 -0
- pythinker_code/auth/platforms.py +475 -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 +668 -0
- pythinker_code/background/models.py +118 -0
- pythinker_code/background/store.py +243 -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 +268 -0
- pythinker_code/cli/debug.py +11 -0
- pythinker_code/cli/export.py +322 -0
- pythinker_code/cli/info.py +62 -0
- pythinker_code/cli/mcp.py +362 -0
- pythinker_code/cli/plugin.py +351 -0
- pythinker_code/cli/review.py +74 -0
- pythinker_code/cli/secscan.py +11 -0
- pythinker_code/cli/security_scan.py +35 -0
- pythinker_code/cli/toad.py +74 -0
- pythinker_code/cli/update.py +26 -0
- pythinker_code/cli/vis.py +38 -0
- pythinker_code/cli/web.py +80 -0
- pythinker_code/config.py +511 -0
- pythinker_code/constant.py +33 -0
- pythinker_code/events.py +106 -0
- pythinker_code/exception.py +43 -0
- pythinker_code/extensions.py +151 -0
- pythinker_code/hooks/__init__.py +4 -0
- pythinker_code/hooks/config.py +34 -0
- pythinker_code/hooks/engine.py +383 -0
- pythinker_code/hooks/events.py +190 -0
- pythinker_code/hooks/runner.py +92 -0
- pythinker_code/llm.py +441 -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 +166 -0
- pythinker_code/plugin/tool.py +173 -0
- pythinker_code/prompt_templates.py +181 -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 +552 -0
- pythinker_code/soul/approval.py +267 -0
- pythinker_code/soul/btw.py +220 -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/permission.py +368 -0
- pythinker_code/soul/pythinkersoul.py +1763 -0
- pythinker_code/soul/slash.py +340 -0
- pythinker_code/soul/toolset.py +826 -0
- pythinker_code/subagents/__init__.py +21 -0
- pythinker_code/subagents/builder.py +43 -0
- pythinker_code/subagents/core.py +86 -0
- pythinker_code/subagents/discovery.py +234 -0
- pythinker_code/subagents/git_context.py +172 -0
- pythinker_code/subagents/models.py +56 -0
- pythinker_code/subagents/output.py +71 -0
- pythinker_code/subagents/registry.py +28 -0
- pythinker_code/subagents/runner.py +442 -0
- pythinker_code/subagents/store.py +200 -0
- pythinker_code/telemetry/__init__.py +217 -0
- pythinker_code/telemetry/config.py +113 -0
- pythinker_code/telemetry/crash.py +191 -0
- pythinker_code/telemetry/errors.py +113 -0
- pythinker_code/telemetry/metrics.py +208 -0
- pythinker_code/telemetry/otel.py +303 -0
- pythinker_code/telemetry/sentry.py +212 -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 +326 -0
- pythinker_code/tools/agent/description.md +65 -0
- pythinker_code/tools/ask_user/__init__.py +162 -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 +31 -0
- pythinker_code/tools/file/glob.md +17 -0
- pythinker_code/tools/file/glob.py +163 -0
- pythinker_code/tools/file/grep.md +6 -0
- pythinker_code/tools/file/grep_local.py +904 -0
- pythinker_code/tools/file/plan_mode.py +45 -0
- pythinker_code/tools/file/read.md +16 -0
- pythinker_code/tools/file/read.py +303 -0
- pythinker_code/tools/file/read_media.md +24 -0
- pythinker_code/tools/file/read_media.py +220 -0
- pythinker_code/tools/file/replace.md +7 -0
- pythinker_code/tools/file/replace.py +204 -0
- pythinker_code/tools/file/utils.py +257 -0
- pythinker_code/tools/file/write.md +5 -0
- pythinker_code/tools/file/write.py +186 -0
- pythinker_code/tools/plan/__init__.py +362 -0
- pythinker_code/tools/plan/description.md +29 -0
- pythinker_code/tools/plan/enter.py +193 -0
- pythinker_code/tools/plan/enter_description.md +35 -0
- pythinker_code/tools/plan/handoff.py +69 -0
- pythinker_code/tools/plan/heroes.py +277 -0
- pythinker_code/tools/shell/__init__.py +263 -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 +200 -0
- pythinker_code/tools/web/__init__.py +4 -0
- pythinker_code/tools/web/fetch.md +1 -0
- pythinker_code/tools/web/fetch.py +261 -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 +1806 -0
- pythinker_code/ui/shell/components/__init__.py +110 -0
- pythinker_code/ui/shell/components/base.py +25 -0
- pythinker_code/ui/shell/components/bash_execution.py +249 -0
- pythinker_code/ui/shell/components/bordered_loader.py +62 -0
- pythinker_code/ui/shell/components/diff.py +308 -0
- pythinker_code/ui/shell/components/footer.py +231 -0
- pythinker_code/ui/shell/components/key_hints.py +27 -0
- pythinker_code/ui/shell/components/messages.py +152 -0
- pythinker_code/ui/shell/components/render_utils.py +198 -0
- pythinker_code/ui/shell/components/settings_list.py +369 -0
- pythinker_code/ui/shell/components/special_messages.py +125 -0
- pythinker_code/ui/shell/components/tool_execution.py +261 -0
- pythinker_code/ui/shell/console.py +109 -0
- pythinker_code/ui/shell/debug.py +190 -0
- pythinker_code/ui/shell/echo.py +30 -0
- pythinker_code/ui/shell/export_import.py +117 -0
- pythinker_code/ui/shell/keyboard.py +300 -0
- pythinker_code/ui/shell/keymap.py +84 -0
- pythinker_code/ui/shell/mcp_status.py +112 -0
- pythinker_code/ui/shell/model_picker.py +318 -0
- pythinker_code/ui/shell/oauth.py +273 -0
- pythinker_code/ui/shell/placeholders.py +578 -0
- pythinker_code/ui/shell/prompt.py +2888 -0
- pythinker_code/ui/shell/replay.py +215 -0
- pythinker_code/ui/shell/selector.py +364 -0
- pythinker_code/ui/shell/selectors/__init__.py +38 -0
- pythinker_code/ui/shell/selectors/extension.py +37 -0
- pythinker_code/ui/shell/selectors/oauth.py +63 -0
- pythinker_code/ui/shell/selectors/settings.py +349 -0
- pythinker_code/ui/shell/selectors/show_images.py +29 -0
- pythinker_code/ui/shell/selectors/theme.py +28 -0
- pythinker_code/ui/shell/selectors/thinking.py +42 -0
- pythinker_code/ui/shell/session_picker.py +227 -0
- pythinker_code/ui/shell/setup.py +212 -0
- pythinker_code/ui/shell/slash.py +1433 -0
- pythinker_code/ui/shell/spinner_words.py +222 -0
- pythinker_code/ui/shell/startup.py +32 -0
- pythinker_code/ui/shell/task_browser.py +486 -0
- pythinker_code/ui/shell/tool_renderers/__init__.py +197 -0
- pythinker_code/ui/shell/tool_renderers/_render_utils.py +168 -0
- pythinker_code/ui/shell/tool_renderers/agent.py +140 -0
- pythinker_code/ui/shell/tool_renderers/ask_user.py +93 -0
- pythinker_code/ui/shell/tool_renderers/background.py +144 -0
- pythinker_code/ui/shell/tool_renderers/bash.py +103 -0
- pythinker_code/ui/shell/tool_renderers/edit.py +163 -0
- pythinker_code/ui/shell/tool_renderers/find.py +81 -0
- pythinker_code/ui/shell/tool_renderers/generic.py +60 -0
- pythinker_code/ui/shell/tool_renderers/grep.py +98 -0
- pythinker_code/ui/shell/tool_renderers/plan.py +98 -0
- pythinker_code/ui/shell/tool_renderers/read.py +103 -0
- pythinker_code/ui/shell/tool_renderers/think.py +66 -0
- pythinker_code/ui/shell/tool_renderers/todo.py +164 -0
- pythinker_code/ui/shell/tool_renderers/web.py +128 -0
- pythinker_code/ui/shell/tool_renderers/write.py +102 -0
- pythinker_code/ui/shell/update.py +352 -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 +539 -0
- pythinker_code/ui/shell/visualize/_blocks.py +802 -0
- pythinker_code/ui/shell/visualize/_btw_panel.py +227 -0
- pythinker_code/ui/shell/visualize/_input_router.py +48 -0
- pythinker_code/ui/shell/visualize/_interactive.py +531 -0
- pythinker_code/ui/shell/visualize/_live_view.py +891 -0
- pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
- pythinker_code/ui/shell/visualize/_worklog.py +245 -0
- pythinker_code/ui/theme.py +395 -0
- pythinker_code/ui/tui_config.py +82 -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 +38 -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 +935 -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 +83 -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 +714 -0
- pythinker_code/vis/api/statistics.py +209 -0
- pythinker_code/vis/api/system.py +19 -0
- pythinker_code/vis/app.py +199 -0
- pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-CY1rtwrX.js +1 -0
- pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
- pythinker_code/vis/static/assets/index-DgmTI2M_.js +185 -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 +217 -0
- pythinker_code/web/api/open_in.py +233 -0
- pythinker_code/web/api/sessions.py +1256 -0
- pythinker_code/web/app.py +449 -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-DpSMr1jx.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-DpsahJyV.js +1 -0
- pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-DqiRv9Eg.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-WgtUvqbp.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-rK0RPuZd.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-B0rlvkH-.js +1 -0
- pythinker_code/web/static/assets/chunk-4BX2VUAB-DIkMuLV-.js +1 -0
- pythinker_code/web/static/assets/chunk-55IACEB6-CORdm4k4.js +1 -0
- pythinker_code/web/static/assets/chunk-B4BG7PRW-D9xDhwHO.js +165 -0
- pythinker_code/web/static/assets/chunk-DI55MBZ5-BDmF9Bh-.js +220 -0
- pythinker_code/web/static/assets/chunk-FMBD7UC4-BCse_HmM.js +15 -0
- pythinker_code/web/static/assets/chunk-QN33PNHL-DCpBmTzA.js +1 -0
- pythinker_code/web/static/assets/chunk-QZHKN3VN-BqLuqobw.js +1 -0
- pythinker_code/web/static/assets/chunk-TZMSLE5B-8K2ogOKS.js +1 -0
- pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
- pythinker_code/web/static/assets/classDiagram-2ON5EDUG-D_ZHSii2.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-D_ZHSii2.js +1 -0
- pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
- pythinker_code/web/static/assets/clone-GSXejyY1.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-DWTFYA28.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-BRI7ES-N.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-B6BxUuKW.js +321 -0
- pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
- pythinker_code/web/static/assets/dagre-6UL2VRFP-Ci5GdWfi.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-0hhAylV4.js +24 -0
- pythinker_code/web/static/assets/diagram-QEK2KX5R-8fxgaW6d.js +43 -0
- pythinker_code/web/static/assets/diagram-S2PKOQOG-FRr0_atE.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-B3T-hJUM.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-D0S3u7ot.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-CHrN2a23.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-CfcXZWg0.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-8jMJwCqE.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-BXrFnzMy.js +153 -0
- pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
- pythinker_code/web/static/assets/index-BrfQJnRD.js +5 -0
- pythinker_code/web/static/assets/index-C4gFzubz.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/infoDiagram-WHAUD3N6-DdxonBf3.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-BXf4aQei.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-DLpPPOu8.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-DH73UoAH.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-bAer2-sK.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-CuqbwKXv.js +465 -0
- pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
- pythinker_code/web/static/assets/mermaid.core-Nx-rTKiV.js +191 -0
- pythinker_code/web/static/assets/min-DbfD8Ywu.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-C6l761Ue.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-fNg41mT9.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-DJz3Kx87.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-B4SbrfE9.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-CoSUjLAG.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-PjhBNHi3.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-DOwESt8-.js +1 -0
- pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-yl3OHWiP.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-CkCLnAgi.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-CZS5XwTf.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-DkqqHNLh.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 +433 -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 +1072 -0
- pythinker_code/wire/types.py +698 -0
- pythinker_code-0.8.0.dist-info/METADATA +706 -0
- pythinker_code-0.8.0.dist-info/RECORD +790 -0
- pythinker_code-0.8.0.dist-info/WHEEL +4 -0
- pythinker_code-0.8.0.dist-info/entry_points.txt +4 -0
- pythinker_code-0.8.0.dist-info/licenses/LICENSE +202 -0
- pythinker_code-0.8.0.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,1433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.shortcuts.choice_input import ChoiceInput
|
|
8
|
+
|
|
9
|
+
from pythinker_code.auth.platforms import get_platform_name_for_provider, refresh_managed_models
|
|
10
|
+
from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb
|
|
11
|
+
from pythinker_code.config import load_config, save_config
|
|
12
|
+
from pythinker_code.exception import ConfigError
|
|
13
|
+
from pythinker_code.session import Session
|
|
14
|
+
from pythinker_code.soul.pythinkersoul import PythinkerSoul
|
|
15
|
+
from pythinker_code.ui.shell.console import console
|
|
16
|
+
from pythinker_code.ui.shell.mcp_status import render_mcp_console
|
|
17
|
+
from pythinker_code.ui.shell.task_browser import TaskBrowserApp
|
|
18
|
+
from pythinker_code.utils.changelog import CHANGELOG
|
|
19
|
+
from pythinker_code.utils.logging import logger
|
|
20
|
+
from pythinker_code.utils.slashcmd import SlashCommand, SlashCommandRegistry
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pythinker_code.ui.shell import Shell
|
|
24
|
+
|
|
25
|
+
type ShellSlashCmdFunc = Callable[[Shell, str], None | Awaitable[None]]
|
|
26
|
+
"""
|
|
27
|
+
A function that runs as a Shell-level slash command.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
Reload: When the configuration should be reloaded.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
registry = SlashCommandRegistry[ShellSlashCmdFunc]()
|
|
35
|
+
shell_mode_registry = SlashCommandRegistry[ShellSlashCmdFunc]()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_pythinker_soul(app: Shell) -> PythinkerSoul | None:
|
|
39
|
+
if not isinstance(app.soul, PythinkerSoul):
|
|
40
|
+
console.print("[red]PythinkerSoul required[/red]")
|
|
41
|
+
return None
|
|
42
|
+
return app.soul
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@registry.command(aliases=["quit"])
|
|
46
|
+
@shell_mode_registry.command(aliases=["quit"])
|
|
47
|
+
def exit(app: Shell, args: str):
|
|
48
|
+
"""Exit the application"""
|
|
49
|
+
# should be handled by `Shell`
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
SKILL_COMMAND_PREFIX = "skill:"
|
|
54
|
+
|
|
55
|
+
_KEYBOARD_SHORTCUTS = [
|
|
56
|
+
("Ctrl-X", "Toggle agent/shell mode"),
|
|
57
|
+
("Shift-Tab", "Toggle plan mode (read-only research)"),
|
|
58
|
+
("Ctrl-O", "Edit in external editor ($VISUAL/$EDITOR)"),
|
|
59
|
+
("Ctrl-J / Alt-Enter", "Insert newline"),
|
|
60
|
+
("Ctrl-V", "Paste (supports images)"),
|
|
61
|
+
("Ctrl-D", "Exit"),
|
|
62
|
+
("Ctrl-C", "Interrupt"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@registry.command(aliases=["h", "?"])
|
|
67
|
+
@shell_mode_registry.command(aliases=["h", "?"])
|
|
68
|
+
def help(app: Shell, args: str):
|
|
69
|
+
"""Show help information"""
|
|
70
|
+
from rich.console import Group, RenderableType
|
|
71
|
+
from rich.text import Text
|
|
72
|
+
|
|
73
|
+
from pythinker_code.utils.rich.columns import BulletColumns
|
|
74
|
+
|
|
75
|
+
def section(title: str, items: list[tuple[str, str]], color: str) -> BulletColumns:
|
|
76
|
+
lines: list[RenderableType] = [Text.from_markup(f"[bold]{title}:[/bold]")]
|
|
77
|
+
for name, desc in items:
|
|
78
|
+
lines.append(
|
|
79
|
+
BulletColumns(
|
|
80
|
+
Text.from_markup(f"[{color}]{name}[/{color}]: [grey50]{desc}[/grey50]"),
|
|
81
|
+
bullet_style=color,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return BulletColumns(Group(*lines))
|
|
85
|
+
|
|
86
|
+
renderables: list[RenderableType] = []
|
|
87
|
+
renderables.append(
|
|
88
|
+
BulletColumns(
|
|
89
|
+
Group(
|
|
90
|
+
Text.from_markup("[grey50]Help! I need somebody. Help! Not just anybody.[/grey50]"),
|
|
91
|
+
Text.from_markup("[grey50]Help! You know I need someone. Help![/grey50]"),
|
|
92
|
+
Text.from_markup("[grey50]\u2015 The Beatles, [italic]Help![/italic][/grey50]"),
|
|
93
|
+
),
|
|
94
|
+
bullet_style="grey50",
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
renderables.append(
|
|
98
|
+
BulletColumns(
|
|
99
|
+
Text(
|
|
100
|
+
"Sure, Pythinker is ready to help! "
|
|
101
|
+
"Just send me messages and I will help you get things done!"
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
commands: list[SlashCommand[Any]] = []
|
|
107
|
+
skills: list[SlashCommand[Any]] = []
|
|
108
|
+
for cmd in app.available_slash_commands.values():
|
|
109
|
+
if cmd.name.startswith(SKILL_COMMAND_PREFIX):
|
|
110
|
+
skills.append(cmd)
|
|
111
|
+
else:
|
|
112
|
+
commands.append(cmd)
|
|
113
|
+
|
|
114
|
+
renderables.append(section("Keyboard shortcuts", _KEYBOARD_SHORTCUTS, "yellow"))
|
|
115
|
+
renderables.append(
|
|
116
|
+
section(
|
|
117
|
+
"Slash commands",
|
|
118
|
+
[(c.slash_name(), c.description) for c in sorted(commands, key=lambda c: c.name)],
|
|
119
|
+
"blue",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
if skills:
|
|
123
|
+
renderables.append(
|
|
124
|
+
section(
|
|
125
|
+
"Skills",
|
|
126
|
+
[(c.slash_name(), c.description) for c in sorted(skills, key=lambda c: c.name)],
|
|
127
|
+
"cyan",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
with console.pager(styles=True):
|
|
132
|
+
console.print(Group(*renderables))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@registry.command
|
|
136
|
+
@shell_mode_registry.command
|
|
137
|
+
def version(app: Shell, args: str):
|
|
138
|
+
"""Show version information"""
|
|
139
|
+
from pythinker_code.constant import VERSION
|
|
140
|
+
|
|
141
|
+
console.print(f"pythinker, version {VERSION}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@registry.command
|
|
145
|
+
@shell_mode_registry.command
|
|
146
|
+
def agents(app: Shell, args: str):
|
|
147
|
+
"""List available subagent types"""
|
|
148
|
+
from rich import box
|
|
149
|
+
from rich.panel import Panel
|
|
150
|
+
from rich.table import Table
|
|
151
|
+
from rich.text import Text
|
|
152
|
+
|
|
153
|
+
soul = ensure_pythinker_soul(app)
|
|
154
|
+
if soul is None:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
labor_market = getattr(soul.runtime, "labor_market", None)
|
|
158
|
+
builtin_types = getattr(labor_market, "builtin_types", {}) or {}
|
|
159
|
+
type_defs = sorted(builtin_types.values(), key=lambda item: item.name)
|
|
160
|
+
if not type_defs:
|
|
161
|
+
console.print("[yellow]No subagents are registered for this agent.[/yellow]")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
table = Table.grid(expand=True)
|
|
165
|
+
table.add_column(ratio=1, no_wrap=True)
|
|
166
|
+
table.add_column(ratio=4)
|
|
167
|
+
table.add_column(ratio=1, no_wrap=True)
|
|
168
|
+
table.add_column(ratio=2, no_wrap=True)
|
|
169
|
+
table.add_row(
|
|
170
|
+
Text("agent", style="bold cyan"),
|
|
171
|
+
Text("when to use", style="bold"),
|
|
172
|
+
Text("model", style="bold"),
|
|
173
|
+
Text("tools", style="bold"),
|
|
174
|
+
)
|
|
175
|
+
for type_def in type_defs:
|
|
176
|
+
tool_policy = getattr(type_def, "tool_policy", None)
|
|
177
|
+
tool_label = (
|
|
178
|
+
f"allow {len(tool_policy.tools)}"
|
|
179
|
+
if tool_policy is not None and tool_policy.mode == "allowlist"
|
|
180
|
+
else "inherit"
|
|
181
|
+
)
|
|
182
|
+
table.add_row(
|
|
183
|
+
Text(type_def.name, style="cyan"),
|
|
184
|
+
Text(type_def.when_to_use or type_def.description or "—", style="grey70"),
|
|
185
|
+
Text(type_def.default_model or "inherit", style="grey58"),
|
|
186
|
+
Text(tool_label, style="grey58"),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
footer = Text("Use the Agent tool with subagent_type=<agent>, or ask Pythinker to delegate.")
|
|
190
|
+
footer.stylize("grey58")
|
|
191
|
+
console.print(
|
|
192
|
+
Panel(
|
|
193
|
+
table,
|
|
194
|
+
title="[bold cyan]Agents[/bold cyan]",
|
|
195
|
+
subtitle=footer,
|
|
196
|
+
border_style="cyan",
|
|
197
|
+
box=box.ROUNDED,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@registry.command
|
|
203
|
+
async def model(app: Shell, args: str):
|
|
204
|
+
"""Switch LLM model or thinking mode"""
|
|
205
|
+
from pythinker_code.llm import derive_model_capabilities
|
|
206
|
+
|
|
207
|
+
soul = ensure_pythinker_soul(app)
|
|
208
|
+
if soul is None:
|
|
209
|
+
return
|
|
210
|
+
config = soul.runtime.config
|
|
211
|
+
|
|
212
|
+
await refresh_managed_models(config)
|
|
213
|
+
|
|
214
|
+
if not config.models:
|
|
215
|
+
console.print('[yellow]No models configured, send "/login" to login.[/yellow]')
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if not config.is_from_default_location:
|
|
219
|
+
console.print(
|
|
220
|
+
"[yellow]Model switching requires the default config file; "
|
|
221
|
+
"restart without --config/--config-file.[/yellow]"
|
|
222
|
+
)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
# Find current model/thinking from runtime (may be overridden by --model/--thinking)
|
|
226
|
+
curr_model_cfg = soul.runtime.llm.model_config if soul.runtime.llm else None
|
|
227
|
+
curr_model_name: str | None = None
|
|
228
|
+
if curr_model_cfg is not None:
|
|
229
|
+
for name, model_cfg in config.models.items():
|
|
230
|
+
if model_cfg == curr_model_cfg:
|
|
231
|
+
curr_model_name = name
|
|
232
|
+
break
|
|
233
|
+
curr_thinking = soul.thinking
|
|
234
|
+
|
|
235
|
+
# Step 1: Pick a model — single grouped picker with type-to-filter.
|
|
236
|
+
from pythinker_code.ui.shell.model_picker import ModelPickerApp, build_provider_groups
|
|
237
|
+
|
|
238
|
+
groups = build_provider_groups(
|
|
239
|
+
config_models=config.models,
|
|
240
|
+
label_for=_provider_label,
|
|
241
|
+
)
|
|
242
|
+
selected_model_name = await ModelPickerApp(
|
|
243
|
+
groups=groups,
|
|
244
|
+
current_model_name=curr_model_name,
|
|
245
|
+
).run()
|
|
246
|
+
if not selected_model_name:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
selected_model_cfg = config.models[selected_model_name]
|
|
250
|
+
selected_provider = config.providers.get(selected_model_cfg.provider)
|
|
251
|
+
if selected_provider is None:
|
|
252
|
+
console.print(f"[red]Provider not found: {selected_model_cfg.provider}[/red]")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# Step 2: Determine thinking mode
|
|
256
|
+
capabilities = derive_model_capabilities(selected_model_cfg)
|
|
257
|
+
new_thinking: bool
|
|
258
|
+
|
|
259
|
+
if "always_thinking" in capabilities:
|
|
260
|
+
new_thinking = True
|
|
261
|
+
elif "thinking" in capabilities:
|
|
262
|
+
from pythinker_code.ui.shell.selectors.thinking import ThinkingLevel, run_thinking_selector
|
|
263
|
+
|
|
264
|
+
_curr_level: ThinkingLevel = "high" if curr_thinking else "off"
|
|
265
|
+
_level = await run_thinking_selector(
|
|
266
|
+
current_level=_curr_level,
|
|
267
|
+
available_levels=["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
268
|
+
)
|
|
269
|
+
if _level is None:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
new_thinking = _level != "off"
|
|
273
|
+
else:
|
|
274
|
+
new_thinking = False
|
|
275
|
+
|
|
276
|
+
# Check if anything changed
|
|
277
|
+
model_changed = curr_model_name != selected_model_name
|
|
278
|
+
thinking_changed = curr_thinking != new_thinking
|
|
279
|
+
selected_display = selected_model_cfg.display_name or selected_model_cfg.model
|
|
280
|
+
|
|
281
|
+
if not model_changed and not thinking_changed:
|
|
282
|
+
console.print(
|
|
283
|
+
f"[yellow]Already using {selected_display} "
|
|
284
|
+
f"with thinking {'on' if new_thinking else 'off'}.[/yellow]"
|
|
285
|
+
)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# Save and reload
|
|
289
|
+
prev_model = config.default_model
|
|
290
|
+
prev_thinking = config.default_thinking
|
|
291
|
+
config.default_model = selected_model_name
|
|
292
|
+
config.default_thinking = new_thinking
|
|
293
|
+
try:
|
|
294
|
+
config_for_save = load_config()
|
|
295
|
+
config_for_save.default_model = selected_model_name
|
|
296
|
+
config_for_save.default_thinking = new_thinking
|
|
297
|
+
save_config(config_for_save)
|
|
298
|
+
except (ConfigError, OSError) as exc:
|
|
299
|
+
config.default_model = prev_model
|
|
300
|
+
config.default_thinking = prev_thinking
|
|
301
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
from pythinker_code.telemetry import track
|
|
305
|
+
|
|
306
|
+
if model_changed:
|
|
307
|
+
track("model_switch", model=selected_model_name)
|
|
308
|
+
if thinking_changed:
|
|
309
|
+
track("thinking_toggle", enabled=new_thinking)
|
|
310
|
+
console.print(
|
|
311
|
+
f"[green]Switched to {selected_display} "
|
|
312
|
+
f"with thinking {'on' if new_thinking else 'off'}. "
|
|
313
|
+
"Reloading...[/green]"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Pre-load LM Studio models so the user doesn't hit a 10-60s wait on
|
|
317
|
+
# the first message. Fire-and-forget on error — Reload still proceeds.
|
|
318
|
+
if model_changed and selected_model_cfg.provider == "managed:lm-studio":
|
|
319
|
+
await _preload_lm_studio_model(selected_provider, selected_model_cfg.model)
|
|
320
|
+
|
|
321
|
+
raise Reload(session_id=soul.runtime.session.id)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
_PROVIDER_LABEL_OVERRIDES = {
|
|
325
|
+
"managed:minimax-anthropic": "MiniMax",
|
|
326
|
+
"managed:opencode-go-openai": "OpenCode Go (OpenAI)",
|
|
327
|
+
"managed:opencode-go-anthropic": "OpenCode Go (Anthropic)",
|
|
328
|
+
"managed:deepseek": "DeepSeek",
|
|
329
|
+
"managed:anthropic": "Anthropic",
|
|
330
|
+
"managed:openrouter": "OpenRouter",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _provider_label(provider_key: str) -> str:
|
|
335
|
+
if name := get_platform_name_for_provider(provider_key):
|
|
336
|
+
return name
|
|
337
|
+
if name := _PROVIDER_LABEL_OVERRIDES.get(provider_key):
|
|
338
|
+
return name
|
|
339
|
+
# Fall back: strip "managed:" prefix and humanize.
|
|
340
|
+
raw = provider_key.removeprefix("managed:")
|
|
341
|
+
return raw.replace("-", " ").title() if raw else provider_key
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
async def _preload_lm_studio_model(provider: Any, model_id: str) -> None:
|
|
345
|
+
"""Best-effort: ask LM Studio to load the model now."""
|
|
346
|
+
from rich.status import Status
|
|
347
|
+
|
|
348
|
+
from pythinker_code.auth.lm_studio import request_lm_studio_load
|
|
349
|
+
|
|
350
|
+
base_url = provider.base_url
|
|
351
|
+
api_key = provider.api_key.get_secret_value()
|
|
352
|
+
status_msg = f"Pre-loading {model_id} in LM Studio (this may take a moment)..."
|
|
353
|
+
status = Status(status_msg, console=console)
|
|
354
|
+
status.start()
|
|
355
|
+
try:
|
|
356
|
+
result = await request_lm_studio_load(base_url=base_url, model_id=model_id, api_key=api_key)
|
|
357
|
+
status.stop()
|
|
358
|
+
console.print(
|
|
359
|
+
f"[dim]LM Studio loaded {model_id} in "
|
|
360
|
+
f"{result.load_time_seconds:.1f}s (status={result.status}).[/dim]"
|
|
361
|
+
)
|
|
362
|
+
except Exception as exc:
|
|
363
|
+
status.stop()
|
|
364
|
+
console.print(
|
|
365
|
+
f"[yellow]LM Studio pre-load failed for {model_id}: {exc}[/yellow]\n"
|
|
366
|
+
"[dim]The chat will still try the model on first message.[/dim]"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@registry.command
|
|
371
|
+
@shell_mode_registry.command
|
|
372
|
+
async def editor(app: Shell, args: str):
|
|
373
|
+
"""Set default external editor for Ctrl-O"""
|
|
374
|
+
from pythinker_code.utils.editor import get_editor_command
|
|
375
|
+
|
|
376
|
+
soul = ensure_pythinker_soul(app)
|
|
377
|
+
if soul is None:
|
|
378
|
+
return
|
|
379
|
+
config = soul.runtime.config
|
|
380
|
+
config_file = config.source_file
|
|
381
|
+
if config_file is None:
|
|
382
|
+
console.print(
|
|
383
|
+
"[yellow]Editor switching is unavailable with inline --config; "
|
|
384
|
+
"use --config-file to persist this setting.[/yellow]"
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
current_editor = config.default_editor
|
|
389
|
+
|
|
390
|
+
# If args provided directly, use as editor command
|
|
391
|
+
if args.strip():
|
|
392
|
+
new_editor = args.strip()
|
|
393
|
+
else:
|
|
394
|
+
options: list[tuple[str, str]] = [
|
|
395
|
+
("code --wait", "VS Code (code --wait)"),
|
|
396
|
+
("vim", "Vim"),
|
|
397
|
+
("nano", "Nano"),
|
|
398
|
+
("", "Auto-detect (use $VISUAL/$EDITOR)"),
|
|
399
|
+
]
|
|
400
|
+
# Mark current selection
|
|
401
|
+
options = [
|
|
402
|
+
(val, label + (" ← current" if val == current_editor else "")) for val, label in options
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
choice = cast(
|
|
407
|
+
str | None,
|
|
408
|
+
await ChoiceInput(
|
|
409
|
+
message="Select an editor (↑↓ navigate, Enter select, Ctrl+C cancel):",
|
|
410
|
+
options=options,
|
|
411
|
+
default=(
|
|
412
|
+
current_editor
|
|
413
|
+
if current_editor in {v for v, _ in options}
|
|
414
|
+
else "code --wait"
|
|
415
|
+
),
|
|
416
|
+
).prompt_async(),
|
|
417
|
+
)
|
|
418
|
+
except (EOFError, KeyboardInterrupt):
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
if choice is None:
|
|
422
|
+
return
|
|
423
|
+
new_editor = choice
|
|
424
|
+
|
|
425
|
+
# Validate the editor binary is available
|
|
426
|
+
if new_editor:
|
|
427
|
+
import shlex
|
|
428
|
+
import shutil
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
parts = shlex.split(new_editor)
|
|
432
|
+
except ValueError:
|
|
433
|
+
console.print(f"[red]Invalid editor command: {new_editor}[/red]")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
binary = parts[0]
|
|
437
|
+
if not shutil.which(binary):
|
|
438
|
+
console.print(
|
|
439
|
+
f"[yellow]Warning: '{binary}' not found in PATH. "
|
|
440
|
+
f"Saving anyway — make sure it's installed before using Ctrl-O.[/yellow]"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if new_editor == current_editor:
|
|
444
|
+
console.print(f"[yellow]Editor is already set to: {new_editor or 'auto-detect'}[/yellow]")
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
# Save to disk
|
|
448
|
+
try:
|
|
449
|
+
config_for_save = load_config(config_file)
|
|
450
|
+
config_for_save.default_editor = new_editor
|
|
451
|
+
save_config(config_for_save, config_file)
|
|
452
|
+
except (ConfigError, OSError) as exc:
|
|
453
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Sync in-memory config so Ctrl-O picks it up immediately
|
|
457
|
+
config.default_editor = new_editor
|
|
458
|
+
|
|
459
|
+
if new_editor:
|
|
460
|
+
console.print(f"[green]Editor set to: {new_editor}[/green]")
|
|
461
|
+
else:
|
|
462
|
+
resolved = get_editor_command()
|
|
463
|
+
label = " ".join(resolved) if resolved else "none"
|
|
464
|
+
console.print(f"[green]Editor set to auto-detect (resolved: {label})[/green]")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@registry.command(aliases=["release-notes"])
|
|
468
|
+
@shell_mode_registry.command(aliases=["release-notes"])
|
|
469
|
+
def changelog(app: Shell, args: str):
|
|
470
|
+
"""Show release notes"""
|
|
471
|
+
from rich.console import Group, RenderableType
|
|
472
|
+
from rich.text import Text
|
|
473
|
+
|
|
474
|
+
from pythinker_code.utils.rich.columns import BulletColumns
|
|
475
|
+
|
|
476
|
+
renderables: list[RenderableType] = []
|
|
477
|
+
for ver, entry in CHANGELOG.items():
|
|
478
|
+
title = f"[bold]{ver}[/bold]"
|
|
479
|
+
if entry.description:
|
|
480
|
+
title += f": {entry.description}"
|
|
481
|
+
|
|
482
|
+
lines: list[RenderableType] = [Text.from_markup(title)]
|
|
483
|
+
for item in entry.entries:
|
|
484
|
+
if item.lower().startswith("lib:"):
|
|
485
|
+
continue
|
|
486
|
+
lines.append(
|
|
487
|
+
BulletColumns(
|
|
488
|
+
Text.from_markup(f"[grey50]{item}[/grey50]"),
|
|
489
|
+
bullet_style="grey50",
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
renderables.append(BulletColumns(Group(*lines)))
|
|
493
|
+
|
|
494
|
+
with console.pager(styles=True):
|
|
495
|
+
console.print(Group(*renderables))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _feedback_destination(soul: PythinkerSoul) -> tuple[str, dict[str, str]] | None:
|
|
499
|
+
"""Resolve the endpoint and headers for explicit user feedback submissions."""
|
|
500
|
+
import os
|
|
501
|
+
|
|
502
|
+
from pythinker_code.auth import PYTHINKER_CODE_PLATFORM_ID
|
|
503
|
+
from pythinker_code.auth.platforms import get_platform_by_id, managed_provider_key
|
|
504
|
+
|
|
505
|
+
headers: dict[str, str] = {}
|
|
506
|
+
feedback_config = soul.runtime.config.feedback
|
|
507
|
+
|
|
508
|
+
endpoint_url = os.getenv("PYTHINKER_FEEDBACK_URL", "").strip()
|
|
509
|
+
if not endpoint_url:
|
|
510
|
+
endpoint_url = feedback_config.endpoint_url.strip()
|
|
511
|
+
|
|
512
|
+
if endpoint_url:
|
|
513
|
+
if feedback_config.custom_headers:
|
|
514
|
+
headers.update(feedback_config.custom_headers)
|
|
515
|
+
api_key = os.getenv("PYTHINKER_FEEDBACK_API_KEY", "").strip()
|
|
516
|
+
if not api_key and feedback_config.api_key is not None:
|
|
517
|
+
api_key = feedback_config.api_key.get_secret_value()
|
|
518
|
+
if api_key:
|
|
519
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
520
|
+
return endpoint_url, headers
|
|
521
|
+
|
|
522
|
+
pythinker_platform = get_platform_by_id(PYTHINKER_CODE_PLATFORM_ID)
|
|
523
|
+
if pythinker_platform is None:
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
provider = soul.runtime.config.providers.get(managed_provider_key(PYTHINKER_CODE_PLATFORM_ID))
|
|
527
|
+
if provider is not None:
|
|
528
|
+
if provider.custom_headers:
|
|
529
|
+
headers.update(provider.custom_headers)
|
|
530
|
+
api_key = provider.api_key.get_secret_value()
|
|
531
|
+
if provider.oauth is not None:
|
|
532
|
+
api_key = soul.runtime.oauth.resolve_api_key(provider.api_key, provider.oauth)
|
|
533
|
+
if api_key:
|
|
534
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
535
|
+
|
|
536
|
+
return f"{pythinker_platform.base_url.rstrip('/')}/feedback", headers
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _feedback_github_config(soul: PythinkerSoul) -> tuple[str, str] | None:
|
|
540
|
+
"""Return GitHub OAuth client_id and repo when direct user-owned issues are enabled."""
|
|
541
|
+
import os
|
|
542
|
+
|
|
543
|
+
feedback_config = soul.runtime.config.feedback
|
|
544
|
+
client_id = os.getenv("PYTHINKER_FEEDBACK_GITHUB_CLIENT_ID", "").strip()
|
|
545
|
+
if not client_id:
|
|
546
|
+
client_id = feedback_config.github_client_id.strip()
|
|
547
|
+
repo = os.getenv("PYTHINKER_FEEDBACK_GITHUB_REPO", "").strip()
|
|
548
|
+
if not repo:
|
|
549
|
+
repo = feedback_config.github_repo.strip()
|
|
550
|
+
if not client_id or not repo:
|
|
551
|
+
return None
|
|
552
|
+
return client_id, repo
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _feedback_issue_title(payload: dict[str, str | None]) -> str:
|
|
556
|
+
version = f" {payload['version']}" if payload.get("version") else ""
|
|
557
|
+
session = payload.get("session_id") or ""
|
|
558
|
+
suffix = f" ({session[:8]})" if session else ""
|
|
559
|
+
return f"[Pythinker CLI] Feedback{version}{suffix}"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _feedback_issue_body(payload: dict[str, str | None]) -> str:
|
|
563
|
+
return "\n".join(
|
|
564
|
+
[
|
|
565
|
+
"## User submission",
|
|
566
|
+
"",
|
|
567
|
+
payload.get("content") or "_(no comment)_",
|
|
568
|
+
"",
|
|
569
|
+
"## Context",
|
|
570
|
+
"",
|
|
571
|
+
"- Type: feedback",
|
|
572
|
+
f"- Session: {payload.get('session_id') or 'unknown'}",
|
|
573
|
+
f"- Version: {payload.get('version') or 'unknown'}",
|
|
574
|
+
f"- OS: {payload.get('os') or 'unknown'}",
|
|
575
|
+
f"- Model: {payload.get('model') or 'unknown'}",
|
|
576
|
+
]
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@registry.command
|
|
581
|
+
@shell_mode_registry.command
|
|
582
|
+
async def feedback(app: Shell, args: str):
|
|
583
|
+
"""Submit feedback to make Pythinker CLI better"""
|
|
584
|
+
import platform
|
|
585
|
+
import webbrowser
|
|
586
|
+
|
|
587
|
+
import aiohttp
|
|
588
|
+
|
|
589
|
+
from pythinker_code.constant import VERSION
|
|
590
|
+
from pythinker_code.ui.shell.oauth import current_model_key
|
|
591
|
+
from pythinker_code.utils.aiohttp import new_client_session
|
|
592
|
+
|
|
593
|
+
ISSUE_URL = "https://github.com/mohamed-elkholy95/Pythinker-Code/issues"
|
|
594
|
+
|
|
595
|
+
def _fallback_to_issues():
|
|
596
|
+
if not webbrowser.open(ISSUE_URL):
|
|
597
|
+
console.print(f"Please submit feedback at [underline]{ISSUE_URL}[/underline].")
|
|
598
|
+
|
|
599
|
+
soul = ensure_pythinker_soul(app)
|
|
600
|
+
if soul is None:
|
|
601
|
+
_fallback_to_issues()
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
github_config = _feedback_github_config(soul)
|
|
605
|
+
destination = None if github_config is not None else _feedback_destination(soul)
|
|
606
|
+
if github_config is None and destination is None:
|
|
607
|
+
_fallback_to_issues()
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
from prompt_toolkit import PromptSession
|
|
611
|
+
|
|
612
|
+
prompt_session: PromptSession[str] = PromptSession()
|
|
613
|
+
try:
|
|
614
|
+
content = await prompt_session.prompt_async("Enter your feedback: ")
|
|
615
|
+
except (EOFError, KeyboardInterrupt):
|
|
616
|
+
console.print("[grey50]Feedback cancelled.[/grey50]")
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
content = content.strip()
|
|
620
|
+
if not content:
|
|
621
|
+
console.print("[yellow]Feedback cannot be empty.[/yellow]")
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
payload = {
|
|
625
|
+
"session_id": soul.runtime.session.id,
|
|
626
|
+
"content": content,
|
|
627
|
+
"version": VERSION,
|
|
628
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
629
|
+
"model": current_model_key(soul),
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if github_config is not None:
|
|
633
|
+
client_id, repo = github_config
|
|
634
|
+
from pythinker_code.auth.github_feedback import (
|
|
635
|
+
GitHubFeedbackError,
|
|
636
|
+
create_github_issue,
|
|
637
|
+
load_github_feedback_token,
|
|
638
|
+
login_github_feedback,
|
|
639
|
+
star_github_repo,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
token = load_github_feedback_token()
|
|
644
|
+
if token is None:
|
|
645
|
+
console.print("[cyan]GitHub login required to create the issue as you.[/cyan]")
|
|
646
|
+
async for event in login_github_feedback(client_id):
|
|
647
|
+
if event.type == "waiting":
|
|
648
|
+
console.print(event.message, markup=False)
|
|
649
|
+
elif event.type in {"verification_url", "success", "error"}:
|
|
650
|
+
style = None
|
|
651
|
+
if event.type == "success":
|
|
652
|
+
style = "green"
|
|
653
|
+
elif event.type == "error":
|
|
654
|
+
style = "red"
|
|
655
|
+
console.print(event.message, markup=False, style=style)
|
|
656
|
+
token = load_github_feedback_token()
|
|
657
|
+
if token is None:
|
|
658
|
+
console.print("[red]GitHub login did not produce a usable token.[/red]")
|
|
659
|
+
return
|
|
660
|
+
with console.status("[cyan]Creating GitHub issue...[/cyan]"):
|
|
661
|
+
issue = await create_github_issue(
|
|
662
|
+
repo,
|
|
663
|
+
token,
|
|
664
|
+
title=_feedback_issue_title(payload),
|
|
665
|
+
body=_feedback_issue_body(payload),
|
|
666
|
+
)
|
|
667
|
+
from pythinker_code.telemetry import track
|
|
668
|
+
|
|
669
|
+
track("feedback_submitted", destination="github")
|
|
670
|
+
if issue.html_url:
|
|
671
|
+
console.print(f"[green]GitHub issue created:[/green] {issue.html_url}")
|
|
672
|
+
else:
|
|
673
|
+
console.print("[green]GitHub issue created.[/green]")
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
star_answer = await prompt_session.prompt_async(
|
|
677
|
+
"Do you like Pythinker CLI? Star the GitHub repo? [y/N]: "
|
|
678
|
+
)
|
|
679
|
+
except (EOFError, KeyboardInterrupt):
|
|
680
|
+
star_answer = ""
|
|
681
|
+
if star_answer.strip().lower() in {"y", "yes"}:
|
|
682
|
+
try:
|
|
683
|
+
with console.status("[cyan]Starring GitHub repo...[/cyan]"):
|
|
684
|
+
await star_github_repo(repo, token)
|
|
685
|
+
track("github_repo_starred")
|
|
686
|
+
console.print("[green]Thanks for starring the repo![/green]")
|
|
687
|
+
except (GitHubFeedbackError, TimeoutError, aiohttp.ClientError) as e:
|
|
688
|
+
console.print(f"[yellow]Could not star the repo: {e}[/yellow]")
|
|
689
|
+
except (GitHubFeedbackError, TimeoutError, aiohttp.ClientError) as e:
|
|
690
|
+
console.print(f"[red]Failed to create GitHub issue: {e}[/red]")
|
|
691
|
+
_fallback_to_issues()
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
assert destination is not None
|
|
695
|
+
feedback_url, headers = destination
|
|
696
|
+
|
|
697
|
+
with console.status("[cyan]Submitting feedback...[/cyan]"):
|
|
698
|
+
try:
|
|
699
|
+
async with (
|
|
700
|
+
new_client_session() as session,
|
|
701
|
+
session.post(
|
|
702
|
+
feedback_url,
|
|
703
|
+
json=payload,
|
|
704
|
+
headers=headers,
|
|
705
|
+
raise_for_status=True,
|
|
706
|
+
),
|
|
707
|
+
):
|
|
708
|
+
pass
|
|
709
|
+
session_id = soul.runtime.session.id
|
|
710
|
+
from pythinker_code.telemetry import track
|
|
711
|
+
|
|
712
|
+
track("feedback_submitted")
|
|
713
|
+
console.print(
|
|
714
|
+
f"[green]Feedback submitted, thank you! Your session ID is: {session_id}[/green]"
|
|
715
|
+
)
|
|
716
|
+
except TimeoutError:
|
|
717
|
+
console.print("[red]Feedback submission timed out.[/red]")
|
|
718
|
+
_fallback_to_issues()
|
|
719
|
+
except aiohttp.ClientError as e:
|
|
720
|
+
status = getattr(e, "status", None)
|
|
721
|
+
if status:
|
|
722
|
+
msg = f"Failed to submit feedback (HTTP {status})."
|
|
723
|
+
else:
|
|
724
|
+
msg = "Network error, failed to submit feedback."
|
|
725
|
+
console.print(f"[red]{msg}[/red]")
|
|
726
|
+
_fallback_to_issues()
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@registry.command(aliases=["report-error", "report"])
|
|
730
|
+
@shell_mode_registry.command(aliases=["report-error", "report"])
|
|
731
|
+
async def report_error(app: Shell, args: str):
|
|
732
|
+
"""Submit a report about an error you hit, with a snapshot of recent failures."""
|
|
733
|
+
import platform
|
|
734
|
+
import webbrowser
|
|
735
|
+
|
|
736
|
+
import aiohttp
|
|
737
|
+
|
|
738
|
+
from pythinker_code.constant import VERSION
|
|
739
|
+
from pythinker_code.telemetry.errors import clear_recent_errors, recent_errors
|
|
740
|
+
from pythinker_code.ui.shell.oauth import current_model_key
|
|
741
|
+
from pythinker_code.utils.aiohttp import new_client_session
|
|
742
|
+
|
|
743
|
+
ISSUE_URL = "https://github.com/mohamed-elkholy95/Pythinker-Code/issues"
|
|
744
|
+
|
|
745
|
+
def _fallback_to_issues():
|
|
746
|
+
if not webbrowser.open(ISSUE_URL):
|
|
747
|
+
console.print(f"Please file the report at [underline]{ISSUE_URL}[/underline].")
|
|
748
|
+
|
|
749
|
+
soul = ensure_pythinker_soul(app)
|
|
750
|
+
if soul is None:
|
|
751
|
+
_fallback_to_issues()
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
destination = _feedback_destination(soul)
|
|
755
|
+
if destination is None:
|
|
756
|
+
_fallback_to_issues()
|
|
757
|
+
return
|
|
758
|
+
feedback_url, headers = destination
|
|
759
|
+
|
|
760
|
+
errors = recent_errors()
|
|
761
|
+
if errors:
|
|
762
|
+
console.print("[bold]Recent errors this session (most recent last):[/bold]")
|
|
763
|
+
for i, err in enumerate(errors, start=1):
|
|
764
|
+
tool_part = f" (tool={err.tool})" if err.tool else ""
|
|
765
|
+
console.print(f" [dim]{i}.[/dim] [cyan]{err.site}[/cyan]{tool_part}: {err.exc_class}")
|
|
766
|
+
else:
|
|
767
|
+
console.print(
|
|
768
|
+
"[grey50]No errors recorded this session. "
|
|
769
|
+
"You can still attach a comment describing what went wrong.[/grey50]"
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
from prompt_toolkit import PromptSession
|
|
773
|
+
|
|
774
|
+
prompt_session: PromptSession[str] = PromptSession()
|
|
775
|
+
try:
|
|
776
|
+
comment = await prompt_session.prompt_async("Describe what went wrong (Ctrl-C to cancel): ")
|
|
777
|
+
except (EOFError, KeyboardInterrupt):
|
|
778
|
+
console.print("[grey50]Report cancelled.[/grey50]")
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
comment = comment.strip()
|
|
782
|
+
if not comment and not errors:
|
|
783
|
+
console.print("[yellow]Nothing to report. Cancelled.[/yellow]")
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
payload = {
|
|
787
|
+
"session_id": soul.runtime.session.id,
|
|
788
|
+
"type": "error",
|
|
789
|
+
"content": comment,
|
|
790
|
+
"version": VERSION,
|
|
791
|
+
"os": f"{platform.system()} {platform.release()}",
|
|
792
|
+
"model": current_model_key(soul),
|
|
793
|
+
"recent_errors": [
|
|
794
|
+
{
|
|
795
|
+
"timestamp": err.timestamp,
|
|
796
|
+
"site": err.site,
|
|
797
|
+
"exc_class": err.exc_class,
|
|
798
|
+
"message": err.message,
|
|
799
|
+
"tool": err.tool,
|
|
800
|
+
}
|
|
801
|
+
for err in errors
|
|
802
|
+
],
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
with console.status("[cyan]Submitting error report...[/cyan]"):
|
|
806
|
+
try:
|
|
807
|
+
async with (
|
|
808
|
+
new_client_session() as session,
|
|
809
|
+
session.post(
|
|
810
|
+
feedback_url,
|
|
811
|
+
json=payload,
|
|
812
|
+
headers=headers,
|
|
813
|
+
raise_for_status=True,
|
|
814
|
+
),
|
|
815
|
+
):
|
|
816
|
+
pass
|
|
817
|
+
from pythinker_code.telemetry import track
|
|
818
|
+
|
|
819
|
+
track("error_report_submitted", error_count=len(errors), has_comment=bool(comment))
|
|
820
|
+
clear_recent_errors()
|
|
821
|
+
console.print(
|
|
822
|
+
f"[green]Error report submitted. Session ID: {soul.runtime.session.id}[/green]"
|
|
823
|
+
)
|
|
824
|
+
except TimeoutError:
|
|
825
|
+
console.print("[red]Submission timed out.[/red]")
|
|
826
|
+
_fallback_to_issues()
|
|
827
|
+
except aiohttp.ClientError as e:
|
|
828
|
+
status = getattr(e, "status", None)
|
|
829
|
+
msg = (
|
|
830
|
+
f"Failed to submit error report (HTTP {status})."
|
|
831
|
+
if status
|
|
832
|
+
else "Network error, failed to submit error report."
|
|
833
|
+
)
|
|
834
|
+
console.print(f"[red]{msg}[/red]")
|
|
835
|
+
_fallback_to_issues()
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@registry.command(aliases=["reset"])
|
|
839
|
+
async def clear(app: Shell, args: str):
|
|
840
|
+
"""Clear the context"""
|
|
841
|
+
if ensure_pythinker_soul(app) is None:
|
|
842
|
+
return
|
|
843
|
+
from pythinker_code.telemetry import track
|
|
844
|
+
|
|
845
|
+
track("clear")
|
|
846
|
+
await app.run_soul_command("/clear")
|
|
847
|
+
raise Reload()
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
@registry.command
|
|
851
|
+
async def new(app: Shell, args: str):
|
|
852
|
+
"""Start a new session"""
|
|
853
|
+
soul = ensure_pythinker_soul(app)
|
|
854
|
+
if soul is None:
|
|
855
|
+
return
|
|
856
|
+
current_session = soul.runtime.session
|
|
857
|
+
work_dir = current_session.work_dir
|
|
858
|
+
# Clean up the current session if it has no content, so that chaining
|
|
859
|
+
# /new commands (or switching away before the first message) does not
|
|
860
|
+
# leave orphan empty session directories on disk.
|
|
861
|
+
if current_session.is_empty():
|
|
862
|
+
await current_session.delete()
|
|
863
|
+
session = await Session.create(work_dir)
|
|
864
|
+
from pythinker_code.telemetry import track
|
|
865
|
+
|
|
866
|
+
track("session_new")
|
|
867
|
+
console.print("[green]New session created. Switching...[/green]")
|
|
868
|
+
raise Reload(session_id=session.id)
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@registry.command(name="title", aliases=["rename"])
|
|
872
|
+
async def title(app: Shell, args: str):
|
|
873
|
+
"""Set or show the session title"""
|
|
874
|
+
soul = ensure_pythinker_soul(app)
|
|
875
|
+
if soul is None:
|
|
876
|
+
return
|
|
877
|
+
session = soul.runtime.session
|
|
878
|
+
if not args.strip():
|
|
879
|
+
console.print(f"Session title: [bold]{session.title}[/bold]")
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
from pythinker_code.session_state import load_session_state, save_session_state
|
|
883
|
+
|
|
884
|
+
new_title = args.strip()[:200]
|
|
885
|
+
# Read-modify-write: load fresh state to avoid overwriting concurrent web changes
|
|
886
|
+
fresh = load_session_state(session.dir)
|
|
887
|
+
fresh.custom_title = new_title
|
|
888
|
+
fresh.title_generated = True
|
|
889
|
+
save_session_state(fresh, session.dir)
|
|
890
|
+
session.state.custom_title = new_title
|
|
891
|
+
session.state.title_generated = True
|
|
892
|
+
session.title = new_title
|
|
893
|
+
console.print(f"[green]Session title set to: {new_title}[/green]")
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
@registry.command(name="sessions", aliases=["resume"])
|
|
897
|
+
async def list_sessions(app: Shell, args: str):
|
|
898
|
+
"""List sessions and resume optionally"""
|
|
899
|
+
import shlex
|
|
900
|
+
|
|
901
|
+
from pythinker_code.ui.shell.session_picker import SessionPickerApp
|
|
902
|
+
|
|
903
|
+
soul = ensure_pythinker_soul(app)
|
|
904
|
+
if soul is None:
|
|
905
|
+
return
|
|
906
|
+
|
|
907
|
+
current_session = soul.runtime.session
|
|
908
|
+
result = await SessionPickerApp(
|
|
909
|
+
work_dir=current_session.work_dir,
|
|
910
|
+
current_session=current_session,
|
|
911
|
+
).run()
|
|
912
|
+
|
|
913
|
+
if result is None:
|
|
914
|
+
return
|
|
915
|
+
|
|
916
|
+
selection, selected_work_dir = result
|
|
917
|
+
|
|
918
|
+
if selection == current_session.id:
|
|
919
|
+
console.print("[yellow]You are already in this session.[/yellow]")
|
|
920
|
+
return
|
|
921
|
+
|
|
922
|
+
if selected_work_dir != current_session.work_dir:
|
|
923
|
+
cmd = f"pythinker --work-dir {shlex.quote(str(selected_work_dir))} --session {selection}"
|
|
924
|
+
console.print(f"[yellow]Session is in a different directory. Run:[/yellow]\n {cmd}")
|
|
925
|
+
return
|
|
926
|
+
|
|
927
|
+
from pythinker_code.telemetry import track
|
|
928
|
+
|
|
929
|
+
track("session_resume")
|
|
930
|
+
console.print(f"[green]Switching to session {selection}...[/green]")
|
|
931
|
+
raise Reload(session_id=selection)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@registry.command(name="task")
|
|
935
|
+
@shell_mode_registry.command(name="task")
|
|
936
|
+
async def task(app: Shell, args: str):
|
|
937
|
+
"""Browse and manage background tasks"""
|
|
938
|
+
soul = ensure_pythinker_soul(app)
|
|
939
|
+
if soul is None:
|
|
940
|
+
return
|
|
941
|
+
if args.strip():
|
|
942
|
+
console.print('[yellow]Usage: "/task" opens the interactive task browser.[/yellow]')
|
|
943
|
+
return
|
|
944
|
+
if soul.runtime.role != "root":
|
|
945
|
+
console.print("[yellow]Background tasks are only available from the root agent.[/yellow]")
|
|
946
|
+
return
|
|
947
|
+
|
|
948
|
+
await TaskBrowserApp(soul).run()
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@registry.command
|
|
952
|
+
@shell_mode_registry.command
|
|
953
|
+
async def theme(app: Shell, args: str) -> None:
|
|
954
|
+
"""Switch terminal color theme — interactive picker when no args given"""
|
|
955
|
+
from pythinker_code.ui.theme import get_active_theme
|
|
956
|
+
|
|
957
|
+
soul = ensure_pythinker_soul(app)
|
|
958
|
+
if soul is None:
|
|
959
|
+
return
|
|
960
|
+
|
|
961
|
+
current = get_active_theme()
|
|
962
|
+
arg = args.strip().lower()
|
|
963
|
+
|
|
964
|
+
if not arg:
|
|
965
|
+
from pythinker_code.ui.shell.selectors.theme import run_theme_selector
|
|
966
|
+
|
|
967
|
+
chosen = await run_theme_selector(
|
|
968
|
+
current_theme=current,
|
|
969
|
+
available_themes=["dark", "light"],
|
|
970
|
+
)
|
|
971
|
+
if chosen is None or chosen == current:
|
|
972
|
+
return
|
|
973
|
+
arg = chosen
|
|
974
|
+
|
|
975
|
+
if arg not in ("dark", "light"):
|
|
976
|
+
console.print(f"[red]Unknown theme: {arg}. Use 'dark' or 'light'.[/red]")
|
|
977
|
+
return
|
|
978
|
+
|
|
979
|
+
if arg == current:
|
|
980
|
+
console.print(f"[yellow]Already using {arg} theme.[/yellow]")
|
|
981
|
+
return
|
|
982
|
+
|
|
983
|
+
config_file = soul.runtime.config.source_file
|
|
984
|
+
if config_file is None:
|
|
985
|
+
console.print(
|
|
986
|
+
"[yellow]Theme switching requires a config file; "
|
|
987
|
+
"restart without --config to persist this setting.[/yellow]"
|
|
988
|
+
)
|
|
989
|
+
return
|
|
990
|
+
|
|
991
|
+
try:
|
|
992
|
+
config_for_save = load_config(config_file)
|
|
993
|
+
config_for_save.theme = arg # type: ignore[assignment]
|
|
994
|
+
save_config(config_for_save, config_file)
|
|
995
|
+
except (ConfigError, OSError) as exc:
|
|
996
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
997
|
+
return
|
|
998
|
+
|
|
999
|
+
from pythinker_code.telemetry import track
|
|
1000
|
+
|
|
1001
|
+
track("theme_switch", theme=arg)
|
|
1002
|
+
console.print(f"[green]Switched to {arg} theme. Reloading...[/green]")
|
|
1003
|
+
raise Reload(session_id=soul.runtime.session.id)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
@registry.command
|
|
1007
|
+
@shell_mode_registry.command
|
|
1008
|
+
async def thinking(app: Shell, args: str) -> None:
|
|
1009
|
+
"""Switch thinking level — interactive picker"""
|
|
1010
|
+
soul = ensure_pythinker_soul(app)
|
|
1011
|
+
if soul is None:
|
|
1012
|
+
return
|
|
1013
|
+
|
|
1014
|
+
from pythinker_code.ui.shell.selectors.thinking import ThinkingLevel, run_thinking_selector
|
|
1015
|
+
|
|
1016
|
+
curr_level: ThinkingLevel = "high" if soul.thinking else "off"
|
|
1017
|
+
level = await run_thinking_selector(
|
|
1018
|
+
current_level=curr_level,
|
|
1019
|
+
available_levels=["off", "minimal", "low", "medium", "high", "xhigh"],
|
|
1020
|
+
)
|
|
1021
|
+
if level is None:
|
|
1022
|
+
return
|
|
1023
|
+
|
|
1024
|
+
new_thinking = level != "off"
|
|
1025
|
+
if new_thinking == soul.thinking:
|
|
1026
|
+
console.print("[yellow]Thinking setting unchanged.[/yellow]")
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
config_file = soul.runtime.config.source_file
|
|
1030
|
+
if config_file is None:
|
|
1031
|
+
console.print(
|
|
1032
|
+
"[yellow]Thinking requires a config file; "
|
|
1033
|
+
"restart without --config to persist this setting.[/yellow]"
|
|
1034
|
+
)
|
|
1035
|
+
return
|
|
1036
|
+
|
|
1037
|
+
try:
|
|
1038
|
+
config_for_save = load_config(config_file)
|
|
1039
|
+
config_for_save.default_thinking = new_thinking
|
|
1040
|
+
save_config(config_for_save, config_file)
|
|
1041
|
+
except (ConfigError, OSError) as exc:
|
|
1042
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
1043
|
+
return
|
|
1044
|
+
|
|
1045
|
+
from pythinker_code.telemetry import track
|
|
1046
|
+
|
|
1047
|
+
track("thinking_toggle", enabled=new_thinking)
|
|
1048
|
+
console.print(
|
|
1049
|
+
f"[green]Thinking {'enabled' if new_thinking else 'disabled'}. Reloading...[/green]"
|
|
1050
|
+
)
|
|
1051
|
+
raise Reload(session_id=soul.runtime.session.id)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
@registry.command
|
|
1055
|
+
@shell_mode_registry.command
|
|
1056
|
+
def keys(app: Shell, args: str):
|
|
1057
|
+
"""List keyboard shortcuts (semantic keymap)"""
|
|
1058
|
+
from rich.console import Group, RenderableType
|
|
1059
|
+
from rich.table import Table
|
|
1060
|
+
from rich.text import Text
|
|
1061
|
+
|
|
1062
|
+
from pythinker_code.ui.shell.keymap import all_keybindings
|
|
1063
|
+
|
|
1064
|
+
bindings = all_keybindings()
|
|
1065
|
+
if not bindings:
|
|
1066
|
+
console.print("[yellow]No keybindings registered.[/yellow]")
|
|
1067
|
+
return
|
|
1068
|
+
|
|
1069
|
+
table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2))
|
|
1070
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
1071
|
+
table.add_column("Keys", style="bold")
|
|
1072
|
+
|
|
1073
|
+
for name in sorted(bindings):
|
|
1074
|
+
keys_text = "/".join(bindings[name])
|
|
1075
|
+
table.add_row(name, keys_text)
|
|
1076
|
+
|
|
1077
|
+
blocks: list[RenderableType] = [
|
|
1078
|
+
Text.from_markup("[bold]Keyboard shortcuts[/bold]"),
|
|
1079
|
+
table,
|
|
1080
|
+
]
|
|
1081
|
+
console.print(Group(*blocks))
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
@registry.command
|
|
1085
|
+
@shell_mode_registry.command
|
|
1086
|
+
def tui(app: Shell, args: str):
|
|
1087
|
+
"""Show or set the TUI style: card or pythinker"""
|
|
1088
|
+
from pythinker_code.ui.tui_config import get_active_tui_style, set_active_tui_style
|
|
1089
|
+
|
|
1090
|
+
soul = ensure_pythinker_soul(app)
|
|
1091
|
+
if soul is None:
|
|
1092
|
+
return
|
|
1093
|
+
|
|
1094
|
+
current = get_active_tui_style()
|
|
1095
|
+
arg = args.strip().lower()
|
|
1096
|
+
|
|
1097
|
+
# Accept "/tui card", "/tui style card", "/tui set card" — all natural shapes.
|
|
1098
|
+
parts = arg.split()
|
|
1099
|
+
if parts and parts[0] in ("style", "set"):
|
|
1100
|
+
parts = parts[1:]
|
|
1101
|
+
target = parts[0] if parts else ""
|
|
1102
|
+
|
|
1103
|
+
if not target:
|
|
1104
|
+
console.print(f"Current TUI style: [bold]{current}[/bold]")
|
|
1105
|
+
console.print("[grey50]Usage: /tui card | /tui pythinker[/grey50]")
|
|
1106
|
+
return
|
|
1107
|
+
|
|
1108
|
+
if target not in ("card", "pythinker"):
|
|
1109
|
+
console.print(f"[red]Unknown style: {target}. Use 'card' or 'pythinker'.[/red]")
|
|
1110
|
+
return
|
|
1111
|
+
|
|
1112
|
+
if target == current:
|
|
1113
|
+
console.print(f"[yellow]Already using {target} style.[/yellow]")
|
|
1114
|
+
return
|
|
1115
|
+
|
|
1116
|
+
config_file = soul.runtime.config.source_file
|
|
1117
|
+
if config_file is None:
|
|
1118
|
+
# Apply in-memory only — useful for one-off testing without a persisted config.
|
|
1119
|
+
set_active_tui_style(target)
|
|
1120
|
+
console.print(
|
|
1121
|
+
f"[green]Switched to {target} style for this session "
|
|
1122
|
+
"(no config file to persist).[/green]"
|
|
1123
|
+
)
|
|
1124
|
+
return
|
|
1125
|
+
|
|
1126
|
+
try:
|
|
1127
|
+
config_for_save = load_config(config_file)
|
|
1128
|
+
config_for_save.tui.style = target # type: ignore[assignment]
|
|
1129
|
+
save_config(config_for_save, config_file)
|
|
1130
|
+
except (ConfigError, OSError) as exc:
|
|
1131
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
1132
|
+
return
|
|
1133
|
+
|
|
1134
|
+
set_active_tui_style(target)
|
|
1135
|
+
if target == "card":
|
|
1136
|
+
# Make sure renderers are present immediately for current session.
|
|
1137
|
+
from pythinker_code.ui.shell.tool_renderers import register_builtin_renderers
|
|
1138
|
+
|
|
1139
|
+
register_builtin_renderers()
|
|
1140
|
+
|
|
1141
|
+
from pythinker_code.telemetry import track
|
|
1142
|
+
|
|
1143
|
+
track("tui_style_switch", style=target)
|
|
1144
|
+
console.print(f"[green]Switched to {target} style. Reloading...[/green]")
|
|
1145
|
+
raise Reload(session_id=soul.runtime.session.id)
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@registry.command
|
|
1149
|
+
@shell_mode_registry.command
|
|
1150
|
+
async def settings(app: Shell, args: str):
|
|
1151
|
+
"""Open the interactive settings panel; use `/settings show` for read-only view"""
|
|
1152
|
+
from rich.console import Group, RenderableType
|
|
1153
|
+
from rich.table import Table
|
|
1154
|
+
from rich.text import Text
|
|
1155
|
+
|
|
1156
|
+
from pythinker_code.ui.theme import get_active_theme
|
|
1157
|
+
from pythinker_code.ui.tui_config import get_active_tui_style
|
|
1158
|
+
|
|
1159
|
+
soul = ensure_pythinker_soul(app)
|
|
1160
|
+
if soul is None:
|
|
1161
|
+
return
|
|
1162
|
+
|
|
1163
|
+
config = soul.runtime.config
|
|
1164
|
+
mode = args.strip().lower()
|
|
1165
|
+
|
|
1166
|
+
def print_settings_table() -> None:
|
|
1167
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1168
|
+
table.add_column(style="cyan", no_wrap=True)
|
|
1169
|
+
table.add_column()
|
|
1170
|
+
|
|
1171
|
+
table.add_row("Theme", get_active_theme())
|
|
1172
|
+
table.add_row("TUI style", get_active_tui_style())
|
|
1173
|
+
table.add_row("Default model", config.default_model or "(none)")
|
|
1174
|
+
table.add_row("Telemetry", "on" if config.telemetry else "off")
|
|
1175
|
+
table.add_row("Default thinking", "on" if config.default_thinking else "off")
|
|
1176
|
+
table.add_row("Show thinking stream", "on" if config.show_thinking_stream else "off")
|
|
1177
|
+
table.add_row("Default yolo", "on" if config.default_yolo else "off")
|
|
1178
|
+
table.add_row("Default plan mode", "on" if config.default_plan_mode else "off")
|
|
1179
|
+
if config.source_file is not None:
|
|
1180
|
+
table.add_row("Config file", str(config.source_file))
|
|
1181
|
+
else:
|
|
1182
|
+
table.add_row("Config file", "(none — runtime overrides only)")
|
|
1183
|
+
|
|
1184
|
+
blocks: list[RenderableType] = [Text.from_markup("[bold]Settings[/bold]"), table]
|
|
1185
|
+
console.print(Group(*blocks))
|
|
1186
|
+
console.print(
|
|
1187
|
+
"[grey50]Tip: /settings opens the interactive panel; "
|
|
1188
|
+
"/theme, /tui, /model, /keys for related controls.[/grey50]"
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
if mode in {"show", "list", "view"}:
|
|
1192
|
+
print_settings_table()
|
|
1193
|
+
return
|
|
1194
|
+
if mode:
|
|
1195
|
+
console.print("[yellow]Usage: /settings [show|list][/yellow]")
|
|
1196
|
+
return
|
|
1197
|
+
|
|
1198
|
+
config_file = config.source_file
|
|
1199
|
+
if config_file is None:
|
|
1200
|
+
print_settings_table()
|
|
1201
|
+
console.print(
|
|
1202
|
+
"[yellow]Interactive settings require a config file; "
|
|
1203
|
+
"restart without --config text to persist settings.[/yellow]"
|
|
1204
|
+
)
|
|
1205
|
+
return
|
|
1206
|
+
|
|
1207
|
+
from pythinker_code.ui.shell.selectors.settings import (
|
|
1208
|
+
apply_settings_changes,
|
|
1209
|
+
run_settings_selector,
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
try:
|
|
1213
|
+
config_for_save = load_config(config_file)
|
|
1214
|
+
except (ConfigError, OSError) as exc:
|
|
1215
|
+
console.print(f"[red]Failed to load config: {exc}[/red]")
|
|
1216
|
+
return
|
|
1217
|
+
|
|
1218
|
+
changes = await run_settings_selector(config_for_save)
|
|
1219
|
+
if changes is None:
|
|
1220
|
+
return
|
|
1221
|
+
|
|
1222
|
+
changed_ids = apply_settings_changes(config_for_save, changes)
|
|
1223
|
+
if not changed_ids:
|
|
1224
|
+
console.print("[yellow]Settings unchanged.[/yellow]")
|
|
1225
|
+
return
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
save_config(config_for_save, config_file)
|
|
1229
|
+
except (ConfigError, OSError) as exc:
|
|
1230
|
+
console.print(f"[red]Failed to save config: {exc}[/red]")
|
|
1231
|
+
return
|
|
1232
|
+
|
|
1233
|
+
from pythinker_code.telemetry import track
|
|
1234
|
+
|
|
1235
|
+
track("settings_update", changed=",".join(changed_ids), count=len(changed_ids))
|
|
1236
|
+
console.print(f"[green]Updated {len(changed_ids)} setting(s). Reloading...[/green]")
|
|
1237
|
+
raise Reload(session_id=soul.runtime.session.id)
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
@registry.command
|
|
1241
|
+
def web(app: Shell, args: str):
|
|
1242
|
+
"""Open Pythinker Web UI in browser"""
|
|
1243
|
+
from pythinker_code.telemetry import track
|
|
1244
|
+
|
|
1245
|
+
track("web_opened")
|
|
1246
|
+
soul = ensure_pythinker_soul(app)
|
|
1247
|
+
session_id = soul.runtime.session.id if soul else None
|
|
1248
|
+
raise SwitchToWeb(session_id=session_id)
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
@registry.command
|
|
1252
|
+
def vis(app: Shell, args: str):
|
|
1253
|
+
"""Open Pythinker Agent Tracing Visualizer in browser"""
|
|
1254
|
+
from pythinker_code.telemetry import track
|
|
1255
|
+
|
|
1256
|
+
track("vis_opened")
|
|
1257
|
+
soul = ensure_pythinker_soul(app)
|
|
1258
|
+
session_id = soul.runtime.session.id if soul else None
|
|
1259
|
+
raise SwitchToVis(session_id=session_id)
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
@registry.command
|
|
1263
|
+
async def mcp(app: Shell, args: str):
|
|
1264
|
+
"""Show MCP servers and tools"""
|
|
1265
|
+
from rich.live import Live
|
|
1266
|
+
|
|
1267
|
+
soul = ensure_pythinker_soul(app)
|
|
1268
|
+
if soul is None:
|
|
1269
|
+
return
|
|
1270
|
+
await soul.start_background_mcp_loading()
|
|
1271
|
+
snapshot = soul.status.mcp_status
|
|
1272
|
+
if snapshot is None:
|
|
1273
|
+
console.print("[yellow]No MCP servers configured.[/yellow]")
|
|
1274
|
+
return
|
|
1275
|
+
|
|
1276
|
+
if not snapshot.loading:
|
|
1277
|
+
console.print(render_mcp_console(snapshot))
|
|
1278
|
+
return
|
|
1279
|
+
|
|
1280
|
+
with Live(
|
|
1281
|
+
render_mcp_console(snapshot),
|
|
1282
|
+
console=console,
|
|
1283
|
+
refresh_per_second=8,
|
|
1284
|
+
transient=False,
|
|
1285
|
+
) as live:
|
|
1286
|
+
while True:
|
|
1287
|
+
snapshot = soul.status.mcp_status
|
|
1288
|
+
if snapshot is None:
|
|
1289
|
+
break
|
|
1290
|
+
live.update(render_mcp_console(snapshot), refresh=True)
|
|
1291
|
+
if not snapshot.loading:
|
|
1292
|
+
break
|
|
1293
|
+
await asyncio.sleep(0.125)
|
|
1294
|
+
try:
|
|
1295
|
+
await soul.wait_for_background_mcp_loading()
|
|
1296
|
+
except Exception as e:
|
|
1297
|
+
logger.debug("MCP loading completed with error while rendering /mcp: {error}", error=e)
|
|
1298
|
+
snapshot = soul.status.mcp_status
|
|
1299
|
+
if snapshot is not None:
|
|
1300
|
+
live.update(render_mcp_console(snapshot), refresh=True)
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
@registry.command
|
|
1304
|
+
@shell_mode_registry.command
|
|
1305
|
+
def hooks(app: Shell, args: str):
|
|
1306
|
+
"""List configured hooks"""
|
|
1307
|
+
soul = ensure_pythinker_soul(app)
|
|
1308
|
+
if soul is None:
|
|
1309
|
+
return
|
|
1310
|
+
|
|
1311
|
+
engine = soul.hook_engine
|
|
1312
|
+
if not engine.summary:
|
|
1313
|
+
console.print(
|
|
1314
|
+
"[yellow]No hooks configured. "
|
|
1315
|
+
"Add [[hooks]] sections to your config.toml to set up hooks.[/yellow]"
|
|
1316
|
+
)
|
|
1317
|
+
return
|
|
1318
|
+
|
|
1319
|
+
console.print()
|
|
1320
|
+
console.print("[bold]Configured Hooks:[/bold]")
|
|
1321
|
+
console.print()
|
|
1322
|
+
|
|
1323
|
+
for event, entries in engine.details().items():
|
|
1324
|
+
console.print(f" [cyan]{event}[/cyan]: {len(entries)} hook(s)")
|
|
1325
|
+
for entry in entries:
|
|
1326
|
+
source_tag = f" [dim]({entry['source']})[/dim]" if entry["source"] == "wire" else ""
|
|
1327
|
+
console.print(f" [dim]{entry['matcher']}[/dim] {entry['command']}{source_tag}")
|
|
1328
|
+
|
|
1329
|
+
console.print()
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
@registry.command
|
|
1333
|
+
async def undo(app: Shell, args: str):
|
|
1334
|
+
"""Undo: fork the session at a previous turn and retry"""
|
|
1335
|
+
from pythinker_code.session_fork import enumerate_turns, fork_session
|
|
1336
|
+
from pythinker_code.utils.string import shorten
|
|
1337
|
+
|
|
1338
|
+
soul = ensure_pythinker_soul(app)
|
|
1339
|
+
if soul is None:
|
|
1340
|
+
return
|
|
1341
|
+
|
|
1342
|
+
session = soul.runtime.session
|
|
1343
|
+
wire_path = session.dir / "wire.jsonl"
|
|
1344
|
+
turns = enumerate_turns(wire_path)
|
|
1345
|
+
|
|
1346
|
+
if not turns:
|
|
1347
|
+
console.print("[yellow]No turns found in this session.[/yellow]")
|
|
1348
|
+
return
|
|
1349
|
+
|
|
1350
|
+
# Build choices: each turn's first line, truncated
|
|
1351
|
+
choices: list[tuple[str, str]] = []
|
|
1352
|
+
for turn in turns:
|
|
1353
|
+
first_line = turn.user_text.split("\n", 1)[0]
|
|
1354
|
+
label = shorten(first_line, width=80, placeholder="...")
|
|
1355
|
+
choices.append((str(turn.index), f"[{turn.index}] {label}"))
|
|
1356
|
+
|
|
1357
|
+
try:
|
|
1358
|
+
selected = await ChoiceInput(
|
|
1359
|
+
message="Select a turn to undo (↑↓ navigate, Enter select, Ctrl+C cancel):",
|
|
1360
|
+
options=choices,
|
|
1361
|
+
default=choices[-1][0],
|
|
1362
|
+
).prompt_async()
|
|
1363
|
+
except (EOFError, KeyboardInterrupt):
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
turn_index = int(selected)
|
|
1367
|
+
|
|
1368
|
+
# The selected turn is the one we want to redo — fork includes turns *before* it
|
|
1369
|
+
selected_turn = turns[turn_index]
|
|
1370
|
+
user_text = selected_turn.user_text
|
|
1371
|
+
|
|
1372
|
+
if turn_index == 0:
|
|
1373
|
+
# Fork with no history — just the user text
|
|
1374
|
+
new_session = await Session.create(session.work_dir)
|
|
1375
|
+
new_session_id = new_session.id
|
|
1376
|
+
# Set title to match the convention used by fork_session
|
|
1377
|
+
from pythinker_code.session_state import load_session_state, save_session_state
|
|
1378
|
+
|
|
1379
|
+
new_state = load_session_state(new_session.dir)
|
|
1380
|
+
new_state.custom_title = f"Undo: {session.title}"
|
|
1381
|
+
new_state.title_generated = True
|
|
1382
|
+
save_session_state(new_state, new_session.dir)
|
|
1383
|
+
else:
|
|
1384
|
+
# Fork includes turns 0..turn_index-1
|
|
1385
|
+
fork_turn_index = turn_index - 1
|
|
1386
|
+
new_session_id = await fork_session(
|
|
1387
|
+
source_session_dir=session.dir,
|
|
1388
|
+
work_dir=session.work_dir,
|
|
1389
|
+
turn_index=fork_turn_index,
|
|
1390
|
+
title_prefix="Undo",
|
|
1391
|
+
source_title=session.title,
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
from pythinker_code.telemetry import track
|
|
1395
|
+
|
|
1396
|
+
track("undo")
|
|
1397
|
+
console.print(f"[green]Forked at turn {turn_index}. Switching to new session...[/green]")
|
|
1398
|
+
raise Reload(session_id=new_session_id, prefill_text=user_text)
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
@registry.command
|
|
1402
|
+
async def fork(app: Shell, args: str):
|
|
1403
|
+
"""Fork the current session (copy all history to a new session)"""
|
|
1404
|
+
from pythinker_code.session_fork import fork_session
|
|
1405
|
+
|
|
1406
|
+
soul = ensure_pythinker_soul(app)
|
|
1407
|
+
if soul is None:
|
|
1408
|
+
return
|
|
1409
|
+
|
|
1410
|
+
session = soul.runtime.session
|
|
1411
|
+
new_session_id = await fork_session(
|
|
1412
|
+
source_session_dir=session.dir,
|
|
1413
|
+
work_dir=session.work_dir,
|
|
1414
|
+
turn_index=None,
|
|
1415
|
+
title_prefix="Fork",
|
|
1416
|
+
source_title=session.title,
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
from pythinker_code.telemetry import track
|
|
1420
|
+
|
|
1421
|
+
track("session_fork")
|
|
1422
|
+
console.print("[green]Session forked. Switching to new session...[/green]")
|
|
1423
|
+
raise Reload(session_id=new_session_id)
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
from . import ( # noqa: E402
|
|
1427
|
+
debug, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1428
|
+
export_import, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1429
|
+
oauth, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1430
|
+
setup, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1431
|
+
update, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1432
|
+
usage, # noqa: F401 # type: ignore[reportUnusedImport]
|
|
1433
|
+
)
|