conduct-cli 0.4.44__tar.gz → 0.4.45__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.44
3
+ Version: 0.4.45
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: rich>=13.0
24
25
 
25
26
  # conduct-cli
26
27
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.4.44"
7
+ version = "0.4.45"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Topic :: Software Development :: Libraries :: Application Frameworks",
25
25
  ]
26
- dependencies = ["pyyaml>=6.0"]
26
+ dependencies = ["pyyaml>=6.0", "rich>=13.0"]
27
27
 
28
28
  [project.urls]
29
29
  Homepage = "https://conductai.ai"
@@ -1757,79 +1757,137 @@ def _render_table(rows: list[dict]) -> str:
1757
1757
 
1758
1758
 
1759
1759
  def _render_tui(rows: list[dict]) -> None:
1760
- """Full-screen TUI with live refresh using ANSI escape codes."""
1760
+ """Full-screen TUI with live refresh. Uses rich.live if available, else ANSI loop."""
1761
+
1762
+ def _build_rich_display(rows):
1763
+ from rich.table import Table
1764
+ from rich.panel import Panel
1765
+ from rich.columns import Columns
1766
+ from rich import box
1767
+ from rich.text import Text
1768
+
1769
+ active = sum(1 for r in rows if r["alive"])
1770
+ guard_on = sum(1 for r in rows if r["guard"])
1771
+ high_ctx = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
1772
+
1773
+ # Main sessions table
1774
+ tbl = Table(box=box.ROUNDED, expand=True, show_header=True, header_style="bold white")
1775
+ tbl.add_column("AI", width=4, style="bold")
1776
+ tbl.add_column("Project", min_width=14)
1777
+ tbl.add_column("Session", width=10, style="dim")
1778
+ tbl.add_column("Model", min_width=14)
1779
+ tbl.add_column("CTX", width=16)
1780
+ tbl.add_column("Tokens", width=10, justify="right")
1781
+ tbl.add_column("Turns", width=6, justify="right")
1782
+ tbl.add_column("Guard", width=6, justify="center")
1783
+ tbl.add_column("Status", width=8)
1784
+
1785
+ for r in rows:
1786
+ ai_text = Text(r.get("ai", "CC"), style="bold cyan" if r.get("ai") == "CD" else "bold blue")
1787
+ status_text = Text("active", style="green") if r["alive"] else Text("idle", style="dim")
1788
+ guard_text = Text("✓", style="green") if r["guard"] else Text("—", style="dim")
1789
+ model_short = r["model"].replace("claude-", "").replace("-20", " 20") if r["model"] not in ("—", "") else "—"
1790
+
1791
+ pct = r["ctx_pct"]
1792
+ if pct:
1793
+ filled = round(pct / 100 * 10)
1794
+ bar = "█" * filled + "░" * (10 - filled)
1795
+ color = "red" if pct >= 80 else "yellow" if pct >= 60 else "green"
1796
+ ctx_text = Text(f"{bar} {pct}%", style=color)
1797
+ else:
1798
+ ctx_text = Text("—", style="dim")
1799
+
1800
+ tbl.add_row(
1801
+ ai_text,
1802
+ r["project"],
1803
+ r["session_id"],
1804
+ model_short,
1805
+ ctx_text,
1806
+ _fmt_tokens(r["ctx_tokens"]) if r["ctx_tokens"] else "—",
1807
+ str(r["turns"]),
1808
+ guard_text,
1809
+ status_text,
1810
+ )
1811
+
1812
+ # Summary header
1813
+ summary = f"[bold]{active}[/] active · [bold]{guard_on}[/] with Guard · [dim]refreshing every 5s — Ctrl+C to quit[/]"
1814
+
1815
+ panels = [Panel(tbl, title="[bold blue]conduct sessions[/]", subtitle=summary, expand=True)]
1816
+
1817
+ # Context pressure panel
1818
+ if high_ctx:
1819
+ warnings = "\n".join(
1820
+ f"[{'red' if r['ctx_pct'] >= 80 else 'yellow'}]{r['project']}[/] "
1821
+ f"[dim]{r['session_id']}[/] {r['ctx_pct']}% — consider /compact"
1822
+ for r in high_ctx
1823
+ )
1824
+ panels.append(Panel(warnings, title="[yellow bold]⚠ Context pressure[/]", expand=True))
1825
+
1826
+ from rich.console import Group
1827
+ return Group(*panels)
1828
+
1829
+ # Try rich.live first (works without raw TTY)
1761
1830
  try:
1762
- import shutil
1763
- import signal
1764
-
1765
- stop = False
1766
- def _sigint(sig, frame):
1767
- nonlocal stop
1768
- stop = True
1769
- signal.signal(signal.SIGINT, _sigint)
1770
-
1771
- def _clear():
1772
- sys.stdout.write("\033[2J\033[H")
1773
- sys.stdout.flush()
1774
-
1775
- def _render_frame(rows):
1776
- cols, _ = shutil.get_terminal_size((120, 40))
1777
- out = []
1778
-
1779
- # Header bar
1780
- title = " conduct sessions (TUI — q or Ctrl+C to quit) "
1781
- pad = max(0, cols - len(title))
1782
- out.append(f"{BOLD}\033[44m{title}{' ' * pad}\033[0m")
1783
- out.append("")
1784
-
1785
- # Summary line
1786
- active = sum(1 for r in rows if r["alive"])
1787
- guard_on = sum(1 for r in rows if r["guard"])
1788
- out.append(f" {BOLD}{active}{RESET} active session(s) · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s")
1789
- out.append("")
1790
-
1791
- # Table
1792
- out.append(_render_table(rows))
1793
-
1794
- # Context pressure panel — sessions above 60%
1795
- high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
1796
- if high:
1797
- out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
1798
- for r in high:
1799
- color = RED if r["ctx_pct"] >= 80 else YELLOW
1800
- out.append(f" {color}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
1801
- out.append("")
1802
-
1803
- sys.stdout.write("\n".join(out))
1804
- sys.stdout.flush()
1805
-
1806
- # Use raw terminal input for 'q' detection (non-blocking)
1807
- import select, tty, termios
1808
- old = termios.tcgetattr(sys.stdin)
1809
- try:
1810
- tty.setcbreak(sys.stdin.fileno())
1811
- while not stop:
1812
- _clear()
1813
- rows = _load_sessions()
1814
- _render_frame(rows)
1815
- # Wait up to 5s, exit on 'q'
1816
- for _ in range(50):
1817
- if select.select([sys.stdin], [], [], 0.1)[0]:
1818
- ch = sys.stdin.read(1)
1819
- if ch in ("q", "Q"):
1820
- stop = True
1821
- break
1822
- if stop:
1823
- break
1824
- finally:
1825
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
1826
- _clear()
1827
- print("conduct sessions exited.")
1831
+ from rich.live import Live
1832
+ from rich.console import Console
1833
+ console = Console()
1834
+ with Live(console=console, refresh_per_second=0.2, screen=True) as live:
1835
+ while True:
1836
+ live.update(_build_rich_display(_load_sessions()))
1837
+ time.sleep(5)
1838
+ return
1839
+ except KeyboardInterrupt:
1840
+ return
1841
+ except ImportError:
1842
+ pass # fall through to ANSI loop
1828
1843
 
1829
- except Exception as e:
1830
- # Graceful fallback to table if TUI setup fails
1831
- print(f"{YELLOW}TUI unavailable ({e}), showing table:{RESET}")
1844
+ # ANSI fallback — requires real TTY
1845
+ if not sys.stdin.isatty():
1846
+ print(f"{YELLOW}TUI requires a real terminal. Run directly in your shell, not via a subprocess.{RESET}")
1832
1847
  print(_render_table(rows))
1848
+ return
1849
+
1850
+ import shutil, signal, select, tty, termios
1851
+ stop = False
1852
+ def _sig(s, f): nonlocal stop; stop = True
1853
+ signal.signal(signal.SIGINT, _sig)
1854
+
1855
+ def _frame(rows):
1856
+ cols, _ = shutil.get_terminal_size((120, 40))
1857
+ title = " conduct sessions (Ctrl+C or q to quit) "
1858
+ pad = max(0, cols - len(title))
1859
+ active = sum(1 for r in rows if r["alive"])
1860
+ guard_on = sum(1 for r in rows if r["guard"])
1861
+ out = [
1862
+ f"{BOLD}\033[44m{title}{' ' * pad}\033[0m",
1863
+ f"\n {BOLD}{active}{RESET} active · {BOLD}{guard_on}{RESET} with Guard · refreshing every 5s\n",
1864
+ _render_table(rows),
1865
+ ]
1866
+ high = [r for r in rows if r["ctx_pct"] >= 60 and r["alive"]]
1867
+ if high:
1868
+ out.append(f" {YELLOW}{BOLD}⚠ Context pressure{RESET}")
1869
+ for r in high:
1870
+ c = RED if r["ctx_pct"] >= 80 else YELLOW
1871
+ out.append(f" {c}{r['project']}{RESET} ({r['session_id']}) {r['ctx_pct']}% — consider /compact")
1872
+ sys.stdout.write("\033[2J\033[H" + "\n".join(out))
1873
+ sys.stdout.flush()
1874
+
1875
+ old = termios.tcgetattr(sys.stdin)
1876
+ try:
1877
+ tty.setcbreak(sys.stdin.fileno())
1878
+ while not stop:
1879
+ _frame(_load_sessions())
1880
+ for _ in range(50):
1881
+ if select.select([sys.stdin], [], [], 0.1)[0]:
1882
+ if sys.stdin.read(1) in ("q", "Q"):
1883
+ stop = True
1884
+ break
1885
+ if stop:
1886
+ break
1887
+ finally:
1888
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
1889
+ sys.stdout.write("\033[2J\033[H")
1890
+ print("conduct sessions exited.")
1833
1891
 
1834
1892
 
1835
1893
  def cmd_sessions(args):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.44
3
+ Version: 0.4.45
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -21,6 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
21
21
  Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: rich>=13.0
24
25
 
25
26
  # conduct-cli
26
27
 
File without changes
File without changes
File without changes