klaude-code 1.3.0__py3-none-any.whl → 1.4.1__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.
@@ -273,7 +273,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
273
273
  display.wrapped_display.renderer.stop_bottom_live()
274
274
 
275
275
  # Pass the pre-detected theme to avoid redundant TTY queries.
276
- # Querying the terminal background again after questionary's interactive selection
276
+ # Querying the terminal background again after an interactive selection
277
277
  # can interfere with prompt_toolkit's terminal state and break history navigation.
278
278
  is_light_background: bool | None = None
279
279
  if components.theme == "light":
@@ -352,7 +352,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
352
352
  op.UserInputOperation(input=user_input, session_id=active_session_id)
353
353
  )
354
354
  # If it's an interactive command (e.g., /model), avoid starting the ESC monitor
355
- # to prevent TTY conflicts with interactive prompts (questionary/prompt_toolkit).
355
+ # to prevent TTY conflicts with interactive prompt_toolkit UIs.
356
356
  if has_interactive_command(user_input.text):
357
357
  await components.executor.wait_for(submission_id)
358
358
  else:
@@ -7,8 +7,11 @@ from klaude_code.trace import log
7
7
 
8
8
 
9
9
  def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) -> bool:
10
- """Show session list and confirm deletion using questionary."""
11
- import questionary
10
+ """Show session list and confirm deletion using prompt_toolkit."""
11
+
12
+ from prompt_toolkit.styles import Style
13
+
14
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
12
15
 
13
16
  def _fmt(ts: float) -> str:
14
17
  try:
@@ -24,14 +27,26 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
24
27
  first_msg += "..."
25
28
  log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
26
29
 
27
- return (
28
- questionary.confirm(
29
- message,
30
- default=False,
31
- style=questionary.Style([("question", "bold")]),
32
- ).ask()
33
- or False
30
+ items: list[SelectItem[bool]] = [
31
+ SelectItem(title=[("class:text", "No\n")], value=False, search_text="No"),
32
+ SelectItem(title=[("class:text", "Yes\n")], value=True, search_text="Yes"),
33
+ ]
34
+
35
+ result = select_one(
36
+ message=message,
37
+ items=items,
38
+ pointer="→",
39
+ style=Style(
40
+ [
41
+ ("question", "bold"),
42
+ ("pointer", "ansigreen"),
43
+ ("highlighted", "ansigreen"),
44
+ ("text", ""),
45
+ ]
46
+ ),
47
+ use_search_filter=False,
34
48
  )
49
+ return bool(result)
35
50
 
36
51
 
37
52
  def session_clean(
@@ -1,38 +1,43 @@
1
1
  import asyncio
2
2
 
3
- import questionary
3
+ from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
6
  from klaude_code.config.select_model import select_model_from_config
7
7
  from klaude_code.protocol import commands, events, model, op
8
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
8
9
 
9
- SELECT_STYLE = questionary.Style(
10
+ SELECT_STYLE = Style(
10
11
  [
11
12
  ("instruction", "ansibrightblack"),
12
- ("pointer", "ansicyan"),
13
- ("highlighted", "ansicyan"),
13
+ ("pointer", "ansigreen"),
14
+ ("highlighted", "ansigreen"),
14
15
  ("text", "ansibrightblack"),
16
+ ("question", ""),
15
17
  ]
16
18
  )
17
19
 
18
20
 
19
21
  def _confirm_change_default_model_sync(selected_model: str) -> bool:
20
- choices: list[questionary.Choice] = [
21
- questionary.Choice(title="No (session only)", value=False),
22
- questionary.Choice(title="Yes (save as default main_model in ~/.klaude/klaude-config.yaml)", value=True),
22
+ items: list[SelectItem[bool]] = [
23
+ SelectItem(title=[("class:text", "No (session only)\n")], value=False, search_text="No"),
24
+ SelectItem(
25
+ title=[("class:text", "Yes (save as default main_model in ~/.klaude/klaude-config.yaml)\n")],
26
+ value=True,
27
+ search_text="Yes",
28
+ ),
23
29
  ]
24
30
 
25
31
  try:
26
32
  # Add a blank line between the model selector and this confirmation prompt.
27
- questionary.print("")
28
- result = questionary.select(
33
+ print("")
34
+ result = select_one(
29
35
  message=f"Save '{selected_model}' as default model?",
30
- choices=choices,
36
+ items=items,
31
37
  pointer="→",
32
- instruction="Use arrow keys to move, Enter to select",
33
- use_jk_keys=False,
34
38
  style=SELECT_STYLE,
35
- ).ask()
39
+ use_search_filter=False,
40
+ )
36
41
  except KeyboardInterrupt:
37
42
  return False
38
43
 
@@ -1,9 +1,13 @@
1
1
  import asyncio
2
+ from typing import Literal, cast
2
3
 
3
- import questionary
4
+ from prompt_toolkit.styles import Style
4
5
 
5
6
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
7
  from klaude_code.protocol import commands, events, llm_param, model
8
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
9
+
10
+ ReasoningEffort = Literal["high", "medium", "low", "minimal", "none", "xhigh"]
7
11
 
8
12
  # Thinking level options for different protocols
9
13
  RESPONSES_LEVELS = ["low", "medium", "high"]
@@ -118,12 +122,13 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
118
122
  return "unknown protocol"
119
123
 
120
124
 
121
- SELECT_STYLE = questionary.Style(
125
+ SELECT_STYLE = Style(
122
126
  [
123
127
  ("instruction", "ansibrightblack"),
124
- ("pointer", "ansicyan"),
125
- ("highlighted", "ansicyan"),
128
+ ("pointer", "ansigreen"),
129
+ ("highlighted", "ansigreen"),
126
130
  ("text", "ansibrightblack"),
131
+ ("question", ""),
127
132
  ]
128
133
  )
129
134
 
@@ -131,43 +136,46 @@ SELECT_STYLE = questionary.Style(
131
136
  def _select_responses_thinking_sync(model_name: str | None) -> llm_param.Thinking | None:
132
137
  """Select thinking level for responses/codex protocol (sync version)."""
133
138
  levels = _get_levels_for_responses(model_name)
134
- choices: list[questionary.Choice] = [questionary.Choice(title=level, value=level) for level in levels]
139
+ items: list[SelectItem[str]] = [
140
+ SelectItem(title=[("class:text", level + "\n")], value=level, search_text=level) for level in levels
141
+ ]
135
142
 
136
143
  try:
137
- result = questionary.select(
144
+ result = select_one(
138
145
  message="Select reasoning effort:",
139
- choices=choices,
146
+ items=items,
140
147
  pointer="→",
141
- instruction="Use arrow keys to move, Enter to select",
142
- use_jk_keys=False,
143
148
  style=SELECT_STYLE,
144
- ).ask()
149
+ use_search_filter=False,
150
+ )
145
151
 
146
152
  if result is None:
147
153
  return None
148
- return llm_param.Thinking(reasoning_effort=result)
154
+ return llm_param.Thinking(reasoning_effort=cast(ReasoningEffort, result))
149
155
  except KeyboardInterrupt:
150
156
  return None
151
157
 
152
158
 
153
159
  def _select_anthropic_thinking_sync() -> llm_param.Thinking | None:
154
160
  """Select thinking level for anthropic/openai_compatible protocol (sync version)."""
155
- choices: list[questionary.Choice] = [
156
- questionary.Choice(title=label, value=tokens) for label, tokens in ANTHROPIC_LEVELS
161
+ items: list[SelectItem[int]] = [
162
+ SelectItem(title=[("class:text", label + "\n")], value=tokens or 0, search_text=label)
163
+ for label, tokens in ANTHROPIC_LEVELS
157
164
  ]
158
165
 
159
166
  try:
160
- result = questionary.select(
167
+ result = select_one(
161
168
  message="Select thinking level:",
162
- choices=choices,
169
+ items=items,
163
170
  pointer="→",
164
- instruction="Use arrow keys to move, Enter to select",
165
- use_jk_keys=False,
166
171
  style=SELECT_STYLE,
167
- ).ask()
168
- if not result:
172
+ use_search_filter=False,
173
+ )
174
+ if result is None:
175
+ return None
176
+ if result == 0:
169
177
  return llm_param.Thinking(type="disabled", budget_tokens=0)
170
- return llm_param.Thinking(type="enabled", budget_tokens=result or 0)
178
+ return llm_param.Thinking(type="enabled", budget_tokens=result)
171
179
  except KeyboardInterrupt:
172
180
  return None
173
181
 
@@ -108,60 +108,70 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
108
108
  return None
109
109
 
110
110
  try:
111
- import questionary
111
+ from prompt_toolkit.styles import Style
112
112
 
113
- choices: list[questionary.Choice] = []
113
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
114
114
 
115
115
  max_model_name_length = max(len(m.model_name) for m in filtered_models)
116
116
 
117
- # Build model_id with thinking suffix as a single unit
118
- def build_model_id_with_thinking(m: ModelEntry) -> str:
119
- model_id = m.model_params.model or "N/A"
117
+ def _thinking_info(m: ModelEntry) -> str:
120
118
  thinking = m.model_params.thinking
121
- if thinking:
122
- if thinking.reasoning_effort:
123
- return f"{model_id} ({thinking.reasoning_effort})"
124
- elif thinking.budget_tokens:
125
- return f"{model_id} (think {thinking.budget_tokens})"
126
- return model_id
127
-
128
- model_id_with_thinking = {m.model_name: build_model_id_with_thinking(m) for m in filtered_models}
129
- max_model_id_length = max(len(v) for v in model_id_with_thinking.values())
130
-
119
+ if not thinking:
120
+ return ""
121
+ if thinking.reasoning_effort:
122
+ return f"reasoning {thinking.reasoning_effort}"
123
+ if thinking.budget_tokens:
124
+ return f"thinking budget {thinking.budget_tokens}"
125
+ return "thinking (configured)"
126
+
127
+ items: list[SelectItem[str]] = []
131
128
  for m in filtered_models:
132
- star = "★ " if m.model_name == config.main_model else " "
133
- model_id_str = model_id_with_thinking[m.model_name]
134
- title = f"{star}{m.model_name:<{max_model_name_length}} → {model_id_str:<{max_model_id_length}} @ {m.provider}"
135
- choices.append(questionary.Choice(title=title, value=m.model_name))
129
+ model_id = m.model_params.model or "N/A"
130
+ first_line_prefix = f"{m.model_name:<{max_model_name_length}} → "
131
+ thinking_info = _thinking_info(m)
132
+ meta_parts: list[str] = [m.provider]
133
+ if thinking_info:
134
+ meta_parts.append(thinking_info)
135
+ if m.model_params.verbosity:
136
+ meta_parts.append(f"verbosity {m.model_params.verbosity}")
137
+ meta_str = " · ".join(meta_parts)
138
+ title = [
139
+ ("class:msg", first_line_prefix),
140
+ ("class:msg bold", model_id),
141
+ ("class:meta", f" {meta_str}\n"),
142
+ ]
143
+ search_text = f"{m.model_name} {model_id} {m.provider}"
144
+ items.append(SelectItem(title=title, value=m.model_name, search_text=search_text))
136
145
 
137
146
  try:
138
147
  message = f"Select a model (filtered by '{preferred}'):" if preferred else "Select a model:"
139
- result = questionary.select(
148
+ result = select_one(
140
149
  message=message,
141
- choices=choices,
150
+ items=items,
142
151
  pointer="→",
143
- instruction="↑↓ to move • Enter to select",
144
- use_jk_keys=False,
145
152
  use_search_filter=True,
146
- style=questionary.Style(
153
+ initial_value=config.main_model,
154
+ style=Style(
147
155
  [
148
- ("instruction", "ansibrightblack"),
149
- ("pointer", "ansicyan"),
150
- ("highlighted", "ansicyan"),
156
+ ("pointer", "ansigreen"),
157
+ ("highlighted", "ansigreen"),
158
+ ("msg", ""),
159
+ ("meta", "fg:ansibrightblack"),
151
160
  ("text", "ansibrightblack"),
161
+ ("question", "bold"),
162
+ ("search_prefix", "ansibrightblack"),
152
163
  # search filter colors at the bottom
153
164
  ("search_success", "noinherit fg:ansigreen"),
154
165
  ("search_none", "noinherit fg:ansired"),
155
- ("question-mark", "fg:ansigreen"),
156
166
  ]
157
167
  ),
158
- ).ask()
168
+ )
159
169
  if isinstance(result, str) and result in names:
160
170
  return result
161
171
  except KeyboardInterrupt:
162
172
  return None
163
173
  except Exception as e:
164
- log((f"Failed to use questionary for model selection: {e}", "yellow"))
174
+ log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
165
175
  # Never return an unvalidated model name here.
166
176
  # If we can't interactively select, fall back to a known configured model.
167
177
  if isinstance(preferred, str) and preferred in names:
@@ -1,7 +1,7 @@
1
1
  from .file.apply_patch import DiffError, process_patch
2
2
  from .file.apply_patch_tool import ApplyPatchTool
3
- from .file.move_tool import MoveTool
4
3
  from .file.edit_tool import EditTool
4
+ from .file.move_tool import MoveTool
5
5
  from .file.read_tool import ReadTool
6
6
  from .file.write_tool import WriteTool
7
7
  from .report_back_tool import ReportBackTool
@@ -32,11 +32,11 @@ from .web.web_search_tool import WebSearchTool
32
32
  __all__ = [
33
33
  "ApplyPatchTool",
34
34
  "BashTool",
35
- "MoveTool",
36
35
  "DiffError",
37
36
  "EditTool",
38
37
  "FileTracker",
39
38
  "MermaidTool",
39
+ "MoveTool",
40
40
  "ReadTool",
41
41
  "ReportBackTool",
42
42
  "SafetyCheckResult",
@@ -1,76 +1,93 @@
1
1
  import time
2
- from typing import TYPE_CHECKING
3
2
 
4
3
  from klaude_code.trace import log, log_debug
5
-
6
- if TYPE_CHECKING:
7
- from questionary import Choice
4
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
8
5
 
9
6
  from .session import Session
10
7
 
11
8
 
9
+ def _relative_time(ts: float) -> str:
10
+ """Format timestamp as relative time like '5 days ago'."""
11
+ now = time.time()
12
+ diff = now - ts
13
+
14
+ if diff < 60:
15
+ return "just now"
16
+ elif diff < 3600:
17
+ mins = int(diff / 60)
18
+ return f"{mins} minute{'s' if mins != 1 else ''} ago"
19
+ elif diff < 86400:
20
+ hours = int(diff / 3600)
21
+ return f"{hours} hour{'s' if hours != 1 else ''} ago"
22
+ elif diff < 604800:
23
+ days = int(diff / 86400)
24
+ return f"{days} day{'s' if days != 1 else ''} ago"
25
+ elif diff < 2592000:
26
+ weeks = int(diff / 604800)
27
+ return f"{weeks} week{'s' if weeks != 1 else ''} ago"
28
+ else:
29
+ months = int(diff / 2592000)
30
+ return f"{months} month{'s' if months != 1 else ''} ago"
31
+
32
+
12
33
  def resume_select_session() -> str | None:
13
- # Column widths
14
- UPDATED_AT_WIDTH = 16
15
- MSG_COUNT_WIDTH = 3
16
- MODEL_WIDTH = 25
17
- FIRST_MESSAGE_WIDTH = 50
18
34
  sessions = Session.list_sessions()
19
35
  if not sessions:
20
36
  log("No sessions found for this project.")
21
37
  return None
22
38
 
23
- def _fmt(ts: float) -> str:
24
- try:
25
- return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
26
- except (ValueError, OSError):
27
- return str(ts)
28
-
29
39
  try:
30
- import questionary
40
+ from prompt_toolkit.styles import Style
31
41
 
32
- choices: list[Choice] = []
42
+ items: list[SelectItem[str]] = []
33
43
  for s in sessions:
34
- first_user_message = s.first_user_message or "N/A"
35
- msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
36
- model_display = s.model_name or "N/A"
44
+ first_msg = s.first_user_message or "N/A"
45
+ first_msg = first_msg.strip().replace("\n", " ")
46
+
47
+ msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} messages"
48
+ model = s.model_name or "N/A"
37
49
 
38
50
  title = [
39
- ("class:d", f"{_fmt(s.updated_at):<{UPDATED_AT_WIDTH}} "),
40
- ("class:b", f"{msg_count_display:>{MSG_COUNT_WIDTH}} "),
41
- (
42
- "class:t",
43
- f"{model_display[: MODEL_WIDTH - 1] + '…' if len(model_display) > MODEL_WIDTH else model_display:<{MODEL_WIDTH}} ",
44
- ),
45
- (
46
- "class:t",
47
- f"{first_user_message.strip().replace('\n', ' ↩ '):<{FIRST_MESSAGE_WIDTH}}",
48
- ),
51
+ ("class:msg", f"{first_msg}\n"),
52
+ ("class:meta", f" {msg_count} · {_relative_time(s.updated_at)} · {model} · {s.id}\n\n"),
49
53
  ]
50
- choices.append(questionary.Choice(title=title, value=s.id))
51
- return questionary.select(
52
- message=f"{' Updated at':<{UPDATED_AT_WIDTH + 1}} {'Msg':>{MSG_COUNT_WIDTH}} {'Model':<{MODEL_WIDTH}} {'First message':<{FIRST_MESSAGE_WIDTH}}",
53
- choices=choices,
54
+ items.append(
55
+ SelectItem(
56
+ title=title,
57
+ value=str(s.id),
58
+ search_text=f"{first_msg} {model} {s.id}",
59
+ )
60
+ )
61
+
62
+ return select_one(
63
+ message="Select a session to resume:",
64
+ items=items,
54
65
  pointer="→",
55
- instruction="↑↓ to move",
56
- style=questionary.Style(
66
+ style=Style(
57
67
  [
58
- ("t", ""),
59
- ("b", "bold"),
60
- ("d", "dim"),
68
+ ("msg", ""),
69
+ ("meta", "fg:ansibrightblack"),
70
+ ("pointer", "bold fg:ansigreen"),
71
+ ("highlighted", "fg:ansigreen"),
72
+ ("search_prefix", "fg:ansibrightblack"),
73
+ ("search_success", "noinherit fg:ansigreen"),
74
+ ("search_none", "noinherit fg:ansired"),
75
+ ("question", "bold"),
76
+ ("text", ""),
61
77
  ]
62
78
  ),
63
- ).ask()
79
+ )
64
80
  except Exception as e:
65
- log_debug(f"Failed to use questionary for session select, {e}")
81
+ log_debug(f"Failed to use prompt_toolkit for session select, {e}")
66
82
 
67
83
  for i, s in enumerate(sessions, 1):
68
- msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
69
- model_display = s.model_name or "N/A"
70
- print(
71
- f"{i}. {_fmt(s.updated_at)} {msg_count_display:>{MSG_COUNT_WIDTH}} "
72
- f"{model_display[: MODEL_WIDTH - 1] + '…' if len(model_display) > MODEL_WIDTH else model_display:<{MODEL_WIDTH}} {s.id} {s.work_dir}"
73
- )
84
+ first_msg = (s.first_user_message or "N/A").strip().replace("\n", " ")
85
+ if len(first_msg) > 60:
86
+ first_msg = first_msg[:59] + "…"
87
+ msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} msgs"
88
+ model = s.model_name or "N/A"
89
+ print(f"{i}. {first_msg}")
90
+ print(f" {_relative_time(s.updated_at)} · {msg_count} · {model}")
74
91
  try:
75
92
  raw = input("Select a session number: ").strip()
76
93
  idx = int(raw)
@@ -12,6 +12,7 @@ from klaude_code.ui.core.stage_manager import Stage, StageManager
12
12
  from klaude_code.ui.modes.repl.renderer import REPLRenderer
13
13
  from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
14
14
  from klaude_code.ui.renderers.thinking import THINKING_MESSAGE_MARK, normalize_thinking_content
15
+ from klaude_code.ui.rich import status as r_status
15
16
  from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
16
17
  from klaude_code.ui.rich.theme import ThemeKey
17
18
  from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
@@ -352,6 +353,8 @@ class DisplayEventHandler:
352
353
  self.renderer.display_user_message(event)
353
354
 
354
355
  def _on_task_start(self, event: events.TaskStartEvent) -> None:
356
+ if event.sub_agent_state is None:
357
+ r_status.set_task_start()
355
358
  self.renderer.spinner_start()
356
359
  self.renderer.display_task_start(event)
357
360
  emit_osc94(OSC94States.INDETERMINATE)
@@ -501,6 +504,7 @@ class DisplayEventHandler:
501
504
  async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
502
505
  self.renderer.display_task_finish(event)
503
506
  if not self.renderer.is_sub_agent_session(event.session_id):
507
+ r_status.clear_task_start()
504
508
  emit_osc94(OSC94States.HIDDEN)
505
509
  self.spinner_status.reset()
506
510
  self.renderer.spinner_stop()
@@ -511,6 +515,7 @@ class DisplayEventHandler:
511
515
  async def _on_interrupt(self, event: events.InterruptEvent) -> None:
512
516
  self.renderer.spinner_stop()
513
517
  self.spinner_status.reset()
518
+ r_status.clear_task_start()
514
519
  await self.stage_manager.transition_to(Stage.WAITING)
515
520
  emit_osc94(OSC94States.HIDDEN)
516
521
  self.renderer.display_interrupt()
@@ -528,6 +533,7 @@ class DisplayEventHandler:
528
533
  await self.stage_manager.transition_to(Stage.WAITING)
529
534
  self.renderer.spinner_stop()
530
535
  self.spinner_status.reset()
536
+ r_status.clear_task_start()
531
537
 
532
538
  # ─────────────────────────────────────────────────────────────────────────────
533
539
  # Private helper methods
@@ -619,7 +625,7 @@ class DisplayEventHandler:
619
625
  # 1 cell for glyph + 1 cell of padding between columns (collapsed).
620
626
  spinner_prefix_cells = 2
621
627
 
622
- hint_cells = cell_len(const.STATUS_HINT_TEXT)
628
+ hint_cells = cell_len(r_status.current_hint_text())
623
629
  right_cells = cell_len(right_text.plain) if right_text is not None else 0
624
630
 
625
631
  max_main_cells = terminal_width - spinner_prefix_cells - hint_cells - right_cells - 1
@@ -55,7 +55,7 @@ class PromptToolkitInput(InputProviderABC):
55
55
  self._pre_prompt = pre_prompt
56
56
  self._post_prompt = post_prompt
57
57
  # Use provided value if available to avoid redundant TTY queries that may interfere
58
- # with prompt_toolkit's terminal state after questionary has been used.
58
+ # with prompt_toolkit's terminal state after interactive UIs have been used.
59
59
  self._is_light_terminal_background = (
60
60
  is_light_background if is_light_background is not None else is_light_terminal_background(timeout=0.2)
61
61
  )
@@ -34,6 +34,47 @@ def create_key_bindings(
34
34
  """
35
35
  kb = KeyBindings()
36
36
 
37
+ def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
38
+ """Return True when Enter should submit even if completions are visible.
39
+
40
+ We show completions proactively for contexts like `/`.
41
+ If the user already typed an exact candidate (e.g. `/clear`), accepting
42
+ a completion often only adds a trailing space and makes Enter require
43
+ two presses. In that case, prefer submitting.
44
+ """
45
+ state = buf.complete_state
46
+ if state is None or not state.completions:
47
+ return False
48
+
49
+ try:
50
+ doc = buf.document # type: ignore[reportUnknownMemberType]
51
+ text = cast(str, doc.text) # type: ignore[reportUnknownMemberType]
52
+ cursor_pos = cast(int, doc.cursor_position) # type: ignore[reportUnknownMemberType]
53
+ except Exception:
54
+ return False
55
+
56
+ # Only apply this heuristic when the caret is at the end of the buffer.
57
+ if cursor_pos != len(text):
58
+ return False
59
+
60
+ for completion in state.completions:
61
+ try:
62
+ start = cursor_pos + completion.start_position
63
+ if start < 0 or start > cursor_pos:
64
+ continue
65
+
66
+ replaced = text[start:cursor_pos]
67
+ inserted = completion.text
68
+
69
+ # If the user already typed an exact candidate, don't force
70
+ # accepting a completion (which often just adds a space).
71
+ if replaced == inserted or replaced == inserted.rstrip():
72
+ return True
73
+ except Exception:
74
+ continue
75
+
76
+ return False
77
+
37
78
  def _select_first_completion_if_needed(buf: Buffer) -> None:
38
79
  """Ensure the completion menu has an active selection.
39
80
 
@@ -98,7 +139,7 @@ def create_key_bindings(
98
139
  # When completions are visible, Enter accepts the current selection.
99
140
  # This aligns with common TUI completion UX: navigation doesn't modify
100
141
  # the buffer, and Enter/Tab inserts the selected option.
101
- if _accept_current_completion(buf):
142
+ if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
102
143
  return
103
144
 
104
145
  # If the entire buffer is whitespace-only, insert a newline rather than submitting.
@@ -67,23 +67,32 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
67
67
 
68
68
  if e.item.at_files:
69
69
  grid = create_grid()
70
+ # Group at_files by (operation, mentioned_in)
71
+ grouped: dict[tuple[str, str | None], list[str]] = {}
70
72
  for at_file in e.item.at_files:
71
- if at_file.mentioned_in:
73
+ key = (at_file.operation, at_file.mentioned_in)
74
+ if key not in grouped:
75
+ grouped[key] = []
76
+ grouped[key].append(at_file.path)
77
+
78
+ for (operation, mentioned_in), paths in grouped.items():
79
+ path_texts = Text(", ", ThemeKey.REMINDER).join(render_path(p, ThemeKey.REMINDER_BOLD) for p in paths)
80
+ if mentioned_in:
72
81
  grid.add_row(
73
82
  Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
74
83
  Text.assemble(
75
- (f"{at_file.operation} ", ThemeKey.REMINDER),
76
- render_path(at_file.path, ThemeKey.REMINDER_BOLD),
84
+ (f"{operation} ", ThemeKey.REMINDER),
85
+ path_texts,
77
86
  (" mentioned in ", ThemeKey.REMINDER),
78
- render_path(at_file.mentioned_in, ThemeKey.REMINDER_BOLD),
87
+ render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
79
88
  ),
80
89
  )
81
90
  else:
82
91
  grid.add_row(
83
92
  Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
84
93
  Text.assemble(
85
- (f"{at_file.operation} ", ThemeKey.REMINDER),
86
- render_path(at_file.path, ThemeKey.REMINDER_BOLD),
94
+ (f"{operation} ", ThemeKey.REMINDER),
95
+ path_texts,
87
96
  ),
88
97
  )
89
98
  parts.append(grid)
@@ -17,5 +17,5 @@ def render_tool_error(error_msg: Text) -> RenderableType:
17
17
  """Render error with indent for tool results."""
18
18
  grid = create_grid()
19
19
  error_msg.style = ThemeKey.ERROR
20
- grid.add_row(Text(" "), error_msg)
20
+ grid.add_row(Text(" "), error_msg)
21
21
  return grid
@@ -27,7 +27,7 @@ MARK_PLAN = "◈"
27
27
  MARK_READ = "→"
28
28
  MARK_EDIT = "±"
29
29
  MARK_WRITE = "+"
30
- MARK_MOVE = ""
30
+ MARK_MOVE = "±"
31
31
  MARK_MERMAID = "⧉"
32
32
  MARK_WEB_FETCH = "→"
33
33
  MARK_WEB_SEARCH = "✱"
@@ -6,9 +6,7 @@ from collections.abc import Iterable, Sequence
6
6
  class SearchableFormattedText:
7
7
  """
8
8
  Wrapper for prompt_toolkit formatted text that also supports string-like
9
- methods used by questionary's search filter (e.g., ``.lower()``).
10
-
11
- This allows using ``use_search_filter=True`` with a formatted ``Choice.title``.
9
+ methods commonly expected by search filters (e.g., ``.lower()``).
12
10
 
13
11
  - ``fragments``: A sequence of (style, text) tuples accepted by
14
12
  prompt_toolkit's ``to_formatted_text``.
@@ -32,7 +30,7 @@ class SearchableFormattedText:
32
30
  def __str__(self) -> str: # pragma: no cover - utility
33
31
  return self._plain
34
32
 
35
- # Minimal string API to satisfy questionary's search filter logic.
33
+ # Minimal string API for search filtering.
36
34
  def lower(self) -> str:
37
35
  return self._plain.lower()
38
36
 
@@ -47,10 +45,9 @@ class SearchableFormattedText:
47
45
 
48
46
  class SearchableFormattedList(list[tuple[str, str]]):
49
47
  """
50
- List variant compatible with questionary's expected ``Choice.title`` type.
48
+ List variant compatible with prompt_toolkit formatted-text usage.
51
49
 
52
- - Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works),
53
- preserving existing styling behavior in questionary.
50
+ - Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works).
54
51
  - Provides ``.lower()``/``.upper()`` returning the plain text for search filtering.
55
52
  """
56
53
 
@@ -31,6 +31,7 @@ random.shuffle(BREATHING_SPINNER_GLYPHS)
31
31
 
32
32
 
33
33
  _process_start: float | None = None
34
+ _task_start: float | None = None
34
35
 
35
36
 
36
37
  def _elapsed_since_start() -> float:
@@ -42,6 +43,59 @@ def _elapsed_since_start() -> float:
42
43
  return now - _process_start
43
44
 
44
45
 
46
+ def set_task_start(start: float | None = None) -> None:
47
+ """Set the current task start time (perf_counter seconds)."""
48
+
49
+ global _task_start
50
+ _task_start = time.perf_counter() if start is None else start
51
+
52
+
53
+ def clear_task_start() -> None:
54
+ """Clear the current task start time."""
55
+
56
+ global _task_start
57
+ _task_start = None
58
+
59
+
60
+ def _task_elapsed_seconds(now: float | None = None) -> float | None:
61
+ if _task_start is None:
62
+ return None
63
+ current = time.perf_counter() if now is None else now
64
+ return max(0.0, current - _task_start)
65
+
66
+
67
+ def _format_elapsed_compact(seconds: float) -> str:
68
+ total_seconds = max(0, int(seconds))
69
+ if total_seconds < 60:
70
+ return f"{total_seconds}s"
71
+
72
+ minutes, sec = divmod(total_seconds, 60)
73
+ if minutes < 60:
74
+ return f"{minutes}m{sec:02d}s"
75
+
76
+ hours, minute = divmod(minutes, 60)
77
+ return f"{hours}h{minute:02d}m{sec:02d}s"
78
+
79
+
80
+ def current_hint_text(*, min_time_width: int = 0) -> str:
81
+ """Return the full hint string shown on the status line.
82
+
83
+ Includes an optional elapsed time prefix (right-aligned, min width) and
84
+ the constant hint suffix.
85
+ """
86
+
87
+ elapsed = _task_elapsed_seconds()
88
+ if elapsed is None:
89
+ return const.STATUS_HINT_TEXT
90
+ time_text = _format_elapsed_compact(elapsed)
91
+ if min_time_width > 0:
92
+ time_text = time_text.rjust(min_time_width)
93
+ suffix = const.STATUS_HINT_TEXT.strip()
94
+ if suffix.startswith("(") and suffix.endswith(")"):
95
+ suffix = suffix[1:-1]
96
+ return f" ({time_text} · {suffix})"
97
+
98
+
45
99
  def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
46
100
  """Compute per-character shimmer intensity for a horizontal band.
47
101
 
@@ -176,7 +230,6 @@ class ShimmerStatusText:
176
230
  self._main_text = text
177
231
  else:
178
232
  self._main_text = Text(main_text, style=main_style)
179
- self._hint_text = Text(const.STATUS_HINT_TEXT)
180
233
  self._hint_style = ThemeKey.STATUS_HINT
181
234
  self._right_text = right_text
182
235
 
@@ -206,7 +259,7 @@ class ShimmerStatusText:
206
259
  result.append(ch, style=style)
207
260
 
208
261
  # Append hint text without shimmer
209
- result.append(self._hint_text.plain, style=hint_style)
262
+ result.append(current_hint_text().strip("\n"), style=hint_style)
210
263
 
211
264
  return result
212
265
 
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import sys
5
+ from dataclasses import dataclass
6
+ from functools import partial
7
+
8
+ from prompt_toolkit.application import Application
9
+ from prompt_toolkit.application.current import get_app
10
+ from prompt_toolkit.buffer import Buffer
11
+ from prompt_toolkit.filters import Always, Condition
12
+ from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent, merge_key_bindings
13
+ from prompt_toolkit.key_binding.defaults import load_key_bindings
14
+ from prompt_toolkit.keys import Keys
15
+ from prompt_toolkit.layout import ConditionalContainer, Float, FloatContainer, HSplit, Layout, VSplit, Window
16
+ from prompt_toolkit.layout.containers import Container, ScrollOffsets
17
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
18
+ from prompt_toolkit.styles import Style, merge_styles
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class SelectItem[T]:
23
+ """One selectable item for terminal selection UI."""
24
+
25
+ title: list[tuple[str, str]]
26
+ value: T
27
+ search_text: str
28
+
29
+
30
+ def select_one[T](
31
+ *,
32
+ message: str,
33
+ items: list[SelectItem[T]],
34
+ pointer: str = "→",
35
+ style: Style | None = None,
36
+ use_search_filter: bool = True,
37
+ initial_value: T | None = None,
38
+ search_placeholder: str = "type to search",
39
+ ) -> T | None:
40
+ """Terminal single-choice selector based on prompt_toolkit.
41
+
42
+ Features:
43
+ - Search-as-you-type filter (optional)
44
+ - Multi-line titles (via formatted text fragments)
45
+ - Highlight entire pointed item via `class:highlighted`
46
+ """
47
+
48
+ if not items:
49
+ return None
50
+
51
+ # Non-interactive environments should not enter an interactive prompt.
52
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
53
+ return None
54
+
55
+ pointed_at = 0
56
+
57
+ search_buffer: Buffer | None = None
58
+ if use_search_filter:
59
+ search_buffer = Buffer()
60
+
61
+ def visible_indices() -> tuple[list[int], bool]:
62
+ filter_text = search_buffer.text if (use_search_filter and search_buffer is not None) else ""
63
+ if not filter_text:
64
+ return list(range(len(items))), True
65
+
66
+ needle = filter_text.lower()
67
+ matched = [i for i, it in enumerate(items) if needle in it.search_text.lower()]
68
+ if matched:
69
+ return matched, True
70
+ return list(range(len(items))), False
71
+
72
+ def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, str]]:
73
+ # Keep simple text attributes like bold/italic while overriding colors via `cls`.
74
+ keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike"}
75
+ restyled: list[tuple[str, str]] = []
76
+ for old_style, text in title:
77
+ attrs = [tok for tok in old_style.split() if tok in keep_attrs]
78
+ style = f"{cls} {' '.join(attrs)}".strip()
79
+ restyled.append((style, text))
80
+ return restyled
81
+
82
+ def get_header_tokens() -> list[tuple[str, str]]:
83
+ return [("class:question", message + " ")]
84
+
85
+ def get_choices_tokens() -> list[tuple[str, str]]:
86
+ nonlocal pointed_at
87
+ indices, _found = visible_indices()
88
+ if not indices:
89
+ return [("class:text", "(no items)\n")]
90
+
91
+ pointed_at %= len(indices)
92
+ tokens: list[tuple[str, str]] = []
93
+
94
+ pointer_pad = " " * (2 + len(pointer))
95
+ pointed_prefix = f" {pointer} "
96
+
97
+ for pos, idx in enumerate(indices):
98
+ is_pointed = pos == pointed_at
99
+
100
+ if is_pointed:
101
+ tokens.append(("class:pointer", pointed_prefix))
102
+ tokens.append(("[SetCursorPosition]", ""))
103
+ else:
104
+ tokens.append(("class:text", pointer_pad))
105
+
106
+ title_tokens = _restyle_title(items[idx].title, "class:highlighted") if is_pointed else items[idx].title
107
+ tokens.extend(title_tokens)
108
+
109
+ return tokens
110
+
111
+ def _on_search_changed(_buf: Buffer) -> None:
112
+ nonlocal pointed_at
113
+ pointed_at = 0
114
+ with contextlib.suppress(Exception):
115
+ get_app().invalidate()
116
+
117
+ kb = KeyBindings()
118
+
119
+ @kb.add(Keys.ControlC, eager=True)
120
+ @kb.add(Keys.ControlQ, eager=True)
121
+ def _cancel(event: KeyPressEvent) -> None:
122
+ event.app.exit(result=None)
123
+
124
+ _ = _cancel # registered via decorator
125
+
126
+ @kb.add(Keys.Down, eager=True)
127
+ def _down(event: KeyPressEvent) -> None:
128
+ nonlocal pointed_at
129
+ pointed_at += 1
130
+ event.app.invalidate()
131
+
132
+ _ = _down # registered via decorator
133
+
134
+ @kb.add(Keys.Up, eager=True)
135
+ def _up(event: KeyPressEvent) -> None:
136
+ nonlocal pointed_at
137
+ pointed_at -= 1
138
+ event.app.invalidate()
139
+
140
+ _ = _up # registered via decorator
141
+
142
+ @kb.add(Keys.Enter, eager=True)
143
+ def _enter(event: KeyPressEvent) -> None:
144
+ indices, _ = visible_indices()
145
+ if not indices:
146
+ event.app.exit(result=None)
147
+ return
148
+ idx = indices[pointed_at % len(indices)]
149
+ event.app.exit(result=items[idx].value)
150
+
151
+ _ = _enter # registered via decorator
152
+
153
+ @kb.add(Keys.Escape, eager=True)
154
+ def _esc(event: KeyPressEvent) -> None:
155
+ nonlocal pointed_at
156
+ if use_search_filter and search_buffer is not None and search_buffer.text:
157
+ search_buffer.reset(append_to_history=False)
158
+ pointed_at = 0
159
+ event.app.invalidate()
160
+ return
161
+ event.app.exit(result=None)
162
+
163
+ _ = _esc # registered via decorator
164
+
165
+ if use_search_filter and search_buffer is not None:
166
+ search_buffer.on_text_changed += _on_search_changed
167
+
168
+ if initial_value is not None:
169
+ try:
170
+ full_index = next(i for i, it in enumerate(items) if it.value == initial_value)
171
+ indices, _ = visible_indices()
172
+ pointed_at = indices.index(full_index) if full_index in indices else 0
173
+ except StopIteration:
174
+ pointed_at = 0
175
+
176
+ header_window = Window(
177
+ FormattedTextControl(get_header_tokens),
178
+ height=1,
179
+ dont_extend_height=Always(),
180
+ always_hide_cursor=Always(),
181
+ )
182
+ spacer_window = Window(
183
+ FormattedTextControl([("", "")]),
184
+ height=1,
185
+ dont_extend_height=Always(),
186
+ always_hide_cursor=Always(),
187
+ )
188
+ list_window = Window(
189
+ FormattedTextControl(get_choices_tokens),
190
+ scroll_offsets=ScrollOffsets(top=0, bottom=2),
191
+ allow_scroll_beyond_bottom=True,
192
+ dont_extend_height=Always(),
193
+ always_hide_cursor=Always(),
194
+ )
195
+
196
+ search_container = None
197
+ search_input_window: Window | None = None
198
+ if use_search_filter and search_buffer is not None:
199
+ placeholder_text = f"{search_placeholder} · ↑↓ to select"
200
+
201
+ search_prefix_window = Window(
202
+ FormattedTextControl([("class:search_prefix", "/ ")]),
203
+ width=2,
204
+ height=1,
205
+ dont_extend_height=Always(),
206
+ always_hide_cursor=Always(),
207
+ )
208
+ input_window = Window(
209
+ BufferControl(buffer=search_buffer),
210
+ height=1,
211
+ dont_extend_height=Always(),
212
+ style="class:search_input",
213
+ )
214
+ placeholder_window = ConditionalContainer(
215
+ content=Window(
216
+ FormattedTextControl([("class:search_placeholder", placeholder_text)]),
217
+ height=1,
218
+ dont_extend_height=Always(),
219
+ always_hide_cursor=Always(),
220
+ ),
221
+ filter=Condition(lambda: search_buffer.text == ""),
222
+ )
223
+ search_input_window = input_window
224
+ search_input_container = FloatContainer(
225
+ content=input_window,
226
+ floats=[Float(content=placeholder_window, top=0, left=0)],
227
+ )
228
+
229
+ def _rounded_frame(body: Container) -> HSplit:
230
+ border = partial(Window, style="class:frame.border", height=1)
231
+ top = VSplit(
232
+ [
233
+ border(width=1, char="╭"),
234
+ border(char="─"),
235
+ border(width=1, char="╮"),
236
+ ],
237
+ height=1,
238
+ padding=0,
239
+ )
240
+ middle = VSplit(
241
+ [
242
+ border(width=1, char="│"),
243
+ body,
244
+ border(width=1, char="│"),
245
+ ],
246
+ padding=0,
247
+ )
248
+ bottom = VSplit(
249
+ [
250
+ border(width=1, char="╰"),
251
+ border(char="─"),
252
+ border(width=1, char="╯"),
253
+ ],
254
+ height=1,
255
+ padding=0,
256
+ )
257
+ return HSplit([top, middle, bottom], padding=0, style="class:frame")
258
+
259
+ search_container = _rounded_frame(VSplit([search_prefix_window, search_input_container], padding=0))
260
+
261
+ base_style = Style(
262
+ [
263
+ ("frame.border", "fg:ansibrightblack"),
264
+ ("frame.label", "fg:ansibrightblack italic"),
265
+ ("search_prefix", "fg:ansibrightblack"),
266
+ ("search_placeholder", "fg:ansibrightblack italic"),
267
+ ]
268
+ )
269
+ merged_style = merge_styles([base_style, style] if style is not None else [base_style])
270
+
271
+ root_children: list[Container] = [header_window, spacer_window, list_window]
272
+ if search_container is not None:
273
+ root_children.append(search_container)
274
+
275
+ app: Application[T | None] = Application(
276
+ layout=Layout(HSplit(root_children), focused_element=search_input_window or list_window),
277
+ key_bindings=merge_key_bindings([load_key_bindings(), kb]),
278
+ style=merged_style,
279
+ mouse_support=False,
280
+ full_screen=False,
281
+ erase_when_done=True,
282
+ )
283
+ return app.run()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -12,7 +12,6 @@ Requires-Dist: pillow>=12.0.0
12
12
  Requires-Dist: prompt-toolkit>=3.0.52
13
13
  Requires-Dist: pydantic>=2.11.7
14
14
  Requires-Dist: pyyaml>=6.0.2
15
- Requires-Dist: questionary>=2.1.1
16
15
  Requires-Dist: rich>=14.1.0
17
16
  Requires-Dist: trafilatura>=2.0.0
18
17
  Requires-Dist: typer>=0.17.3
@@ -11,9 +11,9 @@ klaude_code/cli/config_cmd.py,sha256=hlvslLNgdRHkokq1Pnam0XOdR3jqO3K0vNLqtWnPa6Q
11
11
  klaude_code/cli/debug.py,sha256=cPQ7cgATcJTyBIboleW_Q4Pa_t-tGG6x-Hj3woeeuHE,2669
12
12
  klaude_code/cli/list_model.py,sha256=uA0PNR1RjUK7BCKu2Q0Sh2xB9j9Gpwp_bsWhroTW6JY,9260
13
13
  klaude_code/cli/main.py,sha256=td_nMg0AyFDdyh3TLi7WCpi9DyAehduI4jZSOAaCNXI,12857
14
- klaude_code/cli/runtime.py,sha256=EGlfr839OWAJjLSfYHnTxwG7W-IxkmktgDOj8npKkFo,15219
14
+ klaude_code/cli/runtime.py,sha256=9PbyCiyaSm3wQZViIMOgJRNnjgLLIkCkQrDcn8yMn1E,15190
15
15
  klaude_code/cli/self_update.py,sha256=iGuj0i869Zi0M70W52-VVLxZp90ISr30fQpZkHGMK2o,8059
16
- klaude_code/cli/session_cmd.py,sha256=mVL2fDi76nmpK2bWYrU7Wh6jCi90A9YK8hv3UNo4GI0,2661
16
+ klaude_code/cli/session_cmd.py,sha256=q2YarlV6KARkFnbm_36ZUvBh8Noj8B7TlMg1RIlt1GE,3154
17
17
  klaude_code/command/__init__.py,sha256=hN239WtGddSsln7qF-VMJZw1FNOTb1gBikWZrzfoC9M,3366
18
18
  klaude_code/command/clear_cmd.py,sha256=3Ru6pFmOwru06XTLTuEGNUhKgy3COOaNe22Dk0TpGrQ,719
19
19
  klaude_code/command/command_abc.py,sha256=wZl_azY6Dpd4OvjtkSEPI3ilXaygLIVkO7NCgNlrofQ,2536
@@ -22,7 +22,7 @@ klaude_code/command/export_cmd.py,sha256=Cs7YXWtos-ZfN9OEppIl8Xrb017kDG7R6hGiilq
22
22
  klaude_code/command/export_online_cmd.py,sha256=RYYLnkLtg6edsgysmhsfTw16ncFRIT6PqeTdWhWXLHE,6094
23
23
  klaude_code/command/fork_session_cmd.py,sha256=T3o0mOVcqL2Ts39Ijl4t2Sl0GYbvh8zyd9z21M-AThU,1682
24
24
  klaude_code/command/help_cmd.py,sha256=yQJnVtj6sgXQdGsi4u9aS7EcjJLSrXccUA-v_bqmsRw,1633
25
- klaude_code/command/model_cmd.py,sha256=xnBMB9odyX4SZTerbkG01HNc3wlPLzoB57AZJr0vCmc,2935
25
+ klaude_code/command/model_cmd.py,sha256=Ff-2tkvpdnnSoCN98FUjHMtoA2qk1PNNkhzZlW22HAk,3053
26
26
  klaude_code/command/prompt-init.md,sha256=a4_FQ3gKizqs2vl9oEY5jtG6HNhv3f-1b5RSCFq0A18,1873
27
27
  klaude_code/command/prompt-jj-describe.md,sha256=n-7hiXU8oodCMR3ipNyRR86pAUzXMz6seloU9a6QQnY,974
28
28
  klaude_code/command/prompt_command.py,sha256=rMi-ZRLpUSt1t0IQVtwnzIYqcrXK-MwZrabbZ8dc8U4,2774
@@ -32,13 +32,13 @@ klaude_code/command/release_notes_cmd.py,sha256=FIrBRfKTlXEp8mBh15buNjgOrl_GMX7F
32
32
  klaude_code/command/resume_cmd.py,sha256=z5PNVZ_jzH-8YWPd0Qjj8mFwE8kjPSpM_GohExXL_QA,1970
33
33
  klaude_code/command/status_cmd.py,sha256=95cp4-Qg7ju4TZhKIV6_dfv1rrjcyNO6816NHtfk6v0,5413
34
34
  klaude_code/command/terminal_setup_cmd.py,sha256=SivM1gX_anGY_8DCQNFZ5VblFqt4sVgCMEWPRlo6K5w,10911
35
- klaude_code/command/thinking_cmd.py,sha256=8EdSN6huXihM5NHJEryZLA7CkgRT7mZgMVTJsT1-x8U,9108
35
+ klaude_code/command/thinking_cmd.py,sha256=uip1Cvam2f9j44yyT57j8H5LjPeLP7mkRK3A5bmuAMU,9327
36
36
  klaude_code/config/__init__.py,sha256=Qe1BeMekBfO2-Zd30x33lB70hdM1QQZGrp4DbWSQ-II,353
37
37
  klaude_code/config/assets/__init__.py,sha256=uMUfmXT3I-gYiI-HVr1DrE60mx5cY1o8V7SYuGqOmvY,32
38
38
  klaude_code/config/assets/builtin_config.yaml,sha256=9sfpcVqY3uWVGSdyteH3P_B8ZDwPhfJAoT2Q5o7I1bk,5605
39
39
  klaude_code/config/builtin_config.py,sha256=RgbawLV4yKk1IhJsQn04RkitDyLLhCwTqQ3nJkdvHGI,1113
40
40
  klaude_code/config/config.py,sha256=rTMU-7IYl_fmthB4LsrCaSgUVttNSw7Agif8XRCmU1g,16331
41
- klaude_code/config/select_model.py,sha256=vj4Qs1cl9-u6ucAjDwDvoODmlplwdYo1U57jUcKPya4,7035
41
+ klaude_code/config/select_model.py,sha256=0ON5O2ITBvyBNU80uEFQr65z2wbhdHtlzYLVTYx0AjA,7303
42
42
  klaude_code/const.py,sha256=Xc6UKku2sGQE05mvPNCpBbKK205vJrS9CaNAeKvu1AA,4612
43
43
  klaude_code/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  klaude_code/core/agent.py,sha256=bWm-UFX_0-KAy5j_YHH8X8o3MJT4-40Ni2EaDP2SL5k,5819
@@ -60,7 +60,7 @@ klaude_code/core/prompts/prompt-sub-agent-web.md,sha256=ewS7-h8_u4QZftFpqrZWpht9
60
60
  klaude_code/core/prompts/prompt-sub-agent.md,sha256=dmmdsOenbAOfqG6FmdR88spOLZkXmntDBs-cmZ9DN_g,897
61
61
  klaude_code/core/reminders.py,sha256=M_YPlOuZ2TpTjoxfEp1FbswB4yuk9_XUgSGb9MoMBCA,24741
62
62
  klaude_code/core/task.py,sha256=V0z-QSDSxB4d4mcqXl6z_KydG_yT0JhD7274AQbNGKM,11858
63
- klaude_code/core/tool/__init__.py,sha256=im3Tv1cE-H7PcVVZXaLD040U8O55cyoMyGlYo4Rp210,1992
63
+ klaude_code/core/tool/__init__.py,sha256=Y1r-xka9Zk5e5SrB0kcweFp4LyH0aafQ-BDiwYCAqFY,1992
64
64
  klaude_code/core/tool/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  klaude_code/core/tool/file/_utils.py,sha256=OG4BE9WyJqzH8ilVCL3D9yvAcHk-r-L9snd-E8gO_io,967
66
66
  klaude_code/core/tool/file/apply_patch.py,sha256=LZd3pYQ9ow_TxiFnqYuzD216HmvkLX6lW6BoMd9iQRs,17080
@@ -141,7 +141,7 @@ klaude_code/protocol/tools.py,sha256=ejhMCBBMz1ODbPEiynhzjB-aLbIRKL-wipPFv-nEz4g
141
141
  klaude_code/session/__init__.py,sha256=oXcDA5w-gJCbzmlF8yuWy3ezIW9DgFBNUs-gJHUJ-Rc,121
142
142
  klaude_code/session/codec.py,sha256=ummbqT7t6uHHXtaS9lOkyhi1h0YpMk7SNSms8DyGAHU,2015
143
143
  klaude_code/session/export.py,sha256=dj-IRUNtXL8uONDj9bsEXcEHKyeHY7lIcXv80yP88h4,31022
144
- klaude_code/session/selector.py,sha256=uJQTQAw1ce9LzNOswSFEPkB7_PTzoJRXbA9rwK8hvdQ,2913
144
+ klaude_code/session/selector.py,sha256=T4CUAJ6trrN14W9jXm1rnVMuoetKtn3FZoQhftnxBoo,3409
145
145
  klaude_code/session/session.py,sha256=otWpPnCk5LGS5IW_zTdeXBtLdxbBlEK2jH5FnrOIpF4,16969
146
146
  klaude_code/session/store.py,sha256=-e-lInCB3N1nFLlet7bipkmPk1PXmGthuMxv5z3hg5o,6953
147
147
  klaude_code/session/templates/export_session.html,sha256=bA27AkcC7DQRoWmcMBeaR8WOx1z76hezEDf0aYH-0HQ,119780
@@ -171,22 +171,22 @@ klaude_code/ui/modes/repl/__init__.py,sha256=_0II73jlz5JUtvJsZ9sGRJzeHIQyJJpaI0e
171
171
  klaude_code/ui/modes/repl/clipboard.py,sha256=ZCpk7kRSXGhh0Q_BWtUUuSYT7ZOqRjAoRcg9T9n48Wo,5137
172
172
  klaude_code/ui/modes/repl/completers.py,sha256=AElBFnWculNsSadom7ScnKf-P_vilC4V5AUn2qpRkXE,30005
173
173
  klaude_code/ui/modes/repl/display.py,sha256=06wawOHWO2ItEA9EIEh97p3GDID7TJhAtpaA03nPQXs,2335
174
- klaude_code/ui/modes/repl/event_handler.py,sha256=2b4KBhprfDw8MVYDz7MzKV_lOgfHihPZ8Ae1ewqVgBM,25777
175
- klaude_code/ui/modes/repl/input_prompt_toolkit.py,sha256=8P3i9Q8DljSOmVJVjlMlWx0aeyp1iPBhcmi0RWAl_go,8762
176
- klaude_code/ui/modes/repl/key_bindings.py,sha256=4oXdzcOw6f0SQa43bMS_mGdtXjsO_NWqgf7n7UiIRb4,10412
174
+ klaude_code/ui/modes/repl/event_handler.py,sha256=pXjiLGilSzrsrr9lsk19NeiRGFjVq91Rtfp1PNLK36A,26026
175
+ klaude_code/ui/modes/repl/input_prompt_toolkit.py,sha256=P8L7GhupoWTQNDkRXs8JpNiv5JRaw9VjXTG060h4DIw,8767
176
+ klaude_code/ui/modes/repl/key_bindings.py,sha256=NKllb1W0LfJAZEqDjlW2aedBQ2PBEOOddoTiRNY1_mU,12121
177
177
  klaude_code/ui/modes/repl/renderer.py,sha256=7cum6SuKSuuBePDSyk4UvWs6q5dwgLA0NrJZ3eD9tHw,15902
178
178
  klaude_code/ui/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
179
  klaude_code/ui/renderers/assistant.py,sha256=7iu5zlHR7JGviHs2eA25Dsbd7ZkzCR2_0XzkqMPVxDI,862
180
180
  klaude_code/ui/renderers/bash_syntax.py,sha256=VcX_tuojOtS58s_Ff-Zmhw_6LBRn2wsvR5UBtEr_qQU,5923
181
181
  klaude_code/ui/renderers/common.py,sha256=l9V7yuiejowyw3FdZ2n3VJ2OO_K1rEUINmFz-mC2xlw,2648
182
- klaude_code/ui/renderers/developer.py,sha256=RrJsZphGdjuphIuTDnU1ivJ6tc-a3ndBvuyU17MQSQs,7776
182
+ klaude_code/ui/renderers/developer.py,sha256=J6hBko8fiRxF67scI1NQ_Ztz3Y47VF3uxXblhIqUciE,8138
183
183
  klaude_code/ui/renderers/diffs.py,sha256=uLpgYTudH38wucozoUw4xbPWMC6uYTQTaDTHcg-0zvM,10418
184
- klaude_code/ui/renderers/errors.py,sha256=-geQA6neIkeQK_m0jaboSdVHmO61qd9620lo1a029bU,656
184
+ klaude_code/ui/renderers/errors.py,sha256=MavmYOQ7lyjA_VpuUpDVFCuY9W7XrMVdLsg2lCOn4GY,655
185
185
  klaude_code/ui/renderers/mermaid_viewer.py,sha256=TIUFLtTqdG-iFD4Mgm8OdzU_9UO14niftTJ11f4makc,1691
186
186
  klaude_code/ui/renderers/metadata.py,sha256=jE_yxNIwsBMEiaiwd0b3czyKXkFbQZWQc5MYI-_2k5c,8280
187
187
  klaude_code/ui/renderers/sub_agent.py,sha256=g8QCFXTtFX_w8oTaGMYGuy6u5KqbFMlvzWofER0hGKk,5946
188
188
  klaude_code/ui/renderers/thinking.py,sha256=TbQxkjR6MuDXzASBK_rMaxxqvSdhfwDtVwXhOExuvlM,1946
189
- klaude_code/ui/renderers/tools.py,sha256=NWUwnNqXEWPrm6RnaIuQFpOrx0p5_6robwZhSayvp-Y,27892
189
+ klaude_code/ui/renderers/tools.py,sha256=lebQHccj2tkJIjO-JB0TvCIixx-BKXHfD-egXSxBV7Y,27891
190
190
  klaude_code/ui/renderers/user_input.py,sha256=e2hZS7UUnzQuQ6UqzSKRDkFJMkKTLUoub1JclHMX40g,3941
191
191
  klaude_code/ui/rich/__init__.py,sha256=zEZjnHR3Fnv_sFMxwIMjoJfwDoC4GRGv3lHJzAGRq_o,236
192
192
  klaude_code/ui/rich/cjk_wrap.py,sha256=ncmifgTwF6q95iayHQyazGbntt7BRQb_Ed7aXc8JU6Y,7551
@@ -194,17 +194,18 @@ klaude_code/ui/rich/code_panel.py,sha256=ZKuJHh-kh-hIkBXSGLERLaDbJ7I9hvtvmYKocJn
194
194
  klaude_code/ui/rich/live.py,sha256=qiBLPSE4KW_Dpemy5MZ5BKhkFWEN2fjXBiQHmhJrPSM,2722
195
195
  klaude_code/ui/rich/markdown.py,sha256=ltcm4qVX6fsqUNkPWeOwX636FsQ6-gST6uLLcXAl9yA,15397
196
196
  klaude_code/ui/rich/quote.py,sha256=tZcxN73SfDBHF_qk0Jkh9gWBqPBn8VLp9RF36YRdKEM,1123
197
- klaude_code/ui/rich/searchable_text.py,sha256=DCVZgEFv7_ergAvT2v7XrfQAUXUzhmAwuVAchlIx8RY,2448
198
- klaude_code/ui/rich/status.py,sha256=QHg4oWmPSQH19H81vOFpImEqWyDtAbIXjuCGsuDjBPA,9278
197
+ klaude_code/ui/rich/searchable_text.py,sha256=PUe6MotKxSBY4FlPeojVjVQgxCsx_jiQ41bCzLp8WvE,2271
198
+ klaude_code/ui/rich/status.py,sha256=GKX_kMT42epQE_xIw2TvudprcMUNF8OpH9QvVjocdhk,10812
199
199
  klaude_code/ui/rich/theme.py,sha256=-jsQ5vB2kIM6hZD_YZZejS1OJQE4b48hJur-Hcnbhs4,14439
200
200
  klaude_code/ui/terminal/__init__.py,sha256=GIMnsEcIAGT_vBHvTlWEdyNmAEpruyscUA6M_j3GQZU,1412
201
201
  klaude_code/ui/terminal/color.py,sha256=jvVbuysf5pnI0uAjUVeyW2HwU58dutTg2msykbu2w4Y,7197
202
202
  klaude_code/ui/terminal/control.py,sha256=WhkqEWdtzUO4iWULp-iI9VazAWmzzW52qTQXk-4Dr4s,4922
203
203
  klaude_code/ui/terminal/notifier.py,sha256=wkRM66d98Oh6PujnN4bB7NiQxIYEHqQXverMKU43H5E,3187
204
204
  klaude_code/ui/terminal/progress_bar.py,sha256=MDnhPbqCnN4GDgLOlxxOEVZPDwVC_XL2NM5sl1MFNcQ,2133
205
+ klaude_code/ui/terminal/selector.py,sha256=v0akaR4Jw5U_3fgK4INqf0R2nIhRZC-JqhECsF3z22I,9633
205
206
  klaude_code/ui/utils/__init__.py,sha256=YEsCLjbCPaPza-UXTPUMTJTrc9BmNBUP5CbFWlshyOQ,15
206
207
  klaude_code/ui/utils/common.py,sha256=tqHqwgLtAyP805kwRFyoAL4EgMutcNb3Y-GAXJ4IeuM,2263
207
- klaude_code-1.3.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
208
- klaude_code-1.3.0.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
209
- klaude_code-1.3.0.dist-info/METADATA,sha256=Eb3oOqTaMnmtF1ETnJda-XXad45O4_wvjeNTF8mviO4,9125
210
- klaude_code-1.3.0.dist-info/RECORD,,
208
+ klaude_code-1.4.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
209
+ klaude_code-1.4.1.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
210
+ klaude_code-1.4.1.dist-info/METADATA,sha256=5cZHqchLcz-Jw9wBBudFwewS-S97SSJS3b8bJ3iEo4Y,9091
211
+ klaude_code-1.4.1.dist-info/RECORD,,