kiwi-code 0.0.33__tar.gz → 0.0.35__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.33 → kiwi_code-0.0.35}/PKG-INFO +1 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/pyproject.toml +1 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/main.py +40 -33
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/attach_content.py +1 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/command_result.py +1 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/dashboard.py +243 -44
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/file_browser.py +23 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/help.py +2 -2
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/id_picker.py +11 -11
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/login.py +1 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/runtime_cleanup.py +31 -7
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/runtime_logs.py +28 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/slash_picker.py +2 -1
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/widgets.py +98 -6
- kiwi_code-0.0.35/tests/test_tui_headless.py +677 -0
- kiwi_code-0.0.35/tests/test_tui_palette.py +68 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/uv.lock +1 -1
- kiwi_code-0.0.33/tests/test_tui_headless.py +0 -327
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/.gitignore +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/.python-version +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/CLAUDE.md +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/Makefile +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/README.md +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/test_hello.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/__init__.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/conftest.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.33 → kiwi_code-0.0.35}/tests/test_tokens.py +0 -0
|
@@ -235,8 +235,8 @@ class AutobotsTUI(App):
|
|
|
235
235
|
# Brand color for the kiwi cyan, kept readable on both dark and
|
|
236
236
|
# light themes. Exposed to CSS as `$brand-cyan` via
|
|
237
237
|
# `get_css_variables` below.
|
|
238
|
-
BRAND_CYAN_DARK = "#
|
|
239
|
-
BRAND_CYAN_LIGHT = "#
|
|
238
|
+
BRAND_CYAN_DARK = "#63d8dc" # softened cyan — same feel, less eye strain on dark backgrounds
|
|
239
|
+
BRAND_CYAN_LIGHT = "#0b7084" # slightly softened teal for light backgrounds
|
|
240
240
|
|
|
241
241
|
def _is_dark_theme(self) -> bool:
|
|
242
242
|
"""Best-effort detection of whether the active theme is dark."""
|
|
@@ -249,65 +249,72 @@ class AutobotsTUI(App):
|
|
|
249
249
|
return bool(getattr(self, "dark", True))
|
|
250
250
|
|
|
251
251
|
def get_css_variables(self) -> dict[str, str]:
|
|
252
|
-
"""Inject theme-aware brand color and
|
|
253
|
-
|
|
254
|
-
UX requirement: use a minimal palette (white + kiwi cyan) for UI text and
|
|
255
|
-
accents across screens.
|
|
256
|
-
"""
|
|
252
|
+
"""Inject theme-aware brand color and a softer neutral palette."""
|
|
257
253
|
variables = super().get_css_variables()
|
|
258
|
-
|
|
254
|
+
dark = self._is_dark_theme()
|
|
255
|
+
cyan = self.BRAND_CYAN_DARK if dark else self.BRAND_CYAN_LIGHT
|
|
259
256
|
variables["brand-cyan"] = cyan
|
|
260
257
|
|
|
261
|
-
|
|
262
|
-
|
|
258
|
+
if dark:
|
|
259
|
+
# Match the reference terminal theme more closely:
|
|
260
|
+
# - global background around #1E1E1E
|
|
261
|
+
# - user prompts on a lighter gray chip around #3A3A3A
|
|
262
|
+
bg = "#1E1E1E"
|
|
263
|
+
surface = "#252526"
|
|
264
|
+
raised = "#2D2D2D"
|
|
265
|
+
panel = "#333333"
|
|
266
|
+
foreground = "#F3F3F3"
|
|
267
|
+
user_msg_bg = "#3A3A3A"
|
|
268
|
+
assistant_stream_bg = "#252526"
|
|
269
|
+
else:
|
|
270
|
+
bg = "#f6f7f8"
|
|
271
|
+
surface = "#ffffff"
|
|
272
|
+
raised = "#eef1f3"
|
|
273
|
+
panel = "#dbe7ea"
|
|
274
|
+
foreground = "#101213"
|
|
275
|
+
user_msg_bg = "#f1f3f4"
|
|
276
|
+
assistant_stream_bg = "#f4f6f7"
|
|
277
|
+
|
|
278
|
+
variables["background"] = bg
|
|
263
279
|
variables["foreground"] = foreground
|
|
264
280
|
variables["text"] = foreground
|
|
265
281
|
|
|
266
|
-
#
|
|
282
|
+
# Keep the kiwi cyan for accents while softening the surrounding surfaces.
|
|
267
283
|
for k in ("primary", "secondary", "accent", "error", "warning", "success"):
|
|
268
284
|
variables[k] = cyan
|
|
269
285
|
|
|
270
|
-
# Also align text-* semantic variants used by Markdown, notifications, etc.
|
|
271
286
|
for k in ("text-warning", "text-error", "text-success"):
|
|
272
287
|
variables[k] = cyan
|
|
273
288
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
variables["surface"] = bg
|
|
277
|
-
variables["boost"] = bg
|
|
289
|
+
variables["surface"] = surface
|
|
290
|
+
variables["boost"] = raised
|
|
278
291
|
variables["primary-background"] = bg
|
|
279
|
-
variables["panel"] =
|
|
280
|
-
variables["border"] =
|
|
281
|
-
variables["
|
|
292
|
+
variables["panel"] = panel
|
|
293
|
+
variables["border"] = panel
|
|
294
|
+
variables["block-hover-background"] = surface
|
|
295
|
+
variables["border-blurred"] = raised
|
|
282
296
|
|
|
297
|
+
variables["user-msg-bg"] = user_msg_bg
|
|
298
|
+
variables["assistant-stream-bg"] = assistant_stream_bg
|
|
283
299
|
|
|
284
|
-
# Chat UX: full-width highlight color for user-message rows.
|
|
285
|
-
# Keep it subtle and theme-aware.
|
|
286
|
-
variables["user-msg-bg"] = "#333333" if self._is_dark_theme() else "#e8e8e8"
|
|
287
|
-
# Chat UX: subtle highlight for the *active streaming* assistant message.
|
|
288
|
-
variables["assistant-stream-bg"] = "#222222" if self._is_dark_theme() else "#f2f2f2"
|
|
289
|
-
# Scrollbars: cyan thumb, dark track.
|
|
290
300
|
variables["scrollbar"] = cyan
|
|
291
301
|
variables["scrollbar-hover"] = cyan
|
|
292
302
|
variables["scrollbar-active"] = cyan
|
|
293
|
-
variables["scrollbar-background"] =
|
|
294
|
-
variables["scrollbar-background-hover"] =
|
|
295
|
-
variables["scrollbar-background-active"] =
|
|
303
|
+
variables["scrollbar-background"] = surface
|
|
304
|
+
variables["scrollbar-background-hover"] = surface
|
|
305
|
+
variables["scrollbar-background-active"] = raised
|
|
296
306
|
variables["scrollbar-corner-color"] = bg
|
|
297
307
|
|
|
298
|
-
|
|
299
|
-
variables["footer-background"] = bg
|
|
308
|
+
variables["footer-background"] = surface
|
|
300
309
|
variables["footer-foreground"] = foreground
|
|
301
|
-
variables["footer-description-background"] =
|
|
310
|
+
variables["footer-description-background"] = surface
|
|
302
311
|
variables["footer-description-foreground"] = foreground
|
|
303
312
|
|
|
304
|
-
# Cursor / selection highlight: cyan text on background (no extra colors).
|
|
305
313
|
variables["block-cursor-foreground"] = cyan
|
|
306
314
|
variables["block-cursor-background"] = bg
|
|
307
315
|
variables["block-cursor-blurred-foreground"] = cyan
|
|
308
316
|
variables["block-cursor-blurred-background"] = bg
|
|
309
317
|
return variables
|
|
310
|
-
|
|
311
318
|
def watch_theme(self, theme: str) -> None:
|
|
312
319
|
"""Re-resolve palette variables whenever the user switches theme."""
|
|
313
320
|
try:
|
|
@@ -6,6 +6,7 @@ from typing import Callable
|
|
|
6
6
|
from textual.app import ComposeResult
|
|
7
7
|
from textual.binding import Binding
|
|
8
8
|
from textual.screen import Screen
|
|
9
|
+
from textual import events
|
|
9
10
|
from textual.widgets import Header, Footer, Input, Static, Button, Markdown, LoadingIndicator
|
|
10
11
|
from textual.containers import Vertical, VerticalScroll, Horizontal, Center
|
|
11
12
|
from kiwi_tui.screens.file_browser import FileBrowserScreen
|
|
@@ -159,6 +160,7 @@ class DashboardScreen(Screen):
|
|
|
159
160
|
self.current_run_kind: str | None = None # 'action' or 'graph' (best-effort)
|
|
160
161
|
self._last_listed_ids: list[str] = [] # Index for #N shortcuts
|
|
161
162
|
self._pending_urls: list[str] = [] # File URLs to attach to next message
|
|
163
|
+
self._upload_status_text: str = "" # Transient upload status shown above the input box
|
|
162
164
|
self._metadata: dict = {} # Persistent metadata for all subsequent runs
|
|
163
165
|
self._is_streaming: bool = False # Track streaming state for send/stop toggle
|
|
164
166
|
|
|
@@ -260,7 +262,7 @@ class DashboardScreen(Screen):
|
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
|
|
263
|
-
#bottom-area, #input-bar, #input-row, #pending-files-bar {
|
|
265
|
+
#bottom-area, #input-bar, #input-row, #pending-files-row, #pending-files-bar {
|
|
264
266
|
background: $background;
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -377,13 +379,34 @@ class DashboardScreen(Screen):
|
|
|
377
379
|
padding: 0;
|
|
378
380
|
}
|
|
379
381
|
|
|
380
|
-
#pending-files-
|
|
382
|
+
#pending-files-row {
|
|
381
383
|
height: auto;
|
|
382
384
|
width: 100%;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#pending-files-bar {
|
|
388
|
+
height: auto;
|
|
389
|
+
width: 1fr;
|
|
383
390
|
padding: 0 1;
|
|
384
391
|
color: $secondary;
|
|
385
392
|
}
|
|
386
393
|
|
|
394
|
+
#detach-files-btn {
|
|
395
|
+
width: auto;
|
|
396
|
+
height: 1;
|
|
397
|
+
min-width: 8;
|
|
398
|
+
margin: 0 1 0 0;
|
|
399
|
+
padding: 0 1;
|
|
400
|
+
background: transparent;
|
|
401
|
+
color: $warning;
|
|
402
|
+
border: none;
|
|
403
|
+
display: none;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#detach-files-btn.visible {
|
|
407
|
+
display: block;
|
|
408
|
+
}
|
|
409
|
+
|
|
387
410
|
#input-row {
|
|
388
411
|
height: auto;
|
|
389
412
|
width: 100%;
|
|
@@ -539,7 +562,9 @@ class DashboardScreen(Screen):
|
|
|
539
562
|
with Horizontal(id="activity-bar"):
|
|
540
563
|
yield LoadingIndicator(id="activity-spinner")
|
|
541
564
|
yield Static("", id="activity-text", markup=False)
|
|
542
|
-
|
|
565
|
+
with Horizontal(id="pending-files-row"):
|
|
566
|
+
yield Static("", id="pending-files-bar", markup=False)
|
|
567
|
+
yield Button("Detach", id="detach-files-btn", variant="default")
|
|
543
568
|
with Horizontal(id="input-row"):
|
|
544
569
|
yield ChatInput(placeholder="Message... (@ for files, / for commands)", id="chat-input")
|
|
545
570
|
with Horizontal(id="button-row"):
|
|
@@ -655,10 +680,8 @@ class DashboardScreen(Screen):
|
|
|
655
680
|
pass
|
|
656
681
|
chat_input.picker_active = False
|
|
657
682
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if at_pos != -1:
|
|
661
|
-
chat_input.value = val[:at_pos]
|
|
683
|
+
if chat_input.value.startswith("@"):
|
|
684
|
+
chat_input.value = ""
|
|
662
685
|
|
|
663
686
|
# Open the full file browser; it returns list[str] of selected files.
|
|
664
687
|
self.app.push_screen(FileBrowserScreen(), callback=self._on_files_selected)
|
|
@@ -701,11 +724,9 @@ class DashboardScreen(Screen):
|
|
|
701
724
|
chat_input = self.query_one("#chat-input", ChatInput)
|
|
702
725
|
picker.hide_picker()
|
|
703
726
|
chat_input.picker_active = False
|
|
704
|
-
# Remove the
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if at_pos != -1:
|
|
708
|
-
chat_input.value = val[:at_pos]
|
|
727
|
+
# Remove the leading @-picker text from the input.
|
|
728
|
+
if chat_input.value.startswith("@"):
|
|
729
|
+
chat_input.value = ""
|
|
709
730
|
chat_input.focus()
|
|
710
731
|
|
|
711
732
|
def on_chat_input_changed(self, event: ChatInput.Changed) -> None:
|
|
@@ -714,19 +735,60 @@ class DashboardScreen(Screen):
|
|
|
714
735
|
if not chat_input.picker_active:
|
|
715
736
|
return
|
|
716
737
|
val = chat_input.value
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
# @ was deleted — close picker
|
|
738
|
+
if not val.startswith("@"):
|
|
739
|
+
# The leading @ was removed or text was inserted before it.
|
|
720
740
|
self._close_inline_picker()
|
|
721
741
|
return
|
|
722
|
-
filter_text = val[
|
|
742
|
+
filter_text = val[1:]
|
|
723
743
|
picker = self.query_one("#inline-file-picker", InlineFilePicker)
|
|
724
744
|
picker.filter(filter_text)
|
|
725
745
|
|
|
746
|
+
|
|
747
|
+
def on_paste(self, event: events.Paste) -> None:
|
|
748
|
+
"""Handle terminal paste/drop events that land on the screen instead of the input."""
|
|
749
|
+
try:
|
|
750
|
+
chat_input = self.query_one("#chat-input", ChatInput)
|
|
751
|
+
except Exception:
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
file_paths = chat_input._extract_pasted_file_paths(event.text)
|
|
755
|
+
if not file_paths:
|
|
756
|
+
return
|
|
757
|
+
|
|
758
|
+
event.prevent_default()
|
|
759
|
+
event.stop()
|
|
760
|
+
try:
|
|
761
|
+
self._close_inline_picker()
|
|
762
|
+
except Exception:
|
|
763
|
+
pass
|
|
764
|
+
self.notify(
|
|
765
|
+
f"Detected {len(file_paths)} file{'s' if len(file_paths) != 1 else ''}. Uploading...",
|
|
766
|
+
title="Attachments",
|
|
767
|
+
severity="information",
|
|
768
|
+
timeout=2.5,
|
|
769
|
+
markup=False,
|
|
770
|
+
)
|
|
771
|
+
self._on_files_selected(file_paths)
|
|
772
|
+
|
|
726
773
|
def on_chat_input_submitted(self, event: ChatInput.Submitted) -> None:
|
|
727
774
|
"""Handle message submission (Enter)."""
|
|
728
775
|
self._do_send()
|
|
729
776
|
|
|
777
|
+
def on_chat_input_file_paths_pasted(self, event: ChatInput.FilePathsPasted) -> None:
|
|
778
|
+
"""Handle terminal drag/drop file pastes via the normal upload flow."""
|
|
779
|
+
try:
|
|
780
|
+
self._close_inline_picker()
|
|
781
|
+
except Exception:
|
|
782
|
+
pass
|
|
783
|
+
self.notify(
|
|
784
|
+
f"Detected {len(event.file_paths)} file{'s' if len(event.file_paths) != 1 else ''}. Uploading...",
|
|
785
|
+
title="Attachments",
|
|
786
|
+
severity="information",
|
|
787
|
+
timeout=2.5,
|
|
788
|
+
markup=False,
|
|
789
|
+
)
|
|
790
|
+
self._on_files_selected(event.file_paths)
|
|
791
|
+
|
|
730
792
|
def handle_slash_command(self, command: str) -> None:
|
|
731
793
|
"""Schedule a slash command to run without blocking the UI."""
|
|
732
794
|
# If a command is already running, ignore (prevents the "hung" feeling / double execution).
|
|
@@ -831,9 +893,7 @@ class DashboardScreen(Screen):
|
|
|
831
893
|
return
|
|
832
894
|
|
|
833
895
|
if cmd == "/clear-files":
|
|
834
|
-
count =
|
|
835
|
-
self._pending_urls.clear()
|
|
836
|
-
self._update_pending_files_bar()
|
|
896
|
+
count = self._clear_pending_files()
|
|
837
897
|
show(f"Cleared {count} pending file(s).")
|
|
838
898
|
return
|
|
839
899
|
|
|
@@ -2095,6 +2155,30 @@ class DashboardScreen(Screen):
|
|
|
2095
2155
|
self._do_send()
|
|
2096
2156
|
return
|
|
2097
2157
|
|
|
2158
|
+
if btn_id == "detach-files-btn":
|
|
2159
|
+
if len(self._pending_urls) <= 1:
|
|
2160
|
+
count = self._clear_pending_files()
|
|
2161
|
+
self.notify(
|
|
2162
|
+
f"Detached {count} file{'s' if count != 1 else ''}.",
|
|
2163
|
+
title="Attachments",
|
|
2164
|
+
severity="information",
|
|
2165
|
+
timeout=2.5,
|
|
2166
|
+
markup=False,
|
|
2167
|
+
)
|
|
2168
|
+
try:
|
|
2169
|
+
self.query_one("#chat-input", ChatInput).focus()
|
|
2170
|
+
except Exception:
|
|
2171
|
+
pass
|
|
2172
|
+
else:
|
|
2173
|
+
self.app.push_screen(
|
|
2174
|
+
IdPickerScreen(
|
|
2175
|
+
"Detach pending file",
|
|
2176
|
+
options=self._pending_file_picker_options(),
|
|
2177
|
+
),
|
|
2178
|
+
callback=self._on_pending_file_detach_selected,
|
|
2179
|
+
)
|
|
2180
|
+
return
|
|
2181
|
+
|
|
2098
2182
|
api_client = self._get_api_client()
|
|
2099
2183
|
if not api_client:
|
|
2100
2184
|
return
|
|
@@ -2228,9 +2312,11 @@ class DashboardScreen(Screen):
|
|
|
2228
2312
|
except Exception:
|
|
2229
2313
|
pass
|
|
2230
2314
|
# Disable auxiliary buttons while busy.
|
|
2231
|
-
for btn_id in ("#upload-btn", "#slash-btn", "#copy-run-id"):
|
|
2315
|
+
for btn_id in ("#upload-btn", "#slash-btn", "#copy-run-id", "#detach-files-btn"):
|
|
2232
2316
|
try:
|
|
2233
|
-
|
|
2317
|
+
is_copy_run = btn_id == "#copy-run-id" and not bool(self.current_run_id)
|
|
2318
|
+
is_detach = btn_id == "#detach-files-btn" and (not bool(self._pending_urls) or bool(self._upload_status_text))
|
|
2319
|
+
self.query_one(btn_id, Button).disabled = disabled or is_copy_run or is_detach
|
|
2234
2320
|
except Exception:
|
|
2235
2321
|
pass
|
|
2236
2322
|
self._update_run_status_bar()
|
|
@@ -2402,6 +2488,11 @@ class DashboardScreen(Screen):
|
|
|
2402
2488
|
self.handle_slash_command(command)
|
|
2403
2489
|
self.query_one("#chat-input", ChatInput).focus()
|
|
2404
2490
|
|
|
2491
|
+
def _set_upload_status(self, text: str) -> None:
|
|
2492
|
+
"""Update the transient upload status shown immediately above the text box."""
|
|
2493
|
+
self._upload_status_text = text
|
|
2494
|
+
self._update_pending_files_bar()
|
|
2495
|
+
|
|
2405
2496
|
def _on_files_selected(self, file_paths: list[str]) -> None:
|
|
2406
2497
|
"""Callback from FileBrowserScreen — upload selected files."""
|
|
2407
2498
|
if not file_paths:
|
|
@@ -2411,40 +2502,148 @@ class DashboardScreen(Screen):
|
|
|
2411
2502
|
self.add_message("Error: Client not initialized", "error")
|
|
2412
2503
|
return
|
|
2413
2504
|
names = [Path(p).name for p in file_paths]
|
|
2414
|
-
self.
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2505
|
+
self._set_upload_status(f"Uploading: {', '.join(names)}")
|
|
2506
|
+
self.notify(
|
|
2507
|
+
f"Uploading {len(file_paths)} file{'s' if len(file_paths) != 1 else ''}: {', '.join(names)}",
|
|
2508
|
+
title="Attachments",
|
|
2509
|
+
severity="information",
|
|
2510
|
+
timeout=3,
|
|
2511
|
+
markup=False,
|
|
2512
|
+
)
|
|
2513
|
+
self.run_worker(
|
|
2514
|
+
self._upload_files_async(file_paths),
|
|
2515
|
+
group="upload",
|
|
2516
|
+
exclusive=False,
|
|
2517
|
+
exit_on_error=False,
|
|
2518
|
+
)
|
|
2519
|
+
self.query_one("#chat-input", ChatInput).focus()
|
|
2420
2520
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2521
|
+
async def _upload_files_async(self, file_paths: list[str]) -> None:
|
|
2522
|
+
"""Upload files without blocking the UI so the upload bar renders immediately."""
|
|
2523
|
+
try:
|
|
2524
|
+
# Ensure the token is fresh before upload (important for long-running sessions).
|
|
2425
2525
|
try:
|
|
2426
|
-
|
|
2526
|
+
await asyncio.to_thread(self.app._refresh_token_if_needed)
|
|
2427
2527
|
except Exception:
|
|
2528
|
+
pass
|
|
2529
|
+
|
|
2530
|
+
success, urls, message = await asyncio.to_thread(
|
|
2531
|
+
lambda: self.app.autobots_client.upload_files(file_paths)
|
|
2532
|
+
)
|
|
2533
|
+
if (not success) and ("HTTP 401" in message or "HTTP 403" in message):
|
|
2428
2534
|
refreshed = False
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
self.add_message(f"Upload failed: {message}", "error")
|
|
2438
|
-
self.query_one("#chat-input", ChatInput).focus()
|
|
2535
|
+
try:
|
|
2536
|
+
refreshed = bool(await asyncio.to_thread(self.app._refresh_token_if_needed, True))
|
|
2537
|
+
except Exception:
|
|
2538
|
+
refreshed = False
|
|
2539
|
+
if refreshed:
|
|
2540
|
+
success, urls, message = await asyncio.to_thread(
|
|
2541
|
+
lambda: self.app.autobots_client.upload_files(file_paths)
|
|
2542
|
+
)
|
|
2439
2543
|
|
|
2544
|
+
if success:
|
|
2545
|
+
self._pending_urls.extend(urls)
|
|
2546
|
+
self._set_upload_status("")
|
|
2547
|
+
self.notify(
|
|
2548
|
+
f"Attached {len(urls)} file{'s' if len(urls) != 1 else ''} to your next message.",
|
|
2549
|
+
title="Attachments",
|
|
2550
|
+
severity="information",
|
|
2551
|
+
timeout=3.5,
|
|
2552
|
+
markup=False,
|
|
2553
|
+
)
|
|
2554
|
+
self._update_pending_files_bar()
|
|
2555
|
+
else:
|
|
2556
|
+
self._set_upload_status("")
|
|
2557
|
+
self.add_message(f"Upload failed: {message}", "error")
|
|
2558
|
+
self.notify(
|
|
2559
|
+
f"Upload failed: {message}",
|
|
2560
|
+
title="Attachments",
|
|
2561
|
+
severity="error",
|
|
2562
|
+
timeout=4.5,
|
|
2563
|
+
markup=False,
|
|
2564
|
+
)
|
|
2565
|
+
finally:
|
|
2566
|
+
try:
|
|
2567
|
+
self.query_one("#chat-input", ChatInput).focus()
|
|
2568
|
+
except Exception:
|
|
2569
|
+
pass
|
|
2570
|
+
|
|
2571
|
+
def _clear_pending_files(self) -> int:
|
|
2572
|
+
"""Detach all pending files from the next message."""
|
|
2573
|
+
count = len(self._pending_urls)
|
|
2574
|
+
self._pending_urls.clear()
|
|
2575
|
+
self._set_upload_status("")
|
|
2576
|
+
self._update_pending_files_bar()
|
|
2577
|
+
return count
|
|
2578
|
+
|
|
2579
|
+
|
|
2580
|
+
def _detach_pending_file(self, url: str) -> bool:
|
|
2581
|
+
"""Detach a single pending file by URL."""
|
|
2582
|
+
try:
|
|
2583
|
+
self._pending_urls.remove(url)
|
|
2584
|
+
except ValueError:
|
|
2585
|
+
return False
|
|
2586
|
+
self._update_pending_files_bar()
|
|
2587
|
+
return True
|
|
2588
|
+
|
|
2589
|
+
def _pending_file_picker_options(self) -> list[tuple[str, str]]:
|
|
2590
|
+
"""Build picker options for detaching one or all pending files."""
|
|
2591
|
+
options: list[tuple[str, str]] = [("__all__", "Detach all files")]
|
|
2592
|
+
for url in self._pending_urls:
|
|
2593
|
+
name = Path(url.rsplit("/", 1)[-1]).name if "/" in url else url
|
|
2594
|
+
options.append((url, name))
|
|
2595
|
+
return options
|
|
2596
|
+
|
|
2597
|
+
def _on_pending_file_detach_selected(self, selection: str) -> None:
|
|
2598
|
+
"""Detach a selected pending file from the next message."""
|
|
2599
|
+
if not selection:
|
|
2600
|
+
try:
|
|
2601
|
+
self.query_one("#chat-input", ChatInput).focus()
|
|
2602
|
+
except Exception:
|
|
2603
|
+
pass
|
|
2604
|
+
return
|
|
2605
|
+
|
|
2606
|
+
if selection == "__all__":
|
|
2607
|
+
count = self._clear_pending_files()
|
|
2608
|
+
self.notify(
|
|
2609
|
+
f"Detached {count} file{'s' if count != 1 else ''}.",
|
|
2610
|
+
title="Attachments",
|
|
2611
|
+
severity="information",
|
|
2612
|
+
timeout=2.5,
|
|
2613
|
+
markup=False,
|
|
2614
|
+
)
|
|
2615
|
+
else:
|
|
2616
|
+
removed = self._detach_pending_file(selection)
|
|
2617
|
+
if removed:
|
|
2618
|
+
name = Path(selection.rsplit("/", 1)[-1]).name if "/" in selection else selection
|
|
2619
|
+
self.notify(
|
|
2620
|
+
f"Detached: {name}",
|
|
2621
|
+
title="Attachments",
|
|
2622
|
+
severity="information",
|
|
2623
|
+
timeout=2.5,
|
|
2624
|
+
markup=False,
|
|
2625
|
+
)
|
|
2626
|
+
try:
|
|
2627
|
+
self.query_one("#chat-input", ChatInput).focus()
|
|
2628
|
+
except Exception:
|
|
2629
|
+
pass
|
|
2440
2630
|
def _update_pending_files_bar(self) -> None:
|
|
2441
|
-
"""Update the
|
|
2631
|
+
"""Update the bar above the input to show upload progress or attached files."""
|
|
2442
2632
|
bar = self.query_one("#pending-files-bar", Static)
|
|
2443
|
-
|
|
2633
|
+
detach_btn = self.query_one("#detach-files-btn", Button)
|
|
2634
|
+
if self._upload_status_text:
|
|
2635
|
+
bar.update(self._upload_status_text)
|
|
2636
|
+
detach_btn.remove_class("visible")
|
|
2637
|
+
detach_btn.disabled = True
|
|
2638
|
+
elif self._pending_urls:
|
|
2444
2639
|
names = [Path(url.rsplit("/", 1)[-1]).name for url in self._pending_urls]
|
|
2445
2640
|
bar.update(f"Attached: {', '.join(names)}")
|
|
2641
|
+
detach_btn.add_class("visible")
|
|
2642
|
+
detach_btn.disabled = False
|
|
2446
2643
|
else:
|
|
2447
2644
|
bar.update("")
|
|
2645
|
+
detach_btn.remove_class("visible")
|
|
2646
|
+
detach_btn.disabled = True
|
|
2448
2647
|
|
|
2449
2648
|
def process_message(self, message: str) -> None:
|
|
2450
2649
|
"""Process user message by running action."""
|
|
@@ -29,7 +29,7 @@ class FileBrowserScreen(ModalScreen[list[str]]):
|
|
|
29
29
|
#file-browser-container {
|
|
30
30
|
width: 96%;
|
|
31
31
|
height: 95%;
|
|
32
|
-
background: $
|
|
32
|
+
background: $primary-background;
|
|
33
33
|
border: solid $accent;
|
|
34
34
|
padding: 0 1;
|
|
35
35
|
}
|
|
@@ -75,10 +75,32 @@ class FileBrowserScreen(ModalScreen[list[str]]):
|
|
|
75
75
|
#dir-tree {
|
|
76
76
|
height: 1fr;
|
|
77
77
|
width: 100%;
|
|
78
|
+
background: $primary-background;
|
|
78
79
|
border: solid $panel;
|
|
79
80
|
scrollbar-size: 1 1;
|
|
81
|
+
#dir-tree:focus {
|
|
82
|
+
background-tint: 0%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#dir-tree > .tree--highlight-line {
|
|
86
|
+
background: $surface;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#dir-tree > .tree--cursor {
|
|
90
|
+
background: $surface;
|
|
91
|
+
color: $foreground;
|
|
92
|
+
text-style: none;
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
#dir-tree:focus > .tree--cursor {
|
|
96
|
+
background: $boost;
|
|
97
|
+
color: $foreground;
|
|
98
|
+
text-style: bold;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
82
104
|
#selected-files {
|
|
83
105
|
height: auto;
|
|
84
106
|
max-height: 3;
|
|
@@ -29,7 +29,7 @@ class HelpScreen(ModalScreen[None]):
|
|
|
29
29
|
#help-container {
|
|
30
30
|
width: 96;
|
|
31
31
|
height: 80%;
|
|
32
|
-
background: $
|
|
32
|
+
background: $primary-background;
|
|
33
33
|
border: solid $accent;
|
|
34
34
|
/* Reduce top padding so the title/hint sit closer to the border. */
|
|
35
35
|
padding: 0 3 1 3;
|
|
@@ -58,7 +58,7 @@ class HelpScreen(ModalScreen[None]):
|
|
|
58
58
|
height: 1fr;
|
|
59
59
|
width: 100%;
|
|
60
60
|
border: solid $panel;
|
|
61
|
-
background: $
|
|
61
|
+
background: $primary-background;
|
|
62
62
|
scrollbar-size: 1 1;
|
|
63
63
|
/* Keep the list away from the container border for consistent margins. */
|
|
64
64
|
margin: 0 1;
|
|
@@ -101,7 +101,7 @@ class IdPickerScreen(ModalScreen[str]):
|
|
|
101
101
|
#idpicker-container {
|
|
102
102
|
width: 90;
|
|
103
103
|
height: 90%;
|
|
104
|
-
background: $
|
|
104
|
+
background: $primary-background;
|
|
105
105
|
border: solid $accent;
|
|
106
106
|
padding: 0 1;
|
|
107
107
|
}
|
|
@@ -111,7 +111,7 @@ class IdPickerScreen(ModalScreen[str]):
|
|
|
111
111
|
text-align: center;
|
|
112
112
|
text-style: bold;
|
|
113
113
|
color: $primary;
|
|
114
|
-
background: $
|
|
114
|
+
background: $primary-background;
|
|
115
115
|
height: 1;
|
|
116
116
|
margin-bottom: 0;
|
|
117
117
|
}
|
|
@@ -119,17 +119,17 @@ class IdPickerScreen(ModalScreen[str]):
|
|
|
119
119
|
#idpicker-table {
|
|
120
120
|
height: 1fr;
|
|
121
121
|
width: 100%;
|
|
122
|
-
background: $
|
|
122
|
+
background: $primary-background;
|
|
123
123
|
/* Slightly dim the row text so it's easier on the eyes. */
|
|
124
124
|
color: $foreground 80%;
|
|
125
125
|
border: none;
|
|
126
126
|
overflow-x: auto;
|
|
127
127
|
overflow-y: auto;
|
|
128
128
|
scrollbar-gutter: auto;
|
|
129
|
-
scrollbar-background: $
|
|
130
|
-
scrollbar-background-hover: $
|
|
131
|
-
scrollbar-background-active: $
|
|
132
|
-
scrollbar-corner-color: $
|
|
129
|
+
scrollbar-background: $primary-background;
|
|
130
|
+
scrollbar-background-hover: $primary-background;
|
|
131
|
+
scrollbar-background-active: $primary-background;
|
|
132
|
+
scrollbar-corner-color: $primary-background;
|
|
133
133
|
scrollbar-color: $panel;
|
|
134
134
|
scrollbar-color-hover: $primary;
|
|
135
135
|
scrollbar-color-active: $primary;
|
|
@@ -163,10 +163,10 @@ class IdPickerScreen(ModalScreen[str]):
|
|
|
163
163
|
overflow-x: auto;
|
|
164
164
|
overflow-y: auto;
|
|
165
165
|
scrollbar-gutter: auto;
|
|
166
|
-
scrollbar-background: $
|
|
167
|
-
scrollbar-background-hover: $
|
|
168
|
-
scrollbar-background-active: $
|
|
169
|
-
scrollbar-corner-color: $
|
|
166
|
+
scrollbar-background: $primary-background;
|
|
167
|
+
scrollbar-background-hover: $primary-background;
|
|
168
|
+
scrollbar-background-active: $primary-background;
|
|
169
|
+
scrollbar-corner-color: $primary-background;
|
|
170
170
|
scrollbar-color: $panel;
|
|
171
171
|
scrollbar-color-hover: $primary;
|
|
172
172
|
scrollbar-color-active: $primary;
|