kiwi-code 0.0.7__tar.gz → 0.0.9__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 (33) hide show
  1. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/PKG-INFO +2 -1
  2. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/pyproject.toml +2 -1
  3. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/cli.py +2 -0
  4. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/models.py +1 -1
  5. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/main.py +271 -6
  6. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/main.py +4 -3
  7. kiwi_code-0.0.9/src/kiwi_tui/screens/__init__.py +7 -0
  8. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/dashboard.py +11 -0
  9. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/widgets.py +2 -0
  10. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/uv.lock +51 -1
  11. kiwi_code-0.0.7/src/kiwi_tui/screens/__init__.py +0 -9
  12. kiwi_code-0.0.7/src/kiwi_tui/screens/actions.py +0 -271
  13. kiwi_code-0.0.7/src/kiwi_tui/screens/autobots.py +0 -216
  14. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.github/workflows/publish.yml +0 -0
  15. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.gitignore +0 -0
  16. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.python-version +0 -0
  17. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/CLAUDE.md +0 -0
  18. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/Makefile +0 -0
  19. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/README.md +0 -0
  20. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/__init__.py +0 -0
  21. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/auth.py +0 -0
  22. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/client.py +0 -0
  23. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/commands.py +0 -0
  24. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/config.py +0 -0
  25. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/logger.py +0 -0
  26. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/runtime_manager.py +0 -0
  27. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/__init__.py +0 -0
  28. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/__main__.py +0 -0
  29. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/__init__.py +0 -0
  30. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/file_browser.py +0 -0
  31. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/login.py +0 -0
  32. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/runtime_logs.py +0 -0
  33. {kiwi_code-0.0.7 → kiwi_code-0.0.9}/test_hello.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kiwi-code
3
- Version: 0.0.7
3
+ Version: 0.0.9
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
@@ -18,6 +18,7 @@ Requires-Dist: httpx>=0.25.0
18
18
  Requires-Dist: loguru>=0.7.3
19
19
  Requires-Dist: psutil>=5.9.0
20
20
  Requires-Dist: pydantic>=2.12.5
21
+ Requires-Dist: setproctitle>=1.3.0
21
22
  Requires-Dist: textual-dev>=1.8.0
22
23
  Requires-Dist: textual>=8.1.1
23
24
  Requires-Dist: typer>=0.24.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kiwi-code"
3
- version = "0.0.7"
3
+ version = "0.0.9"
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.13,<4.0"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "websockets>=14.1",
15
15
  "httpx>=0.25.0",
16
16
  "psutil>=5.9.0",
17
+ "setproctitle>=1.3.0",
17
18
  ]
18
19
  authors = [
19
20
  { name = "Anurag Jha", email = "anurag@meetkiwi.co" }
@@ -237,6 +237,8 @@ def tui():
237
237
 
238
238
  def cli():
239
239
  """Entry point for the CLI."""
240
+ import setproctitle
241
+ setproctitle.setproctitle("kiwicli")
240
242
  app()
241
243
 
242
244
  if __name__ == "__main__":
@@ -78,7 +78,7 @@ class LoginCredentials(BaseModel):
78
78
 
79
79
  class AppConfig(BaseModel):
80
80
  """Application configuration."""
81
- backend_url: str = "https://dev.api.myautobots.com"#"https://api.meetkiwi.ai"
81
+ backend_url: str = "https://api.meetkiwi.ai"
82
82
  api_key: Optional[str] = None
83
83
  log_level: str = "INFO"
84
84
  theme: str = "dark"
@@ -610,6 +610,229 @@ if not IS_WINDOWS:
610
610
  self.slave_fd = None
611
611
 
612
612
 
613
+ class PersistentShell:
614
+ """A long-lived bash PTY that persists across commands.
615
+
616
+ All one-shot ``command`` messages are routed through the same shell so
617
+ that ``cd``, environment variables, virtualenv activations, aliases, and
618
+ other shell state carry over between commands. The server still
619
+ receives ``{stdout, stderr, exit_code}`` — no backend changes needed.
620
+ """
621
+
622
+ _SHELL_SENTINEL = "__KIWI_SHELL_DONE__"
623
+
624
+ def __init__(self, cwd: str | None = None, cols: int = 200, rows: int = 50):
625
+ self._cwd = cwd
626
+ self._cols = cols
627
+ self._rows = rows
628
+ self.master_fd: int | None = None
629
+ self._pid: int | None = None
630
+
631
+ # -- lifecycle --------------------------------------------------------
632
+
633
+ def start(self) -> None:
634
+ """Spawn a bash PTY that stays alive for the runtime's lifetime."""
635
+ master_fd, slave_fd = pty.openpty()
636
+
637
+ # Set terminal size
638
+ winsize = struct.pack("HHHH", self._rows, self._cols, 0, 0)
639
+ fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, winsize)
640
+
641
+ pid = os.fork()
642
+ if pid == 0:
643
+ # Child — become session leader and exec bash
644
+ os.setsid()
645
+ os.dup2(slave_fd, 0)
646
+ os.dup2(slave_fd, 1)
647
+ os.dup2(slave_fd, 2)
648
+ if slave_fd > 2:
649
+ os.close(slave_fd)
650
+ os.close(master_fd)
651
+ if self._cwd:
652
+ os.chdir(self._cwd)
653
+ os.execvp("bash", [
654
+ "bash", "--norc", "--noprofile", "-i",
655
+ ])
656
+ else:
657
+ # Parent
658
+ os.close(slave_fd)
659
+ self.master_fd = master_fd
660
+ self._pid = pid
661
+
662
+ # Set non-blocking reads
663
+ flags = fcntl.fcntl(master_fd, fcntl.F_GETFL)
664
+ fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
665
+
666
+ # Wait for bash to be ready, drain the initial prompt
667
+ import time
668
+ time.sleep(0.2)
669
+ self._drain()
670
+
671
+ # Disable echo and set a blank prompt so output is clean
672
+ self._write("stty -echo\n")
673
+ time.sleep(0.1)
674
+ self._drain()
675
+ self._write("PS1=''; PS2=''\n")
676
+ time.sleep(0.1)
677
+ self._drain()
678
+
679
+ print_status(">", "Persistent shell started (bash PTY)", GREEN)
680
+
681
+ def is_alive(self) -> bool:
682
+ """Check whether the shell process is still running."""
683
+ if self._pid is None:
684
+ return False
685
+ try:
686
+ pid, status = os.waitpid(self._pid, os.WNOHANG)
687
+ if pid != 0:
688
+ self._pid = None
689
+ return False
690
+ return True
691
+ except ChildProcessError:
692
+ self._pid = None
693
+ return False
694
+
695
+ def close(self) -> None:
696
+ """Terminate the persistent shell."""
697
+ if self.master_fd is not None:
698
+ try:
699
+ os.write(self.master_fd, b"exit\n")
700
+ except OSError:
701
+ pass
702
+ try:
703
+ os.close(self.master_fd)
704
+ except OSError:
705
+ pass
706
+ self.master_fd = None
707
+ if self._pid is not None:
708
+ try:
709
+ os.kill(self._pid, signal.SIGTERM)
710
+ os.waitpid(self._pid, 0)
711
+ except (ProcessLookupError, ChildProcessError):
712
+ pass
713
+ self._pid = None
714
+
715
+ # -- I/O helpers ------------------------------------------------------
716
+
717
+ def _write(self, data: str) -> None:
718
+ if self.master_fd is not None:
719
+ os.write(self.master_fd, data.encode("utf-8"))
720
+
721
+ def _drain(self) -> str:
722
+ """Read all currently available data from the PTY (non-blocking)."""
723
+ import select
724
+ chunks: list[str] = []
725
+ while True:
726
+ ready, _, _ = select.select([self.master_fd], [], [], 0.05)
727
+ if not ready:
728
+ break
729
+ try:
730
+ data = os.read(self.master_fd, 65536)
731
+ if not data:
732
+ break
733
+ chunks.append(data.decode("utf-8", errors="replace"))
734
+ except OSError:
735
+ break
736
+ return "".join(chunks)
737
+
738
+ # -- command execution ------------------------------------------------
739
+
740
+ async def run_command(self, command: str, timeout: int = 120) -> dict:
741
+ """Run a command in the persistent shell, returning {stdout, stderr, exit_code}.
742
+
743
+ Uses a sentinel pattern to detect when the command has finished.
744
+ """
745
+ if not self.is_alive():
746
+ return {"stdout": "", "stderr": "Persistent shell is not running", "exit_code": -1}
747
+
748
+ sentinel = self._SHELL_SENTINEL
749
+ # Wrap the command: run it, then echo sentinel+exit_code+sentinel
750
+ wrapped = (
751
+ f"{command}\n"
752
+ f"__kiwi_ec=$?; echo \"{sentinel}${{__kiwi_ec}}{sentinel}\"; "
753
+ f"echo \"{sentinel}${{__kiwi_ec}}{sentinel}\" >&2\n"
754
+ )
755
+
756
+ # Drain any leftover output
757
+ self._drain()
758
+
759
+ # Send the command
760
+ self._write(wrapped)
761
+
762
+ # Read output until we see the sentinel
763
+ import select
764
+ collected: list[str] = []
765
+ loop = asyncio.get_event_loop()
766
+
767
+ try:
768
+ output = await asyncio.wait_for(
769
+ loop.run_in_executor(None, self._read_until_sentinel, sentinel, timeout),
770
+ timeout=timeout + 5,
771
+ )
772
+ except (asyncio.TimeoutError, TimeoutError):
773
+ return {"stdout": "", "stderr": f"Command timed out after {timeout}s", "exit_code": -1}
774
+
775
+ return output
776
+
777
+ def _read_until_sentinel(self, sentinel: str, timeout: int) -> dict:
778
+ """Blocking read from PTY until sentinel is found. Returns parsed result."""
779
+ import select
780
+ import time
781
+
782
+ deadline = time.monotonic() + timeout
783
+ buf = ""
784
+
785
+ while time.monotonic() < deadline:
786
+ remaining = deadline - time.monotonic()
787
+ if remaining <= 0:
788
+ break
789
+
790
+ ready, _, _ = select.select(
791
+ [self.master_fd], [], [], min(0.1, remaining)
792
+ )
793
+ if ready:
794
+ try:
795
+ data = os.read(self.master_fd, 65536)
796
+ if not data:
797
+ break
798
+ buf += data.decode("utf-8", errors="replace")
799
+ except OSError:
800
+ break
801
+
802
+ # Check if sentinel appeared
803
+ sentinel_marker = sentinel
804
+ # We look for the pattern: sentinel + exit_code + sentinel
805
+ import re
806
+ pattern = re.escape(sentinel) + r"(\d+)" + re.escape(sentinel)
807
+ match = re.search(pattern, buf)
808
+ if match:
809
+ exit_code = int(match.group(1))
810
+ # Everything before the sentinel line is stdout
811
+ stdout = buf[:match.start()]
812
+ # Clean up: remove the sentinel echo command itself if visible
813
+ lines = stdout.splitlines(keepends=True)
814
+ clean_lines = []
815
+ for line in lines:
816
+ stripped = line.strip()
817
+ if sentinel in stripped:
818
+ continue
819
+ if stripped.startswith("__kiwi_ec="):
820
+ continue
821
+ clean_lines.append(line)
822
+ stdout = "".join(clean_lines).strip()
823
+
824
+ # Truncate if needed
825
+ stdout = stdout[:MAX_OUTPUT_BYTES]
826
+
827
+ return {
828
+ "stdout": stdout,
829
+ "stderr": "",
830
+ "exit_code": exit_code,
831
+ }
832
+
833
+ return {"stdout": buf[:MAX_OUTPUT_BYTES], "stderr": "Command timed out", "exit_code": -1}
834
+
835
+
613
836
  class PipeProcess:
614
837
  """Manages a persistent shell session using subprocess pipes (Windows-compatible)."""
615
838
 
@@ -777,6 +1000,7 @@ async def connect(
777
1000
  token: str,
778
1001
  mode: str = "restricted",
779
1002
  allowed_dirs: list[str] | None = None,
1003
+ shell: "PersistentShell | None" = None,
780
1004
  ) -> str:
781
1005
  """Connect to the server WebSocket and process commands.
782
1006
 
@@ -831,17 +1055,39 @@ async def connect(
831
1055
  command = msg.get("command", "")
832
1056
  print_cmd_log(request_id, f"$ {BOLD}{command}{RESET}")
833
1057
 
834
- result = await run_command(
835
- command,
836
- mode=mode,
837
- allowed_dirs=allowed_dirs,
838
- )
1058
+ # Validate in restricted mode before execution
1059
+ if mode == "restricted" and allowed_dirs:
1060
+ ok, reason = validate_command(
1061
+ command, allowed_dirs,
1062
+ pid=shell._pid if shell and shell.is_alive() else None,
1063
+ )
1064
+ if not ok:
1065
+ result = {"stdout": "", "stderr": reason, "exit_code": 1}
1066
+ elif shell and shell.is_alive():
1067
+ result = await shell.run_command(command)
1068
+ else:
1069
+ result = await run_command(
1070
+ command, mode=mode, allowed_dirs=allowed_dirs,
1071
+ )
1072
+ elif shell and shell.is_alive():
1073
+ result = await shell.run_command(command)
1074
+ else:
1075
+ result = await run_command(
1076
+ command, mode=mode, allowed_dirs=allowed_dirs,
1077
+ )
839
1078
  exit_code = result["exit_code"]
840
1079
  success = exit_code == 0
841
1080
  status_text = (
842
1081
  f"{GREEN}OK{RESET}" if success
843
1082
  else f"{RED}FAILED (exit {exit_code}){RESET}"
844
1083
  )
1084
+ # Print command output to runtime console
1085
+ if result.get("stdout"):
1086
+ for line in result["stdout"].splitlines():
1087
+ print_cmd_log(request_id, line)
1088
+ if result.get("stderr"):
1089
+ for line in result["stderr"].splitlines():
1090
+ print_cmd_log(request_id, f"{RED}{line}{RESET}")
845
1091
  print_cmd_log(request_id, status_text, success)
846
1092
 
847
1093
  await ws.send(json.dumps({
@@ -954,6 +1200,9 @@ async def connect(
954
1200
 
955
1201
 
956
1202
  def main():
1203
+ import setproctitle
1204
+ setproctitle.setproctitle("kiwi-runtime")
1205
+
957
1206
  parser = argparse.ArgumentParser(
958
1207
  description="Kiwi AI CLI Agent — execute terminal commands for LLM agents"
959
1208
  )
@@ -1084,6 +1333,18 @@ def main():
1084
1333
  loop.close()
1085
1334
  sys.exit(1)
1086
1335
 
1336
+ # Start persistent shell (Unix only) — survives reconnections
1337
+ persistent_shell = None
1338
+ if not IS_WINDOWS:
1339
+ print_section("Shell")
1340
+ try:
1341
+ cwd = allowed_dirs[0] if allowed_dirs else os.getcwd()
1342
+ persistent_shell = PersistentShell(cwd=cwd)
1343
+ persistent_shell.start()
1344
+ except Exception as e:
1345
+ print_status("!", f"Persistent shell failed, using one-shot mode: {e}", YELLOW)
1346
+ persistent_shell = None
1347
+
1087
1348
  print_section("Connection")
1088
1349
 
1089
1350
  _shutting_down = False
@@ -1115,7 +1376,7 @@ def main():
1115
1376
 
1116
1377
  try:
1117
1378
  status = loop.run_until_complete(
1118
- connect(ws_url, token, mode=mode, allowed_dirs=allowed_dirs)
1379
+ connect(ws_url, token, mode=mode, allowed_dirs=allowed_dirs, shell=persistent_shell)
1119
1380
  )
1120
1381
  except asyncio.CancelledError:
1121
1382
  break
@@ -1157,6 +1418,10 @@ def main():
1157
1418
  break
1158
1419
  backoff = min(backoff * 2, max_backoff)
1159
1420
 
1421
+ # Clean up persistent shell
1422
+ if persistent_shell:
1423
+ persistent_shell.close()
1424
+
1160
1425
  loop.close()
1161
1426
  print()
1162
1427
  print(f" {GREY}{'─' * 50}{RESET}")
@@ -13,7 +13,7 @@ from kiwi_cli.config import ConfigManager
13
13
  from kiwi_cli.client import AutobotsClientWrapper
14
14
  from kiwi_cli.auth import TokenManager
15
15
  from kiwi_cli import runtime_manager
16
- from .screens import LoginScreen, DashboardScreen, AutobotsScreen, ActionsScreen, RuntimeLogsScreen
16
+ from .screens import LoginScreen, DashboardScreen, RuntimeLogsScreen
17
17
 
18
18
 
19
19
  class AutobotsTUI(App):
@@ -45,8 +45,6 @@ class AutobotsTUI(App):
45
45
  SCREENS = {
46
46
  "login": LoginScreen,
47
47
  "dashboard": DashboardScreen,
48
- "autobots": AutobotsScreen,
49
- "actions": ActionsScreen,
50
48
  "runtime_logs": RuntimeLogsScreen,
51
49
  }
52
50
 
@@ -420,6 +418,9 @@ def _serve_tui(port: int = 8566):
420
418
 
421
419
  def main():
422
420
  """Entry point for the kiwi command."""
421
+ import setproctitle
422
+ setproctitle.setproctitle("kiwi")
423
+
423
424
  import argparse
424
425
 
425
426
  parser = argparse.ArgumentParser(prog="kiwi", description="Kiwi Code TUI")
@@ -0,0 +1,7 @@
1
+ """Screens for Autobots TUI."""
2
+
3
+ from .login import LoginScreen
4
+ from .dashboard import DashboardScreen
5
+ from .runtime_logs import RuntimeLogsScreen
6
+
7
+ __all__ = ["LoginScreen", "DashboardScreen", "RuntimeLogsScreen"]
@@ -239,6 +239,17 @@ class DashboardScreen(Screen):
239
239
  self.add_message(f"Cleared {count} pending file(s).", "info")
240
240
  return
241
241
 
242
+ if cmd == "/login":
243
+ if self.app.token_manager.is_authenticated():
244
+ self.add_message("Already logged in. Use /logout first to switch accounts.", "info")
245
+ return
246
+ self.app.push_screen("login")
247
+ return
248
+
249
+ if cmd == "/logout":
250
+ self.app.action_logout()
251
+ return
252
+
242
253
  if cmd == "/metadata":
243
254
  if not args:
244
255
  # Show effective metadata (defaults + overrides)
@@ -27,6 +27,8 @@ _SLASH_COMMANDS = [
27
27
  "/upload ",
28
28
  "/files",
29
29
  "/clear-files",
30
+ "/login",
31
+ "/logout",
30
32
  "/metadata",
31
33
  "/metadata set ",
32
34
  "/metadata remove ",
@@ -319,7 +319,7 @@ wheels = [
319
319
 
320
320
  [[package]]
321
321
  name = "kiwi-code"
322
- version = "0.0.7"
322
+ version = "0.0.9"
323
323
  source = { editable = "." }
324
324
  dependencies = [
325
325
  { name = "autobots-client" },
@@ -327,6 +327,7 @@ dependencies = [
327
327
  { name = "loguru" },
328
328
  { name = "psutil" },
329
329
  { name = "pydantic" },
330
+ { name = "setproctitle" },
330
331
  { name = "textual" },
331
332
  { name = "textual-dev" },
332
333
  { name = "typer" },
@@ -340,6 +341,7 @@ requires-dist = [
340
341
  { name = "loguru", specifier = ">=0.7.3" },
341
342
  { name = "psutil", specifier = ">=5.9.0" },
342
343
  { name = "pydantic", specifier = ">=2.12.5" },
344
+ { name = "setproctitle", specifier = ">=1.3.0" },
343
345
  { name = "textual", specifier = ">=8.1.1" },
344
346
  { name = "textual-dev", specifier = ">=1.8.0" },
345
347
  { name = "typer", specifier = ">=0.24.1" },
@@ -785,6 +787,54 @@ wheels = [
785
787
  { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458 },
786
788
  ]
787
789
 
790
+ [[package]]
791
+ name = "setproctitle"
792
+ version = "1.3.7"
793
+ source = { registry = "https://pypi.org/simple" }
794
+ sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002 }
795
+ wheels = [
796
+ { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047 },
797
+ { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073 },
798
+ { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284 },
799
+ { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104 },
800
+ { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982 },
801
+ { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150 },
802
+ { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463 },
803
+ { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848 },
804
+ { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544 },
805
+ { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235 },
806
+ { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058 },
807
+ { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072 },
808
+ { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490 },
809
+ { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267 },
810
+ { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376 },
811
+ { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963 },
812
+ { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550 },
813
+ { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727 },
814
+ { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549 },
815
+ { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243 },
816
+ { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052 },
817
+ { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071 },
818
+ { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180 },
819
+ { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043 },
820
+ { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892 },
821
+ { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898 },
822
+ { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308 },
823
+ { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536 },
824
+ { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731 },
825
+ { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464 },
826
+ { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062 },
827
+ { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075 },
828
+ { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744 },
829
+ { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589 },
830
+ { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698 },
831
+ { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201 },
832
+ { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801 },
833
+ { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958 },
834
+ { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745 },
835
+ { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469 },
836
+ ]
837
+
788
838
  [[package]]
789
839
  name = "shellingham"
790
840
  version = "1.5.4"
@@ -1,9 +0,0 @@
1
- """Screens for Autobots TUI."""
2
-
3
- from .login import LoginScreen
4
- from .dashboard import DashboardScreen
5
- from .autobots import AutobotsScreen
6
- from .actions import ActionsScreen
7
- from .runtime_logs import RuntimeLogsScreen
8
-
9
- __all__ = ["LoginScreen", "DashboardScreen", "AutobotsScreen", "ActionsScreen", "RuntimeLogsScreen"]
@@ -1,271 +0,0 @@
1
- """Actions execution screen."""
2
-
3
- from datetime import datetime
4
- from textual.app import ComposeResult
5
- from textual.screen import Screen
6
- from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
7
- from textual.widgets import Header, Footer, Static, DataTable, Button, Input, Label
8
- from loguru import logger
9
-
10
- from kiwi_cli.models import Action, ActionExecution, ActionStatus
11
- from kiwi_tui.widgets import StatusBadge, ActionButton, InfoPanel
12
-
13
-
14
- class ActionsScreen(Screen):
15
- """Screen for viewing and executing actions."""
16
-
17
- CSS = """
18
- ActionsScreen {
19
- background: $surface;
20
- }
21
-
22
- #actions-header {
23
- height: auto;
24
- padding: 1;
25
- background: $panel;
26
- border-bottom: solid $primary;
27
- }
28
-
29
- #actions-title {
30
- text-style: bold;
31
- color: $accent;
32
- text-align: center;
33
- }
34
-
35
- #main-content {
36
- height: 1fr;
37
- }
38
-
39
- #left-panel {
40
- width: 1fr;
41
- padding: 1;
42
- border-right: solid $primary;
43
- }
44
-
45
- #right-panel {
46
- width: 2fr;
47
- padding: 1;
48
- }
49
-
50
- #actions-list {
51
- height: 1fr;
52
- margin-top: 1;
53
- }
54
-
55
- #execution-container {
56
- height: 1fr;
57
- }
58
-
59
- #action-buttons {
60
- height: auto;
61
- padding: 1;
62
- align: center middle;
63
- }
64
-
65
- .section-title {
66
- text-style: bold;
67
- color: $accent;
68
- padding: 1 0;
69
- }
70
-
71
- #status-display {
72
- height: auto;
73
- padding: 1;
74
- border: solid $primary;
75
- margin: 1 0;
76
- }
77
- """
78
-
79
- BINDINGS = [
80
- ("escape", "back", "Back"),
81
- ("e", "execute", "Execute"),
82
- ("r", "refresh", "Refresh"),
83
- ("q", "quit", "Quit"),
84
- ]
85
-
86
- def __init__(self):
87
- """Initialize actions screen."""
88
- super().__init__()
89
- self.actions: list[Action] = []
90
- self.executions: list[ActionExecution] = []
91
- self.selected_action: Action | None = None
92
-
93
- def compose(self) -> ComposeResult:
94
- """Compose actions screen widgets."""
95
- yield Header()
96
-
97
- with Container(id="actions-header"):
98
- yield Static("⚡ Actions & Execution", id="actions-title")
99
-
100
- with Horizontal(id="main-content"):
101
- # Left panel - Actions list
102
- with Vertical(id="left-panel"):
103
- yield Static("Available Actions", classes="section-title")
104
- yield DataTable(id="actions-list")
105
-
106
- # Right panel - Execution details
107
- with ScrollableContainer(id="right-panel"):
108
- yield Static("Action Details", classes="section-title")
109
-
110
- yield InfoPanel(
111
- "No Action Selected",
112
- "Select an action from the list to view details and execute.",
113
- id="action-details"
114
- )
115
-
116
- with Horizontal(id="action-buttons"):
117
- yield ActionButton("▶ Execute", variant="success", id="btn-execute")
118
- yield ActionButton("⏹ Stop", variant="danger", id="btn-stop")
119
-
120
- yield Static("Execution History", classes="section-title")
121
- yield Container(id="execution-container")
122
-
123
- yield Footer()
124
-
125
- def on_mount(self) -> None:
126
- """Called when screen is mounted."""
127
- logger.info("Actions screen mounted")
128
-
129
- # Setup table
130
- table = self.query_one("#actions-list", DataTable)
131
- table.cursor_type = "row"
132
- table.zebra_stripes = True
133
- table.add_columns("ID", "Name", "Autobot")
134
-
135
- # Load actions
136
- self.load_actions()
137
-
138
- def load_actions(self) -> None:
139
- """Load available actions."""
140
- logger.debug("Loading actions")
141
-
142
- # TODO: Fetch from autobots client
143
- # Mock data
144
- self.actions = [
145
- Action(
146
- id="act-001",
147
- name="Generate Report",
148
- description="Generate monthly sales report",
149
- autobot_id="ab-002",
150
- parameters={"format": "pdf", "month": "current"}
151
- ),
152
- Action(
153
- id="act-002",
154
- name="Send Notification",
155
- description="Send notification to subscribers",
156
- autobot_id="ab-005",
157
- parameters={"channel": "email", "template": "newsletter"}
158
- ),
159
- Action(
160
- id="act-003",
161
- name="Analyze Data",
162
- description="Run data analysis pipeline",
163
- autobot_id="ab-002",
164
- parameters={"dataset": "sales_2024", "model": "regression"}
165
- ),
166
- Action(
167
- id="act-004",
168
- name="Scrape Prices",
169
- description="Scrape competitor pricing",
170
- autobot_id="ab-003",
171
- parameters={"urls": ["example.com"], "frequency": "daily"}
172
- ),
173
- ]
174
-
175
- self.populate_table()
176
-
177
- def populate_table(self) -> None:
178
- """Populate actions table."""
179
- table = self.query_one("#actions-list", DataTable)
180
- table.clear()
181
-
182
- for action in self.actions:
183
- table.add_row(
184
- action.id,
185
- action.name,
186
- action.autobot_id,
187
- )
188
-
189
- logger.info(f"Loaded {len(self.actions)} actions")
190
-
191
- def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
192
- """Handle action selection."""
193
- row_key = event.row_key
194
- table = self.query_one("#actions-list", DataTable)
195
- row_data = table.get_row(row_key)
196
-
197
- action_id = str(row_data[0])
198
- self.selected_action = next(
199
- (act for act in self.actions if act.id == action_id), None
200
- )
201
-
202
- if self.selected_action:
203
- logger.info(f"Selected action: {self.selected_action.name}")
204
- self.update_action_details()
205
-
206
- def update_action_details(self) -> None:
207
- """Update action details panel."""
208
- if not self.selected_action:
209
- return
210
-
211
- details_text = f"{self.selected_action.description}\n\n"
212
- details_text += f"ID: {self.selected_action.id}\n"
213
- details_text += f"Autobot: {self.selected_action.autobot_id}\n\n"
214
- details_text += "Parameters:\n"
215
-
216
- for key, value in self.selected_action.parameters.items():
217
- details_text += f" • {key}: {value}\n"
218
-
219
- panel = self.query_one("#action-details", InfoPanel)
220
- panel.query_one(".panel-title", Static).update(self.selected_action.name)
221
- panel.update_content(details_text)
222
-
223
- def on_button_pressed(self, event: Button.Pressed) -> None:
224
- """Handle button press events."""
225
- if event.button.id == "btn-execute":
226
- self.action_execute()
227
- elif event.button.id == "btn-stop":
228
- self.stop_execution()
229
-
230
- def action_execute(self) -> None:
231
- """Execute selected action."""
232
- if not self.selected_action:
233
- self.notify("Please select an action first", severity="warning")
234
- return
235
-
236
- logger.info(f"Executing action: {self.selected_action.name}")
237
-
238
- # TODO: Execute via autobots client
239
- # Mock execution
240
- execution = ActionExecution(
241
- id=f"exec-{len(self.executions) + 1:03d}",
242
- action_id=self.selected_action.id,
243
- status=ActionStatus.RUNNING,
244
- )
245
- self.executions.append(execution)
246
-
247
- self.notify(
248
- f"Executing: {self.selected_action.name}",
249
- severity="information"
250
- )
251
-
252
- def stop_execution(self) -> None:
253
- """Stop running execution."""
254
- logger.info("Stop execution requested")
255
- self.notify("Stop execution coming soon!", severity="warning")
256
-
257
- def action_back(self) -> None:
258
- """Return to previous screen."""
259
- logger.info("Returning to dashboard")
260
- self.app.pop_screen()
261
-
262
- def action_refresh(self) -> None:
263
- """Refresh actions list."""
264
- logger.info("Refreshing actions list")
265
- self.load_actions()
266
- self.notify("Actions list refreshed", severity="information")
267
-
268
- def action_quit(self) -> None:
269
- """Quit the application."""
270
- logger.info("Quitting application from actions screen")
271
- self.app.exit()
@@ -1,216 +0,0 @@
1
- """Autobots list screen."""
2
-
3
- from datetime import datetime
4
- from textual.app import ComposeResult
5
- from textual.screen import Screen
6
- from textual.containers import Container, Vertical, Horizontal
7
- from textual.widgets import Header, Footer, Static, DataTable, Button
8
- from loguru import logger
9
-
10
- from kiwi_cli.models import Autobot, AutobotType
11
- from kiwi_tui.widgets import StatusBadge, ActionButton
12
-
13
-
14
- class AutobotsScreen(Screen):
15
- """Screen displaying list of autobots."""
16
-
17
- CSS = """
18
- AutobotsScreen {
19
- background: $surface;
20
- }
21
-
22
- #autobots-header {
23
- height: auto;
24
- padding: 1;
25
- background: $panel;
26
- border-bottom: solid $primary;
27
- }
28
-
29
- #autobots-title {
30
- text-style: bold;
31
- color: $accent;
32
- text-align: center;
33
- }
34
-
35
- #actions-bar {
36
- height: auto;
37
- padding: 1;
38
- align: center middle;
39
- }
40
-
41
- #table-container {
42
- height: 1fr;
43
- padding: 1;
44
- }
45
-
46
- DataTable {
47
- height: 100%;
48
- }
49
- """
50
-
51
- BINDINGS = [
52
- ("escape", "back", "Back"),
53
- ("n", "new_autobot", "New"),
54
- ("r", "refresh", "Refresh"),
55
- ("q", "quit", "Quit"),
56
- ]
57
-
58
- def __init__(self):
59
- """Initialize autobots screen."""
60
- super().__init__()
61
- self.autobots: list[Autobot] = []
62
-
63
- def compose(self) -> ComposeResult:
64
- """Compose autobots screen widgets."""
65
- yield Header()
66
-
67
- with Container(id="autobots-header"):
68
- yield Static("🤖 Autobots Management", id="autobots-title")
69
-
70
- with Horizontal(id="actions-bar"):
71
- yield ActionButton("➕ New Autobot", variant="success", id="btn-new")
72
- yield ActionButton("🔄 Refresh", variant="primary", id="btn-refresh")
73
-
74
- with Container(id="table-container"):
75
- yield DataTable(id="autobots-table")
76
-
77
- yield Footer()
78
-
79
- def on_mount(self) -> None:
80
- """Called when screen is mounted."""
81
- logger.info("Autobots screen mounted")
82
-
83
- # Setup table
84
- table = self.query_one(DataTable)
85
- table.cursor_type = "row"
86
- table.zebra_stripes = True
87
-
88
- # Add columns
89
- table.add_columns("ID", "Name", "Type", "Status", "Runs", "Last Run")
90
-
91
- # Load autobots
92
- self.load_autobots()
93
-
94
- def load_autobots(self) -> None:
95
- """Load autobots from backend."""
96
- logger.debug("Loading autobots")
97
-
98
- # TODO: Fetch from autobots client
99
- # Mock data for now
100
- self.autobots = [
101
- Autobot(
102
- id="ab-001",
103
- name="Content Generator",
104
- type=AutobotType.AUTOMATION,
105
- description="Generates blog content automatically",
106
- enabled=True,
107
- run_count=45,
108
- last_run=datetime.now(),
109
- ),
110
- Autobot(
111
- id="ab-002",
112
- name="Data Analyzer",
113
- type=AutobotType.ANALYSIS,
114
- description="Analyzes sales data and generates reports",
115
- enabled=True,
116
- run_count=23,
117
- last_run=datetime.now(),
118
- ),
119
- Autobot(
120
- id="ab-003",
121
- name="Web Scraper",
122
- type=AutobotType.SEARCH,
123
- description="Scrapes competitor pricing data",
124
- enabled=False,
125
- run_count=12,
126
- last_run=None,
127
- ),
128
- Autobot(
129
- id="ab-004",
130
- name="Chat Assistant",
131
- type=AutobotType.CHAT,
132
- description="Handles customer inquiries",
133
- enabled=True,
134
- run_count=156,
135
- last_run=datetime.now(),
136
- ),
137
- Autobot(
138
- id="ab-005",
139
- name="Email Responder",
140
- type=AutobotType.AUTOMATION,
141
- description="Automated email responses",
142
- enabled=True,
143
- run_count=89,
144
- last_run=datetime.now(),
145
- ),
146
- ]
147
-
148
- self.populate_table()
149
-
150
- def populate_table(self) -> None:
151
- """Populate table with autobot data."""
152
- table = self.query_one(DataTable)
153
- table.clear()
154
-
155
- for autobot in self.autobots:
156
- status = "✓ Enabled" if autobot.enabled else "✗ Disabled"
157
- last_run = (
158
- autobot.last_run.strftime("%Y-%m-%d %H:%M")
159
- if autobot.last_run
160
- else "Never"
161
- )
162
-
163
- table.add_row(
164
- autobot.id,
165
- autobot.name,
166
- autobot.type.value.title(),
167
- status,
168
- str(autobot.run_count),
169
- last_run,
170
- )
171
-
172
- logger.info(f"Loaded {len(self.autobots)} autobots")
173
-
174
- def on_button_pressed(self, event: Button.Pressed) -> None:
175
- """Handle button press events."""
176
- if event.button.id == "btn-new":
177
- self.action_new_autobot()
178
- elif event.button.id == "btn-refresh":
179
- self.action_refresh()
180
-
181
- def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
182
- """Handle table row selection."""
183
- row_key = event.row_key
184
- table = self.query_one(DataTable)
185
- row_data = table.get_row(row_key)
186
-
187
- autobot_id = str(row_data[0])
188
- autobot = next((ab for ab in self.autobots if ab.id == autobot_id), None)
189
-
190
- if autobot:
191
- logger.info(f"Selected autobot: {autobot.name}")
192
- self.notify(
193
- f"Selected: {autobot.name}\n{autobot.description}",
194
- severity="information",
195
- )
196
-
197
- def action_back(self) -> None:
198
- """Return to previous screen."""
199
- logger.info("Returning to dashboard")
200
- self.app.pop_screen()
201
-
202
- def action_new_autobot(self) -> None:
203
- """Create new autobot."""
204
- logger.info("New autobot requested")
205
- self.notify("New autobot creation coming soon!", severity="information")
206
-
207
- def action_refresh(self) -> None:
208
- """Refresh autobots list."""
209
- logger.info("Refreshing autobots list")
210
- self.load_autobots()
211
- self.notify("Autobots list refreshed", severity="information")
212
-
213
- def action_quit(self) -> None:
214
- """Quit the application."""
215
- logger.info("Quitting application from autobots screen")
216
- self.app.exit()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes