kiwi-code 0.0.437.dev1__tar.gz → 0.0.438__tar.gz
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.
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/PKG-INFO +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/pyproject.toml +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/__init__.py +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/__init__.py +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/__init__.py +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/main.py +54 -6
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/dashboard.py +46 -4
- kiwi_code-0.0.438/src/kiwi_tui/screens/detach_files.py +146 -0
- kiwi_code-0.0.438/src/kiwi_tui/screens/term_dashboard.py +3898 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/slash_commands.py +1 -0
- kiwi_code-0.0.438/src/kiwi_tui/term_app.py +9 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_cli_help.py +9 -0
- kiwi_code-0.0.438/tests/test_term_dashboard_ui.py +302 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_terminal_mode.py +33 -3
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/uv.lock +1 -1
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.gitignore +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/.python-version +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/CLAUDE.md +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/Makefile +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/README.md +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/checkpoints.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/src/kiwi_tui/worktrees.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/test_hello.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/__init__.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/conftest.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_checkpoints.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_tui_palette.py +0 -0
- {kiwi_code-0.0.437.dev1 → kiwi_code-0.0.438}/tests/test_worktrees.py +0 -0
|
@@ -962,15 +962,47 @@ def _run_tui(runtime_args: RuntimeConnectArgs | None = None):
|
|
|
962
962
|
logger.info("Autobots TUI terminated")
|
|
963
963
|
|
|
964
964
|
|
|
965
|
-
def
|
|
965
|
+
def _run_term_tui(runtime_args: RuntimeConnectArgs | None = None) -> None:
|
|
966
|
+
"""Run the *term* (terminal-first) TUI.
|
|
967
|
+
|
|
968
|
+
This is the default UI for `kiwi`. Use `kiwi alt` for the legacy UI.
|
|
969
|
+
"""
|
|
970
|
+
from kiwi_tui.term_app import TermApp
|
|
971
|
+
|
|
972
|
+
config = AppConfig()
|
|
973
|
+
|
|
974
|
+
# If the user passed `--server`, use the same value for the TUI HTTP backend.
|
|
975
|
+
if runtime_args and runtime_args.server:
|
|
976
|
+
try:
|
|
977
|
+
config.backend_url = http_url_from_server(runtime_args.server)
|
|
978
|
+
except Exception:
|
|
979
|
+
pass
|
|
980
|
+
|
|
981
|
+
setup_logging(log_level=config.log_level)
|
|
982
|
+
|
|
983
|
+
logger.info("=" * 60)
|
|
984
|
+
logger.info("Starting Kiwi Term TUI")
|
|
985
|
+
logger.info("=" * 60)
|
|
986
|
+
|
|
987
|
+
app = TermApp(config=config, runtime_args=runtime_args)
|
|
988
|
+
app.run()
|
|
989
|
+
|
|
990
|
+
logger.info("Kiwi Term TUI terminated")
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _serve_tui(port: int = 8566, extra_argv: list[str] | None = None) -> None:
|
|
966
994
|
"""Serve the TUI in the browser via textual serve."""
|
|
995
|
+
import shlex
|
|
967
996
|
import sys
|
|
997
|
+
|
|
968
998
|
from textual_serve.server import Server
|
|
969
999
|
|
|
970
|
-
|
|
1000
|
+
cmd = [sys.executable, "-m", "kiwi_tui.main", *(extra_argv or [])]
|
|
1001
|
+
server = Server(shlex.join(cmd), port=port)
|
|
971
1002
|
server.serve()
|
|
972
1003
|
|
|
973
1004
|
|
|
1005
|
+
|
|
974
1006
|
def _reexec_as_kiwi() -> None:
|
|
975
1007
|
"""Re-exec via a symlink named ``kiwi`` so the kernel-reported
|
|
976
1008
|
process name (``ps -o comm`` / VS Code's ``${process}``) becomes
|
|
@@ -1126,6 +1158,19 @@ def main() -> int:
|
|
|
1126
1158
|
sys.stdout.flush()
|
|
1127
1159
|
|
|
1128
1160
|
argv = sys.argv[1:]
|
|
1161
|
+
|
|
1162
|
+
# UI selector (defaults to term UI).
|
|
1163
|
+
# - `kiwi` => term UI
|
|
1164
|
+
# - `kiwi alt` => legacy UI
|
|
1165
|
+
# - `kiwi term` => explicit term UI (compat)
|
|
1166
|
+
ui_mode = "term"
|
|
1167
|
+
if argv and argv[0] == "alt":
|
|
1168
|
+
ui_mode = "legacy"
|
|
1169
|
+
argv = argv[1:]
|
|
1170
|
+
elif argv and argv[0] == "term":
|
|
1171
|
+
ui_mode = "term"
|
|
1172
|
+
argv = argv[1:]
|
|
1173
|
+
|
|
1129
1174
|
if argv and argv[0] in {"login", "logout", "whoami"}:
|
|
1130
1175
|
return _run_auth_command(argv)
|
|
1131
1176
|
|
|
@@ -1137,14 +1182,14 @@ def main() -> int:
|
|
|
1137
1182
|
"--port", "-p", type=int, default=8566, help="Port to serve on (default: 8566)"
|
|
1138
1183
|
)
|
|
1139
1184
|
serve_args = serve_parser.parse_args(argv[1:])
|
|
1140
|
-
_serve_tui(port=serve_args.port)
|
|
1185
|
+
_serve_tui(port=serve_args.port, extra_argv=["alt"] if ui_mode == "legacy" else None)
|
|
1141
1186
|
return 0
|
|
1142
1187
|
|
|
1143
1188
|
parser = argparse.ArgumentParser(
|
|
1144
|
-
prog="kiwi",
|
|
1189
|
+
prog="kiwi alt" if ui_mode == "legacy" else "kiwi",
|
|
1145
1190
|
description="Kiwi Code",
|
|
1146
1191
|
epilog=(
|
|
1147
|
-
"Examples: `kiwi` (TUI), `kiwi --terminal \"Hi\"`, "
|
|
1192
|
+
"Examples: `kiwi` (TUI), `kiwi alt` (legacy TUI), `kiwi --terminal \"Hi\"`, "
|
|
1148
1193
|
"`kiwi login`, `kiwi whoami`, `kiwi serve`."
|
|
1149
1194
|
),
|
|
1150
1195
|
)
|
|
@@ -1241,7 +1286,10 @@ def main() -> int:
|
|
|
1241
1286
|
)
|
|
1242
1287
|
)
|
|
1243
1288
|
|
|
1244
|
-
|
|
1289
|
+
if ui_mode == "legacy":
|
|
1290
|
+
_run_tui(runtime_args=runtime_args)
|
|
1291
|
+
else:
|
|
1292
|
+
_run_term_tui(runtime_args=runtime_args)
|
|
1245
1293
|
return 0
|
|
1246
1294
|
|
|
1247
1295
|
|
|
@@ -49,7 +49,7 @@ class AssistantMessageRow(Horizontal):
|
|
|
49
49
|
"""A single assistant message rendered with a left dot + markdown body.
|
|
50
50
|
|
|
51
51
|
While streaming, shows a footer line under the message:
|
|
52
|
-
|
|
52
|
+
<verb> <spinner>
|
|
53
53
|
|
|
54
54
|
Requirement: the footer is visible only while streaming and disappears when
|
|
55
55
|
the final output is available.
|
|
@@ -80,7 +80,7 @@ class AssistantMessageRow(Horizontal):
|
|
|
80
80
|
|
|
81
81
|
# Footer comes after the body, and is only visible while streaming.
|
|
82
82
|
with Horizontal(classes="assistant-footer"):
|
|
83
|
-
yield Static(f"
|
|
83
|
+
yield Static(f"{self._verb}", classes="assistant-name", markup=False)
|
|
84
84
|
yield Static("⣾", classes="assistant-spinner", markup=False)
|
|
85
85
|
|
|
86
86
|
def update_markdown(self, markdown_text: str) -> None:
|
|
@@ -299,7 +299,8 @@ class DashboardScreen(Screen):
|
|
|
299
299
|
|
|
300
300
|
/* Grayish background to indicate "in progress" assistant output. */
|
|
301
301
|
.assistant-message.streaming {
|
|
302
|
-
background
|
|
302
|
+
/* Match the normal background so the streaming row doesn't look like a separate "block". */
|
|
303
|
+
background: $background;
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
.assistant-dot {
|
|
@@ -937,6 +938,47 @@ class DashboardScreen(Screen):
|
|
|
937
938
|
show(body)
|
|
938
939
|
return
|
|
939
940
|
|
|
941
|
+
if cmd == "/detach":
|
|
942
|
+
if not self._pending_urls:
|
|
943
|
+
toast("No files attached.", title="/detach", severity="warning")
|
|
944
|
+
return
|
|
945
|
+
|
|
946
|
+
from kiwi_tui.screens.detach_files import PendingFilesDetachScreen
|
|
947
|
+
|
|
948
|
+
pending_snapshot = list(self._pending_urls)
|
|
949
|
+
|
|
950
|
+
def _on_detach_result(result: list[str] | None) -> None:
|
|
951
|
+
if not result:
|
|
952
|
+
return
|
|
953
|
+
if "__all__" in result:
|
|
954
|
+
removed = self._clear_pending_files()
|
|
955
|
+
if removed:
|
|
956
|
+
toast(
|
|
957
|
+
f"Detached {removed} file{'s' if removed != 1 else ''}.",
|
|
958
|
+
title="Attachments",
|
|
959
|
+
timeout=4,
|
|
960
|
+
)
|
|
961
|
+
return
|
|
962
|
+
|
|
963
|
+
removed_urls: list[str] = []
|
|
964
|
+
for u in result:
|
|
965
|
+
if self._detach_pending_file(u):
|
|
966
|
+
removed_urls.append(u)
|
|
967
|
+
|
|
968
|
+
if removed_urls:
|
|
969
|
+
names = [Path(u.rsplit('/', 1)[-1]).name for u in removed_urls]
|
|
970
|
+
toast(
|
|
971
|
+
f"Detached {len(removed_urls)} file{'s' if len(removed_urls) != 1 else ''}: {', '.join(names)}",
|
|
972
|
+
title="Attachments",
|
|
973
|
+
timeout=4,
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
self.app.push_screen(
|
|
977
|
+
PendingFilesDetachScreen(pending_snapshot),
|
|
978
|
+
callback=_on_detach_result,
|
|
979
|
+
)
|
|
980
|
+
return
|
|
981
|
+
|
|
940
982
|
if cmd == "/clear-files":
|
|
941
983
|
count = self._clear_pending_files()
|
|
942
984
|
show(f"Cleared {count} pending file(s).")
|
|
@@ -2874,7 +2916,7 @@ class DashboardScreen(Screen):
|
|
|
2874
2916
|
"""Run action and stream results via SSE.
|
|
2875
2917
|
|
|
2876
2918
|
UX requirement: as soon as the user hits Enter, show an "in progress"
|
|
2877
|
-
assistant row (dot +
|
|
2919
|
+
assistant row (dot + header + spinner) and animate it until we
|
|
2878
2920
|
have a terminal result (success/error), regardless of whether SSE is
|
|
2879
2921
|
currently emitting tokens.
|
|
2880
2922
|
"""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Modal UI for detaching one or more pending file attachments.
|
|
2
|
+
|
|
3
|
+
Used by both DashboardScreen (stable) and TermDashboardScreen (term mode).
|
|
4
|
+
|
|
5
|
+
The modal returns:
|
|
6
|
+
- None when cancelled
|
|
7
|
+
- ["__all__"] when user chose to detach all
|
|
8
|
+
- list[str] of attachment URLs selected for detaching
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from textual.app import ComposeResult
|
|
16
|
+
from textual.binding import Binding
|
|
17
|
+
from textual.containers import Horizontal, Vertical
|
|
18
|
+
from textual.screen import ModalScreen
|
|
19
|
+
from textual.widgets import Button, SelectionList, Static
|
|
20
|
+
from textual.widgets.selection_list import Selection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PendingFilesDetachScreen(ModalScreen[list[str] | None]):
|
|
24
|
+
"""Multi-select detach UI for pending attachments."""
|
|
25
|
+
|
|
26
|
+
BINDINGS = [
|
|
27
|
+
Binding("escape", "cancel", "Cancel", show=True),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
CSS = """
|
|
31
|
+
PendingFilesDetachScreen {
|
|
32
|
+
align: center middle;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#detach-container {
|
|
36
|
+
width: 90;
|
|
37
|
+
height: 70%;
|
|
38
|
+
background: $primary-background;
|
|
39
|
+
border: solid $accent;
|
|
40
|
+
padding: 1 2;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#detach-title {
|
|
44
|
+
width: 100%;
|
|
45
|
+
text-style: bold;
|
|
46
|
+
color: $primary;
|
|
47
|
+
height: 1;
|
|
48
|
+
content-align: center middle;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#detach-subtitle {
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 1;
|
|
54
|
+
color: $foreground;
|
|
55
|
+
opacity: 0.75;
|
|
56
|
+
content-align: center middle;
|
|
57
|
+
margin-bottom: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#detach-list {
|
|
61
|
+
height: 1fr;
|
|
62
|
+
width: 100%;
|
|
63
|
+
border: none;
|
|
64
|
+
background: $primary-background;
|
|
65
|
+
color: $foreground;
|
|
66
|
+
scrollbar-size-vertical: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#detach-buttons {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: auto;
|
|
72
|
+
margin-top: 1;
|
|
73
|
+
content-align: center middle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#detach-buttons Button {
|
|
77
|
+
margin: 0 1;
|
|
78
|
+
}
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, pending_urls: list[str]):
|
|
82
|
+
super().__init__()
|
|
83
|
+
self._pending_urls = list(pending_urls)
|
|
84
|
+
|
|
85
|
+
def compose(self) -> ComposeResult:
|
|
86
|
+
with Vertical(id="detach-container"):
|
|
87
|
+
yield Static("Detach attachments", id="detach-title", markup=False)
|
|
88
|
+
yield Static("Select file(s) to detach", id="detach-subtitle", markup=False)
|
|
89
|
+
|
|
90
|
+
# Deduplicate display names to avoid confusing duplicates.
|
|
91
|
+
name_counts: dict[str, int] = {}
|
|
92
|
+
selections: list[Selection] = []
|
|
93
|
+
for url in self._pending_urls:
|
|
94
|
+
name = Path(str(url).rsplit("/", 1)[-1]).name or "(unnamed)"
|
|
95
|
+
count = name_counts.get(name, 0) + 1
|
|
96
|
+
name_counts[name] = count
|
|
97
|
+
label = name if count == 1 else f"{name} ({count})"
|
|
98
|
+
selections.append(Selection(label, url, initial_state=False))
|
|
99
|
+
|
|
100
|
+
yield SelectionList(*selections, id="detach-list")
|
|
101
|
+
|
|
102
|
+
with Horizontal(id="detach-buttons"):
|
|
103
|
+
yield Button("Detach selected", id="detach-selected", variant="default")
|
|
104
|
+
yield Button("Detach all", id="detach-all", variant="default")
|
|
105
|
+
yield Button("Cancel", id="cancel", variant="default")
|
|
106
|
+
|
|
107
|
+
def on_mount(self) -> None:
|
|
108
|
+
try:
|
|
109
|
+
self.query_one("#detach-list", SelectionList).focus()
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
def action_cancel(self) -> None:
|
|
114
|
+
self.dismiss(None)
|
|
115
|
+
|
|
116
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
117
|
+
btn_id = (event.button.id or "").strip()
|
|
118
|
+
if btn_id == "cancel":
|
|
119
|
+
self.dismiss(None)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
if btn_id == "detach-all":
|
|
123
|
+
self.dismiss(["__all__"])
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if btn_id == "detach-selected":
|
|
127
|
+
try:
|
|
128
|
+
selected = list(self.query_one("#detach-list", SelectionList).selected)
|
|
129
|
+
except Exception:
|
|
130
|
+
selected = []
|
|
131
|
+
|
|
132
|
+
if not selected:
|
|
133
|
+
try:
|
|
134
|
+
self.app.notify(
|
|
135
|
+
"No files selected.",
|
|
136
|
+
title="Detach",
|
|
137
|
+
severity="warning",
|
|
138
|
+
timeout=2.5,
|
|
139
|
+
markup=False,
|
|
140
|
+
)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
self.dismiss(selected)
|
|
146
|
+
return
|