klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,67 +1,131 @@
1
- from klaude_code.config.config import load_config
1
+ from dataclasses import dataclass
2
+
3
+ from klaude_code.config.config import ModelEntry, load_config, print_no_available_models_hint
2
4
  from klaude_code.trace import log
3
- from klaude_code.ui.rich.searchable_text import SearchableFormattedList
4
5
 
5
6
 
6
- def select_model_from_config(preferred: str | None = None) -> str | None:
7
+ def _normalize_model_key(value: str) -> str:
8
+ """Normalize a model identifier for loose matching.
9
+
10
+ This enables aliases like:
11
+ - gpt52 -> gpt-5.2
12
+ - gpt5.2 -> gpt-5.2
13
+
14
+ Strategy: case-fold + keep only alphanumeric characters.
7
15
  """
8
- Interactive single-choice model selector.
9
- for `--select-model`
16
+
17
+ return "".join(ch for ch in value.casefold() if ch.isalnum())
18
+
19
+
20
+ @dataclass
21
+ class ModelMatchResult:
22
+ """Result of model matching.
23
+
24
+ Attributes:
25
+ matched_model: The single matched model name, or None if ambiguous/no match.
26
+ filtered_models: List of filtered models for interactive selection.
27
+ filter_hint: The filter hint to show (original preferred value), or None.
28
+ error_message: Error message if no models available, or None.
29
+ """
30
+
31
+ matched_model: str | None
32
+ filtered_models: list[ModelEntry]
33
+ filter_hint: str | None
34
+ error_message: str | None = None
35
+
36
+
37
+ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
38
+ """Match model from config without interactive selection.
39
+
40
+ If preferred is provided:
41
+ - Exact match: returns matched_model
42
+ - Single partial match (case-insensitive): returns matched_model
43
+ - Multiple matches: returns filtered_models for interactive selection
44
+ - No matches: returns all models with filter_hint=None
45
+
46
+ Returns:
47
+ ModelMatchResult with match state.
10
48
  """
11
49
  config = load_config()
12
- assert config is not None
13
- models = sorted(config.model_list, key=lambda m: m.model_name.lower())
50
+
51
+ # Only show models from providers with valid API keys
52
+ models: list[ModelEntry] = sorted(
53
+ config.iter_model_entries(only_available=True), key=lambda m: m.model_name.lower()
54
+ )
14
55
 
15
56
  if not models:
16
- raise ValueError("No models configured. Please update your config.yaml")
57
+ print_no_available_models_hint()
58
+ return ModelMatchResult(
59
+ matched_model=None,
60
+ filtered_models=[],
61
+ filter_hint=None,
62
+ error_message="No models available",
63
+ )
17
64
 
18
65
  names: list[str] = [m.model_name for m in models]
19
- default_name: str | None = (
20
- preferred if preferred in names else (config.main_model if config.main_model in names else None)
21
- )
22
66
 
23
- try:
24
- import questionary
67
+ # Try to match preferred model name
68
+ filter_hint = preferred
69
+ if preferred and preferred.strip():
70
+ preferred = preferred.strip()
71
+ # Exact match
72
+ if preferred in names:
73
+ return ModelMatchResult(matched_model=preferred, filtered_models=models, filter_hint=None)
25
74
 
26
- choices: list[questionary.Choice] = []
75
+ preferred_lower = preferred.lower()
76
+ # Case-insensitive exact match (model_name or model_params.model)
77
+ exact_ci_matches = [
78
+ m
79
+ for m in models
80
+ if preferred_lower == m.model_name.lower() or preferred_lower == (m.model_params.model or "").lower()
81
+ ]
82
+ if len(exact_ci_matches) == 1:
83
+ return ModelMatchResult(
84
+ matched_model=exact_ci_matches[0].model_name, filtered_models=models, filter_hint=None
85
+ )
27
86
 
28
- max_model_name_length = max(len(m.model_name) for m in models)
29
- for m in models:
30
- star = "★ " if m.model_name == config.main_model else " "
31
- fragments = [
32
- ("class:t", f"{star}{m.model_name:<{max_model_name_length}} → "),
33
- ("class:b", m.model_params.model or "N/A"),
34
- ("class:d", f" {m.provider}"),
87
+ # Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
88
+ preferred_norm = _normalize_model_key(preferred)
89
+ normalized_matches: list[ModelEntry] = []
90
+ if preferred_norm:
91
+ normalized_matches = [
92
+ m
93
+ for m in models
94
+ if preferred_norm == _normalize_model_key(m.model_name)
95
+ or preferred_norm == _normalize_model_key(m.model_params.model or "")
35
96
  ]
36
- # Provide a formatted title for display and a plain text for search.
37
- title = SearchableFormattedList(fragments)
38
- choices.append(questionary.Choice(title=title, value=m.model_name))
39
-
40
- try:
41
- result = questionary.select(
42
- message="Select a model:",
43
- choices=choices,
44
- default=default_name,
45
- pointer="→",
46
- instruction="↑↓ to move Enter to select",
47
- use_jk_keys=False,
48
- use_search_filter=True,
49
- style=questionary.Style(
50
- [
51
- ("t", ""),
52
- ("b", "bold"),
53
- ("d", "dim"),
54
- # search filter colors at the bottom
55
- ("search_success", "noinherit fg:ansigreen"),
56
- ("search_none", "noinherit fg:ansired"),
57
- ("question-mark", "fg:ansigreen"),
58
- ]
59
- ),
60
- ).ask()
61
- if isinstance(result, str) and result in names:
62
- return result
63
- except KeyboardInterrupt:
64
- return None
65
- except Exception as e:
66
- log(f"Failed to use questionary, falling back to default model, {e}")
67
- return preferred
97
+ if len(normalized_matches) == 1:
98
+ return ModelMatchResult(
99
+ matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
100
+ )
101
+
102
+ if not normalized_matches and len(preferred_norm) >= 4:
103
+ normalized_matches = [
104
+ m
105
+ for m in models
106
+ if preferred_norm in _normalize_model_key(m.model_name)
107
+ or preferred_norm in _normalize_model_key(m.model_params.model or "")
108
+ ]
109
+ if len(normalized_matches) == 1:
110
+ return ModelMatchResult(
111
+ matched_model=normalized_matches[0].model_name, filtered_models=models, filter_hint=None
112
+ )
113
+
114
+ # Partial match (case-insensitive) on model_name or model_params.model.
115
+ # If normalized matching found candidates (even if multiple), prefer those as the filter set.
116
+ matches = normalized_matches or [
117
+ m
118
+ for m in models
119
+ if preferred_lower in m.model_name.lower() or preferred_lower in (m.model_params.model or "").lower()
120
+ ]
121
+ if len(matches) == 1:
122
+ return ModelMatchResult(matched_model=matches[0].model_name, filtered_models=models, filter_hint=None)
123
+ if matches:
124
+ # Multiple matches: filter the list for interactive selection
125
+ return ModelMatchResult(matched_model=None, filtered_models=matches, filter_hint=filter_hint)
126
+ else:
127
+ # No matches: show all models without filter hint
128
+ log(("No matching models found. Showing all models.", "yellow"))
129
+ return ModelMatchResult(matched_model=None, filtered_models=models, filter_hint=None)
130
+
131
+ return ModelMatchResult(matched_model=None, filtered_models=models, filter_hint=None)
@@ -0,0 +1,269 @@
1
+ """Thinking level configuration data and helpers.
2
+
3
+ This module contains thinking level definitions and helper functions
4
+ that are shared between command layer and UI layer.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Literal
9
+
10
+ from klaude_code.protocol import llm_param
11
+
12
+ ReasoningEffort = Literal["high", "medium", "low", "minimal", "none", "xhigh"]
13
+
14
+ # Thinking level options for different protocols
15
+ RESPONSES_LEVELS = ["low", "medium", "high"]
16
+ RESPONSES_GPT51_LEVELS = ["none", "low", "medium", "high"]
17
+ RESPONSES_GPT52_LEVELS = ["none", "low", "medium", "high", "xhigh"]
18
+ RESPONSES_CODEX_MAX_LEVELS = ["medium", "high", "xhigh"]
19
+ RESPONSES_GEMINI_FLASH_LEVELS = ["minimal", "low", "medium", "high"]
20
+
21
+ ANTHROPIC_LEVELS: list[tuple[str, int | None]] = [
22
+ ("off", 0),
23
+ ("low (2048 tokens)", 2048),
24
+ ("medium (8192 tokens)", 8192),
25
+ ("high (31999 tokens)", 31999),
26
+ ]
27
+
28
+
29
+ def is_openrouter_model_with_reasoning_effort(model_name: str | None) -> bool:
30
+ """Check if the model is GPT series, Grok or Gemini 3."""
31
+ if not model_name:
32
+ return False
33
+ model_lower = model_name.lower()
34
+ return model_lower.startswith(("openai/gpt-", "x-ai/grok-", "google/gemini-3"))
35
+
36
+
37
+ def _is_gpt51_model(model_name: str | None) -> bool:
38
+ """Check if the model is GPT-5.1."""
39
+ if not model_name:
40
+ return False
41
+ return model_name.lower() in ["gpt-5.1", "openai/gpt-5.1", "gpt-5.1-codex-2025-11-13"]
42
+
43
+
44
+ def _is_gpt52_model(model_name: str | None) -> bool:
45
+ """Check if the model is GPT-5.2."""
46
+ if not model_name:
47
+ return False
48
+ return model_name.lower() in ["gpt-5.2", "openai/gpt-5.2"]
49
+
50
+
51
+ def _is_codex_max_model(model_name: str | None) -> bool:
52
+ """Check if the model is GPT-5.1-codex-max."""
53
+ if not model_name:
54
+ return False
55
+ return "codex-max" in model_name.lower()
56
+
57
+
58
+ def _is_gemini_flash_model(model_name: str | None) -> bool:
59
+ """Check if the model is Gemini 3 Flash."""
60
+ if not model_name:
61
+ return False
62
+ return "gemini-3-flash" in model_name.lower()
63
+
64
+
65
+ def should_auto_trigger_thinking(model_name: str | None) -> bool:
66
+ """Check if model should auto-trigger thinking selection on switch."""
67
+ if not model_name:
68
+ return False
69
+ model_lower = model_name.lower()
70
+ return "gpt-5" in model_lower or "gemini-3" in model_lower or "opus" in model_lower
71
+
72
+
73
+ def get_levels_for_responses(model_name: str | None) -> list[str]:
74
+ """Get thinking levels for responses protocol."""
75
+ if _is_codex_max_model(model_name):
76
+ return RESPONSES_CODEX_MAX_LEVELS
77
+ if _is_gpt52_model(model_name):
78
+ return RESPONSES_GPT52_LEVELS
79
+ if _is_gpt51_model(model_name):
80
+ return RESPONSES_GPT51_LEVELS
81
+ if _is_gemini_flash_model(model_name):
82
+ return RESPONSES_GEMINI_FLASH_LEVELS
83
+ return RESPONSES_LEVELS
84
+
85
+
86
+ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
87
+ """Format the current thinking configuration for display."""
88
+ thinking = config.thinking
89
+ if not thinking:
90
+ return "not configured"
91
+
92
+ protocol = config.protocol
93
+
94
+ if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
95
+ if thinking.reasoning_effort:
96
+ return f"reasoning_effort={thinking.reasoning_effort}"
97
+ return "not set"
98
+
99
+ if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
100
+ if thinking.type == "disabled":
101
+ return "off"
102
+ if thinking.type == "enabled":
103
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
104
+ return "not set"
105
+
106
+ if protocol == llm_param.LLMClientProtocol.OPENROUTER:
107
+ if is_openrouter_model_with_reasoning_effort(config.model):
108
+ if thinking.reasoning_effort:
109
+ return f"reasoning_effort={thinking.reasoning_effort}"
110
+ else:
111
+ if thinking.type == "disabled":
112
+ return "off"
113
+ if thinking.type == "enabled":
114
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
115
+ return "not set"
116
+
117
+ if protocol == llm_param.LLMClientProtocol.OPENAI:
118
+ if thinking.type == "disabled":
119
+ return "off"
120
+ if thinking.type == "enabled":
121
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
122
+ return "not set"
123
+
124
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
125
+ if thinking.type == "disabled":
126
+ return "off"
127
+ if thinking.type == "enabled":
128
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
129
+ return "not set"
130
+
131
+ return "unknown protocol"
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # Thinking picker data structures
136
+ # ---------------------------------------------------------------------------
137
+
138
+
139
+ @dataclass
140
+ class ThinkingOption:
141
+ """A thinking option for selection.
142
+
143
+ Attributes:
144
+ label: Display label for this option (e.g., "low", "medium (8192 tokens)").
145
+ value: Encoded value string (e.g., "effort:low", "budget:2048").
146
+ """
147
+
148
+ label: str
149
+ value: str
150
+
151
+
152
+ @dataclass
153
+ class ThinkingPickerData:
154
+ """Data for building thinking picker UI.
155
+
156
+ Attributes:
157
+ options: List of thinking options.
158
+ message: Prompt message (e.g., "Select reasoning effort:").
159
+ current_value: Currently selected value, or None.
160
+ """
161
+
162
+ options: list[ThinkingOption]
163
+ message: str
164
+ current_value: str | None
165
+
166
+
167
+ def _build_effort_options(levels: list[str]) -> list[ThinkingOption]:
168
+ """Build effort-based thinking options."""
169
+ return [ThinkingOption(label=level, value=f"effort:{level}") for level in levels]
170
+
171
+
172
+ def _build_budget_options() -> list[ThinkingOption]:
173
+ """Build budget-based thinking options."""
174
+ return [ThinkingOption(label=label, value=f"budget:{tokens or 0}") for label, tokens in ANTHROPIC_LEVELS]
175
+
176
+
177
+ def _get_current_effort_value(thinking: llm_param.Thinking | None) -> str | None:
178
+ """Get current value for effort-based thinking."""
179
+ if thinking and thinking.reasoning_effort:
180
+ return f"effort:{thinking.reasoning_effort}"
181
+ return None
182
+
183
+
184
+ def _get_current_budget_value(thinking: llm_param.Thinking | None) -> str | None:
185
+ """Get current value for budget-based thinking."""
186
+ if thinking:
187
+ if thinking.type == "disabled":
188
+ return "budget:0"
189
+ if thinking.budget_tokens:
190
+ return f"budget:{thinking.budget_tokens}"
191
+ return None
192
+
193
+
194
+ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPickerData | None:
195
+ """Get thinking picker data based on LLM config.
196
+
197
+ Returns:
198
+ ThinkingPickerData with options and current value, or None if protocol doesn't support thinking.
199
+ """
200
+ protocol = config.protocol
201
+ model_name = config.model
202
+ thinking = config.thinking
203
+
204
+ if protocol in (llm_param.LLMClientProtocol.RESPONSES, llm_param.LLMClientProtocol.CODEX):
205
+ levels = get_levels_for_responses(model_name)
206
+ return ThinkingPickerData(
207
+ options=_build_effort_options(levels),
208
+ message="Select reasoning effort:",
209
+ current_value=_get_current_effort_value(thinking),
210
+ )
211
+
212
+ if protocol == llm_param.LLMClientProtocol.ANTHROPIC:
213
+ return ThinkingPickerData(
214
+ options=_build_budget_options(),
215
+ message="Select thinking level:",
216
+ current_value=_get_current_budget_value(thinking),
217
+ )
218
+
219
+ if protocol == llm_param.LLMClientProtocol.OPENROUTER:
220
+ if is_openrouter_model_with_reasoning_effort(model_name):
221
+ levels = get_levels_for_responses(model_name)
222
+ return ThinkingPickerData(
223
+ options=_build_effort_options(levels),
224
+ message="Select reasoning effort:",
225
+ current_value=_get_current_effort_value(thinking),
226
+ )
227
+ return ThinkingPickerData(
228
+ options=_build_budget_options(),
229
+ message="Select thinking level:",
230
+ current_value=_get_current_budget_value(thinking),
231
+ )
232
+
233
+ if protocol == llm_param.LLMClientProtocol.OPENAI:
234
+ return ThinkingPickerData(
235
+ options=_build_budget_options(),
236
+ message="Select thinking level:",
237
+ current_value=_get_current_budget_value(thinking),
238
+ )
239
+
240
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
241
+ return ThinkingPickerData(
242
+ options=_build_budget_options(),
243
+ message="Select thinking level:",
244
+ current_value=_get_current_budget_value(thinking),
245
+ )
246
+
247
+ return None
248
+
249
+
250
+ def parse_thinking_value(value: str) -> llm_param.Thinking | None:
251
+ """Parse a thinking value string into a Thinking object.
252
+
253
+ Args:
254
+ value: Encoded value string (e.g., "effort:low", "budget:2048").
255
+
256
+ Returns:
257
+ Thinking object, or None if invalid format.
258
+ """
259
+ if value.startswith("effort:"):
260
+ effort = value[7:]
261
+ return llm_param.Thinking(reasoning_effort=effort) # type: ignore[arg-type]
262
+
263
+ if value.startswith("budget:"):
264
+ budget = int(value[7:])
265
+ if budget == 0:
266
+ return llm_param.Thinking(type="disabled", budget_tokens=0)
267
+ return llm_param.Thinking(type="enabled", budget_tokens=budget)
268
+
269
+ return None
@@ -4,6 +4,21 @@ This module consolidates all magic numbers and configuration values
4
4
  that were previously scattered across the codebase.
5
5
  """
6
6
 
7
+ import os
8
+ from pathlib import Path
9
+
10
+
11
+ def _get_int_env(name: str, default: int) -> int:
12
+ """Get an integer value from environment variable, or return default."""
13
+ val = os.environ.get(name)
14
+ if val is None:
15
+ return default
16
+ try:
17
+ return int(val)
18
+ except ValueError:
19
+ return default
20
+
21
+
7
22
  # =============================================================================
8
23
  # Agent Configuration
9
24
  # =============================================================================
@@ -45,13 +60,12 @@ TODO_REMINDER_TOOL_CALL_THRESHOLD = 10
45
60
  READ_CHAR_LIMIT_PER_LINE = 2000
46
61
 
47
62
  # Maximum number of lines to read from a file
48
- READ_GLOBAL_LINE_CAP = 2000
49
-
50
- # Maximum total characters to read
51
- READ_MAX_CHARS = 60000
63
+ # Can be overridden via KLAUDE_READ_GLOBAL_LINE_CAP environment variable
64
+ READ_GLOBAL_LINE_CAP = _get_int_env("KLAUDE_READ_GLOBAL_LINE_CAP", 2000)
52
65
 
53
- # Maximum file size in KB for text files
54
- READ_MAX_KB = 256
66
+ # Maximum total characters to read (truncates beyond this limit)
67
+ # Can be overridden via KLAUDE_READ_MAX_CHARS environment variable
68
+ READ_MAX_CHARS = _get_int_env("KLAUDE_READ_MAX_CHARS", 50000)
55
69
 
56
70
  # Maximum image file size in bytes (4MB)
57
71
  READ_MAX_IMAGE_BYTES = 4 * 1024 * 1024
@@ -62,7 +76,7 @@ BASH_DEFAULT_TIMEOUT_MS = 120000
62
76
 
63
77
  # -- Tool Output --
64
78
  # Maximum length for tool output before truncation
65
- TOOL_OUTPUT_MAX_LENGTH = 50000
79
+ TOOL_OUTPUT_MAX_LENGTH = 40000
66
80
 
67
81
  # Characters to show from the beginning of truncated output
68
82
  TOOL_OUTPUT_DISPLAY_HEAD = 10000
@@ -91,40 +105,57 @@ INVALID_TOOL_CALL_MAX_LENGTH = 500
91
105
  TRUNCATE_DISPLAY_MAX_LINE_LENGTH = 1000
92
106
 
93
107
  # Maximum lines for truncated display output
94
- TRUNCATE_DISPLAY_MAX_LINES = 10
108
+ TRUNCATE_DISPLAY_MAX_LINES = 8
95
109
 
96
110
  # Maximum lines for sub-agent result display
97
- SUB_AGENT_RESULT_MAX_LINES = 12
111
+ SUB_AGENT_RESULT_MAX_LINES = 50
98
112
 
99
113
 
100
114
  # UI refresh rate (frames per second) for debounced content streaming
101
- UI_REFRESH_RATE_FPS = 20
115
+ UI_REFRESH_RATE_FPS = 10
116
+
117
+ # Enable live area for streaming markdown (shows incomplete blocks being typed)
118
+ # When False, only completed markdown blocks are displayed (more stable, less flicker)
119
+ MARKDOWN_STREAM_LIVE_REPAINT_ENABLED = False
102
120
 
103
121
  # Number of lines to keep visible at bottom of markdown streaming window
104
- MARKDOWN_STREAM_LIVE_WINDOW = 20
122
+ MARKDOWN_STREAM_LIVE_WINDOW = 6
123
+
124
+ # Left margin (columns) to reserve when rendering markdown
125
+ MARKDOWN_LEFT_MARGIN = 2
126
+
127
+ # Right margin (columns) to reserve when rendering markdown
128
+ MARKDOWN_RIGHT_MARGIN = 2
129
+
130
+ # Status hint text shown after spinner status
131
+ STATUS_HINT_TEXT = " (esc to interrupt)"
132
+
133
+ # Default spinner status text when idle/thinking
134
+ STATUS_DEFAULT_TEXT = "Thinking …"
105
135
 
106
136
  # Status shimmer animation
107
137
  # Horizontal padding used when computing shimmer band position
108
138
  STATUS_SHIMMER_PADDING = 10
109
- # Duration in seconds for one full shimmer sweep across the text
110
- STATUS_SHIMMER_SWEEP_SECONDS = 2
111
139
  # Half-width of the shimmer band in characters
112
140
  STATUS_SHIMMER_BAND_HALF_WIDTH = 5.0
113
141
  # Scale factor applied to shimmer intensity when blending colors
114
142
  STATUS_SHIMMER_ALPHA_SCALE = 0.7
115
143
 
116
- # Spinner breathing animation
117
- # Duration in seconds for one full breathe-in + breathe-out cycle
118
- # Keep in sync with STATUS_SHIMMER_SWEEP_SECONDS for visual consistency
119
- SPINNER_BREATH_PERIOD_SECONDS = 2
144
+ # Spinner breathing and shimmer animation period
145
+ # Duration in seconds for one full breathe-in + breathe-out cycle (breathing)
146
+ # and one full shimmer sweep across the text (shimmer)
147
+ SPINNER_BREATH_PERIOD_SECONDS: float = 2.0
120
148
 
121
149
 
122
150
  # =============================================================================
123
151
  # Debug / Logging
124
152
  # =============================================================================
125
153
 
126
- # Default debug log file path
127
- DEFAULT_DEBUG_LOG_FILE = "debug.log"
154
+ # Default debug log directory (user cache)
155
+ DEFAULT_DEBUG_LOG_DIR = Path.home() / ".klaude" / "logs"
156
+
157
+ # Default debug log file path (symlink to latest session)
158
+ DEFAULT_DEBUG_LOG_FILE = DEFAULT_DEBUG_LOG_DIR / "debug.log"
128
159
 
129
160
  # Maximum log file size before rotation (10MB)
130
161
  LOG_MAX_BYTES = 10 * 1024 * 1024
klaude_code/core/agent.py CHANGED
@@ -4,10 +4,10 @@ from collections.abc import AsyncGenerator, Iterable
4
4
  from dataclasses import dataclass
5
5
  from typing import Protocol
6
6
 
7
- from klaude_code.core.prompt import get_system_prompt as load_system_prompt
7
+ from klaude_code.core.prompt import load_system_prompt
8
8
  from klaude_code.core.reminders import Reminder, load_agent_reminders
9
- from klaude_code.core.task import TaskExecutionContext, TaskExecutor
10
- from klaude_code.core.tool import TodoContext, get_registry, load_agent_tools
9
+ from klaude_code.core.task import SessionContext, TaskExecutionContext, TaskExecutor
10
+ from klaude_code.core.tool import build_todo_context, get_registry, load_agent_tools
11
11
  from klaude_code.llm import LLMClientABC
12
12
  from klaude_code.protocol import events, llm_param, model, tools
13
13
  from klaude_code.protocol.model import UserInputPayload
@@ -46,7 +46,7 @@ class DefaultModelProfileProvider(ModelProfileProvider):
46
46
  model_name = llm_client.model_name
47
47
  return AgentProfile(
48
48
  llm_client=llm_client,
49
- system_prompt=load_system_prompt(model_name, sub_agent_type),
49
+ system_prompt=load_system_prompt(model_name, llm_client.protocol, sub_agent_type),
50
50
  tools=load_agent_tools(model_name, sub_agent_type),
51
51
  reminders=load_agent_reminders(model_name, sub_agent_type),
52
52
  )
@@ -76,11 +76,10 @@ class Agent:
76
76
  profile: AgentProfile,
77
77
  ):
78
78
  self.session: Session = session
79
- self.profile: AgentProfile | None = None
80
- # Active task executor, if any
79
+ self.profile: AgentProfile = profile
81
80
  self._current_task: TaskExecutor | None = None
82
- # Ensure runtime configuration matches the active model on initialization
83
- self.set_model_profile(profile)
81
+ if not self.session.model_name:
82
+ self.session.model_name = profile.llm_client.model_name
84
83
 
85
84
  def cancel(self) -> Iterable[events.Event]:
86
85
  """Handle agent cancellation and persist an interrupt marker and tool cancellations.
@@ -93,8 +92,7 @@ class Agent:
93
92
  """
94
93
  # First, cancel any running task so it stops emitting events.
95
94
  if self._current_task is not None:
96
- for ui_event in self._current_task.cancel():
97
- yield ui_event
95
+ yield from self._current_task.cancel()
98
96
  self._current_task = None
99
97
 
100
98
  # Record an interrupt marker in the session history
@@ -105,18 +103,18 @@ class Agent:
105
103
  debug_type=DebugType.EXECUTION,
106
104
  )
107
105
 
108
- async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event, None]:
109
- context = TaskExecutionContext(
106
+ async def run_task(self, user_input: UserInputPayload) -> AsyncGenerator[events.Event]:
107
+ session_ctx = SessionContext(
110
108
  session_id=self.session.id,
111
- profile=self._require_profile(),
112
109
  get_conversation_history=lambda: self.session.conversation_history,
113
110
  append_history=self.session.append_history,
114
- tool_registry=get_registry(),
115
111
  file_tracker=self.session.file_tracker,
116
- todo_context=TodoContext(
117
- get_todos=lambda: self.session.todos,
118
- set_todos=lambda todos: setattr(self.session, "todos", todos),
119
- ),
112
+ todo_context=build_todo_context(self.session),
113
+ )
114
+ context = TaskExecutionContext(
115
+ session_ctx=session_ctx,
116
+ profile=self.profile,
117
+ tool_registry=get_registry(),
120
118
  process_reminder=self._process_reminder,
121
119
  sub_agent_state=self.session.sub_agent_state,
122
120
  )
@@ -130,7 +128,7 @@ class Agent:
130
128
  finally:
131
129
  self._current_task = None
132
130
 
133
- async def replay_history(self) -> AsyncGenerator[events.Event, None]:
131
+ async def replay_history(self) -> AsyncGenerator[events.Event]:
134
132
  """Yield UI events reconstructed from saved conversation history."""
135
133
 
136
134
  if len(self.session.conversation_history) == 0:
@@ -142,7 +140,7 @@ class Agent:
142
140
  session_id=self.session.id,
143
141
  )
144
142
 
145
- async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent, None]:
143
+ async def _process_reminder(self, reminder: Reminder) -> AsyncGenerator[events.DeveloperMessageEvent]:
146
144
  """Process a single reminder and yield events if it produces output."""
147
145
  item = await reminder(self.session)
148
146
  if item is not None:
@@ -153,13 +151,7 @@ class Agent:
153
151
  """Apply a fully constructed profile to the agent."""
154
152
 
155
153
  self.profile = profile
156
- if not self.session.model_name:
157
- self.session.model_name = profile.llm_client.model_name
154
+ self.session.model_name = profile.llm_client.model_name
158
155
 
159
156
  def get_llm_client(self) -> LLMClientABC:
160
- return self._require_profile().llm_client
161
-
162
- def _require_profile(self) -> AgentProfile:
163
- if self.profile is None:
164
- raise RuntimeError("Agent profile is not initialized")
165
- return self.profile
157
+ return self.profile.llm_client