klaude-code 1.4.3__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.
@@ -2,8 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import sys
5
+ from collections.abc import Callable, Coroutine
5
6
  from dataclasses import dataclass
6
7
  from functools import partial
8
+ from typing import Any, cast
7
9
 
8
10
  from prompt_toolkit.application import Application
9
11
  from prompt_toolkit.application.current import get_app
@@ -27,6 +29,194 @@ class SelectItem[T]:
27
29
  search_text: str
28
30
 
29
31
 
32
+ # ---------------------------------------------------------------------------
33
+ # Model selection items builder
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
38
+ """Build SelectItem list from ModelEntry objects.
39
+
40
+ Args:
41
+ models: List of ModelEntry objects (from config.iter_model_entries).
42
+
43
+ Returns:
44
+ List of SelectItem[str] with model_name as the value.
45
+ """
46
+ if not models:
47
+ return []
48
+
49
+ max_model_name_length = max(len(m.model_name) for m in models)
50
+ num_width = len(str(len(models)))
51
+
52
+ def _thinking_info(m: Any) -> str:
53
+ thinking = m.model_params.thinking
54
+ if not thinking:
55
+ return ""
56
+ if thinking.reasoning_effort:
57
+ return f"reasoning {thinking.reasoning_effort}"
58
+ if thinking.budget_tokens:
59
+ return f"thinking budget {thinking.budget_tokens}"
60
+ return "thinking (configured)"
61
+
62
+ items: list[SelectItem[str]] = []
63
+ for idx, m in enumerate(models, 1):
64
+ model_id = m.model_params.model or "N/A"
65
+ first_line_prefix = f"{m.model_name:<{max_model_name_length}} → "
66
+ thinking_info = _thinking_info(m)
67
+ meta_parts: list[str] = [m.provider]
68
+ if thinking_info:
69
+ meta_parts.append(thinking_info)
70
+ if m.model_params.verbosity:
71
+ meta_parts.append(f"verbosity {m.model_params.verbosity}")
72
+ meta_str = " · ".join(meta_parts)
73
+ title = [
74
+ ("class:meta", f"{idx:>{num_width}}. "),
75
+ ("class:msg", first_line_prefix),
76
+ ("class:msg bold", model_id),
77
+ ("class:meta", f" {meta_str}\n"),
78
+ ]
79
+ search_text = f"{m.model_name} {model_id} {m.provider}"
80
+ items.append(SelectItem(title=title, value=m.model_name, search_text=search_text))
81
+
82
+ return items
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Shared helpers for select_one() and SelectOverlay
87
+ # ---------------------------------------------------------------------------
88
+
89
+
90
+ def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, str]]:
91
+ """Re-apply a style class while keeping text attributes like bold/italic."""
92
+ keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike"}
93
+ restyled: list[tuple[str, str]] = []
94
+ for old_style, text in title:
95
+ attrs = [tok for tok in old_style.split() if tok in keep_attrs]
96
+ style = f"{cls} {' '.join(attrs)}".strip()
97
+ restyled.append((style, text))
98
+ return restyled
99
+
100
+
101
+ def _filter_items[T](
102
+ items: list[SelectItem[T]],
103
+ filter_text: str,
104
+ ) -> tuple[list[int], bool]:
105
+ """Return visible item indices and whether any matched the filter."""
106
+ if not items:
107
+ return [], True
108
+ if not filter_text:
109
+ return list(range(len(items))), True
110
+
111
+ needle = filter_text.lower()
112
+ matched = [i for i, it in enumerate(items) if needle in it.search_text.lower()]
113
+ if matched:
114
+ return matched, True
115
+ return list(range(len(items))), False
116
+
117
+
118
+ def _build_choices_tokens[T](
119
+ items: list[SelectItem[T]],
120
+ visible_indices: list[int],
121
+ pointed_at: int,
122
+ pointer: str,
123
+ ) -> list[tuple[str, str]]:
124
+ """Build formatted tokens for the choice list."""
125
+ if not visible_indices:
126
+ return [("class:text", "(no items)\n")]
127
+
128
+ tokens: list[tuple[str, str]] = []
129
+ pointer_pad = " " * (2 + len(pointer))
130
+ pointed_prefix = f" {pointer} "
131
+
132
+ for pos, idx in enumerate(visible_indices):
133
+ is_pointed = pos == pointed_at
134
+ if is_pointed:
135
+ tokens.append(("class:pointer", pointed_prefix))
136
+ tokens.append(("[SetCursorPosition]", ""))
137
+ else:
138
+ tokens.append(("class:text", pointer_pad))
139
+
140
+ title_tokens = _restyle_title(items[idx].title, "class:highlighted") if is_pointed else items[idx].title
141
+ tokens.extend(title_tokens)
142
+
143
+ return tokens
144
+
145
+
146
+ def _build_rounded_frame(body: Container) -> HSplit:
147
+ """Build a rounded border frame around the given container."""
148
+ border = partial(Window, style="class:frame.border", height=1)
149
+ top = VSplit(
150
+ [
151
+ border(width=1, char="╭"),
152
+ border(char="─"),
153
+ border(width=1, char="╮"),
154
+ ],
155
+ height=1,
156
+ padding=0,
157
+ )
158
+ middle = VSplit(
159
+ [
160
+ border(width=1, char="│"),
161
+ body,
162
+ border(width=1, char="│"),
163
+ ],
164
+ padding=0,
165
+ )
166
+ bottom = VSplit(
167
+ [
168
+ border(width=1, char="╰"),
169
+ border(char="─"),
170
+ border(width=1, char="╯"),
171
+ ],
172
+ height=1,
173
+ padding=0,
174
+ )
175
+ return HSplit([top, middle, bottom], padding=0, style="class:frame")
176
+
177
+
178
+ def _build_search_container(
179
+ search_buffer: Buffer,
180
+ search_placeholder: str,
181
+ ) -> tuple[Window, Container]:
182
+ """Build the search input container with placeholder."""
183
+ placeholder_text = f"{search_placeholder} · ↑↓ to select · esc to quit"
184
+
185
+ search_prefix_window = Window(
186
+ FormattedTextControl([("class:search_prefix", "/ ")]),
187
+ width=2,
188
+ height=1,
189
+ dont_extend_height=Always(),
190
+ always_hide_cursor=Always(),
191
+ )
192
+ input_window = Window(
193
+ BufferControl(buffer=search_buffer),
194
+ height=1,
195
+ dont_extend_height=Always(),
196
+ style="class:search_input",
197
+ )
198
+ placeholder_window = ConditionalContainer(
199
+ content=Window(
200
+ FormattedTextControl([("class:search_placeholder", placeholder_text)]),
201
+ height=1,
202
+ dont_extend_height=Always(),
203
+ always_hide_cursor=Always(),
204
+ ),
205
+ filter=Condition(lambda: search_buffer.text == ""),
206
+ )
207
+ search_input_container = FloatContainer(
208
+ content=input_window,
209
+ floats=[Float(content=placeholder_window, top=0, left=0)],
210
+ )
211
+ framed = _build_rounded_frame(VSplit([search_prefix_window, search_input_container], padding=0))
212
+ return input_window, framed
213
+
214
+
215
+ # ---------------------------------------------------------------------------
216
+ # select_one: standalone single-choice selector
217
+ # ---------------------------------------------------------------------------
218
+
219
+
30
220
  def select_one[T](
31
221
  *,
32
222
  message: str,
@@ -37,14 +227,7 @@ def select_one[T](
37
227
  initial_value: T | None = None,
38
228
  search_placeholder: str = "type to search",
39
229
  ) -> 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
-
230
+ """Terminal single-choice selector based on prompt_toolkit."""
48
231
  if not items:
49
232
  return None
50
233
 
@@ -54,61 +237,22 @@ def select_one[T](
54
237
 
55
238
  pointed_at = 0
56
239
 
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
240
+ search_buffer: Buffer | None = Buffer() if use_search_filter else None
241
+
242
+ def get_filter_text() -> str:
243
+ return search_buffer.text if (use_search_filter and search_buffer is not None) else ""
81
244
 
82
245
  def get_header_tokens() -> list[tuple[str, str]]:
83
246
  return [("class:question", message + " ")]
84
247
 
85
248
  def get_choices_tokens() -> list[tuple[str, str]]:
86
249
  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))
250
+ indices, _ = _filter_items(items, get_filter_text())
251
+ if indices:
252
+ pointed_at %= len(indices)
253
+ return _build_choices_tokens(items, indices, pointed_at, pointer)
105
254
 
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:
255
+ def on_search_changed(_buf: Buffer) -> None:
112
256
  nonlocal pointed_at
113
257
  pointed_at = 0
114
258
  with contextlib.suppress(Exception):
@@ -118,40 +262,32 @@ def select_one[T](
118
262
 
119
263
  @kb.add(Keys.ControlC, eager=True)
120
264
  @kb.add(Keys.ControlQ, eager=True)
121
- def _cancel(event: KeyPressEvent) -> None:
265
+ def _(event: KeyPressEvent) -> None:
122
266
  event.app.exit(result=None)
123
267
 
124
- _ = _cancel # registered via decorator
125
-
126
268
  @kb.add(Keys.Down, eager=True)
127
- def _down(event: KeyPressEvent) -> None:
269
+ def _(event: KeyPressEvent) -> None:
128
270
  nonlocal pointed_at
129
271
  pointed_at += 1
130
272
  event.app.invalidate()
131
273
 
132
- _ = _down # registered via decorator
133
-
134
274
  @kb.add(Keys.Up, eager=True)
135
- def _up(event: KeyPressEvent) -> None:
275
+ def _(event: KeyPressEvent) -> None:
136
276
  nonlocal pointed_at
137
277
  pointed_at -= 1
138
278
  event.app.invalidate()
139
279
 
140
- _ = _up # registered via decorator
141
-
142
280
  @kb.add(Keys.Enter, eager=True)
143
- def _enter(event: KeyPressEvent) -> None:
144
- indices, _ = visible_indices()
281
+ def _(event: KeyPressEvent) -> None:
282
+ indices, _ = _filter_items(items, get_filter_text())
145
283
  if not indices:
146
284
  event.app.exit(result=None)
147
285
  return
148
286
  idx = indices[pointed_at % len(indices)]
149
287
  event.app.exit(result=items[idx].value)
150
288
 
151
- _ = _enter # registered via decorator
152
-
153
289
  @kb.add(Keys.Escape, eager=True)
154
- def _esc(event: KeyPressEvent) -> None:
290
+ def _(event: KeyPressEvent) -> None:
155
291
  nonlocal pointed_at
156
292
  if use_search_filter and search_buffer is not None and search_buffer.text:
157
293
  search_buffer.reset(append_to_history=False)
@@ -160,15 +296,13 @@ def select_one[T](
160
296
  return
161
297
  event.app.exit(result=None)
162
298
 
163
- _ = _esc # registered via decorator
164
-
165
299
  if use_search_filter and search_buffer is not None:
166
- search_buffer.on_text_changed += _on_search_changed
300
+ search_buffer.on_text_changed += on_search_changed
167
301
 
168
302
  if initial_value is not None:
169
303
  try:
170
304
  full_index = next(i for i, it in enumerate(items) if it.value == initial_value)
171
- indices, _ = visible_indices()
305
+ indices, _ = _filter_items(items, get_filter_text()) # pyright: ignore[reportAssignmentType]
172
306
  pointed_at = indices.index(full_index) if full_index in indices else 0
173
307
  except StopIteration:
174
308
  pointed_at = 0
@@ -193,70 +327,10 @@ def select_one[T](
193
327
  always_hide_cursor=Always(),
194
328
  )
195
329
 
196
- search_container = None
330
+ search_container: Container | None = None
197
331
  search_input_window: Window | None = None
198
332
  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))
333
+ search_input_window, search_container = _build_search_container(search_buffer, search_placeholder)
260
334
 
261
335
  base_style = Style(
262
336
  [
@@ -281,3 +355,212 @@ def select_one[T](
281
355
  erase_when_done=True,
282
356
  )
283
357
  return app.run()
358
+
359
+
360
+ # ---------------------------------------------------------------------------
361
+ # SelectOverlay: embedded overlay for existing prompt_toolkit Application
362
+ # ---------------------------------------------------------------------------
363
+
364
+
365
+ class SelectOverlay[T]:
366
+ """Embedded single-choice selector overlay for an existing prompt_toolkit Application.
367
+
368
+ Unlike `select_one()`, this does not create or run a new Application.
369
+ It is designed for use inside an already-running PromptSession.app.
370
+ """
371
+
372
+ def __init__(
373
+ self,
374
+ *,
375
+ pointer: str = "→",
376
+ use_search_filter: bool = True,
377
+ search_placeholder: str = "type to search",
378
+ list_height: int = 8,
379
+ on_select: Callable[[T], Coroutine[Any, Any, None] | None] | None = None,
380
+ on_cancel: Callable[[], Coroutine[Any, Any, None] | None] | None = None,
381
+ ) -> None:
382
+ self._pointer = pointer
383
+ self._use_search_filter = use_search_filter
384
+ self._search_placeholder = search_placeholder
385
+ self._list_height = max(1, list_height)
386
+ self._on_select = on_select
387
+ self._on_cancel = on_cancel
388
+
389
+ self._is_open = False
390
+ self._message: str = ""
391
+ self._items: list[SelectItem[T]] = []
392
+ self._pointed_at = 0
393
+
394
+ self._prev_focus: Window | None = None
395
+ self._search_buffer: Buffer | None = Buffer() if use_search_filter else None
396
+
397
+ self._list_window: Window | None = None
398
+ self._search_input_window: Window | None = None
399
+
400
+ self.key_bindings = self._build_key_bindings()
401
+ self.container = self._build_layout()
402
+
403
+ if self._use_search_filter and self._search_buffer is not None:
404
+ self._search_buffer.on_text_changed += self._on_search_changed
405
+
406
+ def _get_filter_text(self) -> str:
407
+ if self._use_search_filter and self._search_buffer is not None:
408
+ return self._search_buffer.text
409
+ return ""
410
+
411
+ def _get_visible_indices(self) -> tuple[list[int], bool]:
412
+ return _filter_items(self._items, self._get_filter_text())
413
+
414
+ def _on_search_changed(self, _buf: Buffer) -> None:
415
+ self._pointed_at = 0
416
+ with contextlib.suppress(Exception):
417
+ get_app().invalidate()
418
+
419
+ def _build_key_bindings(self) -> KeyBindings:
420
+ kb = KeyBindings()
421
+ is_open_filter = Condition(lambda: self._is_open)
422
+
423
+ @kb.add(Keys.Down, filter=is_open_filter, eager=True)
424
+ def _(event: KeyPressEvent) -> None:
425
+ self._pointed_at += 1
426
+ event.app.invalidate()
427
+
428
+ @kb.add(Keys.Up, filter=is_open_filter, eager=True)
429
+ def _(event: KeyPressEvent) -> None:
430
+ self._pointed_at -= 1
431
+ event.app.invalidate()
432
+
433
+ @kb.add(Keys.Enter, filter=is_open_filter, eager=True)
434
+ def _(event: KeyPressEvent) -> None:
435
+ indices, _ = self._get_visible_indices()
436
+ if not indices:
437
+ self.close()
438
+ return
439
+ idx = indices[self._pointed_at % len(indices)]
440
+ value = self._items[idx].value
441
+ self.close()
442
+
443
+ if self._on_select is None:
444
+ return
445
+
446
+ result = self._on_select(value)
447
+ if hasattr(result, "__await__"):
448
+ event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
449
+
450
+ @kb.add(Keys.Escape, filter=is_open_filter, eager=True)
451
+ def _(event: KeyPressEvent) -> None:
452
+ if self._use_search_filter and self._search_buffer is not None and self._search_buffer.text:
453
+ self._search_buffer.reset(append_to_history=False)
454
+ self._pointed_at = 0
455
+ event.app.invalidate()
456
+ return
457
+ self._close_and_invoke_cancel(event)
458
+
459
+ @kb.add(Keys.ControlL, filter=is_open_filter, eager=True)
460
+ def _(event: KeyPressEvent) -> None:
461
+ self.close()
462
+ event.app.invalidate()
463
+
464
+ @kb.add(Keys.ControlC, filter=is_open_filter, eager=True)
465
+ def _(event: KeyPressEvent) -> None:
466
+ self._close_and_invoke_cancel(event)
467
+
468
+ return kb
469
+
470
+ def _close_and_invoke_cancel(self, event: KeyPressEvent) -> None:
471
+ self.close()
472
+ if self._on_cancel is not None:
473
+ result = self._on_cancel()
474
+ if hasattr(result, "__await__"):
475
+ event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
476
+
477
+ def _build_layout(self) -> ConditionalContainer:
478
+ def get_header_tokens() -> list[tuple[str, str]]:
479
+ return [("class:question", self._message + " ")]
480
+
481
+ def get_choices_tokens() -> list[tuple[str, str]]:
482
+ indices, _ = self._get_visible_indices()
483
+ if indices:
484
+ self._pointed_at %= len(indices)
485
+ return _build_choices_tokens(self._items, indices, self._pointed_at, self._pointer)
486
+
487
+ header_window = Window(
488
+ FormattedTextControl(get_header_tokens),
489
+ height=1,
490
+ dont_extend_height=Always(),
491
+ always_hide_cursor=Always(),
492
+ )
493
+ spacer_window = Window(
494
+ FormattedTextControl([("", "")]),
495
+ height=1,
496
+ dont_extend_height=Always(),
497
+ always_hide_cursor=Always(),
498
+ )
499
+ list_window = Window(
500
+ FormattedTextControl(get_choices_tokens),
501
+ height=self._list_height,
502
+ scroll_offsets=ScrollOffsets(top=0, bottom=2),
503
+ allow_scroll_beyond_bottom=True,
504
+ dont_extend_height=Always(),
505
+ always_hide_cursor=Always(),
506
+ )
507
+ self._list_window = list_window
508
+
509
+ search_container: Container | None = None
510
+ if self._use_search_filter and self._search_buffer is not None:
511
+ self._search_input_window, search_container = _build_search_container(
512
+ self._search_buffer, self._search_placeholder
513
+ )
514
+
515
+ root_children: list[Container] = [header_window, spacer_window, list_window]
516
+ if search_container is not None:
517
+ root_children.append(search_container)
518
+
519
+ return ConditionalContainer(
520
+ content=HSplit(root_children, padding=0),
521
+ filter=Condition(lambda: self._is_open),
522
+ )
523
+
524
+ @property
525
+ def is_open(self) -> bool:
526
+ return self._is_open
527
+
528
+ def set_content(self, *, message: str, items: list[SelectItem[T]], initial_value: T | None = None) -> None:
529
+ self._message = message
530
+ self._items = items
531
+
532
+ self._pointed_at = 0
533
+ if initial_value is not None:
534
+ try:
535
+ full_index = next(i for i, it in enumerate(items) if it.value == initial_value)
536
+ self._pointed_at = full_index
537
+ except StopIteration:
538
+ self._pointed_at = 0
539
+
540
+ if self._use_search_filter and self._search_buffer is not None:
541
+ self._search_buffer.reset(append_to_history=False)
542
+
543
+ def open(self) -> None:
544
+ if self._is_open:
545
+ return
546
+ self._is_open = True
547
+ app = get_app()
548
+ self._prev_focus = cast(Window | None, getattr(app.layout, "current_window", None))
549
+ with contextlib.suppress(Exception):
550
+ if self._search_input_window is not None:
551
+ app.layout.focus(self._search_input_window)
552
+ elif self._list_window is not None:
553
+ app.layout.focus(self._list_window)
554
+ app.invalidate()
555
+
556
+ def close(self) -> None:
557
+ if not self._is_open:
558
+ return
559
+ self._is_open = False
560
+ app = get_app()
561
+ prev = self._prev_focus
562
+ self._prev_focus = None
563
+ if prev is not None:
564
+ with contextlib.suppress(Exception):
565
+ app.layout.focus(prev)
566
+ app.invalidate()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.4.3
3
+ Version: 1.5.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0