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.
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/PKG-INFO +2 -1
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/pyproject.toml +2 -1
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/cli.py +2 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/models.py +1 -1
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/main.py +271 -6
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/main.py +4 -3
- kiwi_code-0.0.9/src/kiwi_tui/screens/__init__.py +7 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/dashboard.py +11 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/widgets.py +2 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/uv.lock +51 -1
- kiwi_code-0.0.7/src/kiwi_tui/screens/__init__.py +0 -9
- kiwi_code-0.0.7/src/kiwi_tui/screens/actions.py +0 -271
- kiwi_code-0.0.7/src/kiwi_tui/screens/autobots.py +0 -216
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.github/workflows/publish.yml +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.gitignore +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/.python-version +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/CLAUDE.md +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/Makefile +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/README.md +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/__init__.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/auth.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/client.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/commands.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/config.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/logger.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_cli/runtime_manager.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/__init__.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_runtime/__main__.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/__init__.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/file_browser.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/login.py +0 -0
- {kiwi_code-0.0.7 → kiwi_code-0.0.9}/src/kiwi_tui/screens/runtime_logs.py +0 -0
- {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.
|
|
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.
|
|
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" }
|
|
@@ -78,7 +78,7 @@ class LoginCredentials(BaseModel):
|
|
|
78
78
|
|
|
79
79
|
class AppConfig(BaseModel):
|
|
80
80
|
"""Application configuration."""
|
|
81
|
-
backend_url: str = "https://
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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,
|
|
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")
|
|
@@ -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)
|
|
@@ -319,7 +319,7 @@ wheels = [
|
|
|
319
319
|
|
|
320
320
|
[[package]]
|
|
321
321
|
name = "kiwi-code"
|
|
322
|
-
version = "0.0.
|
|
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
|
|
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
|