klaude-code 2.0.0__py3-none-any.whl → 2.0.2__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 (50) hide show
  1. klaude_code/cli/cost_cmd.py +1 -1
  2. klaude_code/cli/runtime.py +1 -8
  3. klaude_code/command/debug_cmd.py +1 -1
  4. klaude_code/command/export_online_cmd.py +4 -4
  5. klaude_code/command/fork_session_cmd.py +6 -6
  6. klaude_code/command/help_cmd.py +1 -1
  7. klaude_code/command/model_cmd.py +1 -1
  8. klaude_code/command/registry.py +10 -1
  9. klaude_code/command/release_notes_cmd.py +1 -1
  10. klaude_code/command/resume_cmd.py +2 -2
  11. klaude_code/command/status_cmd.py +2 -2
  12. klaude_code/command/terminal_setup_cmd.py +2 -2
  13. klaude_code/command/thinking_cmd.py +1 -1
  14. klaude_code/config/assets/builtin_config.yaml +4 -0
  15. klaude_code/const.py +5 -3
  16. klaude_code/core/executor.py +15 -36
  17. klaude_code/core/reminders.py +55 -68
  18. klaude_code/core/tool/__init__.py +0 -2
  19. klaude_code/core/tool/file/edit_tool.py +3 -2
  20. klaude_code/core/tool/todo/todo_write_tool.py +1 -2
  21. klaude_code/core/tool/tool_registry.py +3 -3
  22. klaude_code/protocol/events.py +1 -0
  23. klaude_code/protocol/message.py +3 -11
  24. klaude_code/protocol/model.py +79 -13
  25. klaude_code/protocol/op.py +0 -13
  26. klaude_code/protocol/op_handler.py +0 -5
  27. klaude_code/protocol/sub_agent/explore.py +0 -15
  28. klaude_code/protocol/sub_agent/task.py +1 -1
  29. klaude_code/protocol/sub_agent/web.py +1 -17
  30. klaude_code/protocol/tools.py +0 -1
  31. klaude_code/ui/modes/exec/display.py +2 -3
  32. klaude_code/ui/modes/repl/display.py +1 -1
  33. klaude_code/ui/modes/repl/event_handler.py +2 -10
  34. klaude_code/ui/modes/repl/input_prompt_toolkit.py +5 -1
  35. klaude_code/ui/modes/repl/key_bindings.py +135 -1
  36. klaude_code/ui/modes/repl/renderer.py +2 -1
  37. klaude_code/ui/renderers/bash_syntax.py +36 -4
  38. klaude_code/ui/renderers/common.py +8 -6
  39. klaude_code/ui/renderers/developer.py +113 -97
  40. klaude_code/ui/renderers/metadata.py +28 -15
  41. klaude_code/ui/renderers/tools.py +17 -59
  42. klaude_code/ui/rich/markdown.py +69 -11
  43. klaude_code/ui/rich/theme.py +22 -17
  44. klaude_code/ui/terminal/selector.py +36 -17
  45. {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/METADATA +1 -1
  46. {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/RECORD +48 -50
  47. klaude_code/core/tool/file/move_tool.md +0 -41
  48. klaude_code/core/tool/file/move_tool.py +0 -435
  49. {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/WHEEL +0 -0
  50. {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/entry_points.txt +0 -0
@@ -179,7 +179,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
179
179
  show_header=True,
180
180
  header_style="bold",
181
181
  border_style="bright_black dim",
182
- padding=(0, 1, 0, 2),
182
+ padding=(0, 1, 0, 1),
183
183
  box=ASCII_HORIZONAL,
184
184
  )
185
185
 
@@ -25,7 +25,6 @@ from klaude_code.ui.modes.repl import build_repl_status_snapshot
25
25
  from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
26
26
  from klaude_code.ui.terminal.color import is_light_terminal_background
27
27
  from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
28
- from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
29
28
 
30
29
 
31
30
  class PrintCapable(Protocol):
@@ -269,11 +268,6 @@ async def cleanup_app_components(components: AppComponents) -> None:
269
268
  await components.event_queue.put(events.EndEvent())
270
269
  await components.display_task
271
270
  finally:
272
- # Always attempt to clear Ghostty progress bar and restore cursor visibility
273
- # Best-effort only; never fail cleanup due to OSC errors
274
- with contextlib.suppress(Exception):
275
- emit_osc94(OSC94States.HIDDEN)
276
-
277
271
  # Ensure the terminal cursor is visible even if Rich's Status spinner
278
272
  # did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
279
273
  # If this fails the shell can still recover via `reset`/`stty sane`.
@@ -452,8 +446,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
452
446
  print(MSG, file=sys.stderr)
453
447
 
454
448
  def _hide_progress() -> None:
455
- with contextlib.suppress(Exception):
456
- emit_osc94(OSC94States.HIDDEN)
449
+ return
457
450
 
458
451
  restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
459
452
 
@@ -72,7 +72,7 @@ class DebugCommand(CommandABC):
72
72
  session_id=agent.session.id,
73
73
  item=message.DeveloperMessage(
74
74
  parts=message.text_parts_from_str(content),
75
- command_output=model.CommandOutput(command_name=self.name),
75
+ ui_extra=model.build_command_output_extra(self.name),
76
76
  ),
77
77
  )
78
78
  ]
@@ -42,7 +42,7 @@ class ExportOnlineCommand(CommandABC):
42
42
  session_id=agent.session.id,
43
43
  item=message.DeveloperMessage(
44
44
  parts=message.text_parts_from_str("surge.sh CLI not found. Install with: npm install -g surge"),
45
- command_output=model.CommandOutput(command_name=self.name, is_error=True),
45
+ ui_extra=model.build_command_output_extra(self.name, is_error=True),
46
46
  ),
47
47
  )
48
48
  return CommandResult(events=[event])
@@ -59,7 +59,7 @@ class ExportOnlineCommand(CommandABC):
59
59
  session_id=agent.session.id,
60
60
  item=message.DeveloperMessage(
61
61
  parts=message.text_parts_from_str(f"Not logged in to surge.sh. Please run: {login_cmd}"),
62
- command_output=model.CommandOutput(command_name=self.name, is_error=True),
62
+ ui_extra=model.build_command_output_extra(self.name, is_error=True),
63
63
  ),
64
64
  )
65
65
  return CommandResult(events=[event])
@@ -73,7 +73,7 @@ class ExportOnlineCommand(CommandABC):
73
73
  session_id=agent.session.id,
74
74
  item=message.DeveloperMessage(
75
75
  parts=message.text_parts_from_str(f"Session deployed to: {url}"),
76
- command_output=model.CommandOutput(command_name=self.name),
76
+ ui_extra=model.build_command_output_extra(self.name),
77
77
  ),
78
78
  )
79
79
  return CommandResult(events=[event])
@@ -84,7 +84,7 @@ class ExportOnlineCommand(CommandABC):
84
84
  session_id=agent.session.id,
85
85
  item=message.DeveloperMessage(
86
86
  parts=message.text_parts_from_str(f"Failed to deploy session: {exc}\n{traceback.format_exc()}"),
87
- command_output=model.CommandOutput(command_name=self.name, is_error=True),
87
+ ui_extra=model.build_command_output_extra(self.name, is_error=True),
88
88
  ),
89
89
  )
90
90
  return CommandResult(events=[event])
@@ -207,7 +207,7 @@ class ForkSessionCommand(CommandABC):
207
207
  session_id=agent.session.id,
208
208
  item=message.DeveloperMessage(
209
209
  parts=message.text_parts_from_str("(no messages to fork)"),
210
- command_output=model.CommandOutput(command_name=self.name),
210
+ ui_extra=model.build_command_output_extra(self.name),
211
211
  ),
212
212
  )
213
213
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
@@ -227,8 +227,8 @@ class ForkSessionCommand(CommandABC):
227
227
  session_id=agent.session.id,
228
228
  item=message.DeveloperMessage(
229
229
  parts=message.text_parts_from_str(f"Session forked successfully. New session id: {new_session.id}"),
230
- command_output=model.CommandOutput(
231
- command_name=self.name,
230
+ ui_extra=model.build_command_output_extra(
231
+ self.name,
232
232
  ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
233
233
  ),
234
234
  ),
@@ -243,7 +243,7 @@ class ForkSessionCommand(CommandABC):
243
243
  session_id=agent.session.id,
244
244
  item=message.DeveloperMessage(
245
245
  parts=message.text_parts_from_str("(fork cancelled)"),
246
- command_output=model.CommandOutput(command_name=self.name),
246
+ ui_extra=model.build_command_output_extra(self.name),
247
247
  ),
248
248
  )
249
249
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
@@ -264,8 +264,8 @@ class ForkSessionCommand(CommandABC):
264
264
  parts=message.text_parts_from_str(
265
265
  f"Session forked ({fork_description}). New session id: {new_session.id}"
266
266
  ),
267
- command_output=model.CommandOutput(
268
- command_name=self.name,
267
+ ui_extra=model.build_command_output_extra(
268
+ self.name,
269
269
  ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
270
270
  ),
271
271
  ),
@@ -43,7 +43,7 @@ Available slash commands:"""
43
43
  session_id=agent.session.id,
44
44
  item=message.DeveloperMessage(
45
45
  parts=message.text_parts_from_str("\n".join(lines)),
46
- command_output=model.CommandOutput(command_name=self.name),
46
+ ui_extra=model.build_command_output_extra(self.name),
47
47
  ),
48
48
  )
49
49
 
@@ -76,7 +76,7 @@ class ModelCommand(CommandABC):
76
76
  session_id=agent.session.id,
77
77
  item=message.DeveloperMessage(
78
78
  parts=message.text_parts_from_str("(no change)"),
79
- command_output=model.CommandOutput(command_name=self.name),
79
+ ui_extra=model.build_command_output_extra(self.name),
80
80
  ),
81
81
  )
82
82
  ]
@@ -183,6 +183,15 @@ async def dispatch_command(user_input: message.UserInputPayload, agent: Agent, *
183
183
  if isinstance(command_identifier, commands.CommandName)
184
184
  else None
185
185
  )
186
+ ui_extra = (
187
+ model.build_command_output_extra(
188
+ command_output.command_name,
189
+ ui_extra=command_output.ui_extra,
190
+ is_error=command_output.is_error,
191
+ )
192
+ if command_output is not None
193
+ else None
194
+ )
186
195
  return CommandResult(
187
196
  events=[
188
197
  events.DeveloperMessageEvent(
@@ -191,7 +200,7 @@ async def dispatch_command(user_input: message.UserInputPayload, agent: Agent, *
191
200
  parts=message.text_parts_from_str(
192
201
  f"Command {command_identifier} error: [{e.__class__.__name__}] {e!s}"
193
202
  ),
194
- command_output=command_output,
203
+ ui_extra=ui_extra,
195
204
  ),
196
205
  )
197
206
  ]
@@ -77,7 +77,7 @@ class ReleaseNotesCommand(CommandABC):
77
77
  session_id=agent.session.id,
78
78
  item=message.DeveloperMessage(
79
79
  parts=message.text_parts_from_str(content),
80
- command_output=model.CommandOutput(command_name=self.name),
80
+ ui_extra=model.build_command_output_extra(self.name),
81
81
  ),
82
82
  )
83
83
 
@@ -92,7 +92,7 @@ class ResumeCommand(CommandABC):
92
92
  parts=message.text_parts_from_str(
93
93
  "Cannot resume: current session already has messages. Use `klaude -r` to start a new instance with session selection."
94
94
  ),
95
- command_output=model.CommandOutput(command_name=self.name, is_error=True),
95
+ ui_extra=model.build_command_output_extra(self.name, is_error=True),
96
96
  ),
97
97
  )
98
98
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
@@ -103,7 +103,7 @@ class ResumeCommand(CommandABC):
103
103
  session_id=agent.session.id,
104
104
  item=message.DeveloperMessage(
105
105
  parts=message.text_parts_from_str("(no session selected)"),
106
- command_output=model.CommandOutput(command_name=self.name),
106
+ ui_extra=model.build_command_output_extra(self.name),
107
107
  ),
108
108
  )
109
109
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
@@ -141,8 +141,8 @@ class StatusCommand(CommandABC):
141
141
  session_id=session.id,
142
142
  item=message.DeveloperMessage(
143
143
  parts=message.text_parts_from_str(format_status_content(aggregated)),
144
- command_output=model.CommandOutput(
145
- command_name=self.name,
144
+ ui_extra=model.build_command_output_extra(
145
+ self.name,
146
146
  ui_extra=model.SessionStatusUIExtra(
147
147
  usage=aggregated.total,
148
148
  task_count=aggregated.task_count,
@@ -229,7 +229,7 @@ class TerminalSetupCommand(CommandABC):
229
229
  session_id=agent.session.id,
230
230
  item=message.DeveloperMessage(
231
231
  parts=message.text_parts_from_str(msg),
232
- command_output=model.CommandOutput(command_name=self.name, is_error=False),
232
+ ui_extra=model.build_command_output_extra(self.name),
233
233
  ),
234
234
  )
235
235
  ]
@@ -243,7 +243,7 @@ class TerminalSetupCommand(CommandABC):
243
243
  session_id=agent.session.id,
244
244
  item=message.DeveloperMessage(
245
245
  parts=message.text_parts_from_str(msg),
246
- command_output=model.CommandOutput(command_name=self.name, is_error=True),
246
+ ui_extra=model.build_command_output_extra(self.name, is_error=True),
247
247
  ),
248
248
  )
249
249
  ]
@@ -82,7 +82,7 @@ class ThinkingCommand(CommandABC):
82
82
  session_id=agent.session.id,
83
83
  item=message.DeveloperMessage(
84
84
  parts=message.text_parts_from_str("(no change)"),
85
- command_output=model.CommandOutput(command_name=self.name),
85
+ ui_extra=model.build_command_output_extra(self.name),
86
86
  ),
87
87
  )
88
88
  ]
@@ -347,6 +347,10 @@ provider_list:
347
347
  reasoning_effort: medium
348
348
  context_limit: 400000
349
349
  max_tokens: 128000
350
+ cost:
351
+ input: 1.75
352
+ output: 14.0
353
+ cache_read: 0.17
350
354
 
351
355
  sub_agent_models:
352
356
  ImageGen: nano-banana-pro@or
klaude_code/const.py CHANGED
@@ -122,13 +122,14 @@ TOOL_OUTPUT_TRUNCATION_DIR = "/tmp/klaude" # Directory for saving full truncate
122
122
  TAB_EXPAND_WIDTH = 8 # Tab expansion width for text rendering
123
123
  DIFF_PREFIX_WIDTH = 4 # Width of line number prefix in diff display
124
124
  MAX_DIFF_LINES = 500 # Maximum lines to show in diff output
125
- INVALID_TOOL_CALL_MAX_LENGTH = 500 # Maximum length for invalid tool call display
126
- TRUNCATE_DISPLAY_MAX_LINE_LENGTH = 1000 # Maximum line length for truncated display
127
- TRUNCATE_DISPLAY_MAX_LINES = 6 # Maximum lines for truncated display
125
+ INVALID_TOOL_CALL_MAX_LENGTH = 200 # Maximum length for invalid tool call display
126
+ TRUNCATE_DISPLAY_MAX_LINE_LENGTH = 200 # Maximum line length for truncated display
127
+ TRUNCATE_DISPLAY_MAX_LINES = 4 # Maximum lines for truncated display
128
128
  MIN_HIDDEN_LINES_FOR_INDICATOR = 5 # Minimum hidden lines before showing truncation indicator
129
129
  SUB_AGENT_RESULT_MAX_LINES = 10 # Maximum lines for sub-agent result display
130
130
  TRUNCATE_HEAD_MAX_LINES = 2 # Maximum lines for sub-agent error display
131
131
  BASH_OUTPUT_PANEL_THRESHOLD = 10 # Bash output line threshold for CodePanel display
132
+ BASH_MULTILINE_STRING_TRUNCATE_MAX_LINES = 2 # Max lines shown for heredoc / multiline string tokens in bash tool calls
132
133
  URL_TRUNCATE_MAX_LENGTH = 400 # Maximum length for URL truncation in display
133
134
  QUERY_DISPLAY_TRUNCATE_LENGTH = 80 # Maximum length for search query display
134
135
  NOTIFY_COMPACT_LIMIT = 160 # Maximum length for notification body text
@@ -141,6 +142,7 @@ NOTIFY_COMPACT_LIMIT = 160 # Maximum length for notification body text
141
142
  UI_REFRESH_RATE_FPS = 10 # UI refresh rate (frames per second)
142
143
  CROP_ABOVE_LIVE_REFRESH_PER_SECOND = 4.0 # CropAboveLive default refresh rate
143
144
  MARKDOWN_STREAM_LIVE_REPAINT_ENABLED = True # Enable live area for streaming markdown
145
+ MARKDOWN_STREAM_SYNCHRONIZED_OUTPUT_ENABLED = True # Use terminal "Synchronized Output" to reduce flicker
144
146
  STREAM_MAX_HEIGHT_SHRINK_RESET_LINES = 20 # Reset stream height ceiling after this shrinkage
145
147
  MARKDOWN_LEFT_MARGIN = 2 # Left margin (columns) for markdown rendering
146
148
  MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
@@ -176,35 +176,6 @@ class ExecutorContext:
176
176
  """Initialize an agent for a session and replay history to UI."""
177
177
  await self._ensure_agent(operation.session_id)
178
178
 
179
- async def handle_user_input(self, operation: op.UserInputOperation) -> None:
180
- """Handle a user input operation.
181
-
182
- Core should not parse slash commands. The UI/CLI layer is responsible for
183
- turning raw user input into one or more operations.
184
- """
185
-
186
- if operation.session_id is None:
187
- raise ValueError("session_id cannot be None")
188
-
189
- session_id = operation.session_id
190
- agent = await self._ensure_agent(session_id)
191
- user_input = operation.input
192
-
193
- await self.emit_event(
194
- events.UserMessageEvent(content=user_input.text, session_id=session_id, images=user_input.images)
195
- )
196
- agent.session.append_history(
197
- [message.UserMessage(parts=message.parts_from_text_and_images(user_input.text, user_input.images))]
198
- )
199
-
200
- await self.handle_run_agent(
201
- op.RunAgentOperation(
202
- id=operation.id,
203
- session_id=session_id,
204
- input=user_input,
205
- )
206
- )
207
-
208
179
  async def handle_run_agent(self, operation: op.RunAgentOperation) -> None:
209
180
  agent = await self._ensure_agent(operation.session_id)
210
181
  existing_active = self.task_manager.get(operation.id)
@@ -234,7 +205,7 @@ class ExecutorContext:
234
205
  default_note = " (saved as default)" if operation.save_as_default else ""
235
206
  developer_item = message.DeveloperMessage(
236
207
  parts=message.text_parts_from_str(f"Switched to: {llm_config.model}{default_note}"),
237
- command_output=model.CommandOutput(command_name=commands.CommandName.MODEL),
208
+ ui_extra=model.build_command_output_extra(commands.CommandName.MODEL),
238
209
  )
239
210
  agent.session.append_history([developer_item])
240
211
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
@@ -243,7 +214,11 @@ class ExecutorContext:
243
214
  self._on_model_change(llm_client.model_name)
244
215
 
245
216
  if operation.emit_welcome_event:
246
- await self.emit_event(events.WelcomeEvent(llm_config=llm_config, work_dir=str(agent.session.work_dir)))
217
+ await self.emit_event(
218
+ events.WelcomeEvent(
219
+ llm_config=llm_config, work_dir=str(agent.session.work_dir), show_klaude_code_info=False
220
+ )
221
+ )
247
222
 
248
223
  async def handle_change_thinking(self, operation: op.ChangeThinkingOperation) -> None:
249
224
  """Handle a change thinking operation.
@@ -279,13 +254,17 @@ class ExecutorContext:
279
254
  if operation.emit_switch_message:
280
255
  developer_item = message.DeveloperMessage(
281
256
  parts=message.text_parts_from_str(f"Thinking changed: {current} -> {new_status}"),
282
- command_output=model.CommandOutput(command_name=commands.CommandName.THINKING),
257
+ ui_extra=model.build_command_output_extra(commands.CommandName.THINKING),
283
258
  )
284
259
  agent.session.append_history([developer_item])
285
260
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
286
261
 
287
262
  if operation.emit_welcome_event:
288
- await self.emit_event(events.WelcomeEvent(work_dir=str(agent.session.work_dir), llm_config=config))
263
+ await self.emit_event(
264
+ events.WelcomeEvent(
265
+ work_dir=str(agent.session.work_dir), llm_config=config, show_klaude_code_info=False
266
+ )
267
+ )
289
268
 
290
269
  async def handle_clear_session(self, operation: op.ClearSessionOperation) -> None:
291
270
  agent = await self._ensure_agent(operation.session_id)
@@ -297,7 +276,7 @@ class ExecutorContext:
297
276
 
298
277
  developer_item = message.DeveloperMessage(
299
278
  parts=message.text_parts_from_str("started new conversation"),
300
- command_output=model.CommandOutput(command_name=commands.CommandName.CLEAR),
279
+ ui_extra=model.build_command_output_extra(commands.CommandName.CLEAR),
301
280
  )
302
281
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
303
282
  await self.emit_event(
@@ -348,7 +327,7 @@ class ExecutorContext:
348
327
  await asyncio.to_thread(self._open_file, output_path)
349
328
  developer_item = message.DeveloperMessage(
350
329
  parts=message.text_parts_from_str(f"Session exported and opened: {output_path}"),
351
- command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT),
330
+ ui_extra=model.build_command_output_extra(commands.CommandName.EXPORT),
352
331
  )
353
332
  agent.session.append_history([developer_item])
354
333
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
@@ -357,7 +336,7 @@ class ExecutorContext:
357
336
 
358
337
  developer_item = message.DeveloperMessage(
359
338
  parts=message.text_parts_from_str(f"Failed to export session: {exc}\n{traceback.format_exc()}"),
360
- command_output=model.CommandOutput(command_name=commands.CommandName.EXPORT, is_error=True),
339
+ ui_extra=model.build_command_output_extra(commands.CommandName.EXPORT, is_error=True),
361
340
  )
362
341
  agent.session.append_history([developer_item])
363
342
  await self.emit_event(events.DeveloperMessageEvent(session_id=agent.session.id, item=developer_item))
@@ -71,10 +71,13 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
71
71
  patterns.append(AtPatternSource(pattern=path_str, mentioned_in=None))
72
72
  break
73
73
 
74
- if isinstance(item, message.DeveloperMessage) and item.memory_mentioned:
75
- for memory_path, mentioned_patterns in item.memory_mentioned.items():
76
- for pattern in mentioned_patterns:
77
- patterns.append(AtPatternSource(pattern=pattern, mentioned_in=memory_path))
74
+ if isinstance(item, message.DeveloperMessage) and item.ui_extra:
75
+ for ui_item in item.ui_extra.items:
76
+ if not isinstance(ui_item, model.MemoryLoadedUIItem):
77
+ continue
78
+ for mem in ui_item.files:
79
+ for pattern in mem.mentioned_patterns:
80
+ patterns.append(AtPatternSource(pattern=pattern, mentioned_in=mem.path))
78
81
  return patterns
79
82
 
80
83
 
@@ -113,7 +116,8 @@ def _is_tracked_file_unchanged(session: Session, path: str) -> bool:
113
116
  async def _load_at_file_recursive(
114
117
  session: Session,
115
118
  pattern: str,
116
- at_files: dict[str, model.AtPatternParseResult],
119
+ at_ops: list[model.AtFileOp],
120
+ formatted_blocks: list[str],
117
121
  collected_images: list[message.ImageURLPart],
118
122
  visited: set[str],
119
123
  base_dir: Path | None = None,
@@ -135,14 +139,15 @@ async def _load_at_file_recursive(
135
139
  args = ReadTool.ReadArguments(file_path=path_str)
136
140
  tool_result = await ReadTool.call_with_args(args)
137
141
  images = [part for part in tool_result.parts if isinstance(part, message.ImageURLPart)]
138
- at_files[path_str] = model.AtPatternParseResult(
139
- path=path_str,
140
- tool_name=tools.READ,
141
- result=tool_result.output_text,
142
- tool_args=args.model_dump_json(exclude_none=True),
143
- operation="Read",
144
- mentioned_in=mentioned_in,
142
+
143
+ tool_args = args.model_dump_json(exclude_none=True)
144
+ formatted_blocks.append(
145
+ f"""Called the {tools.READ} tool with the following input: {tool_args}
146
+ Result of calling the {tools.READ} tool:
147
+ {tool_result.output_text}
148
+ """
145
149
  )
150
+ at_ops.append(model.AtFileOp(operation="Read", path=path_str, mentioned_in=mentioned_in))
146
151
  if images:
147
152
  collected_images.extend(images)
148
153
 
@@ -155,7 +160,8 @@ async def _load_at_file_recursive(
155
160
  await _load_at_file_recursive(
156
161
  session,
157
162
  nested,
158
- at_files,
163
+ at_ops,
164
+ formatted_blocks,
159
165
  collected_images,
160
166
  visited,
161
167
  base_dir=path.parent,
@@ -165,13 +171,15 @@ async def _load_at_file_recursive(
165
171
  quoted_path = shlex.quote(path_str)
166
172
  args = BashTool.BashArguments(command=f"ls {quoted_path}")
167
173
  tool_result = await BashTool.call_with_args(args)
168
- at_files[path_str] = model.AtPatternParseResult(
169
- path=path_str + "/",
170
- tool_name=tools.BASH,
171
- result=tool_result.output_text,
172
- tool_args=args.model_dump_json(exclude_none=True),
173
- operation="List",
174
+
175
+ tool_args = args.model_dump_json(exclude_none=True)
176
+ formatted_blocks.append(
177
+ f"""Called the {tools.BASH} tool with the following input: {tool_args}
178
+ Result of calling the {tools.BASH} tool:
179
+ {tool_result.output_text}
180
+ """
174
181
  )
182
+ at_ops.append(model.AtFileOp(operation="List", path=path_str + "/", mentioned_in=mentioned_in))
175
183
  finally:
176
184
  reset_tool_context(context_token)
177
185
 
@@ -184,7 +192,8 @@ async def at_file_reader_reminder(
184
192
  if not at_pattern_sources:
185
193
  return None
186
194
 
187
- at_files: dict[str, model.AtPatternParseResult] = {} # path -> content
195
+ at_ops: list[model.AtFileOp] = []
196
+ formatted_blocks: list[str] = []
188
197
  collected_images: list[message.ImageURLPart] = []
189
198
  visited: set[str] = set()
190
199
 
@@ -192,30 +201,23 @@ async def at_file_reader_reminder(
192
201
  await _load_at_file_recursive(
193
202
  session,
194
203
  source.pattern,
195
- at_files,
204
+ at_ops,
205
+ formatted_blocks,
196
206
  collected_images,
197
207
  visited,
198
208
  mentioned_in=source.mentioned_in,
199
209
  )
200
210
 
201
- if len(at_files) == 0:
211
+ if len(formatted_blocks) == 0:
202
212
  return None
203
213
 
204
- at_files_str = "\n\n".join(
205
- [
206
- f"""Called the {result.tool_name} tool with the following input: {result.tool_args}
207
- Result of calling the {result.tool_name} tool:
208
- {result.result}
209
- """
210
- for result in at_files.values()
211
- ]
212
- )
214
+ at_files_str = "\n\n".join(formatted_blocks)
213
215
  return message.DeveloperMessage(
214
216
  parts=message.parts_from_text_and_images(
215
217
  f"""<system-reminder>{at_files_str}\n</system-reminder>""",
216
218
  collected_images or None,
217
219
  ),
218
- at_files=list(at_files.values()),
220
+ ui_extra=model.DeveloperUIExtra(items=[model.AtFileOpsUIItem(ops=at_ops)]),
219
221
  )
220
222
 
221
223
 
@@ -299,7 +301,7 @@ Here are the existing contents of your todo list:
299
301
 
300
302
  {model.todo_list_str(session.todos)}</system-reminder>"""
301
303
  ),
302
- todo_use=True,
304
+ ui_extra=model.DeveloperUIExtra(items=[model.TodoReminderUIItem(reason="not_used_recently")]),
303
305
  )
304
306
 
305
307
  if session.need_todo_not_used_cooldown_counter > 0:
@@ -357,10 +359,12 @@ async def file_changed_externally_reminder(
357
359
  )
358
360
  return message.DeveloperMessage(
359
361
  parts=message.parts_from_text_and_images(
360
- f"""<system-reminder>{changed_files_str}""",
362
+ f"""<system-reminder>{changed_files_str}</system-reminder>""",
361
363
  collected_images or None,
362
364
  ),
363
- external_file_changes=[file_path for file_path, _, _ in changed_files],
365
+ ui_extra=model.DeveloperUIExtra(
366
+ items=[model.ExternalFileChangesUIItem(paths=[file_path for file_path, _, _ in changed_files])]
367
+ ),
364
368
  )
365
369
 
366
370
  return None
@@ -426,7 +430,7 @@ async def image_reminder(session: Session) -> message.DeveloperMessage | None:
426
430
  parts=message.text_parts_from_str(
427
431
  f"<system-reminder>User attached {image_count} image{'s' if image_count > 1 else ''} in their message. Make sure to analyze and reference these images as needed.</system-reminder>"
428
432
  ),
429
- user_image_count=image_count,
433
+ ui_extra=model.DeveloperUIExtra(items=[model.UserImagesUIItem(count=image_count)]),
430
434
  )
431
435
 
432
436
 
@@ -456,7 +460,7 @@ async def skill_reminder(session: Session) -> message.DeveloperMessage | None:
456
460
 
457
461
  return message.DeveloperMessage(
458
462
  parts=message.text_parts_from_str(content),
459
- skill_name=skill.name,
463
+ ui_extra=model.DeveloperUIExtra(items=[model.SkillActivatedUIItem(name=skill.name)]),
460
464
  )
461
465
 
462
466
 
@@ -487,7 +491,7 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
487
491
  path_str = str(memory_path)
488
492
  if memory_path.exists() and memory_path.is_file() and not _is_memory_loaded(session, path_str):
489
493
  try:
490
- text = memory_path.read_text()
494
+ text = memory_path.read_text(encoding="utf-8", errors="replace")
491
495
  _mark_memory_loaded(session, path_str)
492
496
  memories.append(Memory(path=path_str, instruction=instruction, content=text))
493
497
  except (PermissionError, UnicodeDecodeError, OSError):
@@ -496,32 +500,19 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
496
500
  memories_str = "\n\n".join(
497
501
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
498
502
  )
499
- # Build memory_mentioned: extract @ patterns from each memory's content
500
- memory_mentioned: dict[str, list[str]] = {}
501
- for memory in memories:
502
- patterns = _extract_at_patterns(memory.content)
503
- if patterns:
504
- memory_mentioned[memory.path] = patterns
505
-
503
+ loaded_files = [
504
+ model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
505
+ for memory in memories
506
+ ]
506
507
  return message.DeveloperMessage(
507
508
  parts=message.text_parts_from_str(
508
- f"""<system-reminder>As you answer the user's questions, you can use the following context:
509
+ f"""<system-reminder>
510
+ Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
509
511
 
510
- # claudeMd
511
- Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.
512
512
  {memories_str}
513
-
514
- #important-instruction-reminders
515
- Do what has been asked; nothing more, nothing less.
516
- NEVER create files unless they're absolutely necessary for achieving your goal.
517
- ALWAYS prefer editing an existing file to creating a new one.
518
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
519
-
520
- IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
521
513
  </system-reminder>"""
522
514
  ),
523
- memory_paths=[memory.path for memory in memories],
524
- memory_mentioned=memory_mentioned or None,
515
+ ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
525
516
  )
526
517
  return None
527
518
 
@@ -572,7 +563,7 @@ async def last_path_memory_reminder(
572
563
  continue
573
564
  if mem_path.exists() and mem_path.is_file():
574
565
  try:
575
- text = mem_path.read_text()
566
+ text = mem_path.read_text(encoding="utf-8", errors="replace")
576
567
  except (PermissionError, UnicodeDecodeError, OSError):
577
568
  continue
578
569
  _mark_memory_loaded(session, mem_path_str)
@@ -589,20 +580,16 @@ async def last_path_memory_reminder(
589
580
  memories_str = "\n\n".join(
590
581
  [f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
591
582
  )
592
- # Build memory_mentioned: extract @ patterns from each memory's content
593
- memory_mentioned: dict[str, list[str]] = {}
594
- for memory in memories:
595
- patterns = _extract_at_patterns(memory.content)
596
- if patterns:
597
- memory_mentioned[memory.path] = patterns
598
-
583
+ loaded_files = [
584
+ model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
585
+ for memory in memories
586
+ ]
599
587
  return message.DeveloperMessage(
600
588
  parts=message.text_parts_from_str(
601
589
  f"""<system-reminder>{memories_str}
602
590
  </system-reminder>"""
603
591
  ),
604
- memory_paths=[memory.path for memory in memories],
605
- memory_mentioned=memory_mentioned or None,
592
+ ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
606
593
  )
607
594
 
608
595
 
@@ -1,7 +1,6 @@
1
1
  from .file.apply_patch import DiffError, process_patch
2
2
  from .file.apply_patch_tool import ApplyPatchTool
3
3
  from .file.edit_tool import EditTool
4
- from .file.move_tool import MoveTool
5
4
  from .file.read_tool import ReadTool
6
5
  from .file.write_tool import WriteTool
7
6
  from .report_back_tool import ReportBackTool
@@ -36,7 +35,6 @@ __all__ = [
36
35
  "EditTool",
37
36
  "FileTracker",
38
37
  "MermaidTool",
39
- "MoveTool",
40
38
  "ReadTool",
41
39
  "ReportBackTool",
42
40
  "SafetyCheckResult",