pythinker-code 0.44.0__py3-none-any.whl → 0.45.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 (143) hide show
  1. pythinker_code/CHANGELOG.md +89 -0
  2. pythinker_code/agents/default/system.md +1 -12
  3. pythinker_code/auth/__init__.py +2 -0
  4. pythinker_code/auth/kimi.py +278 -0
  5. pythinker_code/auth/moonshot.py +2 -1
  6. pythinker_code/auth/platforms.py +17 -0
  7. pythinker_code/auth/z_ai.py +35 -1
  8. pythinker_code/cli/__init__.py +13 -13
  9. pythinker_code/cli/_lazy_group.py +12 -2
  10. pythinker_code/cli/{vis.py → dashboard.py} +4 -4
  11. pythinker_code/cli/system_prompt.py +76 -0
  12. pythinker_code/config.py +48 -15
  13. pythinker_code/dashboard/api/__init__.py +5 -0
  14. pythinker_code/{vis → dashboard}/api/sessions.py +4 -3
  15. pythinker_code/{vis → dashboard}/api/statistics.py +11 -3
  16. pythinker_code/{vis → dashboard}/api/system.py +2 -2
  17. pythinker_code/{vis → dashboard}/app.py +28 -14
  18. pythinker_code/{vis/static/assets/highlighted-body-OFNGDK62-Bvu9aDxC.js → dashboard/static/assets/highlighted-body-OFNGDK62-BxVUIXn9.js} +1 -1
  19. pythinker_code/{vis/static/assets/index-Cot2VCM1.css → dashboard/static/assets/index-DAN_VzOP.css} +1 -1
  20. pythinker_code/{vis/static/assets/index-Bf7NpcRg.js → dashboard/static/assets/index-DYyxzHPY.js} +6 -6
  21. pythinker_code/dashboard/static/assets/mermaid-GHXKKRXX-BR6KjYwB.js +1 -0
  22. pythinker_code/{vis → dashboard}/static/index.html +2 -2
  23. pythinker_code/memory/recall.py +5 -2
  24. pythinker_code/soul/agent.py +117 -2
  25. pythinker_code/soul/approval.py +89 -7
  26. pythinker_code/soul/flow_runner.py +3 -0
  27. pythinker_code/soul/pythinkersoul.py +177 -6
  28. pythinker_code/soul/slash.py +25 -0
  29. pythinker_code/soul/toolset.py +37 -13
  30. pythinker_code/subagents/discovery.py +16 -0
  31. pythinker_code/subagents/models.py +4 -0
  32. pythinker_code/tools/agent/__init__.py +52 -0
  33. pythinker_code/tools/file/__init__.py +14 -5
  34. pythinker_code/tools/file/read.py +8 -0
  35. pythinker_code/tools/file/replace.py +35 -0
  36. pythinker_code/tools/file/write.py +52 -3
  37. pythinker_code/tools/shell/__init__.py +5 -1
  38. pythinker_code/tools/shell/bash.md +2 -2
  39. pythinker_code/tools/shell/powershell.md +2 -2
  40. pythinker_code/ui/shell/__init__.py +8 -6
  41. pythinker_code/ui/shell/components/diff.py +6 -6
  42. pythinker_code/ui/shell/oauth.py +20 -2
  43. pythinker_code/ui/shell/slash.py +4 -4
  44. pythinker_code/ui/shell/stats_pricing.py +5 -1
  45. pythinker_code/ui/shell/tool_renderers/agent.py +234 -10
  46. pythinker_code/ui/shell/usage_adapters/minimax.py +93 -50
  47. pythinker_code/ui/shell/visualize/_blocks.py +0 -13
  48. pythinker_code/ui/shell/visualize/_interactive.py +1 -1
  49. pythinker_code/ui/shell/visualize/_live_view.py +8 -23
  50. pythinker_code/ui/theme.py +9 -9
  51. pythinker_code/utils/file_read_cache.py +65 -0
  52. pythinker_code/utils/path.py +48 -0
  53. pythinker_code/utils/pyinstaller.py +6 -6
  54. pythinker_code/utils/server.py +2 -2
  55. pythinker_code/utils/subprocess_env.py +1 -1
  56. pythinker_code/web/static/assets/architecture-7EHR7CIX-CxeoRZcL.js +1 -0
  57. pythinker_code/web/static/assets/{architectureDiagram-3BPJPVTR-BnkpaR5O.js → architectureDiagram-3BPJPVTR-Cmrv4t4A.js} +1 -1
  58. pythinker_code/web/static/assets/{blockDiagram-GPEHLZMM-e_MhfITx.js → blockDiagram-GPEHLZMM-DJtt-l4b.js} +1 -1
  59. pythinker_code/web/static/assets/{bootstrap-CwMnV7Zd.js → bootstrap-GQDZdCZn.js} +5 -5
  60. pythinker_code/web/static/assets/{c4Diagram-AAUBKEIU-BHeP9Afi.js → c4Diagram-AAUBKEIU-o0KsSeIT.js} +1 -1
  61. pythinker_code/web/static/assets/channel-CzOfc6qg.js +1 -0
  62. pythinker_code/web/static/assets/{chunk-2J33WTMH-sjzfsYRI.js → chunk-2J33WTMH-CVPDc3uI.js} +1 -1
  63. pythinker_code/web/static/assets/{chunk-3OPIFGDE-Diy54J-m.js → chunk-3OPIFGDE-Tu9TJ8FO.js} +1 -1
  64. pythinker_code/web/static/assets/{chunk-5ZQYHXKU-Cha6K3fv.js → chunk-5ZQYHXKU-CoBmhCur.js} +1 -1
  65. pythinker_code/web/static/assets/{chunk-727SXJPM-8maRYfGZ.js → chunk-727SXJPM-DByzCodm.js} +1 -1
  66. pythinker_code/web/static/assets/{chunk-AQP2D5EJ-BugAJq9M.js → chunk-AQP2D5EJ-DclqBNWp.js} +1 -1
  67. pythinker_code/web/static/assets/{chunk-CSCIHK7Q-fNfOkomy.js → chunk-CSCIHK7Q-CUIX1-rb.js} +1 -1
  68. pythinker_code/web/static/assets/{chunk-JAPRZBRM-BqD8tL42.js → chunk-JAPRZBRM-gOR8tSRc.js} +4 -4
  69. pythinker_code/web/static/assets/{chunk-KSCS5N6A-DjE4cbB9.js → chunk-KSCS5N6A-hta5hym2.js} +1 -1
  70. pythinker_code/web/static/assets/{chunk-L5ZTLDWV-BBuq_T7N.js → chunk-L5ZTLDWV-cj__FNrg.js} +1 -1
  71. pythinker_code/web/static/assets/{chunk-LZXEDZCA-C9Ib4xBs.js → chunk-LZXEDZCA-CSHDE7c1.js} +2 -2
  72. pythinker_code/web/static/assets/{chunk-ND2GUHAM-Bm81WxoP.js → chunk-ND2GUHAM-DMFQO6Cb.js} +1 -1
  73. pythinker_code/web/static/assets/{chunk-NZK2D7GU-BSRiYhUY.js → chunk-NZK2D7GU-Cjm014we.js} +1 -1
  74. pythinker_code/web/static/assets/{chunk-O5CBEL6O-ikkVvriS.js → chunk-O5CBEL6O-BSr1l1Fd.js} +1 -1
  75. pythinker_code/web/static/assets/{chunk-WU5MYG2G-B0dxIX1Z.js → chunk-WU5MYG2G-veNzkohb.js} +1 -1
  76. pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-Cl__vhhZ.js +1 -0
  77. pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-Cl__vhhZ.js +1 -0
  78. pythinker_code/web/static/assets/{code-block-IT6T5CEO-ykYQSspy.js → code-block-IT6T5CEO-DTgIHAkQ.js} +1 -1
  79. pythinker_code/web/static/assets/{dagre-BM42HDAG-BVgOGrpH.js → dagre-BM42HDAG-D-_HCbAC.js} +1 -1
  80. pythinker_code/web/static/assets/{diagram-2AECGRRQ-B_OMSNvN.js → diagram-2AECGRRQ-CvX58Rpg.js} +1 -1
  81. pythinker_code/web/static/assets/{diagram-5GNKFQAL-DAi1-R2d.js → diagram-5GNKFQAL-CsRaiNkg.js} +1 -1
  82. pythinker_code/web/static/assets/{diagram-KO2AKTUF-CVPaBXbF.js → diagram-KO2AKTUF-Dn6aiXG5.js} +1 -1
  83. pythinker_code/web/static/assets/{diagram-LMA3HP47-DfPWmpwz.js → diagram-LMA3HP47-DXQlab3r.js} +1 -1
  84. pythinker_code/web/static/assets/{diagram-OG6HWLK6-CpV9uXPx.js → diagram-OG6HWLK6-BWB_FmTS.js} +1 -1
  85. pythinker_code/web/static/assets/{dist-h8mi_-6j.js → dist-C8Q1EY4S.js} +1 -1
  86. pythinker_code/web/static/assets/{erDiagram-TEJ5UH35-DDhG-euv.js → erDiagram-TEJ5UH35-BrQiudEc.js} +1 -1
  87. pythinker_code/web/static/assets/eventmodeling-FCH6USID-1z7xo-Al.js +1 -0
  88. pythinker_code/web/static/assets/{flowDiagram-I6XJVG4X-CaS1OzXM.js → flowDiagram-I6XJVG4X-C6jARfsw.js} +1 -1
  89. pythinker_code/web/static/assets/{ganttDiagram-6RSMTGT7-Bd7at8fB.js → ganttDiagram-6RSMTGT7-DH-i7Wci.js} +1 -1
  90. pythinker_code/web/static/assets/{gitGraph-WXDBUCRP-BXZoPljO.js → gitGraph-WXDBUCRP-lU8PeAwh.js} +1 -1
  91. pythinker_code/web/static/assets/{gitGraphDiagram-PVQCEYII-C6WKYJ1c.js → gitGraphDiagram-PVQCEYII-JOoIM40T.js} +1 -1
  92. pythinker_code/web/static/assets/{index-BVLby4kT.js → index-BJIE96ev.js} +2 -2
  93. pythinker_code/web/static/assets/{info-J43DQDTF-BSjXjPWD.js → info-J43DQDTF-B9a8E9HF.js} +1 -1
  94. pythinker_code/web/static/assets/{infoDiagram-5YYISTIA-BmRBSqys.js → infoDiagram-5YYISTIA-D5wmE10q.js} +1 -1
  95. pythinker_code/web/static/assets/{ishikawaDiagram-YF4QCWOH-fgPipcob.js → ishikawaDiagram-YF4QCWOH-DzNdTJqt.js} +1 -1
  96. pythinker_code/web/static/assets/{journeyDiagram-JHISSGLW-DL8Jtwnh.js → journeyDiagram-JHISSGLW-CwevEltw.js} +1 -1
  97. pythinker_code/web/static/assets/{kanban-definition-UN3LZRKU-D1Jts2BV.js → kanban-definition-UN3LZRKU-BAmLyJgC.js} +1 -1
  98. pythinker_code/web/static/assets/{line-BSuVwcdH.js → line-CktbshjS.js} +1 -1
  99. pythinker_code/web/static/assets/mermaid-VLURNSYL-B9qVFSXg.js +1 -0
  100. pythinker_code/web/static/assets/{mermaid-parser.core-D1JrN2zq.js → mermaid-parser.core-B1Hc3VHx.js} +2 -2
  101. pythinker_code/web/static/assets/{mermaid.core-C3w1FMla.js → mermaid.core-zqRnwN3B.js} +3 -3
  102. pythinker_code/web/static/assets/{mindmap-definition-RKZ34NQL-BnMo2qJe.js → mindmap-definition-RKZ34NQL-CamoLUlD.js} +1 -1
  103. pythinker_code/web/static/assets/{packet-YPE3B663-CIXAH8ey.js → packet-YPE3B663-s78KeeiS.js} +1 -1
  104. pythinker_code/web/static/assets/{pie-LRSECV5Y-BvQ_6Rhy.js → pie-LRSECV5Y-DSusaq_V.js} +1 -1
  105. pythinker_code/web/static/assets/{pieDiagram-4H26LBE5-lDidBc5t.js → pieDiagram-4H26LBE5-BcJSWsmz.js} +1 -1
  106. pythinker_code/web/static/assets/{quadrantDiagram-W4KKPZXB-CqffYofN.js → quadrantDiagram-W4KKPZXB-Cxp3MYGY.js} +1 -1
  107. pythinker_code/web/static/assets/{radar-GUYGQ44K-BMqTdNUV.js → radar-GUYGQ44K-sZyAfli_.js} +1 -1
  108. pythinker_code/web/static/assets/{requirementDiagram-4Y6WPE33---Iqpcn8.js → requirementDiagram-4Y6WPE33-DSaeIAHw.js} +1 -1
  109. pythinker_code/web/static/assets/{sankeyDiagram-5OEKKPKP-jx06oDq6.js → sankeyDiagram-5OEKKPKP-BI87xNZ8.js} +1 -1
  110. pythinker_code/web/static/assets/{sequenceDiagram-3UESZ5HK-HaEvgD49.js → sequenceDiagram-3UESZ5HK-B3mOEaPg.js} +1 -1
  111. pythinker_code/web/static/assets/{stateDiagram-AJRCARHV-JLA-477t.js → stateDiagram-AJRCARHV-CloKSCZq.js} +1 -1
  112. pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-bwtGCtLW.js +1 -0
  113. pythinker_code/web/static/assets/{timeline-definition-PNZ67QCA-CCTBgJfg.js → timeline-definition-PNZ67QCA-dy1xxojp.js} +1 -1
  114. pythinker_code/web/static/assets/{treeView-BLDUP644-BygVvhlI.js → treeView-BLDUP644-CouMM8Ld.js} +1 -1
  115. pythinker_code/web/static/assets/{treemap-LRROVOQU-BuV3B2a0.js → treemap-LRROVOQU-BurOGxmB.js} +1 -1
  116. pythinker_code/web/static/assets/{vennDiagram-CIIHVFJN-Cl1lT_Ny.js → vennDiagram-CIIHVFJN-ceoQfI8-.js} +1 -1
  117. pythinker_code/web/static/assets/{wardley-L42UT6IY-K8n6TVEI.js → wardley-L42UT6IY-DQIeshOY.js} +1 -1
  118. pythinker_code/web/static/assets/{wardleyDiagram-YWT4CUSO-D68VMB5e.js → wardleyDiagram-YWT4CUSO-yJgnjbxz.js} +1 -1
  119. pythinker_code/web/static/assets/{xychartDiagram-2RQKCTM6-tTtP-k4s.js → xychartDiagram-2RQKCTM6-jMkhXWCc.js} +1 -1
  120. pythinker_code/web/static/index.html +1 -1
  121. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/METADATA +27 -26
  122. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/RECORD +134 -131
  123. pythinker_code/vis/api/__init__.py +0 -5
  124. pythinker_code/vis/static/assets/mermaid-GHXKKRXX-C-XxnFiA.js +0 -1
  125. pythinker_code/web/static/assets/architecture-7EHR7CIX-B9IRTQo7.js +0 -1
  126. pythinker_code/web/static/assets/channel-D6eTbsR5.js +0 -1
  127. pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-D6Cn2t8q.js +0 -1
  128. pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-D6Cn2t8q.js +0 -1
  129. pythinker_code/web/static/assets/eventmodeling-FCH6USID-ChNU-x8b.js +0 -1
  130. pythinker_code/web/static/assets/mermaid-VLURNSYL-B2gPIPJx.js +0 -1
  131. pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-C_zjek2r.js +0 -1
  132. /pythinker_code/{vis → dashboard}/__init__.py +0 -0
  133. /pythinker_code/{vis → dashboard}/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  134. /pythinker_code/{vis → dashboard}/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  135. /pythinker_code/{vis → dashboard}/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  136. /pythinker_code/{vis → dashboard}/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  137. /pythinker_code/{vis → dashboard}/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  138. /pythinker_code/{vis → dashboard}/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  139. /pythinker_code/{vis → dashboard}/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  140. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/WHEEL +0 -0
  141. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/entry_points.txt +0 -0
  142. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/licenses/LICENSE +0 -0
  143. {pythinker_code-0.44.0.dist-info → pythinker_code-0.45.0.dist-info}/licenses/NOTICE +0 -0
@@ -15,6 +15,95 @@ GitHub Releases page; `0.8.0` is the new starting line.
15
15
 
16
16
  ## Unreleased
17
17
 
18
+ ## 0.45.0 (2026-06-14)
19
+
20
+ - **Agent-tracing dashboard.** Added `pythinker dashboard` — a local web UI for inspecting sessions, wire events, context messages, tool statistics, and usage over time. It is also reachable from the interactive shell via the `/reports` slash command (aliased `/dashboard`).
21
+ - **Gemini finish_reason is now sticky once truncation is detected.** A second candidate with any non-`MAX_TOKENS` finish reason no longer overwrites a previously captured `"length"` signal at either the streaming or non-streaming path in the Google GenAI provider.
22
+ - **Budget-exhausted and stuck-loop messages are now visible in the shell.** Both handoff messages were appended to context but never sent to the wire, so the interactive shell showed no feedback when a spend ceiling or stuck-loop exit fired. Both now emit a `TextPart` wire event so the message appears in the shell.
23
+ - **Dark-theme prompt_toolkit border colors now match the TUI token constants.** Six stale slate hex values in `_PROMPT_STYLE_DARK` diverged from the current `border` and `border_muted` token values; they are now in sync and a parity test binds them going forward.
24
+ - **Non-review successful agents now show a summary preview in the agent tree.** Completed subagents with a `summary_preview` now display a dim preview line in the RunAgents tree renderer (review runs continue to use the findings table instead).
25
+ - **Non-string values in `required_mcp_servers` YAML are silently dropped.** Integers, booleans, and `null` entries were previously coerced to strings (`"1"`, `"False"`, `"None"`), creating permanently unsatisfiable MCP server names. Only actual string entries are now retained.
26
+ - **Fixed MiniMax token-plan `/usage` accuracy.** The token-plan response now meters by
27
+ percentage (the count fields are 0) and reports reset times in milliseconds; the adapter was
28
+ reading the zero counts (showing "0 requests used") and treating milliseconds as seconds
29
+ (showing resets like "171d" for a 5-hour window). It now reads `*_remaining_percent` and
30
+ converts reset times correctly, so e.g. a week at 82% remaining shows "18% used" resetting in
31
+ hours, not days.
32
+ - **GLM-5.2 (1M context) is now the default Z.AI model.** Logging in with Z.AI selects
33
+ `z-ai/glm-5.2` with its full 1,000,000-token context window. GLM-5.2 is absent from z.ai's
34
+ model-listing API, so it is pinned into the catalog and offered even when discovery omits it;
35
+ if z.ai later lists it, the API definition wins and it is shown once (no duplicate). Earlier
36
+ GLM models (5.1, 5, 5-turbo, 4.7, 4.5-air) remain available.
37
+ - **Kimi K2.7 Code added, and a new Kimi Coding Plan provider.** `kimi-k2.7-code` is now the
38
+ default model on the Moonshot plan, and a separate "Kimi Coding Plan" provider
39
+ (`/login kimi`) targets Moonshot's Anthropic-compatible coding endpoint with `kimi-k2.7-code`.
40
+ - **Verb spinner stays visible while a foreground tool or subagent runs.** The shimmering
41
+ "Working…/Thinking…" activity indicator now persists for the whole active turn — including
42
+ while a foreground tool (such as a shell-started server) or a subagent is executing — instead
43
+ of disappearing until the tool finished. This keeps long tool/subagent waits feeling alive
44
+ rather than frozen. An in-progress todo still swaps the verb for the todo title as before.
45
+ - **Project instructions delivered as a separate authoritative message.** The merged
46
+ `AGENTS.md` is no longer baked into the system prompt; it is delivered as a session-start,
47
+ user-role `<system-reminder>` preamble, assembled fresh on every request from session
48
+ state. This keeps the project rules immune to two regressions a system-prompt move would
49
+ otherwise risk — context compaction can no longer summarize them away (they never enter the
50
+ persisted history) and the dynamic-injection token budget can no longer truncate them —
51
+ while keeping the system prompt free of per-project content. `pythinker system-prompt` shows
52
+ the reminder below a labeled divider so the dump stays faithful.
53
+ - **Inspect the assembled system prompt.** New `pythinker system-prompt` command
54
+ renders and prints the fully-assembled system prompt for an agent
55
+ (`--agent <name>`, `--agent-file <path>`, `--work-dir <dir>`) — substituting the
56
+ live work directory, OS/shell, merged `AGENTS.md`, and discovered skills. It is
57
+ read-only: no session is created, no provider auth is required, and no MCP
58
+ servers are loaded.
59
+ - **Bound parallel tool fan-out.** Parallel-safe tool calls in one turn still
60
+ overlap, but now up to a fixed concurrency cap (10) instead of without bound, so
61
+ a turn that fans out many readers (e.g. dozens of web fetches) can no longer open
62
+ an unbounded number of sockets/file handles at once. Mutating-tool ordering and
63
+ writer exclusivity are unchanged.
64
+ - **Optional per-session spend ceiling.** New `loop_control.max_session_cost_usd`
65
+ config option (off by default). When set, a turn stops with a clear
66
+ budget-exhausted handoff message once the session's accumulated estimated cost
67
+ reaches the ceiling, instead of running to the step limit — and goal
68
+ auto-continuations and agent flows halt too. Best-effort: cost is `0` for models
69
+ with unknown pricing, so the ceiling never blocks when spend cannot be estimated.
70
+ - **Sensitive host files always re-confirm.** Writes to shell startup files
71
+ (`.zshrc`, `.bash_profile`, …), `.git` internals/hooks, the custom `.githooks`
72
+ hooks directory, `.ssh`, `.vscode`, and git
73
+ credentials (`.gitconfig`, `.netrc`, `.git-credentials`) are now classified as a
74
+ distinct edit action that re-confirms every time — even under yolo/auto — and is
75
+ never recorded as session-approved, exactly like edits to pythinker's own config.
76
+ This closes an auto-approve gap where a `.git/hooks` write inside the workspace was
77
+ treated as an ordinary edit.
78
+ - **Accept-edits mode.** New `/accept-edits` toggle auto-approves reversible
79
+ in-workspace ordinary file edits while still prompting for shell, destructive,
80
+ outside-workspace, config-surface, and sensitive host-file edits. It is
81
+ session-local (not persisted) and suppressed by safe mode. Pairs with the deny-set
82
+ above so a `.git/hooks` or shell-rc write is never swept into the auto-approve tier.
83
+ - **Agents can declare required MCP servers.** A markdown agent's frontmatter may set
84
+ `required_mcp_servers: [..]`; spawning that agent (via `Agent` or `RunAgents`) is
85
+ rejected with a clear message when those servers are configured-and-absent, instead of
86
+ wasting a turn on an agent that cannot reach its tools. While MCP is still loading the
87
+ spawn is allowed (the servers may yet connect).
88
+ - **UserPromptSubmit hooks can add context.** A non-blocking `UserPromptSubmit` hook's
89
+ `additionalContext` is now injected into the user turn as a system reminder, so the
90
+ model sees it as context for the prompt (previously only a hook *block* was honored).
91
+ Slash-command parsing still reads only the user's text, never the appended context.
92
+ - **Stale-overwrite guard for file edits.** If you read a file and it then changes on
93
+ disk (edited by you in another window or by another tool), overwriting it with WriteFile
94
+ or editing it with StrReplaceFile is now rejected with "File has been modified since you
95
+ last read it" so external changes are not silently clobbered — read it again first. This
96
+ catches external edits that StrReplaceFile's exact-string matching alone would miss.
97
+ First-contact writes (a file you never read) are unaffected, and a tool's own write
98
+ refreshes the read-state so consecutive edits are never falsely flagged.
99
+ - **Recover from output-token truncation.** When a response is cut off by the
100
+ output-token limit and makes no tool call, the turn no longer ends with a half-finished
101
+ answer treated as complete — the model is nudged to continue from where it stopped, up
102
+ to `loop_control.max_truncation_recoveries` times per turn (default 3; `0` disables).
103
+ pythinker-core now surfaces the provider's truncation signal so the loop can detect it.
104
+
105
+ Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.45.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
106
+
18
107
  ## 0.44.0 (2026-06-13)
19
108
 
20
109
  - **Toggle auto-update from the CLI.** Running `/update` now opens a menu — *Check for updates now* (the default, so a bare `/update` + Enter still checks immediately) or *Auto-update on startup* with its current state — so the toggle is discoverable without knowing a subcommand. `/update auto on|off` still sets it directly, and `/update auto` with no value opens an interactive On/Off picker (cursor defaulted to the current setting). The same toggle appears in the interactive `/settings` panel, and `pythinker info` reports the auto-update status. All surfaces show the *effective* state — an external override (`PYTHINKER_CLI_NO_AUTO_UPDATE` or a source checkout) is surfaced as the reason, renders the `/settings` row read-only, and makes `/update auto` report the read-only state rather than popping a no-op picker, so the toggle is never a silent no-op.
@@ -242,19 +242,8 @@ ${PYTHINKER_ADDITIONAL_DIRS_INFO}
242
242
  ## 11. Project Instructions (AGENTS.md)
243
243
 
244
244
  `AGENTS.md` files carry the agent-facing context a README omits — build steps, test commands, conventions, structure, and user preferences — kept separate so agents have a predictable place for instructions while READMEs stay human-focused.
245
- {% if PYTHINKER_AGENTS_MD %}
246
245
 
247
- The block below is authoritative and already merged: every `AGENTS.md` from the project root down to the working directory, deeper (more specific) files overriding shallower ones, each governing its own directory and everything beneath it.
248
-
249
- ${PYTHINKER_AGENTS_MD_FENCE}
250
- ${PYTHINKER_AGENTS_MD}
251
- ${PYTHINKER_AGENTS_MD_FENCE}
252
-
253
- Treat the merged block as complete for the root-to-working-directory range; look for additional `AGENTS.md` only in directories **below** the working directory and apply them by the same precedence when editing there.
254
- {% else %}
255
-
256
- No `AGENTS.md` files were found between the project root and the working directory; look for them only in directories **below** the working directory and apply them when editing there.
257
- {% endif %}
246
+ When any `AGENTS.md` files apply between the project root and the working directory, their merged content is **delivered as a separate authoritative message at the start of this session** — every file from the project root down to the working directory, deeper (more specific) files overriding shallower ones, each governing its own directory and everything beneath it. Treat that merged message as complete for the root-to-working-directory range, with the same authority as these instructions; look for additional `AGENTS.md` only in directories **below the working directory** and apply them by the same precedence when editing there.
258
247
 
259
248
  Precedence per §2. `README`/`README.md` files are optional supplementary context, not instructions. If a change you make invalidates anything an `AGENTS.md` documents (build/test commands, conventions, structure, workflows), update that `AGENTS.md` in the same change so it stays trustworthy.
260
249
 
@@ -7,6 +7,7 @@ OPENAI_CHATGPT_PLATFORM_ID = "openai-chatgpt"
7
7
  OPENCODE_GO_PLATFORM_ID = "opencode-go"
8
8
  MINIMAX_PLATFORM_ID = "minimax"
9
9
  MOONSHOT_PLATFORM_ID = "moonshot"
10
+ KIMI_PLATFORM_ID = "kimi"
10
11
  DEEPSEEK_PLATFORM_ID = "deepseek"
11
12
  ANTHROPIC_PLATFORM_ID = "anthropic"
12
13
  OPENROUTER_PLATFORM_ID = "openrouter"
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "ALIBABA_PLATFORM_ID",
19
20
  "ANTHROPIC_PLATFORM_ID",
20
21
  "DEEPSEEK_PLATFORM_ID",
22
+ "KIMI_PLATFORM_ID",
21
23
  "LM_STUDIO_PLATFORM_ID",
22
24
  "MINIMAX_PLATFORM_ID",
23
25
  "MOONSHOT_PLATFORM_ID",
@@ -0,0 +1,278 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import AsyncIterator, Mapping
5
+ from dataclasses import dataclass
6
+ from typing import Any, cast
7
+
8
+ import aiohttp
9
+ from pydantic import SecretStr
10
+
11
+ from pythinker_code.auth import KIMI_PLATFORM_ID
12
+ from pythinker_code.auth.oauth import OAuthEvent
13
+ from pythinker_code.auth.platforms import managed_model_key, managed_provider_key
14
+ from pythinker_code.config import Config, LLMModel, LLMProvider, save_config
15
+ from pythinker_code.thinking import apply_login_thinking_defaults
16
+ from pythinker_code.utils.aiohttp import new_client_session
17
+
18
+ # The Kimi coding plan is served from Moonshot's Anthropic-compatible endpoint
19
+ # (ANTHROPIC_BASE_URL in the Claude Code integration guide) and authenticates
20
+ # with the same Moonshot API key. It is a distinct plan from the OpenAI-compatible
21
+ # Moonshot provider (`auth/moonshot.py`), which targets `api.moonshot.ai/v1`.
22
+ KIMI_BASE_URL = "https://api.moonshot.ai/anthropic"
23
+ KIMI_MODELS_URL = "https://api.moonshot.ai/anthropic/v1/models"
24
+ KIMI_PROVIDER_KEY = managed_provider_key(KIMI_PLATFORM_ID)
25
+ KIMI_DEFAULT_MODEL_ALIAS = managed_model_key(KIMI_PLATFORM_ID, "kimi-k2.7-code")
26
+ KIMI_MODEL_DISCOVERY_TIMEOUT = aiohttp.ClientTimeout(total=15, sock_connect=8, sock_read=10)
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class KimiModel:
31
+ model_id: str
32
+ alias_suffix: str
33
+ display_name: str
34
+ provider_key: str = KIMI_PROVIDER_KEY
35
+ max_context_size: int = 262_144
36
+
37
+ @property
38
+ def alias(self) -> str:
39
+ return f"{KIMI_PLATFORM_ID}/{self.alias_suffix}"
40
+
41
+
42
+ KIMI_MODELS: tuple[KimiModel, ...] = (
43
+ KimiModel("kimi-k2.7-code", "kimi-k2.7-code", "Kimi K2.7 Code"),
44
+ )
45
+
46
+
47
+ def get_kimi_api_key_from_env() -> str | None:
48
+ # The coding plan reuses the Moonshot API key; accept either name.
49
+ for var in ("KIMI_API_KEY", "MOONSHOT_API_KEY"):
50
+ value = os.getenv(var)
51
+ if value and value.strip():
52
+ return value.strip()
53
+ return None
54
+
55
+
56
+ def _model_by_id() -> dict[str, KimiModel]:
57
+ return {model.model_id: model for model in KIMI_MODELS}
58
+
59
+
60
+ def _parse_discovered_models(data: object) -> tuple[KimiModel, ...] | None:
61
+ """Return parsed models, or None if the payload is structurally invalid.
62
+
63
+ Only models already in the curated catalog are kept: the coding plan is
64
+ focused on `kimi-k2.7-code`, and discovery is used to refresh its context
65
+ window/display name, not to widen the plan.
66
+ """
67
+ if not isinstance(data, dict):
68
+ return None
69
+ raw_items = cast(dict[str, Any], data).get("data")
70
+ if not isinstance(raw_items, list):
71
+ return None
72
+
73
+ catalog = _model_by_id()
74
+ seen: set[str] = set()
75
+ result: list[KimiModel] = []
76
+ for raw_item in cast(list[Any], raw_items):
77
+ if not isinstance(raw_item, Mapping):
78
+ continue
79
+ item = cast(Mapping[str, Any], raw_item)
80
+ model_id = item.get("id")
81
+ if not isinstance(model_id, str) or model_id not in catalog or model_id in seen:
82
+ continue
83
+ seen.add(model_id)
84
+ base = catalog[model_id]
85
+ max_ctx = base.max_context_size
86
+ ctx = item.get("context_length")
87
+ if isinstance(ctx, int) and ctx > 0:
88
+ max_ctx = ctx
89
+ display_name = base.display_name
90
+ api_name = item.get("display_name")
91
+ if isinstance(api_name, str) and api_name.strip():
92
+ display_name = api_name.strip()
93
+ result.append(
94
+ KimiModel(
95
+ model_id=base.model_id,
96
+ alias_suffix=base.alias_suffix,
97
+ display_name=display_name,
98
+ provider_key=base.provider_key,
99
+ max_context_size=max_ctx,
100
+ )
101
+ )
102
+ return tuple(result)
103
+
104
+
105
+ async def _discover_kimi_models(api_key: str) -> tuple[KimiModel, ...] | None:
106
+ async with (
107
+ new_client_session(timeout=KIMI_MODEL_DISCOVERY_TIMEOUT) as session,
108
+ session.get(
109
+ KIMI_MODELS_URL,
110
+ headers={"Authorization": f"Bearer {api_key}", "x-api-key": api_key},
111
+ raise_for_status=True,
112
+ ) as response,
113
+ ):
114
+ payload = await response.json(content_type=None)
115
+ return _parse_discovered_models(payload)
116
+
117
+
118
+ def _apply_kimi_config(
119
+ config: Config,
120
+ api_key: SecretStr,
121
+ models: tuple[KimiModel, ...] = KIMI_MODELS,
122
+ ) -> None:
123
+ config.providers[KIMI_PROVIDER_KEY] = LLMProvider(
124
+ type="anthropic",
125
+ base_url=KIMI_BASE_URL,
126
+ api_key=api_key,
127
+ )
128
+
129
+ provider_keys = {KIMI_PROVIDER_KEY}
130
+ for key, model in list(config.models.items()):
131
+ if model.provider in provider_keys:
132
+ del config.models[key]
133
+
134
+ for model in models:
135
+ config.models[model.alias] = LLMModel(
136
+ provider=model.provider_key,
137
+ model=model.model_id,
138
+ max_context_size=model.max_context_size,
139
+ display_name=model.display_name,
140
+ )
141
+
142
+ fallback = next(
143
+ (m.alias for m in models),
144
+ next(iter(config.models), ""),
145
+ )
146
+ if KIMI_DEFAULT_MODEL_ALIAS in config.models:
147
+ config.default_model = KIMI_DEFAULT_MODEL_ALIAS
148
+ else:
149
+ config.default_model = fallback
150
+ apply_login_thinking_defaults(config, thinking=False, effort="off")
151
+
152
+
153
+ async def login_kimi_api_key(
154
+ config: Config, api_key: str | None = None
155
+ ) -> AsyncIterator[OAuthEvent]:
156
+ if not config.is_from_default_location:
157
+ yield OAuthEvent(
158
+ "error",
159
+ "Login requires the default config file; restart without --config/--config-file.",
160
+ )
161
+ return
162
+
163
+ resolved_key = (api_key or get_kimi_api_key_from_env() or "").strip()
164
+ if not resolved_key:
165
+ yield OAuthEvent("error", "Kimi API key is required.")
166
+ return
167
+
168
+ models = KIMI_MODELS
169
+ try:
170
+ discovered = await _discover_kimi_models(resolved_key)
171
+ if discovered is not None and discovered:
172
+ models = discovered
173
+ except aiohttp.ClientResponseError as exc:
174
+ if exc.status in {401, 403}:
175
+ yield OAuthEvent("error", "Invalid Kimi API key; the key was not saved.")
176
+ return
177
+ yield OAuthEvent(
178
+ "info",
179
+ "Kimi model listing is unavailable; using the built-in model list.",
180
+ )
181
+ except (aiohttp.ClientError, TimeoutError, ValueError):
182
+ yield OAuthEvent(
183
+ "info",
184
+ "Kimi model listing is unavailable; using the built-in model list.",
185
+ )
186
+
187
+ _apply_kimi_config(config, SecretStr(resolved_key), models=models)
188
+ save_config(config)
189
+ yield OAuthEvent("success", f"Kimi configured with model {config.default_model}.")
190
+
191
+
192
+ async def logout_kimi(config: Config) -> AsyncIterator[OAuthEvent]:
193
+ if not config.is_from_default_location:
194
+ yield OAuthEvent(
195
+ "error",
196
+ "Logout requires the default config file; restart without --config/--config-file.",
197
+ )
198
+ return
199
+
200
+ provider_keys = {KIMI_PROVIDER_KEY}
201
+ config.providers.pop(KIMI_PROVIDER_KEY, None)
202
+ for key, model in list(config.models.items()):
203
+ if model.provider in provider_keys:
204
+ del config.models[key]
205
+
206
+ if config.default_model not in config.models:
207
+ config.default_model = next(iter(config.models), "")
208
+ save_config(config)
209
+ yield OAuthEvent("success", "Logged out of Kimi successfully.")
210
+
211
+
212
+ def apply_kimi_models(config: Config, models: tuple[KimiModel, ...]) -> bool:
213
+ """Upsert the live Kimi catalog and prune models no longer returned.
214
+
215
+ Preserves user preferences unless the selected Kimi model disappeared.
216
+ """
217
+ changed = False
218
+ aliases: list[str] = []
219
+ for model in models:
220
+ alias = model.alias
221
+ aliases.append(alias)
222
+ existing = config.models.get(alias)
223
+ if existing is None:
224
+ config.models[alias] = LLMModel(
225
+ provider=model.provider_key,
226
+ model=model.model_id,
227
+ max_context_size=model.max_context_size,
228
+ display_name=model.display_name,
229
+ )
230
+ changed = True
231
+ continue
232
+ if existing.provider != model.provider_key:
233
+ existing.provider = model.provider_key
234
+ changed = True
235
+ if existing.model != model.model_id:
236
+ existing.model = model.model_id
237
+ changed = True
238
+ if existing.max_context_size != model.max_context_size:
239
+ existing.max_context_size = model.max_context_size
240
+ changed = True
241
+ if existing.display_name != model.display_name:
242
+ existing.display_name = model.display_name
243
+ changed = True
244
+
245
+ alias_set = set(aliases)
246
+ removed_default = False
247
+ for alias, model_cfg in list(config.models.items()):
248
+ if model_cfg.provider != KIMI_PROVIDER_KEY:
249
+ continue
250
+ if alias in alias_set:
251
+ continue
252
+ del config.models[alias]
253
+ if config.default_model == alias:
254
+ removed_default = True
255
+ changed = True
256
+
257
+ if removed_default:
258
+ config.default_model = aliases[0] if aliases else next(iter(config.models), "")
259
+ changed = True
260
+ elif config.default_model and config.default_model not in config.models:
261
+ config.default_model = next(iter(config.models), "")
262
+ changed = True
263
+ return changed
264
+
265
+
266
+ def _kimi_api_key(config: Config) -> str | None:
267
+ provider = config.providers.get(KIMI_PROVIDER_KEY)
268
+ if provider is None:
269
+ return None
270
+ value = provider.api_key.get_secret_value().strip()
271
+ return value or None
272
+
273
+
274
+ async def refresh_kimi_models(config: Config) -> tuple[KimiModel, ...] | None:
275
+ api_key = _kimi_api_key(config)
276
+ if api_key is None:
277
+ return None
278
+ return await _discover_kimi_models(api_key)
@@ -17,7 +17,7 @@ from pythinker_code.utils.aiohttp import new_client_session
17
17
 
18
18
  MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"
19
19
  MOONSHOT_PROVIDER_KEY = managed_provider_key(MOONSHOT_PLATFORM_ID)
20
- MOONSHOT_DEFAULT_MODEL_ALIAS = managed_model_key(MOONSHOT_PLATFORM_ID, "kimi-k2.6")
20
+ MOONSHOT_DEFAULT_MODEL_ALIAS = managed_model_key(MOONSHOT_PLATFORM_ID, "kimi-k2.7-code")
21
21
 
22
22
 
23
23
  @dataclass(frozen=True, slots=True)
@@ -34,6 +34,7 @@ class MoonshotModel:
34
34
 
35
35
 
36
36
  MOONSHOT_MODELS: tuple[MoonshotModel, ...] = (
37
+ MoonshotModel("kimi-k2.7-code", "kimi-k2.7-code", "Kimi K2.7 Code"),
37
38
  MoonshotModel("kimi-k2.6", "kimi-k2.6", "Kimi K2.6"),
38
39
  MoonshotModel("kimi-k2.5", "kimi-k2.5", "Kimi K2.5"),
39
40
  MoonshotModel("kimi-k2-thinking", "kimi-k2-thinking", "Kimi K2 Thinking"),
@@ -231,6 +231,12 @@ async def refresh_managed_models(config: Config) -> bool:
231
231
  if not config.is_from_default_location:
232
232
  return False
233
233
 
234
+ from pythinker_code.auth.kimi import (
235
+ KIMI_PROVIDER_KEY,
236
+ KimiModel,
237
+ apply_kimi_models,
238
+ refresh_kimi_models,
239
+ )
234
240
  from pythinker_code.auth.minimax import (
235
241
  MINIMAX_ANTHROPIC_PROVIDER_KEY,
236
242
  MiniMaxModel,
@@ -267,6 +273,7 @@ async def refresh_managed_models(config: Config) -> bool:
267
273
  if provider_key in OPENCODE_GO_PROVIDER_KEYS or provider_key in (
268
274
  MINIMAX_ANTHROPIC_PROVIDER_KEY,
269
275
  ZAI_PROVIDER_KEY,
276
+ KIMI_PROVIDER_KEY,
270
277
  ):
271
278
  continue
272
279
  platform_id = parse_managed_provider_key(provider_key)
@@ -431,6 +438,14 @@ async def refresh_managed_models(config: Config) -> bool:
431
438
  if z_ai_models is not None and apply_z_ai_models(config, z_ai_models):
432
439
  changed = True
433
440
 
441
+ kimi_models: tuple[KimiModel, ...] | None = None
442
+ try:
443
+ kimi_models = await refresh_kimi_models(config)
444
+ except (aiohttp.ClientError, TimeoutError, ValueError) as exc:
445
+ logger.warning("Failed to refresh Kimi models: {error}", error=exc)
446
+ if kimi_models is not None and apply_kimi_models(config, kimi_models):
447
+ changed = True
448
+
434
449
  if changed:
435
450
  config_for_save = load_config()
436
451
  save_changed = False
@@ -443,6 +458,8 @@ async def refresh_managed_models(config: Config) -> bool:
443
458
  save_changed = True
444
459
  if z_ai_models is not None and apply_z_ai_models(config_for_save, z_ai_models):
445
460
  save_changed = True
461
+ if kimi_models is not None and apply_kimi_models(config_for_save, kimi_models):
462
+ save_changed = True
446
463
  if save_changed:
447
464
  save_config(config_for_save)
448
465
  return changed
@@ -18,7 +18,7 @@ from pythinker_code.utils.aiohttp import new_client_session
18
18
  ZAI_BASE_URL = "https://api.z.ai/api/anthropic"
19
19
  ZAI_MODELS_URL = "https://api.z.ai/api/anthropic/v1/models"
20
20
  ZAI_PROVIDER_KEY = managed_provider_key(ZAI_PLATFORM_ID)
21
- ZAI_DEFAULT_MODEL_ALIAS = managed_model_key(ZAI_PLATFORM_ID, "glm-5.1")
21
+ ZAI_DEFAULT_MODEL_ALIAS = managed_model_key(ZAI_PLATFORM_ID, "glm-5.2")
22
22
  ZAI_MODEL_DISCOVERY_TIMEOUT = aiohttp.ClientTimeout(total=15, sock_connect=8, sock_read=10)
23
23
 
24
24
 
@@ -35,7 +35,18 @@ class ZaiModel:
35
35
  return f"{ZAI_PLATFORM_ID}/{self.alias_suffix}"
36
36
 
37
37
 
38
+ # GLM-5.2 is served on z.ai's Anthropic-compatible endpoint under the plain id
39
+ # "glm-5.2", which carries the full 1M-token context window. Verified empirically
40
+ # 2026-06-15 against api.z.ai/api/anthropic: a request with 1,002,378 input
41
+ # tokens succeeded while ~1.05M returned stop_reason="model_context_window_exceeded".
42
+ # The documented "glm-5.2[1m]" suffix is NOT a valid model code here (returns
43
+ # HTTP 400 "Unknown Model") — the plain id already grants 1M, so we use it and
44
+ # set the real window. z.ai's /models listings expose no context field and omit
45
+ # glm-5.2 entirely, so both the id and the size are curated.
46
+ _GLM_5_2 = ZaiModel("glm-5.2", "glm-5.2", "GLM-5.2", max_context_size=1_000_000)
47
+
38
48
  ZAI_MODELS: tuple[ZaiModel, ...] = (
49
+ _GLM_5_2,
39
50
  ZaiModel("glm-5.1", "glm-5.1", "GLM-5.1", max_context_size=204_800),
40
51
  ZaiModel("glm-5", "glm-5", "GLM-5"),
41
52
  ZaiModel("glm-5-turbo", "glm-5-turbo", "GLM-5-Turbo"),
@@ -43,6 +54,26 @@ ZAI_MODELS: tuple[ZaiModel, ...] = (
43
54
  ZaiModel("glm-4.5-air", "glm-4.5-air", "GLM-4.5-Air", max_context_size=98_304),
44
55
  )
45
56
 
57
+ # Curated models that must always be offered even when z.ai's /models endpoint
58
+ # does not list them. GLM-5.2 is usable for chat but is absent from both the
59
+ # Anthropic and OpenAI-compatible /models listings (verified 2026-06-15), so
60
+ # without pinning it never reaches the model menu and a successful login or
61
+ # periodic refresh would drop it. Discovered entries win for everything else.
62
+ _PINNED_MODELS: tuple[ZaiModel, ...] = (_GLM_5_2,)
63
+
64
+
65
+ def _with_pinned_models(models: tuple[ZaiModel, ...]) -> tuple[ZaiModel, ...]:
66
+ """Prepend curated pinned models the live catalog omitted, deduped by alias.
67
+
68
+ If z.ai later starts returning a pinned model (e.g. it adds "glm-5.2" to its
69
+ /models listing), the discovered entry already occupies that alias, so the
70
+ pin is dropped — the API-provided definition wins and the model appears once,
71
+ never twice. The pin only fills the gap while the endpoint omits it.
72
+ """
73
+ present = {model.alias for model in models}
74
+ missing = tuple(model for model in _PINNED_MODELS if model.alias not in present)
75
+ return missing + models
76
+
46
77
 
47
78
  def get_z_ai_api_key_from_env() -> str | None:
48
79
  value = os.getenv("ZAI_API_KEY")
@@ -163,6 +194,8 @@ def _apply_z_ai_config(
163
194
  api_key=api_key,
164
195
  )
165
196
 
197
+ models = _with_pinned_models(models)
198
+
166
199
  provider_keys = {ZAI_PROVIDER_KEY}
167
200
  for key, model in list(config.models.items()):
168
201
  if model.provider in provider_keys:
@@ -251,6 +284,7 @@ def apply_z_ai_models(config: Config, models: tuple[ZaiModel, ...]) -> bool:
251
284
 
252
285
  Preserves user preferences unless the selected Z AI model disappeared.
253
286
  """
287
+ models = _with_pinned_models(models)
254
288
  changed = False
255
289
  aliases: list[str] = []
256
290
  for model in models:
@@ -36,11 +36,11 @@ class SwitchToWeb(Exception):
36
36
  self.session_id = session_id
37
37
 
38
38
 
39
- class SwitchToVis(Exception):
40
- """Switch to vis (tracing visualizer) interface."""
39
+ class SwitchToDashboard(Exception):
40
+ """Switch to dashboard (tracing visualizer) interface."""
41
41
 
42
- def __init__(self, session_id: str | None = None):
43
- super().__init__("switch_to_vis")
42
+ def __init__(self, session_id: str | None = None) -> None:
43
+ super().__init__("switch_to_dashboard")
44
44
  self.session_id = session_id
45
45
 
46
46
 
@@ -1042,7 +1042,7 @@ def pythinker(
1042
1042
  except SwitchToWeb:
1043
1043
  preserve_background_tasks = True
1044
1044
  raise
1045
- except SwitchToVis:
1045
+ except SwitchToDashboard:
1046
1046
  preserve_background_tasks = True
1047
1047
  raise
1048
1048
  finally:
@@ -1134,10 +1134,10 @@ def pythinker(
1134
1134
  await asyncio.to_thread(mutate_metadata, _mark_last)
1135
1135
 
1136
1136
  async def _reload_loop(session_id: str | None) -> tuple[str | None, int]:
1137
- """Run the main loop, handling Reload/SwitchToWeb/SwitchToVis.
1137
+ """Run the main loop, handling Reload/SwitchToWeb/SwitchToDashboard.
1138
1138
 
1139
1139
  Returns:
1140
- (switch_target, exit_code) where switch_target is "web", "vis",
1140
+ (switch_target, exit_code) where switch_target is "web", "dashboard",
1141
1141
  or None if the session ended normally.
1142
1142
  """
1143
1143
  last_session: Session | None = None
@@ -1182,19 +1182,19 @@ def pythinker(
1182
1182
  if session is not None:
1183
1183
  await _post_run(session, ExitCode.SUCCESS)
1184
1184
  return "web", ExitCode.SUCCESS
1185
- except SwitchToVis as e:
1185
+ except SwitchToDashboard as e:
1186
1186
  if _latest_created_session is not None:
1187
1187
  _latest_created_session.release_ownership()
1188
1188
  if e.session_id is not None:
1189
1189
  session = await Session.find(work_dir, e.session_id)
1190
1190
  if session is not None:
1191
1191
  await _post_run(session, ExitCode.SUCCESS)
1192
- return "vis", ExitCode.SUCCESS
1192
+ return "dashboard", ExitCode.SUCCESS
1193
1193
  assert last_session is not None
1194
1194
  await _post_run(last_session, exit_code)
1195
1195
  last_session.release_ownership()
1196
1196
  return None, exit_code
1197
- except (SwitchToWeb, SwitchToVis):
1197
+ except (SwitchToWeb, SwitchToDashboard):
1198
1198
  # Currently handled inside the loop (return), but re-raise explicitly
1199
1199
  # so the generic except below never treats them as unexpected errors.
1200
1200
  raise
@@ -1316,7 +1316,7 @@ def pythinker(
1316
1316
  "Run with --debug for full traceback, or run pythinker export to share diagnostics."
1317
1317
  )
1318
1318
  raise typer.Exit(code=1) from exc
1319
- if switch_target in ("web", "vis"):
1319
+ if switch_target in ("web", "dashboard"):
1320
1320
  from pythinker_code.utils.logging import restore_stderr
1321
1321
 
1322
1322
  restore_stderr()
@@ -1336,9 +1336,9 @@ def pythinker(
1336
1336
 
1337
1337
  run_web_server(open_browser=True)
1338
1338
  else:
1339
- from pythinker_code.vis.app import run_vis_server
1339
+ from pythinker_code.dashboard.app import run_dashboard_server
1340
1340
 
1341
- run_vis_server(open_browser=True)
1341
+ run_dashboard_server(open_browser=True)
1342
1342
  elif exit_code != ExitCode.SUCCESS:
1343
1343
  raise typer.Exit(code=exit_code)
1344
1344
 
@@ -20,6 +20,11 @@ class LazySubcommandGroup(typer.core.TyperGroup):
20
20
  "mcp": ("pythinker_code.cli.mcp", "cli", "Manage MCP server configurations."),
21
21
  "plugin": ("pythinker_code.cli.plugin", "cli", "Manage plugins."),
22
22
  "skill": ("pythinker_code.cli.skill", "cli", "Inspect and lock Pythinker skills."),
23
+ "system-prompt": (
24
+ "pythinker_code.cli.system_prompt",
25
+ "cli",
26
+ "Print the assembled system prompt for an agent.",
27
+ ),
23
28
  "review": (
24
29
  "pythinker_code.cli.review",
25
30
  "cli",
@@ -45,7 +50,11 @@ class LazySubcommandGroup(typer.core.TyperGroup):
45
50
  "cli",
46
51
  "Check for and install Pythinker CLI updates.",
47
52
  ),
48
- "vis": ("pythinker_code.cli.vis", "cli", "Run Pythinker Agent Tracing Visualizer."),
53
+ "dashboard": (
54
+ "pythinker_code.cli.dashboard",
55
+ "cli",
56
+ "Run Pythinker Agent Tracing Visualizer.",
57
+ ),
49
58
  "web": ("pythinker_code.cli.web", "cli", "Run Pythinker CLI web interface."),
50
59
  }
51
60
  lazy_command_order: tuple[str, ...] = (
@@ -54,12 +63,13 @@ class LazySubcommandGroup(typer.core.TyperGroup):
54
63
  "mcp",
55
64
  "plugin",
56
65
  "skill",
66
+ "system-prompt",
57
67
  "review",
58
68
  "secscan",
59
69
  "security-scan",
60
70
  "debug",
61
71
  "update",
62
- "vis",
72
+ "dashboard",
63
73
  "web",
64
74
  )
65
75