klaude-code 1.5.0__py3-none-any.whl → 1.7.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.
- klaude_code/cli/list_model.py +55 -4
- klaude_code/cli/main.py +3 -56
- klaude_code/cli/session_cmd.py +3 -2
- klaude_code/command/fork_session_cmd.py +220 -2
- klaude_code/command/refresh_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +21 -11
- klaude_code/config/assets/builtin_config.yaml +37 -2
- klaude_code/config/builtin_config.py +1 -0
- klaude_code/config/config.py +14 -0
- klaude_code/config/thinking.py +14 -0
- klaude_code/llm/anthropic/client.py +127 -114
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/registry.py +10 -5
- klaude_code/llm/usage.py +1 -1
- klaude_code/protocol/llm_param.py +9 -0
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/selector.py +32 -4
- klaude_code/session/session.py +20 -12
- klaude_code/ui/modes/repl/event_handler.py +22 -32
- klaude_code/ui/modes/repl/renderer.py +1 -1
- klaude_code/ui/renderers/developer.py +2 -2
- klaude_code/ui/renderers/metadata.py +8 -0
- klaude_code/ui/rich/markdown.py +41 -9
- klaude_code/ui/rich/status.py +83 -22
- klaude_code/ui/terminal/selector.py +72 -3
- {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/METADATA +33 -5
- {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/RECORD +33 -28
- {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.5.0.dist-info → klaude_code-1.7.0.dist-info}/entry_points.txt +0 -0
klaude_code/cli/list_model.py
CHANGED
|
@@ -6,7 +6,7 @@ from rich.table import Table
|
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
8
8
|
from klaude_code.config import Config
|
|
9
|
-
from klaude_code.config.config import ModelConfig, ProviderConfig
|
|
9
|
+
from klaude_code.config.config import ModelConfig, ProviderConfig, parse_env_var_syntax
|
|
10
10
|
from klaude_code.protocol.llm_param import LLMClientProtocol
|
|
11
11
|
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
12
12
|
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
@@ -94,6 +94,29 @@ def format_api_key_display(provider: ProviderConfig) -> Text:
|
|
|
94
94
|
return Text("N/A")
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def format_env_var_display(value: str | None) -> Text:
|
|
98
|
+
"""Format environment variable display with warning if not set."""
|
|
99
|
+
env_var, resolved = parse_env_var_syntax(value)
|
|
100
|
+
|
|
101
|
+
if env_var:
|
|
102
|
+
# Using ${ENV_VAR} syntax
|
|
103
|
+
if resolved:
|
|
104
|
+
return Text.assemble(
|
|
105
|
+
(f"${{{env_var}}} = ", "dim"),
|
|
106
|
+
(mask_api_key(resolved), ""),
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
return Text.assemble(
|
|
110
|
+
(f"${{{env_var}}} ", ""),
|
|
111
|
+
("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
|
|
112
|
+
)
|
|
113
|
+
elif value:
|
|
114
|
+
# Plain value
|
|
115
|
+
return Text(mask_api_key(value))
|
|
116
|
+
else:
|
|
117
|
+
return Text("N/A")
|
|
118
|
+
|
|
119
|
+
|
|
97
120
|
def _get_model_params_display(model: ModelConfig) -> list[Text]:
|
|
98
121
|
"""Get display elements for model parameters."""
|
|
99
122
|
params: list[Text] = []
|
|
@@ -162,15 +185,43 @@ def display_models_and_providers(config: Config):
|
|
|
162
185
|
format_api_key_display(provider),
|
|
163
186
|
)
|
|
164
187
|
|
|
188
|
+
# AWS Bedrock parameters
|
|
189
|
+
if provider.protocol == LLMClientProtocol.BEDROCK:
|
|
190
|
+
if provider.aws_access_key:
|
|
191
|
+
provider_info.add_row(
|
|
192
|
+
Text("AWS Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
193
|
+
format_env_var_display(provider.aws_access_key),
|
|
194
|
+
)
|
|
195
|
+
if provider.aws_secret_key:
|
|
196
|
+
provider_info.add_row(
|
|
197
|
+
Text("AWS Secret:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
198
|
+
format_env_var_display(provider.aws_secret_key),
|
|
199
|
+
)
|
|
200
|
+
if provider.aws_region:
|
|
201
|
+
provider_info.add_row(
|
|
202
|
+
Text("AWS Region:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
203
|
+
format_env_var_display(provider.aws_region),
|
|
204
|
+
)
|
|
205
|
+
if provider.aws_session_token:
|
|
206
|
+
provider_info.add_row(
|
|
207
|
+
Text("AWS Token:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
208
|
+
format_env_var_display(provider.aws_session_token),
|
|
209
|
+
)
|
|
210
|
+
if provider.aws_profile:
|
|
211
|
+
provider_info.add_row(
|
|
212
|
+
Text("AWS Profile:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
213
|
+
format_env_var_display(provider.aws_profile),
|
|
214
|
+
)
|
|
215
|
+
|
|
165
216
|
# Check if provider has valid API key
|
|
166
217
|
provider_available = not provider.is_api_key_missing()
|
|
167
218
|
|
|
168
219
|
# Models table for this provider
|
|
169
220
|
models_table = Table.grid(padding=(0, 1), expand=True)
|
|
170
221
|
models_table.add_column(width=2, no_wrap=True) # Status
|
|
171
|
-
models_table.add_column(overflow="fold", ratio=
|
|
172
|
-
models_table.add_column(overflow="fold", ratio=
|
|
173
|
-
models_table.add_column(overflow="fold", ratio=
|
|
222
|
+
models_table.add_column(overflow="fold", ratio=2) # Name
|
|
223
|
+
models_table.add_column(overflow="fold", ratio=3) # Model
|
|
224
|
+
models_table.add_column(overflow="fold", ratio=4) # Params
|
|
174
225
|
|
|
175
226
|
# Add header
|
|
176
227
|
models_table.add_row(
|
klaude_code/cli/main.py
CHANGED
|
@@ -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.
|
|
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 =
|
|
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
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -22,8 +22,9 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
|
|
|
22
22
|
log(f"Sessions to delete ({len(sessions)}):")
|
|
23
23
|
for s in sessions:
|
|
24
24
|
msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
first_msg_text = s.user_messages[0] if s.user_messages else ""
|
|
26
|
+
first_msg = first_msg_text.strip().replace("\n", " ")[:50]
|
|
27
|
+
if len(first_msg_text) > 50:
|
|
27
28
|
first_msg += "..."
|
|
28
29
|
log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
|
|
29
30
|
|
|
@@ -1,5 +1,182 @@
|
|
|
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
|
+
|
|
1
8
|
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
2
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"
|
|
3
180
|
|
|
4
181
|
|
|
5
182
|
class ForkSessionCommand(CommandABC):
|
|
@@ -13,6 +190,10 @@ class ForkSessionCommand(CommandABC):
|
|
|
13
190
|
def summary(self) -> str:
|
|
14
191
|
return "Fork the current session and show a resume-by-id command"
|
|
15
192
|
|
|
193
|
+
@property
|
|
194
|
+
def is_interactive(self) -> bool:
|
|
195
|
+
return True
|
|
196
|
+
|
|
16
197
|
async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
|
|
17
198
|
del user_input # unused
|
|
18
199
|
|
|
@@ -26,13 +207,50 @@ class ForkSessionCommand(CommandABC):
|
|
|
26
207
|
)
|
|
27
208
|
return CommandResult(events=[event], persist_user_input=False, persist_events=False)
|
|
28
209
|
|
|
29
|
-
|
|
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)
|
|
30
245
|
await new_session.wait_for_flush()
|
|
31
246
|
|
|
247
|
+
# Build result message
|
|
248
|
+
fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
|
|
249
|
+
|
|
32
250
|
event = events.DeveloperMessageEvent(
|
|
33
251
|
session_id=agent.session.id,
|
|
34
252
|
item=model.DeveloperMessageItem(
|
|
35
|
-
content=f"Session forked
|
|
253
|
+
content=f"Session forked ({fork_description}). New session id: {new_session.id}",
|
|
36
254
|
command_output=model.CommandOutput(
|
|
37
255
|
command_name=self.name,
|
|
38
256
|
ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
|
|
@@ -23,7 +23,7 @@ class RefreshTerminalCommand(CommandABC):
|
|
|
23
23
|
|
|
24
24
|
os.system("cls" if os.name == "nt" else "clear")
|
|
25
25
|
|
|
26
|
-
|
|
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", "
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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=
|
|
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(
|
|
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,
|
|
@@ -7,7 +7,7 @@ provider_list:
|
|
|
7
7
|
protocol: anthropic
|
|
8
8
|
api_key: ${ANTHROPIC_API_KEY}
|
|
9
9
|
model_list:
|
|
10
|
-
- model_name: sonnet
|
|
10
|
+
- model_name: sonnet@ant
|
|
11
11
|
model_params:
|
|
12
12
|
model: claude-sonnet-4-5-20250929
|
|
13
13
|
context_limit: 200000
|
|
@@ -18,7 +18,7 @@ provider_list:
|
|
|
18
18
|
output: 15.0
|
|
19
19
|
cache_read: 0.3
|
|
20
20
|
cache_write: 3.75
|
|
21
|
-
- model_name: opus
|
|
21
|
+
- model_name: opus@ant
|
|
22
22
|
model_params:
|
|
23
23
|
model: claude-opus-4-5-20251101
|
|
24
24
|
context_limit: 200000
|
|
@@ -194,6 +194,41 @@ provider_list:
|
|
|
194
194
|
output: 1.74
|
|
195
195
|
cache_read: 0.04
|
|
196
196
|
|
|
197
|
+
- provider_name: google
|
|
198
|
+
protocol: google
|
|
199
|
+
api_key: ${GOOGLE_API_KEY}
|
|
200
|
+
model_list:
|
|
201
|
+
- model_name: gemini-pro@google
|
|
202
|
+
model_params:
|
|
203
|
+
model: gemini-3-pro-preview
|
|
204
|
+
context_limit: 1048576
|
|
205
|
+
cost:
|
|
206
|
+
input: 2.0
|
|
207
|
+
output: 12.0
|
|
208
|
+
cache_read: 0.2
|
|
209
|
+
- model_name: gemini-flash@google
|
|
210
|
+
model_params:
|
|
211
|
+
model: gemini-3-flash-preview
|
|
212
|
+
context_limit: 1048576
|
|
213
|
+
cost:
|
|
214
|
+
input: 0.5
|
|
215
|
+
output: 3.0
|
|
216
|
+
cache_read: 0.05
|
|
217
|
+
- provider_name: bedrock
|
|
218
|
+
protocol: bedrock
|
|
219
|
+
aws_access_key: ${AWS_ACCESS_KEY_ID}
|
|
220
|
+
aws_secret_key: ${AWS_SECRET_ACCESS_KEY}
|
|
221
|
+
aws_region: ${AWS_REGION}
|
|
222
|
+
model_list:
|
|
223
|
+
- model_name: sonnet@bedrock
|
|
224
|
+
model_params:
|
|
225
|
+
model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|
226
|
+
context_limit: 200000
|
|
227
|
+
cost:
|
|
228
|
+
input: 3.0
|
|
229
|
+
output: 15.0
|
|
230
|
+
cache_read: 0.3
|
|
231
|
+
cache_write: 3.75
|
|
197
232
|
- provider_name: deepseek
|
|
198
233
|
protocol: anthropic
|
|
199
234
|
api_key: ${DEEPSEEK_API_KEY}
|
klaude_code/config/config.py
CHANGED
|
@@ -77,6 +77,7 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
|
|
|
77
77
|
"""Check if the API key is missing (either not set or env var not found).
|
|
78
78
|
|
|
79
79
|
For codex protocol, checks OAuth login status instead of API key.
|
|
80
|
+
For bedrock protocol, checks AWS credentials instead of API key.
|
|
80
81
|
"""
|
|
81
82
|
from klaude_code.protocol.llm_param import LLMClientProtocol
|
|
82
83
|
|
|
@@ -89,6 +90,19 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
|
|
|
89
90
|
# Consider available if logged in and token not expired
|
|
90
91
|
return state is None or state.is_expired()
|
|
91
92
|
|
|
93
|
+
if self.protocol == LLMClientProtocol.BEDROCK:
|
|
94
|
+
# Bedrock uses AWS credentials, not API key. Region is always required.
|
|
95
|
+
_, resolved_profile = parse_env_var_syntax(self.aws_profile)
|
|
96
|
+
_, resolved_region = parse_env_var_syntax(self.aws_region)
|
|
97
|
+
|
|
98
|
+
# When using profile, we still need region to initialize the client.
|
|
99
|
+
if resolved_profile:
|
|
100
|
+
return resolved_region is None
|
|
101
|
+
|
|
102
|
+
_, resolved_access_key = parse_env_var_syntax(self.aws_access_key)
|
|
103
|
+
_, resolved_secret_key = parse_env_var_syntax(self.aws_secret_key)
|
|
104
|
+
return resolved_region is None or resolved_access_key is None or resolved_secret_key is None
|
|
105
|
+
|
|
92
106
|
return self.get_resolved_api_key() is None
|
|
93
107
|
|
|
94
108
|
|
klaude_code/config/thinking.py
CHANGED
|
@@ -121,6 +121,13 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
|
|
|
121
121
|
return f"enabled (budget_tokens={thinking.budget_tokens})"
|
|
122
122
|
return "not set"
|
|
123
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
|
+
|
|
124
131
|
return "unknown protocol"
|
|
125
132
|
|
|
126
133
|
|
|
@@ -230,6 +237,13 @@ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPi
|
|
|
230
237
|
current_value=_get_current_budget_value(thinking),
|
|
231
238
|
)
|
|
232
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
|
+
|
|
233
247
|
return None
|
|
234
248
|
|
|
235
249
|
|