Download src/codex_dashboard/app.py from smlflg/CodexDashBoard: direct link, hf CLI and curl.
- Browser
- Download file 24.5 kB
-
https://huggingface.co/smlflg/CodexDashBoard/resolve/main/src/codex_dashboard/app.py
- Command line
-
hf download hf://smlflg/CodexDashBoard/src/codex_dashboard/app.py
-
curl -L -o app.py https://huggingface.co/smlflg/CodexDashBoard/resolve/main/src/codex_dashboard/app.py
24.5 kB
| from __future__ import annotations | |
| import fcntl | |
| import logging | |
| import os | |
| import shutil | |
| import subprocess | |
| import threading | |
| import time | |
| from pathlib import Path | |
| import gi | |
| gi.require_version("Gtk", "3.0") | |
| gi.require_version("Gdk", "3.0") | |
| from gi.repository import Gdk, GLib, Gtk, Pango | |
| try: | |
| gi.require_version("AyatanaAppIndicator3", "0.1") | |
| from gi.repository import AyatanaAppIndicator3 | |
| except (ImportError, ValueError): | |
| AyatanaAppIndicator3 = None | |
| from .codex_data import ( | |
| DashboardStats, | |
| ThreadSummary, | |
| format_time, | |
| format_tokens, | |
| list_logs_result, | |
| list_threads, | |
| list_threads_result, | |
| get_stats, | |
| ) | |
| from .paths import CACHE_LOG, CONFIG_TOML, LOCK_PATH | |
| from .processes import ProcessInfo, format_uptime, scan_processes, terminate_process | |
| log = logging.getLogger("codex_dashboard") | |
| _lock_file = None | |
| CSS = b""" | |
| window { | |
| background: #0f172a; | |
| color: #e2e8f0; | |
| } | |
| .topbar { | |
| background: #111827; | |
| border-bottom: 1px solid #1f2937; | |
| padding: 10px 12px; | |
| } | |
| .app-title { | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: #f8fafc; | |
| } | |
| .muted { | |
| color: #94a3b8; | |
| } | |
| .panel { | |
| background: #0f172a; | |
| } | |
| .stat { | |
| background: #111827; | |
| border: 1px solid #1f2937; | |
| border-radius: 10px; | |
| padding: 12px; | |
| } | |
| .stat-value { | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: #f8fafc; | |
| } | |
| .badge, | |
| .badge-ok, | |
| .badge-warn, | |
| .badge-idle { | |
| border-radius: 999px; | |
| padding: 4px 10px; | |
| font-weight: 600; | |
| } | |
| .badge { | |
| background: #1e293b; | |
| color: #cbd5e1; | |
| } | |
| .badge-ok { | |
| background: #123a2b; | |
| color: #86efac; | |
| } | |
| .badge-warn { | |
| background: #3f2a12; | |
| color: #fcd34d; | |
| } | |
| .badge-idle { | |
| background: #1f2937; | |
| color: #cbd5e1; | |
| } | |
| .row { | |
| border: 1px solid transparent; | |
| border-bottom: 1px solid #1e293b; | |
| border-radius: 8px; | |
| padding: 10px 12px; | |
| } | |
| .row:hover { | |
| background: #111827; | |
| border-color: #334155; | |
| } | |
| button { | |
| border-radius: 8px; | |
| } | |
| entry { | |
| border-radius: 8px; | |
| background: #111827; | |
| color: #e2e8f0; | |
| } | |
| """ | |
| def _setup_logging() -> None: | |
| CACHE_LOG.parent.mkdir(parents=True, exist_ok=True) | |
| logging.basicConfig( | |
| filename=str(CACHE_LOG), | |
| level=logging.INFO, | |
| format="%(asctime)s %(name)s %(levelname)s %(message)s", | |
| ) | |
| def _acquire_singleton_lock() -> None: | |
| global _lock_file | |
| fd = os.open(LOCK_PATH, os.O_CREAT | os.O_WRONLY, 0o644) | |
| _lock_file = os.fdopen(fd, "w") | |
| try: | |
| fcntl.flock(_lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) | |
| _lock_file.truncate(0) | |
| _lock_file.write(str(os.getpid())) | |
| _lock_file.flush() | |
| except BlockingIOError: | |
| log.warning("CodexDashBoard already running") | |
| raise SystemExit(0) | |
| def _label(text: str = "", *, xalign: float = 0.0, css: str | None = None) -> Gtk.Label: | |
| label = Gtk.Label(label=text, xalign=xalign) | |
| label.set_ellipsize(Pango.EllipsizeMode.END) | |
| if css: | |
| label.get_style_context().add_class(css) | |
| return label | |
| def _button(icon_name: str, tooltip: str) -> Gtk.Button: | |
| button = Gtk.Button.new_from_icon_name(icon_name, Gtk.IconSize.BUTTON) | |
| button.set_tooltip_text(tooltip) | |
| return button | |
| def _clear_list(list_box: Gtk.ListBox) -> None: | |
| for child in list_box.get_children(): | |
| list_box.remove(child) | |
| def _run_bg(work, done) -> None: | |
| def worker(): | |
| try: | |
| result = work() | |
| except Exception as exc: # pragma: no cover - UI guard | |
| log.exception("background task failed") | |
| result = exc | |
| GLib.idle_add(done, result) | |
| threading.Thread(target=worker, daemon=True).start() | |
| def _open_path(path: str | Path) -> None: | |
| subprocess.Popen(["xdg-open", str(path)], start_new_session=True) | |
| class CodexDashboard(Gtk.Window): | |
| def __init__(self) -> None: | |
| super().__init__(title="CodexDashBoard") | |
| self.set_default_size(940, 640) | |
| self.set_resizable(True) | |
| self.set_wmclass("codex-dashboard", "CodexDashBoard") | |
| self._tray_indicator = None | |
| self._session_query = "" | |
| self._log_query = "" | |
| self._include_archived = False | |
| self._stats_labels: dict[str, Gtk.Label] = {} | |
| self._refresh_in_flight: set[str] = set() | |
| self._overview_sessions = Gtk.ListBox() | |
| self._overview_processes = Gtk.ListBox() | |
| self._sessions_list = Gtk.ListBox() | |
| self._processes_list = Gtk.ListBox() | |
| self._logs_list = Gtk.ListBox() | |
| self._status = _label("Ready", css="muted") | |
| self._setup_css() | |
| self._build_ui() | |
| self._build_tray() | |
| self.connect("delete-event", self._on_delete) | |
| GLib.timeout_add_seconds(30, self._refresh_visible) | |
| self._refresh_visible() | |
| def _setup_css(self) -> None: | |
| provider = Gtk.CssProvider() | |
| provider.load_from_data(CSS) | |
| screen = Gdk.Screen.get_default() | |
| Gtk.StyleContext.add_provider_for_screen( | |
| screen, | |
| provider, | |
| Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, | |
| ) | |
| def _build_ui(self) -> None: | |
| root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) | |
| self.add(root) | |
| topbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) | |
| topbar.get_style_context().add_class("topbar") | |
| root.pack_start(topbar, False, False, 0) | |
| title = _label("CodexDashBoard", css="app-title") | |
| topbar.pack_start(title, False, False, 0) | |
| topbar.pack_start(self._status, True, True, 0) | |
| config_button = _button("emblem-system-symbolic", "Open Codex config") | |
| config_button.connect("clicked", lambda *_: _open_path(CONFIG_TOML)) | |
| topbar.pack_end(config_button, False, False, 0) | |
| refresh_button = _button("view-refresh-symbolic", "Refresh") | |
| refresh_button.connect("clicked", lambda *_: self.refresh_all()) | |
| topbar.pack_end(refresh_button, False, False, 0) | |
| body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) | |
| root.pack_start(body, True, True, 0) | |
| self.stack = Gtk.Stack() | |
| self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) | |
| self.stack.set_transition_duration(120) | |
| self.stack.connect("notify::visible-child-name", lambda *_: self._refresh_visible()) | |
| sidebar = Gtk.StackSidebar() | |
| sidebar.set_stack(self.stack) | |
| sidebar.set_size_request(150, -1) | |
| body.pack_start(sidebar, False, False, 0) | |
| body.pack_start(self.stack, True, True, 0) | |
| self.stack.add_titled(self._build_overview(), "overview", "Overview") | |
| self.stack.add_titled(self._build_sessions(), "sessions", "Sessions") | |
| self.stack.add_titled(self._build_processes(), "processes", "Processes") | |
| self.stack.add_titled(self._build_logs(), "logs", "Logs") | |
| def _build_overview(self) -> Gtk.Widget: | |
| scrolled = Gtk.ScrolledWindow() | |
| scrolled.get_style_context().add_class("panel") | |
| box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14) | |
| box.set_margin_top(14) | |
| box.set_margin_bottom(14) | |
| box.set_margin_start(14) | |
| box.set_margin_end(14) | |
| scrolled.add(box) | |
| stats = Gtk.Grid(column_spacing=10, row_spacing=10) | |
| box.pack_start(stats, False, False, 0) | |
| for idx, (key, label_text) in enumerate( | |
| [ | |
| ("threads", "Threads"), | |
| ("recent", "Updated Last Hour"), | |
| ("tokens", "Tokens"), | |
| ("processes", "Agent Processes"), | |
| ] | |
| ): | |
| card = self._stat_card(label_text) | |
| stats.attach(card, idx % 3, idx // 3, 1, 1) | |
| self._stats_labels[key] = card.value_label | |
| box.pack_start(_label("Letzte 10 Codex Sessions", css="app-title"), False, False, 0) | |
| box.pack_start(self._overview_sessions, False, False, 0) | |
| box.pack_start(_label("Active Processes", css="app-title"), False, False, 0) | |
| box.pack_start(self._overview_processes, False, False, 0) | |
| return scrolled | |
| def _stat_card(self, label_text: str) -> Gtk.Box: | |
| card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) | |
| card.get_style_context().add_class("stat") | |
| card.set_hexpand(True) | |
| value = _label("-", css="stat-value") | |
| label = _label(label_text, css="muted") | |
| card.pack_start(value, False, False, 0) | |
| card.pack_start(label, False, False, 0) | |
| card.value_label = value # type: ignore[attr-defined] | |
| return card | |
| def _build_sessions(self) -> Gtk.Widget: | |
| box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) | |
| box.set_margin_top(10) | |
| box.set_margin_bottom(10) | |
| box.set_margin_start(10) | |
| box.set_margin_end(10) | |
| controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) | |
| search = Gtk.SearchEntry() | |
| search.set_placeholder_text("Search title, cwd, branch") | |
| search.connect("search-changed", self._on_session_search) | |
| controls.pack_start(search, True, True, 0) | |
| archived = Gtk.CheckButton(label="Archived") | |
| archived.connect("toggled", self._on_archived_toggled) | |
| controls.pack_end(archived, False, False, 0) | |
| box.pack_start(controls, False, False, 0) | |
| scrolled = Gtk.ScrolledWindow() | |
| scrolled.add(self._sessions_list) | |
| box.pack_start(scrolled, True, True, 0) | |
| return box | |
| def _build_processes(self) -> Gtk.Widget: | |
| box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) | |
| box.set_margin_top(10) | |
| box.set_margin_bottom(10) | |
| box.set_margin_start(10) | |
| box.set_margin_end(10) | |
| hint = _label( | |
| "Codex-related processes. Stop actions ask first.", | |
| css="muted", | |
| ) | |
| box.pack_start(hint, False, False, 0) | |
| scrolled = Gtk.ScrolledWindow() | |
| scrolled.add(self._processes_list) | |
| box.pack_start(scrolled, True, True, 0) | |
| return box | |
| def _build_logs(self) -> Gtk.Widget: | |
| box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) | |
| box.set_margin_top(10) | |
| box.set_margin_bottom(10) | |
| box.set_margin_start(10) | |
| box.set_margin_end(10) | |
| search = Gtk.SearchEntry() | |
| search.set_placeholder_text("Search logs") | |
| search.connect("search-changed", self._on_log_search) | |
| box.pack_start(search, False, False, 0) | |
| scrolled = Gtk.ScrolledWindow() | |
| scrolled.add(self._logs_list) | |
| box.pack_start(scrolled, True, True, 0) | |
| return box | |
| def _build_tray(self) -> None: | |
| if AyatanaAppIndicator3 is None: | |
| return | |
| indicator = AyatanaAppIndicator3.Indicator.new( | |
| "codex-dashboard", | |
| "utilities-system-monitor", | |
| AyatanaAppIndicator3.IndicatorCategory.APPLICATION_STATUS, | |
| ) | |
| indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE) | |
| indicator.set_title("CodexDashBoard") | |
| menu = Gtk.Menu() | |
| show_item = Gtk.MenuItem(label="Show") | |
| show_item.connect("activate", lambda *_: self._show_window()) | |
| menu.append(show_item) | |
| refresh_item = Gtk.MenuItem(label="Refresh") | |
| refresh_item.connect("activate", lambda *_: self.refresh_all()) | |
| menu.append(refresh_item) | |
| menu.append(Gtk.SeparatorMenuItem()) | |
| quit_item = Gtk.MenuItem(label="Quit") | |
| quit_item.connect("activate", lambda *_: Gtk.main_quit()) | |
| menu.append(quit_item) | |
| menu.show_all() | |
| indicator.set_menu(menu) | |
| self._tray_indicator = indicator | |
| def _show_window(self) -> None: | |
| self.show_all() | |
| self.present() | |
| def _on_delete(self, *_args) -> bool: | |
| if self._tray_indicator is not None: | |
| self.hide() | |
| return True | |
| Gtk.main_quit() | |
| return False | |
| def _on_session_search(self, entry: Gtk.SearchEntry) -> None: | |
| self._session_query = entry.get_text() | |
| self.refresh_sessions() | |
| def _on_archived_toggled(self, button: Gtk.CheckButton) -> None: | |
| self._include_archived = button.get_active() | |
| self.refresh_sessions() | |
| def _on_log_search(self, entry: Gtk.SearchEntry) -> None: | |
| self._log_query = entry.get_text() | |
| self.refresh_logs() | |
| def _refresh_visible(self) -> bool: | |
| name = self.stack.get_visible_child_name() | |
| if name == "sessions": | |
| self.refresh_sessions() | |
| elif name == "processes": | |
| self.refresh_processes() | |
| elif name == "logs": | |
| self.refresh_logs() | |
| else: | |
| self.refresh_overview() | |
| return True | |
| def refresh_all(self) -> None: | |
| self._refresh_visible() | |
| def _run_refresh_once(self, key: str, work, done) -> None: | |
| if key in self._refresh_in_flight: | |
| self._set_status(f"{key.title()} refresh already running") | |
| return | |
| self._refresh_in_flight.add(key) | |
| def wrapped_done(result): | |
| try: | |
| return done(result) | |
| finally: | |
| self._refresh_in_flight.discard(key) | |
| _run_bg(work, wrapped_done) | |
| def refresh_overview(self) -> None: | |
| def work(): | |
| return get_stats(), list_threads(limit=10), scan_processes() | |
| def done(result): | |
| if isinstance(result, Exception): | |
| self._set_status("Refresh failed") | |
| return False | |
| stats, threads, processes = result | |
| self._render_overview(stats, threads, processes) | |
| self._set_status("Overview refreshed") | |
| return False | |
| self._run_refresh_once("overview", work, done) | |
| def refresh_sessions(self) -> None: | |
| def work(): | |
| return list_threads_result( | |
| query=self._session_query, | |
| include_archived=self._include_archived, | |
| limit=120, | |
| ) | |
| def done(result): | |
| if isinstance(result, Exception): | |
| self._set_status("Session refresh failed") | |
| return False | |
| if result.error is not None: | |
| self._render_threads(self._sessions_list, result.data) | |
| self._set_status(f"Session database error: {result.error.message}") | |
| return False | |
| self._render_threads(self._sessions_list, result.data) | |
| self._set_status(f"{len(result.data)} sessions") | |
| return False | |
| self._run_refresh_once("sessions", work, done) | |
| def refresh_processes(self) -> None: | |
| def done(result): | |
| if isinstance(result, Exception): | |
| self._set_status("Process refresh failed") | |
| return False | |
| self._render_processes(self._processes_list, result, full=True) | |
| self._set_status(f"{len(result)} processes") | |
| return False | |
| self._run_refresh_once("processes", scan_processes, done) | |
| def refresh_logs(self) -> None: | |
| def work(): | |
| return list_logs_result(query=self._log_query, limit=150) | |
| def done(result): | |
| if isinstance(result, Exception): | |
| self._set_status("Log refresh failed") | |
| return False | |
| if result.error is not None: | |
| self._render_logs(result.data) | |
| self._set_status(f"Log database error: {result.error.message}") | |
| return False | |
| self._render_logs(result.data) | |
| self._set_status(f"{len(result.data)} log rows") | |
| return False | |
| self._run_refresh_once("logs", work, done) | |
| def _render_overview( | |
| self, | |
| stats: DashboardStats, | |
| threads: list[ThreadSummary], | |
| processes: list[ProcessInfo], | |
| ) -> None: | |
| self._stats_labels["threads"].set_text(str(stats.thread_count)) | |
| self._stats_labels["recent"].set_text(str(stats.recent_hour_count)) | |
| self._stats_labels["tokens"].set_text(format_tokens(stats.total_tokens)) | |
| self._stats_labels["processes"].set_text(str(len(processes))) | |
| active_cwds = {process.cwd for process in processes if process.cwd} | |
| self._render_threads( | |
| self._overview_sessions, | |
| threads, | |
| compact=True, | |
| active_cwds=active_cwds, | |
| ) | |
| self._render_processes(self._overview_processes, processes[:6], full=False) | |
| def _render_threads( | |
| self, | |
| list_box: Gtk.ListBox, | |
| threads: list[ThreadSummary], | |
| *, | |
| compact: bool = False, | |
| active_cwds: set[str] | None = None, | |
| ) -> None: | |
| _clear_list(list_box) | |
| if not threads: | |
| list_box.add(self._empty_row("No Codex sessions found")) | |
| list_box.show_all() | |
| return | |
| for thread in threads: | |
| list_box.add( | |
| self._thread_row(thread, compact=compact, active_cwds=active_cwds or set()) | |
| ) | |
| list_box.show_all() | |
| def _thread_row( | |
| self, | |
| thread: ThreadSummary, | |
| *, | |
| compact: bool = False, | |
| active_cwds: set[str] | None = None, | |
| ) -> Gtk.ListBoxRow: | |
| row = Gtk.ListBoxRow() | |
| outer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) | |
| outer.get_style_context().add_class("row") | |
| row.add(outer) | |
| text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) | |
| outer.pack_start(text_box, True, True, 0) | |
| title_prefix = "Thema: " if compact else "" | |
| title = _label(title_prefix + thread.title) | |
| title.set_tooltip_text(thread.id) | |
| text_box.pack_start(title, False, False, 0) | |
| state_text, state_class = self._thread_state(thread, active_cwds or set()) | |
| meta_parts = [ | |
| "Stand: " + state_text, | |
| "Update: " + format_time(thread.updated_ms), | |
| "Projekt: " + (Path(thread.cwd).name if thread.cwd else "no cwd"), | |
| thread.model or "model?", | |
| ] | |
| if thread.reasoning_effort: | |
| meta_parts.append(thread.reasoning_effort) | |
| if thread.git_branch: | |
| meta_parts.append(thread.git_branch) | |
| if not compact: | |
| meta_parts.append(format_tokens(thread.tokens_used) + " tokens") | |
| meta = _label(" | ".join(meta_parts), css="muted") | |
| text_box.pack_start(meta, False, False, 0) | |
| badge = _label(state_text, xalign=0.5, css=state_class) | |
| outer.pack_end(badge, False, False, 0) | |
| if not compact: | |
| open_button = _button("folder-open-symbolic", "Open folder") | |
| open_button.connect("clicked", lambda *_: self._open_thread_folder(thread)) | |
| outer.pack_end(open_button, False, False, 0) | |
| resume_button = _button("media-playback-start-symbolic", "Resume in terminal") | |
| resume_button.connect("clicked", lambda *_: self._resume_thread(thread)) | |
| outer.pack_end(resume_button, False, False, 0) | |
| return row | |
| def _thread_state(self, thread: ThreadSummary, active_cwds: set[str]) -> tuple[str, str]: | |
| if thread.archived: | |
| return "Archived", "badge" | |
| if thread.cwd and thread.cwd in active_cwds: | |
| return "Running", "badge-ok" | |
| age_ms = int(time.time() * 1000) - thread.updated_ms | |
| if age_ms <= 10 * 60 * 1000: | |
| return "Active", "badge-ok" | |
| if age_ms <= 60 * 60 * 1000: | |
| return "Recent", "badge-warn" | |
| return "Idle", "badge-idle" | |
| def _render_processes( | |
| self, | |
| list_box: Gtk.ListBox, | |
| processes: list[ProcessInfo], | |
| *, | |
| full: bool, | |
| ) -> None: | |
| _clear_list(list_box) | |
| if not processes: | |
| list_box.add(self._empty_row("No Codex-related processes found")) | |
| list_box.show_all() | |
| return | |
| for process in processes: | |
| list_box.add(self._process_row(process, full=full)) | |
| list_box.show_all() | |
| def _process_row(self, process: ProcessInfo, *, full: bool) -> Gtk.ListBoxRow: | |
| row = Gtk.ListBoxRow() | |
| outer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) | |
| outer.get_style_context().add_class("row") | |
| row.add(outer) | |
| text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) | |
| outer.pack_start(text_box, True, True, 0) | |
| title = _label(f"{process.name} pid {process.pid}") | |
| title.set_tooltip_text(process.command) | |
| text_box.pack_start(title, False, False, 0) | |
| cwd = Path(process.cwd).name if process.cwd else "-" | |
| meta = ( | |
| f"{process.ram_mb:.1f} MB | {process.cpu:.1f}% CPU | " | |
| f"{format_uptime(process.uptime_seconds)} | {cwd}" | |
| ) | |
| text_box.pack_start(_label(meta, css="muted"), False, False, 0) | |
| if process.stale: | |
| badge = _label("Stale", xalign=0.5, css="badge-warn") | |
| outer.pack_end(badge, False, False, 0) | |
| if full: | |
| stop_button = _button("process-stop-symbolic", "Stop process") | |
| stop_button.connect("clicked", lambda *_: self._confirm_stop(process)) | |
| outer.pack_end(stop_button, False, False, 0) | |
| return row | |
| def _render_logs(self, logs) -> None: | |
| _clear_list(self._logs_list) | |
| if not logs: | |
| self._logs_list.add(self._empty_row("No Codex logs found")) | |
| self._logs_list.show_all() | |
| return | |
| for entry in logs: | |
| row = Gtk.ListBoxRow() | |
| outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=3) | |
| outer.get_style_context().add_class("row") | |
| row.add(outer) | |
| header = _label( | |
| f"{format_time(entry.ts_ms)} | {entry.level} | {entry.target}" | |
| ) | |
| outer.pack_start(header, False, False, 0) | |
| if entry.message: | |
| outer.pack_start(_label(entry.message, css="muted"), False, False, 0) | |
| elif entry.thread_id: | |
| outer.pack_start(_label(entry.thread_id, css="muted"), False, False, 0) | |
| self._logs_list.add(row) | |
| self._logs_list.show_all() | |
| def _empty_row(self, text: str) -> Gtk.ListBoxRow: | |
| row = Gtk.ListBoxRow() | |
| label = _label(text, css="muted") | |
| label.set_margin_top(12) | |
| label.set_margin_bottom(12) | |
| label.set_margin_start(12) | |
| label.set_margin_end(12) | |
| row.add(label) | |
| return row | |
| def _resume_thread(self, thread: ThreadSummary) -> None: | |
| codex = shutil.which("codex") | |
| kitty = shutil.which("kitty") | |
| cwd = thread.cwd if thread.cwd and Path(thread.cwd).is_dir() else str(Path.home()) | |
| if codex is None: | |
| self._set_status("codex command not found") | |
| return | |
| if kitty: | |
| cmd = [kitty, "-e", codex, "resume", thread.id] | |
| else: | |
| cmd = [codex, "resume", thread.id] | |
| try: | |
| subprocess.Popen(cmd, cwd=cwd, start_new_session=True) | |
| self._set_status("Resume launched") | |
| except OSError: | |
| log.exception("resume failed") | |
| self._set_status("Resume failed") | |
| def _open_thread_folder(self, thread: ThreadSummary) -> None: | |
| path = Path(thread.cwd) | |
| if path.is_dir(): | |
| _open_path(path) | |
| self._set_status("Folder opened") | |
| else: | |
| self._set_status("Folder missing") | |
| def _confirm_stop(self, process: ProcessInfo) -> None: | |
| dialog = Gtk.MessageDialog( | |
| transient_for=self, | |
| flags=0, | |
| message_type=Gtk.MessageType.WARNING, | |
| buttons=Gtk.ButtonsType.CANCEL, | |
| text=f"Stop process {process.pid}?", | |
| ) | |
| dialog.format_secondary_text(process.command) | |
| dialog.add_button("Stop", Gtk.ResponseType.OK) | |
| response = dialog.run() | |
| dialog.destroy() | |
| if response != Gtk.ResponseType.OK: | |
| return | |
| try: | |
| terminate_process(process.pid) | |
| self._set_status(f"Sent SIGTERM to {process.pid}") | |
| self.refresh_processes() | |
| except OSError: | |
| log.exception("terminate failed") | |
| self._set_status("Stop failed") | |
| def _set_status(self, text: str) -> None: | |
| self._status.set_text(text) | |
| def main() -> int: | |
| _setup_logging() | |
| _acquire_singleton_lock() | |
| win = CodexDashboard() | |
| win.connect("destroy", Gtk.main_quit) | |
| win.show_all() | |
| Gtk.main() | |
| return 0 | |