klaude-code 1.5.0__tar.gz → 1.7.0__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 (219) hide show
  1. {klaude_code-1.5.0 → klaude_code-1.7.0}/PKG-INFO +33 -5
  2. {klaude_code-1.5.0 → klaude_code-1.7.0}/README.md +31 -4
  3. {klaude_code-1.5.0 → klaude_code-1.7.0}/pyproject.toml +3 -1
  4. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/list_model.py +55 -4
  5. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/main.py +3 -56
  6. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/session_cmd.py +3 -2
  7. klaude_code-1.7.0/src/klaude_code/command/fork_session_cmd.py +260 -0
  8. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/refresh_cmd.py +4 -4
  9. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/resume_cmd.py +21 -11
  10. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/assets/builtin_config.yaml +37 -2
  11. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/builtin_config.py +1 -0
  12. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/config.py +14 -0
  13. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/thinking.py +14 -0
  14. klaude_code-1.7.0/src/klaude_code/llm/anthropic/client.py +233 -0
  15. klaude_code-1.7.0/src/klaude_code/llm/bedrock/__init__.py +3 -0
  16. klaude_code-1.7.0/src/klaude_code/llm/bedrock/client.py +60 -0
  17. klaude_code-1.7.0/src/klaude_code/llm/google/__init__.py +3 -0
  18. klaude_code-1.7.0/src/klaude_code/llm/google/client.py +309 -0
  19. klaude_code-1.7.0/src/klaude_code/llm/google/input.py +215 -0
  20. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/registry.py +10 -5
  21. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/usage.py +1 -1
  22. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/llm_param.py +9 -0
  23. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/__init__.py +2 -2
  24. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/selector.py +32 -4
  25. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/session.py +20 -12
  26. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/event_handler.py +22 -32
  27. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/renderer.py +1 -1
  28. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/developer.py +2 -2
  29. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/metadata.py +8 -0
  30. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/markdown.py +41 -9
  31. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/status.py +83 -22
  32. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/selector.py +72 -3
  33. klaude_code-1.5.0/src/klaude_code/command/fork_session_cmd.py +0 -42
  34. klaude_code-1.5.0/src/klaude_code/llm/anthropic/client.py +0 -220
  35. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/__init__.py +0 -0
  36. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/__init__.py +0 -0
  37. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/codex/__init__.py +0 -0
  38. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/codex/exceptions.py +0 -0
  39. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/codex/jwt_utils.py +0 -0
  40. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/codex/oauth.py +0 -0
  41. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/auth/codex/token_manager.py +0 -0
  42. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/__init__.py +0 -0
  43. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/auth_cmd.py +0 -0
  44. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/config_cmd.py +0 -0
  45. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/debug.py +0 -0
  46. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/runtime.py +0 -0
  47. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/cli/self_update.py +0 -0
  48. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/__init__.py +0 -0
  49. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/clear_cmd.py +0 -0
  50. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/command_abc.py +0 -0
  51. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/debug_cmd.py +0 -0
  52. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/export_cmd.py +0 -0
  53. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/export_online_cmd.py +0 -0
  54. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/help_cmd.py +0 -0
  55. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/model_cmd.py +0 -0
  56. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/model_select.py +0 -0
  57. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/prompt-init.md +0 -0
  58. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/prompt-jj-describe.md +0 -0
  59. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/prompt_command.py +0 -0
  60. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/registry.py +0 -0
  61. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/release_notes_cmd.py +0 -0
  62. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/status_cmd.py +0 -0
  63. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/terminal_setup_cmd.py +0 -0
  64. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/command/thinking_cmd.py +0 -0
  65. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/__init__.py +0 -0
  66. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/assets/__init__.py +0 -0
  67. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/config/select_model.py +0 -0
  68. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/const.py +0 -0
  69. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/__init__.py +0 -0
  70. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/agent.py +0 -0
  71. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/executor.py +0 -0
  72. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/manager/__init__.py +0 -0
  73. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/manager/llm_clients.py +0 -0
  74. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/manager/llm_clients_builder.py +0 -0
  75. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/manager/sub_agent_manager.py +0 -0
  76. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompt.py +0 -0
  77. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-claude-code.md +0 -0
  78. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -0
  79. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +0 -0
  80. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-codex.md +0 -0
  81. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-gemini.md +0 -0
  82. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-minimal.md +0 -0
  83. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-sub-agent-explore.md +0 -0
  84. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -0
  85. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-sub-agent-web.md +0 -0
  86. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/prompts/prompt-sub-agent.md +0 -0
  87. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/reminders.py +0 -0
  88. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/task.py +0 -0
  89. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/__init__.py +0 -0
  90. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/__init__.py +0 -0
  91. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/_utils.py +0 -0
  92. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/apply_patch.py +0 -0
  93. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/apply_patch_tool.md +0 -0
  94. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/apply_patch_tool.py +0 -0
  95. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/diff_builder.py +0 -0
  96. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/edit_tool.md +0 -0
  97. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/edit_tool.py +0 -0
  98. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/move_tool.md +0 -0
  99. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/move_tool.py +0 -0
  100. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/read_tool.md +0 -0
  101. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/read_tool.py +0 -0
  102. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/write_tool.md +0 -0
  103. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/file/write_tool.py +0 -0
  104. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/report_back_tool.py +0 -0
  105. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/shell/__init__.py +0 -0
  106. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/shell/bash_tool.md +0 -0
  107. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/shell/bash_tool.py +0 -0
  108. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/shell/command_safety.py +0 -0
  109. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/skill/__init__.py +0 -0
  110. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/skill/skill_tool.md +0 -0
  111. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/skill/skill_tool.py +0 -0
  112. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/sub_agent_tool.py +0 -0
  113. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/__init__.py +0 -0
  114. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/todo_write_tool.md +0 -0
  115. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/todo_write_tool.py +0 -0
  116. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/todo_write_tool_raw.md +0 -0
  117. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/update_plan_tool.md +0 -0
  118. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/todo/update_plan_tool.py +0 -0
  119. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/tool_abc.py +0 -0
  120. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/tool_context.py +0 -0
  121. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/tool_registry.py +0 -0
  122. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/tool_runner.py +0 -0
  123. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/truncation.py +0 -0
  124. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/__init__.py +0 -0
  125. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/mermaid_tool.md +0 -0
  126. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/mermaid_tool.py +0 -0
  127. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/web_fetch_tool.md +0 -0
  128. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/web_fetch_tool.py +0 -0
  129. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/web_search_tool.md +0 -0
  130. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/tool/web/web_search_tool.py +0 -0
  131. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/core/turn.py +0 -0
  132. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/__init__.py +0 -0
  133. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/anthropic/__init__.py +0 -0
  134. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/anthropic/input.py +0 -0
  135. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/client.py +0 -0
  136. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/codex/__init__.py +0 -0
  137. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/codex/client.py +0 -0
  138. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/input_common.py +0 -0
  139. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openai_compatible/__init__.py +0 -0
  140. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openai_compatible/client.py +0 -0
  141. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openai_compatible/input.py +0 -0
  142. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openai_compatible/stream.py +0 -0
  143. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -0
  144. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openrouter/__init__.py +0 -0
  145. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openrouter/client.py +0 -0
  146. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openrouter/input.py +0 -0
  147. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/openrouter/reasoning.py +0 -0
  148. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/responses/__init__.py +0 -0
  149. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/responses/client.py +0 -0
  150. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/llm/responses/input.py +0 -0
  151. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/__init__.py +0 -0
  152. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/commands.py +0 -0
  153. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/events.py +0 -0
  154. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/model.py +0 -0
  155. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/op.py +0 -0
  156. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/op_handler.py +0 -0
  157. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/sub_agent/__init__.py +0 -0
  158. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/sub_agent/explore.py +0 -0
  159. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/sub_agent/oracle.py +0 -0
  160. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/sub_agent/task.py +0 -0
  161. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/sub_agent/web.py +0 -0
  162. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/protocol/tools.py +0 -0
  163. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/codec.py +0 -0
  164. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/export.py +0 -0
  165. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/store.py +0 -0
  166. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/templates/export_session.html +0 -0
  167. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/session/templates/mermaid_viewer.html +0 -0
  168. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/__init__.py +0 -0
  169. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/assets/deslop/SKILL.md +0 -0
  170. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/assets/dev-docs/SKILL.md +0 -0
  171. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/assets/handoff/SKILL.md +0 -0
  172. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/assets/jj-workspace/SKILL.md +0 -0
  173. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/assets/skill-creator/SKILL.md +0 -0
  174. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/loader.py +0 -0
  175. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/manager.py +0 -0
  176. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/skill/system_skills.py +0 -0
  177. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/trace/__init__.py +0 -0
  178. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/trace/log.py +0 -0
  179. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/__init__.py +0 -0
  180. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/core/__init__.py +0 -0
  181. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/core/display.py +0 -0
  182. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/core/input.py +0 -0
  183. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/core/stage_manager.py +0 -0
  184. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/__init__.py +0 -0
  185. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/debug/__init__.py +0 -0
  186. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/debug/display.py +0 -0
  187. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/exec/__init__.py +0 -0
  188. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/exec/display.py +0 -0
  189. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/__init__.py +0 -0
  190. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/clipboard.py +0 -0
  191. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/completers.py +0 -0
  192. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/display.py +0 -0
  193. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/input_prompt_toolkit.py +0 -0
  194. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/modes/repl/key_bindings.py +0 -0
  195. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/__init__.py +0 -0
  196. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/assistant.py +0 -0
  197. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/bash_syntax.py +0 -0
  198. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/common.py +0 -0
  199. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/diffs.py +0 -0
  200. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/errors.py +0 -0
  201. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/mermaid_viewer.py +0 -0
  202. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/sub_agent.py +0 -0
  203. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/thinking.py +0 -0
  204. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/tools.py +0 -0
  205. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/renderers/user_input.py +0 -0
  206. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/__init__.py +0 -0
  207. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/cjk_wrap.py +0 -0
  208. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/code_panel.py +0 -0
  209. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/live.py +0 -0
  210. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/quote.py +0 -0
  211. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/searchable_text.py +0 -0
  212. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/rich/theme.py +0 -0
  213. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/__init__.py +0 -0
  214. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/color.py +0 -0
  215. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/control.py +0 -0
  216. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/notifier.py +0 -0
  217. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/terminal/progress_bar.py +0 -0
  218. {klaude_code-1.5.0 → klaude_code-1.7.0}/src/klaude_code/ui/utils/__init__.py +0 -0
  219. {klaude_code-1.5.0 → klaude_code-1.7.0}/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.5.0
3
+ Version: 1.7.0
4
4
  Summary: Minimal code agent CLI
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: google-genai>=1.56.0
9
10
  Requires-Dist: markdown-it-py>=4.0.0
10
11
  Requires-Dist: openai>=1.102.0
11
12
  Requires-Dist: pillow>=12.0.0
@@ -34,6 +35,10 @@ Minimal code agent CLI.
34
35
  - **Output truncation**: Large outputs saved to file system with snapshot links
35
36
  - **Skills**: Built-in + user + project Agent Skills (with implicit invocation by Skill tool or explicit invocation by typing `$`)
36
37
  - **Sessions**: Resumable with `--continue`
38
+ - **Cost tracking**: Automatic API cost calculation and display (USD/CNY)
39
+ - **Version update check**: Background PyPI version check with upgrade prompts
40
+ - **Terminal title**: Shows current directory and model name
41
+ - **Mermaid diagrams**: Interactive local HTML viewer with zoom, pan, and SVG export
37
42
  - **Extras**: Slash commands, sub-agents, image paste, terminal notifications, auto-theming
38
43
 
39
44
  ## Installation
@@ -77,6 +82,7 @@ klaude [--model <name>] [--select-model]
77
82
  - `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
78
83
  - `--continue`/`-c`: Resume the most recent session.
79
84
  - `--resume`/`-r`: Select a session to resume for this project.
85
+ - `--resume-by-id <id>`: Resume a session by its ID directly.
80
86
  - `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit) and no system prompts.
81
87
 
82
88
  **Model selection behavior:**
@@ -251,12 +257,18 @@ klaude session clean-all
251
257
 
252
258
  Inside the interactive session (`klaude`), use these commands to streamline your workflow:
253
259
 
254
- - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
255
- - `/export` - Export last assistant message to a temp Markdown file.
256
- - `/init` - Bootstrap a new project structure or module.
257
260
  - `/model` - Switch the active LLM during the session.
261
+ - `/thinking` - Configure model thinking/reasoning level.
258
262
  - `/clear` - Clear the current conversation context.
259
- - `/diff` - Show local git diff changes.
263
+ - `/status` - Show session usage statistics (cost, tokens, model breakdown).
264
+ - `/resume` - Select and resume a previous session.
265
+ - `/fork-session` - Fork current session to a new session ID (supports interactive fork point selection).
266
+ - `/export` - Export last assistant message to a temp Markdown file.
267
+ - `/export-online` - Export and deploy session to surge.sh as a static webpage.
268
+ - `/debug [filters]` - Toggle debug mode and configure debug filters.
269
+ - `/init` - Bootstrap a new project structure or module.
270
+ - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
271
+ - `/terminal-setup` - Configure terminal for Shift+Enter support.
260
272
  - `/help` - List all available commands.
261
273
 
262
274
 
@@ -267,6 +279,8 @@ Inside the interactive session (`klaude`), use these commands to streamline your
267
279
  | `Enter` | Submit input |
268
280
  | `Shift+Enter` | Insert newline (requires `/terminal-setup`) |
269
281
  | `Ctrl+J` | Insert newline |
282
+ | `Ctrl+L` | Open model picker overlay |
283
+ | `Ctrl+T` | Open thinking level picker overlay |
270
284
  | `Ctrl+V` | Paste image from clipboard |
271
285
  | `Left/Right` | Move cursor (wraps across lines) |
272
286
  | `Backspace` | Delete character or selected text |
@@ -290,4 +304,18 @@ echo "generate quicksort in python" | klaude exec --model gpt-5.1
290
304
 
291
305
  # Partial/ambiguous name opens the interactive selector (filtered)
292
306
  echo "generate quicksort in python" | klaude exec --model gpt
307
+
308
+ # Stream all events as JSON lines (for programmatic processing)
309
+ klaude exec "what is 2+2?" --stream-json
293
310
  ```
311
+
312
+ ### Sub-Agents
313
+
314
+ The main agent can spawn specialized sub-agents for specific tasks:
315
+
316
+ | Sub-Agent | Purpose |
317
+ |-----------|---------|
318
+ | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
319
+ | **Task** | Handle complex multi-step tasks autonomously |
320
+ | **WebAgent** | Search the web, fetch pages, and analyze content |
321
+ | **Oracle** | Advanced reasoning advisor for code reviews, architecture planning, and bug analysis |
@@ -14,6 +14,10 @@ Minimal code agent CLI.
14
14
  - **Output truncation**: Large outputs saved to file system with snapshot links
15
15
  - **Skills**: Built-in + user + project Agent Skills (with implicit invocation by Skill tool or explicit invocation by typing `$`)
16
16
  - **Sessions**: Resumable with `--continue`
17
+ - **Cost tracking**: Automatic API cost calculation and display (USD/CNY)
18
+ - **Version update check**: Background PyPI version check with upgrade prompts
19
+ - **Terminal title**: Shows current directory and model name
20
+ - **Mermaid diagrams**: Interactive local HTML viewer with zoom, pan, and SVG export
17
21
  - **Extras**: Slash commands, sub-agents, image paste, terminal notifications, auto-theming
18
22
 
19
23
  ## Installation
@@ -57,6 +61,7 @@ klaude [--model <name>] [--select-model]
57
61
  - `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
58
62
  - `--continue`/`-c`: Resume the most recent session.
59
63
  - `--resume`/`-r`: Select a session to resume for this project.
64
+ - `--resume-by-id <id>`: Resume a session by its ID directly.
60
65
  - `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit) and no system prompts.
61
66
 
62
67
  **Model selection behavior:**
@@ -231,12 +236,18 @@ klaude session clean-all
231
236
 
232
237
  Inside the interactive session (`klaude`), use these commands to streamline your workflow:
233
238
 
234
- - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
235
- - `/export` - Export last assistant message to a temp Markdown file.
236
- - `/init` - Bootstrap a new project structure or module.
237
239
  - `/model` - Switch the active LLM during the session.
240
+ - `/thinking` - Configure model thinking/reasoning level.
238
241
  - `/clear` - Clear the current conversation context.
239
- - `/diff` - Show local git diff changes.
242
+ - `/status` - Show session usage statistics (cost, tokens, model breakdown).
243
+ - `/resume` - Select and resume a previous session.
244
+ - `/fork-session` - Fork current session to a new session ID (supports interactive fork point selection).
245
+ - `/export` - Export last assistant message to a temp Markdown file.
246
+ - `/export-online` - Export and deploy session to surge.sh as a static webpage.
247
+ - `/debug [filters]` - Toggle debug mode and configure debug filters.
248
+ - `/init` - Bootstrap a new project structure or module.
249
+ - `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
250
+ - `/terminal-setup` - Configure terminal for Shift+Enter support.
240
251
  - `/help` - List all available commands.
241
252
 
242
253
 
@@ -247,6 +258,8 @@ Inside the interactive session (`klaude`), use these commands to streamline your
247
258
  | `Enter` | Submit input |
248
259
  | `Shift+Enter` | Insert newline (requires `/terminal-setup`) |
249
260
  | `Ctrl+J` | Insert newline |
261
+ | `Ctrl+L` | Open model picker overlay |
262
+ | `Ctrl+T` | Open thinking level picker overlay |
250
263
  | `Ctrl+V` | Paste image from clipboard |
251
264
  | `Left/Right` | Move cursor (wraps across lines) |
252
265
  | `Backspace` | Delete character or selected text |
@@ -270,4 +283,18 @@ echo "generate quicksort in python" | klaude exec --model gpt-5.1
270
283
 
271
284
  # Partial/ambiguous name opens the interactive selector (filtered)
272
285
  echo "generate quicksort in python" | klaude exec --model gpt
286
+
287
+ # Stream all events as JSON lines (for programmatic processing)
288
+ klaude exec "what is 2+2?" --stream-json
273
289
  ```
290
+
291
+ ### Sub-Agents
292
+
293
+ The main agent can spawn specialized sub-agents for specific tasks:
294
+
295
+ | Sub-Agent | Purpose |
296
+ |-----------|---------|
297
+ | **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
298
+ | **Task** | Handle complex multi-step tasks autonomously |
299
+ | **WebAgent** | Search the web, fetch pages, and analyze content |
300
+ | **Oracle** | Advanced reasoning advisor for code reviews, architecture planning, and bug analysis |
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "klaude-code"
7
- version = "1.5.0"
7
+ version = "1.7.0"
8
8
  description = "Minimal code agent CLI"
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
+ "google-genai>=1.56.0",
16
17
  "markdown-it-py>=4.0.0",
17
18
  "openai>=1.102.0",
18
19
  "pillow>=12.0.0",
@@ -33,6 +34,7 @@ module-name = "klaude_code"
33
34
 
34
35
  [dependency-groups]
35
36
  dev = [
37
+ "hypothesis>=6.148.8",
36
38
  "import-linter>=2.6",
37
39
  "pyright>=1.1.407",
38
40
  "pytest>=8.4.1",
@@ -6,7 +6,7 @@ from rich.table import Table
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code.config import Config
9
- from klaude_code.config.config import ModelConfig, ProviderConfig
9
+ from klaude_code.config.config import ModelConfig, ProviderConfig, parse_env_var_syntax
10
10
  from klaude_code.protocol.llm_param import LLMClientProtocol
11
11
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
12
12
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
@@ -94,6 +94,29 @@ def format_api_key_display(provider: ProviderConfig) -> Text:
94
94
  return Text("N/A")
95
95
 
96
96
 
97
+ def format_env_var_display(value: str | None) -> Text:
98
+ """Format environment variable display with warning if not set."""
99
+ env_var, resolved = parse_env_var_syntax(value)
100
+
101
+ if env_var:
102
+ # Using ${ENV_VAR} syntax
103
+ if resolved:
104
+ return Text.assemble(
105
+ (f"${{{env_var}}} = ", "dim"),
106
+ (mask_api_key(resolved), ""),
107
+ )
108
+ else:
109
+ return Text.assemble(
110
+ (f"${{{env_var}}} ", ""),
111
+ ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
112
+ )
113
+ elif value:
114
+ # Plain value
115
+ return Text(mask_api_key(value))
116
+ else:
117
+ return Text("N/A")
118
+
119
+
97
120
  def _get_model_params_display(model: ModelConfig) -> list[Text]:
98
121
  """Get display elements for model parameters."""
99
122
  params: list[Text] = []
@@ -162,15 +185,43 @@ def display_models_and_providers(config: Config):
162
185
  format_api_key_display(provider),
163
186
  )
164
187
 
188
+ # AWS Bedrock parameters
189
+ if provider.protocol == LLMClientProtocol.BEDROCK:
190
+ if provider.aws_access_key:
191
+ provider_info.add_row(
192
+ Text("AWS Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
193
+ format_env_var_display(provider.aws_access_key),
194
+ )
195
+ if provider.aws_secret_key:
196
+ provider_info.add_row(
197
+ Text("AWS Secret:", style=ThemeKey.CONFIG_PARAM_LABEL),
198
+ format_env_var_display(provider.aws_secret_key),
199
+ )
200
+ if provider.aws_region:
201
+ provider_info.add_row(
202
+ Text("AWS Region:", style=ThemeKey.CONFIG_PARAM_LABEL),
203
+ format_env_var_display(provider.aws_region),
204
+ )
205
+ if provider.aws_session_token:
206
+ provider_info.add_row(
207
+ Text("AWS Token:", style=ThemeKey.CONFIG_PARAM_LABEL),
208
+ format_env_var_display(provider.aws_session_token),
209
+ )
210
+ if provider.aws_profile:
211
+ provider_info.add_row(
212
+ Text("AWS Profile:", style=ThemeKey.CONFIG_PARAM_LABEL),
213
+ format_env_var_display(provider.aws_profile),
214
+ )
215
+
165
216
  # Check if provider has valid API key
166
217
  provider_available = not provider.is_api_key_missing()
167
218
 
168
219
  # Models table for this provider
169
220
  models_table = Table.grid(padding=(0, 1), expand=True)
170
221
  models_table.add_column(width=2, no_wrap=True) # Status
171
- models_table.add_column(overflow="fold", ratio=1) # Name
172
- models_table.add_column(overflow="fold", ratio=2) # Model
173
- models_table.add_column(overflow="fold", ratio=3) # Params
222
+ models_table.add_column(overflow="fold", ratio=2) # Name
223
+ models_table.add_column(overflow="fold", ratio=3) # Model
224
+ models_table.add_column(overflow="fold", ratio=4) # Params
174
225
 
175
226
  # Add header
176
227
  models_table.add_row(
@@ -11,64 +11,11 @@ from klaude_code.cli.config_cmd import register_config_commands
11
11
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
12
12
  from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
13
13
  from klaude_code.cli.session_cmd import register_session_commands
14
- from klaude_code.session import Session, build_session_select_options
14
+ from klaude_code.command.resume_cmd import select_session_sync
15
+ from klaude_code.session import Session
15
16
  from klaude_code.trace import DebugType, prepare_debug_log_file
16
17
 
17
18
 
18
- def select_session_interactive() -> str | None:
19
- """Interactive session selection for CLI.
20
-
21
- Returns:
22
- Selected session ID, or None if no session selected or no sessions exist.
23
- """
24
- from klaude_code.trace import log
25
-
26
- options = build_session_select_options()
27
- if not options:
28
- log("No sessions found for this project.")
29
- return None
30
-
31
- from prompt_toolkit.styles import Style
32
-
33
- from klaude_code.ui.terminal.selector import SelectItem, select_one
34
-
35
- items: list[SelectItem[str]] = []
36
- for opt in options:
37
- title = [
38
- ("class:msg", f"{opt.first_user_message}\n"),
39
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
40
- ]
41
- items.append(
42
- SelectItem(
43
- title=title,
44
- value=opt.session_id,
45
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
46
- )
47
- )
48
-
49
- try:
50
- return select_one(
51
- message="Select a session to resume:",
52
- items=items,
53
- pointer="→",
54
- style=Style(
55
- [
56
- ("msg", ""),
57
- ("meta", "fg:ansibrightblack"),
58
- ("pointer", "bold fg:ansigreen"),
59
- ("highlighted", "fg:ansigreen"),
60
- ("search_prefix", "fg:ansibrightblack"),
61
- ("search_success", "noinherit fg:ansigreen"),
62
- ("search_none", "noinherit fg:ansired"),
63
- ("question", "bold"),
64
- ("text", ""),
65
- ]
66
- ),
67
- )
68
- except KeyboardInterrupt:
69
- return None
70
-
71
-
72
19
  def set_terminal_title(title: str) -> None:
73
20
  """Set terminal window title using ANSI escape sequence."""
74
21
  # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
@@ -361,7 +308,7 @@ def main_callback(
361
308
  session_id: str | None = None
362
309
 
363
310
  if resume:
364
- session_id = select_session_interactive()
311
+ session_id = select_session_sync()
365
312
  if session_id is None:
366
313
  return
367
314
  # If user didn't pick, allow fallback to --continue
@@ -22,8 +22,9 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
22
22
  log(f"Sessions to delete ({len(sessions)}):")
23
23
  for s in sessions:
24
24
  msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
25
- first_msg = (s.first_user_message or "").strip().replace("\n", " ")[:50]
26
- if len(s.first_user_message or "") > 50:
25
+ first_msg_text = s.user_messages[0] if s.user_messages else ""
26
+ first_msg = first_msg_text.strip().replace("\n", " ")[:50]
27
+ if len(first_msg_text) > 50:
27
28
  first_msg += "..."
28
29
  log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
29
30
 
@@ -0,0 +1,260 @@
1
+ import asyncio
2
+ import sys
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from prompt_toolkit.styles import Style
7
+
8
+ from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
9
+ from klaude_code.protocol import commands, events, model
10
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
11
+
12
+ FORK_SELECT_STYLE = Style(
13
+ [
14
+ ("msg", ""),
15
+ ("meta", "fg:ansibrightblack"),
16
+ ("separator", "fg:ansibrightblack"),
17
+ ("assistant", "fg:ansiblue"),
18
+ ("pointer", "bold fg:ansigreen"),
19
+ ("search_prefix", "fg:ansibrightblack"),
20
+ ("search_success", "noinherit fg:ansigreen"),
21
+ ("search_none", "noinherit fg:ansired"),
22
+ ("question", "bold"),
23
+ ("text", ""),
24
+ ]
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class ForkPoint:
30
+ """A fork point in conversation history."""
31
+
32
+ history_index: int | None # None means fork entire conversation
33
+ user_message: str
34
+ tool_call_stats: dict[str, int] # tool_name -> count
35
+ last_assistant_summary: str
36
+
37
+
38
+ def _truncate(text: str, max_len: int = 60) -> str:
39
+ """Truncate text to max_len, adding ellipsis if needed."""
40
+ text = text.replace("\n", " ").strip()
41
+ if len(text) <= max_len:
42
+ return text
43
+ return text[: max_len - 3] + "..."
44
+
45
+
46
+ def _build_fork_points(conversation_history: list[model.ConversationItem]) -> list[ForkPoint]:
47
+ """Build list of fork points from conversation history.
48
+
49
+ Fork points are:
50
+ - Each UserMessageItem position (for UI display, including first which would be empty session)
51
+ - The end of the conversation (fork entire conversation)
52
+ """
53
+ fork_points: list[ForkPoint] = []
54
+ user_indices: list[int] = []
55
+
56
+ for i, item in enumerate(conversation_history):
57
+ if isinstance(item, model.UserMessageItem):
58
+ user_indices.append(i)
59
+
60
+ # For each UserMessageItem, create a fork point at that position
61
+ for i, user_idx in enumerate(user_indices):
62
+ user_item = conversation_history[user_idx]
63
+ assert isinstance(user_item, model.UserMessageItem)
64
+
65
+ # Find the end of this "task" (next UserMessageItem or end of history)
66
+ next_user_idx = user_indices[i + 1] if i + 1 < len(user_indices) else len(conversation_history)
67
+
68
+ # Count tool calls by name and find last assistant message in this segment
69
+ tool_stats: dict[str, int] = {}
70
+ last_assistant_content = ""
71
+ for j in range(user_idx, next_user_idx):
72
+ item = conversation_history[j]
73
+ if isinstance(item, model.ToolCallItem):
74
+ tool_stats[item.name] = tool_stats.get(item.name, 0) + 1
75
+ elif isinstance(item, model.AssistantMessageItem) and item.content:
76
+ last_assistant_content = item.content
77
+
78
+ fork_points.append(
79
+ ForkPoint(
80
+ history_index=user_idx,
81
+ user_message=user_item.content or "(empty)",
82
+ tool_call_stats=tool_stats,
83
+ last_assistant_summary=_truncate(last_assistant_content) if last_assistant_content else "",
84
+ )
85
+ )
86
+
87
+ # Add the "fork entire conversation" option at the end
88
+ if user_indices:
89
+ fork_points.append(
90
+ ForkPoint(
91
+ history_index=None, # None means fork entire conversation
92
+ user_message="", # No specific message, this represents the end
93
+ tool_call_stats={},
94
+ last_assistant_summary="",
95
+ )
96
+ )
97
+
98
+ return fork_points
99
+
100
+
101
+ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int | None]]:
102
+ """Build SelectItem list from fork points."""
103
+ items: list[SelectItem[int | None]] = []
104
+
105
+ for i, fp in enumerate(fork_points):
106
+ is_first = i == 0
107
+ is_last = i == len(fork_points) - 1
108
+
109
+ # Build the title
110
+ title_parts: list[tuple[str, str]] = []
111
+
112
+ # First line: separator (with special markers for first/last fork points)
113
+ if is_first and not is_last:
114
+ title_parts.append(("class:separator", "----- fork from here (empty session) -----\n\n"))
115
+ elif is_last:
116
+ title_parts.append(("class:separator", "----- fork from here (entire session) -----\n\n"))
117
+ else:
118
+ title_parts.append(("class:separator", "----- fork from here -----\n\n"))
119
+
120
+ if not is_last:
121
+ # Second line: user message
122
+ title_parts.append(("class:msg", f"user: {_truncate(fp.user_message, 70)}\n"))
123
+
124
+ # Third line: tool call stats (if any)
125
+ if fp.tool_call_stats:
126
+ tool_parts = [f"{name} × {count}" for name, count in fp.tool_call_stats.items()]
127
+ title_parts.append(("class:meta", f"tools: {', '.join(tool_parts)}\n"))
128
+
129
+ # Fourth line: last assistant message summary (if any)
130
+ if fp.last_assistant_summary:
131
+ title_parts.append(("class:assistant", f"ai: {fp.last_assistant_summary}\n"))
132
+
133
+ # Empty line at the end
134
+ title_parts.append(("class:text", "\n"))
135
+
136
+ items.append(
137
+ SelectItem(
138
+ title=title_parts,
139
+ value=fp.history_index,
140
+ search_text=fp.user_message if not is_last else "fork entire conversation",
141
+ )
142
+ )
143
+
144
+ return items
145
+
146
+
147
+ def _select_fork_point_sync(fork_points: list[ForkPoint]) -> int | None | Literal["cancelled"]:
148
+ """Interactive fork point selection (sync version for asyncio.to_thread).
149
+
150
+ Returns:
151
+ - int: history index to fork at (exclusive)
152
+ - None: fork entire conversation
153
+ - "cancelled": user cancelled selection
154
+ """
155
+ items = _build_select_items(fork_points)
156
+ if not items:
157
+ return None
158
+
159
+ # Default to the last option (fork entire conversation)
160
+ last_value = items[-1].value
161
+
162
+ # Non-interactive environments default to forking entire conversation
163
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
164
+ return last_value
165
+
166
+ try:
167
+ result = select_one(
168
+ message="Select fork point (messages before this point will be included):",
169
+ items=items,
170
+ pointer="→",
171
+ style=FORK_SELECT_STYLE,
172
+ initial_value=last_value,
173
+ highlight_pointed_item=False,
174
+ )
175
+ if result is None:
176
+ return "cancelled"
177
+ return result
178
+ except KeyboardInterrupt:
179
+ return "cancelled"
180
+
181
+
182
+ class ForkSessionCommand(CommandABC):
183
+ """Fork current session to a new session id and show a resume command."""
184
+
185
+ @property
186
+ def name(self) -> commands.CommandName:
187
+ return commands.CommandName.FORK_SESSION
188
+
189
+ @property
190
+ def summary(self) -> str:
191
+ return "Fork the current session and show a resume-by-id command"
192
+
193
+ @property
194
+ def is_interactive(self) -> bool:
195
+ return True
196
+
197
+ async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
198
+ del user_input # unused
199
+
200
+ if agent.session.messages_count == 0:
201
+ event = events.DeveloperMessageEvent(
202
+ session_id=agent.session.id,
203
+ item=model.DeveloperMessageItem(
204
+ content="(no messages to fork)",
205
+ command_output=model.CommandOutput(command_name=self.name),
206
+ ),
207
+ )
208
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
209
+
210
+ # Build fork points from conversation history
211
+ fork_points = _build_fork_points(agent.session.conversation_history)
212
+
213
+ if not fork_points:
214
+ # Only one user message, just fork entirely
215
+ new_session = agent.session.fork()
216
+ await new_session.wait_for_flush()
217
+
218
+ event = events.DeveloperMessageEvent(
219
+ session_id=agent.session.id,
220
+ item=model.DeveloperMessageItem(
221
+ content=f"Session forked successfully. New session id: {new_session.id}",
222
+ command_output=model.CommandOutput(
223
+ command_name=self.name,
224
+ ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
225
+ ),
226
+ ),
227
+ )
228
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
229
+
230
+ # Interactive selection
231
+ selected = await asyncio.to_thread(_select_fork_point_sync, fork_points)
232
+
233
+ if selected == "cancelled":
234
+ event = events.DeveloperMessageEvent(
235
+ session_id=agent.session.id,
236
+ item=model.DeveloperMessageItem(
237
+ content="(fork cancelled)",
238
+ command_output=model.CommandOutput(command_name=self.name),
239
+ ),
240
+ )
241
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
242
+
243
+ # Perform the fork
244
+ new_session = agent.session.fork(until_index=selected)
245
+ await new_session.wait_for_flush()
246
+
247
+ # Build result message
248
+ fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
249
+
250
+ event = events.DeveloperMessageEvent(
251
+ session_id=agent.session.id,
252
+ item=model.DeveloperMessageItem(
253
+ content=f"Session forked ({fork_description}). New session id: {new_session.id}",
254
+ command_output=model.CommandOutput(
255
+ command_name=self.name,
256
+ ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
257
+ ),
258
+ ),
259
+ )
260
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
@@ -23,7 +23,7 @@ class RefreshTerminalCommand(CommandABC):
23
23
 
24
24
  os.system("cls" if os.name == "nt" else "clear")
25
25
 
26
- result = CommandResult(
26
+ return CommandResult(
27
27
  events=[
28
28
  events.WelcomeEvent(
29
29
  work_dir=str(agent.session.work_dir),
@@ -35,7 +35,7 @@ class RefreshTerminalCommand(CommandABC):
35
35
  updated_at=agent.session.updated_at,
36
36
  is_load=False,
37
37
  ),
38
- ]
38
+ ],
39
+ persist_user_input=False,
40
+ persist_events=False,
39
41
  )
40
-
41
- return result