klaude-code 1.2.19__py3-none-any.whl → 1.2.21__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/main.py +23 -0
- klaude_code/cli/runtime.py +17 -0
- klaude_code/command/__init__.py +1 -3
- klaude_code/command/clear_cmd.py +5 -4
- klaude_code/command/command_abc.py +5 -40
- klaude_code/command/debug_cmd.py +2 -2
- klaude_code/command/diff_cmd.py +2 -1
- klaude_code/command/export_cmd.py +14 -49
- klaude_code/command/export_online_cmd.py +2 -1
- klaude_code/command/help_cmd.py +2 -1
- klaude_code/command/model_cmd.py +7 -5
- klaude_code/command/prompt-jj-workspace.md +18 -0
- klaude_code/command/prompt_command.py +16 -9
- klaude_code/command/refresh_cmd.py +3 -2
- klaude_code/command/registry.py +31 -6
- klaude_code/command/release_notes_cmd.py +2 -1
- klaude_code/command/status_cmd.py +2 -1
- klaude_code/command/terminal_setup_cmd.py +2 -1
- klaude_code/command/thinking_cmd.py +12 -1
- klaude_code/core/executor.py +177 -190
- klaude_code/core/manager/sub_agent_manager.py +3 -0
- klaude_code/core/prompt.py +4 -1
- klaude_code/core/prompts/prompt-sub-agent-web.md +3 -3
- klaude_code/core/reminders.py +70 -26
- klaude_code/core/task.py +4 -5
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/apply_patch_tool.py +3 -1
- klaude_code/core/tool/file/edit_tool.py +7 -5
- klaude_code/core/tool/file/multi_edit_tool.py +7 -5
- klaude_code/core/tool/file/read_tool.py +5 -2
- klaude_code/core/tool/file/write_tool.py +8 -6
- klaude_code/core/tool/shell/bash_tool.py +90 -17
- klaude_code/core/tool/sub_agent_tool.py +5 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +6 -6
- klaude_code/core/tool/tool_runner.py +7 -7
- klaude_code/core/tool/web/mermaid_tool.md +26 -0
- klaude_code/core/tool/web/web_fetch_tool.py +77 -22
- klaude_code/core/tool/web/web_search_tool.py +5 -1
- klaude_code/protocol/model.py +8 -1
- klaude_code/protocol/op.py +47 -0
- klaude_code/protocol/op_handler.py +25 -1
- klaude_code/protocol/sub_agent/web.py +1 -1
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +21 -11
- klaude_code/session/session.py +182 -331
- klaude_code/session/store.py +215 -0
- klaude_code/session/templates/export_session.html +13 -14
- klaude_code/ui/modes/repl/completers.py +1 -2
- klaude_code/ui/modes/repl/event_handler.py +7 -23
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -6
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/status.py +0 -1
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/METADATA +2 -1
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/RECORD +58 -55
- klaude_code/ui/utils/debouncer.py +0 -42
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.19.dist-info → klaude_code-1.2.21.dist-info}/entry_points.txt +0 -0
klaude_code/protocol/model.py
CHANGED
|
@@ -69,6 +69,13 @@ class TodoItem(BaseModel):
|
|
|
69
69
|
active_form: str = Field(default="", alias="activeForm")
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
class FileStatus(BaseModel):
|
|
73
|
+
"""Tracks file state including modification time and memory file flag."""
|
|
74
|
+
|
|
75
|
+
mtime: float
|
|
76
|
+
is_memory: bool = False
|
|
77
|
+
|
|
78
|
+
|
|
72
79
|
class TodoUIExtra(BaseModel):
|
|
73
80
|
todos: list[TodoItem]
|
|
74
81
|
new_completed: list[str]
|
|
@@ -170,7 +177,7 @@ A conversation history input contains:
|
|
|
170
177
|
- [DeveloperMessageItem]
|
|
171
178
|
|
|
172
179
|
When adding a new item, please also modify the following:
|
|
173
|
-
- session.py
|
|
180
|
+
- session/codec.py (ConversationItem registry derived from ConversationItem union)
|
|
174
181
|
"""
|
|
175
182
|
|
|
176
183
|
|
klaude_code/protocol/op.py
CHANGED
|
@@ -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
|
|
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))
|
klaude_code/session/export.py
CHANGED
|
@@ -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.
|
|
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)
|
|
273
|
+
if not isinstance(parsed, dict):
|
|
274
274
|
return None
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
282
|
-
content = _escape_html(
|
|
283
|
-
status =
|
|
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
|
|
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
|
|
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:
|