klaude-code 2.9.0__py3-none-any.whl → 2.10.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 (69) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/antigravity/oauth.py +33 -29
  3. klaude_code/auth/claude/oauth.py +34 -49
  4. klaude_code/cli/cost_cmd.py +4 -4
  5. klaude_code/cli/list_model.py +1 -2
  6. klaude_code/config/assets/builtin_config.yaml +17 -0
  7. klaude_code/const.py +4 -3
  8. klaude_code/core/agent_profile.py +2 -5
  9. klaude_code/core/bash_mode.py +276 -0
  10. klaude_code/core/executor.py +40 -7
  11. klaude_code/core/manager/llm_clients.py +1 -0
  12. klaude_code/core/manager/llm_clients_builder.py +2 -2
  13. klaude_code/core/memory.py +140 -0
  14. klaude_code/core/reminders.py +17 -89
  15. klaude_code/core/task.py +1 -1
  16. klaude_code/core/tool/file/read_tool.py +13 -2
  17. klaude_code/core/tool/shell/bash_tool.py +1 -1
  18. klaude_code/core/turn.py +10 -4
  19. klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
  20. klaude_code/llm/input_common.py +18 -0
  21. klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
  22. klaude_code/llm/{codex → openai_codex}/client.py +3 -3
  23. klaude_code/llm/openai_compatible/client.py +3 -1
  24. klaude_code/llm/openai_compatible/stream.py +19 -9
  25. klaude_code/llm/{responses → openai_responses}/client.py +1 -1
  26. klaude_code/llm/registry.py +3 -3
  27. klaude_code/llm/stream_parts.py +3 -1
  28. klaude_code/llm/usage.py +1 -1
  29. klaude_code/protocol/events.py +17 -1
  30. klaude_code/protocol/message.py +1 -0
  31. klaude_code/protocol/model.py +14 -1
  32. klaude_code/protocol/op.py +12 -0
  33. klaude_code/protocol/op_handler.py +5 -0
  34. klaude_code/session/session.py +22 -1
  35. klaude_code/tui/command/resume_cmd.py +1 -1
  36. klaude_code/tui/commands.py +15 -0
  37. klaude_code/tui/components/bash_syntax.py +4 -0
  38. klaude_code/tui/components/command_output.py +4 -5
  39. klaude_code/tui/components/developer.py +1 -3
  40. klaude_code/tui/components/diffs.py +3 -2
  41. klaude_code/tui/components/metadata.py +23 -26
  42. klaude_code/tui/components/rich/code_panel.py +31 -16
  43. klaude_code/tui/components/rich/markdown.py +44 -28
  44. klaude_code/tui/components/rich/status.py +2 -2
  45. klaude_code/tui/components/rich/theme.py +28 -16
  46. klaude_code/tui/components/tools.py +23 -0
  47. klaude_code/tui/components/user_input.py +49 -58
  48. klaude_code/tui/components/welcome.py +47 -2
  49. klaude_code/tui/display.py +15 -7
  50. klaude_code/tui/input/completers.py +8 -0
  51. klaude_code/tui/input/key_bindings.py +37 -1
  52. klaude_code/tui/input/prompt_toolkit.py +58 -31
  53. klaude_code/tui/machine.py +87 -49
  54. klaude_code/tui/renderer.py +148 -30
  55. klaude_code/tui/runner.py +22 -0
  56. klaude_code/tui/terminal/image.py +24 -3
  57. klaude_code/tui/terminal/notifier.py +11 -12
  58. klaude_code/tui/terminal/selector.py +1 -1
  59. klaude_code/ui/terminal/title.py +4 -2
  60. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
  61. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/RECORD +67 -66
  62. klaude_code/llm/bedrock/__init__.py +0 -3
  63. klaude_code/tui/components/assistant.py +0 -2
  64. /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
  65. /klaude_code/llm/{codex → openai_codex}/prompt_sync.py +0 -0
  66. /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
  67. /klaude_code/llm/{responses → openai_responses}/input.py +0 -0
  68. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
  69. {klaude_code-2.9.0.dist-info → klaude_code-2.10.0.dist-info}/entry_points.txt +0 -0
@@ -18,9 +18,11 @@ from klaude_code.config import load_config
18
18
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
19
19
  from klaude_code.core.agent import Agent
20
20
  from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
21
+ from klaude_code.core.bash_mode import run_bash_command
21
22
  from klaude_code.core.compaction import CompactionReason, run_compaction
22
23
  from klaude_code.core.loaded_skills import get_loaded_skill_names_by_location
23
24
  from klaude_code.core.manager import LLMClients, SubAgentManager
25
+ from klaude_code.core.memory import get_existing_memory_paths_by_location
24
26
  from klaude_code.llm.registry import create_llm_client
25
27
  from klaude_code.log import DebugType, log_debug
26
28
  from klaude_code.protocol import commands, events, message, model, op
@@ -134,18 +136,19 @@ class AgentRuntime:
134
136
  compact_llm_client=self._llm_clients.compact,
135
137
  )
136
138
 
137
- async for evt in agent.replay_history():
138
- await self._emit_event(evt)
139
-
140
139
  await self._emit_event(
141
140
  events.WelcomeEvent(
142
141
  session_id=session.id,
143
142
  work_dir=str(session.work_dir),
144
143
  llm_config=self._llm_clients.main.get_llm_config(),
145
144
  loaded_skills=get_loaded_skill_names_by_location(),
145
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=session.work_dir),
146
146
  )
147
147
  )
148
148
 
149
+ async for evt in agent.replay_history():
150
+ await self._emit_event(evt)
151
+
149
152
  self._agent = agent
150
153
  log_debug(
151
154
  f"Initialized agent for session: {session.id}",
@@ -179,6 +182,23 @@ class AgentRuntime:
179
182
  )
180
183
  self._task_manager.register(operation.id, task, operation.session_id)
181
184
 
185
+ async def run_bash(self, operation: op.RunBashOperation) -> None:
186
+ agent = await self.ensure_agent(operation.session_id)
187
+
188
+ existing_active = self._task_manager.get(operation.id)
189
+ if existing_active is not None and not existing_active.task.done():
190
+ raise RuntimeError(f"Active task already registered for operation {operation.id}")
191
+
192
+ task: asyncio.Task[None] = asyncio.create_task(
193
+ self._run_bash_task(
194
+ session=agent.session,
195
+ command=operation.command,
196
+ task_id=operation.id,
197
+ session_id=operation.session_id,
198
+ )
199
+ )
200
+ self._task_manager.register(operation.id, task, operation.session_id)
201
+
182
202
  async def continue_agent(self, operation: op.ContinueAgentOperation) -> None:
183
203
  """Continue agent execution without adding a new user message."""
184
204
  agent = await self.ensure_agent(operation.session_id)
@@ -230,6 +250,7 @@ class AgentRuntime:
230
250
  work_dir=str(agent.session.work_dir),
231
251
  llm_config=self._llm_clients.main.get_llm_config(),
232
252
  loaded_skills=get_loaded_skill_names_by_location(),
253
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=agent.session.work_dir),
233
254
  )
234
255
  )
235
256
 
@@ -249,18 +270,19 @@ class AgentRuntime:
249
270
  compact_llm_client=self._llm_clients.compact,
250
271
  )
251
272
 
252
- async for evt in agent.replay_history():
253
- await self._emit_event(evt)
254
-
255
273
  await self._emit_event(
256
274
  events.WelcomeEvent(
257
275
  session_id=target_session.id,
258
276
  work_dir=str(target_session.work_dir),
259
277
  llm_config=self._llm_clients.main.get_llm_config(),
260
278
  loaded_skills=get_loaded_skill_names_by_location(),
279
+ loaded_memories=get_existing_memory_paths_by_location(work_dir=target_session.work_dir),
261
280
  )
262
281
  )
263
282
 
283
+ async for evt in agent.replay_history():
284
+ await self._emit_event(evt)
285
+
264
286
  self._agent = agent
265
287
  log_debug(
266
288
  f"Resumed session: {target_session.id}",
@@ -359,6 +381,14 @@ class AgentRuntime:
359
381
  debug_type=DebugType.EXECUTION,
360
382
  )
361
383
 
384
+ async def _run_bash_task(self, *, session: Session, command: str, task_id: str, session_id: str) -> None:
385
+ await run_bash_command(
386
+ emit_event=self._emit_event,
387
+ session=session,
388
+ session_id=session_id,
389
+ command=command,
390
+ )
391
+
362
392
  async def _run_compaction_task(
363
393
  self,
364
394
  agent: Agent,
@@ -467,7 +497,7 @@ class ModelSwitcher:
467
497
  config.main_model = model_name
468
498
  await config.save()
469
499
 
470
- return llm_config, llm_client.model_name
500
+ return llm_config, model_name
471
501
 
472
502
  def change_thinking(self, agent: Agent, *, thinking: Thinking) -> Thinking | None:
473
503
  """Apply thinking configuration to the agent's active LLM config and persisted session."""
@@ -540,6 +570,9 @@ class ExecutorContext:
540
570
  async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
541
571
  await self._agent_runtime.run_agent(operation)
542
572
 
573
+ async def handle_run_bash(self, operation: op.RunBashOperation) -> None:
574
+ await self._agent_runtime.run_bash(operation)
575
+
543
576
  async def handle_continue_agent(self, operation: op.ContinueAgentOperation) -> None:
544
577
  await self._agent_runtime.continue_agent(operation)
545
578
 
@@ -19,6 +19,7 @@ class LLMClients:
19
19
  """Container for LLM clients used by main agent and sub-agents."""
20
20
 
21
21
  main: LLMClientABC
22
+ main_model_alias: str = ""
22
23
  sub_clients: dict[SubAgentType, LLMClientABC] = dataclass_field(default_factory=_default_sub_clients)
23
24
  compact: LLMClientABC | None = None
24
25
 
@@ -53,7 +53,7 @@ def build_llm_clients(
53
53
  compact_client = create_llm_client(compact_llm_config)
54
54
 
55
55
  if skip_sub_agents:
56
- return LLMClients(main=main_client, compact=compact_client)
56
+ return LLMClients(main=main_client, main_model_alias=model_name, compact=compact_client)
57
57
 
58
58
  helper = SubAgentModelHelper(config)
59
59
  sub_agent_configs = helper.build_sub_agent_client_configs()
@@ -63,4 +63,4 @@ def build_llm_clients(
63
63
  sub_llm_config = config.get_model_config(sub_model_name)
64
64
  sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
65
65
 
66
- return LLMClients(main=main_client, sub_clients=sub_clients, compact=compact_client)
66
+ return LLMClients(main=main_client, main_model_alias=model_name, sub_clients=sub_clients, compact=compact_client)
@@ -0,0 +1,140 @@
1
+ """Memory file loading and management.
2
+
3
+ This module handles CLAUDE.md and AGENTS.md memory files - discovery, loading,
4
+ and providing summaries for UI display.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+
10
+ from pydantic import BaseModel
11
+
12
+ MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md", "AGENT.md"]
13
+
14
+
15
+ class Memory(BaseModel):
16
+ """Represents a loaded memory file."""
17
+
18
+ path: str
19
+ instruction: str
20
+ content: str
21
+
22
+
23
+ def get_memory_paths(*, work_dir: Path) -> list[tuple[Path, str]]:
24
+ """Return all possible memory file paths with their descriptions."""
25
+ user_dirs = [Path.home() / ".claude", Path.home() / ".codex"]
26
+ project_dirs = [work_dir, work_dir / ".claude"]
27
+
28
+ paths: list[tuple[Path, str]] = []
29
+ for d in user_dirs:
30
+ for fname in MEMORY_FILE_NAMES:
31
+ paths.append((d / fname, "user's private global instructions for all projects"))
32
+ for d in project_dirs:
33
+ for fname in MEMORY_FILE_NAMES:
34
+ paths.append((d / fname, "project instructions, checked into the codebase"))
35
+ return paths
36
+
37
+
38
+ def get_existing_memory_files(*, work_dir: Path) -> dict[str, list[str]]:
39
+ """Return existing memory file paths grouped by location (user/project)."""
40
+ result: dict[str, list[str]] = {"user": [], "project": []}
41
+ work_dir = work_dir.resolve()
42
+
43
+ for memory_path, _instruction in get_memory_paths(work_dir=work_dir):
44
+ if memory_path.exists() and memory_path.is_file():
45
+ path_str = str(memory_path)
46
+ resolved = memory_path.resolve()
47
+ try:
48
+ resolved.relative_to(work_dir)
49
+ result["project"].append(path_str)
50
+ except ValueError:
51
+ result["user"].append(path_str)
52
+
53
+ return result
54
+
55
+
56
+ def get_existing_memory_paths_by_location(*, work_dir: Path) -> dict[str, list[str]]:
57
+ """Return existing memory file paths grouped by location for WelcomeEvent."""
58
+ result = get_existing_memory_files(work_dir=work_dir)
59
+ if not result.get("user") and not result.get("project"):
60
+ return {}
61
+ return result
62
+
63
+
64
+ def format_memory_content(memory: Memory) -> str:
65
+ """Format a single memory file content for display."""
66
+ return f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}"
67
+
68
+
69
+ def format_memories_reminder(memories: list[Memory], include_header: bool = True) -> str:
70
+ """Format memory files into a system reminder string."""
71
+ memories_str = "\n\n".join(format_memory_content(m) for m in memories)
72
+ if include_header:
73
+ return f"""<system-reminder>
74
+ Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
75
+
76
+ {memories_str}
77
+ </system-reminder>"""
78
+ return f"<system-reminder>{memories_str}\n</system-reminder>"
79
+
80
+
81
+ def discover_memory_files_near_paths(
82
+ paths: list[str],
83
+ *,
84
+ work_dir: Path,
85
+ is_memory_loaded: Callable[[str], bool],
86
+ mark_memory_loaded: Callable[[str], None],
87
+ ) -> list[Memory]:
88
+ """Discover and load CLAUDE.md/AGENTS.md from directories containing accessed files.
89
+
90
+ Args:
91
+ paths: List of file paths that have been accessed.
92
+ is_memory_loaded: Callback to check if a memory file is already loaded.
93
+ mark_memory_loaded: Callback to mark a memory file as loaded.
94
+
95
+ Returns:
96
+ List of newly discovered Memory objects.
97
+ """
98
+ memories: list[Memory] = []
99
+ work_dir = work_dir.resolve()
100
+ seen_memory_files: set[str] = set()
101
+
102
+ for p_str in paths:
103
+ p = Path(p_str)
104
+ full = (work_dir / p).resolve() if not p.is_absolute() else p.resolve()
105
+ try:
106
+ _ = full.relative_to(work_dir)
107
+ except ValueError:
108
+ continue
109
+
110
+ deepest_dir = full if full.is_dir() else full.parent
111
+
112
+ try:
113
+ rel_parts = deepest_dir.relative_to(work_dir).parts
114
+ except ValueError:
115
+ continue
116
+
117
+ current_dir = work_dir
118
+ for part in rel_parts:
119
+ current_dir = current_dir / part
120
+ for fname in MEMORY_FILE_NAMES:
121
+ mem_path = current_dir / fname
122
+ mem_path_str = str(mem_path)
123
+ if mem_path_str in seen_memory_files or is_memory_loaded(mem_path_str):
124
+ continue
125
+ if mem_path.exists() and mem_path.is_file():
126
+ try:
127
+ text = mem_path.read_text(encoding="utf-8", errors="replace")
128
+ except (PermissionError, UnicodeDecodeError, OSError):
129
+ continue
130
+ mark_memory_loaded(mem_path_str)
131
+ seen_memory_files.add(mem_path_str)
132
+ memories.append(
133
+ Memory(
134
+ path=mem_path_str,
135
+ instruction="project instructions, discovered near last accessed path",
136
+ content=text,
137
+ )
138
+ )
139
+
140
+ return memories
@@ -4,9 +4,13 @@ import shlex
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
 
7
- from pydantic import BaseModel
8
-
9
- from klaude_code.const import MEMORY_FILE_NAMES, REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
7
+ from klaude_code.const import REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
8
+ from klaude_code.core.memory import (
9
+ Memory,
10
+ discover_memory_files_near_paths,
11
+ format_memories_reminder,
12
+ get_memory_paths,
13
+ )
10
14
  from klaude_code.core.tool import BashTool, ReadTool, build_todo_context
11
15
  from klaude_code.core.tool.context import ToolContext
12
16
  from klaude_code.core.tool.file._utils import hash_text_sha256
@@ -382,28 +386,6 @@ def _compute_file_content_sha256(path: str) -> str | None:
382
386
  return None
383
387
 
384
388
 
385
- def get_memory_paths() -> list[tuple[Path, str]]:
386
- return [
387
- (
388
- Path.home() / ".claude" / "CLAUDE.md",
389
- "user's private global instructions for all projects",
390
- ),
391
- (
392
- Path.home() / ".codex" / "AGENTS.md",
393
- "user's private global instructions for all projects",
394
- ),
395
- (Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
396
- (Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
397
- (Path.cwd() / ".claude" / "CLAUDE.md", "project instructions, checked into the codebase"),
398
- ]
399
-
400
-
401
- class Memory(BaseModel):
402
- path: str
403
- instruction: str
404
- content: str
405
-
406
-
407
389
  def get_last_user_message_image_paths(session: Session) -> list[str]:
408
390
  """Get image file paths from the last user message in conversation history."""
409
391
  for item in reversed(session.conversation_history):
@@ -502,7 +484,7 @@ def _mark_memory_loaded(session: Session, path: str) -> None:
502
484
 
503
485
  async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
504
486
  """CLAUDE.md AGENTS.md"""
505
- memory_paths = get_memory_paths()
487
+ memory_paths = get_memory_paths(work_dir=session.work_dir)
506
488
  memories: list[Memory] = []
507
489
  for memory_path, instruction in memory_paths:
508
490
  path_str = str(memory_path)
@@ -514,21 +496,12 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
514
496
  except (PermissionError, UnicodeDecodeError, OSError):
515
497
  continue
516
498
  if len(memories) > 0:
517
- memories_str = "\n\n".join(
518
- [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
519
- )
520
499
  loaded_files = [
521
500
  model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
522
501
  for memory in memories
523
502
  ]
524
503
  return message.DeveloperMessage(
525
- parts=message.text_parts_from_str(
526
- f"""<system-reminder>
527
- Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
528
-
529
- {memories_str}
530
- </system-reminder>"""
531
- ),
504
+ parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=True)),
532
505
  ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
533
506
  )
534
507
  return None
@@ -546,68 +519,23 @@ async def last_path_memory_reminder(
546
519
  return None
547
520
 
548
521
  paths = list(session.file_tracker.keys())
549
- memories: list[Memory] = []
550
-
551
- cwd = Path.cwd().resolve()
552
- seen_memory_files: set[str] = set()
553
-
554
- for p_str in paths:
555
- p = Path(p_str)
556
- full = (cwd / p).resolve() if not p.is_absolute() else p.resolve()
557
- try:
558
- _ = full.relative_to(cwd)
559
- except ValueError:
560
- # Not under cwd; skip
561
- continue
562
-
563
- # Determine the deepest directory to scan (file parent or directory itself)
564
- deepest_dir = full if full.is_dir() else full.parent
565
-
566
- # Iterate each directory level from cwd to deepest_dir
567
- try:
568
- rel_parts = deepest_dir.relative_to(cwd).parts
569
- except ValueError:
570
- # Shouldn't happen due to check above, but guard anyway
571
- continue
572
-
573
- current_dir = cwd
574
- for part in rel_parts:
575
- current_dir = current_dir / part
576
- for fname in MEMORY_FILE_NAMES:
577
- mem_path = current_dir / fname
578
- mem_path_str = str(mem_path)
579
- if mem_path_str in seen_memory_files or _is_memory_loaded(session, mem_path_str):
580
- continue
581
- if mem_path.exists() and mem_path.is_file():
582
- try:
583
- text = mem_path.read_text(encoding="utf-8", errors="replace")
584
- except (PermissionError, UnicodeDecodeError, OSError):
585
- continue
586
- _mark_memory_loaded(session, mem_path_str)
587
- seen_memory_files.add(mem_path_str)
588
- memories.append(
589
- Memory(
590
- path=mem_path_str,
591
- instruction="project instructions, discovered near last accessed path",
592
- content=text,
593
- )
594
- )
522
+ memories = discover_memory_files_near_paths(
523
+ paths,
524
+ work_dir=session.work_dir,
525
+ is_memory_loaded=lambda p: _is_memory_loaded(session, p),
526
+ mark_memory_loaded=lambda p: _mark_memory_loaded(session, p),
527
+ )
595
528
 
596
529
  if len(memories) > 0:
597
- memories_str = "\n\n".join(
598
- [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
599
- )
600
530
  loaded_files = [
601
531
  model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
602
532
  for memory in memories
603
533
  ]
604
534
  return message.DeveloperMessage(
605
- parts=message.text_parts_from_str(
606
- f"""<system-reminder>{memories_str}
607
- </system-reminder>"""
608
- ),
535
+ parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=False)),
609
536
  ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
610
537
  )
538
+ return None
611
539
 
612
540
 
613
541
  ALL_REMINDERS = [
klaude_code/core/task.py CHANGED
@@ -210,7 +210,7 @@ class TaskExecutor:
210
210
  accumulated = self._metadata_accumulator.get_partial_item(task_duration_s)
211
211
  if accumulated is not None:
212
212
  session_id = self._context.session_ctx.session_id
213
- ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id, cancelled=True))
213
+ ui_events.append(events.TaskMetadataEvent(metadata=accumulated, session_id=session_id))
214
214
  self._context.session_ctx.append_history([accumulated])
215
215
 
216
216
  return ui_events
@@ -22,7 +22,7 @@ from klaude_code.core.tool.file._utils import file_exists, is_directory
22
22
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
23
23
  from klaude_code.core.tool.tool_registry import register
24
24
  from klaude_code.protocol import llm_param, message, model, tools
25
- from klaude_code.protocol.model import ImageUIExtra
25
+ from klaude_code.protocol.model import ImageUIExtra, ReadPreviewLine, ReadPreviewUIExtra
26
26
 
27
27
  _IMAGE_MIME_TYPES: dict[str, str] = {
28
28
  ".png": "image/png",
@@ -346,4 +346,15 @@ class ReadTool(ToolABC):
346
346
  read_result_str = "\n".join(lines_out)
347
347
  _track_file_access(context.file_tracker, file_path, content_sha256=read_result.content_sha256)
348
348
 
349
- return message.ToolResultMessage(status="success", output_text=read_result_str)
349
+ # When offset > 1, show a preview of the first 5 lines in UI
350
+ ui_extra = None
351
+ if args.offset is not None and args.offset > 1:
352
+ preview_count = 5
353
+ preview_lines = [
354
+ ReadPreviewLine(line_no=line_no, content=content)
355
+ for line_no, content in read_result.selected_lines[:preview_count]
356
+ ]
357
+ remaining = len(read_result.selected_lines) - len(preview_lines)
358
+ ui_extra = ReadPreviewUIExtra(lines=preview_lines, remaining_lines=remaining)
359
+
360
+ return message.ToolResultMessage(status="success", output_text=read_result_str, ui_extra=ui_extra)
@@ -342,7 +342,7 @@ class BashTool(ToolABC):
342
342
  if not combined:
343
343
  combined = f"Command exited with code {rc}"
344
344
  return message.ToolResultMessage(
345
- status="error",
345
+ status="success",
346
346
  # Preserve leading whitespace; only trim trailing newlines.
347
347
  output_text=combined.rstrip("\n"),
348
348
  )
klaude_code/core/turn.py CHANGED
@@ -194,11 +194,17 @@ class TurnExecutor:
194
194
  and self._turn_result.assistant_message is not None
195
195
  and self._turn_result.assistant_message.parts
196
196
  ):
197
- session_ctx.append_history([self._turn_result.assistant_message])
198
- # Add continuation prompt to avoid Anthropic thinking block requirement
199
- session_ctx.append_history(
200
- [message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])]
197
+ # Discard partial message if it only contains thinking parts
198
+ has_non_thinking = any(
199
+ not isinstance(part, message.ThinkingTextPart)
200
+ for part in self._turn_result.assistant_message.parts
201
201
  )
202
+ if has_non_thinking:
203
+ session_ctx.append_history([self._turn_result.assistant_message])
204
+ # Add continuation prompt to avoid Anthropic thinking block requirement
205
+ session_ctx.append_history(
206
+ [message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])]
207
+ )
202
208
  yield events.TurnEndEvent(session_id=session_ctx.session_id)
203
209
  raise TurnError(self._turn_result.stream_error.error)
204
210
 
@@ -0,0 +1,3 @@
1
+ from klaude_code.llm.bedrock_anthropic.client import BedrockClient
2
+
3
+ __all__ = ["BedrockClient"]
@@ -149,6 +149,14 @@ def build_assistant_common_fields(
149
149
  }
150
150
  for tc in tool_calls
151
151
  ]
152
+
153
+ thinking_parts = [part for part in msg.parts if isinstance(part, message.ThinkingTextPart)]
154
+ if thinking_parts:
155
+ thinking_text = "".join(part.text for part in thinking_parts)
156
+ reasoning_field = next((p.reasoning_field for p in thinking_parts if p.reasoning_field), None)
157
+ if thinking_text and reasoning_field:
158
+ result[reasoning_field] = thinking_text
159
+
152
160
  return result
153
161
 
154
162
 
@@ -185,4 +193,14 @@ def apply_config_defaults(param: "LLMCallParameter", config: "LLMConfigParameter
185
193
  param.verbosity = config.verbosity
186
194
  if param.thinking is None:
187
195
  param.thinking = config.thinking
196
+ if param.modalities is None:
197
+ param.modalities = config.modalities
198
+ if param.image_config is None:
199
+ param.image_config = config.image_config
200
+ elif config.image_config is not None:
201
+ # Merge field-level: param overrides config defaults
202
+ if param.image_config.aspect_ratio is None:
203
+ param.image_config.aspect_ratio = config.image_config.aspect_ratio
204
+ if param.image_config.image_size is None:
205
+ param.image_config.image_size = config.image_config.image_size
188
206
  return param
@@ -1,5 +1,5 @@
1
1
  """Codex LLM client using ChatGPT subscription."""
2
2
 
3
- from klaude_code.llm.codex.client import CodexClient
3
+ from klaude_code.llm.openai_codex.client import CodexClient
4
4
 
5
5
  __all__ = ["CodexClient"]
@@ -20,9 +20,9 @@ from klaude_code.const import (
20
20
  )
21
21
  from klaude_code.llm.client import LLMClientABC, LLMStreamABC
22
22
  from klaude_code.llm.input_common import apply_config_defaults
23
+ from klaude_code.llm.openai_responses.client import ResponsesLLMStream
24
+ from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
23
25
  from klaude_code.llm.registry import register
24
- from klaude_code.llm.responses.client import ResponsesLLMStream
25
- from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
26
26
  from klaude_code.llm.usage import MetadataTracker, error_llm_stream
27
27
  from klaude_code.log import DebugType, log_debug
28
28
  from klaude_code.protocol import llm_param
@@ -164,7 +164,7 @@ def _is_invalid_instruction_error(e: Exception) -> bool:
164
164
 
165
165
  def _invalidate_prompt_cache_for_model(model_id: str) -> None:
166
166
  """Invalidate the cached prompt for a model to force refresh."""
167
- from klaude_code.llm.codex.prompt_sync import invalidate_cache
167
+ from klaude_code.llm.openai_codex.prompt_sync import invalidate_cache
168
168
 
169
169
  log_debug(
170
170
  f"Invalidating prompt cache for model {model_id} due to invalid instruction error",
@@ -39,9 +39,11 @@ def build_payload(param: llm_param.LLMCallParameter) -> tuple[CompletionCreatePa
39
39
  "max_tokens": param.max_tokens,
40
40
  "tools": tools,
41
41
  "reasoning_effort": param.thinking.reasoning_effort if param.thinking else None,
42
- "verbosity": param.verbosity,
43
42
  }
44
43
 
44
+ if param.verbosity:
45
+ payload["verbosity"] = param.verbosity
46
+
45
47
  return payload, extra_body
46
48
 
47
49
 
@@ -76,9 +76,11 @@ class StreamStateManager:
76
76
  """Set the response ID once received from the stream."""
77
77
  self.response_id = response_id
78
78
 
79
- def append_thinking_text(self, text: str) -> None:
79
+ def append_thinking_text(self, text: str, *, reasoning_field: str | None = None) -> None:
80
80
  """Append thinking text, merging with the previous ThinkingTextPart when possible."""
81
- append_thinking_text_part(self.assistant_parts, text, model_id=self.param_model)
81
+ append_thinking_text_part(
82
+ self.assistant_parts, text, model_id=self.param_model, reasoning_field=reasoning_field
83
+ )
82
84
 
83
85
  def append_text(self, text: str) -> None:
84
86
  """Append assistant text, merging with the previous TextPart when possible."""
@@ -150,6 +152,7 @@ class ReasoningDeltaResult:
150
152
 
151
153
  handled: bool
152
154
  outputs: list[str | message.Part]
155
+ reasoning_field: str | None = None # Original field name: reasoning_content, reasoning, reasoning_text
153
156
 
154
157
 
155
158
  class ReasoningHandlerABC(ABC):
@@ -168,8 +171,11 @@ class ReasoningHandlerABC(ABC):
168
171
  """Flush buffered reasoning content (usually at stage transition/finalize)."""
169
172
 
170
173
 
174
+ REASONING_FIELDS = ("reasoning_content", "reasoning", "reasoning_text")
175
+
176
+
171
177
  class DefaultReasoningHandler(ReasoningHandlerABC):
172
- """Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning)."""
178
+ """Handles OpenAI-compatible reasoning fields (reasoning_content / reasoning / reasoning_text)."""
173
179
 
174
180
  def __init__(
175
181
  self,
@@ -179,16 +185,20 @@ class DefaultReasoningHandler(ReasoningHandlerABC):
179
185
  ) -> None:
180
186
  self._param_model = param_model
181
187
  self._response_id = response_id
188
+ self._reasoning_field: str | None = None
182
189
 
183
190
  def set_response_id(self, response_id: str | None) -> None:
184
191
  self._response_id = response_id
185
192
 
186
193
  def on_delta(self, delta: object) -> ReasoningDeltaResult:
187
- reasoning_content = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None) or ""
188
- if not reasoning_content:
189
- return ReasoningDeltaResult(handled=False, outputs=[])
190
- text = str(reasoning_content)
191
- return ReasoningDeltaResult(handled=True, outputs=[text])
194
+ for field_name in REASONING_FIELDS:
195
+ content = getattr(delta, field_name, None)
196
+ if content:
197
+ if self._reasoning_field is None:
198
+ self._reasoning_field = field_name
199
+ text = str(content)
200
+ return ReasoningDeltaResult(handled=True, outputs=[text], reasoning_field=self._reasoning_field)
201
+ return ReasoningDeltaResult(handled=False, outputs=[])
192
202
 
193
203
  def flush(self) -> list[message.Part]:
194
204
  return []
@@ -282,7 +292,7 @@ async def parse_chat_completions_stream(
282
292
  if not output:
283
293
  continue
284
294
  metadata_tracker.record_token()
285
- state.append_thinking_text(output)
295
+ state.append_thinking_text(output, reasoning_field=reasoning_result.reasoning_field)
286
296
  yield message.ThinkingTextDelta(content=output, response_id=state.response_id)
287
297
  else:
288
298
  state.assistant_parts.append(output)
@@ -11,8 +11,8 @@ from openai.types.responses.response_create_params import ResponseCreateParamsSt
11
11
  from klaude_code.const import LLM_HTTP_TIMEOUT_CONNECT, LLM_HTTP_TIMEOUT_READ, LLM_HTTP_TIMEOUT_TOTAL
12
12
  from klaude_code.llm.client import LLMClientABC, LLMStreamABC
13
13
  from klaude_code.llm.input_common import apply_config_defaults
14
+ from klaude_code.llm.openai_responses.input import convert_history_to_input, convert_tool_schema
14
15
  from klaude_code.llm.registry import register
15
- from klaude_code.llm.responses.input import convert_history_to_input, convert_tool_schema
16
16
  from klaude_code.llm.stream_parts import (
17
17
  append_text_part,
18
18
  append_thinking_text_part,