klaude-code 1.2.24__tar.gz → 1.2.25__tar.gz

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 (199) hide show
  1. {klaude_code-1.2.24 → klaude_code-1.2.25}/PKG-INFO +2 -1
  2. {klaude_code-1.2.24 → klaude_code-1.2.25}/pyproject.toml +2 -1
  3. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/runtime.py +17 -1
  4. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/thinking_cmd.py +37 -28
  5. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/const.py +3 -5
  6. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/executor.py +45 -2
  7. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/op.py +11 -0
  8. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/op_handler.py +5 -0
  9. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/display.py +2 -0
  10. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/event_handler.py +3 -11
  11. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/input_prompt_toolkit.py +12 -1
  12. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/renderer.py +99 -12
  13. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/code_panel.py +24 -5
  14. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/live.py +17 -0
  15. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/markdown.py +167 -104
  16. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/status.py +5 -11
  17. {klaude_code-1.2.24 → klaude_code-1.2.25}/README.md +0 -0
  18. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/__init__.py +0 -0
  19. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/__init__.py +0 -0
  20. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/codex/__init__.py +0 -0
  21. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/codex/exceptions.py +0 -0
  22. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/codex/jwt_utils.py +0 -0
  23. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/codex/oauth.py +0 -0
  24. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/auth/codex/token_manager.py +0 -0
  25. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/__init__.py +0 -0
  26. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/auth_cmd.py +0 -0
  27. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/config_cmd.py +0 -0
  28. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/debug.py +0 -0
  29. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/list_model.py +0 -0
  30. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/main.py +0 -0
  31. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/self_update.py +0 -0
  32. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/cli/session_cmd.py +0 -0
  33. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/__init__.py +0 -0
  34. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/clear_cmd.py +0 -0
  35. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/command_abc.py +0 -0
  36. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/debug_cmd.py +0 -0
  37. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/export_cmd.py +0 -0
  38. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/export_online_cmd.py +0 -0
  39. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/help_cmd.py +0 -0
  40. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/model_cmd.py +0 -0
  41. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/prompt-init.md +0 -0
  42. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/prompt-jj-describe.md +0 -0
  43. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/prompt_command.py +0 -0
  44. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/refresh_cmd.py +0 -0
  45. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/registry.py +0 -0
  46. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/release_notes_cmd.py +0 -0
  47. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/status_cmd.py +0 -0
  48. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/command/terminal_setup_cmd.py +0 -0
  49. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/config/__init__.py +0 -0
  50. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/config/config.py +0 -0
  51. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/config/select_model.py +0 -0
  52. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/__init__.py +0 -0
  53. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/agent.py +0 -0
  54. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/manager/__init__.py +0 -0
  55. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/manager/llm_clients.py +0 -0
  56. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/manager/llm_clients_builder.py +0 -0
  57. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/manager/sub_agent_manager.py +0 -0
  58. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompt.py +0 -0
  59. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-claude-code.md +0 -0
  60. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -0
  61. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +0 -0
  62. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-codex.md +0 -0
  63. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-gemini.md +0 -0
  64. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-minimal.md +0 -0
  65. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-sub-agent-explore.md +0 -0
  66. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -0
  67. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-sub-agent-web.md +0 -0
  68. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/prompts/prompt-sub-agent.md +0 -0
  69. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/reminders.py +0 -0
  70. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/task.py +0 -0
  71. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/__init__.py +0 -0
  72. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/__init__.py +0 -0
  73. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/_utils.py +0 -0
  74. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/apply_patch.py +0 -0
  75. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/apply_patch_tool.md +0 -0
  76. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/apply_patch_tool.py +0 -0
  77. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/diff_builder.py +0 -0
  78. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/edit_tool.md +0 -0
  79. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/edit_tool.py +0 -0
  80. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/read_tool.md +0 -0
  81. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/read_tool.py +0 -0
  82. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/write_tool.md +0 -0
  83. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/file/write_tool.py +0 -0
  84. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/report_back_tool.py +0 -0
  85. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/shell/__init__.py +0 -0
  86. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/shell/bash_tool.md +0 -0
  87. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/shell/bash_tool.py +0 -0
  88. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/shell/command_safety.py +0 -0
  89. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/skill/__init__.py +0 -0
  90. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/skill/skill_tool.md +0 -0
  91. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/skill/skill_tool.py +0 -0
  92. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/sub_agent_tool.py +0 -0
  93. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/__init__.py +0 -0
  94. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/todo_write_tool.md +0 -0
  95. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/todo_write_tool.py +0 -0
  96. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/todo_write_tool_raw.md +0 -0
  97. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/update_plan_tool.md +0 -0
  98. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/todo/update_plan_tool.py +0 -0
  99. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/tool_abc.py +0 -0
  100. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/tool_context.py +0 -0
  101. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/tool_registry.py +0 -0
  102. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/tool_runner.py +0 -0
  103. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/truncation.py +0 -0
  104. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/__init__.py +0 -0
  105. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/mermaid_tool.md +0 -0
  106. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/mermaid_tool.py +0 -0
  107. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/web_fetch_tool.md +0 -0
  108. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/web_fetch_tool.py +0 -0
  109. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/web_search_tool.md +0 -0
  110. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/tool/web/web_search_tool.py +0 -0
  111. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/core/turn.py +0 -0
  112. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/__init__.py +0 -0
  113. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/anthropic/__init__.py +0 -0
  114. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/anthropic/client.py +0 -0
  115. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/anthropic/input.py +0 -0
  116. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/client.py +0 -0
  117. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/codex/__init__.py +0 -0
  118. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/codex/client.py +0 -0
  119. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/input_common.py +0 -0
  120. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openai_compatible/__init__.py +0 -0
  121. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openai_compatible/client.py +0 -0
  122. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openai_compatible/input.py +0 -0
  123. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openai_compatible/stream.py +0 -0
  124. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -0
  125. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openrouter/__init__.py +0 -0
  126. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openrouter/client.py +0 -0
  127. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openrouter/input.py +0 -0
  128. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/openrouter/reasoning.py +0 -0
  129. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/registry.py +0 -0
  130. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/responses/__init__.py +0 -0
  131. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/responses/client.py +0 -0
  132. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/responses/input.py +0 -0
  133. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/llm/usage.py +0 -0
  134. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/__init__.py +0 -0
  135. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/commands.py +0 -0
  136. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/events.py +0 -0
  137. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/llm_param.py +0 -0
  138. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/model.py +0 -0
  139. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/sub_agent/__init__.py +0 -0
  140. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/sub_agent/explore.py +0 -0
  141. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/sub_agent/oracle.py +0 -0
  142. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/sub_agent/task.py +0 -0
  143. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/sub_agent/web.py +0 -0
  144. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/protocol/tools.py +0 -0
  145. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/__init__.py +0 -0
  146. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/codec.py +0 -0
  147. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/export.py +0 -0
  148. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/selector.py +0 -0
  149. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/session.py +0 -0
  150. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/store.py +0 -0
  151. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/session/templates/export_session.html +0 -0
  152. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/__init__.py +0 -0
  153. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/assets/deslop/SKILL.md +0 -0
  154. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/assets/dev-docs/SKILL.md +0 -0
  155. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/assets/handoff/SKILL.md +0 -0
  156. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/assets/jj-workspace/SKILL.md +0 -0
  157. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/assets/skill-creator/SKILL.md +0 -0
  158. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/loader.py +0 -0
  159. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/manager.py +0 -0
  160. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/skill/system_skills.py +0 -0
  161. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/trace/__init__.py +0 -0
  162. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/trace/log.py +0 -0
  163. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/__init__.py +0 -0
  164. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/core/__init__.py +0 -0
  165. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/core/display.py +0 -0
  166. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/core/input.py +0 -0
  167. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/core/stage_manager.py +0 -0
  168. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/__init__.py +0 -0
  169. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/debug/__init__.py +0 -0
  170. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/debug/display.py +0 -0
  171. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/exec/__init__.py +0 -0
  172. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/exec/display.py +0 -0
  173. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/__init__.py +0 -0
  174. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/clipboard.py +0 -0
  175. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/completers.py +0 -0
  176. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/modes/repl/key_bindings.py +0 -0
  177. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/__init__.py +0 -0
  178. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/assistant.py +0 -0
  179. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/common.py +0 -0
  180. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/developer.py +0 -0
  181. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/diffs.py +0 -0
  182. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/errors.py +0 -0
  183. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/metadata.py +0 -0
  184. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/sub_agent.py +0 -0
  185. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/thinking.py +0 -0
  186. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/tools.py +0 -0
  187. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/renderers/user_input.py +0 -0
  188. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/__init__.py +0 -0
  189. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/cjk_wrap.py +0 -0
  190. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/quote.py +0 -0
  191. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/searchable_text.py +0 -0
  192. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/rich/theme.py +0 -0
  193. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/terminal/__init__.py +0 -0
  194. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/terminal/color.py +0 -0
  195. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/terminal/control.py +0 -0
  196. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/terminal/notifier.py +0 -0
  197. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/terminal/progress_bar.py +0 -0
  198. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/utils/__init__.py +0 -0
  199. {klaude_code-1.2.24 → klaude_code-1.2.25}/src/klaude_code/ui/utils/common.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.24
3
+ Version: 1.2.25
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
7
7
  Requires-Dist: ddgs>=9.9.3
8
8
  Requires-Dist: diff-match-patch>=20241021
9
+ Requires-Dist: markdown-it-py>=4.0.0
9
10
  Requires-Dist: openai>=1.102.0
10
11
  Requires-Dist: pillow>=12.0.0
11
12
  Requires-Dist: prompt-toolkit>=3.0.52
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "klaude-code"
7
- version = "1.2.24"
7
+ version = "1.2.25"
8
8
  description = "Add your description here"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "chardet>=5.2.0",
14
14
  "ddgs>=9.9.3",
15
15
  "diff-match-patch>=20241021",
16
+ "markdown-it-py>=4.0.0",
16
17
  "openai>=1.102.0",
17
18
  "pillow>=12.0.0",
18
19
  "prompt-toolkit>=3.0.52",
@@ -265,7 +265,23 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
265
265
  )
266
266
 
267
267
  # Set up input provider for interactive mode
268
- input_provider: ui.InputProviderABC = ui.PromptToolkitInput(status_provider=_status_provider)
268
+ def _stop_rich_bottom_ui() -> None:
269
+ display = components.display
270
+ if isinstance(display, ui.REPLDisplay):
271
+ display.renderer.spinner_stop()
272
+ display.renderer.stop_bottom_live()
273
+ elif (
274
+ isinstance(display, ui.DebugEventDisplay)
275
+ and display.wrapped_display
276
+ and isinstance(display.wrapped_display, ui.REPLDisplay)
277
+ ):
278
+ display.wrapped_display.renderer.spinner_stop()
279
+ display.wrapped_display.renderer.stop_bottom_live()
280
+
281
+ input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
282
+ status_provider=_status_provider,
283
+ pre_prompt=_stop_rich_bottom_ui,
284
+ )
269
285
 
270
286
  # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
271
287
  def _show_toast_once() -> None:
@@ -56,6 +56,14 @@ def _is_gemini_flash_model(model_name: str | None) -> bool:
56
56
  return "gemini-3-flash" in model_name.lower()
57
57
 
58
58
 
59
+ def should_auto_trigger_thinking(model_name: str | None) -> bool:
60
+ """Check if model should auto-trigger thinking selection on switch."""
61
+ if not model_name:
62
+ return False
63
+ model_lower = model_name.lower()
64
+ return "gpt-5" in model_lower or "gemini-3" in model_lower or "opus" in model_lower
65
+
66
+
59
67
  def _get_levels_for_responses(model_name: str | None) -> list[str]:
60
68
  """Get thinking levels for responses protocol."""
61
69
  if _is_codex_max_model(model_name):
@@ -69,7 +77,7 @@ def _get_levels_for_responses(model_name: str | None) -> list[str]:
69
77
  return RESPONSES_LEVELS
70
78
 
71
79
 
72
- def _format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
80
+ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
73
81
  """Format the current thinking configuration for display."""
74
82
  thinking = config.thinking
75
83
  if not thinking:
@@ -164,6 +172,31 @@ def _select_anthropic_thinking_sync() -> llm_param.Thinking | None:
164
172
  return None
165
173
 
166
174
 
175
+ async def select_thinking_for_protocol(config: llm_param.LLMConfigParameter) -> llm_param.Thinking | None:
176
+ """Select thinking configuration based on the LLM protocol.
177
+
178
+ Returns the selected Thinking config, or None if user cancelled.
179
+ """
180
+ protocol = config.protocol
181
+ model_name = config.model
182
+
183
+ if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
184
+ return await asyncio.to_thread(_select_responses_thinking_sync, model_name)
185
+
186
+ if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
187
+ return await asyncio.to_thread(_select_anthropic_thinking_sync)
188
+
189
+ if protocol == llm_param.LLMClientProtocol.OPENROUTER:
190
+ if _is_openrouter_model_with_reasoning_effort(model_name):
191
+ return await asyncio.to_thread(_select_responses_thinking_sync, model_name)
192
+ return await asyncio.to_thread(_select_anthropic_thinking_sync)
193
+
194
+ if protocol == llm_param.LLMClientProtocol.OPENAI:
195
+ return await asyncio.to_thread(_select_anthropic_thinking_sync)
196
+
197
+ return None
198
+
199
+
167
200
  class ThinkingCommand(CommandABC):
168
201
  """Configure model thinking/reasoning level."""
169
202
 
@@ -185,40 +218,16 @@ class ThinkingCommand(CommandABC):
185
218
  return self._no_change_result(agent, "No profile configured")
186
219
 
187
220
  config = agent.profile.llm_client.get_llm_config()
188
- protocol = config.protocol
189
- model_name = config.model
190
-
191
- current = _format_current_thinking(config)
192
-
193
- # Select new thinking configuration based on protocol
194
- new_thinking: llm_param.Thinking | None = None
195
-
196
- if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
197
- new_thinking = await asyncio.to_thread(_select_responses_thinking_sync, model_name)
198
-
199
- elif protocol == llm_param.LLMClientProtocol.ANTHROPIC:
200
- new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
201
-
202
- elif protocol == llm_param.LLMClientProtocol.OPENROUTER:
203
- if _is_openrouter_model_with_reasoning_effort(model_name):
204
- new_thinking = await asyncio.to_thread(_select_responses_thinking_sync, model_name)
205
- else:
206
- new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
207
-
208
- elif protocol == llm_param.LLMClientProtocol.OPENAI:
209
- # openai_compatible uses anthropic style
210
- new_thinking = await asyncio.to_thread(_select_anthropic_thinking_sync)
211
-
212
- else:
213
- return self._no_change_result(agent, f"Unsupported protocol: {protocol}")
221
+ current = format_current_thinking(config)
214
222
 
223
+ new_thinking = await select_thinking_for_protocol(config)
215
224
  if new_thinking is None:
216
225
  return self._no_change_result(agent, "(no change)")
217
226
 
218
227
  # Apply the new thinking configuration
219
228
  config.thinking = new_thinking
220
229
  agent.session.model_thinking = new_thinking
221
- new_status = _format_current_thinking(config)
230
+ new_status = format_current_thinking(config)
222
231
 
223
232
  return CommandResult(
224
233
  events=[
@@ -117,16 +117,14 @@ STATUS_DEFAULT_TEXT = "Thinking …"
117
117
  # Status shimmer animation
118
118
  # Horizontal padding used when computing shimmer band position
119
119
  STATUS_SHIMMER_PADDING = 10
120
- # Duration in seconds for one full shimmer sweep across the text
121
- STATUS_SHIMMER_SWEEP_SECONDS = 2
122
120
  # Half-width of the shimmer band in characters
123
121
  STATUS_SHIMMER_BAND_HALF_WIDTH = 5.0
124
122
  # Scale factor applied to shimmer intensity when blending colors
125
123
  STATUS_SHIMMER_ALPHA_SCALE = 0.7
126
124
 
127
- # Spinner breathing animation
128
- # Duration in seconds for one full breathe-in + breathe-out cycle
129
- # Keep in sync with STATUS_SHIMMER_SWEEP_SECONDS for visual consistency
125
+ # Spinner breathing and shimmer animation period
126
+ # Duration in seconds for one full breathe-in + breathe-out cycle (breathing)
127
+ # and one full shimmer sweep across the text (shimmer)
130
128
  SPINNER_BREATH_PERIOD_SECONDS: float = 2.0
131
129
 
132
130
 
@@ -14,6 +14,11 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
 
16
16
  from klaude_code.command import dispatch_command
17
+ from klaude_code.command.thinking_cmd import (
18
+ format_current_thinking,
19
+ select_thinking_for_protocol,
20
+ should_auto_trigger_thinking,
21
+ )
17
22
  from klaude_code.config import load_config
18
23
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, ModelProfileProvider
19
24
  from klaude_code.core.manager import LLMClients, SubAgentManager
@@ -235,17 +240,55 @@ class ExecutorContext:
235
240
  agent.session.model_thinking = llm_config.thinking
236
241
 
237
242
  developer_item = model.DeveloperMessageItem(
238
- content=f"switched to model: {operation.model_name}",
243
+ content=f"Switched to: {llm_config.model}",
239
244
  command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
240
245
  )
241
246
  agent.session.append_history([developer_item])
242
247
 
243
248
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
244
- await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
245
249
 
246
250
  if self._on_model_change is not None:
247
251
  self._on_model_change(llm_client.model_name)
248
252
 
253
+ if should_auto_trigger_thinking(llm_config.model):
254
+ thinking_op = op.ChangeThinkingOperation(session_id=operation.session_id)
255
+ await thinking_op.execute(handler=self)
256
+ # WelcomeEvent is already handled by the thinking change
257
+ else:
258
+ await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
259
+
260
+ async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
261
+ """Handle a change thinking operation by prompting user to select thinking level."""
262
+ agent = await self._ensure_agent(operation.session_id)
263
+ if not agent.profile:
264
+ return
265
+
266
+ config = agent.profile.llm_client.get_llm_config()
267
+ current = format_current_thinking(config)
268
+
269
+ new_thinking = await select_thinking_for_protocol(config)
270
+
271
+ if new_thinking is None:
272
+ developer_item = model.DeveloperMessageItem(
273
+ content="(thinking unchanged)",
274
+ command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
275
+ )
276
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
277
+ return
278
+
279
+ config.thinking = new_thinking
280
+ agent.session.model_thinking = new_thinking
281
+ new_status = format_current_thinking(config)
282
+
283
+ developer_item = model.DeveloperMessageItem(
284
+ content=f"Thinking changed: {current} -> {new_status}",
285
+ command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
286
+ )
287
+ agent.session.append_history([developer_item])
288
+
289
+ await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
290
+ await self.emit_event(events.WelcomeEvent(work_dir=str(agent.session.work_dir), llm_config=config))
291
+
249
292
  async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
250
293
  agent = await self._ensure_agent(operation.session_id)
251
294
  new_session = Session.create(work_dir=agent.session.work_dir)
@@ -25,6 +25,7 @@ class OperationType(Enum):
25
25
  USER_INPUT = "user_input"
26
26
  RUN_AGENT = "run_agent"
27
27
  CHANGE_MODEL = "change_model"
28
+ CHANGE_THINKING = "change_thinking"
28
29
  CLEAR_SESSION = "clear_session"
29
30
  EXPORT_SESSION = "export_session"
30
31
  INTERRUPT = "interrupt"
@@ -77,6 +78,16 @@ class ChangeModelOperation(Operation):
77
78
  await handler.handle_change_model(self)
78
79
 
79
80
 
81
+ class ChangeThinkingOperation(Operation):
82
+ """Operation for changing the thinking/reasoning configuration."""
83
+
84
+ type: OperationType = OperationType.CHANGE_THINKING
85
+ session_id: str
86
+
87
+ async def execute(self, handler: OperationHandler) -> None:
88
+ await handler.handle_change_thinking(self)
89
+
90
+
80
91
  class ClearSessionOperation(Operation):
81
92
  """Operation for clearing the active session and starting a new one."""
82
93
 
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Protocol
11
11
  if TYPE_CHECKING:
12
12
  from klaude_code.protocol.op import (
13
13
  ChangeModelOperation,
14
+ ChangeThinkingOperation,
14
15
  ClearSessionOperation,
15
16
  ExportSessionOperation,
16
17
  InitAgentOperation,
@@ -35,6 +36,10 @@ class OperationHandler(Protocol):
35
36
  """Handle a change model operation."""
36
37
  ...
37
38
 
39
+ async def handle_change_thinking(self, operation: ChangeThinkingOperation) -> None:
40
+ """Handle a change thinking operation."""
41
+ ...
42
+
38
43
  async def handle_clear_session(self, operation: ClearSessionOperation) -> None:
39
44
  """Handle a clear session operation."""
40
45
  ...
@@ -57,3 +57,5 @@ class REPLDisplay(DisplayABC):
57
57
  # Spinner may already be stopped or not started; ignore.
58
58
  with contextlib.suppress(Exception):
59
59
  self.renderer.spinner_stop()
60
+ with contextlib.suppress(Exception):
61
+ self.renderer.stop_bottom_live()
@@ -368,22 +368,20 @@ class DisplayEventHandler:
368
368
 
369
369
  first_delta = not self.thinking_stream.is_active
370
370
  if first_delta:
371
- self.renderer.console.push_theme(self.renderer.themes.thinking_markdown_theme)
372
371
  mdstream = MarkdownStream(
373
372
  mdargs={
374
373
  "code_theme": self.renderer.themes.code_theme,
375
- "style": self.renderer.console.get_style(ThemeKey.THINKING),
374
+ "style": ThemeKey.THINKING,
376
375
  },
377
376
  theme=self.renderer.themes.thinking_markdown_theme,
378
377
  console=self.renderer.console,
379
- spinner=self.renderer.spinner_renderable(),
378
+ live_sink=self.renderer.set_stream_renderable,
380
379
  mark=THINKING_MESSAGE_MARK,
381
380
  mark_style=ThemeKey.THINKING,
382
381
  left_margin=const.MARKDOWN_LEFT_MARGIN,
383
382
  markdown_class=ThinkingMarkdown,
384
383
  )
385
384
  self.thinking_stream.start(mdstream)
386
- self.renderer.spinner_stop()
387
385
 
388
386
  self.thinking_stream.append(event.content)
389
387
 
@@ -414,17 +412,13 @@ class DisplayEventHandler:
414
412
  mdargs={"code_theme": self.renderer.themes.code_theme},
415
413
  theme=self.renderer.themes.markdown_theme,
416
414
  console=self.renderer.console,
417
- spinner=self.renderer.spinner_renderable(),
415
+ live_sink=self.renderer.set_stream_renderable,
418
416
  mark=ASSISTANT_MESSAGE_MARK,
419
417
  left_margin=const.MARKDOWN_LEFT_MARGIN,
420
418
  )
421
419
  self.assistant_stream.start(mdstream)
422
420
  self.assistant_stream.append(event.content)
423
421
  if first_delta and self.assistant_stream.mdstream is not None:
424
- # Stop spinner and immediately start MarkdownStream's Live
425
- # to avoid flicker. The update() call starts the Live with
426
- # the spinner embedded, providing seamless transition.
427
- self.renderer.spinner_stop()
428
422
  self.assistant_stream.mdstream.update(self.assistant_stream.buffer)
429
423
  await self.stage_manager.transition_to(Stage.ASSISTANT)
430
424
  await self._flush_assistant_buffer(self.assistant_stream)
@@ -488,7 +482,6 @@ class DisplayEventHandler:
488
482
  self.spinner_status.reset()
489
483
  self.renderer.spinner_stop()
490
484
  self.renderer.console.print(Rule(characters="-", style=ThemeKey.LINES))
491
- self.renderer.print()
492
485
  await self.stage_manager.transition_to(Stage.WAITING)
493
486
  self._maybe_notify_task_finish(event)
494
487
 
@@ -552,7 +545,6 @@ class DisplayEventHandler:
552
545
  assert mdstream is not None
553
546
  mdstream.update(normalize_thinking_content(self.thinking_stream.buffer), final=True)
554
547
  self.thinking_stream.finish()
555
- self.renderer.console.pop_theme()
556
548
  self.renderer.print()
557
549
  self.renderer.spinner_start()
558
550
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import shutil
4
5
  from collections.abc import AsyncIterator, Callable
5
6
  from pathlib import Path
@@ -51,8 +52,12 @@ class PromptToolkitInput(InputProviderABC):
51
52
  self,
52
53
  prompt: str = USER_MESSAGE_MARK,
53
54
  status_provider: Callable[[], REPLStatusSnapshot] | None = None,
55
+ pre_prompt: Callable[[], None] | None = None,
56
+ post_prompt: Callable[[], None] | None = None,
54
57
  ): # ▌
55
58
  self._status_provider = status_provider
59
+ self._pre_prompt = pre_prompt
60
+ self._post_prompt = post_prompt
56
61
  self._is_light_terminal_background = is_light_terminal_background(timeout=0.2)
57
62
 
58
63
  project = str(Path.cwd()).strip("/").replace("/", "-")
@@ -80,7 +85,7 @@ class PromptToolkitInput(InputProviderABC):
80
85
  [(INPUT_PROMPT_STYLE, prompt)],
81
86
  history=FileHistory(str(history_path)),
82
87
  multiline=True,
83
- cursor=CursorShape.BEAM,
88
+ cursor=CursorShape.BLINKING_BEAM,
84
89
  prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
85
90
  key_bindings=kb,
86
91
  completer=ThreadedCompleter(create_repl_completer()),
@@ -202,8 +207,14 @@ class PromptToolkitInput(InputProviderABC):
202
207
  @override
203
208
  async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
204
209
  while True:
210
+ if self._pre_prompt is not None:
211
+ with contextlib.suppress(Exception):
212
+ self._pre_prompt()
205
213
  with patch_stdout():
206
214
  line: str = await self._session.prompt_async(placeholder=self._render_input_placeholder())
215
+ if self._post_prompt is not None:
216
+ with contextlib.suppress(Exception):
217
+ self._post_prompt()
207
218
 
208
219
  # Extract images referenced in the input text
209
220
  images = extract_images_from_text(line)
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from collections.abc import Iterator
4
5
  from contextlib import contextmanager
5
6
  from dataclasses import dataclass
6
7
  from typing import Any
7
8
 
8
- from rich.console import Console
9
+ from rich.console import Console, Group, RenderableType
10
+ from rich.padding import Padding
9
11
  from rich.spinner import Spinner
10
- from rich.status import Status
11
12
  from rich.style import Style, StyleType
12
13
  from rich.text import Text
13
14
 
@@ -23,8 +24,9 @@ from klaude_code.ui.renderers import tools as r_tools
23
24
  from klaude_code.ui.renderers import user_input as r_user_input
24
25
  from klaude_code.ui.renderers.common import truncate_display
25
26
  from klaude_code.ui.rich import status as r_status
27
+ from klaude_code.ui.rich.live import CropAboveLive, SingleLine
26
28
  from klaude_code.ui.rich.quote import Quote
27
- from klaude_code.ui.rich.status import ShimmerStatusText
29
+ from klaude_code.ui.rich.status import BreathingSpinner, ShimmerStatusText
28
30
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
29
31
 
30
32
 
@@ -42,10 +44,18 @@ class REPLRenderer:
42
44
  self.themes = get_theme(theme)
43
45
  self.console: Console = Console(theme=self.themes.app_theme)
44
46
  self.console.push_theme(self.themes.markdown_theme)
45
- self._spinner: Status = self.console.status(
46
- ShimmerStatusText(const.STATUS_DEFAULT_TEXT),
47
- spinner=r_status.spinner_name(),
48
- spinner_style=ThemeKey.STATUS_SPINNER,
47
+ self._bottom_live: CropAboveLive | None = None
48
+ self._stream_renderable: RenderableType | None = None
49
+ self._stream_max_height: int = 0
50
+ self._stream_last_height: int = 0
51
+ self._stream_last_width: int = 0
52
+ self._spinner_visible: bool = False
53
+
54
+ self._status_text: ShimmerStatusText = ShimmerStatusText(const.STATUS_DEFAULT_TEXT)
55
+ self._status_spinner: Spinner = BreathingSpinner(
56
+ r_status.spinner_name(),
57
+ text=SingleLine(self._status_text),
58
+ style=ThemeKey.STATUS_SPINNER,
49
59
  )
50
60
 
51
61
  self.session_map: dict[str, SessionStatus] = {}
@@ -235,7 +245,11 @@ class REPLRenderer:
235
245
  def display_task_finish(self, event: events.TaskFinishEvent) -> None:
236
246
  if self.is_sub_agent_session(event.session_id):
237
247
  session_status = self.session_map.get(event.session_id)
238
- description = session_status.sub_agent_state.sub_agent_desc if session_status and session_status.sub_agent_state else None
248
+ description = (
249
+ session_status.sub_agent_state.sub_agent_desc
250
+ if session_status and session_status.sub_agent_state
251
+ else None
252
+ )
239
253
  panel_style = self.get_session_sub_agent_background(event.session_id)
240
254
  with self.session_print_context(event.session_id):
241
255
  self.print(
@@ -265,16 +279,89 @@ class REPLRenderer:
265
279
 
266
280
  def spinner_start(self) -> None:
267
281
  """Start the spinner animation."""
268
- self._spinner.start()
282
+ self._spinner_visible = True
283
+ self._ensure_bottom_live_started()
284
+ self._refresh_bottom_live()
269
285
 
270
286
  def spinner_stop(self) -> None:
271
287
  """Stop the spinner animation."""
272
- self._spinner.stop()
288
+ self._spinner_visible = False
289
+ self._refresh_bottom_live()
273
290
 
274
291
  def spinner_update(self, status_text: str | Text, right_text: Text | None = None) -> None:
275
292
  """Update the spinner status text with optional right-aligned text."""
276
- self._spinner.update(ShimmerStatusText(status_text, right_text))
293
+ self._status_text = ShimmerStatusText(status_text, right_text)
294
+ self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
295
+ self._refresh_bottom_live()
277
296
 
278
297
  def spinner_renderable(self) -> Spinner:
279
298
  """Return the spinner's renderable for embedding in other components."""
280
- return self._spinner.renderable
299
+ return self._status_spinner
300
+
301
+ def set_stream_renderable(self, renderable: RenderableType | None) -> None:
302
+ """Set the current streaming renderable displayed above the status line."""
303
+
304
+ if renderable is None:
305
+ self._stream_renderable = None
306
+ self._stream_max_height = 0
307
+ self._stream_last_height = 0
308
+ self._stream_last_width = 0
309
+ self._refresh_bottom_live()
310
+ return
311
+
312
+ self._ensure_bottom_live_started()
313
+ self._stream_renderable = renderable
314
+
315
+ height = len(self.console.render_lines(renderable, self.console.options, pad=False))
316
+ self._stream_last_height = height
317
+ self._stream_last_width = self.console.size.width
318
+ self._stream_max_height = max(self._stream_max_height, height)
319
+ self._refresh_bottom_live()
320
+
321
+ def _ensure_bottom_live_started(self) -> None:
322
+ if self._bottom_live is not None:
323
+ return
324
+ self._bottom_live = CropAboveLive(
325
+ Text(""),
326
+ console=self.console,
327
+ refresh_per_second=30,
328
+ transient=True,
329
+ redirect_stdout=False,
330
+ redirect_stderr=False,
331
+ )
332
+ self._bottom_live.start()
333
+
334
+ def _bottom_renderable(self) -> RenderableType:
335
+ stream = self._stream_renderable
336
+ if stream is not None:
337
+ current_width = self.console.size.width
338
+ if self._stream_last_width != current_width:
339
+ height = len(self.console.render_lines(stream, self.console.options, pad=False))
340
+ self._stream_last_height = height
341
+ self._stream_last_width = current_width
342
+ self._stream_max_height = max(self._stream_max_height, height)
343
+ else:
344
+ height = self._stream_last_height
345
+
346
+ pad_lines = max(self._stream_max_height - height, 0)
347
+ if pad_lines:
348
+ stream = Padding(stream, (0, 0, pad_lines, 0))
349
+
350
+ stream_part: RenderableType = stream if stream is not None else Group()
351
+ gap_part: RenderableType = Text("") if self._spinner_visible else Group()
352
+ status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
353
+ return Group(stream_part, gap_part, status_part)
354
+
355
+ def _refresh_bottom_live(self) -> None:
356
+ if self._bottom_live is None:
357
+ return
358
+ self._bottom_live.update(self._bottom_renderable(), refresh=True)
359
+
360
+ def stop_bottom_live(self) -> None:
361
+ if self._bottom_live is None:
362
+ return
363
+ with contextlib.suppress(Exception):
364
+ # Avoid cursor restore when stopping right before prompt_toolkit.
365
+ self._bottom_live.transient = False
366
+ self._bottom_live.stop()
367
+ self._bottom_live = None
@@ -4,9 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
+ from rich.cells import cell_len
7
8
  from rich.console import ConsoleRenderable, RichCast
8
9
  from rich.jupyter import JupyterMixin
9
- from rich.measure import Measurement, measure_renderables
10
+ from rich.measure import Measurement
10
11
  from rich.segment import Segment
11
12
  from rich.style import StyleType
12
13
 
@@ -58,17 +59,29 @@ class CodePanel(JupyterMixin):
58
59
  self.expand = expand
59
60
  self.padding = padding
60
61
 
62
+ @staticmethod
63
+ def _measure_max_line_cells(lines: list[list[Segment]]) -> int:
64
+ max_cells = 0
65
+ for line in lines:
66
+ plain = "".join(segment.text for segment in line).rstrip()
67
+ max_cells = max(max_cells, cell_len(plain))
68
+ return max_cells
69
+
61
70
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
62
71
  border_style = console.get_style(self.border_style)
63
72
  max_width = options.max_width
64
73
  pad = self.padding
65
74
 
75
+ max_content_width = max(max_width - pad * 2, 1)
76
+
66
77
  # Measure the content width (account for padding)
67
78
  if self.expand:
68
- content_width = max_width - pad * 2
79
+ content_width = max_content_width
69
80
  else:
70
- content_width = console.measure(self.renderable, options=options.update(width=max_width - pad * 2)).maximum
71
- content_width = min(content_width, max_width - pad * 2)
81
+ probe_options = options.update(width=max_content_width)
82
+ probe_lines = console.render_lines(self.renderable, probe_options, pad=False)
83
+ content_width = self._measure_max_line_cells(probe_lines)
84
+ content_width = max(1, min(content_width, max_content_width))
72
85
 
73
86
  # Render content lines
74
87
  child_options = options.update(width=content_width)
@@ -108,5 +121,11 @@ class CodePanel(JupyterMixin):
108
121
  def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
109
122
  if self.expand:
110
123
  return Measurement(options.max_width, options.max_width)
111
- width = measure_renderables(console, options, [self.renderable]).maximum + self.padding * 2
124
+ max_width = options.max_width
125
+ max_content_width = max(max_width - self.padding * 2, 1)
126
+ probe_options = options.update(width=max_content_width)
127
+ probe_lines = console.render_lines(self.renderable, probe_options, pad=False)
128
+ content_width = self._measure_max_line_cells(probe_lines)
129
+ content_width = max(1, min(content_width, max_content_width))
130
+ width = content_width + self.padding * 2
112
131
  return Measurement(width, width)
@@ -63,3 +63,20 @@ class CropAboveLive(Live):
63
63
 
64
64
  def update(self, renderable: RenderableType, refresh: bool = True) -> None: # type: ignore[override]
65
65
  super().update(CropAbove(renderable, style=self._crop_style), refresh=refresh)
66
+
67
+
68
+ class SingleLine:
69
+ """Render only the first line of a renderable.
70
+
71
+ This is used to ensure dynamic UI elements (spinners / status) never wrap
72
+ to multiple lines, which would appear as a vertical "jump".
73
+ """
74
+
75
+ def __init__(self, renderable: RenderableType) -> None:
76
+ self.renderable = renderable
77
+
78
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
79
+ line_options = options.update(no_wrap=True, overflow="ellipsis", height=1)
80
+ lines = console.render_lines(self.renderable, line_options, pad=False)
81
+ if lines:
82
+ yield from lines[0]