pythinker-code 2.0.0__py3-none-any.whl

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