kiwi-code 0.0.41__tar.gz → 0.0.42__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.41 → kiwi_code-0.0.42}/PKG-INFO +1 -1
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/pyproject.toml +1 -1
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/main.py +54 -4
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/dashboard.py +239 -62
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/uv.lock +1 -1
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.github/workflows/test.yml +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.gitignore +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.python-version +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/CLAUDE.md +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/Makefile +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/README.md +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/cli.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/models.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/server.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/terminal_mode.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/main.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/snake_game/.gitignore +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/inline_file_picker.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/random_words.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/runtime_agent.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/__init__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/attach_content.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/command_result.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/help.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/id_picker.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/slash_picker.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/slash_commands.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/status_words.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/widgets.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/test_hello.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/__init__.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/conftest.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_cli_help.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_imports.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_reexec_kiwi.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_runtime_log_trimming.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_slash_commands.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_terminal_mode.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tokens.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_headless.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_interactive_runtime.py +0 -0
- {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_palette.py +0 -0
|
@@ -610,11 +610,12 @@ class AutobotsTUI(App):
|
|
|
610
610
|
# runtime_manager.save_refresh_token(tokens.refresh_token)
|
|
611
611
|
|
|
612
612
|
def _refresh_token_if_needed(self, force: bool = False) -> bool:
|
|
613
|
-
"""Synchronize auth state from disk and refresh
|
|
613
|
+
"""Synchronize auth state from disk and refresh when needed.
|
|
614
614
|
|
|
615
615
|
Args:
|
|
616
|
-
force: If True,
|
|
617
|
-
the
|
|
616
|
+
force: If True, attempt a refresh-token exchange immediately even if
|
|
617
|
+
the current access token does not appear expired on disk. This is
|
|
618
|
+
used after an HTTP 401/403 from the server.
|
|
618
619
|
"""
|
|
619
620
|
with self.token_manager.file_lock():
|
|
620
621
|
tokens = self.token_manager.load_tokens()
|
|
@@ -622,7 +623,7 @@ class AutobotsTUI(App):
|
|
|
622
623
|
logger.debug("No tokens to refresh")
|
|
623
624
|
return False
|
|
624
625
|
|
|
625
|
-
if not tokens.is_expired():
|
|
626
|
+
if not force and not tokens.is_expired():
|
|
626
627
|
logger.debug("Token on disk is valid; synchronizing in-memory auth state")
|
|
627
628
|
if getattr(tokens, "access_token", None):
|
|
628
629
|
self.autobots_client.update_token(tokens.access_token)
|
|
@@ -683,6 +684,55 @@ class AutobotsTUI(App):
|
|
|
683
684
|
except Exception:
|
|
684
685
|
pass
|
|
685
686
|
|
|
687
|
+
def _handle_auth_expired(self, reason: str | None = None) -> None:
|
|
688
|
+
"""Clear auth state and return the user to the login screen.
|
|
689
|
+
|
|
690
|
+
Used when the server rejects the current session (401/403) and a forced
|
|
691
|
+
token refresh cannot recover it.
|
|
692
|
+
"""
|
|
693
|
+
logger.warning(f"Authentication expired; redirecting to login. Reason: {reason or 'unknown'}")
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
screen = self.screen
|
|
697
|
+
if hasattr(screen, "reset_for_auth_expired"):
|
|
698
|
+
screen.reset_for_auth_expired()
|
|
699
|
+
except Exception:
|
|
700
|
+
pass
|
|
701
|
+
|
|
702
|
+
timer = getattr(self, "_token_refresh_timer", None)
|
|
703
|
+
if timer is not None:
|
|
704
|
+
try:
|
|
705
|
+
timer.stop()
|
|
706
|
+
except Exception:
|
|
707
|
+
pass
|
|
708
|
+
self._token_refresh_timer = None
|
|
709
|
+
|
|
710
|
+
self.pending_runtime_id = None
|
|
711
|
+
self.token_manager.clear_tokens()
|
|
712
|
+
self._kiwi_token = None
|
|
713
|
+
|
|
714
|
+
self.autobots_client = AutobotsClientWrapper(
|
|
715
|
+
base_url=self.config.backend_url,
|
|
716
|
+
api_key=self.config.api_key,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
already_on_login = type(self.screen).__name__ == "LoginScreen"
|
|
721
|
+
except Exception:
|
|
722
|
+
already_on_login = False
|
|
723
|
+
|
|
724
|
+
if not already_on_login:
|
|
725
|
+
try:
|
|
726
|
+
self.switch_screen("login")
|
|
727
|
+
except IndexError:
|
|
728
|
+
self.push_screen("login")
|
|
729
|
+
|
|
730
|
+
self.notify(
|
|
731
|
+
reason or "Your session expired. Please log in again.",
|
|
732
|
+
severity="error",
|
|
733
|
+
title="Authentication required",
|
|
734
|
+
)
|
|
735
|
+
|
|
686
736
|
def action_logout(self) -> None:
|
|
687
737
|
"""Logout user and return to login screen."""
|
|
688
738
|
logger.info("User logout requested")
|
|
@@ -600,6 +600,27 @@ class DashboardScreen(Screen):
|
|
|
600
600
|
action_w.update(f"Action: {action_label}")
|
|
601
601
|
run_w.update(f"Run: {run_id}")
|
|
602
602
|
|
|
603
|
+
|
|
604
|
+
def _refresh_auth_or_redirect(self, code: int | None, *, context: str) -> bool:
|
|
605
|
+
"""Try one forced auth refresh after a 401/403.
|
|
606
|
+
|
|
607
|
+
Returns True only when a forced refresh succeeded and the caller should
|
|
608
|
+
retry the original request once. If refresh itself fails, auth is already
|
|
609
|
+
unrecoverable for this request and the user is redirected to login.
|
|
610
|
+
"""
|
|
611
|
+
if code not in (401, 403):
|
|
612
|
+
return False
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
refreshed = bool(self.app._refresh_token_if_needed(force=True))
|
|
616
|
+
except Exception:
|
|
617
|
+
refreshed = False
|
|
618
|
+
|
|
619
|
+
if not refreshed:
|
|
620
|
+
self.app._handle_auth_expired(
|
|
621
|
+
f"Authentication expired while {context} (HTTP {code}). Please log in again."
|
|
622
|
+
)
|
|
623
|
+
return refreshed
|
|
603
624
|
async def _refresh_current_action_name_async(self) -> None:
|
|
604
625
|
"""Best-effort fetch of the current action name for the status bar."""
|
|
605
626
|
from autobots_client.api.actions import get_action_v1_actions_id_get
|
|
@@ -614,13 +635,18 @@ class DashboardScreen(Screen):
|
|
|
614
635
|
|
|
615
636
|
resp = await asyncio.to_thread(lambda: _fetch(api_client))
|
|
616
637
|
if resp.status_code in (401, 403):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
638
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
639
|
+
resp.status_code,
|
|
640
|
+
context="refreshing the current action name",
|
|
641
|
+
)
|
|
621
642
|
if refreshed:
|
|
622
643
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
623
644
|
resp = await asyncio.to_thread(lambda: _fetch(api_client))
|
|
645
|
+
if resp.status_code in (401, 403):
|
|
646
|
+
self.app._handle_auth_expired(
|
|
647
|
+
f"Authentication expired while refreshing the current action name (HTTP {resp.status_code}). Please log in again."
|
|
648
|
+
)
|
|
649
|
+
return
|
|
624
650
|
|
|
625
651
|
if requested_action_id != self.current_action_id:
|
|
626
652
|
return
|
|
@@ -1261,14 +1287,27 @@ class DashboardScreen(Screen):
|
|
|
1261
1287
|
except Exception:
|
|
1262
1288
|
code = None
|
|
1263
1289
|
if code in (401, 403):
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1290
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1291
|
+
code,
|
|
1292
|
+
context="loading conversation history",
|
|
1293
|
+
)
|
|
1268
1294
|
if refreshed:
|
|
1269
1295
|
success, result, msg = await asyncio.to_thread(
|
|
1270
1296
|
lambda: self.app.autobots_client.get_action_result(run_id)
|
|
1271
1297
|
)
|
|
1298
|
+
if (not success) and msg:
|
|
1299
|
+
retry_code = None
|
|
1300
|
+
try:
|
|
1301
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(msg))
|
|
1302
|
+
if m:
|
|
1303
|
+
retry_code = int(m.group(1))
|
|
1304
|
+
except Exception:
|
|
1305
|
+
retry_code = None
|
|
1306
|
+
if retry_code in (401, 403):
|
|
1307
|
+
self.app._handle_auth_expired(
|
|
1308
|
+
f"Authentication expired while loading conversation history (HTTP {retry_code}). Please log in again."
|
|
1309
|
+
)
|
|
1310
|
+
return
|
|
1272
1311
|
if not success or not result:
|
|
1273
1312
|
return
|
|
1274
1313
|
self._update_action_context_from_action_result(result)
|
|
@@ -1309,10 +1348,10 @@ class DashboardScreen(Screen):
|
|
|
1309
1348
|
for action_id in self.AUTOCODE_ACTION_IDS
|
|
1310
1349
|
]
|
|
1311
1350
|
if any(resp.status_code in (401, 403) for _, resp in responses):
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1351
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1352
|
+
401,
|
|
1353
|
+
context="loading the available AutoCode actions",
|
|
1354
|
+
)
|
|
1316
1355
|
if refreshed:
|
|
1317
1356
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1318
1357
|
|
|
@@ -1325,6 +1364,11 @@ class DashboardScreen(Screen):
|
|
|
1325
1364
|
(action_id, await _fetch_action_refreshed(action_id))
|
|
1326
1365
|
for action_id in self.AUTOCODE_ACTION_IDS
|
|
1327
1366
|
]
|
|
1367
|
+
if any(resp.status_code in (401, 403) for _, resp in responses):
|
|
1368
|
+
self.app._handle_auth_expired(
|
|
1369
|
+
"Authentication expired while loading the available AutoCode actions. Please log in again."
|
|
1370
|
+
)
|
|
1371
|
+
return
|
|
1328
1372
|
|
|
1329
1373
|
rows: list[tuple[str, list[str]]] = []
|
|
1330
1374
|
unavailable: list[str] = []
|
|
@@ -1377,10 +1421,10 @@ class DashboardScreen(Screen):
|
|
|
1377
1421
|
)
|
|
1378
1422
|
if resp.status_code in (401, 403):
|
|
1379
1423
|
# Reactive auth guard: force refresh and retry once.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1424
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1425
|
+
resp.status_code,
|
|
1426
|
+
context="listing actions",
|
|
1427
|
+
)
|
|
1384
1428
|
if refreshed:
|
|
1385
1429
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1386
1430
|
resp = await asyncio.to_thread(
|
|
@@ -1391,6 +1435,11 @@ class DashboardScreen(Screen):
|
|
|
1391
1435
|
offset=offset,
|
|
1392
1436
|
)
|
|
1393
1437
|
)
|
|
1438
|
+
if resp.status_code in (401, 403):
|
|
1439
|
+
self.app._handle_auth_expired(
|
|
1440
|
+
f"Authentication expired while listing actions (HTTP {resp.status_code}). Please log in again."
|
|
1441
|
+
)
|
|
1442
|
+
return
|
|
1394
1443
|
if resp.status_code != 200 or not resp.parsed:
|
|
1395
1444
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1396
1445
|
return
|
|
@@ -1446,10 +1495,10 @@ class DashboardScreen(Screen):
|
|
|
1446
1495
|
)
|
|
1447
1496
|
)
|
|
1448
1497
|
if resp.status_code in (401, 403):
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1498
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1499
|
+
resp.status_code,
|
|
1500
|
+
context="listing runs",
|
|
1501
|
+
)
|
|
1453
1502
|
if refreshed:
|
|
1454
1503
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1455
1504
|
resp = await asyncio.to_thread(
|
|
@@ -1462,6 +1511,11 @@ class DashboardScreen(Screen):
|
|
|
1462
1511
|
offset=int(opts.get("offset", "0")),
|
|
1463
1512
|
)
|
|
1464
1513
|
)
|
|
1514
|
+
if resp.status_code in (401, 403):
|
|
1515
|
+
self.app._handle_auth_expired(
|
|
1516
|
+
f"Authentication expired while listing runs (HTTP {resp.status_code}). Please log in again."
|
|
1517
|
+
)
|
|
1518
|
+
return
|
|
1465
1519
|
if resp.status_code != 200 or not resp.parsed:
|
|
1466
1520
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1467
1521
|
return
|
|
@@ -1504,10 +1558,10 @@ class DashboardScreen(Screen):
|
|
|
1504
1558
|
)
|
|
1505
1559
|
)
|
|
1506
1560
|
if resp.status_code in (401, 403):
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1561
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1562
|
+
resp.status_code,
|
|
1563
|
+
context="listing graphs",
|
|
1564
|
+
)
|
|
1511
1565
|
if refreshed:
|
|
1512
1566
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1513
1567
|
resp = await asyncio.to_thread(
|
|
@@ -1518,6 +1572,11 @@ class DashboardScreen(Screen):
|
|
|
1518
1572
|
offset=offset,
|
|
1519
1573
|
)
|
|
1520
1574
|
)
|
|
1575
|
+
if resp.status_code in (401, 403):
|
|
1576
|
+
self.app._handle_auth_expired(
|
|
1577
|
+
f"Authentication expired while listing graphs (HTTP {resp.status_code}). Please log in again."
|
|
1578
|
+
)
|
|
1579
|
+
return
|
|
1521
1580
|
if resp.status_code != 200 or not resp.parsed:
|
|
1522
1581
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1523
1582
|
return
|
|
@@ -1573,10 +1632,10 @@ class DashboardScreen(Screen):
|
|
|
1573
1632
|
)
|
|
1574
1633
|
)
|
|
1575
1634
|
if resp.status_code in (401, 403):
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1635
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1636
|
+
resp.status_code,
|
|
1637
|
+
context="listing graph runs",
|
|
1638
|
+
)
|
|
1580
1639
|
if refreshed:
|
|
1581
1640
|
api_client = getattr(getattr(self.app, "autobots_client", None), "client", api_client)
|
|
1582
1641
|
resp = await asyncio.to_thread(
|
|
@@ -1589,6 +1648,11 @@ class DashboardScreen(Screen):
|
|
|
1589
1648
|
offset=int(opts.get("offset", "0")),
|
|
1590
1649
|
)
|
|
1591
1650
|
)
|
|
1651
|
+
if resp.status_code in (401, 403):
|
|
1652
|
+
self.app._handle_auth_expired(
|
|
1653
|
+
f"Authentication expired while listing graph runs (HTTP {resp.status_code}). Please log in again."
|
|
1654
|
+
)
|
|
1655
|
+
return
|
|
1592
1656
|
if resp.status_code != 200 or not resp.parsed:
|
|
1593
1657
|
self._show_command_result(command, f"Error: HTTP {resp.status_code}", is_error=True)
|
|
1594
1658
|
return
|
|
@@ -1678,16 +1742,21 @@ class DashboardScreen(Screen):
|
|
|
1678
1742
|
lines = await asyncio.to_thread(lambda: fn(api_client))
|
|
1679
1743
|
code = self._http_status_from_lines(lines)
|
|
1680
1744
|
if code in (401, 403):
|
|
1681
|
-
refreshed =
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
refreshed = False
|
|
1745
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
1746
|
+
code,
|
|
1747
|
+
context=f"running {title}",
|
|
1748
|
+
)
|
|
1686
1749
|
if refreshed:
|
|
1687
1750
|
api_client = self._get_api_client_for_command(title)
|
|
1688
1751
|
if not api_client:
|
|
1689
1752
|
return lines
|
|
1690
1753
|
lines = await asyncio.to_thread(lambda: fn(api_client))
|
|
1754
|
+
retry_code = self._http_status_from_lines(lines)
|
|
1755
|
+
if retry_code in (401, 403):
|
|
1756
|
+
self.app._handle_auth_expired(
|
|
1757
|
+
f"Authentication expired while running {title} (HTTP {retry_code}). Please log in again."
|
|
1758
|
+
)
|
|
1759
|
+
return lines
|
|
1691
1760
|
return lines
|
|
1692
1761
|
|
|
1693
1762
|
def _parse_command_opts(self, command: str, skip: int = 2) -> dict[str, str]:
|
|
@@ -2282,6 +2351,50 @@ class DashboardScreen(Screen):
|
|
|
2282
2351
|
pass
|
|
2283
2352
|
messages.scroll_end(animate=False)
|
|
2284
2353
|
|
|
2354
|
+
|
|
2355
|
+
def reset_for_auth_expired(self) -> None:
|
|
2356
|
+
"""Clear transient dashboard state before redirecting to login."""
|
|
2357
|
+
try:
|
|
2358
|
+
for worker in self.workers:
|
|
2359
|
+
if not worker.is_finished:
|
|
2360
|
+
worker.cancel()
|
|
2361
|
+
except Exception:
|
|
2362
|
+
pass
|
|
2363
|
+
|
|
2364
|
+
try:
|
|
2365
|
+
self._set_streaming(False)
|
|
2366
|
+
except Exception:
|
|
2367
|
+
pass
|
|
2368
|
+
|
|
2369
|
+
try:
|
|
2370
|
+
w = getattr(self, "_streaming_widget_ref", None)
|
|
2371
|
+
if w:
|
|
2372
|
+
w.remove()
|
|
2373
|
+
except Exception:
|
|
2374
|
+
pass
|
|
2375
|
+
self._streaming_widget_ref = None
|
|
2376
|
+
|
|
2377
|
+
self.current_run_id = None
|
|
2378
|
+
self.current_run_kind = None
|
|
2379
|
+
self._pending_urls.clear()
|
|
2380
|
+
self._upload_status_text = ""
|
|
2381
|
+
try:
|
|
2382
|
+
self._update_pending_files_bar()
|
|
2383
|
+
except Exception:
|
|
2384
|
+
pass
|
|
2385
|
+
try:
|
|
2386
|
+
self._update_run_status_bar()
|
|
2387
|
+
except Exception:
|
|
2388
|
+
pass
|
|
2389
|
+
|
|
2390
|
+
try:
|
|
2391
|
+
chat_input = self.query_one("#chat-input", ChatInput)
|
|
2392
|
+
chat_input.value = ""
|
|
2393
|
+
chat_input.focus()
|
|
2394
|
+
except Exception:
|
|
2395
|
+
pass
|
|
2396
|
+
|
|
2397
|
+
self._clear_chat_messages()
|
|
2285
2398
|
def add_message(self, text: str, msg_type: str = "assistant") -> None:
|
|
2286
2399
|
"""Add a message to the chat."""
|
|
2287
2400
|
messages = self.query_one("#messages", VerticalScroll)
|
|
@@ -2581,10 +2694,21 @@ class DashboardScreen(Screen):
|
|
|
2581
2694
|
refreshed = bool(await asyncio.to_thread(self.app._refresh_token_if_needed, True))
|
|
2582
2695
|
except Exception:
|
|
2583
2696
|
refreshed = False
|
|
2584
|
-
if refreshed:
|
|
2585
|
-
|
|
2586
|
-
|
|
2697
|
+
if not refreshed:
|
|
2698
|
+
self.app._handle_auth_expired(
|
|
2699
|
+
"Authentication expired while uploading files. Please log in again."
|
|
2700
|
+
)
|
|
2701
|
+
self._set_upload_status("")
|
|
2702
|
+
return
|
|
2703
|
+
success, urls, message = await asyncio.to_thread(
|
|
2704
|
+
lambda: self.app.autobots_client.upload_files(file_paths)
|
|
2705
|
+
)
|
|
2706
|
+
if (not success) and ("HTTP 401" in message or "HTTP 403" in message):
|
|
2707
|
+
self.app._handle_auth_expired(
|
|
2708
|
+
"Authentication expired while uploading files. Please log in again."
|
|
2587
2709
|
)
|
|
2710
|
+
self._set_upload_status("")
|
|
2711
|
+
return
|
|
2588
2712
|
|
|
2589
2713
|
if success:
|
|
2590
2714
|
self._pending_urls.extend(urls)
|
|
@@ -2779,25 +2903,58 @@ class DashboardScreen(Screen):
|
|
|
2779
2903
|
except Exception:
|
|
2780
2904
|
code = None
|
|
2781
2905
|
|
|
2906
|
+
retried_after_auth = False
|
|
2782
2907
|
if code in (401, 403):
|
|
2908
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
2909
|
+
code,
|
|
2910
|
+
context="starting an action",
|
|
2911
|
+
)
|
|
2912
|
+
if not refreshed:
|
|
2913
|
+
try:
|
|
2914
|
+
w = getattr(self, "_streaming_widget_ref", None)
|
|
2915
|
+
if w:
|
|
2916
|
+
w.remove()
|
|
2917
|
+
except Exception:
|
|
2918
|
+
pass
|
|
2919
|
+
self._streaming_widget_ref = None
|
|
2920
|
+
self._set_streaming(False)
|
|
2921
|
+
return
|
|
2922
|
+
retried_after_auth = True
|
|
2783
2923
|
try:
|
|
2784
|
-
|
|
2924
|
+
success, run_id, message = await loop.run_in_executor(
|
|
2925
|
+
None,
|
|
2926
|
+
lambda: client.run_action_async(
|
|
2927
|
+
self.current_action_id,
|
|
2928
|
+
user_input,
|
|
2929
|
+
action_result_id=self.current_run_id,
|
|
2930
|
+
urls=urls if urls else None,
|
|
2931
|
+
metadata=self._metadata if self._metadata else None,
|
|
2932
|
+
),
|
|
2933
|
+
)
|
|
2785
2934
|
except Exception:
|
|
2786
|
-
|
|
2787
|
-
|
|
2935
|
+
pass
|
|
2936
|
+
|
|
2937
|
+
if retried_after_auth and not success:
|
|
2938
|
+
retry_code = None
|
|
2939
|
+
try:
|
|
2940
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(message or ""))
|
|
2941
|
+
if m:
|
|
2942
|
+
retry_code = int(m.group(1))
|
|
2943
|
+
except Exception:
|
|
2944
|
+
retry_code = None
|
|
2945
|
+
if retry_code in (401, 403):
|
|
2946
|
+
self.app._handle_auth_expired(
|
|
2947
|
+
f"Authentication expired while starting an action (HTTP {retry_code}). Please log in again."
|
|
2948
|
+
)
|
|
2788
2949
|
try:
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
self.current_action_id,
|
|
2793
|
-
user_input,
|
|
2794
|
-
action_result_id=self.current_run_id,
|
|
2795
|
-
urls=urls if urls else None,
|
|
2796
|
-
metadata=self._metadata if self._metadata else None,
|
|
2797
|
-
),
|
|
2798
|
-
)
|
|
2950
|
+
w = getattr(self, "_streaming_widget_ref", None)
|
|
2951
|
+
if w:
|
|
2952
|
+
w.remove()
|
|
2799
2953
|
except Exception:
|
|
2800
2954
|
pass
|
|
2955
|
+
self._streaming_widget_ref = None
|
|
2956
|
+
self._set_streaming(False)
|
|
2957
|
+
return
|
|
2801
2958
|
|
|
2802
2959
|
if not success:
|
|
2803
2960
|
self.add_message(f"Error starting action: {message}", "error")
|
|
@@ -2944,25 +3101,32 @@ class DashboardScreen(Screen):
|
|
|
2944
3101
|
code = None
|
|
2945
3102
|
|
|
2946
3103
|
if code in (401, 403):
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3104
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
3105
|
+
code,
|
|
3106
|
+
context="fetching the final action result",
|
|
3107
|
+
)
|
|
2951
3108
|
if refreshed:
|
|
2952
3109
|
success, final_result, message = await asyncio.to_thread(
|
|
2953
3110
|
client.get_action_result, run_id
|
|
2954
3111
|
)
|
|
2955
|
-
|
|
2956
|
-
|
|
3112
|
+
if not success:
|
|
3113
|
+
retry_code = None
|
|
3114
|
+
try:
|
|
3115
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(message or ""))
|
|
3116
|
+
if m:
|
|
3117
|
+
retry_code = int(m.group(1))
|
|
3118
|
+
except Exception:
|
|
3119
|
+
retry_code = None
|
|
3120
|
+
if retry_code in (401, 403):
|
|
3121
|
+
auth_failed_code = retry_code
|
|
2957
3122
|
except Exception as e:
|
|
2958
3123
|
logger.warning(f"Poll error: {e}")
|
|
2959
3124
|
return False
|
|
2960
3125
|
|
|
2961
3126
|
|
|
2962
3127
|
if auth_failed_code in (401, 403):
|
|
2963
|
-
self.
|
|
2964
|
-
f"Authentication
|
|
2965
|
-
"error",
|
|
3128
|
+
self.app._handle_auth_expired(
|
|
3129
|
+
f"Authentication expired while fetching the result (HTTP {auth_failed_code}). Please log in again."
|
|
2966
3130
|
)
|
|
2967
3131
|
got_final_result = True
|
|
2968
3132
|
# Stop spinner/blink immediately when we have a terminal state.
|
|
@@ -3271,12 +3435,25 @@ class DashboardScreen(Screen):
|
|
|
3271
3435
|
except Exception:
|
|
3272
3436
|
code = None
|
|
3273
3437
|
if code in (401, 403):
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3438
|
+
refreshed = self._refresh_auth_or_redirect(
|
|
3439
|
+
code,
|
|
3440
|
+
context="refreshing displayed conversation history",
|
|
3441
|
+
)
|
|
3278
3442
|
if refreshed:
|
|
3279
3443
|
success, result, msg = self.app.autobots_client.get_action_result(run_id)
|
|
3444
|
+
if (not success) and msg:
|
|
3445
|
+
retry_code = None
|
|
3446
|
+
try:
|
|
3447
|
+
m = re.search(r"(?:HTTP|status)\s*(\d{3})", str(msg))
|
|
3448
|
+
if m:
|
|
3449
|
+
retry_code = int(m.group(1))
|
|
3450
|
+
except Exception:
|
|
3451
|
+
retry_code = None
|
|
3452
|
+
if retry_code in (401, 403):
|
|
3453
|
+
self.app._handle_auth_expired(
|
|
3454
|
+
f"Authentication expired while refreshing displayed conversation history (HTTP {retry_code}). Please log in again."
|
|
3455
|
+
)
|
|
3456
|
+
return
|
|
3280
3457
|
if not success or not result:
|
|
3281
3458
|
return
|
|
3282
3459
|
self._render_conversation_history_from_action_result(result)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|