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.
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/runtime.py +1 -8
- klaude_code/command/debug_cmd.py +1 -1
- klaude_code/command/export_online_cmd.py +4 -4
- klaude_code/command/fork_session_cmd.py +6 -6
- klaude_code/command/help_cmd.py +1 -1
- klaude_code/command/model_cmd.py +1 -1
- klaude_code/command/registry.py +10 -1
- klaude_code/command/release_notes_cmd.py +1 -1
- klaude_code/command/resume_cmd.py +2 -2
- klaude_code/command/status_cmd.py +2 -2
- klaude_code/command/terminal_setup_cmd.py +2 -2
- klaude_code/command/thinking_cmd.py +1 -1
- klaude_code/config/assets/builtin_config.yaml +4 -0
- klaude_code/const.py +5 -3
- klaude_code/core/executor.py +15 -36
- klaude_code/core/reminders.py +55 -68
- klaude_code/core/tool/__init__.py +0 -2
- klaude_code/core/tool/file/edit_tool.py +3 -2
- klaude_code/core/tool/todo/todo_write_tool.py +1 -2
- klaude_code/core/tool/tool_registry.py +3 -3
- klaude_code/protocol/events.py +1 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +79 -13
- klaude_code/protocol/op.py +0 -13
- klaude_code/protocol/op_handler.py +0 -5
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/ui/modes/exec/display.py +2 -3
- klaude_code/ui/modes/repl/display.py +1 -1
- klaude_code/ui/modes/repl/event_handler.py +2 -10
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +5 -1
- klaude_code/ui/modes/repl/key_bindings.py +135 -1
- klaude_code/ui/modes/repl/renderer.py +2 -1
- klaude_code/ui/renderers/bash_syntax.py +36 -4
- klaude_code/ui/renderers/common.py +8 -6
- klaude_code/ui/renderers/developer.py +113 -97
- klaude_code/ui/renderers/metadata.py +28 -15
- klaude_code/ui/renderers/tools.py +17 -59
- klaude_code/ui/rich/markdown.py +69 -11
- klaude_code/ui/rich/theme.py +22 -17
- klaude_code/ui/terminal/selector.py +36 -17
- {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/METADATA +1 -1
- {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/RECORD +48 -50
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/WHEEL +0 -0
- {klaude_code-2.0.0.dist-info → klaude_code-2.0.2.dist-info}/entry_points.txt +0 -0
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
+
from klaude_code.const import DIFF_DEFAULT_CONTEXT_LINES
|
|
11
12
|
from klaude_code.core.tool.file._utils import file_exists, hash_text_sha256, is_directory, read_text, write_text
|
|
12
13
|
from klaude_code.core.tool.file.diff_builder import build_structured_diff
|
|
13
14
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
@@ -191,14 +192,14 @@ class EditTool(ToolABC):
|
|
|
191
192
|
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
192
193
|
return message.ToolResultMessage(status="error", output_text=f"<tool_use_error>{e}</tool_use_error>")
|
|
193
194
|
|
|
194
|
-
# Prepare UI extra: unified diff with
|
|
195
|
+
# Prepare UI extra: unified diff with default context lines
|
|
195
196
|
diff_lines = list(
|
|
196
197
|
difflib.unified_diff(
|
|
197
198
|
before.splitlines(),
|
|
198
199
|
after.splitlines(),
|
|
199
200
|
fromfile=file_path,
|
|
200
201
|
tofile=file_path,
|
|
201
|
-
n=
|
|
202
|
+
n=DIFF_DEFAULT_CONTEXT_LINES,
|
|
202
203
|
)
|
|
203
204
|
)
|
|
204
205
|
ui_extra = build_structured_diff(before, after, file_path=file_path)
|
|
@@ -63,9 +63,8 @@ class TodoWriteTool(ToolABC):
|
|
|
63
63
|
"type": "string",
|
|
64
64
|
"enum": ["pending", "in_progress", "completed"],
|
|
65
65
|
},
|
|
66
|
-
"activeForm": {"type": "string", "minLength": 1},
|
|
67
66
|
},
|
|
68
|
-
"required": ["content", "status"
|
|
67
|
+
"required": ["content", "status"],
|
|
69
68
|
"additionalProperties": False,
|
|
70
69
|
},
|
|
71
70
|
"description": "The updated todo list",
|
|
@@ -66,11 +66,11 @@ def load_agent_tools(
|
|
|
66
66
|
|
|
67
67
|
# Main agent tools
|
|
68
68
|
if "gpt-5" in model_name:
|
|
69
|
-
tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.
|
|
69
|
+
tool_names = [tools.BASH, tools.READ, tools.APPLY_PATCH, tools.UPDATE_PLAN]
|
|
70
70
|
elif "gemini-3" in model_name:
|
|
71
|
-
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE
|
|
71
|
+
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE]
|
|
72
72
|
else:
|
|
73
|
-
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.
|
|
73
|
+
tool_names = [tools.BASH, tools.READ, tools.EDIT, tools.WRITE, tools.TODO_WRITE]
|
|
74
74
|
|
|
75
75
|
tool_names.extend(sub_agent_tool_names(enabled_only=True, model_name=model_name))
|
|
76
76
|
tool_names.extend([tools.SKILL, tools.MERMAID])
|
klaude_code/protocol/events.py
CHANGED
klaude_code/protocol/message.py
CHANGED
|
@@ -12,8 +12,7 @@ from typing import Annotated, Literal
|
|
|
12
12
|
from pydantic import BaseModel, Field, field_validator
|
|
13
13
|
|
|
14
14
|
from klaude_code.protocol.model import (
|
|
15
|
-
|
|
16
|
-
CommandOutput,
|
|
15
|
+
DeveloperUIExtra,
|
|
17
16
|
StopReason,
|
|
18
17
|
TaskMetadata,
|
|
19
18
|
TaskMetadataItem,
|
|
@@ -137,15 +136,8 @@ class DeveloperMessage(MessageBase):
|
|
|
137
136
|
role: Literal["developer"] = "developer"
|
|
138
137
|
parts: list[Part]
|
|
139
138
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
memory_mentioned: dict[str, list[str]] | None = None # memory_path -> list of @ patterns mentioned in it
|
|
143
|
-
external_file_changes: list[str] | None = None
|
|
144
|
-
todo_use: bool | None = None
|
|
145
|
-
at_files: list[AtPatternParseResult] | None = None
|
|
146
|
-
command_output: CommandOutput | None = None
|
|
147
|
-
user_image_count: int | None = None
|
|
148
|
-
skill_name: str | None = None # Skill name activated via $skill syntax
|
|
139
|
+
# Structured UI-only metadata (never sent to the LLM).
|
|
140
|
+
ui_extra: DeveloperUIExtra | None = None
|
|
149
141
|
|
|
150
142
|
|
|
151
143
|
class UserMessage(MessageBase):
|
klaude_code/protocol/model.py
CHANGED
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Annotated, Any, Literal
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel,
|
|
5
|
+
from pydantic import BaseModel, Field, computed_field
|
|
6
6
|
|
|
7
7
|
from klaude_code.const import DEFAULT_MAX_TOKENS
|
|
8
8
|
from klaude_code.protocol.commands import CommandName
|
|
@@ -150,11 +150,8 @@ class TaskMetadataItem(BaseModel):
|
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
class TodoItem(BaseModel):
|
|
153
|
-
model_config = ConfigDict(populate_by_name=True)
|
|
154
|
-
|
|
155
153
|
content: str
|
|
156
154
|
status: TodoStatusType
|
|
157
|
-
active_form: str = Field(default="", alias="activeForm")
|
|
158
155
|
|
|
159
156
|
|
|
160
157
|
class FileStatus(BaseModel):
|
|
@@ -275,21 +272,90 @@ ToolResultUIExtra = Annotated[
|
|
|
275
272
|
]
|
|
276
273
|
|
|
277
274
|
|
|
278
|
-
class AtPatternParseResult(BaseModel):
|
|
279
|
-
path: str
|
|
280
|
-
tool_name: str
|
|
281
|
-
result: str
|
|
282
|
-
tool_args: str
|
|
283
|
-
operation: Literal["Read", "List"]
|
|
284
|
-
mentioned_in: str | None = None # Parent file that referenced this file
|
|
285
|
-
|
|
286
|
-
|
|
287
275
|
class CommandOutput(BaseModel):
|
|
288
276
|
command_name: CommandName
|
|
289
277
|
ui_extra: ToolResultUIExtra | None = None
|
|
290
278
|
is_error: bool = False
|
|
291
279
|
|
|
292
280
|
|
|
281
|
+
class MemoryFileLoaded(BaseModel):
|
|
282
|
+
path: str
|
|
283
|
+
mentioned_patterns: list[str] = Field(default_factory=list)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class MemoryLoadedUIItem(BaseModel):
|
|
287
|
+
type: Literal["memory_loaded"] = "memory_loaded"
|
|
288
|
+
files: list[MemoryFileLoaded]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class ExternalFileChangesUIItem(BaseModel):
|
|
292
|
+
type: Literal["external_file_changes"] = "external_file_changes"
|
|
293
|
+
paths: list[str]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class TodoReminderUIItem(BaseModel):
|
|
297
|
+
type: Literal["todo_reminder"] = "todo_reminder"
|
|
298
|
+
reason: Literal["empty", "not_used_recently"]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class AtFileOp(BaseModel):
|
|
302
|
+
operation: Literal["Read", "List"]
|
|
303
|
+
path: str
|
|
304
|
+
mentioned_in: str | None = None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class AtFileOpsUIItem(BaseModel):
|
|
308
|
+
type: Literal["at_file_ops"] = "at_file_ops"
|
|
309
|
+
ops: list[AtFileOp]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UserImagesUIItem(BaseModel):
|
|
313
|
+
type: Literal["user_images"] = "user_images"
|
|
314
|
+
count: int
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class SkillActivatedUIItem(BaseModel):
|
|
318
|
+
type: Literal["skill_activated"] = "skill_activated"
|
|
319
|
+
name: str
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class CommandOutputUIItem(BaseModel):
|
|
323
|
+
type: Literal["command_output"] = "command_output"
|
|
324
|
+
output: CommandOutput
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
type DeveloperUIItem = (
|
|
328
|
+
MemoryLoadedUIItem
|
|
329
|
+
| ExternalFileChangesUIItem
|
|
330
|
+
| TodoReminderUIItem
|
|
331
|
+
| AtFileOpsUIItem
|
|
332
|
+
| UserImagesUIItem
|
|
333
|
+
| SkillActivatedUIItem
|
|
334
|
+
| CommandOutputUIItem
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _empty_developer_ui_items() -> list[DeveloperUIItem]:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class DeveloperUIExtra(BaseModel):
|
|
343
|
+
items: list[DeveloperUIItem] = Field(default_factory=_empty_developer_ui_items)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def build_command_output_extra(
|
|
347
|
+
command_name: CommandName,
|
|
348
|
+
*,
|
|
349
|
+
ui_extra: ToolResultUIExtra | None = None,
|
|
350
|
+
is_error: bool = False,
|
|
351
|
+
) -> DeveloperUIExtra:
|
|
352
|
+
return DeveloperUIExtra(
|
|
353
|
+
items=[
|
|
354
|
+
CommandOutputUIItem(output=CommandOutput(command_name=command_name, ui_extra=ui_extra, is_error=is_error))
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
293
359
|
class SubAgentState(BaseModel):
|
|
294
360
|
sub_agent_type: SubAgentType
|
|
295
361
|
sub_agent_desc: str
|
klaude_code/protocol/op.py
CHANGED
|
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
|
|
|
23
23
|
class OperationType(Enum):
|
|
24
24
|
"""Enumeration of supported operation types."""
|
|
25
25
|
|
|
26
|
-
USER_INPUT = "user_input"
|
|
27
26
|
RUN_AGENT = "run_agent"
|
|
28
27
|
CHANGE_MODEL = "change_model"
|
|
29
28
|
CHANGE_THINKING = "change_thinking"
|
|
@@ -46,18 +45,6 @@ class Operation(BaseModel):
|
|
|
46
45
|
raise NotImplementedError("Subclasses must implement execute()")
|
|
47
46
|
|
|
48
47
|
|
|
49
|
-
class UserInputOperation(Operation):
|
|
50
|
-
"""Operation for handling user input (text and optional images) that should be processed by an agent."""
|
|
51
|
-
|
|
52
|
-
type: OperationType = OperationType.USER_INPUT
|
|
53
|
-
input: UserInputPayload
|
|
54
|
-
session_id: str | None = None
|
|
55
|
-
|
|
56
|
-
async def execute(self, handler: OperationHandler) -> None:
|
|
57
|
-
"""Execute user input by running it through an agent."""
|
|
58
|
-
await handler.handle_user_input(self)
|
|
59
|
-
|
|
60
|
-
|
|
61
48
|
class RunAgentOperation(Operation):
|
|
62
49
|
"""Operation for launching an agent task for a given session."""
|
|
63
50
|
|
|
@@ -18,17 +18,12 @@ if TYPE_CHECKING:
|
|
|
18
18
|
InterruptOperation,
|
|
19
19
|
ResumeSessionOperation,
|
|
20
20
|
RunAgentOperation,
|
|
21
|
-
UserInputOperation,
|
|
22
21
|
)
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
class OperationHandler(Protocol):
|
|
26
25
|
"""Protocol defining the interface for handling operations."""
|
|
27
26
|
|
|
28
|
-
async def handle_user_input(self, operation: UserInputOperation) -> None:
|
|
29
|
-
"""Handle a user input operation."""
|
|
30
|
-
...
|
|
31
|
-
|
|
32
27
|
async def handle_run_agent(self, operation: RunAgentOperation) -> None:
|
|
33
28
|
"""Handle a run agent operation."""
|
|
34
29
|
...
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
from klaude_code.protocol import tools
|
|
6
4
|
from klaude_code.protocol.sub_agent import SubAgentProfile, register_sub_agent
|
|
7
5
|
|
|
@@ -37,11 +35,6 @@ EXPLORE_PARAMETERS = {
|
|
|
37
35
|
"type": "string",
|
|
38
36
|
"description": "The task for the agent to perform",
|
|
39
37
|
},
|
|
40
|
-
"thoroughness": {
|
|
41
|
-
"type": "string",
|
|
42
|
-
"enum": ["quick", "medium", "very thorough"],
|
|
43
|
-
"description": "Controls how deep the sub-agent should search the repo",
|
|
44
|
-
},
|
|
45
38
|
"output_format": {
|
|
46
39
|
"type": "object",
|
|
47
40
|
"description": "Optional JSON Schema for sub-agent structured output",
|
|
@@ -52,13 +45,6 @@ EXPLORE_PARAMETERS = {
|
|
|
52
45
|
}
|
|
53
46
|
|
|
54
47
|
|
|
55
|
-
def _explore_prompt_builder(args: dict[str, Any]) -> str:
|
|
56
|
-
"""Build the Explore prompt from tool arguments."""
|
|
57
|
-
prompt = args.get("prompt", "").strip()
|
|
58
|
-
thoroughness = args.get("thoroughness", "medium")
|
|
59
|
-
return f"{prompt}\nthoroughness: {thoroughness}"
|
|
60
|
-
|
|
61
|
-
|
|
62
48
|
register_sub_agent(
|
|
63
49
|
SubAgentProfile(
|
|
64
50
|
name="Explore",
|
|
@@ -66,7 +52,6 @@ register_sub_agent(
|
|
|
66
52
|
parameters=EXPLORE_PARAMETERS,
|
|
67
53
|
prompt_file="prompts/prompt-sub-agent-explore.md",
|
|
68
54
|
tool_set=(tools.BASH, tools.READ),
|
|
69
|
-
prompt_builder=_explore_prompt_builder,
|
|
70
55
|
active_form="Exploring",
|
|
71
56
|
output_schema_arg="output_format",
|
|
72
57
|
)
|
|
@@ -64,7 +64,7 @@ register_sub_agent(
|
|
|
64
64
|
description=TASK_DESCRIPTION,
|
|
65
65
|
parameters=TASK_PARAMETERS,
|
|
66
66
|
prompt_file="prompts/prompt-sub-agent.md",
|
|
67
|
-
tool_set=(tools.BASH, tools.READ, tools.EDIT, tools.WRITE
|
|
67
|
+
tool_set=(tools.BASH, tools.READ, tools.EDIT, tools.WRITE),
|
|
68
68
|
active_form="Tasking",
|
|
69
69
|
output_schema_arg="output_format",
|
|
70
70
|
)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
3
|
from klaude_code.protocol import tools
|
|
6
4
|
from klaude_code.protocol.sub_agent import SubAgentProfile, register_sub_agent
|
|
7
5
|
|
|
@@ -21,7 +19,7 @@ Capabilities:
|
|
|
21
19
|
How to use:
|
|
22
20
|
- Write a clear prompt describing what information you need - the agent will search and fetch as needed
|
|
23
21
|
- Account for "Today's date" in <env>. For example, if <env> says "Today's date: 2025-07-01", and the user wants the latest docs, do not use 2024 in the search query. Use 2025.
|
|
24
|
-
-
|
|
22
|
+
- Provide the url if you already know the target page
|
|
25
23
|
- Use `output_format` (JSON Schema) to get structured data back from the agent
|
|
26
24
|
|
|
27
25
|
What you receive:
|
|
@@ -48,10 +46,6 @@ WEB_AGENT_PARAMETERS = {
|
|
|
48
46
|
"type": "string",
|
|
49
47
|
"description": "A short (3-5 word) description of the task",
|
|
50
48
|
},
|
|
51
|
-
"url": {
|
|
52
|
-
"type": "string",
|
|
53
|
-
"description": "The URL to fetch and analyze. If not provided, the agent will search the web first",
|
|
54
|
-
},
|
|
55
49
|
"prompt": {
|
|
56
50
|
"type": "string",
|
|
57
51
|
"description": "Instructions for searching, analyzing, or extracting content from the web page",
|
|
@@ -66,15 +60,6 @@ WEB_AGENT_PARAMETERS = {
|
|
|
66
60
|
}
|
|
67
61
|
|
|
68
62
|
|
|
69
|
-
def _web_agent_prompt_builder(args: dict[str, Any]) -> str:
|
|
70
|
-
"""Build the WebAgent prompt from tool arguments."""
|
|
71
|
-
url = args.get("url", "")
|
|
72
|
-
prompt = args.get("prompt", "")
|
|
73
|
-
if url:
|
|
74
|
-
return f"URL to fetch: {url}\nTask: {prompt}"
|
|
75
|
-
return prompt
|
|
76
|
-
|
|
77
|
-
|
|
78
63
|
register_sub_agent(
|
|
79
64
|
SubAgentProfile(
|
|
80
65
|
name="WebAgent",
|
|
@@ -82,7 +67,6 @@ register_sub_agent(
|
|
|
82
67
|
parameters=WEB_AGENT_PARAMETERS,
|
|
83
68
|
prompt_file="prompts/prompt-sub-agent-web.md",
|
|
84
69
|
tool_set=(tools.BASH, tools.READ, tools.WEB_FETCH, tools.WEB_SEARCH, tools.WRITE),
|
|
85
|
-
prompt_builder=_web_agent_prompt_builder,
|
|
86
70
|
active_form="Surfing",
|
|
87
71
|
output_schema_arg="output_format",
|
|
88
72
|
)
|
klaude_code/protocol/tools.py
CHANGED
|
@@ -14,12 +14,11 @@ class ExecDisplay(DisplayABC):
|
|
|
14
14
|
"""Only handle TaskFinishEvent."""
|
|
15
15
|
match event:
|
|
16
16
|
case events.TaskStartEvent():
|
|
17
|
-
|
|
17
|
+
pass
|
|
18
18
|
case events.ErrorEvent() as e:
|
|
19
|
-
emit_osc94(OSC94States.
|
|
19
|
+
emit_osc94(OSC94States.ERROR)
|
|
20
20
|
print(f"Error: {e.error_message}")
|
|
21
21
|
case events.TaskFinishEvent() as e:
|
|
22
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
23
22
|
# Print the task result when task finishes
|
|
24
23
|
if e.task_result.strip():
|
|
25
24
|
print(e.task_result)
|
|
@@ -19,7 +19,7 @@ class REPLDisplay(DisplayABC):
|
|
|
19
19
|
- Syntax-highlighted code blocks and diffs
|
|
20
20
|
- Animated spinners for in-progress operations
|
|
21
21
|
- Tool call and result visualization
|
|
22
|
-
- OSC94
|
|
22
|
+
- OSC94 error indicator (for supported terminals)
|
|
23
23
|
- Desktop notifications on task completion
|
|
24
24
|
|
|
25
25
|
This is the primary display mode for interactive klaude-code sessions.
|
|
@@ -416,14 +416,12 @@ class DisplayEventHandler:
|
|
|
416
416
|
self._sub_agent_thinking_headers[event.session_id] = SubAgentThinkingHeaderState()
|
|
417
417
|
self.renderer.spinner_start()
|
|
418
418
|
self.renderer.display_task_start(event)
|
|
419
|
-
emit_osc94(OSC94States.INDETERMINATE)
|
|
420
419
|
|
|
421
420
|
def _on_developer_message(self, event: events.DeveloperMessageEvent) -> None:
|
|
422
421
|
self.renderer.display_developer_message(event)
|
|
423
422
|
self.renderer.display_command_output(event)
|
|
424
423
|
|
|
425
424
|
def _on_turn_start(self, event: events.TurnStartEvent) -> None:
|
|
426
|
-
emit_osc94(OSC94States.INDETERMINATE)
|
|
427
425
|
self.renderer.display_turn_start(event)
|
|
428
426
|
self.spinner_status.clear_for_new_turn()
|
|
429
427
|
self.spinner_status.set_reasoning_status(None)
|
|
@@ -533,7 +531,6 @@ class DisplayEventHandler:
|
|
|
533
531
|
self.renderer.display_task_finish(event)
|
|
534
532
|
if not self.renderer.is_sub_agent_session(event.session_id):
|
|
535
533
|
r_status.clear_task_start()
|
|
536
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
537
534
|
self.spinner_status.reset()
|
|
538
535
|
self.renderer.spinner_stop()
|
|
539
536
|
self.renderer.console.print(Rule(characters="─", style=ThemeKey.LINES))
|
|
@@ -548,7 +545,6 @@ class DisplayEventHandler:
|
|
|
548
545
|
self.spinner_status.reset()
|
|
549
546
|
r_status.clear_task_start()
|
|
550
547
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
551
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
552
548
|
self.renderer.display_interrupt()
|
|
553
549
|
|
|
554
550
|
async def _on_error(self, event: events.ErrorEvent) -> None:
|
|
@@ -560,7 +556,6 @@ class DisplayEventHandler:
|
|
|
560
556
|
self.spinner_status.reset()
|
|
561
557
|
|
|
562
558
|
async def _on_end(self, event: events.EndEvent) -> None:
|
|
563
|
-
emit_osc94(OSC94States.HIDDEN)
|
|
564
559
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
565
560
|
self.renderer.spinner_stop()
|
|
566
561
|
self.spinner_status.reset()
|
|
@@ -624,11 +619,8 @@ class DisplayEventHandler:
|
|
|
624
619
|
def _extract_active_form_text(self, todo_event: events.TodoChangeEvent) -> str | None:
|
|
625
620
|
status_text: str | None = None
|
|
626
621
|
for todo in todo_event.todos:
|
|
627
|
-
if todo.status == "in_progress":
|
|
628
|
-
|
|
629
|
-
status_text = todo.active_form
|
|
630
|
-
if len(todo.content) > 0:
|
|
631
|
-
status_text = todo.content
|
|
622
|
+
if todo.status == "in_progress" and len(todo.content) > 0:
|
|
623
|
+
status_text = todo.content
|
|
632
624
|
|
|
633
625
|
if status_text is None:
|
|
634
626
|
return None
|
|
@@ -314,7 +314,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
314
314
|
"question": "bold",
|
|
315
315
|
"msg": "",
|
|
316
316
|
"meta": "fg:ansibrightblack",
|
|
317
|
-
"frame.border": "fg:ansibrightblack",
|
|
317
|
+
"frame.border": "fg:ansibrightblack dim",
|
|
318
318
|
"search_prefix": "fg:ansibrightblack",
|
|
319
319
|
"search_placeholder": "fg:ansibrightblack italic",
|
|
320
320
|
"search_input": "",
|
|
@@ -604,6 +604,10 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
604
604
|
(symbol_style, " ctrl-l "),
|
|
605
605
|
(text_style, " "),
|
|
606
606
|
(text_style, "models"),
|
|
607
|
+
(text_style, " "),
|
|
608
|
+
(symbol_style, " ctrl-t "),
|
|
609
|
+
(text_style, " "),
|
|
610
|
+
(text_style, "think"),
|
|
607
611
|
]
|
|
608
612
|
)
|
|
609
613
|
|
|
@@ -11,8 +11,9 @@ import re
|
|
|
11
11
|
from collections.abc import Callable
|
|
12
12
|
from typing import cast
|
|
13
13
|
|
|
14
|
+
from prompt_toolkit.application.current import get_app
|
|
14
15
|
from prompt_toolkit.buffer import Buffer
|
|
15
|
-
from prompt_toolkit.filters import Always, Filter
|
|
16
|
+
from prompt_toolkit.filters import Always, Condition, Filter
|
|
16
17
|
from prompt_toolkit.filters.app import has_completions
|
|
17
18
|
from prompt_toolkit.key_binding import KeyBindings
|
|
18
19
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
@@ -40,6 +41,119 @@ def create_key_bindings(
|
|
|
40
41
|
kb = KeyBindings()
|
|
41
42
|
enabled = input_enabled if input_enabled is not None else Always()
|
|
42
43
|
|
|
44
|
+
def _can_move_cursor_visually_within_wrapped_line(delta_visible_y: int) -> bool:
|
|
45
|
+
"""Return True when Up/Down should move within a wrapped visual line.
|
|
46
|
+
|
|
47
|
+
prompt_toolkit's default Up/Down behavior operates on logical lines
|
|
48
|
+
(split by '\n'). When a single logical line wraps across terminal
|
|
49
|
+
rows, pressing Up/Down should move within those wrapped rows instead of
|
|
50
|
+
triggering history navigation.
|
|
51
|
+
|
|
52
|
+
We only intercept when the cursor can move to an adjacent *visible*
|
|
53
|
+
line that maps to the same input line.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
app = get_app()
|
|
58
|
+
window = app.layout.current_window
|
|
59
|
+
ri = window.render_info
|
|
60
|
+
if ri is None:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
current_visible_y = int(ri.cursor_position.y)
|
|
64
|
+
target_visible_y = current_visible_y + delta_visible_y
|
|
65
|
+
if target_visible_y < 0:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
current_input_line = ri.visible_line_to_input_line.get(current_visible_y)
|
|
69
|
+
target_input_line = ri.visible_line_to_input_line.get(target_visible_y)
|
|
70
|
+
return current_input_line is not None and current_input_line == target_input_line
|
|
71
|
+
except Exception:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def _move_cursor_visually_within_wrapped_line(event: KeyPressEvent, *, delta_visible_y: int) -> None:
|
|
75
|
+
"""Move the cursor Up/Down by one wrapped screen row, keeping column."""
|
|
76
|
+
|
|
77
|
+
buf = event.current_buffer
|
|
78
|
+
try:
|
|
79
|
+
window = event.app.layout.current_window
|
|
80
|
+
ri = window.render_info
|
|
81
|
+
if ri is None:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
rowcol_to_yx = getattr(ri, "_rowcol_to_yx", None)
|
|
85
|
+
x_offset = getattr(ri, "_x_offset", None)
|
|
86
|
+
y_offset = getattr(ri, "_y_offset", None)
|
|
87
|
+
if not isinstance(rowcol_to_yx, dict) or not isinstance(x_offset, int) or not isinstance(y_offset, int):
|
|
88
|
+
return
|
|
89
|
+
rowcol_to_yx_typed = cast(dict[tuple[int, int], tuple[int, int]], rowcol_to_yx)
|
|
90
|
+
|
|
91
|
+
current_visible_y = int(ri.cursor_position.y)
|
|
92
|
+
target_visible_y = current_visible_y + delta_visible_y
|
|
93
|
+
mapping = ri.visible_line_to_row_col
|
|
94
|
+
if current_visible_y not in mapping or target_visible_y not in mapping:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
current_row, _ = mapping[current_visible_y]
|
|
98
|
+
target_row, _ = mapping[target_visible_y]
|
|
99
|
+
|
|
100
|
+
# Only handle wrapped rows within the same input line.
|
|
101
|
+
if current_row != target_row:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
current_abs_y = y_offset + current_visible_y
|
|
105
|
+
target_abs_y = y_offset + target_visible_y
|
|
106
|
+
cursor_abs_x = x_offset + int(ri.cursor_position.x)
|
|
107
|
+
|
|
108
|
+
def _segment_start_abs_x(row: int, abs_y: int) -> int | None:
|
|
109
|
+
xs: list[int] = []
|
|
110
|
+
for (r, _col), (y, x) in rowcol_to_yx_typed.items():
|
|
111
|
+
if r == row and y == abs_y:
|
|
112
|
+
xs.append(x)
|
|
113
|
+
return min(xs) if xs else None
|
|
114
|
+
|
|
115
|
+
current_start_x = _segment_start_abs_x(current_row, current_abs_y)
|
|
116
|
+
target_start_x = _segment_start_abs_x(target_row, target_abs_y)
|
|
117
|
+
if current_start_x is None or target_start_x is None:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
offset_in_segment_cells = max(0, cursor_abs_x - current_start_x)
|
|
121
|
+
desired_abs_x = target_start_x + offset_in_segment_cells
|
|
122
|
+
|
|
123
|
+
candidates: list[tuple[int, int]] = []
|
|
124
|
+
for (r, col), (y, x) in rowcol_to_yx_typed.items():
|
|
125
|
+
if r == target_row and y == target_abs_y:
|
|
126
|
+
candidates.append((col, x))
|
|
127
|
+
if not candidates:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Pick the closest column at/before the desired X. If the desired
|
|
131
|
+
# position is before the first character, snap to the first.
|
|
132
|
+
candidates.sort(key=lambda t: t[1])
|
|
133
|
+
chosen_display_col = candidates[0][0]
|
|
134
|
+
for col, x in candidates:
|
|
135
|
+
if x <= desired_abs_x:
|
|
136
|
+
chosen_display_col = col
|
|
137
|
+
else:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
control = event.app.layout.current_control
|
|
141
|
+
get_processed_line = getattr(control, "_last_get_processed_line", None)
|
|
142
|
+
target_source_col = chosen_display_col
|
|
143
|
+
if callable(get_processed_line):
|
|
144
|
+
processed_line = get_processed_line(target_row)
|
|
145
|
+
display_to_source = getattr(processed_line, "display_to_source", None)
|
|
146
|
+
if callable(display_to_source):
|
|
147
|
+
display_to_source_fn = cast(Callable[[int], int], display_to_source)
|
|
148
|
+
target_source_col = display_to_source_fn(chosen_display_col)
|
|
149
|
+
|
|
150
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
151
|
+
new_index = doc.translate_row_col_to_index(target_row, target_source_col) # type: ignore[reportUnknownMemberType]
|
|
152
|
+
buf.cursor_position = new_index # type: ignore[reportUnknownMemberType]
|
|
153
|
+
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
154
|
+
except Exception:
|
|
155
|
+
return
|
|
156
|
+
|
|
43
157
|
def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
|
|
44
158
|
"""Return True when Enter should submit even if completions are visible.
|
|
45
159
|
|
|
@@ -174,6 +288,26 @@ def create_key_bindings(
|
|
|
174
288
|
_cycle_completion(buf, delta=-1)
|
|
175
289
|
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
176
290
|
|
|
291
|
+
@kb.add(
|
|
292
|
+
"up",
|
|
293
|
+
filter=enabled
|
|
294
|
+
& ~has_completions
|
|
295
|
+
& Condition(lambda: _can_move_cursor_visually_within_wrapped_line(delta_visible_y=-1)),
|
|
296
|
+
eager=True,
|
|
297
|
+
)
|
|
298
|
+
def _(event: KeyPressEvent) -> None:
|
|
299
|
+
_move_cursor_visually_within_wrapped_line(event, delta_visible_y=-1)
|
|
300
|
+
|
|
301
|
+
@kb.add(
|
|
302
|
+
"down",
|
|
303
|
+
filter=enabled
|
|
304
|
+
& ~has_completions
|
|
305
|
+
& Condition(lambda: _can_move_cursor_visually_within_wrapped_line(delta_visible_y=1)),
|
|
306
|
+
eager=True,
|
|
307
|
+
)
|
|
308
|
+
def _(event: KeyPressEvent) -> None:
|
|
309
|
+
_move_cursor_visually_within_wrapped_line(event, delta_visible_y=1)
|
|
310
|
+
|
|
177
311
|
@kb.add("c-j", filter=enabled)
|
|
178
312
|
def _(event: KeyPressEvent) -> None:
|
|
179
313
|
event.current_buffer.insert_text("\n") # type: ignore
|
|
@@ -257,7 +257,7 @@ class REPLRenderer:
|
|
|
257
257
|
self.print(r_developer.render_developer_message(e))
|
|
258
258
|
|
|
259
259
|
def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
|
|
260
|
-
if not e.item
|
|
260
|
+
if not r_developer.get_command_output(e.item):
|
|
261
261
|
return
|
|
262
262
|
with self.session_print_context(e.session_id):
|
|
263
263
|
self.print(r_developer.render_command_output(e))
|
|
@@ -458,6 +458,7 @@ class REPLRenderer:
|
|
|
458
458
|
return
|
|
459
459
|
with contextlib.suppress(Exception):
|
|
460
460
|
# Avoid cursor restore when stopping right before prompt_toolkit.
|
|
461
|
+
# This will leave a blank line before prompt input
|
|
461
462
|
self._bottom_live.transient = False
|
|
462
463
|
self._bottom_live.stop()
|
|
463
464
|
self._bottom_live = None
|