klaude-code 1.2.19__py3-none-any.whl → 1.2.20__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 (57) hide show
  1. klaude_code/cli/runtime.py +5 -0
  2. klaude_code/command/__init__.py +1 -3
  3. klaude_code/command/clear_cmd.py +5 -4
  4. klaude_code/command/command_abc.py +5 -40
  5. klaude_code/command/debug_cmd.py +2 -2
  6. klaude_code/command/diff_cmd.py +2 -1
  7. klaude_code/command/export_cmd.py +14 -49
  8. klaude_code/command/export_online_cmd.py +2 -1
  9. klaude_code/command/help_cmd.py +2 -1
  10. klaude_code/command/model_cmd.py +7 -5
  11. klaude_code/command/prompt-jj-workspace.md +18 -0
  12. klaude_code/command/prompt_command.py +16 -9
  13. klaude_code/command/refresh_cmd.py +3 -2
  14. klaude_code/command/registry.py +31 -6
  15. klaude_code/command/release_notes_cmd.py +2 -1
  16. klaude_code/command/status_cmd.py +2 -1
  17. klaude_code/command/terminal_setup_cmd.py +2 -1
  18. klaude_code/command/thinking_cmd.py +2 -1
  19. klaude_code/core/executor.py +177 -190
  20. klaude_code/core/manager/sub_agent_manager.py +3 -0
  21. klaude_code/core/prompt.py +4 -1
  22. klaude_code/core/prompts/prompt-sub-agent-web.md +3 -3
  23. klaude_code/core/reminders.py +70 -26
  24. klaude_code/core/task.py +4 -5
  25. klaude_code/core/tool/__init__.py +2 -0
  26. klaude_code/core/tool/file/apply_patch_tool.py +3 -1
  27. klaude_code/core/tool/file/edit_tool.py +7 -5
  28. klaude_code/core/tool/file/multi_edit_tool.py +7 -5
  29. klaude_code/core/tool/file/read_tool.py +5 -2
  30. klaude_code/core/tool/file/write_tool.py +8 -6
  31. klaude_code/core/tool/shell/bash_tool.py +89 -17
  32. klaude_code/core/tool/sub_agent_tool.py +5 -1
  33. klaude_code/core/tool/tool_abc.py +18 -0
  34. klaude_code/core/tool/tool_context.py +6 -6
  35. klaude_code/core/tool/tool_runner.py +7 -7
  36. klaude_code/core/tool/web/web_fetch_tool.py +77 -22
  37. klaude_code/core/tool/web/web_search_tool.py +5 -1
  38. klaude_code/protocol/model.py +8 -1
  39. klaude_code/protocol/op.py +47 -0
  40. klaude_code/protocol/op_handler.py +25 -1
  41. klaude_code/protocol/sub_agent/web.py +1 -1
  42. klaude_code/session/codec.py +71 -0
  43. klaude_code/session/export.py +21 -11
  44. klaude_code/session/session.py +177 -333
  45. klaude_code/session/store.py +215 -0
  46. klaude_code/session/templates/export_session.html +13 -14
  47. klaude_code/ui/modes/repl/completers.py +1 -2
  48. klaude_code/ui/modes/repl/event_handler.py +7 -23
  49. klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -6
  50. klaude_code/ui/rich/__init__.py +10 -1
  51. klaude_code/ui/rich/cjk_wrap.py +228 -0
  52. klaude_code/ui/rich/status.py +0 -1
  53. {klaude_code-1.2.19.dist-info → klaude_code-1.2.20.dist-info}/METADATA +2 -1
  54. {klaude_code-1.2.19.dist-info → klaude_code-1.2.20.dist-info}/RECORD +56 -53
  55. klaude_code/ui/utils/debouncer.py +0 -42
  56. {klaude_code-1.2.19.dist-info → klaude_code-1.2.20.dist-info}/WHEEL +0 -0
  57. {klaude_code-1.2.19.dist-info → klaude_code-1.2.20.dist-info}/entry_points.txt +0 -0
@@ -23,6 +23,10 @@ class OperationType(Enum):
23
23
  """Enumeration of supported operation types."""
24
24
 
25
25
  USER_INPUT = "user_input"
26
+ RUN_AGENT = "run_agent"
27
+ CHANGE_MODEL = "change_model"
28
+ CLEAR_SESSION = "clear_session"
29
+ EXPORT_SESSION = "export_session"
26
30
  INTERRUPT = "interrupt"
27
31
  INIT_AGENT = "init_agent"
28
32
  END = "end"
@@ -51,6 +55,49 @@ class UserInputOperation(Operation):
51
55
  await handler.handle_user_input(self)
52
56
 
53
57
 
58
+ class RunAgentOperation(Operation):
59
+ """Operation for launching an agent task for a given session."""
60
+
61
+ type: OperationType = OperationType.RUN_AGENT
62
+ session_id: str
63
+ input: UserInputPayload
64
+
65
+ async def execute(self, handler: OperationHandler) -> None:
66
+ await handler.handle_run_agent(self)
67
+
68
+
69
+ class ChangeModelOperation(Operation):
70
+ """Operation for changing the model used by the active agent session."""
71
+
72
+ type: OperationType = OperationType.CHANGE_MODEL
73
+ session_id: str
74
+ model_name: str
75
+
76
+ async def execute(self, handler: OperationHandler) -> None:
77
+ await handler.handle_change_model(self)
78
+
79
+
80
+ class ClearSessionOperation(Operation):
81
+ """Operation for clearing the active session and starting a new one."""
82
+
83
+ type: OperationType = OperationType.CLEAR_SESSION
84
+ session_id: str
85
+
86
+ async def execute(self, handler: OperationHandler) -> None:
87
+ await handler.handle_clear_session(self)
88
+
89
+
90
+ class ExportSessionOperation(Operation):
91
+ """Operation for exporting a session transcript to HTML."""
92
+
93
+ type: OperationType = OperationType.EXPORT_SESSION
94
+ session_id: str
95
+ output_path: str | None = None
96
+
97
+ async def execute(self, handler: OperationHandler) -> None:
98
+ await handler.handle_export_session(self)
99
+
100
+
54
101
  class InterruptOperation(Operation):
55
102
  """Operation for interrupting currently running tasks."""
56
103
 
@@ -9,7 +9,15 @@ from __future__ import annotations
9
9
  from typing import TYPE_CHECKING, Protocol
10
10
 
11
11
  if TYPE_CHECKING:
12
- from klaude_code.protocol.op import InitAgentOperation, InterruptOperation, UserInputOperation
12
+ from klaude_code.protocol.op import (
13
+ ChangeModelOperation,
14
+ ClearSessionOperation,
15
+ ExportSessionOperation,
16
+ InitAgentOperation,
17
+ InterruptOperation,
18
+ RunAgentOperation,
19
+ UserInputOperation,
20
+ )
13
21
 
14
22
 
15
23
  class OperationHandler(Protocol):
@@ -19,6 +27,22 @@ class OperationHandler(Protocol):
19
27
  """Handle a user input operation."""
20
28
  ...
21
29
 
30
+ async def handle_run_agent(self, operation: RunAgentOperation) -> None:
31
+ """Handle a run agent operation."""
32
+ ...
33
+
34
+ async def handle_change_model(self, operation: ChangeModelOperation) -> None:
35
+ """Handle a change model operation."""
36
+ ...
37
+
38
+ async def handle_clear_session(self, operation: ClearSessionOperation) -> None:
39
+ """Handle a clear session operation."""
40
+ ...
41
+
42
+ async def handle_export_session(self, operation: ExportSessionOperation) -> None:
43
+ """Handle an export session operation."""
44
+ ...
45
+
22
46
  async def handle_interrupt(self, operation: InterruptOperation) -> None:
23
47
  """Handle an interrupt operation."""
24
48
  ...
@@ -71,7 +71,7 @@ register_sub_agent(
71
71
  description=WEB_AGENT_DESCRIPTION,
72
72
  parameters=WEB_AGENT_PARAMETERS,
73
73
  prompt_file="prompts/prompt-sub-agent-web.md",
74
- tool_set=(tools.BASH, tools.READ, tools.WEB_FETCH, tools.WEB_SEARCH),
74
+ tool_set=(tools.BASH, tools.READ, tools.WEB_FETCH, tools.WEB_SEARCH, tools.WRITE),
75
75
  prompt_builder=_web_agent_prompt_builder,
76
76
  active_form="Surfing",
77
77
  output_schema_arg="output_format",
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, TypeGuard, cast, get_args
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from klaude_code.protocol import model
9
+
10
+
11
+ def _is_basemodel_subclass(tp: object) -> TypeGuard[type[BaseModel]]:
12
+ return isinstance(tp, type) and issubclass(tp, BaseModel)
13
+
14
+
15
+ def _flatten_union(tp: object) -> list[object]:
16
+ args = list(get_args(tp))
17
+ if not args:
18
+ return [tp]
19
+ flattened: list[object] = []
20
+ for arg in args:
21
+ flattened.extend(_flatten_union(arg))
22
+ return flattened
23
+
24
+
25
+ def _build_type_registry() -> dict[str, type[BaseModel]]:
26
+ registry: dict[str, type[BaseModel]] = {}
27
+ for tp in _flatten_union(model.ConversationItem):
28
+ if not _is_basemodel_subclass(tp):
29
+ continue
30
+ registry[tp.__name__] = tp
31
+ return registry
32
+
33
+
34
+ _CONVERSATION_ITEM_TYPES: dict[str, type[BaseModel]] = _build_type_registry()
35
+
36
+
37
+ def encode_conversation_item(item: model.ConversationItem) -> dict[str, Any]:
38
+ return {"type": item.__class__.__name__, "data": item.model_dump(mode="json")}
39
+
40
+
41
+ def decode_conversation_item(obj: dict[str, Any]) -> model.ConversationItem | None:
42
+ t = obj.get("type")
43
+ data = obj.get("data", {})
44
+ if not isinstance(t, str) or not isinstance(data, dict):
45
+ return None
46
+ cls = _CONVERSATION_ITEM_TYPES.get(t)
47
+ if cls is None:
48
+ return None
49
+ try:
50
+ item = cls(**data)
51
+ except TypeError:
52
+ return None
53
+ # pyright: ignore[reportReturnType]
54
+ return item # type: ignore[return-value]
55
+
56
+
57
+ def encode_jsonl_line(item: model.ConversationItem) -> str:
58
+ return json.dumps(encode_conversation_item(item), ensure_ascii=False) + "\n"
59
+
60
+
61
+ def decode_jsonl_line(line: str) -> model.ConversationItem | None:
62
+ line = line.strip()
63
+ if not line:
64
+ return None
65
+ try:
66
+ obj = json.loads(line)
67
+ except json.JSONDecodeError:
68
+ return None
69
+ if not isinstance(obj, dict):
70
+ return None
71
+ return decode_conversation_item(cast(dict[str, Any], obj))
@@ -63,7 +63,7 @@ def get_default_export_path(session: Session) -> Path:
63
63
  """Get default export path for a session."""
64
64
  from klaude_code.session.session import Session as SessionClass
65
65
 
66
- exports_dir = SessionClass._exports_dir() # pyright: ignore[reportPrivateUsage]
66
+ exports_dir = SessionClass.exports_dir()
67
67
  timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
68
68
  first_msg = get_first_user_message(session.conversation_history)
69
69
  sanitized_msg = _sanitize_filename(first_msg)
@@ -267,20 +267,30 @@ def _render_assistant_message(index: int, content: str, timestamp: datetime) ->
267
267
  )
268
268
 
269
269
 
270
- def _try_render_todo_args(arguments: str) -> str | None:
270
+ def _try_render_todo_args(arguments: str, tool_name: str) -> str | None:
271
271
  try:
272
272
  parsed = json.loads(arguments)
273
- if not isinstance(parsed, dict) or "todos" not in parsed or not isinstance(parsed["todos"], list):
273
+ if not isinstance(parsed, dict):
274
274
  return None
275
275
 
276
- todos = cast(list[dict[str, str]], parsed["todos"])
277
- if not todos:
276
+ # Support both TodoWrite (todos/content) and update_plan (plan/step)
277
+ parsed_dict = cast(dict[str, Any], parsed)
278
+ if tool_name == "TodoWrite":
279
+ items = parsed_dict.get("todos")
280
+ content_key = "content"
281
+ elif tool_name == "update_plan":
282
+ items = parsed_dict.get("plan")
283
+ content_key = "step"
284
+ else:
285
+ return None
286
+
287
+ if not isinstance(items, list) or not items:
278
288
  return None
279
289
 
280
290
  items_html: list[str] = []
281
- for todo in todos:
282
- content = _escape_html(todo.get("content", ""))
283
- status = todo.get("status", "pending")
291
+ for item in cast(list[dict[str, str]], items):
292
+ content = _escape_html(item.get(content_key, ""))
293
+ status = item.get("status", "pending")
284
294
  status_class = f"status-{status}"
285
295
 
286
296
  items_html.append(
@@ -447,8 +457,8 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
447
457
  is_todo_list = False
448
458
  ts_str = _format_msg_timestamp(tool_call.created_at)
449
459
 
450
- if tool_call.name == "TodoWrite":
451
- args_html = _try_render_todo_args(tool_call.arguments)
460
+ if tool_call.name in ("TodoWrite", "update_plan"):
461
+ args_html = _try_render_todo_args(tool_call.arguments, tool_call.name)
452
462
  if args_html:
453
463
  is_todo_list = True
454
464
 
@@ -506,7 +516,7 @@ def _format_tool_call(tool_call: model.ToolCallItem, result: model.ToolResultIte
506
516
  diff_text = _get_diff_text(result.ui_extra)
507
517
  mermaid_html = _get_mermaid_link_html(result.ui_extra, tool_call)
508
518
 
509
- should_hide_text = tool_call.name == "TodoWrite" and result.status != "error"
519
+ should_hide_text = tool_call.name in ("TodoWrite", "update_plan") and result.status != "error"
510
520
 
511
521
  if tool_call.name == "Edit" and not diff_text and result.status != "error":
512
522
  try: