klaude-code 2.5.3__py3-none-any.whl → 2.7.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/app/runtime.py +1 -1
- klaude_code/auth/__init__.py +10 -0
- klaude_code/auth/env.py +81 -0
- klaude_code/cli/auth_cmd.py +87 -8
- klaude_code/cli/config_cmd.py +5 -5
- klaude_code/cli/cost_cmd.py +159 -60
- klaude_code/cli/main.py +146 -65
- klaude_code/cli/self_update.py +7 -7
- klaude_code/config/builtin_config.py +23 -9
- klaude_code/config/config.py +19 -9
- klaude_code/const.py +10 -1
- klaude_code/core/reminders.py +4 -5
- klaude_code/core/turn.py +8 -9
- klaude_code/llm/google/client.py +12 -0
- klaude_code/llm/openai_compatible/stream.py +5 -1
- klaude_code/llm/openrouter/client.py +1 -0
- klaude_code/protocol/commands.py +0 -1
- klaude_code/protocol/events.py +214 -0
- klaude_code/protocol/sub_agent/image_gen.py +0 -4
- klaude_code/session/session.py +51 -18
- klaude_code/skill/loader.py +12 -13
- klaude_code/skill/manager.py +3 -3
- klaude_code/tui/command/__init__.py +1 -4
- klaude_code/tui/command/copy_cmd.py +1 -1
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/commands.py +0 -5
- klaude_code/tui/components/command_output.py +1 -1
- klaude_code/tui/components/metadata.py +4 -5
- klaude_code/tui/components/rich/markdown.py +60 -0
- klaude_code/tui/components/rich/theme.py +8 -0
- klaude_code/tui/components/sub_agent.py +6 -0
- klaude_code/tui/components/user_input.py +38 -27
- klaude_code/tui/display.py +11 -1
- klaude_code/tui/input/AGENTS.md +44 -0
- klaude_code/tui/input/completers.py +21 -21
- klaude_code/tui/input/drag_drop.py +197 -0
- klaude_code/tui/input/images.py +227 -0
- klaude_code/tui/input/key_bindings.py +173 -19
- klaude_code/tui/input/paste.py +71 -0
- klaude_code/tui/input/prompt_toolkit.py +13 -3
- klaude_code/tui/machine.py +90 -56
- klaude_code/tui/renderer.py +1 -62
- klaude_code/tui/runner.py +1 -1
- klaude_code/tui/terminal/image.py +40 -9
- klaude_code/tui/terminal/selector.py +52 -2
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/METADATA +32 -40
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/RECORD +49 -54
- klaude_code/cli/session_cmd.py +0 -87
- klaude_code/protocol/events/__init__.py +0 -63
- klaude_code/protocol/events/base.py +0 -18
- klaude_code/protocol/events/chat.py +0 -30
- klaude_code/protocol/events/lifecycle.py +0 -23
- klaude_code/protocol/events/metadata.py +0 -16
- klaude_code/protocol/events/streaming.py +0 -43
- klaude_code/protocol/events/system.py +0 -56
- klaude_code/protocol/events/tools.py +0 -27
- klaude_code/tui/command/terminal_setup_cmd.py +0 -248
- klaude_code/tui/input/clipboard.py +0 -152
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/entry_points.txt +0 -0
klaude_code/tui/machine.py
CHANGED
|
@@ -19,6 +19,7 @@ from klaude_code.tui.commands import (
|
|
|
19
19
|
EmitTmuxSignal,
|
|
20
20
|
EndAssistantStream,
|
|
21
21
|
EndThinkingStream,
|
|
22
|
+
PrintBlankLine,
|
|
22
23
|
PrintRuleLine,
|
|
23
24
|
RenderAssistantImage,
|
|
24
25
|
RenderCommand,
|
|
@@ -26,7 +27,6 @@ from klaude_code.tui.commands import (
|
|
|
26
27
|
RenderDeveloperMessage,
|
|
27
28
|
RenderError,
|
|
28
29
|
RenderInterrupt,
|
|
29
|
-
RenderReplayHistory,
|
|
30
30
|
RenderTaskFinish,
|
|
31
31
|
RenderTaskMetadata,
|
|
32
32
|
RenderTaskStart,
|
|
@@ -160,7 +160,7 @@ class ActivityState:
|
|
|
160
160
|
if self._sub_agent_tool_calls:
|
|
161
161
|
_append_counts(self._sub_agent_tool_calls)
|
|
162
162
|
if self._tool_calls:
|
|
163
|
-
activity_text.append("
|
|
163
|
+
activity_text.append(", ")
|
|
164
164
|
|
|
165
165
|
if self._tool_calls:
|
|
166
166
|
_append_counts(self._tool_calls)
|
|
@@ -382,17 +382,25 @@ class DisplayStateMachine:
|
|
|
382
382
|
self._spinner.set_toast_status(None)
|
|
383
383
|
return self._spinner_update_commands()
|
|
384
384
|
|
|
385
|
+
def begin_replay(self) -> list[RenderCommand]:
|
|
386
|
+
self._spinner.reset()
|
|
387
|
+
return [SpinnerStop(), PrintBlankLine()]
|
|
388
|
+
|
|
389
|
+
def end_replay(self) -> list[RenderCommand]:
|
|
390
|
+
return [SpinnerStop()]
|
|
391
|
+
|
|
392
|
+
def transition_replay(self, event: events.Event) -> list[RenderCommand]:
|
|
393
|
+
return self._transition(event, is_replay=True)
|
|
394
|
+
|
|
385
395
|
def transition(self, event: events.Event) -> list[RenderCommand]:
|
|
396
|
+
return self._transition(event, is_replay=False)
|
|
397
|
+
|
|
398
|
+
def _transition(self, event: events.Event, *, is_replay: bool) -> list[RenderCommand]:
|
|
386
399
|
session_id = getattr(event, "session_id", "__app__")
|
|
387
400
|
s = self._session(session_id)
|
|
388
401
|
cmds: list[RenderCommand] = []
|
|
389
402
|
|
|
390
403
|
match event:
|
|
391
|
-
case events.ReplayHistoryEvent() as e:
|
|
392
|
-
cmds.append(RenderReplayHistory(e))
|
|
393
|
-
cmds.append(SpinnerStop())
|
|
394
|
-
return cmds
|
|
395
|
-
|
|
396
404
|
case events.WelcomeEvent() as e:
|
|
397
405
|
cmds.append(RenderWelcome(e))
|
|
398
406
|
return cmds
|
|
@@ -408,13 +416,16 @@ class DisplayStateMachine:
|
|
|
408
416
|
s.model_id = e.model_id
|
|
409
417
|
if not s.is_sub_agent:
|
|
410
418
|
self._set_primary_if_needed(e.session_id)
|
|
411
|
-
|
|
419
|
+
if not is_replay:
|
|
420
|
+
cmds.append(TaskClockStart())
|
|
412
421
|
else:
|
|
413
422
|
s.sub_agent_thinking_header = SubAgentThinkingHeaderState()
|
|
414
423
|
|
|
415
|
-
|
|
424
|
+
if not is_replay:
|
|
425
|
+
cmds.append(SpinnerStart())
|
|
416
426
|
cmds.append(RenderTaskStart(e))
|
|
417
|
-
|
|
427
|
+
if not is_replay:
|
|
428
|
+
cmds.extend(self._spinner_update_commands())
|
|
418
429
|
return cmds
|
|
419
430
|
|
|
420
431
|
case events.DeveloperMessageEvent() as e:
|
|
@@ -427,9 +438,10 @@ class DisplayStateMachine:
|
|
|
427
438
|
|
|
428
439
|
case events.TurnStartEvent() as e:
|
|
429
440
|
cmds.append(RenderTurnStart(e))
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
441
|
+
if not is_replay:
|
|
442
|
+
self._spinner.clear_for_new_turn()
|
|
443
|
+
self._spinner.set_reasoning_status(None)
|
|
444
|
+
cmds.extend(self._spinner_update_commands())
|
|
433
445
|
return cmds
|
|
434
446
|
|
|
435
447
|
case events.ThinkingStartEvent() as e:
|
|
@@ -441,9 +453,11 @@ class DisplayStateMachine:
|
|
|
441
453
|
s.thinking_tail = ""
|
|
442
454
|
# Ensure the status reflects that reasoning has started even
|
|
443
455
|
# before we receive any deltas (or a bold header).
|
|
444
|
-
|
|
456
|
+
if not is_replay:
|
|
457
|
+
self._spinner.set_reasoning_status(STATUS_THINKING_TEXT)
|
|
445
458
|
cmds.append(StartThinkingStream(session_id=e.session_id))
|
|
446
|
-
|
|
459
|
+
if not is_replay:
|
|
460
|
+
cmds.extend(self._spinner_update_commands())
|
|
447
461
|
return cmds
|
|
448
462
|
|
|
449
463
|
case events.ThinkingDeltaEvent() as e:
|
|
@@ -463,7 +477,7 @@ class DisplayStateMachine:
|
|
|
463
477
|
|
|
464
478
|
# Update reasoning status for spinner (based on bounded tail).
|
|
465
479
|
# Only extract headers for models that use markdown bold headers in thinking.
|
|
466
|
-
if s.should_extract_reasoning_header:
|
|
480
|
+
if not is_replay and s.should_extract_reasoning_header:
|
|
467
481
|
s.thinking_tail = (s.thinking_tail + e.content)[-8192:]
|
|
468
482
|
header = extract_last_bold_header(normalize_thinking_content(s.thinking_tail))
|
|
469
483
|
if header:
|
|
@@ -478,26 +492,31 @@ class DisplayStateMachine:
|
|
|
478
492
|
if not self._is_primary(e.session_id):
|
|
479
493
|
return []
|
|
480
494
|
s.thinking_stream_active = False
|
|
481
|
-
|
|
495
|
+
if not is_replay:
|
|
496
|
+
self._spinner.clear_default_reasoning_status()
|
|
482
497
|
cmds.append(EndThinkingStream(session_id=e.session_id))
|
|
483
|
-
|
|
484
|
-
|
|
498
|
+
if not is_replay:
|
|
499
|
+
cmds.append(SpinnerStart())
|
|
500
|
+
cmds.extend(self._spinner_update_commands())
|
|
485
501
|
return cmds
|
|
486
502
|
|
|
487
503
|
case events.AssistantTextStartEvent() as e:
|
|
488
504
|
if s.is_sub_agent:
|
|
489
|
-
|
|
490
|
-
|
|
505
|
+
if not is_replay:
|
|
506
|
+
self._spinner.set_composing(True)
|
|
507
|
+
cmds.extend(self._spinner_update_commands())
|
|
491
508
|
return cmds
|
|
492
509
|
if not self._is_primary(e.session_id):
|
|
493
510
|
return []
|
|
494
511
|
|
|
495
512
|
s.assistant_stream_active = True
|
|
496
513
|
s.assistant_char_count = 0
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
if not is_replay:
|
|
515
|
+
self._spinner.set_composing(True)
|
|
516
|
+
self._spinner.clear_tool_calls()
|
|
499
517
|
cmds.append(StartAssistantStream(session_id=e.session_id))
|
|
500
|
-
|
|
518
|
+
if not is_replay:
|
|
519
|
+
cmds.extend(self._spinner_update_commands())
|
|
501
520
|
return cmds
|
|
502
521
|
|
|
503
522
|
case events.AssistantTextDeltaEvent() as e:
|
|
@@ -507,24 +526,29 @@ class DisplayStateMachine:
|
|
|
507
526
|
return []
|
|
508
527
|
|
|
509
528
|
s.assistant_char_count += len(e.content)
|
|
510
|
-
|
|
529
|
+
if not is_replay:
|
|
530
|
+
self._spinner.set_buffer_length(s.assistant_char_count)
|
|
511
531
|
cmds.append(AppendAssistant(session_id=e.session_id, content=e.content))
|
|
512
|
-
|
|
532
|
+
if not is_replay:
|
|
533
|
+
cmds.extend(self._spinner_update_commands())
|
|
513
534
|
return cmds
|
|
514
535
|
|
|
515
536
|
case events.AssistantTextEndEvent() as e:
|
|
516
537
|
if s.is_sub_agent:
|
|
517
|
-
|
|
518
|
-
|
|
538
|
+
if not is_replay:
|
|
539
|
+
self._spinner.set_composing(False)
|
|
540
|
+
cmds.extend(self._spinner_update_commands())
|
|
519
541
|
return cmds
|
|
520
542
|
if not self._is_primary(e.session_id):
|
|
521
543
|
return []
|
|
522
544
|
|
|
523
545
|
s.assistant_stream_active = False
|
|
524
|
-
|
|
546
|
+
if not is_replay:
|
|
547
|
+
self._spinner.set_composing(False)
|
|
525
548
|
cmds.append(EndAssistantStream(session_id=e.session_id))
|
|
526
|
-
|
|
527
|
-
|
|
549
|
+
if not is_replay:
|
|
550
|
+
cmds.append(SpinnerStart())
|
|
551
|
+
cmds.extend(self._spinner_update_commands())
|
|
528
552
|
return cmds
|
|
529
553
|
|
|
530
554
|
case events.AssistantImageDeltaEvent() as e:
|
|
@@ -536,9 +560,10 @@ class DisplayStateMachine:
|
|
|
536
560
|
return []
|
|
537
561
|
if not self._is_primary(e.session_id):
|
|
538
562
|
return []
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
563
|
+
if not is_replay:
|
|
564
|
+
self._spinner.set_composing(False)
|
|
565
|
+
cmds.append(SpinnerStart())
|
|
566
|
+
cmds.extend(self._spinner_update_commands())
|
|
542
567
|
return cmds
|
|
543
568
|
|
|
544
569
|
case events.ToolCallStartEvent() as e:
|
|
@@ -553,18 +578,20 @@ class DisplayStateMachine:
|
|
|
553
578
|
primary.thinking_stream_active = False
|
|
554
579
|
cmds.append(EndThinkingStream(session_id=primary.session_id))
|
|
555
580
|
|
|
556
|
-
|
|
581
|
+
if not is_replay:
|
|
582
|
+
self._spinner.set_composing(False)
|
|
557
583
|
|
|
558
584
|
# Skip activity state for fast tools on non-streaming models (e.g., Gemini)
|
|
559
585
|
# to avoid flash-and-disappear effect
|
|
560
|
-
if not s.should_skip_tool_activity(e.tool_name):
|
|
586
|
+
if not is_replay and not s.should_skip_tool_activity(e.tool_name):
|
|
561
587
|
tool_active_form = get_tool_active_form(e.tool_name)
|
|
562
588
|
if is_sub_agent_tool(e.tool_name):
|
|
563
589
|
self._spinner.add_sub_agent_tool_call(e.tool_call_id, tool_active_form)
|
|
564
590
|
else:
|
|
565
591
|
self._spinner.add_tool_call(tool_active_form)
|
|
566
592
|
|
|
567
|
-
|
|
593
|
+
if not is_replay:
|
|
594
|
+
cmds.extend(self._spinner_update_commands())
|
|
568
595
|
return cmds
|
|
569
596
|
|
|
570
597
|
case events.ToolCallEvent() as e:
|
|
@@ -583,7 +610,7 @@ class DisplayStateMachine:
|
|
|
583
610
|
return cmds
|
|
584
611
|
|
|
585
612
|
case events.ToolResultEvent() as e:
|
|
586
|
-
if is_sub_agent_tool(e.tool_name):
|
|
613
|
+
if not is_replay and is_sub_agent_tool(e.tool_name):
|
|
587
614
|
self._spinner.finish_sub_agent_tool_call(e.tool_call_id, get_tool_active_form(e.tool_name))
|
|
588
615
|
cmds.extend(self._spinner_update_commands())
|
|
589
616
|
|
|
@@ -601,9 +628,10 @@ class DisplayStateMachine:
|
|
|
601
628
|
|
|
602
629
|
case events.TodoChangeEvent() as e:
|
|
603
630
|
todo_text = _extract_active_form_text(e)
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
631
|
+
if not is_replay:
|
|
632
|
+
self._spinner.set_todo_status(todo_text)
|
|
633
|
+
self._spinner.clear_for_new_turn()
|
|
634
|
+
cmds.extend(self._spinner_update_commands())
|
|
607
635
|
return cmds
|
|
608
636
|
|
|
609
637
|
case events.UsageEvent() as e:
|
|
@@ -613,7 +641,7 @@ class DisplayStateMachine:
|
|
|
613
641
|
if not self._is_primary(e.session_id):
|
|
614
642
|
return []
|
|
615
643
|
context_percent = e.usage.context_usage_percent
|
|
616
|
-
if context_percent is not None:
|
|
644
|
+
if not is_replay and context_percent is not None:
|
|
617
645
|
self._spinner.set_context_percent(context_percent)
|
|
618
646
|
cmds.extend(self._spinner_update_commands())
|
|
619
647
|
return cmds
|
|
@@ -624,37 +652,43 @@ class DisplayStateMachine:
|
|
|
624
652
|
case events.TaskFinishEvent() as e:
|
|
625
653
|
cmds.append(RenderTaskFinish(e))
|
|
626
654
|
if not s.is_sub_agent:
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
655
|
+
if not is_replay:
|
|
656
|
+
cmds.append(TaskClockClear())
|
|
657
|
+
self._spinner.reset()
|
|
658
|
+
cmds.append(SpinnerStop())
|
|
659
|
+
cmds.append(PrintRuleLine())
|
|
660
|
+
cmds.append(EmitTmuxSignal())
|
|
632
661
|
else:
|
|
633
662
|
s.sub_agent_thinking_header = None
|
|
634
663
|
return cmds
|
|
635
664
|
|
|
636
665
|
case events.InterruptEvent() as e:
|
|
637
|
-
|
|
638
|
-
|
|
666
|
+
if not is_replay:
|
|
667
|
+
self._spinner.reset()
|
|
668
|
+
cmds.append(SpinnerStop())
|
|
639
669
|
cmds.append(EndThinkingStream(session_id=e.session_id))
|
|
640
670
|
cmds.append(EndAssistantStream(session_id=e.session_id))
|
|
641
|
-
|
|
671
|
+
if not is_replay:
|
|
672
|
+
cmds.append(TaskClockClear())
|
|
642
673
|
cmds.append(RenderInterrupt(session_id=e.session_id))
|
|
643
674
|
return cmds
|
|
644
675
|
|
|
645
676
|
case events.ErrorEvent() as e:
|
|
646
|
-
|
|
677
|
+
if not is_replay:
|
|
678
|
+
cmds.append(EmitOsc94Error())
|
|
647
679
|
cmds.append(RenderError(e))
|
|
648
|
-
if not e.can_retry:
|
|
680
|
+
if not is_replay and not e.can_retry:
|
|
649
681
|
self._spinner.reset()
|
|
650
682
|
cmds.append(SpinnerStop())
|
|
651
|
-
|
|
683
|
+
if not is_replay:
|
|
684
|
+
cmds.extend(self._spinner_update_commands())
|
|
652
685
|
return cmds
|
|
653
686
|
|
|
654
687
|
case events.EndEvent():
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
688
|
+
if not is_replay:
|
|
689
|
+
self._spinner.reset()
|
|
690
|
+
cmds.append(SpinnerStop())
|
|
691
|
+
cmds.append(TaskClockClear())
|
|
658
692
|
return cmds
|
|
659
693
|
|
|
660
694
|
case _:
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -35,7 +35,6 @@ from klaude_code.tui.commands import (
|
|
|
35
35
|
RenderDeveloperMessage,
|
|
36
36
|
RenderError,
|
|
37
37
|
RenderInterrupt,
|
|
38
|
-
RenderReplayHistory,
|
|
39
38
|
RenderTaskFinish,
|
|
40
39
|
RenderTaskMetadata,
|
|
41
40
|
RenderTaskStart,
|
|
@@ -437,67 +436,10 @@ class TUICommandRenderer:
|
|
|
437
436
|
Text.assemble(
|
|
438
437
|
(c_thinking.THINKING_MESSAGE_MARK, ThemeKey.THINKING),
|
|
439
438
|
" ",
|
|
440
|
-
(stripped, ThemeKey.
|
|
439
|
+
(stripped, ThemeKey.THINKING),
|
|
441
440
|
)
|
|
442
441
|
)
|
|
443
442
|
|
|
444
|
-
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
445
|
-
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
446
|
-
self.print()
|
|
447
|
-
for event in history_events.events:
|
|
448
|
-
event_session_id = getattr(event, "session_id", history_events.session_id)
|
|
449
|
-
is_sub_agent = self.is_sub_agent_session(event_session_id)
|
|
450
|
-
|
|
451
|
-
with self.session_print_context(event_session_id):
|
|
452
|
-
match event:
|
|
453
|
-
case events.TaskStartEvent() as e:
|
|
454
|
-
self.display_task_start(e)
|
|
455
|
-
case events.TurnStartEvent():
|
|
456
|
-
self.print()
|
|
457
|
-
case events.AssistantImageDeltaEvent() as e:
|
|
458
|
-
self.display_image(e.file_path)
|
|
459
|
-
case events.ResponseCompleteEvent() as e:
|
|
460
|
-
if is_sub_agent:
|
|
461
|
-
if self._should_display_sub_agent_thinking_header(event_session_id) and e.thinking_text:
|
|
462
|
-
header = c_thinking.extract_last_bold_header(
|
|
463
|
-
c_thinking.normalize_thinking_content(e.thinking_text)
|
|
464
|
-
)
|
|
465
|
-
if header:
|
|
466
|
-
self.display_thinking_header(header)
|
|
467
|
-
continue
|
|
468
|
-
if e.thinking_text:
|
|
469
|
-
self.display_thinking(e.thinking_text)
|
|
470
|
-
renderable = c_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
|
|
471
|
-
if renderable is not None:
|
|
472
|
-
self.print(renderable)
|
|
473
|
-
self.print()
|
|
474
|
-
case events.DeveloperMessageEvent() as e:
|
|
475
|
-
self.display_developer_message(e)
|
|
476
|
-
case events.UserMessageEvent() as e:
|
|
477
|
-
if is_sub_agent:
|
|
478
|
-
continue
|
|
479
|
-
self.print(c_user_input.render_user_input(e.content))
|
|
480
|
-
case events.ToolCallEvent() as e:
|
|
481
|
-
tool_call_dict[e.tool_call_id] = e
|
|
482
|
-
case events.ToolResultEvent() as e:
|
|
483
|
-
tool_call_event = tool_call_dict.get(e.tool_call_id)
|
|
484
|
-
if tool_call_event is not None:
|
|
485
|
-
self.display_tool_call(tool_call_event)
|
|
486
|
-
tool_call_dict.pop(e.tool_call_id, None)
|
|
487
|
-
if is_sub_agent:
|
|
488
|
-
continue
|
|
489
|
-
self.display_tool_call_result(e)
|
|
490
|
-
case events.TaskMetadataEvent() as e:
|
|
491
|
-
self.print(c_metadata.render_task_metadata(e))
|
|
492
|
-
self.print()
|
|
493
|
-
case events.InterruptEvent():
|
|
494
|
-
self.print()
|
|
495
|
-
self.print(c_user_input.render_interrupt())
|
|
496
|
-
case events.ErrorEvent() as e:
|
|
497
|
-
self.display_error(e)
|
|
498
|
-
case events.TaskFinishEvent() as e:
|
|
499
|
-
self.display_task_finish(e)
|
|
500
|
-
|
|
501
443
|
def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
|
|
502
444
|
if not c_developer.need_render_developer_message(e):
|
|
503
445
|
return
|
|
@@ -615,9 +557,6 @@ class TUICommandRenderer:
|
|
|
615
557
|
async def execute(self, commands: list[RenderCommand]) -> None:
|
|
616
558
|
for cmd in commands:
|
|
617
559
|
match cmd:
|
|
618
|
-
case RenderReplayHistory(event=event):
|
|
619
|
-
await self.replay_history(event)
|
|
620
|
-
self.spinner_stop()
|
|
621
560
|
case RenderWelcome(event=event):
|
|
622
561
|
self.display_welcome(event)
|
|
623
562
|
case RenderUserMessage(event=event):
|
klaude_code/tui/runner.py
CHANGED
|
@@ -312,4 +312,4 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
312
312
|
active_session_id = components.executor.context.current_session_id()
|
|
313
313
|
if active_session_id and Session.exists(active_session_id):
|
|
314
314
|
log(f"Session ID: {active_session_id}")
|
|
315
|
-
log(f"Resume with: klaude --resume
|
|
315
|
+
log(f"Resume with: klaude --resume {active_session_id}")
|
|
@@ -1,34 +1,65 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import IO
|
|
6
7
|
|
|
8
|
+
# Kitty graphics protocol chunk size (4096 is the recommended max)
|
|
9
|
+
_CHUNK_SIZE = 4096
|
|
10
|
+
|
|
7
11
|
|
|
8
12
|
def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
|
|
9
13
|
"""Print an image to the terminal using Kitty graphics protocol.
|
|
10
14
|
|
|
11
15
|
This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
|
|
12
16
|
with raw escape sequences.
|
|
13
|
-
"""
|
|
14
17
|
|
|
18
|
+
Args:
|
|
19
|
+
file_path: Path to the image file (PNG recommended).
|
|
20
|
+
height: Display height in terminal rows. If None, uses terminal default.
|
|
21
|
+
file: Output file stream. Defaults to stdout.
|
|
22
|
+
"""
|
|
15
23
|
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
16
24
|
if not path.exists():
|
|
17
25
|
print(f"Image not found: {path}", file=file or sys.stdout, flush=True)
|
|
18
26
|
return
|
|
19
27
|
|
|
20
28
|
try:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
KittyImage.forced_support = True # type: ignore[reportUnknownMemberType]
|
|
24
|
-
img = KittyImage.from_file(path) # type: ignore[reportUnknownMemberType]
|
|
25
|
-
if height is not None:
|
|
26
|
-
img.height = height # type: ignore[reportUnknownMemberType]
|
|
27
|
-
|
|
29
|
+
data = path.read_bytes()
|
|
30
|
+
encoded = base64.standard_b64encode(data).decode("ascii")
|
|
28
31
|
out = file or sys.stdout
|
|
32
|
+
|
|
29
33
|
print("", file=out)
|
|
30
|
-
|
|
34
|
+
_write_kitty_graphics(out, encoded, height=height)
|
|
31
35
|
print("", file=out)
|
|
32
36
|
out.flush()
|
|
33
37
|
except Exception:
|
|
34
38
|
print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write_kitty_graphics(out: IO[str], encoded_data: str, *, height: int | None = None) -> None:
|
|
42
|
+
"""Write Kitty graphics protocol escape sequences.
|
|
43
|
+
|
|
44
|
+
Protocol format: ESC _ G <control>;<payload> ESC \\
|
|
45
|
+
- a=T: direct transmission (data in payload)
|
|
46
|
+
- f=100: PNG format (auto-detected by Kitty)
|
|
47
|
+
- r=N: display height in rows
|
|
48
|
+
- m=1: more data follows, m=0: last chunk
|
|
49
|
+
"""
|
|
50
|
+
total_len = len(encoded_data)
|
|
51
|
+
|
|
52
|
+
for i in range(0, total_len, _CHUNK_SIZE):
|
|
53
|
+
chunk = encoded_data[i : i + _CHUNK_SIZE]
|
|
54
|
+
is_last = i + _CHUNK_SIZE >= total_len
|
|
55
|
+
|
|
56
|
+
if i == 0:
|
|
57
|
+
# First chunk: include control parameters
|
|
58
|
+
ctrl = "a=T,f=100"
|
|
59
|
+
if height is not None:
|
|
60
|
+
ctrl += f",r={height}"
|
|
61
|
+
ctrl += f",m={0 if is_last else 1}"
|
|
62
|
+
out.write(f"\033_G{ctrl};{chunk}\033\\")
|
|
63
|
+
else:
|
|
64
|
+
# Subsequent chunks: only m parameter needed
|
|
65
|
+
out.write(f"\033_Gm={0 if is_last else 1};{chunk}\033\\")
|
|
@@ -389,7 +389,7 @@ def _build_search_container(
|
|
|
389
389
|
frame: bool = True,
|
|
390
390
|
) -> tuple[Window, Container]:
|
|
391
391
|
"""Build the search input container with placeholder."""
|
|
392
|
-
placeholder_text = f"{search_placeholder} · ↑↓ to select · esc to quit"
|
|
392
|
+
placeholder_text = f"{search_placeholder} · ↑↓ to select · enter/tab to confirm · esc to quit"
|
|
393
393
|
|
|
394
394
|
search_prefix_window = Window(
|
|
395
395
|
FormattedTextControl([("class:search_prefix", "/ ")]),
|
|
@@ -522,6 +522,23 @@ def select_one[T](
|
|
|
522
522
|
return
|
|
523
523
|
event.app.exit(result=value)
|
|
524
524
|
|
|
525
|
+
@kb.add(Keys.Tab, eager=True)
|
|
526
|
+
def _(event: KeyPressEvent) -> None:
|
|
527
|
+
"""Accept the currently pointed item."""
|
|
528
|
+
indices, _ = _filter_items(items, get_filter_text())
|
|
529
|
+
if not indices:
|
|
530
|
+
event.app.exit(result=None)
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
nonlocal pointed_at
|
|
534
|
+
pointed_at = _coerce_pointed_at_to_selectable(items, indices, pointed_at)
|
|
535
|
+
idx = indices[pointed_at % len(indices)]
|
|
536
|
+
value = items[idx].value
|
|
537
|
+
if value is None:
|
|
538
|
+
event.app.exit(result=None)
|
|
539
|
+
return
|
|
540
|
+
event.app.exit(result=value)
|
|
541
|
+
|
|
525
542
|
@kb.add(Keys.Escape, eager=True)
|
|
526
543
|
def _(event: KeyPressEvent) -> None:
|
|
527
544
|
nonlocal pointed_at
|
|
@@ -702,6 +719,28 @@ class SelectOverlay[T]:
|
|
|
702
719
|
if hasattr(result, "__await__"):
|
|
703
720
|
event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
|
|
704
721
|
|
|
722
|
+
@kb.add(Keys.Tab, filter=is_open_filter, eager=True)
|
|
723
|
+
def _(event: KeyPressEvent) -> None:
|
|
724
|
+
indices, _ = self._get_visible_indices()
|
|
725
|
+
if not indices:
|
|
726
|
+
self.close()
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
self._pointed_at = _coerce_pointed_at_to_selectable(self._items, indices, self._pointed_at)
|
|
730
|
+
idx = indices[self._pointed_at % len(indices)]
|
|
731
|
+
value = self._items[idx].value
|
|
732
|
+
if value is None:
|
|
733
|
+
self.close()
|
|
734
|
+
return
|
|
735
|
+
self.close()
|
|
736
|
+
|
|
737
|
+
if self._on_select is None:
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
result = self._on_select(value)
|
|
741
|
+
if hasattr(result, "__await__"):
|
|
742
|
+
event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
|
|
743
|
+
|
|
705
744
|
@kb.add(Keys.Escape, filter=is_open_filter, eager=True)
|
|
706
745
|
def _(event: KeyPressEvent) -> None:
|
|
707
746
|
if self._use_search_filter and self._search_buffer is not None and self._search_buffer.text:
|
|
@@ -757,9 +796,20 @@ class SelectOverlay[T]:
|
|
|
757
796
|
dont_extend_height=Always(),
|
|
758
797
|
always_hide_cursor=Always(),
|
|
759
798
|
)
|
|
799
|
+
def get_list_height() -> int:
|
|
800
|
+
# Dynamic height: min of configured height and available terminal space
|
|
801
|
+
# Overhead: header(1) + spacer(1) + search(1) + frame borders(2) + prompt area(3)
|
|
802
|
+
overhead = 8
|
|
803
|
+
try:
|
|
804
|
+
terminal_height = get_app().output.get_size().rows
|
|
805
|
+
available = max(3, terminal_height - overhead)
|
|
806
|
+
return min(self._list_height, available)
|
|
807
|
+
except Exception:
|
|
808
|
+
return self._list_height
|
|
809
|
+
|
|
760
810
|
list_window = Window(
|
|
761
811
|
FormattedTextControl(get_choices_tokens),
|
|
762
|
-
height=
|
|
812
|
+
height=get_list_height,
|
|
763
813
|
scroll_offsets=ScrollOffsets(top=0, bottom=2),
|
|
764
814
|
allow_scroll_beyond_bottom=True,
|
|
765
815
|
dont_extend_height=Always(),
|