klaude-code 1.2.6__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 (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ from functools import lru_cache
3
+ from pathlib import Path
4
+ from typing import Any, cast
5
+
6
+ import yaml
7
+ from pydantic import BaseModel, Field, ValidationError, model_validator
8
+
9
+ from klaude_code.protocol import llm_param
10
+ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
11
+ from klaude_code.trace import log
12
+
13
+ config_path = Path.home() / ".klaude" / "klaude-config.yaml"
14
+
15
+
16
+ class ModelConfig(BaseModel):
17
+ model_name: str
18
+ provider: str
19
+ model_params: llm_param.LLMConfigModelParameter
20
+
21
+
22
+ class Config(BaseModel):
23
+ provider_list: list[llm_param.LLMConfigProviderParameter]
24
+ model_list: list[ModelConfig]
25
+ main_model: str
26
+ subagent_models: dict[str, str] = Field(default_factory=dict)
27
+ theme: str | None = None
28
+
29
+ @model_validator(mode="before")
30
+ @classmethod
31
+ def _normalize_subagent_models(cls, data: dict[str, Any]) -> dict[str, Any]:
32
+ raw_val: Any = data.get("subagent_models") or {}
33
+ raw_models: dict[str, Any] = cast(dict[str, Any], raw_val) if isinstance(raw_val, dict) else {}
34
+ normalized: dict[str, str] = {}
35
+ key_map = {p.name.lower(): p.name for p in iter_sub_agent_profiles()}
36
+ for key, value in dict(raw_models).items():
37
+ canonical = key_map.get(str(key).lower(), str(key))
38
+ normalized[canonical] = str(value)
39
+ data["subagent_models"] = normalized
40
+ return data
41
+
42
+ def get_main_model_config(self) -> llm_param.LLMConfigParameter:
43
+ return self.get_model_config(self.main_model)
44
+
45
+ def get_model_config(self, model_name: str) -> llm_param.LLMConfigParameter:
46
+ model = next(
47
+ (model for model in self.model_list if model.model_name == model_name),
48
+ None,
49
+ )
50
+ if model is None:
51
+ raise ValueError(f"Unknown model: {model_name}")
52
+
53
+ provider = next(
54
+ (provider for provider in self.provider_list if provider.provider_name == model.provider),
55
+ None,
56
+ )
57
+ if provider is None:
58
+ raise ValueError(f"Unknown provider: {model.provider}")
59
+
60
+ return llm_param.LLMConfigParameter(
61
+ **provider.model_dump(),
62
+ **model.model_params.model_dump(),
63
+ )
64
+
65
+ async def save(self) -> None:
66
+ """
67
+ Save config to file.
68
+ Notice: it won't preserve comments in the config file.
69
+ """
70
+ config_dict = self.model_dump(mode="json", exclude_none=True)
71
+
72
+ def _save_config() -> None:
73
+ config_path.parent.mkdir(parents=True, exist_ok=True)
74
+ _ = config_path.write_text(yaml.dump(config_dict, default_flow_style=False, sort_keys=False))
75
+
76
+ await asyncio.to_thread(_save_config)
77
+
78
+
79
+ def get_example_config() -> Config:
80
+ return Config(
81
+ main_model="gpt-5.1",
82
+ subagent_models={"explore": "haiku", "oracle": "gpt-5.1-high"},
83
+ provider_list=[
84
+ llm_param.LLMConfigProviderParameter(
85
+ provider_name="openai",
86
+ protocol=llm_param.LLMClientProtocol.RESPONSES,
87
+ api_key="your-openai-api-key",
88
+ base_url="https://api.openai.com/v1",
89
+ ),
90
+ llm_param.LLMConfigProviderParameter(
91
+ provider_name="openrouter",
92
+ protocol=llm_param.LLMClientProtocol.OPENROUTER,
93
+ api_key="your-openrouter-api-key",
94
+ ),
95
+ ],
96
+ model_list=[
97
+ ModelConfig(
98
+ model_name="gpt-5.1",
99
+ provider="openai",
100
+ model_params=llm_param.LLMConfigModelParameter(
101
+ model="gpt-5.1-2025-11-13",
102
+ max_tokens=32000,
103
+ verbosity="medium",
104
+ thinking=llm_param.Thinking(
105
+ reasoning_effort="medium",
106
+ reasoning_summary="auto",
107
+ type="enabled",
108
+ budget_tokens=None,
109
+ ),
110
+ context_limit=368000,
111
+ ),
112
+ ),
113
+ ModelConfig(
114
+ model_name="gpt-5.1-high",
115
+ provider="openai",
116
+ model_params=llm_param.LLMConfigModelParameter(
117
+ model="gpt-5.1-2025-11-13",
118
+ max_tokens=32000,
119
+ verbosity="medium",
120
+ thinking=llm_param.Thinking(
121
+ reasoning_effort="high",
122
+ reasoning_summary="auto",
123
+ type="enabled",
124
+ budget_tokens=None,
125
+ ),
126
+ context_limit=368000,
127
+ ),
128
+ ),
129
+ ModelConfig(
130
+ model_name="haiku",
131
+ provider="openrouter",
132
+ model_params=llm_param.LLMConfigModelParameter(
133
+ model="anthropic/claude-haiku-4.5",
134
+ max_tokens=32000,
135
+ provider_routing=llm_param.OpenRouterProviderRouting(
136
+ sort="throughput",
137
+ ),
138
+ context_limit=168000,
139
+ ),
140
+ ),
141
+ ],
142
+ )
143
+
144
+
145
+ @lru_cache(maxsize=1)
146
+ def load_config() -> Config | None:
147
+ if not config_path.exists():
148
+ log(f"Config file not found: {config_path}")
149
+ example_config = get_example_config()
150
+ config_path.parent.mkdir(parents=True, exist_ok=True)
151
+ config_dict = example_config.model_dump(mode="json", exclude_none=True)
152
+
153
+ # Comment out all example config lines
154
+ yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
155
+ commented_yaml = "\n".join(f"# {line}" if line.strip() else "#" for line in yaml_str.splitlines())
156
+ _ = config_path.write_text(commented_yaml)
157
+
158
+ log(f"Example config created at: {config_path}")
159
+ log("Please edit the config file to set up your models", style="yellow bold")
160
+ return None
161
+
162
+ config_yaml = config_path.read_text()
163
+ config_dict = yaml.safe_load(config_yaml)
164
+
165
+ if config_dict is None:
166
+ log(f"Config file is empty or all commented: {config_path}", style="red bold")
167
+ log("Please edit the config file to set up your models", style="yellow bold")
168
+ return None
169
+
170
+ try:
171
+ config = Config.model_validate(config_dict)
172
+ except ValidationError as e:
173
+ log(f"Invalid config file: {config_path}", style="red bold")
174
+ log(str(e), style="red")
175
+ raise ValueError(f"Invalid config file: {config_path}") from e
176
+
177
+ return config
@@ -0,0 +1,162 @@
1
+ from rich.console import Console, Group
2
+ from rich.panel import Panel
3
+ from rich.table import Table
4
+ from rich.text import Text
5
+
6
+ from klaude_code.config import Config
7
+ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
8
+ from klaude_code.ui.rich.theme import ThemeKey, get_theme
9
+
10
+
11
+ def mask_api_key(api_key: str | None) -> str:
12
+ """Mask API key to show only first 6 and last 6 characters with *** in between"""
13
+ if not api_key or api_key == "N/A":
14
+ return "N/A"
15
+
16
+ if len(api_key) <= 12:
17
+ return api_key
18
+
19
+ return f"{api_key[:6]} … {api_key[-6:]}"
20
+
21
+
22
+ def display_models_and_providers(config: Config):
23
+ """Display models and providers configuration using rich formatting"""
24
+ themes = get_theme(config.theme)
25
+ console = Console(theme=themes.app_theme)
26
+
27
+ # Display providers section
28
+ providers_table = Table.grid(padding=(0, 1), expand=True)
29
+ providers_table.add_column(width=2, no_wrap=True) # Status
30
+ providers_table.add_column(overflow="fold") # Name
31
+ providers_table.add_column(overflow="fold") # Protocol
32
+ providers_table.add_column(overflow="fold") # Base URL
33
+ providers_table.add_column(overflow="fold") # API Key
34
+
35
+ # Add header
36
+ providers_table.add_row(
37
+ Text("", style="bold"),
38
+ Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
39
+ Text("Protocol", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
40
+ Text("Base URL", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
41
+ Text("API Key", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
42
+ )
43
+
44
+ # Add providers
45
+ for provider in config.provider_list:
46
+ status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
47
+ name = Text(provider.provider_name, style=ThemeKey.CONFIG_ITEM_NAME)
48
+ protocol = Text(str(provider.protocol.value), style="")
49
+ base_url = Text(provider.base_url or "N/A", style="")
50
+ api_key = Text(mask_api_key(provider.api_key), style="")
51
+
52
+ providers_table.add_row(status, name, protocol, base_url, api_key)
53
+
54
+ # Display models section
55
+ models_table = Table.grid(padding=(0, 1), expand=True)
56
+ models_table.add_column(width=2, no_wrap=True) # Status
57
+ models_table.add_column(overflow="fold", ratio=1) # Name
58
+ models_table.add_column(overflow="fold", ratio=2) # Model
59
+ models_table.add_column(overflow="fold", ratio=2) # Provider
60
+ models_table.add_column(overflow="fold", ratio=3) # Params
61
+
62
+ # Add header
63
+ models_table.add_row(
64
+ Text("", style="bold"),
65
+ Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
66
+ Text("Model", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
67
+ Text("Provider", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
68
+ Text("Params", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
69
+ )
70
+
71
+ # Add models
72
+ for model in config.model_list:
73
+ status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
74
+ if model.model_name == config.main_model:
75
+ status = Text("★", style=f"bold {ThemeKey.CONFIG_STATUS_PRIMARY}") # Mark main model
76
+
77
+ name = Text(
78
+ model.model_name,
79
+ style=ThemeKey.CONFIG_STATUS_PRIMARY
80
+ if model.model_name == config.main_model
81
+ else ThemeKey.CONFIG_ITEM_NAME,
82
+ )
83
+ model_name = Text(model.model_params.model or "N/A", style="")
84
+ provider = Text(model.provider, style="")
85
+ params: list[Text] = []
86
+ if model.model_params.thinking:
87
+ if model.model_params.thinking.reasoning_effort is not None:
88
+ params.append(
89
+ Text.assemble(
90
+ ("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
91
+ ": ",
92
+ model.model_params.thinking.reasoning_effort,
93
+ )
94
+ )
95
+ if model.model_params.thinking.reasoning_summary is not None:
96
+ params.append(
97
+ Text.assemble(
98
+ ("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
99
+ ": ",
100
+ model.model_params.thinking.reasoning_summary,
101
+ )
102
+ )
103
+ if model.model_params.thinking.budget_tokens is not None:
104
+ params.append(
105
+ Text.assemble(
106
+ ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
107
+ ": ",
108
+ str(model.model_params.thinking.budget_tokens),
109
+ )
110
+ )
111
+ if model.model_params.provider_routing:
112
+ params.append(
113
+ Text.assemble(
114
+ ("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
115
+ ": ",
116
+ model.model_params.provider_routing.model_dump_json(exclude_none=True),
117
+ )
118
+ )
119
+ if len(params) == 0:
120
+ params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
121
+ models_table.add_row(status, name, model_name, provider, Group(*params))
122
+
123
+ # Create panels and display
124
+ providers_panel = Panel(
125
+ providers_table,
126
+ title=Text("Providers Configuration", style="white bold"),
127
+ border_style=ThemeKey.CONFIG_PANEL_BORDER,
128
+ padding=(0, 1),
129
+ title_align="left",
130
+ )
131
+
132
+ models_panel = Panel(
133
+ models_table,
134
+ title=Text("Models Configuration", style="white bold"),
135
+ border_style=ThemeKey.CONFIG_PANEL_BORDER,
136
+ padding=(0, 1),
137
+ title_align="left",
138
+ )
139
+
140
+ console.print(providers_panel)
141
+ console.print()
142
+ console.print(models_panel)
143
+
144
+ # Display main model info
145
+ console.print()
146
+ console.print(
147
+ Text.assemble(
148
+ ("Default Model: ", "bold"),
149
+ (config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
150
+ )
151
+ )
152
+
153
+ for profile in iter_sub_agent_profiles():
154
+ sub_model_name = config.subagent_models.get(profile.name)
155
+ if not sub_model_name:
156
+ continue
157
+ console.print(
158
+ Text.assemble(
159
+ (f"{profile.name} Model: ", "bold"),
160
+ (sub_model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
161
+ )
162
+ )
@@ -0,0 +1,67 @@
1
+ from klaude_code.config.config import load_config
2
+ from klaude_code.trace import log
3
+ from klaude_code.ui.rich.searchable_text import SearchableFormattedList
4
+
5
+
6
+ def select_model_from_config(preferred: str | None = None) -> str | None:
7
+ """
8
+ Interactive single-choice model selector.
9
+ for `--select-model`
10
+ """
11
+ config = load_config()
12
+ assert config is not None
13
+ models = sorted(config.model_list, key=lambda m: m.model_name.lower())
14
+
15
+ if not models:
16
+ raise ValueError("No models configured. Please update your config.yaml")
17
+
18
+ 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
+
23
+ try:
24
+ import questionary
25
+
26
+ choices: list[questionary.Choice] = []
27
+
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}"),
35
+ ]
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
@@ -0,0 +1,133 @@
1
+ """Centralized configuration constants for klaude_code.
2
+
3
+ This module consolidates all magic numbers and configuration values
4
+ that were previously scattered across the codebase.
5
+ """
6
+
7
+ # =============================================================================
8
+ # Agent Configuration
9
+ # =============================================================================
10
+
11
+ # Timeout for waiting for the first event from LLM (seconds)
12
+ FIRST_EVENT_TIMEOUT_S = 200.0
13
+
14
+ # Maximum number of retry attempts for failed turns
15
+ MAX_FAILED_TURN_RETRIES = 10
16
+
17
+ # Initial delay before retrying a failed turn (seconds)
18
+ INITIAL_RETRY_DELAY_S = 1.0
19
+
20
+ # Maximum delay between retries (seconds)
21
+ MAX_RETRY_DELAY_S = 30.0
22
+
23
+ # Message shown when a tool call is cancelled by the user
24
+ CANCEL_OUTPUT = "[Request interrupted by user for tool use]"
25
+
26
+ # Default maximum tokens for LLM responses
27
+ DEFAULT_MAX_TOKENS = 32000
28
+
29
+ # Default temperature for LLM requests
30
+ DEFAULT_TEMPERATURE = 1.0
31
+
32
+ # Default thinking budget tokens for Anthropic models
33
+ DEFAULT_ANTHROPIC_THINKING_BUDGET_TOKENS = 2048
34
+
35
+ # Tool call count threshold for todo reminder
36
+ TODO_REMINDER_TOOL_CALL_THRESHOLD = 10
37
+
38
+
39
+ # =============================================================================
40
+ # Tool
41
+ # =============================================================================
42
+
43
+ # -- Read Tool --
44
+ # Maximum characters per line before truncation
45
+ READ_CHAR_LIMIT_PER_LINE = 2000
46
+
47
+ # 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
52
+
53
+ # Maximum file size in KB for text files
54
+ READ_MAX_KB = 256
55
+
56
+ # Maximum image file size in bytes (4MB)
57
+ READ_MAX_IMAGE_BYTES = 4 * 1024 * 1024
58
+
59
+ # -- Bash Tool --
60
+ # Default timeout for bash commands (milliseconds)
61
+ BASH_DEFAULT_TIMEOUT_MS = 120000
62
+
63
+ # -- Tool Output --
64
+ # Maximum length for tool output before truncation
65
+ TOOL_OUTPUT_MAX_LENGTH = 50000
66
+
67
+ # Characters to show from the beginning of truncated output
68
+ TOOL_OUTPUT_DISPLAY_HEAD = 10000
69
+
70
+ # Characters to show from the end of truncated output
71
+ TOOL_OUTPUT_DISPLAY_TAIL = 10000
72
+
73
+ # Directory for saving full truncated output
74
+ TOOL_OUTPUT_TRUNCATION_DIR = "/tmp/klaude"
75
+
76
+
77
+ # =============================================================================
78
+ # UI
79
+ # =============================================================================
80
+
81
+ # Width of line number prefix in diff display
82
+ DIFF_PREFIX_WIDTH = 4
83
+
84
+ # Maximum lines to show in diff output
85
+ MAX_DIFF_LINES = 1000
86
+
87
+ # Maximum length for invalid tool call display
88
+ INVALID_TOOL_CALL_MAX_LENGTH = 500
89
+
90
+ # Maximum line length for truncated display output
91
+ TRUNCATE_DISPLAY_MAX_LINE_LENGTH = 1000
92
+
93
+ # Maximum lines for truncated display output
94
+ TRUNCATE_DISPLAY_MAX_LINES = 10
95
+
96
+ # Maximum lines for sub-agent result display
97
+ SUB_AGENT_RESULT_MAX_LINES = 12
98
+
99
+
100
+ # UI refresh rate (frames per second) for debounced content streaming
101
+ UI_REFRESH_RATE_FPS = 20
102
+
103
+ # Number of lines to keep visible at bottom of markdown streaming window
104
+ MARKDOWN_STREAM_LIVE_WINDOW = 20
105
+
106
+ # Status shimmer animation
107
+ # Horizontal padding used when computing shimmer band position
108
+ STATUS_SHIMMER_PADDING = 10
109
+ # Duration in seconds for one full shimmer sweep across the text
110
+ STATUS_SHIMMER_SWEEP_SECONDS = 2
111
+ # Half-width of the shimmer band in characters
112
+ STATUS_SHIMMER_BAND_HALF_WIDTH = 5.0
113
+ # Scale factor applied to shimmer intensity when blending colors
114
+ STATUS_SHIMMER_ALPHA_SCALE = 0.7
115
+
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
120
+
121
+
122
+ # =============================================================================
123
+ # Debug / Logging
124
+ # =============================================================================
125
+
126
+ # Default debug log file path
127
+ DEFAULT_DEBUG_LOG_FILE = "debug.log"
128
+
129
+ # Maximum log file size before rotation (10MB)
130
+ LOG_MAX_BYTES = 10 * 1024 * 1024
131
+
132
+ # Number of backup log files to keep
133
+ LOG_BACKUP_COUNT = 3
File without changes