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.
Files changed (57) hide show
  1. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/PKG-INFO +1 -1
  2. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/pyproject.toml +1 -1
  3. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/main.py +54 -4
  4. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/dashboard.py +239 -62
  5. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/uv.lock +1 -1
  6. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.github/workflows/publish.yml +0 -0
  7. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.github/workflows/test.yml +0 -0
  8. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.gitignore +0 -0
  9. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/.python-version +0 -0
  10. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/CLAUDE.md +0 -0
  11. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/Makefile +0 -0
  12. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/README.md +0 -0
  13. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/__init__.py +0 -0
  14. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/auth.py +0 -0
  15. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/cli.py +0 -0
  16. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/client.py +0 -0
  17. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/commands.py +0 -0
  18. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/logger.py +0 -0
  19. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/models.py +0 -0
  20. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/runtime_manager.py +0 -0
  21. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/server.py +0 -0
  22. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_cli/terminal_mode.py +0 -0
  23. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/__init__.py +0 -0
  24. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/__main__.py +0 -0
  25. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/main.py +0 -0
  26. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/snake_game/.gitignore +0 -0
  27. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_runtime/snake_game/requirements.txt +0 -0
  28. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/__init__.py +0 -0
  29. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/inline_file_picker.py +0 -0
  30. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/random_words.py +0 -0
  31. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/runtime_agent.py +0 -0
  32. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/__init__.py +0 -0
  33. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/attach_content.py +0 -0
  34. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/command_result.py +0 -0
  35. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/file_browser.py +0 -0
  36. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/help.py +0 -0
  37. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/id_picker.py +0 -0
  38. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/login.py +0 -0
  39. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/runtime_cleanup.py +0 -0
  40. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  41. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/screens/slash_picker.py +0 -0
  42. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/slash_commands.py +0 -0
  43. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/status_words.py +0 -0
  44. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/src/kiwi_tui/widgets.py +0 -0
  45. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/test_hello.py +0 -0
  46. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/__init__.py +0 -0
  47. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/conftest.py +0 -0
  48. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_cli_help.py +0 -0
  49. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_imports.py +0 -0
  50. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_reexec_kiwi.py +0 -0
  51. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_runtime_log_trimming.py +0 -0
  52. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_slash_commands.py +0 -0
  53. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_terminal_mode.py +0 -0
  54. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tokens.py +0 -0
  55. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_headless.py +0 -0
  56. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_interactive_runtime.py +0 -0
  57. {kiwi_code-0.0.41 → kiwi_code-0.0.42}/tests/test_tui_palette.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.41
3
+ Version: 0.0.42
4
4
  Summary: A textual-based terminal user interface application
5
5
  Project-URL: Homepage, https://meetkiwi.ai
6
6
  Project-URL: Repository, https://github.com/jetoslabs/kiwi-code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.41"
3
+ version = "0.0.42"
4
4
  description = "A textual-based terminal user interface application"
5
5
  readme = {file = "README.md", content-type = "text/markdown"}
6
6
  requires-python = ">=3.11,<4.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 only when needed.
613
+ """Synchronize auth state from disk and refresh when needed.
614
614
 
615
615
  Args:
616
- force: If True, re-check disk state immediately and refresh only if
617
- the shared on-disk access token is still expired.
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
- try:
618
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
619
- except Exception:
620
- refreshed = False
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
- try:
1265
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1266
- except Exception:
1267
- refreshed = False
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
- try:
1313
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1314
- except Exception:
1315
- refreshed = False
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
- try:
1381
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1382
- except Exception:
1383
- refreshed = False
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
- try:
1450
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1451
- except Exception:
1452
- refreshed = False
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
- try:
1508
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1509
- except Exception:
1510
- refreshed = False
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
- try:
1577
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1578
- except Exception:
1579
- refreshed = False
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 = False
1682
- try:
1683
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
1684
- except Exception:
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
- success, urls, message = await asyncio.to_thread(
2586
- lambda: self.app.autobots_client.upload_files(file_paths)
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
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
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
- refreshed = False
2787
- if refreshed:
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
- success, run_id, message = await loop.run_in_executor(
2790
- None,
2791
- lambda: client.run_action_async(
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
- try:
2948
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
2949
- except Exception:
2950
- refreshed = False
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
- if (not success) and (code in (401, 403)):
2956
- auth_failed_code = code
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.add_message(
2964
- f"Authentication error while fetching result (HTTP {auth_failed_code}). Please /login again.",
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
- try:
3275
- refreshed = bool(self.app._refresh_token_if_needed(force=True))
3276
- except Exception:
3277
- refreshed = False
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)
@@ -397,7 +397,7 @@ wheels = [
397
397
 
398
398
  [[package]]
399
399
  name = "kiwi-code"
400
- version = "0.0.41"
400
+ version = "0.0.42"
401
401
  source = { editable = "." }
402
402
  dependencies = [
403
403
  { name = "autobots-client" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes