klaude-code 1.4.2__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
klaude_code/cli/main.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import os
3
4
  import sys
4
5
  from pathlib import Path
@@ -10,18 +11,81 @@ from klaude_code.cli.config_cmd import register_config_commands
10
11
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
11
12
  from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
12
13
  from klaude_code.cli.session_cmd import register_session_commands
13
- from klaude_code.session import Session, resume_select_session
14
+ from klaude_code.session import Session, build_session_select_options
14
15
  from klaude_code.trace import DebugType, prepare_debug_log_file
15
16
 
16
17
 
18
+ def select_session_interactive() -> str | None:
19
+ """Interactive session selection for CLI.
20
+
21
+ Returns:
22
+ Selected session ID, or None if no session selected or no sessions exist.
23
+ """
24
+ from klaude_code.trace import log
25
+
26
+ options = build_session_select_options()
27
+ if not options:
28
+ log("No sessions found for this project.")
29
+ return None
30
+
31
+ from prompt_toolkit.styles import Style
32
+
33
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
34
+
35
+ items: list[SelectItem[str]] = []
36
+ for opt in options:
37
+ title = [
38
+ ("class:msg", f"{opt.first_user_message}\n"),
39
+ ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
40
+ ]
41
+ items.append(
42
+ SelectItem(
43
+ title=title,
44
+ value=opt.session_id,
45
+ search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
46
+ )
47
+ )
48
+
49
+ try:
50
+ return select_one(
51
+ message="Select a session to resume:",
52
+ items=items,
53
+ pointer="→",
54
+ style=Style(
55
+ [
56
+ ("msg", ""),
57
+ ("meta", "fg:ansibrightblack"),
58
+ ("pointer", "bold fg:ansigreen"),
59
+ ("highlighted", "fg:ansigreen"),
60
+ ("search_prefix", "fg:ansibrightblack"),
61
+ ("search_success", "noinherit fg:ansigreen"),
62
+ ("search_none", "noinherit fg:ansired"),
63
+ ("question", "bold"),
64
+ ("text", ""),
65
+ ]
66
+ ),
67
+ )
68
+ except KeyboardInterrupt:
69
+ return None
70
+
71
+
17
72
  def set_terminal_title(title: str) -> None:
18
73
  """Set terminal window title using ANSI escape sequence."""
19
74
  # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
20
75
  # This avoids corrupting machine-readable output (e.g., JSON streaming) and log captures.
21
- if not sys.stdout.isatty():
76
+ #
77
+ # Use the original stdout to bypass prompt_toolkit's `patch_stdout()`. Writing OSC
78
+ # sequences to the patched stdout can cause them to appear as visible text.
79
+ stream = getattr(sys, "__stdout__", None) or sys.stdout
80
+ try:
81
+ if not stream.isatty():
82
+ return
83
+ except Exception:
22
84
  return
23
- sys.stdout.write(f"\033]0;{title}\007")
24
- sys.stdout.flush()
85
+
86
+ stream.write(f"\033]0;{title}\007")
87
+ with contextlib.suppress(Exception):
88
+ stream.flush()
25
89
 
26
90
 
27
91
  def update_terminal_title(model_name: str | None = None) -> None:
@@ -147,19 +211,19 @@ def exec_command(
147
211
  raise typer.Exit(1)
148
212
 
149
213
  from klaude_code.cli.runtime import AppInitConfig, run_exec
214
+ from klaude_code.command.model_select import select_model_interactive
150
215
  from klaude_code.config import load_config
151
- from klaude_code.config.select_model import select_model_from_config
152
216
 
153
217
  chosen_model = model
154
218
  if model or select_model:
155
- chosen_model = select_model_from_config(preferred=model)
219
+ chosen_model = select_model_interactive(preferred=model)
156
220
  if chosen_model is None:
157
221
  raise typer.Exit(1)
158
222
  else:
159
223
  # Check if main_model is configured; if not, trigger interactive selection
160
224
  config = load_config()
161
225
  if config.main_model is None:
162
- chosen_model = select_model_from_config()
226
+ chosen_model = select_model_interactive()
163
227
  if chosen_model is None:
164
228
  raise typer.Exit(1)
165
229
  # Save the selection as default
@@ -282,13 +346,13 @@ def main_callback(
282
346
  return
283
347
 
284
348
  from klaude_code.cli.runtime import AppInitConfig, run_interactive
285
- from klaude_code.config.select_model import select_model_from_config
349
+ from klaude_code.command.model_select import select_model_interactive
286
350
 
287
351
  update_terminal_title()
288
352
 
289
353
  chosen_model = model
290
354
  if model or select_model:
291
- chosen_model = select_model_from_config(preferred=model)
355
+ chosen_model = select_model_interactive(preferred=model)
292
356
  if chosen_model is None:
293
357
  return
294
358
 
@@ -297,7 +361,7 @@ def main_callback(
297
361
  session_id: str | None = None
298
362
 
299
363
  if resume:
300
- session_id = resume_select_session()
364
+ session_id = select_session_interactive()
301
365
  if session_id is None:
302
366
  return
303
367
  # If user didn't pick, allow fallback to --continue
@@ -343,7 +407,7 @@ def main_callback(
343
407
 
344
408
  cfg = load_config()
345
409
  if cfg.main_model is None:
346
- chosen_model = select_model_from_config()
410
+ chosen_model = select_model_interactive()
347
411
  if chosen_model is None:
348
412
  raise typer.Exit(1)
349
413
  # Save the selection as default
@@ -3,6 +3,7 @@ import contextlib
3
3
  import sys
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, Protocol
6
+ from uuid import uuid4
6
7
 
7
8
  import typer
8
9
  from rich.text import Text
@@ -10,12 +11,13 @@ from rich.text import Text
10
11
  from klaude_code import ui
11
12
  from klaude_code.cli.main import update_terminal_title
12
13
  from klaude_code.cli.self_update import get_update_message
13
- from klaude_code.command import has_interactive_command
14
+ from klaude_code.command import dispatch_command, get_command_info_list, has_interactive_command, is_slash_command_name
14
15
  from klaude_code.config import Config, load_config
15
16
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
16
17
  from klaude_code.core.executor import Executor
17
18
  from klaude_code.core.manager import build_llm_clients
18
- from klaude_code.protocol import events, op
19
+ from klaude_code.protocol import events, llm_param, op
20
+ from klaude_code.protocol import model as protocol_model
19
21
  from klaude_code.protocol.model import UserInputPayload
20
22
  from klaude_code.session.session import Session, close_default_store
21
23
  from klaude_code.trace import DebugType, log, set_debug_logging
@@ -57,6 +59,79 @@ class AppComponents:
57
59
  theme: str | None
58
60
 
59
61
 
62
+ async def submit_user_input_payload(
63
+ *,
64
+ executor: Executor,
65
+ event_queue: asyncio.Queue[events.Event],
66
+ user_input: UserInputPayload,
67
+ session_id: str | None,
68
+ ) -> str | None:
69
+ """Parse/dispatch a user input payload and submit resulting operations.
70
+
71
+ The UI/CLI layer owns slash command parsing and any interactive prompts.
72
+ Core only executes concrete operations.
73
+
74
+ Returns a submission id that should be awaited, or None if there is nothing
75
+ to wait for (e.g. commands that only emit events).
76
+ """
77
+
78
+ sid = session_id or executor.context.current_session_id()
79
+ if sid is None:
80
+ raise RuntimeError("No active session")
81
+
82
+ agent = executor.context.current_agent
83
+ if agent is None or agent.session.id != sid:
84
+ await executor.submit_and_wait(op.InitAgentOperation(session_id=sid))
85
+ agent = executor.context.current_agent
86
+
87
+ if agent is None:
88
+ raise RuntimeError("Failed to initialize agent")
89
+
90
+ submission_id = uuid4().hex
91
+
92
+ await executor.context.emit_event(
93
+ events.UserMessageEvent(content=user_input.text, session_id=sid, images=user_input.images)
94
+ )
95
+
96
+ result = await dispatch_command(user_input, agent, submission_id=submission_id)
97
+ operations: list[op.Operation] = list(result.operations or [])
98
+
99
+ run_ops = [candidate for candidate in operations if isinstance(candidate, op.RunAgentOperation)]
100
+ if len(run_ops) > 1:
101
+ raise ValueError("Multiple RunAgentOperation results are not supported")
102
+
103
+ persisted_user_input = run_ops[0].input if run_ops else user_input
104
+
105
+ if result.persist_user_input:
106
+ agent.session.append_history(
107
+ [
108
+ protocol_model.UserMessageItem(
109
+ content=persisted_user_input.text,
110
+ images=persisted_user_input.images,
111
+ )
112
+ ]
113
+ )
114
+
115
+ if result.events:
116
+ for evt in result.events:
117
+ if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
118
+ agent.session.append_history([evt.item])
119
+ await executor.context.emit_event(evt)
120
+
121
+ submitted_ids: list[str] = []
122
+ for operation_item in operations:
123
+ submitted_ids.append(await executor.submit(operation_item))
124
+
125
+ if not submitted_ids:
126
+ # Ensure event-only commands are fully rendered before showing the next prompt.
127
+ await event_queue.join()
128
+ return None
129
+
130
+ if run_ops:
131
+ return run_ops[0].id
132
+ return submitted_ids[-1]
133
+
134
+
60
135
  async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
61
136
  """Initialize all application components (LLM clients, executor, UI)."""
62
137
  set_debug_logging(init_config.debug, filters=init_config.debug_filters)
@@ -232,10 +307,15 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
232
307
  is_new_session=True,
233
308
  )
234
309
 
235
- # Submit the input content directly
236
- await components.executor.submit_and_wait(
237
- op.UserInputOperation(input=UserInputPayload(text=input_content), session_id=session_id)
310
+ wait_id = await submit_user_input_payload(
311
+ executor=components.executor,
312
+ event_queue=components.event_queue,
313
+ user_input=UserInputPayload(text=input_content),
314
+ session_id=session_id,
238
315
  )
316
+ if wait_id is not None:
317
+ await components.executor.wait_for(wait_id)
318
+ await components.event_queue.join()
239
319
 
240
320
  except KeyboardInterrupt:
241
321
  await _handle_keyboard_interrupt(components.executor)
@@ -281,10 +361,66 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
281
361
  elif components.theme == "dark":
282
362
  is_light_background = False
283
363
 
364
+ def _get_active_session_id() -> str | None:
365
+ """Get the current active session ID dynamically.
366
+
367
+ This is necessary because /clear command creates a new session with a different ID.
368
+ """
369
+
370
+ return components.executor.context.current_session_id()
371
+
372
+ async def _change_model_from_prompt(model_name: str) -> None:
373
+ sid = _get_active_session_id()
374
+ if not sid:
375
+ return
376
+ await components.executor.submit_and_wait(
377
+ op.ChangeModelOperation(
378
+ session_id=sid,
379
+ model_name=model_name,
380
+ save_as_default=False,
381
+ defer_thinking_selection=True,
382
+ emit_welcome_event=False,
383
+ emit_switch_message=False,
384
+ )
385
+ )
386
+
387
+ def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
388
+ agent = components.executor.context.current_agent
389
+ if agent is None:
390
+ return None
391
+ return agent.profile.llm_client.get_llm_config()
392
+
393
+ async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
394
+ sid = _get_active_session_id()
395
+ if not sid:
396
+ return
397
+ await components.executor.submit_and_wait(
398
+ op.ChangeThinkingOperation(
399
+ session_id=sid,
400
+ thinking=thinking,
401
+ emit_welcome_event=False,
402
+ emit_switch_message=False,
403
+ )
404
+ )
405
+
406
+ # Inject command name checker into user_input renderer (for slash command highlighting)
407
+ from klaude_code.ui.renderers.user_input import set_command_name_checker
408
+
409
+ set_command_name_checker(is_slash_command_name)
410
+
284
411
  input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
285
412
  status_provider=_status_provider,
286
413
  pre_prompt=_stop_rich_bottom_ui,
287
414
  is_light_background=is_light_background,
415
+ get_current_model_config_name=lambda: (
416
+ components.executor.context.current_agent.session.model_config_name
417
+ if components.executor.context.current_agent is not None
418
+ else None
419
+ ),
420
+ on_change_model=_change_model_from_prompt,
421
+ get_current_llm_config=_get_current_llm_config,
422
+ on_change_thinking=_change_thinking_from_prompt,
423
+ command_info_provider=get_command_info_list,
288
424
  )
289
425
 
290
426
  # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
@@ -330,13 +466,6 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
330
466
  is_new_session=session_id is None,
331
467
  )
332
468
 
333
- def _get_active_session_id() -> str | None:
334
- """Get the current active session ID dynamically.
335
-
336
- This is necessary because /clear command creates a new session with a different ID.
337
- """
338
- return components.executor.context.current_session_id()
339
-
340
469
  # Input
341
470
  await input_provider.start()
342
471
  async for user_input in input_provider.iter_inputs():
@@ -345,30 +474,38 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
345
474
  break
346
475
  elif user_input.text.strip() == "":
347
476
  continue
348
- # Submit user input operation - directly use the payload from iter_inputs
349
- # Use dynamic session_id lookup to handle /clear creating new sessions
477
+ # Use dynamic session_id lookup to handle /clear creating new sessions.
478
+ # UI/CLI parses commands and submits concrete operations; core executes operations.
350
479
  active_session_id = _get_active_session_id()
351
- submission_id = await components.executor.submit(
352
- op.UserInputOperation(input=user_input, session_id=active_session_id)
480
+ is_interactive = has_interactive_command(user_input.text)
481
+
482
+ wait_id = await submit_user_input_payload(
483
+ executor=components.executor,
484
+ event_queue=components.event_queue,
485
+ user_input=user_input,
486
+ session_id=active_session_id,
353
487
  )
354
- # If it's an interactive command (e.g., /model), avoid starting the ESC monitor
355
- # to prevent TTY conflicts with interactive prompt_toolkit UIs.
356
- if has_interactive_command(user_input.text):
357
- await components.executor.wait_for(submission_id)
358
- else:
359
- # Esc monitor for long-running, interruptible operations
360
- async def _on_esc_interrupt() -> None:
361
- await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
362
-
363
- stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
364
- # Wait for this specific task to complete before accepting next input
365
- try:
366
- await components.executor.wait_for(submission_id)
367
- finally:
368
- # Stop ESC monitor and wait for it to finish cleaning up TTY
369
- stop_event.set()
370
- with contextlib.suppress(Exception):
371
- await esc_task
488
+
489
+ if wait_id is None:
490
+ continue
491
+
492
+ if is_interactive:
493
+ await components.executor.wait_for(wait_id)
494
+ continue
495
+
496
+ # Esc monitor for long-running, interruptible operations
497
+ async def _on_esc_interrupt() -> None:
498
+ await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
499
+
500
+ stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
501
+ # Wait for this specific task to complete before accepting next input
502
+ try:
503
+ await components.executor.wait_for(wait_id)
504
+ finally:
505
+ # Stop ESC monitor and wait for it to finish cleaning up TTY
506
+ stop_event.set()
507
+ with contextlib.suppress(Exception):
508
+ await esc_task
372
509
 
373
510
  except KeyboardInterrupt:
374
511
  await _handle_keyboard_interrupt(components.executor)
@@ -1,6 +1,8 @@
1
1
  from .command_abc import CommandABC, CommandResult
2
2
  from .registry import (
3
3
  dispatch_command,
4
+ get_command_info_list,
5
+ get_command_names,
4
6
  get_commands,
5
7
  has_interactive_command,
6
8
  is_slash_command_name,
@@ -94,6 +96,8 @@ __all__ = [
94
96
  "CommandResult",
95
97
  "dispatch_command",
96
98
  "ensure_commands_loaded",
99
+ "get_command_info_list",
100
+ "get_command_names",
97
101
  "get_commands",
98
102
  "has_interactive_command",
99
103
  "is_slash_command_name",
@@ -22,8 +22,9 @@ Usage:
22
22
  [b]esc[/b] to interrupt agent task
23
23
  [b]shift-enter[/b] or [b]ctrl-j[/b] for new line
24
24
  [b]ctrl-v[/b] for pasting image
25
+ [b]ctrl-l[/b] to switch model
26
+ [b]ctrl-t[/b] to switch thinking level
25
27
  [b]--continue[/b] or [b]--resume[/b] to continue an old session
26
- [b]--select-model[/b] to switch model
27
28
 
28
29
  Available slash commands:"""
29
30
  ]
@@ -3,7 +3,7 @@ import asyncio
3
3
  from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
- from klaude_code.config.select_model import select_model_from_config
6
+ from klaude_code.command.model_select import select_model_interactive
7
7
  from klaude_code.protocol import commands, events, model, op
8
8
  from klaude_code.ui.terminal.selector import SelectItem, select_one
9
9
 
@@ -13,7 +13,7 @@ SELECT_STYLE = Style(
13
13
  ("pointer", "ansigreen"),
14
14
  ("highlighted", "ansigreen"),
15
15
  ("text", "ansibrightblack"),
16
- ("question", ""),
16
+ ("question", "bold"),
17
17
  ]
18
18
  )
19
19
 
@@ -29,8 +29,6 @@ def _confirm_change_default_model_sync(selected_model: str) -> bool:
29
29
  ]
30
30
 
31
31
  try:
32
- # Add a blank line between the model selector and this confirmation prompt.
33
- print("")
34
32
  result = select_one(
35
33
  message=f"Save '{selected_model}' as default model?",
36
34
  items=items,
@@ -68,7 +66,7 @@ class ModelCommand(CommandABC):
68
66
  return "model name"
69
67
 
70
68
  async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
71
- selected_model = await asyncio.to_thread(select_model_from_config, preferred=user_input.text)
69
+ selected_model = await asyncio.to_thread(select_model_interactive, preferred=user_input.text)
72
70
 
73
71
  current_model = agent.profile.llm_client.model_name if agent.profile else None
74
72
  if selected_model is None or selected_model == current_model:
@@ -0,0 +1,84 @@
1
+ """Interactive model selection for CLI."""
2
+
3
+ import sys
4
+
5
+ from klaude_code.config.config import load_config
6
+ from klaude_code.config.select_model import match_model_from_config
7
+ from klaude_code.trace import log
8
+
9
+
10
+ def select_model_interactive(preferred: str | None = None) -> str | None:
11
+ """Interactive single-choice model selector.
12
+
13
+ This function combines matching logic with interactive UI selection.
14
+ For CLI usage.
15
+
16
+ If preferred is provided:
17
+ - Exact match: return immediately
18
+ - Single partial match (case-insensitive): return immediately
19
+ - Otherwise: fall through to interactive selection
20
+ """
21
+ result = match_model_from_config(preferred)
22
+
23
+ if result.error_message:
24
+ return None
25
+
26
+ if result.matched_model:
27
+ return result.matched_model
28
+
29
+ # Non-interactive environments (CI/pipes) should never enter an interactive prompt.
30
+ # If we couldn't resolve to a single model deterministically above, fail with a clear hint.
31
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
32
+ log(("Error: cannot use interactive model selection without a TTY", "red"))
33
+ log(("Hint: pass --model <config-name> or set main_model in ~/.klaude/klaude-config.yaml", "yellow"))
34
+ if preferred:
35
+ log((f"Hint: '{preferred}' did not resolve to a single configured model", "yellow"))
36
+ return None
37
+
38
+ # Interactive selection
39
+ from prompt_toolkit.styles import Style
40
+
41
+ from klaude_code.ui.terminal.selector import build_model_select_items, select_one
42
+
43
+ config = load_config()
44
+ names = [m.model_name for m in result.filtered_models]
45
+
46
+ try:
47
+ items = build_model_select_items(result.filtered_models)
48
+
49
+ message = f"Select a model (filtered by '{result.filter_hint}'):" if result.filter_hint else "Select a model:"
50
+ selected = select_one(
51
+ message=message,
52
+ items=items,
53
+ pointer="->",
54
+ use_search_filter=True,
55
+ initial_value=config.main_model,
56
+ style=Style(
57
+ [
58
+ ("pointer", "ansigreen"),
59
+ ("highlighted", "ansigreen"),
60
+ ("msg", ""),
61
+ ("meta", "fg:ansibrightblack"),
62
+ ("text", "ansibrightblack"),
63
+ ("question", "bold"),
64
+ ("search_prefix", "ansibrightblack"),
65
+ # search filter colors at the bottom
66
+ ("search_success", "noinherit fg:ansigreen"),
67
+ ("search_none", "noinherit fg:ansired"),
68
+ ]
69
+ ),
70
+ )
71
+ if isinstance(selected, str) and selected in names:
72
+ return selected
73
+ except KeyboardInterrupt:
74
+ return None
75
+ except Exception as e:
76
+ log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
77
+ # Never return an unvalidated model name here.
78
+ # If we can't interactively select, fall back to a known configured model.
79
+ if isinstance(preferred, str) and preferred in names:
80
+ return preferred
81
+ if config.main_model and config.main_model in names:
82
+ return config.main_model
83
+
84
+ return None
@@ -105,6 +105,29 @@ def get_commands() -> dict[commands.CommandName | str, "CommandABC"]:
105
105
  return _COMMANDS.copy()
106
106
 
107
107
 
108
+ def get_command_info_list() -> list[commands.CommandInfo]:
109
+ """Get lightweight command metadata for UI purposes.
110
+
111
+ Returns CommandInfo list in registration order (display order).
112
+ """
113
+ _ensure_commands_loaded()
114
+ return [
115
+ commands.CommandInfo(
116
+ name=_command_key_to_str(cmd.name),
117
+ summary=cmd.summary,
118
+ support_addition_params=cmd.support_addition_params,
119
+ placeholder=cmd.placeholder,
120
+ )
121
+ for cmd in _COMMANDS.values()
122
+ ]
123
+
124
+
125
+ def get_command_names() -> frozenset[str]:
126
+ """Get all registered command names as a frozen set for fast lookup."""
127
+ _ensure_commands_loaded()
128
+ return frozenset(_command_key_to_str(key) for key in _COMMANDS)
129
+
130
+
108
131
  def is_slash_command_name(name: str) -> bool:
109
132
  _ensure_commands_loaded()
110
133
  return _resolve_command_key(name) is not None
@@ -1,8 +1,58 @@
1
1
  import asyncio
2
2
 
3
+ from prompt_toolkit.styles import Style
4
+
3
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
4
6
  from klaude_code.protocol import commands, events, model, op
5
- from klaude_code.session.selector import resume_select_session
7
+ from klaude_code.session.selector import build_session_select_options
8
+ from klaude_code.trace import log
9
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
10
+
11
+ SESSION_SELECT_STYLE = Style(
12
+ [
13
+ ("msg", ""),
14
+ ("meta", "fg:ansibrightblack"),
15
+ ("pointer", "bold fg:ansigreen"),
16
+ ("highlighted", "fg:ansigreen"),
17
+ ("search_prefix", "fg:ansibrightblack"),
18
+ ("search_success", "noinherit fg:ansigreen"),
19
+ ("search_none", "noinherit fg:ansired"),
20
+ ("question", "bold"),
21
+ ("text", ""),
22
+ ]
23
+ )
24
+
25
+
26
+ def _select_session_sync() -> str | None:
27
+ """Interactive session selection (sync version for asyncio.to_thread)."""
28
+ options = build_session_select_options()
29
+ if not options:
30
+ log("No sessions found for this project.")
31
+ return None
32
+
33
+ items: list[SelectItem[str]] = []
34
+ for opt in options:
35
+ title = [
36
+ ("class:msg", f"{opt.first_user_message}\n"),
37
+ ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
38
+ ]
39
+ items.append(
40
+ SelectItem(
41
+ title=title,
42
+ value=opt.session_id,
43
+ search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
44
+ )
45
+ )
46
+
47
+ try:
48
+ return select_one(
49
+ message="Select a session to resume:",
50
+ items=items,
51
+ pointer="→",
52
+ style=SESSION_SELECT_STYLE,
53
+ )
54
+ except KeyboardInterrupt:
55
+ return None
6
56
 
7
57
 
8
58
  class ResumeCommand(CommandABC):
@@ -33,7 +83,7 @@ class ResumeCommand(CommandABC):
33
83
  )
34
84
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
35
85
 
36
- selected_session_id = await asyncio.to_thread(resume_select_session)
86
+ selected_session_id = await asyncio.to_thread(_select_session_sync)
37
87
  if selected_session_id is None:
38
88
  event = events.DeveloperMessageEvent(
39
89
  session_id=agent.session.id,