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.
Files changed (790) hide show
  1. pythinker_code/CHANGELOG.md +60 -0
  2. pythinker_code/__init__.py +0 -0
  3. pythinker_code/__main__.py +97 -0
  4. pythinker_code/acp/AGENTS.md +93 -0
  5. pythinker_code/acp/__init__.py +13 -0
  6. pythinker_code/acp/convert.py +128 -0
  7. pythinker_code/acp/host.py +301 -0
  8. pythinker_code/acp/mcp.py +46 -0
  9. pythinker_code/acp/server.py +497 -0
  10. pythinker_code/acp/session.py +502 -0
  11. pythinker_code/acp/tools.py +174 -0
  12. pythinker_code/acp/types.py +13 -0
  13. pythinker_code/acp/version.py +45 -0
  14. pythinker_code/agents/default/agent.yaml +55 -0
  15. pythinker_code/agents/default/code_reviewer.yaml +47 -0
  16. pythinker_code/agents/default/coder.yaml +42 -0
  17. pythinker_code/agents/default/debugger.yaml +35 -0
  18. pythinker_code/agents/default/explore.yaml +59 -0
  19. pythinker_code/agents/default/implementer.yaml +46 -0
  20. pythinker_code/agents/default/plan.yaml +42 -0
  21. pythinker_code/agents/default/review.yaml +47 -0
  22. pythinker_code/agents/default/security_reviewer.yaml +37 -0
  23. pythinker_code/agents/default/system.md +192 -0
  24. pythinker_code/agents/default/verifier.yaml +46 -0
  25. pythinker_code/agents/okabe/agent.yaml +22 -0
  26. pythinker_code/agentspec.py +163 -0
  27. pythinker_code/app.py +847 -0
  28. pythinker_code/approval_runtime/__init__.py +29 -0
  29. pythinker_code/approval_runtime/models.py +42 -0
  30. pythinker_code/approval_runtime/runtime.py +235 -0
  31. pythinker_code/auth/__init__.py +25 -0
  32. pythinker_code/auth/anthropic_direct.py +207 -0
  33. pythinker_code/auth/deepseek.py +192 -0
  34. pythinker_code/auth/github_feedback.py +228 -0
  35. pythinker_code/auth/lm_studio.py +418 -0
  36. pythinker_code/auth/minimax.py +203 -0
  37. pythinker_code/auth/oauth.py +1145 -0
  38. pythinker_code/auth/ollama.py +293 -0
  39. pythinker_code/auth/openai.py +783 -0
  40. pythinker_code/auth/opencode_go.py +203 -0
  41. pythinker_code/auth/openrouter.py +225 -0
  42. pythinker_code/auth/platforms.py +475 -0
  43. pythinker_code/background/__init__.py +36 -0
  44. pythinker_code/background/agent_runner.py +231 -0
  45. pythinker_code/background/ids.py +19 -0
  46. pythinker_code/background/manager.py +668 -0
  47. pythinker_code/background/models.py +118 -0
  48. pythinker_code/background/store.py +243 -0
  49. pythinker_code/background/summary.py +66 -0
  50. pythinker_code/background/worker.py +209 -0
  51. pythinker_code/cli/__init__.py +1326 -0
  52. pythinker_code/cli/__main__.py +19 -0
  53. pythinker_code/cli/_lazy_group.py +268 -0
  54. pythinker_code/cli/debug.py +11 -0
  55. pythinker_code/cli/export.py +322 -0
  56. pythinker_code/cli/info.py +62 -0
  57. pythinker_code/cli/mcp.py +362 -0
  58. pythinker_code/cli/plugin.py +351 -0
  59. pythinker_code/cli/review.py +74 -0
  60. pythinker_code/cli/secscan.py +11 -0
  61. pythinker_code/cli/security_scan.py +35 -0
  62. pythinker_code/cli/toad.py +74 -0
  63. pythinker_code/cli/update.py +26 -0
  64. pythinker_code/cli/vis.py +38 -0
  65. pythinker_code/cli/web.py +80 -0
  66. pythinker_code/config.py +511 -0
  67. pythinker_code/constant.py +33 -0
  68. pythinker_code/events.py +106 -0
  69. pythinker_code/exception.py +43 -0
  70. pythinker_code/extensions.py +151 -0
  71. pythinker_code/hooks/__init__.py +4 -0
  72. pythinker_code/hooks/config.py +34 -0
  73. pythinker_code/hooks/engine.py +383 -0
  74. pythinker_code/hooks/events.py +190 -0
  75. pythinker_code/hooks/runner.py +92 -0
  76. pythinker_code/llm.py +441 -0
  77. pythinker_code/metadata.py +79 -0
  78. pythinker_code/notifications/__init__.py +33 -0
  79. pythinker_code/notifications/llm.py +77 -0
  80. pythinker_code/notifications/manager.py +145 -0
  81. pythinker_code/notifications/models.py +50 -0
  82. pythinker_code/notifications/notifier.py +41 -0
  83. pythinker_code/notifications/store.py +118 -0
  84. pythinker_code/notifications/wire.py +21 -0
  85. pythinker_code/plugin/__init__.py +124 -0
  86. pythinker_code/plugin/manager.py +166 -0
  87. pythinker_code/plugin/tool.py +173 -0
  88. pythinker_code/prompt_templates.py +181 -0
  89. pythinker_code/prompts/__init__.py +6 -0
  90. pythinker_code/prompts/compact.md +73 -0
  91. pythinker_code/prompts/init.md +21 -0
  92. pythinker_code/py.typed +0 -0
  93. pythinker_code/session.py +319 -0
  94. pythinker_code/session_fork.py +325 -0
  95. pythinker_code/session_state.py +132 -0
  96. pythinker_code/share.py +14 -0
  97. pythinker_code/skill/__init__.py +727 -0
  98. pythinker_code/skill/flow/__init__.py +99 -0
  99. pythinker_code/skill/flow/d2.py +482 -0
  100. pythinker_code/skill/flow/mermaid.py +266 -0
  101. pythinker_code/skills/pythinker-code-help/SKILL.md +54 -0
  102. pythinker_code/skills/skill-creator/SKILL.md +367 -0
  103. pythinker_code/soul/__init__.py +304 -0
  104. pythinker_code/soul/agent.py +552 -0
  105. pythinker_code/soul/approval.py +267 -0
  106. pythinker_code/soul/btw.py +220 -0
  107. pythinker_code/soul/compaction.py +189 -0
  108. pythinker_code/soul/context.py +339 -0
  109. pythinker_code/soul/denwarenji.py +39 -0
  110. pythinker_code/soul/dynamic_injection.py +84 -0
  111. pythinker_code/soul/dynamic_injections/__init__.py +0 -0
  112. pythinker_code/soul/dynamic_injections/auto_mode.py +72 -0
  113. pythinker_code/soul/dynamic_injections/plan_mode.py +239 -0
  114. pythinker_code/soul/message.py +92 -0
  115. pythinker_code/soul/permission.py +368 -0
  116. pythinker_code/soul/pythinkersoul.py +1763 -0
  117. pythinker_code/soul/slash.py +340 -0
  118. pythinker_code/soul/toolset.py +826 -0
  119. pythinker_code/subagents/__init__.py +21 -0
  120. pythinker_code/subagents/builder.py +43 -0
  121. pythinker_code/subagents/core.py +86 -0
  122. pythinker_code/subagents/discovery.py +234 -0
  123. pythinker_code/subagents/git_context.py +172 -0
  124. pythinker_code/subagents/models.py +56 -0
  125. pythinker_code/subagents/output.py +71 -0
  126. pythinker_code/subagents/registry.py +28 -0
  127. pythinker_code/subagents/runner.py +442 -0
  128. pythinker_code/subagents/store.py +200 -0
  129. pythinker_code/telemetry/__init__.py +217 -0
  130. pythinker_code/telemetry/config.py +113 -0
  131. pythinker_code/telemetry/crash.py +191 -0
  132. pythinker_code/telemetry/errors.py +113 -0
  133. pythinker_code/telemetry/metrics.py +208 -0
  134. pythinker_code/telemetry/otel.py +303 -0
  135. pythinker_code/telemetry/sentry.py +212 -0
  136. pythinker_code/telemetry/sink.py +189 -0
  137. pythinker_code/tools/AGENTS.md +6 -0
  138. pythinker_code/tools/__init__.py +105 -0
  139. pythinker_code/tools/agent/__init__.py +326 -0
  140. pythinker_code/tools/agent/description.md +65 -0
  141. pythinker_code/tools/ask_user/__init__.py +162 -0
  142. pythinker_code/tools/ask_user/description.md +19 -0
  143. pythinker_code/tools/background/__init__.py +318 -0
  144. pythinker_code/tools/background/list.md +10 -0
  145. pythinker_code/tools/background/output.md +11 -0
  146. pythinker_code/tools/background/stop.md +8 -0
  147. pythinker_code/tools/display.py +46 -0
  148. pythinker_code/tools/dmail/__init__.py +38 -0
  149. pythinker_code/tools/dmail/dmail.md +17 -0
  150. pythinker_code/tools/file/__init__.py +31 -0
  151. pythinker_code/tools/file/glob.md +17 -0
  152. pythinker_code/tools/file/glob.py +163 -0
  153. pythinker_code/tools/file/grep.md +6 -0
  154. pythinker_code/tools/file/grep_local.py +904 -0
  155. pythinker_code/tools/file/plan_mode.py +45 -0
  156. pythinker_code/tools/file/read.md +16 -0
  157. pythinker_code/tools/file/read.py +303 -0
  158. pythinker_code/tools/file/read_media.md +24 -0
  159. pythinker_code/tools/file/read_media.py +220 -0
  160. pythinker_code/tools/file/replace.md +7 -0
  161. pythinker_code/tools/file/replace.py +204 -0
  162. pythinker_code/tools/file/utils.py +257 -0
  163. pythinker_code/tools/file/write.md +5 -0
  164. pythinker_code/tools/file/write.py +186 -0
  165. pythinker_code/tools/plan/__init__.py +362 -0
  166. pythinker_code/tools/plan/description.md +29 -0
  167. pythinker_code/tools/plan/enter.py +193 -0
  168. pythinker_code/tools/plan/enter_description.md +35 -0
  169. pythinker_code/tools/plan/handoff.py +69 -0
  170. pythinker_code/tools/plan/heroes.py +277 -0
  171. pythinker_code/tools/shell/__init__.py +263 -0
  172. pythinker_code/tools/shell/bash.md +35 -0
  173. pythinker_code/tools/shell/powershell.md +30 -0
  174. pythinker_code/tools/test.py +55 -0
  175. pythinker_code/tools/think/__init__.py +21 -0
  176. pythinker_code/tools/think/think.md +1 -0
  177. pythinker_code/tools/todo/__init__.py +168 -0
  178. pythinker_code/tools/todo/set_todo_list.md +23 -0
  179. pythinker_code/tools/utils.py +200 -0
  180. pythinker_code/tools/web/__init__.py +4 -0
  181. pythinker_code/tools/web/fetch.md +1 -0
  182. pythinker_code/tools/web/fetch.py +261 -0
  183. pythinker_code/tools/web/search.md +1 -0
  184. pythinker_code/tools/web/search.py +163 -0
  185. pythinker_code/ui/__init__.py +0 -0
  186. pythinker_code/ui/acp/__init__.py +99 -0
  187. pythinker_code/ui/print/__init__.py +474 -0
  188. pythinker_code/ui/print/visualize.py +185 -0
  189. pythinker_code/ui/shell/__init__.py +1806 -0
  190. pythinker_code/ui/shell/components/__init__.py +110 -0
  191. pythinker_code/ui/shell/components/base.py +25 -0
  192. pythinker_code/ui/shell/components/bash_execution.py +249 -0
  193. pythinker_code/ui/shell/components/bordered_loader.py +62 -0
  194. pythinker_code/ui/shell/components/diff.py +308 -0
  195. pythinker_code/ui/shell/components/footer.py +231 -0
  196. pythinker_code/ui/shell/components/key_hints.py +27 -0
  197. pythinker_code/ui/shell/components/messages.py +152 -0
  198. pythinker_code/ui/shell/components/render_utils.py +198 -0
  199. pythinker_code/ui/shell/components/settings_list.py +369 -0
  200. pythinker_code/ui/shell/components/special_messages.py +125 -0
  201. pythinker_code/ui/shell/components/tool_execution.py +261 -0
  202. pythinker_code/ui/shell/console.py +109 -0
  203. pythinker_code/ui/shell/debug.py +190 -0
  204. pythinker_code/ui/shell/echo.py +30 -0
  205. pythinker_code/ui/shell/export_import.py +117 -0
  206. pythinker_code/ui/shell/keyboard.py +300 -0
  207. pythinker_code/ui/shell/keymap.py +84 -0
  208. pythinker_code/ui/shell/mcp_status.py +112 -0
  209. pythinker_code/ui/shell/model_picker.py +318 -0
  210. pythinker_code/ui/shell/oauth.py +273 -0
  211. pythinker_code/ui/shell/placeholders.py +578 -0
  212. pythinker_code/ui/shell/prompt.py +2888 -0
  213. pythinker_code/ui/shell/replay.py +215 -0
  214. pythinker_code/ui/shell/selector.py +364 -0
  215. pythinker_code/ui/shell/selectors/__init__.py +38 -0
  216. pythinker_code/ui/shell/selectors/extension.py +37 -0
  217. pythinker_code/ui/shell/selectors/oauth.py +63 -0
  218. pythinker_code/ui/shell/selectors/settings.py +349 -0
  219. pythinker_code/ui/shell/selectors/show_images.py +29 -0
  220. pythinker_code/ui/shell/selectors/theme.py +28 -0
  221. pythinker_code/ui/shell/selectors/thinking.py +42 -0
  222. pythinker_code/ui/shell/session_picker.py +227 -0
  223. pythinker_code/ui/shell/setup.py +212 -0
  224. pythinker_code/ui/shell/slash.py +1433 -0
  225. pythinker_code/ui/shell/spinner_words.py +222 -0
  226. pythinker_code/ui/shell/startup.py +32 -0
  227. pythinker_code/ui/shell/task_browser.py +486 -0
  228. pythinker_code/ui/shell/tool_renderers/__init__.py +197 -0
  229. pythinker_code/ui/shell/tool_renderers/_render_utils.py +168 -0
  230. pythinker_code/ui/shell/tool_renderers/agent.py +140 -0
  231. pythinker_code/ui/shell/tool_renderers/ask_user.py +93 -0
  232. pythinker_code/ui/shell/tool_renderers/background.py +144 -0
  233. pythinker_code/ui/shell/tool_renderers/bash.py +103 -0
  234. pythinker_code/ui/shell/tool_renderers/edit.py +163 -0
  235. pythinker_code/ui/shell/tool_renderers/find.py +81 -0
  236. pythinker_code/ui/shell/tool_renderers/generic.py +60 -0
  237. pythinker_code/ui/shell/tool_renderers/grep.py +98 -0
  238. pythinker_code/ui/shell/tool_renderers/plan.py +98 -0
  239. pythinker_code/ui/shell/tool_renderers/read.py +103 -0
  240. pythinker_code/ui/shell/tool_renderers/think.py +66 -0
  241. pythinker_code/ui/shell/tool_renderers/todo.py +164 -0
  242. pythinker_code/ui/shell/tool_renderers/web.py +128 -0
  243. pythinker_code/ui/shell/tool_renderers/write.py +102 -0
  244. pythinker_code/ui/shell/update.py +352 -0
  245. pythinker_code/ui/shell/usage.py +291 -0
  246. pythinker_code/ui/shell/usage_adapters/__init__.py +50 -0
  247. pythinker_code/ui/shell/usage_adapters/anthropic_admin.py +233 -0
  248. pythinker_code/ui/shell/usage_adapters/base.py +72 -0
  249. pythinker_code/ui/shell/usage_adapters/deepseek.py +137 -0
  250. pythinker_code/ui/shell/usage_adapters/minimax.py +236 -0
  251. pythinker_code/ui/shell/usage_adapters/openai_admin.py +225 -0
  252. pythinker_code/ui/shell/usage_adapters/openai_chatgpt.py +241 -0
  253. pythinker_code/ui/shell/usage_adapters/opencode_go.py +232 -0
  254. pythinker_code/ui/shell/usage_adapters/openrouter.py +105 -0
  255. pythinker_code/ui/shell/usage_adapters/pythinker.py +189 -0
  256. pythinker_code/ui/shell/usage_adapters/pythinker_ai.py +50 -0
  257. pythinker_code/ui/shell/usage_render.py +150 -0
  258. pythinker_code/ui/shell/visualize/__init__.py +165 -0
  259. pythinker_code/ui/shell/visualize/_approval_panel.py +539 -0
  260. pythinker_code/ui/shell/visualize/_blocks.py +802 -0
  261. pythinker_code/ui/shell/visualize/_btw_panel.py +227 -0
  262. pythinker_code/ui/shell/visualize/_input_router.py +48 -0
  263. pythinker_code/ui/shell/visualize/_interactive.py +531 -0
  264. pythinker_code/ui/shell/visualize/_live_view.py +891 -0
  265. pythinker_code/ui/shell/visualize/_question_panel.py +586 -0
  266. pythinker_code/ui/shell/visualize/_worklog.py +245 -0
  267. pythinker_code/ui/theme.py +395 -0
  268. pythinker_code/ui/tui_config.py +82 -0
  269. pythinker_code/usage_ratelimit_cache.py +175 -0
  270. pythinker_code/utils/__init__.py +0 -0
  271. pythinker_code/utils/aiohttp.py +24 -0
  272. pythinker_code/utils/aioqueue.py +72 -0
  273. pythinker_code/utils/broadcast.py +38 -0
  274. pythinker_code/utils/changelog.py +108 -0
  275. pythinker_code/utils/clipboard.py +246 -0
  276. pythinker_code/utils/datetime.py +64 -0
  277. pythinker_code/utils/diff.py +135 -0
  278. pythinker_code/utils/editor.py +91 -0
  279. pythinker_code/utils/environment.py +73 -0
  280. pythinker_code/utils/envvar.py +22 -0
  281. pythinker_code/utils/export.py +696 -0
  282. pythinker_code/utils/file_filter.py +375 -0
  283. pythinker_code/utils/frontmatter.py +70 -0
  284. pythinker_code/utils/io.py +27 -0
  285. pythinker_code/utils/logging.py +146 -0
  286. pythinker_code/utils/media_tags.py +29 -0
  287. pythinker_code/utils/message.py +24 -0
  288. pythinker_code/utils/path.py +199 -0
  289. pythinker_code/utils/proctitle.py +33 -0
  290. pythinker_code/utils/proxy.py +31 -0
  291. pythinker_code/utils/pyinstaller.py +45 -0
  292. pythinker_code/utils/rich/__init__.py +33 -0
  293. pythinker_code/utils/rich/columns.py +99 -0
  294. pythinker_code/utils/rich/diff_render.py +481 -0
  295. pythinker_code/utils/rich/markdown.py +935 -0
  296. pythinker_code/utils/rich/markdown_sample.md +108 -0
  297. pythinker_code/utils/rich/markdown_sample_short.md +2 -0
  298. pythinker_code/utils/rich/syntax.py +114 -0
  299. pythinker_code/utils/sensitive.py +54 -0
  300. pythinker_code/utils/server.py +121 -0
  301. pythinker_code/utils/signals.py +43 -0
  302. pythinker_code/utils/slashcmd.py +124 -0
  303. pythinker_code/utils/string.py +41 -0
  304. pythinker_code/utils/subprocess_env.py +83 -0
  305. pythinker_code/utils/term.py +168 -0
  306. pythinker_code/utils/typing.py +20 -0
  307. pythinker_code/vis/__init__.py +0 -0
  308. pythinker_code/vis/api/__init__.py +5 -0
  309. pythinker_code/vis/api/sessions.py +714 -0
  310. pythinker_code/vis/api/statistics.py +209 -0
  311. pythinker_code/vis/api/system.py +19 -0
  312. pythinker_code/vis/app.py +199 -0
  313. pythinker_code/vis/static/assets/highlighted-body-B3W2YXNL-CY1rtwrX.js +1 -0
  314. pythinker_code/vis/static/assets/index-DSRInNnm.css +1 -0
  315. pythinker_code/vis/static/assets/index-DgmTI2M_.js +185 -0
  316. pythinker_code/vis/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  317. pythinker_code/vis/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  318. pythinker_code/vis/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  319. pythinker_code/vis/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  320. pythinker_code/vis/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  321. pythinker_code/vis/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  322. pythinker_code/vis/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  323. pythinker_code/vis/static/index.html +17 -0
  324. pythinker_code/web/__init__.py +5 -0
  325. pythinker_code/web/api/__init__.py +15 -0
  326. pythinker_code/web/api/config.py +217 -0
  327. pythinker_code/web/api/open_in.py +233 -0
  328. pythinker_code/web/api/sessions.py +1256 -0
  329. pythinker_code/web/app.py +449 -0
  330. pythinker_code/web/auth.py +191 -0
  331. pythinker_code/web/models.py +98 -0
  332. pythinker_code/web/runner/__init__.py +5 -0
  333. pythinker_code/web/runner/messages.py +57 -0
  334. pythinker_code/web/runner/process.py +754 -0
  335. pythinker_code/web/runner/worker.py +97 -0
  336. pythinker_code/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  337. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  338. pythinker_code/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  339. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  340. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  341. pythinker_code/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  342. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  343. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  344. pythinker_code/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  345. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  346. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  347. pythinker_code/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  348. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  349. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  350. pythinker_code/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  351. pythinker_code/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  352. pythinker_code/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  353. pythinker_code/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  354. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  355. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  356. pythinker_code/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  357. pythinker_code/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  358. pythinker_code/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  359. pythinker_code/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  360. pythinker_code/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  361. pythinker_code/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  362. pythinker_code/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  363. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  364. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  365. pythinker_code/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  366. pythinker_code/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  367. pythinker_code/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  368. pythinker_code/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  369. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  370. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  371. pythinker_code/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  372. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  373. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  374. pythinker_code/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  375. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  376. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  377. pythinker_code/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  378. pythinker_code/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  379. pythinker_code/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  380. pythinker_code/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  381. pythinker_code/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  382. pythinker_code/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  383. pythinker_code/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  384. pythinker_code/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  385. pythinker_code/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  386. pythinker_code/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  387. pythinker_code/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  388. pythinker_code/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  389. pythinker_code/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  390. pythinker_code/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  391. pythinker_code/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  392. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  393. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  394. pythinker_code/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  395. pythinker_code/web/static/assets/_baseUniq-DpSMr1jx.js +1 -0
  396. pythinker_code/web/static/assets/abap-BdImnpbu.js +1 -0
  397. pythinker_code/web/static/assets/actionscript-3-CfeIJUat.js +1 -0
  398. pythinker_code/web/static/assets/ada-bCR0ucgS.js +1 -0
  399. pythinker_code/web/static/assets/andromeeda-C-Jbm3Hp.js +1 -0
  400. pythinker_code/web/static/assets/angular-html-CU67Zn6k.js +1 -0
  401. pythinker_code/web/static/assets/angular-ts-BwZT4LLn.js +1 -0
  402. pythinker_code/web/static/assets/apache-Pmp26Uib.js +1 -0
  403. pythinker_code/web/static/assets/apex-D8_7TLub.js +1 -0
  404. pythinker_code/web/static/assets/apl-dKokRX4l.js +1 -0
  405. pythinker_code/web/static/assets/applescript-Co6uUVPk.js +1 -0
  406. pythinker_code/web/static/assets/ara-BRHolxvo.js +1 -0
  407. pythinker_code/web/static/assets/arc-DpsahJyV.js +1 -0
  408. pythinker_code/web/static/assets/architectureDiagram-VXUJARFQ-DqiRv9Eg.js +36 -0
  409. pythinker_code/web/static/assets/asciidoc-Dv7Oe6Be.js +1 -0
  410. pythinker_code/web/static/assets/asm-D_Q5rh1f.js +1 -0
  411. pythinker_code/web/static/assets/astro-CbQHKStN.js +1 -0
  412. pythinker_code/web/static/assets/aurora-x-D-2ljcwZ.js +1 -0
  413. pythinker_code/web/static/assets/awk-DMzUqQB5.js +1 -0
  414. pythinker_code/web/static/assets/ayu-dark-CmMr59Fi.js +1 -0
  415. pythinker_code/web/static/assets/ballerina-BFfxhgS-.js +1 -0
  416. pythinker_code/web/static/assets/bat-BkioyH1T.js +1 -0
  417. pythinker_code/web/static/assets/beancount-k_qm7-4y.js +1 -0
  418. pythinker_code/web/static/assets/berry-uYugtg8r.js +1 -0
  419. pythinker_code/web/static/assets/bibtex-CHM0blh-.js +1 -0
  420. pythinker_code/web/static/assets/bicep-Bmn6On1c.js +1 -0
  421. pythinker_code/web/static/assets/blade-D4QpJJKB.js +1 -0
  422. pythinker_code/web/static/assets/blockDiagram-VD42YOAC-WgtUvqbp.js +122 -0
  423. pythinker_code/web/static/assets/bsl-BO_Y6i37.js +1 -0
  424. pythinker_code/web/static/assets/c-BIGW1oBm.js +1 -0
  425. pythinker_code/web/static/assets/c3-VCDPK7BO.js +1 -0
  426. pythinker_code/web/static/assets/c4Diagram-YG6GDRKO-rK0RPuZd.js +10 -0
  427. pythinker_code/web/static/assets/cadence-Bv_4Rxtq.js +1 -0
  428. pythinker_code/web/static/assets/cairo-KRGpt6FW.js +1 -0
  429. pythinker_code/web/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
  430. pythinker_code/web/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
  431. pythinker_code/web/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
  432. pythinker_code/web/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
  433. pythinker_code/web/static/assets/channel-B0rlvkH-.js +1 -0
  434. pythinker_code/web/static/assets/chunk-4BX2VUAB-DIkMuLV-.js +1 -0
  435. pythinker_code/web/static/assets/chunk-55IACEB6-CORdm4k4.js +1 -0
  436. pythinker_code/web/static/assets/chunk-B4BG7PRW-D9xDhwHO.js +165 -0
  437. pythinker_code/web/static/assets/chunk-DI55MBZ5-BDmF9Bh-.js +220 -0
  438. pythinker_code/web/static/assets/chunk-FMBD7UC4-BCse_HmM.js +15 -0
  439. pythinker_code/web/static/assets/chunk-QN33PNHL-DCpBmTzA.js +1 -0
  440. pythinker_code/web/static/assets/chunk-QZHKN3VN-BqLuqobw.js +1 -0
  441. pythinker_code/web/static/assets/chunk-TZMSLE5B-8K2ogOKS.js +1 -0
  442. pythinker_code/web/static/assets/clarity-D53aC0YG.js +1 -0
  443. pythinker_code/web/static/assets/classDiagram-2ON5EDUG-D_ZHSii2.js +1 -0
  444. pythinker_code/web/static/assets/classDiagram-v2-WZHVMYZB-D_ZHSii2.js +1 -0
  445. pythinker_code/web/static/assets/clojure-P80f7IUj.js +1 -0
  446. pythinker_code/web/static/assets/clone-GSXejyY1.js +1 -0
  447. pythinker_code/web/static/assets/cmake-D1j8_8rp.js +1 -0
  448. pythinker_code/web/static/assets/cobol-nwyudZeR.js +1 -0
  449. pythinker_code/web/static/assets/code-block-IT6T5CEO-DWTFYA28.js +2 -0
  450. pythinker_code/web/static/assets/codeowners-Bp6g37R7.js +1 -0
  451. pythinker_code/web/static/assets/codeql-DsOJ9woJ.js +1 -0
  452. pythinker_code/web/static/assets/coffee-Ch7k5sss.js +1 -0
  453. pythinker_code/web/static/assets/common-lisp-Cg-RD9OK.js +1 -0
  454. pythinker_code/web/static/assets/coq-DkFqJrB1.js +1 -0
  455. pythinker_code/web/static/assets/cose-bilkent-S5V4N54A-BRI7ES-N.js +1 -0
  456. pythinker_code/web/static/assets/cpp-CofmeUqb.js +1 -0
  457. pythinker_code/web/static/assets/crystal-tKQVLTB8.js +1 -0
  458. pythinker_code/web/static/assets/csharp-K5feNrxe.js +1 -0
  459. pythinker_code/web/static/assets/css-DPfMkruS.js +1 -0
  460. pythinker_code/web/static/assets/csv-fuZLfV_i.js +1 -0
  461. pythinker_code/web/static/assets/cue-D82EKSYY.js +1 -0
  462. pythinker_code/web/static/assets/cypher-COkxafJQ.js +1 -0
  463. pythinker_code/web/static/assets/cytoscape.esm-B6BxUuKW.js +321 -0
  464. pythinker_code/web/static/assets/d-85-TOEBH.js +1 -0
  465. pythinker_code/web/static/assets/dagre-6UL2VRFP-Ci5GdWfi.js +4 -0
  466. pythinker_code/web/static/assets/dark-plus-C3mMm8J8.js +1 -0
  467. pythinker_code/web/static/assets/dart-CF10PKvl.js +1 -0
  468. pythinker_code/web/static/assets/dax-CEL-wOlO.js +1 -0
  469. pythinker_code/web/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  470. pythinker_code/web/static/assets/desktop-BmXAJ9_W.js +1 -0
  471. pythinker_code/web/static/assets/diagram-PSM6KHXK-0hhAylV4.js +24 -0
  472. pythinker_code/web/static/assets/diagram-QEK2KX5R-8fxgaW6d.js +43 -0
  473. pythinker_code/web/static/assets/diagram-S2PKOQOG-FRr0_atE.js +24 -0
  474. pythinker_code/web/static/assets/diff-D97Zzqfu.js +1 -0
  475. pythinker_code/web/static/assets/docker-BcOcwvcX.js +1 -0
  476. pythinker_code/web/static/assets/dotenv-Da5cRb03.js +1 -0
  477. pythinker_code/web/static/assets/dracula-BzJJZx-M.js +1 -0
  478. pythinker_code/web/static/assets/dracula-soft-BXkSAIEj.js +1 -0
  479. pythinker_code/web/static/assets/dream-maker-BtqSS_iP.js +1 -0
  480. pythinker_code/web/static/assets/edge-BkV0erSs.js +1 -0
  481. pythinker_code/web/static/assets/elixir-CDX3lj18.js +1 -0
  482. pythinker_code/web/static/assets/elm-DbKCFpqz.js +1 -0
  483. pythinker_code/web/static/assets/emacs-lisp-C9XAeP06.js +1 -0
  484. pythinker_code/web/static/assets/erDiagram-Q2GNP2WA-B3T-hJUM.js +60 -0
  485. pythinker_code/web/static/assets/erb-BOJIQeun.js +1 -0
  486. pythinker_code/web/static/assets/erlang-DsQrWhSR.js +1 -0
  487. pythinker_code/web/static/assets/everforest-dark-BgDCqdQA.js +1 -0
  488. pythinker_code/web/static/assets/everforest-light-C8M2exoo.js +1 -0
  489. pythinker_code/web/static/assets/fennel-BYunw83y.js +1 -0
  490. pythinker_code/web/static/assets/fish-BvzEVeQv.js +1 -0
  491. pythinker_code/web/static/assets/flowDiagram-NV44I4VS-D0S3u7ot.js +162 -0
  492. pythinker_code/web/static/assets/fluent-C4IJs8-o.js +1 -0
  493. pythinker_code/web/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
  494. pythinker_code/web/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
  495. pythinker_code/web/static/assets/fsharp-CXgrBDvD.js +1 -0
  496. pythinker_code/web/static/assets/ganttDiagram-JELNMOA3-CHrN2a23.js +267 -0
  497. pythinker_code/web/static/assets/gdresource-B7Tvp0Sc.js +1 -0
  498. pythinker_code/web/static/assets/gdscript-DTMYz4Jt.js +1 -0
  499. pythinker_code/web/static/assets/gdshader-DkwncUOv.js +1 -0
  500. pythinker_code/web/static/assets/genie-D0YGMca9.js +1 -0
  501. pythinker_code/web/static/assets/gherkin-DyxjwDmM.js +1 -0
  502. pythinker_code/web/static/assets/git-commit-F4YmCXRG.js +1 -0
  503. pythinker_code/web/static/assets/git-rebase-r7XF79zn.js +1 -0
  504. pythinker_code/web/static/assets/gitGraphDiagram-NY62KEGX-CfcXZWg0.js +65 -0
  505. pythinker_code/web/static/assets/github-dark-DHJKELXO.js +1 -0
  506. pythinker_code/web/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
  507. pythinker_code/web/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
  508. pythinker_code/web/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
  509. pythinker_code/web/static/assets/github-light-DAi9KRSo.js +1 -0
  510. pythinker_code/web/static/assets/github-light-default-D7oLnXFd.js +1 -0
  511. pythinker_code/web/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
  512. pythinker_code/web/static/assets/gleam-BspZqrRM.js +1 -0
  513. pythinker_code/web/static/assets/glimmer-js-Rg0-pVw9.js +1 -0
  514. pythinker_code/web/static/assets/glimmer-ts-U6CK756n.js +1 -0
  515. pythinker_code/web/static/assets/glsl-DplSGwfg.js +1 -0
  516. pythinker_code/web/static/assets/gn-n2N0HUVH.js +1 -0
  517. pythinker_code/web/static/assets/gnuplot-DdkO51Og.js +1 -0
  518. pythinker_code/web/static/assets/go-Dn2_MT6a.js +1 -0
  519. pythinker_code/web/static/assets/graph-8jMJwCqE.js +1 -0
  520. pythinker_code/web/static/assets/graphql-ChdNCCLP.js +1 -0
  521. pythinker_code/web/static/assets/groovy-gcz8RCvz.js +1 -0
  522. pythinker_code/web/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
  523. pythinker_code/web/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
  524. pythinker_code/web/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
  525. pythinker_code/web/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
  526. pythinker_code/web/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
  527. pythinker_code/web/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
  528. pythinker_code/web/static/assets/hack-CaT9iCJl.js +1 -0
  529. pythinker_code/web/static/assets/haml-B8DHNrY2.js +1 -0
  530. pythinker_code/web/static/assets/handlebars-BL8al0AC.js +1 -0
  531. pythinker_code/web/static/assets/haskell-Df6bDoY_.js +1 -0
  532. pythinker_code/web/static/assets/haxe-CzTSHFRz.js +1 -0
  533. pythinker_code/web/static/assets/hcl-BWvSN4gD.js +1 -0
  534. pythinker_code/web/static/assets/hjson-D5-asLiD.js +1 -0
  535. pythinker_code/web/static/assets/hlsl-D3lLCCz7.js +1 -0
  536. pythinker_code/web/static/assets/houston-DnULxvSX.js +1 -0
  537. pythinker_code/web/static/assets/html-GMplVEZG.js +1 -0
  538. pythinker_code/web/static/assets/html-derivative-BFtXZ54Q.js +1 -0
  539. pythinker_code/web/static/assets/http-jrhK8wxY.js +1 -0
  540. pythinker_code/web/static/assets/hurl-irOxFIW8.js +1 -0
  541. pythinker_code/web/static/assets/hxml-Bvhsp5Yf.js +1 -0
  542. pythinker_code/web/static/assets/hy-DFXneXwc.js +1 -0
  543. pythinker_code/web/static/assets/imba-DGztddWO.js +1 -0
  544. pythinker_code/web/static/assets/index-BXrFnzMy.js +153 -0
  545. pythinker_code/web/static/assets/index-BpoLgcEt.js +1 -0
  546. pythinker_code/web/static/assets/index-BrfQJnRD.js +5 -0
  547. pythinker_code/web/static/assets/index-C4gFzubz.js +2 -0
  548. pythinker_code/web/static/assets/index-CzV_vCfu.css +1 -0
  549. pythinker_code/web/static/assets/index-DI2oedCt.js +19 -0
  550. pythinker_code/web/static/assets/infoDiagram-WHAUD3N6-DdxonBf3.js +2 -0
  551. pythinker_code/web/static/assets/ini-BEwlwnbL.js +1 -0
  552. pythinker_code/web/static/assets/init-Gi6I4Gst.js +1 -0
  553. pythinker_code/web/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  554. pythinker_code/web/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  555. pythinker_code/web/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  556. pythinker_code/web/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  557. pythinker_code/web/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  558. pythinker_code/web/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  559. pythinker_code/web/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  560. pythinker_code/web/static/assets/java-CylS5w8V.js +1 -0
  561. pythinker_code/web/static/assets/javascript-wDzz0qaB.js +1 -0
  562. pythinker_code/web/static/assets/jinja-4LBKfQ-Z.js +1 -0
  563. pythinker_code/web/static/assets/jison-wvAkD_A8.js +1 -0
  564. pythinker_code/web/static/assets/journeyDiagram-XKPGCS4Q-BXf4aQei.js +139 -0
  565. pythinker_code/web/static/assets/json-Cp-IABpG.js +1 -0
  566. pythinker_code/web/static/assets/json5-C9tS-k6U.js +1 -0
  567. pythinker_code/web/static/assets/jsonc-Des-eS-w.js +1 -0
  568. pythinker_code/web/static/assets/jsonl-DcaNXYhu.js +1 -0
  569. pythinker_code/web/static/assets/jsonnet-DFQXde-d.js +1 -0
  570. pythinker_code/web/static/assets/jssm-C2t-YnRu.js +1 -0
  571. pythinker_code/web/static/assets/jsx-g9-lgVsj.js +1 -0
  572. pythinker_code/web/static/assets/julia-CxzCAyBv.js +1 -0
  573. pythinker_code/web/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
  574. pythinker_code/web/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
  575. pythinker_code/web/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
  576. pythinker_code/web/static/assets/kanban-definition-3W4ZIXB7-DLpPPOu8.js +89 -0
  577. pythinker_code/web/static/assets/katex-D2lIc1rk.css +1 -0
  578. pythinker_code/web/static/assets/kdl-DV7GczEv.js +1 -0
  579. pythinker_code/web/static/assets/kotlin-BdnUsdx6.js +1 -0
  580. pythinker_code/web/static/assets/kusto-DZf3V79B.js +1 -0
  581. pythinker_code/web/static/assets/laserwave-DUszq2jm.js +1 -0
  582. pythinker_code/web/static/assets/latex-B4uzh10-.js +1 -0
  583. pythinker_code/web/static/assets/layout-DH73UoAH.js +1 -0
  584. pythinker_code/web/static/assets/lean-BZvkOJ9d.js +1 -0
  585. pythinker_code/web/static/assets/less-B1dDrJ26.js +1 -0
  586. pythinker_code/web/static/assets/light-plus-B7mTdjB0.js +1 -0
  587. pythinker_code/web/static/assets/linear-bAer2-sK.js +1 -0
  588. pythinker_code/web/static/assets/liquid-DYVedYrR.js +1 -0
  589. pythinker_code/web/static/assets/llvm-BtvRca6l.js +1 -0
  590. pythinker_code/web/static/assets/log-2UxHyX5q.js +1 -0
  591. pythinker_code/web/static/assets/logo-BtOb2qkB.js +1 -0
  592. pythinker_code/web/static/assets/lua-BbnMAYS6.js +1 -0
  593. pythinker_code/web/static/assets/luau-C-HG3fhB.js +1 -0
  594. pythinker_code/web/static/assets/make-CHLpvVh8.js +1 -0
  595. pythinker_code/web/static/assets/markdown-Cvjx9yec.js +1 -0
  596. pythinker_code/web/static/assets/marko-DZsq8hO1.js +1 -0
  597. pythinker_code/web/static/assets/material-theme-D5KoaKCx.js +1 -0
  598. pythinker_code/web/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
  599. pythinker_code/web/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
  600. pythinker_code/web/static/assets/material-theme-ocean-CyktbL80.js +1 -0
  601. pythinker_code/web/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
  602. pythinker_code/web/static/assets/matlab-D7o27uSR.js +1 -0
  603. pythinker_code/web/static/assets/mdc-DUICxH0z.js +1 -0
  604. pythinker_code/web/static/assets/mdx-Cmh6b_Ma.js +1 -0
  605. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2P5VJ9v.css +1 -0
  606. pythinker_code/web/static/assets/mermaid-VLURNSYL-CuqbwKXv.js +465 -0
  607. pythinker_code/web/static/assets/mermaid-mWjccvbQ.js +1 -0
  608. pythinker_code/web/static/assets/mermaid.core-Nx-rTKiV.js +191 -0
  609. pythinker_code/web/static/assets/min-DbfD8Ywu.js +1 -0
  610. pythinker_code/web/static/assets/min-dark-CafNBF8u.js +1 -0
  611. pythinker_code/web/static/assets/min-light-CTRr51gU.js +1 -0
  612. pythinker_code/web/static/assets/mindmap-definition-VGOIOE7T-C6l761Ue.js +68 -0
  613. pythinker_code/web/static/assets/mipsasm-CKIfxQSi.js +1 -0
  614. pythinker_code/web/static/assets/mojo-B93PlW-d.js +1 -0
  615. pythinker_code/web/static/assets/monokai-D4h5O-jR.js +1 -0
  616. pythinker_code/web/static/assets/moonbit-Ba13S78F.js +1 -0
  617. pythinker_code/web/static/assets/move-Bu9oaDYs.js +1 -0
  618. pythinker_code/web/static/assets/narrat-DRg8JJMk.js +1 -0
  619. pythinker_code/web/static/assets/nextflow-BrzmwbiE.js +1 -0
  620. pythinker_code/web/static/assets/nginx-DknmC5AR.js +1 -0
  621. pythinker_code/web/static/assets/night-owl-C39BiMTA.js +1 -0
  622. pythinker_code/web/static/assets/nim-CVrawwO9.js +1 -0
  623. pythinker_code/web/static/assets/nix-CwoSXNpI.js +1 -0
  624. pythinker_code/web/static/assets/nord-Ddv68eIx.js +1 -0
  625. pythinker_code/web/static/assets/nushell-C-sUppwS.js +1 -0
  626. pythinker_code/web/static/assets/objective-c-DXmwc3jG.js +1 -0
  627. pythinker_code/web/static/assets/objective-cpp-CLxacb5B.js +1 -0
  628. pythinker_code/web/static/assets/ocaml-C0hk2d4L.js +1 -0
  629. pythinker_code/web/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
  630. pythinker_code/web/static/assets/one-light-PoHY5YXO.js +1 -0
  631. pythinker_code/web/static/assets/openscad-C4EeE6gA.js +1 -0
  632. pythinker_code/web/static/assets/ordinal-Cboi1Yqb.js +1 -0
  633. pythinker_code/web/static/assets/pascal-D93ZcfNL.js +1 -0
  634. pythinker_code/web/static/assets/perl-C0TMdlhV.js +1 -0
  635. pythinker_code/web/static/assets/php-CDn_0X-4.js +1 -0
  636. pythinker_code/web/static/assets/pieDiagram-ADFJNKIX-fNg41mT9.js +30 -0
  637. pythinker_code/web/static/assets/pkl-u5AG7uiY.js +1 -0
  638. pythinker_code/web/static/assets/plastic-3e1v2bzS.js +1 -0
  639. pythinker_code/web/static/assets/plsql-ChMvpjG-.js +1 -0
  640. pythinker_code/web/static/assets/po-BTJTHyun.js +1 -0
  641. pythinker_code/web/static/assets/poimandres-CS3Unz2-.js +1 -0
  642. pythinker_code/web/static/assets/polar-C0HS_06l.js +1 -0
  643. pythinker_code/web/static/assets/postcss-CXtECtnM.js +1 -0
  644. pythinker_code/web/static/assets/powerquery-CEu0bR-o.js +1 -0
  645. pythinker_code/web/static/assets/powershell-Dpen1YoG.js +1 -0
  646. pythinker_code/web/static/assets/prisma-Dd19v3D-.js +1 -0
  647. pythinker_code/web/static/assets/prolog-CbFg5uaA.js +1 -0
  648. pythinker_code/web/static/assets/proto-C7zT0LnQ.js +1 -0
  649. pythinker_code/web/static/assets/pug-CGlum2m_.js +1 -0
  650. pythinker_code/web/static/assets/puppet-BMWR74SV.js +1 -0
  651. pythinker_code/web/static/assets/purescript-CklMAg4u.js +1 -0
  652. pythinker_code/web/static/assets/python-B6aJPvgy.js +1 -0
  653. pythinker_code/web/static/assets/qml-3beO22l8.js +1 -0
  654. pythinker_code/web/static/assets/qmldir-C8lEn-DE.js +1 -0
  655. pythinker_code/web/static/assets/qss-IeuSbFQv.js +1 -0
  656. pythinker_code/web/static/assets/quadrantDiagram-AYHSOK5B-DJz3Kx87.js +7 -0
  657. pythinker_code/web/static/assets/r-Dspwwk_N.js +1 -0
  658. pythinker_code/web/static/assets/racket-BqYA7rlc.js +1 -0
  659. pythinker_code/web/static/assets/raku-DXvB9xmW.js +1 -0
  660. pythinker_code/web/static/assets/razor-C1TweQQi.js +1 -0
  661. pythinker_code/web/static/assets/red-bN70gL4F.js +1 -0
  662. pythinker_code/web/static/assets/reg-C-SQnVFl.js +1 -0
  663. pythinker_code/web/static/assets/regexp-CDVJQ6XC.js +1 -0
  664. pythinker_code/web/static/assets/rel-C3B-1QV4.js +1 -0
  665. pythinker_code/web/static/assets/requirementDiagram-UZGBJVZJ-B4SbrfE9.js +64 -0
  666. pythinker_code/web/static/assets/riscv-BM1_JUlF.js +1 -0
  667. pythinker_code/web/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
  668. pythinker_code/web/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
  669. pythinker_code/web/static/assets/rose-pine-qdsjHGoJ.js +1 -0
  670. pythinker_code/web/static/assets/rosmsg-BJDFO7_C.js +1 -0
  671. pythinker_code/web/static/assets/rst-B0xPkSld.js +1 -0
  672. pythinker_code/web/static/assets/ruby-BvKwtOVI.js +1 -0
  673. pythinker_code/web/static/assets/rust-B1yitclQ.js +1 -0
  674. pythinker_code/web/static/assets/sankeyDiagram-TZEHDZUN-CoSUjLAG.js +10 -0
  675. pythinker_code/web/static/assets/sas-cz2c8ADy.js +1 -0
  676. pythinker_code/web/static/assets/sass-Cj5Yp3dK.js +1 -0
  677. pythinker_code/web/static/assets/scala-C151Ov-r.js +1 -0
  678. pythinker_code/web/static/assets/scheme-C98Dy4si.js +1 -0
  679. pythinker_code/web/static/assets/scss-OYdSNvt2.js +1 -0
  680. pythinker_code/web/static/assets/sdbl-DVxCFoDh.js +1 -0
  681. pythinker_code/web/static/assets/sequenceDiagram-WL72ISMW-PjhBNHi3.js +145 -0
  682. pythinker_code/web/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
  683. pythinker_code/web/static/assets/shellscript-Yzrsuije.js +1 -0
  684. pythinker_code/web/static/assets/shellsession-BADoaaVG.js +1 -0
  685. pythinker_code/web/static/assets/slack-dark-BthQWCQV.js +1 -0
  686. pythinker_code/web/static/assets/slack-ochin-DqwNpetd.js +1 -0
  687. pythinker_code/web/static/assets/smalltalk-BERRCDM3.js +1 -0
  688. pythinker_code/web/static/assets/snazzy-light-Bw305WKR.js +1 -0
  689. pythinker_code/web/static/assets/solarized-dark-DXbdFlpD.js +1 -0
  690. pythinker_code/web/static/assets/solarized-light-L9t79GZl.js +1 -0
  691. pythinker_code/web/static/assets/solidity-rGO070M0.js +1 -0
  692. pythinker_code/web/static/assets/soy-Brmx7dQM.js +1 -0
  693. pythinker_code/web/static/assets/sparql-rVzFXLq3.js +1 -0
  694. pythinker_code/web/static/assets/splunk-BtCnVYZw.js +1 -0
  695. pythinker_code/web/static/assets/sql-BLtJtn59.js +1 -0
  696. pythinker_code/web/static/assets/ssh-config-_ykCGR6B.js +1 -0
  697. pythinker_code/web/static/assets/stata-BH5u7GGu.js +1 -0
  698. pythinker_code/web/static/assets/stateDiagram-FKZM4ZOC-DOwESt8-.js +1 -0
  699. pythinker_code/web/static/assets/stateDiagram-v2-4FDKWEC3-yl3OHWiP.js +1 -0
  700. pythinker_code/web/static/assets/stylus-BEDo0Tqx.js +1 -0
  701. pythinker_code/web/static/assets/svelte-zxCyuUbr.js +1 -0
  702. pythinker_code/web/static/assets/swift-Dg5xB15N.js +1 -0
  703. pythinker_code/web/static/assets/synthwave-84-CbfX1IO0.js +1 -0
  704. pythinker_code/web/static/assets/system-verilog-CnnmHF94.js +1 -0
  705. pythinker_code/web/static/assets/systemd-4A_iFExJ.js +1 -0
  706. pythinker_code/web/static/assets/talonscript-CkByrt1z.js +1 -0
  707. pythinker_code/web/static/assets/tasl-QIJgUcNo.js +1 -0
  708. pythinker_code/web/static/assets/tcl-dwOrl1Do.js +1 -0
  709. pythinker_code/web/static/assets/templ-W15q3VgB.js +1 -0
  710. pythinker_code/web/static/assets/terraform-BETggiCN.js +1 -0
  711. pythinker_code/web/static/assets/tex-CvyZ59Mk.js +1 -0
  712. pythinker_code/web/static/assets/timeline-definition-IT6M3QCI-CkCLnAgi.js +61 -0
  713. pythinker_code/web/static/assets/tokyo-night-hegEt444.js +1 -0
  714. pythinker_code/web/static/assets/toml-vGWfd6FD.js +1 -0
  715. pythinker_code/web/static/assets/treemap-KMMF4GRG-CZS5XwTf.js +128 -0
  716. pythinker_code/web/static/assets/ts-tags-zn1MmPIZ.js +1 -0
  717. pythinker_code/web/static/assets/tsv-B_m7g4N7.js +1 -0
  718. pythinker_code/web/static/assets/tsx-COt5Ahok.js +1 -0
  719. pythinker_code/web/static/assets/turtle-BsS91CYL.js +1 -0
  720. pythinker_code/web/static/assets/twig-CO9l9SDP.js +1 -0
  721. pythinker_code/web/static/assets/typescript-BPQ3VLAy.js +1 -0
  722. pythinker_code/web/static/assets/typespec-BGHnOYBU.js +1 -0
  723. pythinker_code/web/static/assets/typst-DHCkPAjA.js +1 -0
  724. pythinker_code/web/static/assets/v-BcVCzyr7.js +1 -0
  725. pythinker_code/web/static/assets/vala-CsfeWuGM.js +1 -0
  726. pythinker_code/web/static/assets/vb-D17OF-Vu.js +1 -0
  727. pythinker_code/web/static/assets/verilog-BQ8w6xss.js +1 -0
  728. pythinker_code/web/static/assets/vesper-DU1UobuO.js +1 -0
  729. pythinker_code/web/static/assets/vhdl-CeAyd5Ju.js +1 -0
  730. pythinker_code/web/static/assets/viml-CJc9bBzg.js +1 -0
  731. pythinker_code/web/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
  732. pythinker_code/web/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
  733. pythinker_code/web/static/assets/vitesse-light-CVO1_9PV.js +1 -0
  734. pythinker_code/web/static/assets/vue-DN_0RTcg.js +1 -0
  735. pythinker_code/web/static/assets/vue-html-AaS7Mt5G.js +1 -0
  736. pythinker_code/web/static/assets/vue-vine-CQOfvN7w.js +1 -0
  737. pythinker_code/web/static/assets/vyper-CDx5xZoG.js +1 -0
  738. pythinker_code/web/static/assets/wasm-CG6Dc4jp.js +1 -0
  739. pythinker_code/web/static/assets/wasm-MzD3tlZU.js +1 -0
  740. pythinker_code/web/static/assets/wenyan-BV7otONQ.js +1 -0
  741. pythinker_code/web/static/assets/wgsl-Dx-B1_4e.js +1 -0
  742. pythinker_code/web/static/assets/wikitext-BhOHFoWU.js +1 -0
  743. pythinker_code/web/static/assets/wit-5i3qLPDT.js +1 -0
  744. pythinker_code/web/static/assets/wolfram-lXgVvXCa.js +1 -0
  745. pythinker_code/web/static/assets/xml-sdJ4AIDG.js +1 -0
  746. pythinker_code/web/static/assets/xsl-CtQFsRM5.js +1 -0
  747. pythinker_code/web/static/assets/xychartDiagram-PRI3JC2R-DkqqHNLh.js +7 -0
  748. pythinker_code/web/static/assets/yaml-Buea-lGh.js +1 -0
  749. pythinker_code/web/static/assets/zenscript-DVFEvuxE.js +1 -0
  750. pythinker_code/web/static/assets/zig-VOosw3JB.js +1 -0
  751. pythinker_code/web/static/brand/apple-touch-icon.png +0 -0
  752. pythinker_code/web/static/brand/arctecture.webp +0 -0
  753. pythinker_code/web/static/brand/bimi-logo.svg +46 -0
  754. pythinker_code/web/static/brand/favicon.ico +0 -0
  755. pythinker_code/web/static/brand/fonts/dm-sans-latin-ext.woff2 +0 -0
  756. pythinker_code/web/static/brand/fonts/dm-sans-latin.woff2 +0 -0
  757. pythinker_code/web/static/brand/fonts/instrument-sans-latin-ext.woff2 +0 -0
  758. pythinker_code/web/static/brand/fonts/instrument-sans-latin.woff2 +0 -0
  759. pythinker_code/web/static/brand/fonts/instrument-serif-latin-ext.woff2 +0 -0
  760. pythinker_code/web/static/brand/fonts/instrument-serif-latin.woff2 +0 -0
  761. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin-ext.woff2 +0 -0
  762. pythinker_code/web/static/brand/fonts/libre-baskerville-italic-latin.woff2 +0 -0
  763. pythinker_code/web/static/brand/fonts/libre-baskerville-latin-ext.woff2 +0 -0
  764. pythinker_code/web/static/brand/fonts/libre-baskerville-latin.woff2 +0 -0
  765. pythinker_code/web/static/brand/fonts/roboto-latin-ext.woff2 +0 -0
  766. pythinker_code/web/static/brand/fonts/roboto-latin.woff2 +0 -0
  767. pythinker_code/web/static/brand/icon-192.png +0 -0
  768. pythinker_code/web/static/brand/icon-512.png +0 -0
  769. pythinker_code/web/static/brand/icon.svg +1 -0
  770. pythinker_code/web/static/brand/logo.png +0 -0
  771. pythinker_code/web/static/brand/pythinker_animated.svg +79 -0
  772. pythinker_code/web/static/brand/robots.txt +4 -0
  773. pythinker_code/web/static/index.html +15 -0
  774. pythinker_code/web/static/logo.png +0 -0
  775. pythinker_code/web/store/__init__.py +1 -0
  776. pythinker_code/web/store/sessions.py +433 -0
  777. pythinker_code/wire/__init__.py +148 -0
  778. pythinker_code/wire/file.py +151 -0
  779. pythinker_code/wire/jsonrpc.py +263 -0
  780. pythinker_code/wire/protocol.py +2 -0
  781. pythinker_code/wire/root_hub.py +27 -0
  782. pythinker_code/wire/serde.py +26 -0
  783. pythinker_code/wire/server.py +1072 -0
  784. pythinker_code/wire/types.py +698 -0
  785. pythinker_code-0.8.0.dist-info/METADATA +706 -0
  786. pythinker_code-0.8.0.dist-info/RECORD +790 -0
  787. pythinker_code-0.8.0.dist-info/WHEEL +4 -0
  788. pythinker_code-0.8.0.dist-info/entry_points.txt +4 -0
  789. pythinker_code-0.8.0.dist-info/licenses/LICENSE +202 -0
  790. pythinker_code-0.8.0.dist-info/licenses/NOTICE +14 -0
@@ -0,0 +1,1806 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import re
6
+ import shlex
7
+ import time
8
+ from collections import deque
9
+ from collections.abc import Awaitable, Callable, Coroutine
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+ from typing import Any, Protocol, cast
13
+
14
+ from pythinker_core.chat_provider import (
15
+ APIConnectionError,
16
+ APIEmptyResponseError,
17
+ APIStatusError,
18
+ APITimeoutError,
19
+ ChatProviderError,
20
+ )
21
+ from rich.console import Group, RenderableType
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from rich.text import Text
25
+
26
+ from pythinker_code.background import list_task_views
27
+ from pythinker_code.constant import get_version
28
+ from pythinker_code.llm import model_display_name
29
+ from pythinker_code.notifications import NotificationManager, NotificationWatcher
30
+ from pythinker_code.soul import (
31
+ LLMNotSet,
32
+ LLMNotSupported,
33
+ MaxStepsReached,
34
+ RunCancelled,
35
+ Soul,
36
+ run_soul,
37
+ )
38
+ from pythinker_code.soul.pythinkersoul import FLOW_COMMAND_PREFIX, PythinkerSoul
39
+ from pythinker_code.ui.shell.components.render_utils import render_message_response, sanitize_ansi
40
+ from pythinker_code.ui.shell.console import console
41
+ from pythinker_code.ui.shell.echo import render_user_echo_text
42
+ from pythinker_code.ui.shell.mcp_status import render_mcp_prompt
43
+ from pythinker_code.ui.shell.prompt import (
44
+ BgTaskCounts,
45
+ CustomPromptSession,
46
+ CwdLostError,
47
+ PromptMode,
48
+ UserInput,
49
+ toast,
50
+ )
51
+ from pythinker_code.ui.shell.replay import replay_recent_history
52
+ from pythinker_code.ui.shell.slash import SKILL_COMMAND_PREFIX, shell_mode_registry
53
+ from pythinker_code.ui.shell.slash import registry as shell_slash_registry
54
+ from pythinker_code.ui.shell.update import (
55
+ UpdateResult,
56
+ do_update,
57
+ )
58
+ from pythinker_code.ui.shell.visualize import (
59
+ ApprovalPromptDelegate,
60
+ visualize,
61
+ )
62
+ from pythinker_code.utils.aioqueue import QueueShutDown
63
+ from pythinker_code.utils.envvar import get_env_bool
64
+ from pythinker_code.utils.logging import logger
65
+ from pythinker_code.utils.signals import install_sigint_handler
66
+ from pythinker_code.utils.slashcmd import SlashCommand, SlashCommandCall, parse_slash_command_call
67
+ from pythinker_code.utils.subprocess_env import get_clean_env
68
+ from pythinker_code.utils.term import ensure_new_line, ensure_tty_sane
69
+ from pythinker_code.wire.types import (
70
+ ApprovalRequest,
71
+ ApprovalResponse,
72
+ ContentPart,
73
+ StatusUpdate,
74
+ WireMessage,
75
+ )
76
+
77
+
78
+ @dataclass(slots=True)
79
+ class _PromptEvent:
80
+ kind: str
81
+ user_input: UserInput | None = None
82
+
83
+
84
+ _MAX_BG_AUTO_TRIGGER_FAILURES = 3
85
+ """Stop auto-triggering after this many consecutive failures."""
86
+
87
+ _BG_AUTO_TRIGGER_INPUT_GRACE_S = 0.75
88
+ """Delay background auto-trigger briefly after local prompt activity."""
89
+
90
+ _VISIBLE_WORKFLOW_SLASH_PREFIXES = (SKILL_COMMAND_PREFIX, FLOW_COMMAND_PREFIX)
91
+ """Explicit skill/flow prefixes that should remain visible in transcript."""
92
+
93
+
94
+ def _format_local_shell_output(
95
+ *, stdout: str, stderr: str, returncode: int | None
96
+ ) -> RenderableType | None:
97
+ """Render local shell-mode output in the reference response sequence.
98
+
99
+ Stdout appears first, then stderr, then a compact exit/no-output status,
100
+ all under the caller's shared ``⎿`` gutter.
101
+ """
102
+ children: list[RenderableType] = []
103
+ if stdout:
104
+ children.append(Text(sanitize_ansi(stdout).rstrip("\n"), style="grey70"))
105
+ if stderr:
106
+ children.append(Text(sanitize_ansi(stderr).rstrip("\n"), style="red"))
107
+ if not stdout and not stderr:
108
+ children.append(Text("(No output)", style="grey50"))
109
+ if returncode not in (None, 0):
110
+ children.append(Text(f"exit {returncode}", style="red"))
111
+ if not children:
112
+ return None
113
+ return Group(*children) if len(children) > 1 else children[0]
114
+
115
+
116
+ class _BackgroundCompletionWatcher:
117
+ """Watches for background task completions and auto-triggers the agent.
118
+
119
+ Sits between the idle event loop and the soul: when a background task
120
+ finishes while the agent is idle *and* the LLM hasn't consumed the
121
+ notification yet, it triggers a soul run.
122
+
123
+ Important: pre-existing pending notifications alone should not trigger a
124
+ foreground run immediately on session resume. They are consumed either by
125
+ the next actual background completion signal or by the next user-triggered
126
+ turn.
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ soul: Soul,
132
+ *,
133
+ can_auto_trigger_pending: Callable[[], bool] | None = None,
134
+ ) -> None:
135
+ self._event: asyncio.Event | None = None
136
+ self._notifications: NotificationManager | None = None
137
+ self._can_auto_trigger_pending = can_auto_trigger_pending or (lambda: True)
138
+ if isinstance(soul, PythinkerSoul):
139
+ self._event = soul.runtime.background_tasks.completion_event
140
+ self._notifications = soul.runtime.notifications
141
+
142
+ @property
143
+ def enabled(self) -> bool:
144
+ return self._event is not None
145
+
146
+ def clear(self) -> None:
147
+ """Clear stale signals from the previous soul run."""
148
+ if self._event is not None:
149
+ self._event.clear()
150
+
151
+ async def wait_for_next(self, idle_events: asyncio.Queue[_PromptEvent]) -> _PromptEvent | None:
152
+ """Wait for either a user prompt event or a background completion.
153
+
154
+ Returns the prompt event if user input arrived first, or ``None``
155
+ if a background task completed with unclaimed LLM notifications.
156
+ User input always takes priority over background completions.
157
+ """
158
+ if self.enabled and self._has_pending_llm_notifications():
159
+ # Pending notifications already exist (for example after resume).
160
+ # Before the user sends the first foreground turn after resume,
161
+ # pending background notifications should not auto-trigger a run.
162
+ # Once the shell is armed by a user-triggered turn, pending
163
+ # notifications can resume the normal auto-follow-up behavior.
164
+ try:
165
+ return idle_events.get_nowait()
166
+ except asyncio.QueueEmpty:
167
+ if self._can_auto_trigger_pending():
168
+ return None
169
+
170
+ idle_task = asyncio.create_task(idle_events.get())
171
+ if not self.enabled:
172
+ return await idle_task
173
+
174
+ assert self._event is not None
175
+ bg_wait_task = asyncio.create_task(self._event.wait())
176
+
177
+ done, _ = await asyncio.wait(
178
+ [idle_task, bg_wait_task],
179
+ return_when=asyncio.FIRST_COMPLETED,
180
+ )
181
+ for t in (idle_task, bg_wait_task):
182
+ if t not in done:
183
+ t.cancel()
184
+ with contextlib.suppress(asyncio.CancelledError):
185
+ await t
186
+
187
+ if idle_task in done:
188
+ if bg_wait_task in done:
189
+ self._event.clear()
190
+ return idle_task.result()
191
+
192
+ # Only bg fired
193
+ self._event.clear()
194
+ if self._has_pending_llm_notifications():
195
+ if self._can_auto_trigger_pending():
196
+ return None
197
+ return _PromptEvent(kind="bg_noop")
198
+ return _PromptEvent(kind="bg_noop")
199
+
200
+ def _has_pending_llm_notifications(self) -> bool:
201
+ if self._notifications is None:
202
+ return False
203
+ return self._notifications.has_pending_for_sink("llm")
204
+
205
+
206
+ class _BackgroundAutoTriggerPromptState(Protocol):
207
+ def has_pending_input(self) -> bool: ...
208
+
209
+ def had_recent_input_activity(self, *, within_s: float) -> bool: ...
210
+
211
+ def recent_input_activity_remaining(self, *, within_s: float) -> float: ...
212
+
213
+ async def wait_for_input_activity(self) -> None: ...
214
+
215
+
216
+ _LM_STUDIO_NCTX_RE = re.compile(r"n_keep:\s*(\d+)\s*>=\s*n_ctx:\s*(\d+)")
217
+ _LM_STUDIO_LOAD_FAILED_RE = re.compile(r'Failed to load model\s+"([^"]+)"', re.IGNORECASE)
218
+ _LM_STUDIO_JINJA_ERROR_RE = re.compile(r"Error rendering prompt with jinja template", re.IGNORECASE)
219
+
220
+
221
+ def _is_lm_studio_context_too_small(exc: BaseException) -> bool:
222
+ """Detect LM Studio's `n_keep:N >= n_ctx:M` error pattern.
223
+
224
+ LM Studio returns an HTTP 400 with this message when the loaded
225
+ context length is smaller than the prompt the agent is trying to send.
226
+ """
227
+ return _LM_STUDIO_NCTX_RE.search(str(exc)) is not None
228
+
229
+
230
+ def _parse_n_keep_n_ctx(message: str) -> tuple[int, int]:
231
+ """Extract (n_keep, n_ctx) from an LM Studio context-too-small error.
232
+
233
+ Returns (0, 0) if the pattern doesn't match — caller should have
234
+ gated on `_is_lm_studio_context_too_small` first.
235
+ """
236
+ match = _LM_STUDIO_NCTX_RE.search(message)
237
+ if match is None:
238
+ return (0, 0)
239
+ return (int(match.group(1)), int(match.group(2)))
240
+
241
+
242
+ def _is_lm_studio_load_failed(exc: BaseException) -> bool:
243
+ """Detect LM Studio's `Failed to load model "<id>"` pattern.
244
+
245
+ LM Studio returns this when JIT-loading on a chat request fails — usually
246
+ VRAM exhaustion, but also: model file corrupted, model not compatible
247
+ with the runtime, or the user manually evicted the model.
248
+ """
249
+ return _LM_STUDIO_LOAD_FAILED_RE.search(str(exc)) is not None
250
+
251
+
252
+ def _parse_lm_studio_load_failed_model(message: str) -> str:
253
+ """Extract the failing model id; returns '' if the pattern doesn't match."""
254
+ match = _LM_STUDIO_LOAD_FAILED_RE.search(message)
255
+ return match.group(1) if match else ""
256
+
257
+
258
+ def _is_lm_studio_jinja_template_error(exc: BaseException) -> bool:
259
+ """Detect LM Studio's jinja-template rendering errors.
260
+
261
+ Many GGUF prompt templates are buggy or version-mismatched (e.g., apply
262
+ string filter to a null value). The fix is on LM Studio's side — either
263
+ switch model variant or override the template — so Pythinker can only
264
+ point the user at the right place.
265
+ """
266
+ return _LM_STUDIO_JINJA_ERROR_RE.search(str(exc)) is not None
267
+
268
+
269
+ def _extract_429_detail(exc: BaseException) -> dict[str, str]:
270
+ """Pull a human-readable summary + hint out of a 429 APIStatusError body.
271
+
272
+ Providers vary widely in their 429 payload shape. We try the well-known
273
+ `{"error": {"type": ..., "message": ...}}` envelope first (OpenAI /
274
+ OpenCode / Anthropic / etc.), then fall back to scraping a URL hint and
275
+ finally to the stringified exception.
276
+ """
277
+ body: dict[str, object] | None = None
278
+ payload = getattr(exc, "body", None)
279
+ if isinstance(payload, dict):
280
+ body = cast(dict[str, object], payload)
281
+ if body is None:
282
+ for attr in ("response_json", "response_data"):
283
+ value = getattr(exc, attr, None)
284
+ if isinstance(value, dict):
285
+ body = cast(dict[str, object], value)
286
+ break
287
+
288
+ summary = ""
289
+ err_type = ""
290
+ if body is not None:
291
+ err = body.get("error")
292
+ if isinstance(err, dict):
293
+ typed_err = cast(dict[str, object], err)
294
+ err_type = str(typed_err.get("type") or "")
295
+ summary = str(typed_err.get("message") or "")
296
+
297
+ if not summary:
298
+ text = str(exc)
299
+ summary = text if len(text) <= 280 else text[:277] + "..."
300
+
301
+ hint = "Wait until the limit window resets, or upgrade / top up your plan."
302
+ if "GoUsageLimitError" in err_type:
303
+ hint = "OpenCode-Go monthly limit. Resets in the window the server stated above."
304
+ elif "Anthropic" in str(type(exc).__module__) or "anthropic" in err_type.lower():
305
+ hint = "Anthropic rate limit. Slow request rate, or check your plan tier."
306
+ elif "openai" in str(type(exc).__module__).lower():
307
+ hint = "OpenAI rate or usage limit. Check usage dashboard or wait for the reset window."
308
+
309
+ return {"summary": summary, "hint": hint}
310
+
311
+
312
+ class Shell:
313
+ def __init__(
314
+ self,
315
+ soul: Soul,
316
+ welcome_info: list[WelcomeInfoItem] | None = None,
317
+ prefill_text: str | None = None,
318
+ ):
319
+ self.soul = soul
320
+ self._welcome_info = list(welcome_info or [])
321
+ self._prefill_text = prefill_text
322
+ self._background_tasks: set[asyncio.Task[Any]] = set()
323
+ self._prompt_session: CustomPromptSession | None = None
324
+ self._running_input_handler: Callable[[UserInput], None] | None = None
325
+ self._running_interrupt_handler: Callable[[], None] | None = None
326
+ self._active_approval_sink: Any | None = None
327
+ self._active_view: Any | None = None
328
+ self._pending_approval_requests = deque[ApprovalRequest]()
329
+ self._current_prompt_approval_request: ApprovalRequest | None = None
330
+ self._approval_modal: ApprovalPromptDelegate | None = None
331
+ self._exit_after_run = False
332
+ self._available_slash_commands: dict[str, SlashCommand[Any]] = {
333
+ **{cmd.name: cmd for cmd in soul.available_slash_commands},
334
+ **{cmd.name: cmd for cmd in shell_slash_registry.list_commands()},
335
+ }
336
+ """Shell-level slash commands + soul-level slash commands. Name to command mapping."""
337
+
338
+ @property
339
+ def available_slash_commands(self) -> dict[str, SlashCommand[Any]]:
340
+ """Get all available slash commands, including shell-level and soul-level commands."""
341
+ return self._available_slash_commands
342
+
343
+ def _print_cwd_lost_crash(self) -> None:
344
+ """Print a crash report when the working directory is no longer accessible."""
345
+ runtime = self.soul.runtime if isinstance(self.soul, PythinkerSoul) else None
346
+ session_id = runtime.session.id if runtime else "unknown"
347
+ work_dir = str(runtime.session.work_dir) if runtime else "unknown"
348
+
349
+ info = Table.grid(padding=(0, 1))
350
+ info.add_row("Session:", session_id)
351
+ info.add_row("Working directory:", work_dir)
352
+
353
+ panel = Panel(
354
+ Group(
355
+ Text(
356
+ "The working directory is no longer accessible "
357
+ "(external drive unplugged, directory deleted, or filesystem unmounted).",
358
+ ),
359
+ Text(""),
360
+ info,
361
+ Text(""),
362
+ Text(
363
+ "Your conversation history has been saved. "
364
+ "Restart pythinker in a valid directory to continue.",
365
+ style="dim",
366
+ ),
367
+ ),
368
+ title="[bold red]Session crashed[/bold red]",
369
+ border_style="red",
370
+ )
371
+ console.print()
372
+ console.print(panel)
373
+
374
+ @staticmethod
375
+ def _should_exit_input(user_input: UserInput | str) -> bool:
376
+ command = user_input if isinstance(user_input, str) else user_input.command
377
+ return command.strip() in {"exit", "quit", "/exit", "/quit"}
378
+
379
+ @staticmethod
380
+ def _agent_slash_command_call(user_input: UserInput) -> SlashCommandCall | None:
381
+ if user_input.mode != PromptMode.AGENT:
382
+ return None
383
+ display_call = parse_slash_command_call(user_input.command)
384
+ if display_call is None:
385
+ return None
386
+ resolved_call = parse_slash_command_call(user_input.resolved_command)
387
+ if resolved_call is None or resolved_call.name != display_call.name:
388
+ return display_call
389
+ return resolved_call
390
+
391
+ @staticmethod
392
+ def _should_echo_workflow_slash_input(user_input: UserInput) -> bool:
393
+ command_call = Shell._agent_slash_command_call(user_input)
394
+ return command_call is not None and command_call.name.startswith(
395
+ _VISIBLE_WORKFLOW_SLASH_PREFIXES
396
+ )
397
+
398
+ def _should_echo_agent_input(self, user_input: UserInput) -> bool:
399
+ if user_input.mode != PromptMode.AGENT:
400
+ return False
401
+ if Shell._should_exit_input(user_input):
402
+ return False
403
+ # Phase 1 policy: keep operational slash commands hidden, but show
404
+ # explicit `/skill:*` and `/flow:*` inputs because they represent
405
+ # user-visible workflow intent and otherwise vanish from transcript
406
+ # even when the command later fails to resolve.
407
+ if self._should_echo_workflow_slash_input(user_input):
408
+ return True
409
+ return Shell._agent_slash_command_call(user_input) is None
410
+
411
+ @staticmethod
412
+ def _echo_agent_input(user_input: UserInput) -> None:
413
+ console.print(render_user_echo_text(user_input.command))
414
+
415
+ def _bind_running_input(
416
+ self,
417
+ on_input: Callable[[UserInput], None],
418
+ on_interrupt: Callable[[], None],
419
+ ) -> None:
420
+ self._running_input_handler = on_input
421
+ self._running_interrupt_handler = on_interrupt
422
+
423
+ def _unbind_running_input(self) -> None:
424
+ self._running_input_handler = None
425
+ self._running_interrupt_handler = None
426
+
427
+ async def _route_prompt_events(
428
+ self,
429
+ prompt_session: CustomPromptSession,
430
+ idle_events: asyncio.Queue[_PromptEvent],
431
+ resume_prompt: asyncio.Event,
432
+ ) -> None:
433
+ while True:
434
+ # Keep exactly one active prompt read. Idle submissions pause the
435
+ # router until the shell decides whether the next prompt should
436
+ # wait for a blocking action or stay live during an agent run.
437
+ await resume_prompt.wait()
438
+ ensure_tty_sane()
439
+ try:
440
+ ensure_new_line()
441
+ user_input = await prompt_session.prompt_next()
442
+ except KeyboardInterrupt:
443
+ logger.debug("Prompt router got KeyboardInterrupt")
444
+ if (
445
+ self._running_input_handler is not None
446
+ and prompt_session.running_prompt_accepts_submission()
447
+ ):
448
+ if self._running_interrupt_handler is not None:
449
+ self._running_interrupt_handler()
450
+ continue
451
+ resume_prompt.clear()
452
+ await idle_events.put(_PromptEvent(kind="interrupt"))
453
+ continue
454
+ except EOFError:
455
+ logger.debug("Prompt router got EOF")
456
+ if (
457
+ self._running_input_handler is not None
458
+ and prompt_session.running_prompt_accepts_submission()
459
+ ):
460
+ self._exit_after_run = True
461
+ if self._running_interrupt_handler is not None:
462
+ self._running_interrupt_handler()
463
+ return
464
+ resume_prompt.clear()
465
+ await idle_events.put(_PromptEvent(kind="eof"))
466
+ return
467
+ except CwdLostError:
468
+ logger.error("Working directory no longer exists")
469
+ resume_prompt.clear()
470
+ await idle_events.put(_PromptEvent(kind="cwd_lost"))
471
+ return
472
+ except Exception:
473
+ logger.exception("Prompt router crashed")
474
+ resume_prompt.clear()
475
+ await idle_events.put(_PromptEvent(kind="error"))
476
+ return
477
+
478
+ if prompt_session.last_submission_was_running: # noqa: SIM102
479
+ if self._running_input_handler is not None:
480
+ if user_input:
481
+ self._running_input_handler(user_input)
482
+ continue
483
+ # Handler already unbound — fall through to idle path.
484
+
485
+ resume_prompt.clear()
486
+ await idle_events.put(_PromptEvent(kind="input", user_input=user_input))
487
+
488
+ async def run(self, command: str | None = None) -> bool:
489
+ _run_start_time = time.monotonic()
490
+
491
+ # Initialize theme + TUI style from config
492
+ if isinstance(self.soul, PythinkerSoul):
493
+ from pythinker_code.extensions import run_pending_extensions
494
+ from pythinker_code.ui.theme import set_active_theme
495
+ from pythinker_code.ui.tui_config import (
496
+ is_card_style,
497
+ set_active_tui_style,
498
+ )
499
+
500
+ set_active_theme(self.soul.runtime.config.theme)
501
+ set_active_tui_style(self.soul.runtime.config.tui.style)
502
+ if is_card_style():
503
+ from pythinker_code.ui.shell.tool_renderers import (
504
+ register_builtin_renderers,
505
+ )
506
+
507
+ register_builtin_renderers()
508
+
509
+ # Run any pending extension setup callbacks. Safe to call when
510
+ # nothing's queued — the function returns an empty list and
511
+ # extensions register lazily.
512
+ started = run_pending_extensions()
513
+ if started:
514
+ logger.debug("Started extensions: {names}", names=", ".join(started))
515
+
516
+ if command is not None:
517
+ # run single command and exit
518
+ logger.info("Running agent with command: {command}", command=command)
519
+ if isinstance(self.soul, PythinkerSoul):
520
+ self._start_background_task(self._watch_root_wire_hub())
521
+ try:
522
+ if self._should_exit_input(command):
523
+ console.print("Bye!")
524
+ return True
525
+ if (slash_cmd_call := parse_slash_command_call(command)) and (
526
+ shell_slash_registry.find_command(slash_cmd_call.name)
527
+ ):
528
+ await self._run_slash_command(slash_cmd_call)
529
+ return True
530
+ return await self.run_soul_command(command)
531
+ finally:
532
+ self._cancel_background_tasks()
533
+
534
+ # Start auto-update background task if not disabled
535
+ if get_env_bool("PYTHINKER_CLI_NO_AUTO_UPDATE"):
536
+ logger.info("Auto-update disabled by PYTHINKER_CLI_NO_AUTO_UPDATE environment variable")
537
+ else:
538
+ self._start_background_task(self._auto_update())
539
+
540
+ _print_welcome_info(self.soul.name or "Pythinker CLI", self._welcome_info)
541
+
542
+ # Start telemetry periodic flush and disk retry
543
+ from pythinker_code.telemetry import get_sink
544
+
545
+ _telemetry_sink = get_sink()
546
+ if _telemetry_sink is not None:
547
+ _telemetry_sink.start_periodic_flush()
548
+ self._start_background_task(_telemetry_sink.retry_disk_events())
549
+
550
+ if isinstance(self.soul, PythinkerSoul):
551
+ watcher = NotificationWatcher(
552
+ self.soul.runtime.notifications,
553
+ sink="shell",
554
+ before_poll=self.soul.runtime.background_tasks.reconcile,
555
+ on_notification=lambda notification: toast(
556
+ f"[{notification.event.type}] {notification.event.title}",
557
+ topic="notification",
558
+ duration=10.0,
559
+ ),
560
+ )
561
+ self._start_background_task(watcher.run_forever())
562
+ self._start_background_task(self._watch_root_wire_hub())
563
+ await replay_recent_history(
564
+ self.soul.context.history,
565
+ wire_file=self.soul.wire_file,
566
+ show_thinking_stream=self.soul.runtime.config.show_thinking_stream,
567
+ )
568
+ await self.soul.start_background_mcp_loading()
569
+
570
+ async def _plan_mode_toggle() -> bool:
571
+ if isinstance(self.soul, PythinkerSoul):
572
+ return await self.soul.toggle_plan_mode_from_manual()
573
+ return False
574
+
575
+ def _mcp_status_block(columns: int):
576
+ if not isinstance(self.soul, PythinkerSoul):
577
+ return None
578
+ snapshot = self.soul.status.mcp_status
579
+ if snapshot is None:
580
+ return None
581
+ return render_mcp_prompt(snapshot)
582
+
583
+ def _mcp_status_loading() -> bool:
584
+ if not isinstance(self.soul, PythinkerSoul):
585
+ return False
586
+ snapshot = self.soul.status.mcp_status
587
+ return bool(snapshot and snapshot.loading)
588
+
589
+ @dataclass
590
+ class _BgCountCache:
591
+ time: float = 0.0
592
+ counts: BgTaskCounts = BgTaskCounts()
593
+
594
+ _bg_cache = _BgCountCache()
595
+
596
+ def _bg_task_counts() -> BgTaskCounts:
597
+ if not isinstance(self.soul, PythinkerSoul):
598
+ return BgTaskCounts()
599
+ now = time.monotonic()
600
+ if now - _bg_cache.time < 1.0:
601
+ return _bg_cache.counts
602
+ views = list_task_views(self.soul.runtime.background_tasks, active_only=True)
603
+ bash_n = sum(1 for v in views if v.spec.kind == "bash")
604
+ agent_n = sum(1 for v in views if v.spec.kind == "agent")
605
+ _bg_cache.counts = BgTaskCounts(bash=bash_n, agent=agent_n)
606
+ _bg_cache.time = now
607
+ return _bg_cache.counts
608
+
609
+ with CustomPromptSession(
610
+ status_provider=lambda: self.soul.status,
611
+ status_block_provider=_mcp_status_block,
612
+ fast_refresh_provider=_mcp_status_loading,
613
+ background_task_count_provider=_bg_task_counts,
614
+ model_capabilities=self.soul.model_capabilities or set(),
615
+ model_name=model_display_name(
616
+ self.soul.model_name,
617
+ self.soul.runtime.llm.model_config
618
+ if isinstance(self.soul, PythinkerSoul) and self.soul.runtime.llm
619
+ else None,
620
+ ),
621
+ thinking=self.soul.thinking or False,
622
+ agent_mode_slash_commands=list(self._available_slash_commands.values()),
623
+ shell_mode_slash_commands=shell_mode_registry.list_commands(),
624
+ editor_command_provider=lambda: (
625
+ self.soul.runtime.config.default_editor
626
+ if isinstance(self.soul, PythinkerSoul)
627
+ else ""
628
+ ),
629
+ plan_mode_toggle_callback=_plan_mode_toggle,
630
+ ) as prompt_session:
631
+ self._prompt_session = prompt_session
632
+ if self._prefill_text:
633
+ prompt_session.set_prefill_text(self._prefill_text)
634
+ self._prefill_text = None
635
+ if isinstance(self.soul, PythinkerSoul):
636
+ pythinker_soul = self.soul
637
+ snapshot = pythinker_soul.status.mcp_status
638
+ if snapshot and snapshot.loading:
639
+
640
+ async def _invalidate_after_mcp_loading() -> None:
641
+ try:
642
+ await pythinker_soul.wait_for_background_mcp_loading()
643
+ except Exception:
644
+ logger.debug("MCP loading finished with error while refreshing prompt")
645
+ if self._prompt_session is prompt_session:
646
+ prompt_session.invalidate()
647
+
648
+ self._start_background_task(_invalidate_after_mcp_loading())
649
+ self._exit_after_run = False
650
+ idle_events: asyncio.Queue[_PromptEvent] = asyncio.Queue()
651
+ # resume_prompt controls whether the prompt router reads input.
652
+ # Set BEFORE an await = prompt stays live during the operation
653
+ # (agent runs that accept steer input); set AFTER = prompt is
654
+ # paused until the operation finishes.
655
+ resume_prompt = asyncio.Event()
656
+ resume_prompt.set()
657
+ prompt_task = asyncio.create_task(
658
+ self._route_prompt_events(prompt_session, idle_events, resume_prompt)
659
+ )
660
+ background_autotrigger_armed = False
661
+
662
+ def _can_auto_trigger_pending() -> bool:
663
+ return background_autotrigger_armed
664
+
665
+ bg_watcher = _BackgroundCompletionWatcher(
666
+ self.soul,
667
+ can_auto_trigger_pending=_can_auto_trigger_pending,
668
+ )
669
+
670
+ shell_ok = True
671
+ bg_auto_failures = 0
672
+ deferred_bg_trigger = False
673
+ try:
674
+ while True:
675
+ if deferred_bg_trigger and not self._should_defer_background_auto_trigger(
676
+ prompt_session
677
+ ):
678
+ result = None
679
+ elif deferred_bg_trigger:
680
+ result = await self._wait_for_input_or_activity(
681
+ prompt_session,
682
+ idle_events,
683
+ timeout_s=self._background_auto_trigger_timeout_s(prompt_session),
684
+ )
685
+ else:
686
+ bg_watcher.clear()
687
+ if bg_auto_failures >= _MAX_BG_AUTO_TRIGGER_FAILURES:
688
+ result = await idle_events.get()
689
+ else:
690
+ result = await bg_watcher.wait_for_next(idle_events)
691
+
692
+ if result is None:
693
+ if self._should_defer_background_auto_trigger(prompt_session):
694
+ deferred_bg_trigger = True
695
+ resume_prompt.set()
696
+ continue
697
+ deferred_bg_trigger = False
698
+ logger.info("Background task completed while idle, triggering agent")
699
+ resume_prompt.set()
700
+ ok = await self.run_soul_command(
701
+ "<system-reminder>"
702
+ "Background tasks completed while you"
703
+ " were idle."
704
+ "</system-reminder>"
705
+ )
706
+ console.print()
707
+ if not ok:
708
+ bg_auto_failures += 1
709
+ logger.warning(
710
+ "Background auto-trigger failed ({n}/{max})",
711
+ n=bg_auto_failures,
712
+ max=_MAX_BG_AUTO_TRIGGER_FAILURES,
713
+ )
714
+ else:
715
+ bg_auto_failures = 0
716
+ if self._exit_after_run:
717
+ console.print("Bye!")
718
+ break
719
+ continue
720
+
721
+ event = result
722
+
723
+ if event.kind == "input_activity":
724
+ continue
725
+
726
+ if event.kind == "bg_noop":
727
+ continue
728
+
729
+ if event.kind == "interrupt":
730
+ console.print("[grey50]Tip: press Ctrl-D or send 'exit' to quit[/grey50]")
731
+ resume_prompt.set()
732
+ continue
733
+
734
+ if event.kind == "eof":
735
+ console.print("Bye!")
736
+ break
737
+
738
+ if event.kind == "cwd_lost":
739
+ self._print_cwd_lost_crash()
740
+ shell_ok = False
741
+ break
742
+
743
+ if event.kind == "error":
744
+ shell_ok = False
745
+ break
746
+
747
+ user_input = event.user_input
748
+ assert user_input is not None
749
+ bg_auto_failures = 0
750
+ deferred_bg_trigger = False
751
+ if not user_input:
752
+ logger.debug("Got empty input, skipping")
753
+ resume_prompt.set()
754
+ continue
755
+ logger.debug("Got user input: {user_input}", user_input=user_input)
756
+
757
+ if self._should_echo_agent_input(user_input):
758
+ self._echo_agent_input(user_input)
759
+
760
+ if self._should_exit_input(user_input):
761
+ logger.debug("Exiting by slash command")
762
+ console.print("Bye!")
763
+ break
764
+
765
+ if user_input.mode == PromptMode.SHELL:
766
+ await self._run_shell_command(user_input.command)
767
+ resume_prompt.set()
768
+ continue
769
+
770
+ # Unified input routing — intercept local commands
771
+ # before they reach the soul/wire.
772
+ from pythinker_code.ui.shell.visualize import InputAction, classify_input
773
+
774
+ # Use resolved_command (placeholder-expanded) so /btw
775
+ # receives the actual pasted content, not "[Pasted text #1]".
776
+ input_text = (
777
+ user_input.resolved_command
778
+ if hasattr(user_input, "resolved_command")
779
+ else str(user_input)
780
+ )
781
+ action = classify_input(input_text, is_streaming=False)
782
+ if action.kind == InputAction.BTW and isinstance(self.soul, PythinkerSoul):
783
+ from pythinker_code.telemetry import track
784
+
785
+ track("input_btw")
786
+ await self._run_btw_modal(action.args, prompt_session)
787
+ resume_prompt.set()
788
+ continue
789
+ if action.kind == InputAction.IGNORED:
790
+ console.print(f"[dim]{action.args}[/dim]")
791
+ resume_prompt.set()
792
+ continue
793
+
794
+ if slash_cmd_call := self._agent_slash_command_call(user_input):
795
+ is_soul_slash = (
796
+ slash_cmd_call.name in self._available_slash_commands
797
+ and shell_slash_registry.find_command(slash_cmd_call.name) is None
798
+ )
799
+ if is_soul_slash:
800
+ from pythinker_code.telemetry import track
801
+
802
+ track("input_command", command=slash_cmd_call.name)
803
+ background_autotrigger_armed = True
804
+ resume_prompt.set()
805
+ await self.run_soul_command(slash_cmd_call.raw_input)
806
+ console.print()
807
+ if self._exit_after_run:
808
+ console.print("Bye!")
809
+ break
810
+ else:
811
+ await self._run_slash_command(slash_cmd_call)
812
+ resume_prompt.set()
813
+ continue
814
+
815
+ background_autotrigger_armed = True
816
+ resume_prompt.set()
817
+ await self.run_soul_command(user_input.content)
818
+ console.print()
819
+ if self._exit_after_run:
820
+ console.print("Bye!")
821
+ break
822
+ finally:
823
+ prompt_task.cancel()
824
+ with contextlib.suppress(asyncio.CancelledError):
825
+ await prompt_task
826
+ self._running_input_handler = None
827
+ self._running_interrupt_handler = None
828
+ if self._prompt_session is prompt_session and self._approval_modal is not None:
829
+ prompt_session.detach_modal(self._approval_modal)
830
+ self._approval_modal = None
831
+ self._prompt_session = None
832
+ self._cancel_background_tasks()
833
+ # Track exit and flush remaining telemetry events.
834
+ # Cap the exit-path flush at 3 s so we don't block for ~50 s
835
+ # when the endpoint is unreachable (in-process retry backoff).
836
+ # On timeout the CancelledError handler in transport.send()
837
+ # persists in-flight events to disk; flush_sync() catches any
838
+ # events still in the buffer.
839
+ from pythinker_code.telemetry import track
840
+
841
+ track("exit", duration_s=time.monotonic() - _run_start_time)
842
+ if _telemetry_sink is not None:
843
+ _telemetry_sink.stop_periodic_flush()
844
+ try:
845
+ await asyncio.wait_for(_telemetry_sink.flush(), timeout=3.0)
846
+ except (TimeoutError, Exception):
847
+ _telemetry_sink.flush_sync()
848
+ ensure_tty_sane()
849
+
850
+ return shell_ok
851
+
852
+ async def _run_shell_command(self, command: str) -> None:
853
+ """Run a shell command in foreground."""
854
+ if not command.strip():
855
+ return
856
+
857
+ # Check if it's an allowed slash command in shell mode
858
+ if slash_cmd_call := parse_slash_command_call(command):
859
+ if shell_mode_registry.find_command(slash_cmd_call.name):
860
+ await self._run_slash_command(slash_cmd_call)
861
+ return
862
+ else:
863
+ console.print(
864
+ f'[yellow]"/{slash_cmd_call.name}" is not available in shell mode. '
865
+ "Press Ctrl-X to switch to agent mode.[/yellow]"
866
+ )
867
+ return
868
+
869
+ # Check if user is trying to use 'cd' command
870
+ stripped_cmd = command.strip()
871
+ split_cmd: list[str] | None = None
872
+ try:
873
+ split_cmd = shlex.split(stripped_cmd)
874
+ except ValueError as exc:
875
+ logger.debug("Failed to parse shell command for cd check: {error}", error=exc)
876
+ if split_cmd and len(split_cmd) == 2 and split_cmd[0] == "cd":
877
+ console.print(
878
+ "[yellow]Warning: Directory changes are not preserved across command executions."
879
+ "[/yellow]"
880
+ )
881
+ return
882
+
883
+ logger.info("Running shell command: {cmd}", cmd=command)
884
+ from pythinker_code.telemetry import track
885
+
886
+ track("input_bash")
887
+
888
+ proc: asyncio.subprocess.Process | None = None
889
+ max_output_bytes = 1_000_000
890
+
891
+ async def _read_stream_limited(stream: asyncio.StreamReader | None, limit: int) -> bytes:
892
+ if stream is None:
893
+ return b""
894
+ chunks: list[bytes] = []
895
+ total = 0
896
+ truncated = False
897
+ while True:
898
+ chunk = await stream.read(65536)
899
+ if not chunk:
900
+ break
901
+ remaining = limit - total
902
+ if remaining > 0:
903
+ chunks.append(chunk[:remaining])
904
+ total += min(len(chunk), remaining)
905
+ if len(chunk) > remaining:
906
+ truncated = True
907
+ if truncated:
908
+ chunks.append(b"\n... output truncated ...\n")
909
+ return b"".join(chunks)
910
+
911
+ def _handler():
912
+ logger.debug("SIGINT received.")
913
+ if proc:
914
+ proc.terminate()
915
+
916
+ loop = asyncio.get_running_loop()
917
+ remove_sigint = install_sigint_handler(loop, _handler)
918
+ try:
919
+ # TODO: For the sake of simplicity, we now use `create_subprocess_shell`.
920
+ # Later we should consider making this behave like a real shell.
921
+ proc = await asyncio.create_subprocess_shell(
922
+ command,
923
+ env=get_clean_env(),
924
+ stdout=asyncio.subprocess.PIPE,
925
+ stderr=asyncio.subprocess.PIPE,
926
+ )
927
+ stdout_task = asyncio.create_task(_read_stream_limited(proc.stdout, max_output_bytes))
928
+ stderr_task = asyncio.create_task(_read_stream_limited(proc.stderr, max_output_bytes))
929
+ await proc.wait()
930
+ stdout_bytes, stderr_bytes = await asyncio.gather(stdout_task, stderr_task)
931
+ stdout = stdout_bytes.decode("utf-8", errors="replace") if stdout_bytes else ""
932
+ stderr = stderr_bytes.decode("utf-8", errors="replace") if stderr_bytes else ""
933
+ output = _format_local_shell_output(
934
+ stdout=stdout,
935
+ stderr=stderr,
936
+ returncode=proc.returncode,
937
+ )
938
+ if output is not None:
939
+ console.print(render_message_response(output))
940
+ except Exception as e:
941
+ logger.exception("Failed to run shell command:")
942
+ console.print(f"[red]Failed to run shell command: {e}[/red]")
943
+ finally:
944
+ remove_sigint()
945
+
946
+ async def _run_slash_command(self, command_call: SlashCommandCall) -> None:
947
+ from pythinker_code.cli import Reload, SwitchToVis, SwitchToWeb
948
+ from pythinker_code.telemetry import track
949
+
950
+ if command_call.name not in self._available_slash_commands:
951
+ logger.info("Unknown slash command /{command}", command=command_call.name)
952
+ track("input_command_invalid")
953
+ console.print(
954
+ f'[red]Unknown slash command "/{command_call.name}", '
955
+ 'type "/" for all available commands[/red]'
956
+ )
957
+ return
958
+
959
+ track("input_command", command=command_call.name)
960
+
961
+ command = shell_slash_registry.find_command(command_call.name)
962
+ if command is None:
963
+ # the input is a soul-level slash command call
964
+ await self.run_soul_command(command_call.raw_input)
965
+ return
966
+
967
+ logger.debug(
968
+ "Running shell-level slash command: /{command} with args: {args}",
969
+ command=command_call.name,
970
+ args=command_call.args,
971
+ )
972
+
973
+ try:
974
+ ret = command.func(self, command_call.args)
975
+ if isinstance(ret, Awaitable):
976
+ await ret
977
+ except (Reload, SwitchToWeb, SwitchToVis):
978
+ # just propagate
979
+ raise
980
+ except (asyncio.CancelledError, KeyboardInterrupt):
981
+ # Handle Ctrl-C during slash command execution, return to shell prompt
982
+ logger.debug("Slash command interrupted by KeyboardInterrupt")
983
+ console.print("[red]Interrupted by user[/red]")
984
+ except Exception as e:
985
+ logger.exception("Unknown error:")
986
+ console.print(f"[red]Unknown error: {e}[/red]")
987
+ raise # re-raise unknown error
988
+
989
+ async def run_soul_command(self, user_input: str | list[ContentPart]) -> bool:
990
+ """
991
+ Run the soul and handle any known exceptions.
992
+
993
+ Returns:
994
+ bool: Whether the run is successful.
995
+ """
996
+ logger.info("Running soul with user input: {user_input}", user_input=user_input)
997
+
998
+ cancel_event = asyncio.Event()
999
+
1000
+ def _handler():
1001
+ logger.debug("SIGINT received.")
1002
+ cancel_event.set()
1003
+
1004
+ loop = asyncio.get_running_loop()
1005
+ remove_sigint = install_sigint_handler(loop, _handler)
1006
+
1007
+ # Declare before try so finally can always access it.
1008
+ from pythinker_code.ui.shell.visualize import (
1009
+ _PromptLiveView, # pyright: ignore[reportPrivateUsage]
1010
+ )
1011
+
1012
+ captured_view: _PromptLiveView | None = None
1013
+ pending: list[UserInput] = [] # queued messages being drained
1014
+
1015
+ try:
1016
+ snap = self.soul.status
1017
+ runtime = self.soul.runtime if isinstance(self.soul, PythinkerSoul) else None
1018
+ show_thinking_stream = runtime.config.show_thinking_stream if runtime else False
1019
+ # Capture view reference via closure — _clear_active_view sets
1020
+ # _active_view=None inside visualize()'s finally (before run_soul
1021
+ # returns), so we must capture the view object independently.
1022
+
1023
+ def _on_view_ready(view: Any) -> None:
1024
+ nonlocal captured_view
1025
+ self._set_active_view(view)
1026
+ if isinstance(view, _PromptLiveView):
1027
+ captured_view = view
1028
+
1029
+ await run_soul(
1030
+ self.soul,
1031
+ user_input,
1032
+ lambda wire: visualize(
1033
+ wire.ui_side(merge=False), # shell UI maintain its own merge buffer
1034
+ initial_status=StatusUpdate(
1035
+ context_usage=snap.context_usage,
1036
+ context_tokens=snap.context_tokens,
1037
+ max_context_tokens=snap.max_context_tokens,
1038
+ mcp_status=snap.mcp_status,
1039
+ ),
1040
+ cancel_event=cancel_event,
1041
+ prompt_session=self._prompt_session,
1042
+ steer=self.soul.steer if isinstance(self.soul, PythinkerSoul) else None,
1043
+ btw_runner=self._make_btw_runner(),
1044
+ bind_running_input=self._bind_running_input,
1045
+ unbind_running_input=self._unbind_running_input,
1046
+ on_view_ready=_on_view_ready,
1047
+ on_view_closed=self._clear_active_view,
1048
+ show_thinking_stream=show_thinking_stream,
1049
+ ),
1050
+ cancel_event,
1051
+ runtime.session.wire_file if runtime else None,
1052
+ runtime,
1053
+ )
1054
+ # If btw is still showing, wait for user dismiss BEFORE draining
1055
+ # queue. This runs AFTER visualize_loop returns (within run_soul's
1056
+ # 0.5s ui_task timeout), so the btw modal is still attached to
1057
+ # prompt_session and key events continue to work.
1058
+ if captured_view is not None:
1059
+ await captured_view.wait_for_btw_dismiss()
1060
+
1061
+ # Clear cancel_event so queued turns aren't tainted by a
1062
+ # Ctrl+C that fired during btw dismiss wait.
1063
+ cancel_event.clear()
1064
+
1065
+ # Drain queued messages and send each as a new turn.
1066
+ # Safety valve: cap at 20 "generations" (new batches of messages
1067
+ # from the view). A one-time backlog of 25 messages = 1 generation,
1068
+ # but a user adding new messages every turn = 1 generation per turn.
1069
+ _MAX_DRAIN_GENERATIONS = 20
1070
+ pending.clear()
1071
+ drain_generation = 0
1072
+ while captured_view is not None and drain_generation < _MAX_DRAIN_GENERATIONS:
1073
+ new_messages = captured_view.drain_queued_messages()
1074
+ if new_messages:
1075
+ drain_generation += 1
1076
+ pending.extend(new_messages)
1077
+ if not pending:
1078
+ break
1079
+ queued = pending.pop(0)
1080
+ console.print(render_user_echo_text(queued.command))
1081
+ await run_soul(
1082
+ self.soul,
1083
+ queued.content,
1084
+ lambda wire: visualize(
1085
+ wire.ui_side(merge=False),
1086
+ initial_status=StatusUpdate(
1087
+ context_usage=self.soul.status.context_usage,
1088
+ context_tokens=self.soul.status.context_tokens,
1089
+ max_context_tokens=self.soul.status.max_context_tokens,
1090
+ mcp_status=self.soul.status.mcp_status,
1091
+ ),
1092
+ cancel_event=cancel_event,
1093
+ prompt_session=self._prompt_session,
1094
+ steer=self.soul.steer if isinstance(self.soul, PythinkerSoul) else None,
1095
+ btw_runner=self._make_btw_runner(),
1096
+ bind_running_input=self._bind_running_input,
1097
+ unbind_running_input=self._unbind_running_input,
1098
+ on_view_ready=_on_view_ready,
1099
+ on_view_closed=self._clear_active_view,
1100
+ show_thinking_stream=show_thinking_stream,
1101
+ ),
1102
+ cancel_event,
1103
+ runtime.session.wire_file if runtime else None,
1104
+ runtime,
1105
+ )
1106
+ # Wait for btw dismiss if one was triggered during this queued turn
1107
+ if captured_view is not None:
1108
+ await captured_view.wait_for_btw_dismiss()
1109
+ cancel_event.clear() # same rationale as above
1110
+ # captured_view is now the view from this turn;
1111
+ # next iteration drains it for any new messages.
1112
+ if drain_generation >= _MAX_DRAIN_GENERATIONS:
1113
+ logger.warning(
1114
+ "Queue drain hit safety limit ({n} generations)",
1115
+ n=_MAX_DRAIN_GENERATIONS,
1116
+ )
1117
+ # Warn about remaining items in the local pending buffer.
1118
+ # Clear after printing so finally doesn't duplicate.
1119
+ for msg in pending:
1120
+ console.print(f"[yellow]Queued message dropped: {msg.command}[/yellow]")
1121
+ pending.clear()
1122
+ return True
1123
+ except LLMNotSet:
1124
+ logger.exception("LLM not set:")
1125
+ console.print('[red]LLM not set, send "/login" to login[/red]')
1126
+ except LLMNotSupported as e:
1127
+ # actually unsupported input/mode should already be blocked by prompt session
1128
+ logger.exception("LLM not supported:")
1129
+ console.print(f"[red]{e}[/red]")
1130
+ except ChatProviderError as e:
1131
+ logger.exception("LLM provider error:")
1132
+ if isinstance(e, APIStatusError) and e.status_code == 401:
1133
+ console.print(
1134
+ "[red]Authorization failed. Your session may have expired.[/red]\n"
1135
+ "[dim]Type [bold]/login[/bold] to re-authenticate.[/dim]\n"
1136
+ f"[dim]Server: {e}[/dim]"
1137
+ )
1138
+ elif isinstance(e, APIStatusError) and e.status_code == 402:
1139
+ console.print(
1140
+ f"[red]Membership expired, please renew your plan[/red]\n[dim]Server: {e}[/dim]"
1141
+ )
1142
+ elif isinstance(e, APIStatusError) and e.status_code == 403:
1143
+ console.print(
1144
+ "[red]Quota exceeded, please upgrade your plan or retry later[/red]\n"
1145
+ f"[dim]Server: {e}[/dim]"
1146
+ )
1147
+ elif isinstance(e, APIStatusError) and e.status_code == 429:
1148
+ detail = _extract_429_detail(e)
1149
+ console.print(
1150
+ f"[red]Rate / usage limit hit: {detail['summary']}[/red]\n"
1151
+ f"[dim]{detail['hint']}[/dim]"
1152
+ )
1153
+ elif isinstance(e, APIConnectionError):
1154
+ console.print(
1155
+ f"[red]Network connection failed: {e}[/red]\n"
1156
+ "[dim]Please check your network and try again.[/dim]"
1157
+ )
1158
+ elif isinstance(e, APITimeoutError):
1159
+ console.print(
1160
+ f"[red]Request timed out: {e}[/red]\n"
1161
+ "[dim]The server may be slow or unreachable. Please try again later.[/dim]"
1162
+ )
1163
+ elif isinstance(e, APIEmptyResponseError):
1164
+ console.print(
1165
+ "[red]The server returned an empty response.[/red]\n"
1166
+ "[dim]This is usually a temporary issue. Please try again.[/dim]"
1167
+ )
1168
+ elif _is_lm_studio_context_too_small(e):
1169
+ n_keep, n_ctx = _parse_n_keep_n_ctx(str(e))
1170
+ console.print(
1171
+ f"[red]LM Studio's loaded context window is too small "
1172
+ f"(loaded n_ctx={n_ctx}, agent needs at least {n_keep}).[/red]\n"
1173
+ "[dim]To fix:[/dim]\n"
1174
+ "[dim] 1. In LM Studio, open the model in the Chat tab "
1175
+ "and click the gear/settings icon (or use 'My Models' → 'Edit').[/dim]\n"
1176
+ "[dim] 2. Set [bold]Context Length[/bold] to at least "
1177
+ f"[bold]{max(n_keep + 4096, 32768)}[/bold] tokens (or the model's max).[/dim]\n"
1178
+ "[dim] 3. Reload the model.[/dim]\n"
1179
+ "[dim] 4. Restart pythinker (Ctrl+D then `uv run pythinker` "
1180
+ "or `pythinker -r <session-id>` to resume).[/dim]"
1181
+ )
1182
+ elif _is_lm_studio_load_failed(e):
1183
+ failed_model = _parse_lm_studio_load_failed_model(str(e))
1184
+ model_label = failed_model or "the requested model"
1185
+ console.print(
1186
+ f"[red]LM Studio could not load {model_label}.[/red]\n"
1187
+ "[dim]Most common cause: VRAM exhausted (the model is too big "
1188
+ "for your GPU at its current quantization).[/dim]\n"
1189
+ "[dim]To fix:[/dim]\n"
1190
+ "[dim] 1. Switch to a smaller model: [bold]/model[/bold] and "
1191
+ "pick one with fewer parameters or a lower-bit quantization "
1192
+ "(e.g., Q4_K_M instead of Q8_0).[/dim]\n"
1193
+ "[dim] 2. Or in LM Studio: unload other models "
1194
+ "(My Models → eject), then try again.[/dim]\n"
1195
+ "[dim] 3. Check the LM Studio app for the underlying error "
1196
+ "(Developer → Logs).[/dim]\n"
1197
+ "[dim]Note: the model is registered as a Pythinker alias even "
1198
+ "if LM Studio can't currently load it — you don't need to "
1199
+ "re-run [bold]/login --lm-studio[/bold].[/dim]"
1200
+ )
1201
+ elif _is_lm_studio_jinja_template_error(e):
1202
+ console.print(
1203
+ "[red]LM Studio failed to render this model's prompt template.[/red]\n"
1204
+ "[dim]This is a model-side bug (broken or version-mismatched "
1205
+ "Jinja template baked into the GGUF), not a Pythinker issue.[/dim]\n"
1206
+ "[dim]To fix:[/dim]\n"
1207
+ "[dim] 1. Easiest: switch to a different model with "
1208
+ "[bold]/model[/bold] (most well-known models work out of the box).[/dim]\n"
1209
+ "[dim] 2. Re-download the model from the [bold]lmstudio-community[/bold] "
1210
+ "namespace in LM Studio's model browser — those have audited templates.[/dim]\n"
1211
+ "[dim] 3. Or override the template manually in LM Studio: "
1212
+ "[bold]My Models → model settings → Prompt Template[/bold].[/dim]\n"
1213
+ f"[dim]Server: {e}[/dim]"
1214
+ )
1215
+ else:
1216
+ console.print(f"[red]LLM provider error: {e}[/red]")
1217
+ if not isinstance(e, APIStatusError) or e.status_code not in (401, 402, 403):
1218
+ console.print(
1219
+ "[dim]If this persists, run [bold]pythinker export[/bold] and send the "
1220
+ "exported data to support for assistance. "
1221
+ "Please do not share the exported file publicly.[/dim]"
1222
+ )
1223
+ except MaxStepsReached as e:
1224
+ logger.warning("Max steps reached: {n_steps}", n_steps=e.n_steps)
1225
+ console.print(
1226
+ f"[yellow]{e}[/yellow]\n"
1227
+ "[dim]Send another message to continue where it left off.[/dim]"
1228
+ )
1229
+ except RunCancelled:
1230
+ logger.info("Cancelled by user")
1231
+ from pythinker_code.telemetry import track
1232
+
1233
+ _at_step = (
1234
+ getattr(self.soul, "_current_step_no", 0)
1235
+ if isinstance(self.soul, PythinkerSoul)
1236
+ else 0
1237
+ )
1238
+ track("turn_interrupted", at_step=_at_step)
1239
+ console.print("[red]Interrupted by user[/red]")
1240
+ except Exception as e:
1241
+ logger.exception("Unexpected error:")
1242
+ console.print(
1243
+ f"[red]Unexpected error: {e}[/red]\n"
1244
+ "[dim]Run [bold]pythinker export[/bold] and send the exported data to support "
1245
+ "for assistance. Please do not share the exported file publicly.[/dim]"
1246
+ )
1247
+ raise # re-raise unknown error
1248
+ finally:
1249
+ # Clean up btw modal if it's still attached (exception skipped wait_for_btw_dismiss)
1250
+ if captured_view is not None:
1251
+ captured_view._dismiss_btw() # pyright: ignore[reportPrivateUsage]
1252
+ # Warn about queued messages lost due to error/cancel.
1253
+ # Check both: pending (already drained from view) and view (not yet drained).
1254
+ all_lost: list[UserInput] = list(pending)
1255
+ pending.clear()
1256
+ if captured_view is not None:
1257
+ all_lost.extend(captured_view.drain_queued_messages())
1258
+ for msg in all_lost:
1259
+ console.print(f"[yellow]Queued message dropped: {msg.command}[/yellow]")
1260
+ self._maybe_present_pending_approvals()
1261
+ remove_sigint()
1262
+ return False
1263
+
1264
+ @staticmethod
1265
+ def _should_defer_background_auto_trigger(
1266
+ prompt_session: _BackgroundAutoTriggerPromptState | None,
1267
+ ) -> bool:
1268
+ if prompt_session is None:
1269
+ return False
1270
+ return prompt_session.has_pending_input() or prompt_session.had_recent_input_activity(
1271
+ within_s=_BG_AUTO_TRIGGER_INPUT_GRACE_S
1272
+ )
1273
+
1274
+ @staticmethod
1275
+ def _background_auto_trigger_timeout_s(
1276
+ prompt_session: _BackgroundAutoTriggerPromptState | None,
1277
+ ) -> float | None:
1278
+ if prompt_session is None or prompt_session.has_pending_input():
1279
+ return None
1280
+ remaining = prompt_session.recent_input_activity_remaining(
1281
+ within_s=_BG_AUTO_TRIGGER_INPUT_GRACE_S
1282
+ )
1283
+ return remaining if remaining > 0 else None
1284
+
1285
+ async def _wait_for_input_or_activity(
1286
+ self,
1287
+ prompt_session: _BackgroundAutoTriggerPromptState,
1288
+ idle_events: asyncio.Queue[_PromptEvent],
1289
+ *,
1290
+ timeout_s: float | None = None,
1291
+ ) -> _PromptEvent:
1292
+ idle_task = asyncio.create_task(idle_events.get())
1293
+ activity_task = asyncio.create_task(prompt_session.wait_for_input_activity())
1294
+ timeout_task = (
1295
+ asyncio.create_task(asyncio.sleep(timeout_s)) if timeout_s is not None else None
1296
+ )
1297
+ done: set[asyncio.Task[Any]] = set()
1298
+ try:
1299
+ done, _ = await asyncio.wait(
1300
+ [task for task in (idle_task, activity_task, timeout_task) if task is not None],
1301
+ return_when=asyncio.FIRST_COMPLETED,
1302
+ )
1303
+ finally:
1304
+ for task in (idle_task, activity_task, timeout_task):
1305
+ if task is None:
1306
+ continue
1307
+ if task.done():
1308
+ continue
1309
+ task.cancel()
1310
+ with contextlib.suppress(asyncio.CancelledError):
1311
+ await task
1312
+
1313
+ if idle_task in done:
1314
+ return idle_task.result()
1315
+ return _PromptEvent(kind="input_activity")
1316
+
1317
+ async def _watch_root_wire_hub(self) -> None:
1318
+ if not isinstance(self.soul, PythinkerSoul):
1319
+ return
1320
+ if self.soul.runtime.root_wire_hub is None:
1321
+ return
1322
+ queue = self.soul.runtime.root_wire_hub.subscribe()
1323
+ try:
1324
+ while True:
1325
+ try:
1326
+ msg = await queue.get()
1327
+ except QueueShutDown:
1328
+ return
1329
+ try:
1330
+ await self._handle_root_hub_message(msg)
1331
+ except Exception:
1332
+ logger.exception("Failed to handle root hub message:")
1333
+ finally:
1334
+ self.soul.runtime.root_wire_hub.unsubscribe(queue)
1335
+
1336
+ async def _handle_root_hub_message(self, msg: WireMessage) -> None:
1337
+ if not isinstance(self.soul, PythinkerSoul):
1338
+ return
1339
+ match msg:
1340
+ case ApprovalRequest() as request:
1341
+ request = self._enrich_approval_request_for_ui(request)
1342
+ if self.soul.runtime.approval_runtime is None:
1343
+ return
1344
+ record = self.soul.runtime.approval_runtime.get_request(request.id)
1345
+ if record is None or record.status != "pending":
1346
+ return
1347
+ if self._prompt_session is not None:
1348
+ # Interactive mode: queue and present via modal
1349
+ self._queue_approval_request(request)
1350
+ self._maybe_present_pending_approvals()
1351
+ self._prompt_session.invalidate()
1352
+ elif self._active_approval_sink is not None:
1353
+ # Non-interactive with live view: forward to sink
1354
+ self._forward_approval_to_sink(request)
1355
+ else:
1356
+ # Queue for later
1357
+ self._queue_approval_request(request)
1358
+ case ApprovalResponse() as response:
1359
+ # External resolution (e.g. from web UI)
1360
+ if (
1361
+ self._approval_modal is not None
1362
+ and self._approval_modal.request.id == response.request_id
1363
+ ):
1364
+ if not self._approval_modal.request.resolved:
1365
+ self._approval_modal.request.resolve(response.response)
1366
+ self._clear_current_prompt_approval_request(response.request_id)
1367
+ self._activate_prompt_approval_modal()
1368
+ self._remove_pending_approval_request(response.request_id)
1369
+ self._maybe_present_pending_approvals()
1370
+ if self._prompt_session is not None:
1371
+ self._prompt_session.invalidate()
1372
+ case _:
1373
+ return
1374
+
1375
+ def _enrich_approval_request_for_ui(self, request: ApprovalRequest) -> ApprovalRequest:
1376
+ if not isinstance(self.soul, PythinkerSoul):
1377
+ return request
1378
+ if request.agent_id is None:
1379
+ return request
1380
+ if self.soul.runtime.subagent_store is None:
1381
+ return request
1382
+ record = self.soul.runtime.subagent_store.get_instance(request.agent_id)
1383
+ if record is None:
1384
+ return request
1385
+ return request.model_copy(update={"source_description": record.description})
1386
+
1387
+ async def _run_btw_modal(
1388
+ self,
1389
+ question: str,
1390
+ prompt_session: CustomPromptSession,
1391
+ ) -> None:
1392
+ """Run /btw using the prompt session's modal system.
1393
+
1394
+ Attaches a ``_BtwModalDelegate`` that replaces the input line with
1395
+ the btw panel. A refresh loop animates the spinner. After the LLM
1396
+ responds, we start a new prompt read so prompt_toolkit can render the
1397
+ result and accept dismiss keys.
1398
+ """
1399
+ from pythinker_code.soul.btw import execute_side_question
1400
+ from pythinker_code.ui.shell.visualize import (
1401
+ _BtwModalDelegate, # pyright: ignore[reportPrivateUsage]
1402
+ )
1403
+
1404
+ assert isinstance(self.soul, PythinkerSoul)
1405
+
1406
+ dismiss_event = asyncio.Event()
1407
+ modal = _BtwModalDelegate(on_dismiss=lambda: dismiss_event.set())
1408
+ import time
1409
+
1410
+ modal._question = question # pyright: ignore[reportPrivateUsage]
1411
+ modal.set_start_time(time.monotonic())
1412
+ prompt_session.attach_modal(modal)
1413
+
1414
+ # Refresh loop for spinner animation
1415
+ async def _refresh() -> None:
1416
+ try:
1417
+ while True:
1418
+ await asyncio.sleep(0.08)
1419
+ prompt_session.invalidate()
1420
+ except asyncio.CancelledError:
1421
+ pass
1422
+
1423
+ refresh_task = asyncio.create_task(_refresh())
1424
+ prompt_task: asyncio.Task[None] | None = None
1425
+ llm_task: asyncio.Task[tuple[str | None, str | None]] | None = None
1426
+
1427
+ try:
1428
+
1429
+ def _on_chunk(chunk: str) -> None:
1430
+ modal.append_text(chunk)
1431
+
1432
+ # Start a prompt read concurrently — renders the modal and
1433
+ # handles key events while the LLM call runs in parallel.
1434
+ async def _wait_for_dismiss() -> None:
1435
+ while not dismiss_event.is_set():
1436
+ try:
1437
+ await prompt_session.prompt_next()
1438
+ except (KeyboardInterrupt, EOFError):
1439
+ dismiss_event.set()
1440
+ break
1441
+
1442
+ prompt_task = asyncio.create_task(_wait_for_dismiss())
1443
+
1444
+ # Run LLM call as a separate task so Escape can cancel it
1445
+ llm_task = asyncio.create_task(
1446
+ execute_side_question(self.soul, question, on_text_chunk=_on_chunk)
1447
+ )
1448
+
1449
+ # Wait for either LLM completion or user dismiss
1450
+ dismiss_task = asyncio.create_task(dismiss_event.wait())
1451
+ _done, _ = await asyncio.wait(
1452
+ [llm_task, dismiss_task],
1453
+ return_when=asyncio.FIRST_COMPLETED,
1454
+ )
1455
+
1456
+ if llm_task.done() and not llm_task.cancelled():
1457
+ # LLM finished — show result, wait for user to dismiss
1458
+ dismiss_task.cancel()
1459
+ response, error = llm_task.result()
1460
+ modal.set_result(response, error)
1461
+ prompt_session.invalidate()
1462
+ await dismiss_event.wait()
1463
+ else:
1464
+ # User dismissed during loading — cancel the LLM call
1465
+ llm_task.cancel()
1466
+ with contextlib.suppress(asyncio.CancelledError):
1467
+ await llm_task
1468
+ finally:
1469
+ # Cancel ALL child tasks
1470
+ if llm_task is not None and not llm_task.done():
1471
+ llm_task.cancel()
1472
+ with contextlib.suppress(asyncio.CancelledError):
1473
+ await llm_task
1474
+ if prompt_task is not None:
1475
+ prompt_task.cancel()
1476
+ with contextlib.suppress(asyncio.CancelledError):
1477
+ await prompt_task
1478
+ refresh_task.cancel()
1479
+ with contextlib.suppress(asyncio.CancelledError):
1480
+ await refresh_task
1481
+ prompt_session.detach_modal(modal)
1482
+
1483
+ def _make_btw_runner(self):
1484
+ """Create a btw_runner callback bound to the current soul."""
1485
+ if not isinstance(self.soul, PythinkerSoul):
1486
+ return None
1487
+
1488
+ soul = self.soul
1489
+
1490
+ async def _runner(
1491
+ question: str,
1492
+ on_text_chunk: Callable[[str], None] | None = None,
1493
+ ) -> tuple[str | None, str | None]:
1494
+ from pythinker_code.soul.btw import execute_side_question
1495
+
1496
+ return await execute_side_question(soul, question, on_text_chunk)
1497
+
1498
+ return _runner
1499
+
1500
+ def _set_active_view(self, view: Any) -> None:
1501
+ self._active_approval_sink = view
1502
+ self._active_view = view
1503
+ # In interactive mode, approvals are handled by the prompt modal,
1504
+ # not by the live view sink. Don't flush to avoid losing requests.
1505
+ if self._prompt_session is not None:
1506
+ return
1507
+ # Flush pending approvals to the newly active sink
1508
+ while self._pending_approval_requests:
1509
+ request = self._pending_approval_requests.popleft()
1510
+
1511
+ if (
1512
+ not isinstance(self.soul, PythinkerSoul)
1513
+ or self.soul.runtime.approval_runtime is None
1514
+ ):
1515
+ break
1516
+ record = self.soul.runtime.approval_runtime.get_request(request.id)
1517
+ if record is None or record.status != "pending":
1518
+ continue
1519
+ self._forward_approval_to_sink(request)
1520
+
1521
+ def _clear_active_view(self) -> None:
1522
+ self._active_approval_sink = None
1523
+ self._active_view = None
1524
+ # Re-queue any approval requests that were forwarded to the sink
1525
+ # but not yet resolved. Without this, those requests would be
1526
+ # silently lost when the live view closes between turns.
1527
+ if not isinstance(self.soul, PythinkerSoul) or self.soul.runtime.approval_runtime is None:
1528
+ return
1529
+ for record in self.soul.runtime.approval_runtime.list_pending():
1530
+ self._queue_approval_request(
1531
+ self._enrich_approval_request_for_ui(
1532
+ ApprovalRequest(
1533
+ id=record.id,
1534
+ tool_call_id=record.tool_call_id,
1535
+ sender=record.sender,
1536
+ action=record.action,
1537
+ description=record.description,
1538
+ display=record.display,
1539
+ source_kind=record.source.kind,
1540
+ source_id=record.source.id,
1541
+ agent_id=record.source.agent_id,
1542
+ subagent_type=record.source.subagent_type,
1543
+ )
1544
+ )
1545
+ )
1546
+
1547
+ def _forward_approval_to_sink(self, request: ApprovalRequest) -> None:
1548
+ """Forward an approval request to the active live view sink and bridge the response."""
1549
+ if self._active_approval_sink is None:
1550
+ self._queue_approval_request(request)
1551
+ return
1552
+ self._active_approval_sink.enqueue_external_message(request)
1553
+
1554
+ async def _bridge() -> None:
1555
+ try:
1556
+ response = await request.wait()
1557
+ if (
1558
+ isinstance(self.soul, PythinkerSoul)
1559
+ and self.soul.runtime.approval_runtime is not None
1560
+ ):
1561
+ self.soul.runtime.approval_runtime.resolve(
1562
+ request.id, response, feedback=request.feedback
1563
+ )
1564
+ finally:
1565
+ if self._prompt_session is not None:
1566
+ self._prompt_session.invalidate()
1567
+
1568
+ self._start_background_task(_bridge())
1569
+
1570
+ def _queue_approval_request(self, request: ApprovalRequest) -> None:
1571
+ if self._approval_modal is not None and self._approval_modal.request.id == request.id:
1572
+ return
1573
+ if (
1574
+ self._current_prompt_approval_request is not None
1575
+ and self._current_prompt_approval_request.id == request.id
1576
+ ):
1577
+ return
1578
+ if any(r.id == request.id for r in self._pending_approval_requests):
1579
+ return
1580
+ self._pending_approval_requests.append(request)
1581
+
1582
+ def _remove_pending_approval_request(self, request_id: str) -> None:
1583
+ self._clear_current_prompt_approval_request(request_id)
1584
+ self._pending_approval_requests = deque(
1585
+ r for r in self._pending_approval_requests if r.id != request_id
1586
+ )
1587
+
1588
+ def _clear_current_prompt_approval_request(self, request_id: str) -> None:
1589
+ if (
1590
+ self._current_prompt_approval_request is not None
1591
+ and self._current_prompt_approval_request.id == request_id
1592
+ ):
1593
+ self._current_prompt_approval_request = None
1594
+
1595
+ def _maybe_present_pending_approvals(self) -> None:
1596
+ if self._prompt_session is not None:
1597
+ self._activate_prompt_approval_modal()
1598
+ return
1599
+ if self._active_approval_sink is not None:
1600
+ while self._pending_approval_requests:
1601
+ request = self._pending_approval_requests.popleft()
1602
+
1603
+ if not isinstance(self.soul, PythinkerSoul):
1604
+ break
1605
+ if self.soul.runtime.approval_runtime is None:
1606
+ break
1607
+ record = self.soul.runtime.approval_runtime.get_request(request.id)
1608
+ if record is None or record.status != "pending":
1609
+ continue
1610
+ self._forward_approval_to_sink(request)
1611
+
1612
+ def _get_default_buffer_text_and_cursor(self) -> tuple[str, int]:
1613
+ if self._prompt_session is None:
1614
+ return "", 0
1615
+ buf = self._prompt_session._session.default_buffer # pyright: ignore[reportPrivateUsage]
1616
+ return buf.text, buf.cursor_position
1617
+
1618
+ def _activate_prompt_approval_modal(self) -> None:
1619
+ if self._prompt_session is None:
1620
+ return
1621
+ current_request = self._current_prompt_approval_request
1622
+ if current_request is None:
1623
+ current_request = self._pop_next_pending_approval_request()
1624
+ self._current_prompt_approval_request = current_request
1625
+ if current_request is None:
1626
+ if self._approval_modal is not None:
1627
+ self._prompt_session.detach_modal(self._approval_modal)
1628
+ self._approval_modal = None
1629
+ return
1630
+ if self._approval_modal is None:
1631
+ self._approval_modal = ApprovalPromptDelegate(
1632
+ current_request,
1633
+ on_response=self._handle_prompt_approval_response,
1634
+ buffer_state_provider=self._get_default_buffer_text_and_cursor,
1635
+ text_expander=self._prompt_session._get_placeholder_manager().serialize_for_history, # pyright: ignore[reportPrivateUsage]
1636
+ )
1637
+ self._prompt_session.attach_modal(self._approval_modal)
1638
+ else:
1639
+ if self._approval_modal.request.id != current_request.id:
1640
+ self._approval_modal.set_request(current_request)
1641
+ self._prompt_session.invalidate()
1642
+
1643
+ def _handle_prompt_approval_response(
1644
+ self,
1645
+ request: ApprovalRequest,
1646
+ response: ApprovalResponse.Kind,
1647
+ feedback: str = "",
1648
+ ) -> None:
1649
+ if not isinstance(self.soul, PythinkerSoul):
1650
+ return
1651
+ if self.soul.runtime.approval_runtime is None:
1652
+ return
1653
+ self.soul.runtime.approval_runtime.resolve(request.id, response, feedback=feedback)
1654
+ self._clear_current_prompt_approval_request(request.id)
1655
+ self._activate_prompt_approval_modal()
1656
+
1657
+ def _pop_next_pending_approval_request(self) -> ApprovalRequest | None:
1658
+ if not isinstance(self.soul, PythinkerSoul) or self.soul.runtime.approval_runtime is None:
1659
+ return None
1660
+ while self._pending_approval_requests:
1661
+ request = self._pending_approval_requests.popleft()
1662
+
1663
+ record = self.soul.runtime.approval_runtime.get_request(request.id)
1664
+ if record is None or record.status != "pending":
1665
+ continue
1666
+ return request
1667
+ return None
1668
+
1669
+ async def _auto_update(self) -> None:
1670
+ result = await do_update(print=False, check_only=True)
1671
+ if result == UpdateResult.UPDATED:
1672
+ toast("auto updated, restart to use the new version", topic="update", duration=5.0)
1673
+
1674
+ def _start_background_task(self, coro: Coroutine[Any, Any, Any]) -> asyncio.Task[Any]:
1675
+ task = asyncio.create_task(coro)
1676
+ self._background_tasks.add(task)
1677
+
1678
+ def _cleanup(t: asyncio.Task[Any]) -> None:
1679
+ self._background_tasks.discard(t)
1680
+ try:
1681
+ t.result()
1682
+ except asyncio.CancelledError:
1683
+ pass
1684
+ except Exception:
1685
+ logger.exception("Background task failed:")
1686
+
1687
+ task.add_done_callback(_cleanup)
1688
+ return task
1689
+
1690
+ def _cancel_background_tasks(self) -> None:
1691
+ """Cancel all background tasks (notification watcher, auto-update, etc.)."""
1692
+ for task in self._background_tasks:
1693
+ task.cancel()
1694
+ self._background_tasks.clear()
1695
+
1696
+
1697
+ # Palette transferred from the animated SVG (pythinker_animated.svg).
1698
+ _LOGO_NAVY = "#213853" # outline / chassis (head + body frame, mouth, neck)
1699
+ _LOGO_FACE = "#F9F2F5" # face / chest interior (cream)
1700
+ _LOGO_CORAL = "#EE9983" # antenna ball, ears, accent bits
1701
+ _LOGO_IRIS = "#AFE3F1" # eye iris + chest button glow (light cyan)
1702
+ _PYTHINKER_BORDER = "grey39"
1703
+
1704
+ _LOGO = (
1705
+ f" [{_LOGO_CORAL}]●[/]\n"
1706
+ f" [{_LOGO_NAVY}]│[/]\n"
1707
+ f" [{_LOGO_NAVY}]▛[/][{_LOGO_FACE}]▀▀▀▀▀▀▀[/][{_LOGO_NAVY}]▜[/]\n"
1708
+ f" [{_LOGO_CORAL}]◖[/][{_LOGO_NAVY}]█[/][{_LOGO_FACE}] [/]"
1709
+ f"[{_LOGO_IRIS}]◉[/][{_LOGO_FACE}] [/][{_LOGO_IRIS}]◉[/]"
1710
+ f"[{_LOGO_FACE}] [/][{_LOGO_NAVY}]█[/][{_LOGO_CORAL}]◗[/]\n"
1711
+ f" [{_LOGO_NAVY}]▙▄▄▄[/][{_LOGO_FACE}]≡[/][{_LOGO_NAVY}]▄▄▄▟[/]"
1712
+ )
1713
+
1714
+
1715
+ @dataclass(slots=True)
1716
+ class WelcomeInfoItem:
1717
+ class Level(Enum):
1718
+ INFO = "grey50"
1719
+ WARN = "yellow"
1720
+ ERROR = "red"
1721
+
1722
+ name: str
1723
+ value: str
1724
+ level: Level = Level.INFO
1725
+
1726
+
1727
+ def _value_style_for_label(label: str, level: WelcomeInfoItem.Level) -> str:
1728
+ """INFO-level styling per label; WARN/ERROR colors always win."""
1729
+ if level is not WelcomeInfoItem.Level.INFO:
1730
+ return level.value
1731
+ label = label.strip()
1732
+ if label == "Directory":
1733
+ return "cyan"
1734
+ if label == "Session":
1735
+ return "grey39"
1736
+ if label == "Model":
1737
+ return "bold bright_white"
1738
+ return level.value
1739
+
1740
+
1741
+ def _print_welcome_info(name: str, info_items: list[WelcomeInfoItem]) -> None:
1742
+ head = Text.from_markup("Welcome to Pythinker — think first, then code.")
1743
+ help_text = Text.from_markup(
1744
+ "[grey50]Review · Secure · Diagnose · then Create. Send /help for help.[/grey50]"
1745
+ )
1746
+ help_text.highlight_regex(r"/help\b", "yellow bold")
1747
+
1748
+ # Use Table for precise width control
1749
+ logo = Text.from_markup(_LOGO)
1750
+ table = Table(show_header=False, show_edge=False, box=None, padding=(0, 1), expand=False)
1751
+ table.add_column(justify="left")
1752
+ table.add_column(justify="left", vertical="bottom")
1753
+ table.add_row(logo, Group(head, help_text))
1754
+
1755
+ rows: list[RenderableType] = [table]
1756
+
1757
+ facts = [item for item in info_items if item.name.strip() != "Tip"]
1758
+ tips = [item for item in info_items if item.name.strip() == "Tip"]
1759
+
1760
+ if facts:
1761
+ rows.append(Text("")) # empty line
1762
+ info_table = Table(
1763
+ show_header=False, show_edge=False, box=None, padding=(0, 1), expand=False
1764
+ )
1765
+ info_table.add_column(justify="right", style="grey50")
1766
+ info_table.add_column(justify="center", style="grey39", no_wrap=True)
1767
+ info_table.add_column(justify="left")
1768
+ for item in facts:
1769
+ value_style = _value_style_for_label(item.name, item.level)
1770
+ info_table.add_row(item.name, "│", Text(item.value, style=value_style))
1771
+ rows.append(info_table)
1772
+
1773
+ if tips:
1774
+ rows.append(Text("")) # empty line
1775
+ rows.append(Text("Tips", style="grey50"))
1776
+ # 2-col table → wrapped tip lines hang-indent under the text column,
1777
+ # not under the bullet.
1778
+ tips_table = Table(
1779
+ show_header=False, show_edge=False, box=None, padding=(0, 0), expand=False
1780
+ )
1781
+ tips_table.add_column(style="grey50", no_wrap=True, width=4)
1782
+ tips_table.add_column(justify="left", overflow="fold")
1783
+ for item in tips:
1784
+ tip_text = Text(item.value, style=item.level.value)
1785
+ tip_text.highlight_regex(r"/[A-Za-z][A-Za-z0-9_-]*", "yellow bold")
1786
+ tips_table.add_row(" › ", tip_text)
1787
+ rows.append(tips_table)
1788
+
1789
+ # Update notice is rendered as a standalone banner above the welcome panel
1790
+ # by `print_update_banner()` (called from app.py before this point).
1791
+
1792
+ version_title = Text.assemble(
1793
+ ("Pythinker Code", _PYTHINKER_BORDER),
1794
+ (f" v{get_version()}", "grey50"),
1795
+ )
1796
+
1797
+ console.print(
1798
+ Panel(
1799
+ Group(*rows),
1800
+ title=version_title,
1801
+ title_align="left",
1802
+ border_style=_PYTHINKER_BORDER,
1803
+ expand=False,
1804
+ padding=(1, 2),
1805
+ )
1806
+ )