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
@@ -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 3 context lines
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=3,
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", "activeForm"],
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.MOVE, tools.UPDATE_PLAN]
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, tools.MOVE]
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.MOVE, tools.TODO_WRITE]
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])
@@ -127,6 +127,7 @@ class UserMessageEvent(BaseModel):
127
127
  class WelcomeEvent(BaseModel):
128
128
  work_dir: str
129
129
  llm_config: llm_param.LLMConfigParameter
130
+ show_klaude_code_info: bool = True
130
131
 
131
132
 
132
133
  class InterruptEvent(BaseModel):
@@ -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
- AtPatternParseResult,
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
- # Special fields for reminders UI
141
- memory_paths: list[str] | None = None
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):
@@ -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, ConfigDict, Field, computed_field
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
@@ -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, tools.MOVE),
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
- - Optionally provide a `url` if you already know the target page
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
  )
@@ -1,6 +1,5 @@
1
1
  BASH = "Bash"
2
2
  APPLY_PATCH = "apply_patch"
3
- MOVE = "Move"
4
3
  EDIT = "Edit"
5
4
 
6
5
  READ = "Read"
@@ -14,12 +14,11 @@ class ExecDisplay(DisplayABC):
14
14
  """Only handle TaskFinishEvent."""
15
15
  match event:
16
16
  case events.TaskStartEvent():
17
- emit_osc94(OSC94States.INDETERMINATE)
17
+ pass
18
18
  case events.ErrorEvent() as e:
19
- emit_osc94(OSC94States.HIDDEN)
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 progress bar integration (for supported terminals)
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
- if len(todo.active_form) > 0:
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.command_output:
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