klaude-code 1.5.0__tar.gz → 1.6.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 (213) hide show
  1. {klaude_code-1.5.0 → klaude_code-1.6.0}/PKG-INFO +1 -1
  2. {klaude_code-1.5.0 → klaude_code-1.6.0}/pyproject.toml +2 -1
  3. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/main.py +3 -56
  4. klaude_code-1.6.0/src/klaude_code/command/fork_session_cmd.py +260 -0
  5. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/refresh_cmd.py +4 -4
  6. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/resume_cmd.py +21 -11
  7. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/usage.py +1 -1
  8. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/__init__.py +2 -2
  9. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/selector.py +32 -4
  10. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/session.py +18 -12
  11. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/event_handler.py +22 -32
  12. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/renderer.py +1 -1
  13. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/developer.py +2 -2
  14. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/metadata.py +8 -0
  15. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/markdown.py +41 -9
  16. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/status.py +83 -22
  17. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/selector.py +72 -3
  18. klaude_code-1.5.0/src/klaude_code/command/fork_session_cmd.py +0 -42
  19. {klaude_code-1.5.0 → klaude_code-1.6.0}/README.md +0 -0
  20. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/__init__.py +0 -0
  21. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/__init__.py +0 -0
  22. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/codex/__init__.py +0 -0
  23. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/codex/exceptions.py +0 -0
  24. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/codex/jwt_utils.py +0 -0
  25. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/codex/oauth.py +0 -0
  26. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/auth/codex/token_manager.py +0 -0
  27. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/__init__.py +0 -0
  28. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/auth_cmd.py +0 -0
  29. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/config_cmd.py +0 -0
  30. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/debug.py +0 -0
  31. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/list_model.py +0 -0
  32. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/runtime.py +0 -0
  33. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/self_update.py +0 -0
  34. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/cli/session_cmd.py +0 -0
  35. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/__init__.py +0 -0
  36. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/clear_cmd.py +0 -0
  37. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/command_abc.py +0 -0
  38. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/debug_cmd.py +0 -0
  39. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/export_cmd.py +0 -0
  40. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/export_online_cmd.py +0 -0
  41. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/help_cmd.py +0 -0
  42. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/model_cmd.py +0 -0
  43. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/model_select.py +0 -0
  44. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/prompt-init.md +0 -0
  45. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/prompt-jj-describe.md +0 -0
  46. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/prompt_command.py +0 -0
  47. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/registry.py +0 -0
  48. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/release_notes_cmd.py +0 -0
  49. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/status_cmd.py +0 -0
  50. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/terminal_setup_cmd.py +0 -0
  51. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/command/thinking_cmd.py +0 -0
  52. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/__init__.py +0 -0
  53. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/assets/__init__.py +0 -0
  54. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/assets/builtin_config.yaml +0 -0
  55. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/builtin_config.py +0 -0
  56. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/config.py +0 -0
  57. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/select_model.py +0 -0
  58. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/config/thinking.py +0 -0
  59. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/const.py +0 -0
  60. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/__init__.py +0 -0
  61. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/agent.py +0 -0
  62. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/executor.py +0 -0
  63. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/manager/__init__.py +0 -0
  64. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/manager/llm_clients.py +0 -0
  65. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/manager/llm_clients_builder.py +0 -0
  66. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/manager/sub_agent_manager.py +0 -0
  67. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompt.py +0 -0
  68. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-claude-code.md +0 -0
  69. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -0
  70. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +0 -0
  71. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-codex.md +0 -0
  72. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-gemini.md +0 -0
  73. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-minimal.md +0 -0
  74. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-sub-agent-explore.md +0 -0
  75. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -0
  76. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-sub-agent-web.md +0 -0
  77. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/prompts/prompt-sub-agent.md +0 -0
  78. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/reminders.py +0 -0
  79. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/task.py +0 -0
  80. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/__init__.py +0 -0
  81. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/__init__.py +0 -0
  82. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/_utils.py +0 -0
  83. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/apply_patch.py +0 -0
  84. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/apply_patch_tool.md +0 -0
  85. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/apply_patch_tool.py +0 -0
  86. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/diff_builder.py +0 -0
  87. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/edit_tool.md +0 -0
  88. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/edit_tool.py +0 -0
  89. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/move_tool.md +0 -0
  90. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/move_tool.py +0 -0
  91. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/read_tool.md +0 -0
  92. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/read_tool.py +0 -0
  93. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/write_tool.md +0 -0
  94. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/file/write_tool.py +0 -0
  95. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/report_back_tool.py +0 -0
  96. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/shell/__init__.py +0 -0
  97. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/shell/bash_tool.md +0 -0
  98. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/shell/bash_tool.py +0 -0
  99. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/shell/command_safety.py +0 -0
  100. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/skill/__init__.py +0 -0
  101. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/skill/skill_tool.md +0 -0
  102. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/skill/skill_tool.py +0 -0
  103. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/sub_agent_tool.py +0 -0
  104. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/__init__.py +0 -0
  105. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/todo_write_tool.md +0 -0
  106. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/todo_write_tool.py +0 -0
  107. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/todo_write_tool_raw.md +0 -0
  108. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/update_plan_tool.md +0 -0
  109. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/todo/update_plan_tool.py +0 -0
  110. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/tool_abc.py +0 -0
  111. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/tool_context.py +0 -0
  112. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/tool_registry.py +0 -0
  113. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/tool_runner.py +0 -0
  114. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/truncation.py +0 -0
  115. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/__init__.py +0 -0
  116. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/mermaid_tool.md +0 -0
  117. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/mermaid_tool.py +0 -0
  118. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/web_fetch_tool.md +0 -0
  119. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/web_fetch_tool.py +0 -0
  120. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/web_search_tool.md +0 -0
  121. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/tool/web/web_search_tool.py +0 -0
  122. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/core/turn.py +0 -0
  123. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/__init__.py +0 -0
  124. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/anthropic/__init__.py +0 -0
  125. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/anthropic/client.py +0 -0
  126. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/anthropic/input.py +0 -0
  127. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/client.py +0 -0
  128. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/codex/__init__.py +0 -0
  129. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/codex/client.py +0 -0
  130. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/input_common.py +0 -0
  131. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openai_compatible/__init__.py +0 -0
  132. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openai_compatible/client.py +0 -0
  133. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openai_compatible/input.py +0 -0
  134. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openai_compatible/stream.py +0 -0
  135. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -0
  136. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openrouter/__init__.py +0 -0
  137. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openrouter/client.py +0 -0
  138. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openrouter/input.py +0 -0
  139. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/openrouter/reasoning.py +0 -0
  140. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/registry.py +0 -0
  141. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/responses/__init__.py +0 -0
  142. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/responses/client.py +0 -0
  143. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/llm/responses/input.py +0 -0
  144. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/__init__.py +0 -0
  145. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/commands.py +0 -0
  146. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/events.py +0 -0
  147. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/llm_param.py +0 -0
  148. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/model.py +0 -0
  149. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/op.py +0 -0
  150. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/op_handler.py +0 -0
  151. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/sub_agent/__init__.py +0 -0
  152. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/sub_agent/explore.py +0 -0
  153. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/sub_agent/oracle.py +0 -0
  154. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/sub_agent/task.py +0 -0
  155. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/sub_agent/web.py +0 -0
  156. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/protocol/tools.py +0 -0
  157. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/codec.py +0 -0
  158. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/export.py +0 -0
  159. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/store.py +0 -0
  160. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/templates/export_session.html +0 -0
  161. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/session/templates/mermaid_viewer.html +0 -0
  162. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/__init__.py +0 -0
  163. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/assets/deslop/SKILL.md +0 -0
  164. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/assets/dev-docs/SKILL.md +0 -0
  165. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/assets/handoff/SKILL.md +0 -0
  166. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/assets/jj-workspace/SKILL.md +0 -0
  167. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/assets/skill-creator/SKILL.md +0 -0
  168. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/loader.py +0 -0
  169. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/manager.py +0 -0
  170. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/skill/system_skills.py +0 -0
  171. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/trace/__init__.py +0 -0
  172. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/trace/log.py +0 -0
  173. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/__init__.py +0 -0
  174. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/core/__init__.py +0 -0
  175. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/core/display.py +0 -0
  176. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/core/input.py +0 -0
  177. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/core/stage_manager.py +0 -0
  178. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/__init__.py +0 -0
  179. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/debug/__init__.py +0 -0
  180. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/debug/display.py +0 -0
  181. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/exec/__init__.py +0 -0
  182. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/exec/display.py +0 -0
  183. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/__init__.py +0 -0
  184. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/clipboard.py +0 -0
  185. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/completers.py +0 -0
  186. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/display.py +0 -0
  187. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/input_prompt_toolkit.py +0 -0
  188. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/modes/repl/key_bindings.py +0 -0
  189. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/__init__.py +0 -0
  190. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/assistant.py +0 -0
  191. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/bash_syntax.py +0 -0
  192. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/common.py +0 -0
  193. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/diffs.py +0 -0
  194. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/errors.py +0 -0
  195. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/mermaid_viewer.py +0 -0
  196. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/sub_agent.py +0 -0
  197. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/thinking.py +0 -0
  198. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/tools.py +0 -0
  199. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/renderers/user_input.py +0 -0
  200. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/__init__.py +0 -0
  201. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/cjk_wrap.py +0 -0
  202. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/code_panel.py +0 -0
  203. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/live.py +0 -0
  204. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/quote.py +0 -0
  205. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/searchable_text.py +0 -0
  206. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/rich/theme.py +0 -0
  207. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/__init__.py +0 -0
  208. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/color.py +0 -0
  209. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/control.py +0 -0
  210. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/notifier.py +0 -0
  211. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/terminal/progress_bar.py +0 -0
  212. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/utils/__init__.py +0 -0
  213. {klaude_code-1.5.0 → klaude_code-1.6.0}/src/klaude_code/ui/utils/common.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -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.6.0"
8
8
  description = "Minimal code agent CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -33,6 +33,7 @@ module-name = "klaude_code"
33
33
 
34
34
  [dependency-groups]
35
35
  dev = [
36
+ "hypothesis>=6.148.8",
36
37
  "import-linter>=2.6",
37
38
  "pyright>=1.1.407",
38
39
  "pytest>=8.4.1",
@@ -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
@@ -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
@@ -4,14 +4,14 @@ from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
6
  from klaude_code.protocol import commands, events, model, op
7
- from klaude_code.session.selector import build_session_select_options
7
+ from klaude_code.session.selector import build_session_select_options, format_user_messages_display
8
8
  from klaude_code.trace import log
9
9
  from klaude_code.ui.terminal.selector import SelectItem, select_one
10
10
 
11
11
  SESSION_SELECT_STYLE = Style(
12
12
  [
13
- ("msg", ""),
14
- ("meta", "fg:ansibrightblack"),
13
+ ("msg", "fg:ansibrightblack"),
14
+ ("meta", ""),
15
15
  ("pointer", "bold fg:ansigreen"),
16
16
  ("highlighted", "fg:ansigreen"),
17
17
  ("search_prefix", "fg:ansibrightblack"),
@@ -23,7 +23,7 @@ SESSION_SELECT_STYLE = Style(
23
23
  )
24
24
 
25
25
 
26
- def _select_session_sync() -> str | None:
26
+ def select_session_sync() -> str | None:
27
27
  """Interactive session selection (sync version for asyncio.to_thread)."""
28
28
  options = build_session_select_options()
29
29
  if not options:
@@ -31,16 +31,26 @@ def _select_session_sync() -> str | None:
31
31
  return None
32
32
 
33
33
  items: list[SelectItem[str]] = []
34
- for opt in options:
35
- title = [
36
- ("class:msg", f"{opt.first_user_message}\n"),
37
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
38
- ]
34
+ for idx, opt in enumerate(options, 1):
35
+ display_msgs = format_user_messages_display(opt.user_messages)
36
+ title: list[tuple[str, str]] = []
37
+ title.append(("fg:ansibrightblack", f"{idx:2}. "))
38
+ title.append(
39
+ ("class:meta", f"{opt.relative_time} · {opt.messages_count} · {opt.model_name} · {opt.session_id}\n")
40
+ )
41
+ for msg in display_msgs:
42
+ if msg == "⋮":
43
+ title.append(("class:msg", f" {msg}\n"))
44
+ else:
45
+ title.append(("class:msg", f" > {msg}\n"))
46
+ title.append(("", "\n"))
47
+
48
+ search_text = " ".join(opt.user_messages) + f" {opt.model_name} {opt.session_id}"
39
49
  items.append(
40
50
  SelectItem(
41
51
  title=title,
42
52
  value=opt.session_id,
43
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
53
+ search_text=search_text,
44
54
  )
45
55
  )
46
56
 
@@ -83,7 +93,7 @@ class ResumeCommand(CommandABC):
83
93
  )
84
94
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
85
95
 
86
- selected_session_id = await asyncio.to_thread(_select_session_sync)
96
+ selected_session_id = await asyncio.to_thread(select_session_sync)
87
97
  if selected_session_id is None:
88
98
  event = events.DeveloperMessageEvent(
89
99
  session_id=agent.session.id,
@@ -81,7 +81,7 @@ class MetadataTracker:
81
81
  ) * 1000
82
82
 
83
83
  if self._last_token_time is not None and self._metadata_item.usage.output_tokens > 0:
84
- time_duration = self._last_token_time - self._first_token_time
84
+ time_duration = self._last_token_time - self._request_start_time
85
85
  if time_duration >= 0.15:
86
86
  self._metadata_item.usage.throughput_tps = self._metadata_item.usage.output_tokens / time_duration
87
87
 
@@ -1,4 +1,4 @@
1
- from .selector import SessionSelectOption, build_session_select_options
1
+ from .selector import SessionSelectOption, build_session_select_options, format_user_messages_display
2
2
  from .session import Session
3
3
 
4
- __all__ = ["Session", "SessionSelectOption", "build_session_select_options"]
4
+ __all__ = ["Session", "SessionSelectOption", "build_session_select_options", "format_user_messages_display"]
@@ -33,12 +33,39 @@ class SessionSelectOption:
33
33
  """Option data for session selection UI."""
34
34
 
35
35
  session_id: str
36
- first_user_message: str
36
+ user_messages: list[str]
37
37
  messages_count: str
38
38
  relative_time: str
39
39
  model_name: str
40
40
 
41
41
 
42
+ def _format_message(msg: str) -> str:
43
+ """Format a user message for display (strip and collapse newlines)."""
44
+ return msg.strip().replace("\n", " ")
45
+
46
+
47
+ def format_user_messages_display(messages: list[str]) -> list[str]:
48
+ """Format user messages for display in session selection.
49
+
50
+ Shows up to 6 messages. If more than 6, shows first 3 and last 3 with ellipsis.
51
+ Each message is on its own line.
52
+
53
+ Args:
54
+ messages: List of user messages.
55
+
56
+ Returns:
57
+ List of formatted message lines for display.
58
+ """
59
+ if len(messages) <= 6:
60
+ return messages
61
+
62
+ # More than 6: show first 3, ellipsis, last 3
63
+ result = messages[:3]
64
+ result.append("⋮")
65
+ result.extend(messages[-3:])
66
+ return result
67
+
68
+
42
69
  def build_session_select_options() -> list[SessionSelectOption]:
43
70
  """Build session selection options data.
44
71
 
@@ -51,8 +78,9 @@ def build_session_select_options() -> list[SessionSelectOption]:
51
78
 
52
79
  options: list[SessionSelectOption] = []
53
80
  for s in sessions:
54
- first_msg = s.first_user_message or "N/A"
55
- first_msg = first_msg.strip().replace("\n", " ")
81
+ user_messages = [_format_message(m) for m in s.user_messages if m.strip()]
82
+ if not user_messages:
83
+ user_messages = ["N/A"]
56
84
 
57
85
  msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} messages"
58
86
  model = s.model_name or "N/A"
@@ -60,7 +88,7 @@ def build_session_select_options() -> list[SessionSelectOption]:
60
88
  options.append(
61
89
  SessionSelectOption(
62
90
  session_id=str(s.id),
63
- first_user_message=first_msg,
91
+ user_messages=user_messages,
64
92
  messages_count=msg_count,
65
93
  relative_time=_relative_time(s.updated_at),
66
94
  model_name=model,
@@ -197,11 +197,16 @@ class Session(BaseModel):
197
197
  )
198
198
  self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
199
199
 
200
- def fork(self, *, new_id: str | None = None) -> Session:
200
+ def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
201
201
  """Create a new session as a fork of the current session.
202
202
 
203
203
  The forked session copies metadata and conversation history, but does not
204
204
  modify the current session.
205
+
206
+ Args:
207
+ new_id: Optional ID for the forked session.
208
+ until_index: If provided, only copy conversation history up to (but not including) this index.
209
+ If None, copy all history.
205
210
  """
206
211
 
207
212
  forked = Session.create(id=new_id, work_dir=self.work_dir)
@@ -213,7 +218,8 @@ class Session(BaseModel):
213
218
  forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
214
219
  forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
215
220
 
216
- items = [it.model_copy(deep=True) for it in self.conversation_history]
221
+ history_to_copy = self.conversation_history[:until_index] if until_index is not None else self.conversation_history
222
+ items = [it.model_copy(deep=True) for it in history_to_copy]
217
223
  if items:
218
224
  forked.append_history(items)
219
225
 
@@ -338,7 +344,7 @@ class Session(BaseModel):
338
344
  updated_at: float
339
345
  work_dir: str
340
346
  path: str
341
- first_user_message: str | None = None
347
+ user_messages: list[str] = []
342
348
  messages_count: int = -1
343
349
  model_name: str | None = None
344
350
 
@@ -346,10 +352,11 @@ class Session(BaseModel):
346
352
  def list_sessions(cls) -> list[SessionMetaBrief]:
347
353
  store = get_default_store()
348
354
 
349
- def _get_first_user_message(session_id: str) -> str | None:
355
+ def _get_user_messages(session_id: str) -> list[str]:
350
356
  events_path = store.paths.events_file(session_id)
351
357
  if not events_path.exists():
352
- return None
358
+ return []
359
+ messages: list[str] = []
353
360
  try:
354
361
  for line in events_path.read_text(encoding="utf-8").splitlines():
355
362
  obj_raw = json.loads(line)
@@ -360,15 +367,14 @@ class Session(BaseModel):
360
367
  continue
361
368
  data_raw = obj.get("data")
362
369
  if not isinstance(data_raw, dict):
363
- return None
370
+ continue
364
371
  data = cast(dict[str, Any], data_raw)
365
372
  content = data.get("content")
366
373
  if isinstance(content, str):
367
- return content
368
- return None
374
+ messages.append(content)
369
375
  except (OSError, json.JSONDecodeError):
370
- return None
371
- return None
376
+ pass
377
+ return messages
372
378
 
373
379
  items: list[Session.SessionMetaBrief] = []
374
380
  for meta_path in store.iter_meta_files():
@@ -382,7 +388,7 @@ class Session(BaseModel):
382
388
  created = float(data.get("created_at", meta_path.stat().st_mtime))
383
389
  updated = float(data.get("updated_at", meta_path.stat().st_mtime))
384
390
  work_dir = str(data.get("work_dir", ""))
385
- first_user_message = _get_first_user_message(sid)
391
+ user_messages = _get_user_messages(sid)
386
392
  messages_count = int(data.get("messages_count", -1))
387
393
  model_name = data.get("model_name") if isinstance(data.get("model_name"), str) else None
388
394
 
@@ -393,7 +399,7 @@ class Session(BaseModel):
393
399
  updated_at=updated,
394
400
  work_dir=work_dir,
395
401
  path=str(meta_path),
396
- first_user_message=first_user_message,
402
+ user_messages=user_messages,
397
403
  messages_count=messages_count,
398
404
  model_name=model_name,
399
405
  )