patchbai 0.1.0__tar.gz → 0.1.2__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 (78) hide show
  1. {patchbai-0.1.0 → patchbai-0.1.2}/PKG-INFO +18 -17
  2. {patchbai-0.1.0 → patchbai-0.1.2}/README.md +17 -16
  3. patchbai-0.1.2/patchbai/__init__.py +1 -0
  4. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/app.py +12 -0
  5. patchbai-0.1.2/patchbai/widgets/system_usage.py +244 -0
  6. {patchbai-0.1.0 → patchbai-0.1.2}/pyproject.toml +1 -1
  7. patchbai-0.1.0/patchbai/__init__.py +0 -1
  8. {patchbai-0.1.0 → patchbai-0.1.2}/.gitignore +0 -0
  9. {patchbai-0.1.0 → patchbai-0.1.2}/LICENSE +0 -0
  10. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/__main__.py +0 -0
  11. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/actions.py +0 -0
  12. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/activity/__init__.py +0 -0
  13. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/activity/log.py +0 -0
  14. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/__init__.py +0 -0
  15. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/child_tools.py +0 -0
  16. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/fake_sdk_adapter.py +0 -0
  17. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/manager.py +0 -0
  18. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/request_inbox.py +0 -0
  19. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/sdk_adapter.py +0 -0
  20. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/session.py +0 -0
  21. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/sort.py +0 -0
  22. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/agents/state.py +0 -0
  23. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/config.py +0 -0
  24. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/events.py +0 -0
  25. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/__init__.py +0 -0
  26. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/custom_widgets.py +0 -0
  27. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/defaults.py +0 -0
  28. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/engine.py +0 -0
  29. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/local_widgets.py +0 -0
  30. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/registry.py +0 -0
  31. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/spec.py +0 -0
  32. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/splitter.py +0 -0
  33. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/layout/titles.py +0 -0
  34. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/__init__.py +0 -0
  35. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/formatting.py +0 -0
  36. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/session.py +0 -0
  37. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/tabs_tools.py +0 -0
  38. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/orchestrator/tools.py +0 -0
  39. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/__init__.py +0 -0
  40. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/agents_index.py +0 -0
  41. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/atomic.py +0 -0
  42. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/layout_store.py +0 -0
  43. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/layouts_store.py +0 -0
  44. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/orchestrator_sessions.py +0 -0
  45. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/paths.py +0 -0
  46. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/themes_store.py +0 -0
  47. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/transcript_store.py +0 -0
  48. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/persistence/workspace_store.py +0 -0
  49. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/__init__.py +0 -0
  50. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/engine.py +0 -0
  51. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/theme/spec.py +0 -0
  52. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/__init__.py +0 -0
  53. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_file_lang.py +0 -0
  54. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_terminal_keys.py +0 -0
  55. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/_terminal_render.py +0 -0
  56. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/activity_feed.py +0 -0
  57. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/agent_table.py +0 -0
  58. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/agent_transcript.py +0 -0
  59. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/change_cwd_screen.py +0 -0
  60. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/chrome.py +0 -0
  61. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/diff_viewer.py +0 -0
  62. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_editor.py +0 -0
  63. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_tree.py +0 -0
  64. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/file_viewer.py +0 -0
  65. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/history_screen.py +0 -0
  66. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/layout_switcher.py +0 -0
  67. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/log_tail.py +0 -0
  68. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/markdown.py +0 -0
  69. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/new_tab_screen.py +0 -0
  70. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/notebook.py +0 -0
  71. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/orchestrator_chat.py +0 -0
  72. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/resume_screen.py +0 -0
  73. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/rich_transcript.py +0 -0
  74. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/terminal.py +0 -0
  75. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/theme_switcher.py +0 -0
  76. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/widgets/transcript_screen.py +0 -0
  77. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/workspace/__init__.py +0 -0
  78. {patchbai-0.1.0 → patchbai-0.1.2}/patchbai/workspace/spec.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: patchbai
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A Textual TUI for managing multiple Claude Code agent sessions
5
5
  Project-URL: Homepage, https://github.com/jimmymills/patchbai
6
6
  Project-URL: Repository, https://github.com/jimmymills/patchbai
@@ -294,36 +294,37 @@ repo's `.gitignore` and you should add it to yours.
294
294
  uses your `~/.claude/settings.json` for permissions and tool allowlists.
295
295
  - A terminal with TrueColor support (any modern macOS / Linux terminal).
296
296
 
297
- ### With `uv` (recommended, used by this repo)
297
+ ### From PyPI (recommended)
298
298
 
299
299
  ```bash
300
- git clone <repo> patchbai
301
- cd patchbai
302
- uv sync # install runtime deps into .venv
303
- uv run patchbai # or: uv run mt
300
+ pipx install patchbai # isolated, on PATH
301
+ patchbai # or: mt
304
302
  ```
305
303
 
306
- For development, sync the dev extras (pyright, pytest):
304
+ Or with `uv`:
307
305
 
308
306
  ```bash
309
- uv sync --extra dev
310
- uv run pytest
311
- ./scripts/typecheck.sh # canonical pyright invocation
307
+ uv tool install patchbai
308
+ patchbai
312
309
  ```
313
310
 
314
- ### With `pipx`
311
+ Or with plain `pip` into a venv:
315
312
 
316
313
  ```bash
317
- pipx install .
318
- patchbai # or: mt
314
+ python -m venv .venv && source .venv/bin/activate
315
+ pip install patchbai
316
+ patchbai
319
317
  ```
320
318
 
321
- ### Editable install with `pip`
319
+ ### From source (for hacking on patchbai itself)
322
320
 
323
321
  ```bash
324
- python -m venv .venv && source .venv/bin/activate
325
- pip install -e ".[dev]"
326
- patchbai
322
+ git clone https://github.com/jimmymills/patchbai.git
323
+ cd patchbai
324
+ uv sync --extra dev # runtime + dev deps (pyright, pytest)
325
+ uv run patchbai # or: uv run mt
326
+ uv run pytest
327
+ ./scripts/typecheck.sh # canonical pyright invocation
327
328
  ```
328
329
 
329
330
  ## Running
@@ -259,36 +259,37 @@ repo's `.gitignore` and you should add it to yours.
259
259
  uses your `~/.claude/settings.json` for permissions and tool allowlists.
260
260
  - A terminal with TrueColor support (any modern macOS / Linux terminal).
261
261
 
262
- ### With `uv` (recommended, used by this repo)
262
+ ### From PyPI (recommended)
263
263
 
264
264
  ```bash
265
- git clone <repo> patchbai
266
- cd patchbai
267
- uv sync # install runtime deps into .venv
268
- uv run patchbai # or: uv run mt
265
+ pipx install patchbai # isolated, on PATH
266
+ patchbai # or: mt
269
267
  ```
270
268
 
271
- For development, sync the dev extras (pyright, pytest):
269
+ Or with `uv`:
272
270
 
273
271
  ```bash
274
- uv sync --extra dev
275
- uv run pytest
276
- ./scripts/typecheck.sh # canonical pyright invocation
272
+ uv tool install patchbai
273
+ patchbai
277
274
  ```
278
275
 
279
- ### With `pipx`
276
+ Or with plain `pip` into a venv:
280
277
 
281
278
  ```bash
282
- pipx install .
283
- patchbai # or: mt
279
+ python -m venv .venv && source .venv/bin/activate
280
+ pip install patchbai
281
+ patchbai
284
282
  ```
285
283
 
286
- ### Editable install with `pip`
284
+ ### From source (for hacking on patchbai itself)
287
285
 
288
286
  ```bash
289
- python -m venv .venv && source .venv/bin/activate
290
- pip install -e ".[dev]"
291
- patchbai
287
+ git clone https://github.com/jimmymills/patchbai.git
288
+ cd patchbai
289
+ uv sync --extra dev # runtime + dev deps (pyright, pytest)
290
+ uv run patchbai # or: uv run mt
291
+ uv run pytest
292
+ ./scripts/typecheck.sh # canonical pyright invocation
292
293
  ```
293
294
 
294
295
  ## Running
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -43,6 +43,7 @@ from patchbai.widgets.log_tail import LogTail
43
43
  from patchbai.widgets.notebook import Notebook
44
44
  from patchbai.widgets.file_viewer import FileViewer
45
45
  from patchbai.widgets.markdown import Markdown
46
+ from patchbai.widgets.system_usage import SystemUsage
46
47
  from patchbai.persistence.orchestrator_sessions import OrchestratorSessionsIndex
47
48
  from patchbai.widgets.history_screen import HistoryScreen
48
49
  from patchbai.widgets.layout_switcher import LayoutSwitcherScreen
@@ -222,6 +223,17 @@ def build_default_registry() -> WidgetRegistry:
222
223
  ),
223
224
  props_schema={"command": list, "cwd": str, "env": dict},
224
225
  )
226
+ reg.register(
227
+ "SystemUsage", SystemUsage,
228
+ description=(
229
+ "Compact single-row CPU and RAM gauges with colored bars "
230
+ "(green / yellow / red at 50% / 80% thresholds). Auto-refreshes "
231
+ "every `interval` seconds. Uses `psutil` when available; "
232
+ "otherwise shells out non-blockingly to `top` and `vm_stat` on "
233
+ "macOS. `bar_width` controls the width of each progress bar."
234
+ ),
235
+ props_schema={"interval": float, "bar_width": int},
236
+ )
225
237
  return reg
226
238
 
227
239
 
@@ -0,0 +1,244 @@
1
+ """SystemUsage widget — compact CPU + RAM gauges with auto-refresh.
2
+
3
+ Single-row layout: ``CPU 23.4% [▰▰▰▱▱…] RAM 8.4/16.0 GiB [▰▰▰▰…]``.
4
+
5
+ `psutil` is preferred when present (cleanest, cross-platform). When absent
6
+ the widget falls back to non-blocking ``top -l 1 -n 0`` + ``vm_stat`` +
7
+ ``sysctl hw.memsize`` shell-outs on macOS via
8
+ ``asyncio.create_subprocess_exec`` so the Textual event loop is never
9
+ blocked. On unsupported platforms without psutil, the widget renders an
10
+ error banner and stops scheduling refreshes.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import re
17
+ import sys
18
+ from dataclasses import dataclass
19
+
20
+ from textual.widgets import Static
21
+
22
+
23
+ # Try psutil but never require it — patchbai's deps stay minimal. If a user
24
+ # happens to have psutil in their venv (e.g. via a transitive dep), we use
25
+ # it; otherwise we shell out on macOS.
26
+ try:
27
+ import psutil # type: ignore
28
+ _HAS_PSUTIL = True
29
+ except ImportError:
30
+ psutil = None # type: ignore
31
+ _HAS_PSUTIL = False
32
+
33
+
34
+ _FILLED = "▰"
35
+ _EMPTY = "▱"
36
+ _GIB = 1024 ** 3
37
+
38
+
39
+ @dataclass
40
+ class _Sample:
41
+ cpu_pct: float
42
+ ram_used_gib: float
43
+ ram_total_gib: float
44
+
45
+ @property
46
+ def ram_pct(self) -> float:
47
+ if self.ram_total_gib <= 0:
48
+ return 0.0
49
+ return 100.0 * self.ram_used_gib / self.ram_total_gib
50
+
51
+
52
+ def _color_for(pct: float) -> str:
53
+ if pct < 50:
54
+ return "green"
55
+ if pct < 80:
56
+ return "yellow"
57
+ return "red"
58
+
59
+
60
+ def _bar(pct: float, width: int) -> str:
61
+ pct = max(0.0, min(100.0, pct))
62
+ filled = int(round(pct / 100.0 * width))
63
+ filled = max(0, min(width, filled))
64
+ color = _color_for(pct)
65
+ return (
66
+ f"[{color}]{_FILLED * filled}[/]"
67
+ f"[dim]{_EMPTY * (width - filled)}[/]"
68
+ )
69
+
70
+
71
+ # --------------------------------------------------------------- macOS shells
72
+
73
+ _VMSTAT_PAGE_SIZE_RE = re.compile(r"page size of (\d+) bytes")
74
+ _VMSTAT_LINE_RE = re.compile(r"^([^:]+):\s+(\d+)")
75
+ _TOP_CPU_RE = re.compile(
76
+ r"CPU usage:\s+([\d.]+)%\s+user,\s+([\d.]+)%\s+sys,\s+([\d.]+)%\s+idle"
77
+ )
78
+
79
+
80
+ async def _run(*argv: str) -> str:
81
+ """Run a command without blocking the event loop; return stdout text."""
82
+ proc = await asyncio.create_subprocess_exec(
83
+ *argv,
84
+ stdout=asyncio.subprocess.PIPE,
85
+ stderr=asyncio.subprocess.DEVNULL,
86
+ )
87
+ out, _ = await proc.communicate()
88
+ return out.decode("utf-8", errors="replace")
89
+
90
+
91
+ async def _macos_cpu_pct() -> float:
92
+ """Parse ``top -l 1 -n 0`` for the CPU usage line.
93
+
94
+ `top` already samples internally; ``user + sys`` is equivalent to
95
+ ``100 - idle`` within rounding, and is more robust to spaces/punctuation
96
+ drift than parsing the idle field.
97
+ """
98
+ text = await _run("top", "-l", "1", "-n", "0")
99
+ m = _TOP_CPU_RE.search(text)
100
+ if not m:
101
+ raise RuntimeError("could not parse CPU usage from `top`")
102
+ return float(m.group(1)) + float(m.group(2))
103
+
104
+
105
+ async def _macos_ram_gib() -> tuple[float, float]:
106
+ """Return ``(used_gib, total_gib)`` using ``vm_stat`` + ``sysctl``.
107
+
108
+ ``used = (active + wired_down + occupied_by_compressor) × page_size`` —
109
+ matches Activity Monitor's "Memory Used" closely. Page size is read from
110
+ the ``vm_stat`` header (16 KiB on Apple Silicon, 4 KiB on Intel).
111
+ """
112
+ vm_text, mem_text = await asyncio.gather(
113
+ _run("vm_stat"),
114
+ _run("sysctl", "-n", "hw.memsize"),
115
+ )
116
+
117
+ page_size = 4096
118
+ m = _VMSTAT_PAGE_SIZE_RE.search(vm_text)
119
+ if m:
120
+ page_size = int(m.group(1))
121
+
122
+ pages: dict[str, int] = {}
123
+ for line in vm_text.splitlines():
124
+ mm = _VMSTAT_LINE_RE.match(line)
125
+ if mm:
126
+ pages[mm.group(1).strip().lower()] = int(mm.group(2))
127
+
128
+ active = pages.get("pages active", 0)
129
+ wired = pages.get("pages wired down", 0)
130
+ compressed = pages.get("pages occupied by compressor", 0)
131
+ used_bytes = (active + wired + compressed) * page_size
132
+
133
+ total_bytes = int(mem_text.strip() or "0")
134
+ if total_bytes <= 0:
135
+ raise RuntimeError("hw.memsize returned 0")
136
+
137
+ return used_bytes / _GIB, total_bytes / _GIB
138
+
139
+
140
+ async def _sample_shellout() -> _Sample:
141
+ cpu, ram = await asyncio.gather(_macos_cpu_pct(), _macos_ram_gib())
142
+ used, total = ram
143
+ return _Sample(cpu_pct=cpu, ram_used_gib=used, ram_total_gib=total)
144
+
145
+
146
+ async def _sample_psutil() -> _Sample:
147
+ cpu = psutil.cpu_percent(interval=None) # type: ignore[union-attr]
148
+ vm = psutil.virtual_memory() # type: ignore[union-attr]
149
+ return _Sample(
150
+ cpu_pct=float(cpu),
151
+ ram_used_gib=vm.used / _GIB,
152
+ ram_total_gib=vm.total / _GIB,
153
+ )
154
+
155
+
156
+ # ----------------------------------------------------------------- the widget
157
+
158
+
159
+ class SystemUsage(Static):
160
+ """Single-row CPU + RAM gauge that refreshes itself.
161
+
162
+ ``interval`` (seconds, clamped >= 0.25, default 1.5) controls refresh
163
+ cadence. ``bar_width`` (cells, clamped >= 2, default 12) controls the
164
+ width of each progress bar.
165
+ """
166
+
167
+ DEFAULT_CSS = """
168
+ SystemUsage {
169
+ height: auto;
170
+ min-height: 1;
171
+ padding: 0 1;
172
+ content-align: left middle;
173
+ }
174
+ """
175
+
176
+ def __init__(
177
+ self,
178
+ *,
179
+ interval: float = 1.5,
180
+ bar_width: int = 12,
181
+ **kwargs,
182
+ ) -> None:
183
+ super().__init__("CPU … RAM …", **kwargs)
184
+ self._interval = max(0.25, float(interval))
185
+ self._bar_width = max(2, int(bar_width))
186
+ self._supported = _HAS_PSUTIL or sys.platform == "darwin"
187
+ self._source = "psutil" if _HAS_PSUTIL else "shell"
188
+ self._timer = None
189
+
190
+ def on_mount(self) -> None:
191
+ # Prime psutil so the very first sample isn't 0.0 (psutil's
192
+ # cpu_percent needs a baseline tick to compute deltas against).
193
+ if _HAS_PSUTIL:
194
+ try:
195
+ psutil.cpu_percent(interval=None) # type: ignore[union-attr]
196
+ except Exception:
197
+ pass
198
+
199
+ if not self._supported:
200
+ self._show_error(
201
+ f"unsupported platform: {sys.platform} (install psutil)"
202
+ )
203
+ return
204
+
205
+ self.border_title = f"system — {self._source}"
206
+ # Kick one immediate refresh, then schedule periodic ones.
207
+ self.run_worker(self._tick(), exclusive=True)
208
+ self._timer = self.set_interval(self._interval, self._schedule_tick)
209
+
210
+ def _schedule_tick(self) -> None:
211
+ # `set_interval` fires on the event loop; offload the actual sample
212
+ # to a worker so a slow shell-out can never delay the next tick.
213
+ self.run_worker(self._tick(), exclusive=True)
214
+
215
+ async def _tick(self) -> None:
216
+ try:
217
+ if _HAS_PSUTIL:
218
+ sample = await _sample_psutil()
219
+ else:
220
+ sample = await _sample_shellout()
221
+ except Exception as exc: # never raise into the layout
222
+ self._show_error(str(exc))
223
+ return
224
+ self._show_sample(sample)
225
+
226
+ # Named ``_show_*`` rather than ``_render*`` to avoid colliding with
227
+ # ``Widget._render`` (which has a different signature/return shape).
228
+ def _show_sample(self, s: _Sample) -> None:
229
+ cpu_bar = _bar(s.cpu_pct, self._bar_width)
230
+ ram_bar = _bar(s.ram_pct, self._bar_width)
231
+ cpu_str = f"[b]CPU[/b] {s.cpu_pct:5.1f}% {cpu_bar}"
232
+ ram_str = (
233
+ f"[b]RAM[/b] {s.ram_used_gib:5.1f}/{s.ram_total_gib:.1f} GiB "
234
+ f"{ram_bar}"
235
+ )
236
+ self.update(f"{cpu_str} {ram_str}")
237
+ # Recover from a transient error: title might still say "error".
238
+ if self.border_title and "error" in self.border_title:
239
+ self.border_title = f"system — {self._source}"
240
+
241
+ def _show_error(self, msg: str) -> None:
242
+ self.update("CPU ? RAM ?")
243
+ # Truncate so the border title stays compact.
244
+ self.border_title = f"system — error: {msg[:80]}"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "patchbai"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "A Textual TUI for managing multiple Claude Code agent sessions"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes