pitwall-agents 0.1.0__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.
- pitwall_agents-0.1.0/.github/workflows/release.yml +34 -0
- pitwall_agents-0.1.0/.gitignore +8 -0
- pitwall_agents-0.1.0/LICENSE +21 -0
- pitwall_agents-0.1.0/PKG-INFO +137 -0
- pitwall_agents-0.1.0/README.md +114 -0
- pitwall_agents-0.1.0/badge/pitwall/__init__.py +239 -0
- pitwall_agents-0.1.0/badge/pitwall/icon.png +0 -0
- pitwall_agents-0.1.0/badge/pitwall/links.py +179 -0
- pitwall_agents-0.1.0/badge/pitwall/model.py +98 -0
- pitwall_agents-0.1.0/docs/protocol.md +131 -0
- pitwall_agents-0.1.0/pyproject.toml +44 -0
- pitwall_agents-0.1.0/skills/pitwall/SKILL.md +58 -0
- pitwall_agents-0.1.0/skills/pitwall-device/SKILL.md +30 -0
- pitwall_agents-0.1.0/src/pitwall/__init__.py +2 -0
- pitwall_agents-0.1.0/src/pitwall/agents/__init__.py +1 -0
- pitwall_agents-0.1.0/src/pitwall/agents/claude_code.py +50 -0
- pitwall_agents-0.1.0/src/pitwall/agents/claude_settings.py +119 -0
- pitwall_agents-0.1.0/src/pitwall/board.py +112 -0
- pitwall_agents-0.1.0/src/pitwall/cli.py +146 -0
- pitwall_agents-0.1.0/src/pitwall/config.py +102 -0
- pitwall_agents-0.1.0/src/pitwall/hook.py +43 -0
- pitwall_agents-0.1.0/src/pitwall/hub.py +140 -0
- pitwall_agents-0.1.0/src/pitwall/install_badge.py +66 -0
- pitwall_agents-0.1.0/src/pitwall/links/__init__.py +1 -0
- pitwall_agents-0.1.0/src/pitwall/links/ble.py +186 -0
- pitwall_agents-0.1.0/src/pitwall/links/usb.py +99 -0
- pitwall_agents-0.1.0/src/pitwall/pairing.py +60 -0
- pitwall_agents-0.1.0/src/pitwall/paths.py +24 -0
- pitwall_agents-0.1.0/src/pitwall/protocol.py +86 -0
- pitwall_agents-0.1.0/src/pitwall/render.py +36 -0
- pitwall_agents-0.1.0/src/pitwall/seal.py +132 -0
- pitwall_agents-0.1.0/src/pitwall/server.py +148 -0
- pitwall_agents-0.1.0/src/pitwall/service.py +90 -0
- pitwall_agents-0.1.0/src/pitwall/setup.py +121 -0
- pitwall_agents-0.1.0/src/pitwall/sim.py +88 -0
- pitwall_agents-0.1.0/src/pitwall/terminals/__init__.py +13 -0
- pitwall_agents-0.1.0/src/pitwall/terminals/iterm2.py +84 -0
- pitwall_agents-0.1.0/tests/conftest.py +35 -0
- pitwall_agents-0.1.0/tests/fakes.py +31 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/Notification_idle_prompt.json +8 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/Notification_permission_prompt.json +8 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/PermissionRequest.json +14 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/PostToolUse.json +16 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/PreToolUse.json +14 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/SessionEnd.json +8 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/SessionStart.json +7 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/Stop.json +15 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/StopFailure.json +7 -0
- pitwall_agents-0.1.0/tests/fixtures/claude_code/UserPromptSubmit.json +9 -0
- pitwall_agents-0.1.0/tests/test_badge_model.py +75 -0
- pitwall_agents-0.1.0/tests/test_ble_link.py +200 -0
- pitwall_agents-0.1.0/tests/test_board.py +123 -0
- pitwall_agents-0.1.0/tests/test_claude_code.py +54 -0
- pitwall_agents-0.1.0/tests/test_claude_settings.py +108 -0
- pitwall_agents-0.1.0/tests/test_cli.py +88 -0
- pitwall_agents-0.1.0/tests/test_config.py +52 -0
- pitwall_agents-0.1.0/tests/test_hook.py +62 -0
- pitwall_agents-0.1.0/tests/test_hub.py +178 -0
- pitwall_agents-0.1.0/tests/test_install_badge.py +65 -0
- pitwall_agents-0.1.0/tests/test_iterm2.py +66 -0
- pitwall_agents-0.1.0/tests/test_pairing.py +93 -0
- pitwall_agents-0.1.0/tests/test_protocol.py +42 -0
- pitwall_agents-0.1.0/tests/test_server.py +70 -0
- pitwall_agents-0.1.0/tests/test_service.py +59 -0
- pitwall_agents-0.1.0/tests/test_setup.py +65 -0
- pitwall_agents-0.1.0/tests/test_sim.py +19 -0
- pitwall_agents-0.1.0/tests/test_usb_link.py +151 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (v*) is pushed, or when run by hand.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
|
|
19
|
+
- name: Test
|
|
20
|
+
run: uv run --extra dev pytest -q
|
|
21
|
+
|
|
22
|
+
- name: Tag matches the package version
|
|
23
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
24
|
+
run: |
|
|
25
|
+
version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
26
|
+
test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
run: uv publish
|
|
33
|
+
env:
|
|
34
|
+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Siddhant Sinha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pitwall-agents
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pit wall for your coding agents: see every session's state on a desk device and jump to the right terminal tab.
|
|
5
|
+
Project-URL: Homepage, https://github.com/siddhant573/pitwall
|
|
6
|
+
Author: Siddhant Sinha
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: badge,bluetooth,claude-code,coding-agents,github-universe,iterm2
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: bleak>=0.22
|
|
16
|
+
Requires-Dist: mpremote>=1.24
|
|
17
|
+
Requires-Dist: pyserial>=3.5
|
|
18
|
+
Requires-Dist: tomli>=2; python_version < '3.11'
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pitwall 🏁
|
|
25
|
+
|
|
26
|
+
**A pit wall for your coding agents.** See every agent session's state on a device on your desk, and jump to the right terminal tab with one button.
|
|
27
|
+
|
|
28
|
+
If you run 15–20 Claude Code sessions in iTerm tabs, you know the loop: cycle through tabs, find the one waiting on a permission prompt, approve it, repeat. pitwall shows you which session needs you, so you can stop hunting for it.
|
|
29
|
+
|
|
30
|
+
The first device is the **GitHub Universe 2026 badge** (Pimoroni Badger 2350). The protocol is open, so a Stream Deck, a macro keypad or anything else can join (see [Write your own device](#write-your-own-device)).
|
|
31
|
+
|
|
32
|
+
<!-- docs/media/demo.gif goes here -->
|
|
33
|
+
|
|
34
|
+
## The flags
|
|
35
|
+
|
|
36
|
+
| | State | Means |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 🟢 | **Green flag** | the agent is working |
|
|
39
|
+
| 🟡 | **Box box** | the agent needs you: a permission prompt or a question |
|
|
40
|
+
| 🏁 | **Chequered flag** | the agent finished its turn |
|
|
41
|
+
| 🔴 | **Red flag** | the agent's turn failed |
|
|
42
|
+
|
|
43
|
+
The sessions waiting longest for you are always at the top.
|
|
44
|
+
|
|
45
|
+
## Install (macOS)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pipx install pitwall-agents
|
|
49
|
+
pitwall setup
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`pitwall setup` finds your badge, your agents and your terminal, shows what it will do, and after one "yes":
|
|
53
|
+
|
|
54
|
+
1. writes `~/.pitwall/config.toml`
|
|
55
|
+
2. adds pitwall's hooks to Claude Code (`~/.claude/settings.json`, with a backup)
|
|
56
|
+
3. installs the Pitwall app on the badge (plug it in over USB with a data cable)
|
|
57
|
+
4. keeps the helper running in the background with a LaunchAgent (starts at login)
|
|
58
|
+
|
|
59
|
+
Then open **Pitwall** from the badge's home menu. Your Claude Code sessions show up on their next prompt or tool call.
|
|
60
|
+
|
|
61
|
+
**Bluetooth is automatic.** While the badge is plugged in, the helper gives it a random key over the cable. Unplug it and it keeps working over Bluetooth; plug it back in and it switches to USB.
|
|
62
|
+
|
|
63
|
+
**macOS asks once** to let pitwall control iTerm2 (to switch tabs) and use Bluetooth. Allow both.
|
|
64
|
+
|
|
65
|
+
No badge? `pitwall sim` shows the same board in a terminal tab, and `pitwall status` prints it once.
|
|
66
|
+
|
|
67
|
+
## Configure
|
|
68
|
+
|
|
69
|
+
`~/.pitwall/config.toml`:
|
|
70
|
+
|
|
71
|
+
```toml
|
|
72
|
+
terminal = "iterm2"
|
|
73
|
+
agents = ["claude-code"]
|
|
74
|
+
|
|
75
|
+
[devices.badger2350] # GitHub Universe 2026 badge (Pimoroni Badger 2350)
|
|
76
|
+
transports = ["usb", "ble"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For one run, `pitwall serve --device badger2350:usb` overrides the devices (repeat `--device` for several).
|
|
80
|
+
|
|
81
|
+
| Command | Does |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `pitwall setup` | detect and install everything |
|
|
84
|
+
| `pitwall service install \| uninstall \| status \| restart` | the background LaunchAgent |
|
|
85
|
+
| `pitwall status` | print the board |
|
|
86
|
+
| `pitwall install claude-code` / `pitwall uninstall claude-code` | add or remove the hooks |
|
|
87
|
+
| `pitwall install badge` | (re)install the badge app |
|
|
88
|
+
| `pitwall serve [-v] [--device …]` | run the helper in the foreground |
|
|
89
|
+
| `pitwall sim` | a simulated device in the terminal |
|
|
90
|
+
|
|
91
|
+
Logs: `~/.pitwall/serve.log`.
|
|
92
|
+
|
|
93
|
+
## Badge buttons
|
|
94
|
+
|
|
95
|
+
| Button | Does |
|
|
96
|
+
|---|---|
|
|
97
|
+
| UP / DOWN | move the cursor |
|
|
98
|
+
| SELECT | jump to the selected session's tab |
|
|
99
|
+
| RIGHT | jump to the session that's waited longest for you (🟡) |
|
|
100
|
+
| LEFT | back to the top |
|
|
101
|
+
|
|
102
|
+
The badge's lights pulse when a session goes 🟡. If it loses the helper, it says so and dims the last board.
|
|
103
|
+
|
|
104
|
+
## What pitwall does *not* do
|
|
105
|
+
|
|
106
|
+
- **It never acts on your agents.** Devices only show status and switch tabs. There's no approving or prompting from a button.
|
|
107
|
+
- **It never reads your prompts or code.** The hook sends only the session id, the folder name and the terminal tab. Over Bluetooth, even that is encrypted after pairing.
|
|
108
|
+
- **No cloud, no accounts, no network calls.** Everything stays on your Mac and your desk.
|
|
109
|
+
- **It never slows Claude down.** The hook exits immediately (about 50 ms) even if the helper isn't running.
|
|
110
|
+
|
|
111
|
+
## Write your own device
|
|
112
|
+
|
|
113
|
+
Devices speak a tiny JSON-lines protocol. Connect to `~/.pitwall/devices.sock`, say `hello`, and you'll get a `board` every time something changes. Send `focus` to switch tabs. Everything is in **[docs/protocol.md](docs/protocol.md)**, and no pitwall code needs reading. Other agents can report sessions the same way, through `~/.pitwall/pitwall.sock`.
|
|
114
|
+
|
|
115
|
+
## Roadmap
|
|
116
|
+
|
|
117
|
+
- More devices: Stream Deck (one key per session), Logitech MX Creative Keypad
|
|
118
|
+
- More agents: Codex, Copilot CLI, Gemini CLI
|
|
119
|
+
- More terminals: Ghostty, WezTerm, tmux
|
|
120
|
+
|
|
121
|
+
## Skills for your agent
|
|
122
|
+
|
|
123
|
+
The repo ships two [Claude Code skills](skills/): `pitwall` (set up, check, troubleshoot) and `pitwall-device` (build a new device on the protocol). Copy them into `~/.claude/skills/` to use them.
|
|
124
|
+
|
|
125
|
+
## Uninstall
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pitwall service uninstall
|
|
129
|
+
pitwall uninstall claude-code
|
|
130
|
+
pipx uninstall pitwall-agents
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
On the badge, delete `apps/pitwall` while it's in disk mode.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT © Siddhant Sinha
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# pitwall 🏁
|
|
2
|
+
|
|
3
|
+
**A pit wall for your coding agents.** See every agent session's state on a device on your desk, and jump to the right terminal tab with one button.
|
|
4
|
+
|
|
5
|
+
If you run 15–20 Claude Code sessions in iTerm tabs, you know the loop: cycle through tabs, find the one waiting on a permission prompt, approve it, repeat. pitwall shows you which session needs you, so you can stop hunting for it.
|
|
6
|
+
|
|
7
|
+
The first device is the **GitHub Universe 2026 badge** (Pimoroni Badger 2350). The protocol is open, so a Stream Deck, a macro keypad or anything else can join (see [Write your own device](#write-your-own-device)).
|
|
8
|
+
|
|
9
|
+
<!-- docs/media/demo.gif goes here -->
|
|
10
|
+
|
|
11
|
+
## The flags
|
|
12
|
+
|
|
13
|
+
| | State | Means |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 🟢 | **Green flag** | the agent is working |
|
|
16
|
+
| 🟡 | **Box box** | the agent needs you: a permission prompt or a question |
|
|
17
|
+
| 🏁 | **Chequered flag** | the agent finished its turn |
|
|
18
|
+
| 🔴 | **Red flag** | the agent's turn failed |
|
|
19
|
+
|
|
20
|
+
The sessions waiting longest for you are always at the top.
|
|
21
|
+
|
|
22
|
+
## Install (macOS)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx install pitwall-agents
|
|
26
|
+
pitwall setup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`pitwall setup` finds your badge, your agents and your terminal, shows what it will do, and after one "yes":
|
|
30
|
+
|
|
31
|
+
1. writes `~/.pitwall/config.toml`
|
|
32
|
+
2. adds pitwall's hooks to Claude Code (`~/.claude/settings.json`, with a backup)
|
|
33
|
+
3. installs the Pitwall app on the badge (plug it in over USB with a data cable)
|
|
34
|
+
4. keeps the helper running in the background with a LaunchAgent (starts at login)
|
|
35
|
+
|
|
36
|
+
Then open **Pitwall** from the badge's home menu. Your Claude Code sessions show up on their next prompt or tool call.
|
|
37
|
+
|
|
38
|
+
**Bluetooth is automatic.** While the badge is plugged in, the helper gives it a random key over the cable. Unplug it and it keeps working over Bluetooth; plug it back in and it switches to USB.
|
|
39
|
+
|
|
40
|
+
**macOS asks once** to let pitwall control iTerm2 (to switch tabs) and use Bluetooth. Allow both.
|
|
41
|
+
|
|
42
|
+
No badge? `pitwall sim` shows the same board in a terminal tab, and `pitwall status` prints it once.
|
|
43
|
+
|
|
44
|
+
## Configure
|
|
45
|
+
|
|
46
|
+
`~/.pitwall/config.toml`:
|
|
47
|
+
|
|
48
|
+
```toml
|
|
49
|
+
terminal = "iterm2"
|
|
50
|
+
agents = ["claude-code"]
|
|
51
|
+
|
|
52
|
+
[devices.badger2350] # GitHub Universe 2026 badge (Pimoroni Badger 2350)
|
|
53
|
+
transports = ["usb", "ble"]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For one run, `pitwall serve --device badger2350:usb` overrides the devices (repeat `--device` for several).
|
|
57
|
+
|
|
58
|
+
| Command | Does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `pitwall setup` | detect and install everything |
|
|
61
|
+
| `pitwall service install \| uninstall \| status \| restart` | the background LaunchAgent |
|
|
62
|
+
| `pitwall status` | print the board |
|
|
63
|
+
| `pitwall install claude-code` / `pitwall uninstall claude-code` | add or remove the hooks |
|
|
64
|
+
| `pitwall install badge` | (re)install the badge app |
|
|
65
|
+
| `pitwall serve [-v] [--device …]` | run the helper in the foreground |
|
|
66
|
+
| `pitwall sim` | a simulated device in the terminal |
|
|
67
|
+
|
|
68
|
+
Logs: `~/.pitwall/serve.log`.
|
|
69
|
+
|
|
70
|
+
## Badge buttons
|
|
71
|
+
|
|
72
|
+
| Button | Does |
|
|
73
|
+
|---|---|
|
|
74
|
+
| UP / DOWN | move the cursor |
|
|
75
|
+
| SELECT | jump to the selected session's tab |
|
|
76
|
+
| RIGHT | jump to the session that's waited longest for you (🟡) |
|
|
77
|
+
| LEFT | back to the top |
|
|
78
|
+
|
|
79
|
+
The badge's lights pulse when a session goes 🟡. If it loses the helper, it says so and dims the last board.
|
|
80
|
+
|
|
81
|
+
## What pitwall does *not* do
|
|
82
|
+
|
|
83
|
+
- **It never acts on your agents.** Devices only show status and switch tabs. There's no approving or prompting from a button.
|
|
84
|
+
- **It never reads your prompts or code.** The hook sends only the session id, the folder name and the terminal tab. Over Bluetooth, even that is encrypted after pairing.
|
|
85
|
+
- **No cloud, no accounts, no network calls.** Everything stays on your Mac and your desk.
|
|
86
|
+
- **It never slows Claude down.** The hook exits immediately (about 50 ms) even if the helper isn't running.
|
|
87
|
+
|
|
88
|
+
## Write your own device
|
|
89
|
+
|
|
90
|
+
Devices speak a tiny JSON-lines protocol. Connect to `~/.pitwall/devices.sock`, say `hello`, and you'll get a `board` every time something changes. Send `focus` to switch tabs. Everything is in **[docs/protocol.md](docs/protocol.md)**, and no pitwall code needs reading. Other agents can report sessions the same way, through `~/.pitwall/pitwall.sock`.
|
|
91
|
+
|
|
92
|
+
## Roadmap
|
|
93
|
+
|
|
94
|
+
- More devices: Stream Deck (one key per session), Logitech MX Creative Keypad
|
|
95
|
+
- More agents: Codex, Copilot CLI, Gemini CLI
|
|
96
|
+
- More terminals: Ghostty, WezTerm, tmux
|
|
97
|
+
|
|
98
|
+
## Skills for your agent
|
|
99
|
+
|
|
100
|
+
The repo ships two [Claude Code skills](skills/): `pitwall` (set up, check, troubleshoot) and `pitwall-device` (build a new device on the protocol). Copy them into `~/.claude/skills/` to use them.
|
|
101
|
+
|
|
102
|
+
## Uninstall
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pitwall service uninstall
|
|
106
|
+
pitwall uninstall claude-code
|
|
107
|
+
pipx uninstall pitwall-agents
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
On the badge, delete `apps/pitwall` while it's in disk mode.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT © Siddhant Sinha
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# pitwall: a pit wall for your coding agents, on the GitHub Universe badge.
|
|
2
|
+
# Shows every agent session's state and focuses its terminal tab on a button press.
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import links
|
|
6
|
+
import model
|
|
7
|
+
|
|
8
|
+
badge.mode(HIRES | VSYNC)
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import missing_details
|
|
12
|
+
FONT = missing_details.mona_sans()
|
|
13
|
+
except Exception:
|
|
14
|
+
FONT = None
|
|
15
|
+
|
|
16
|
+
UID = badge.uid if isinstance(badge.uid, str) else "".join("%02x" % b for b in badge.uid)
|
|
17
|
+
CAPS = {"display": [320, 240], "color": True,
|
|
18
|
+
"buttons": ["up", "down", "left", "right", "select"], "keys": 0, "led": True}
|
|
19
|
+
|
|
20
|
+
bg = color.rgb(12, 14, 20)
|
|
21
|
+
panel = color.rgb(30, 34, 46)
|
|
22
|
+
white = color.rgb(238, 240, 246)
|
|
23
|
+
dim = color.rgb(238, 240, 246, 110)
|
|
24
|
+
faint = color.rgb(238, 240, 246, 50)
|
|
25
|
+
sub = color.rgb(238, 240, 246, 165)
|
|
26
|
+
amber = color.rgb(255, 196, 40)
|
|
27
|
+
red = color.rgb(240, 70, 70)
|
|
28
|
+
green = color.rgb(80, 210, 110)
|
|
29
|
+
black = color.rgb(20, 20, 20)
|
|
30
|
+
|
|
31
|
+
STATE_PEN = {"box": amber, "red": red, "green": green}
|
|
32
|
+
STATE_WORD = {"red": "failed", "done": "done", "green": "working"}
|
|
33
|
+
|
|
34
|
+
ROW_TOP, ROW_H, ROWS = 27, 31, 6
|
|
35
|
+
FOOT_Y = 215
|
|
36
|
+
|
|
37
|
+
view = model.View()
|
|
38
|
+
usb = links.Usb()
|
|
39
|
+
try:
|
|
40
|
+
ble = links.Ble(UID, {"type": "hello", "device": "badger2350", "uid": UID, "transport": "ble", "caps": CAPS})
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print("pitwall: bluetooth unavailable", e)
|
|
43
|
+
ble = None
|
|
44
|
+
|
|
45
|
+
last_rx = {"usb": None, "ble": None}
|
|
46
|
+
last_hello_ms = -10000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def hello(transport):
|
|
50
|
+
return {"type": "hello", "device": "badger2350", "uid": UID, "transport": transport, "caps": CAPS}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def active_transport(now):
|
|
54
|
+
if last_rx["usb"] is not None and now - last_rx["usb"] <= model.STALE_MS:
|
|
55
|
+
return "usb"
|
|
56
|
+
if ble is not None and ble.authed:
|
|
57
|
+
return "ble"
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def send(msg, now):
|
|
62
|
+
t = active_transport(now)
|
|
63
|
+
if t == "usb":
|
|
64
|
+
usb.send(msg)
|
|
65
|
+
elif t == "ble":
|
|
66
|
+
ble.send(msg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def handle(msg, transport, now):
|
|
70
|
+
kind = msg.get("type")
|
|
71
|
+
if kind == "hello":
|
|
72
|
+
if transport == "usb":
|
|
73
|
+
usb.send(hello("usb"))
|
|
74
|
+
return
|
|
75
|
+
if kind == "pair_key" and transport == "usb":
|
|
76
|
+
# USB is a local cable: the helper hands over its Bluetooth key here
|
|
77
|
+
try:
|
|
78
|
+
host, key = msg["host"], bytes.fromhex(msg["key"])
|
|
79
|
+
except Exception:
|
|
80
|
+
return
|
|
81
|
+
if len(key) == 32:
|
|
82
|
+
if ble is not None:
|
|
83
|
+
ble.add_key(host, key)
|
|
84
|
+
else:
|
|
85
|
+
keys = links.load_keys()
|
|
86
|
+
keys[host] = key
|
|
87
|
+
links.save_keys(keys)
|
|
88
|
+
usb.send({"type": "paired", "host": host})
|
|
89
|
+
return
|
|
90
|
+
if kind in ("board", "alert"):
|
|
91
|
+
last_rx[transport] = now
|
|
92
|
+
# while USB is live, ignore a stale BLE board (the helper only talks on one anyway)
|
|
93
|
+
if transport == "ble" and active_transport(now) == "usb":
|
|
94
|
+
return
|
|
95
|
+
view.on_message(msg, now)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def text(s, x, y, size, pen):
|
|
99
|
+
screen.pen = pen
|
|
100
|
+
screen.text(s, x, y, size)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def text_w(s, size):
|
|
104
|
+
return screen.measure_text(s, size)[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def chip(state, x, y, s, flash):
|
|
108
|
+
if state == "done":
|
|
109
|
+
half = s / 2
|
|
110
|
+
for i in range(2):
|
|
111
|
+
for j in range(2):
|
|
112
|
+
screen.pen = white if (i + j) % 2 == 0 else black
|
|
113
|
+
screen.shape(shape.rectangle(x + i * half, y + j * half, half, half))
|
|
114
|
+
return
|
|
115
|
+
pen = STATE_PEN.get(state, dim)
|
|
116
|
+
if state == "box" and flash:
|
|
117
|
+
pen = white
|
|
118
|
+
screen.pen = pen
|
|
119
|
+
screen.shape(shape.rounded_rectangle(x, y, s, s, 3))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def draw_header(now):
|
|
123
|
+
screen.pen = panel
|
|
124
|
+
screen.shape(shape.rectangle(0, 0, 320, 24))
|
|
125
|
+
x = 8
|
|
126
|
+
for state in ("box", "red", "done", "green"):
|
|
127
|
+
chip(state, x, 6, 12, False)
|
|
128
|
+
n = str(view.counts.get(state, 0))
|
|
129
|
+
text(n, x + 16, 4, 14, white)
|
|
130
|
+
x += 26 + text_w(n, 14)
|
|
131
|
+
t = active_transport(now)
|
|
132
|
+
link = "USB" if t == "usb" else ("BT" if t == "ble" else "--")
|
|
133
|
+
try:
|
|
134
|
+
level = int(badge.battery_level())
|
|
135
|
+
except Exception:
|
|
136
|
+
level = None
|
|
137
|
+
right = link + (" %d%%" % level if level is not None else "")
|
|
138
|
+
if len(view.sessions) > ROWS:
|
|
139
|
+
right = "%d/%d " % (view.cursor + 1, len(view.sessions)) + right
|
|
140
|
+
text(right, 312 - text_w(right, 12), 6, 12, dim if t else red)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def fit(s, size, max_w):
|
|
144
|
+
"""Cut text to fit max_w pixels, with an ellipsis."""
|
|
145
|
+
s = str(s)
|
|
146
|
+
if text_w(s, size) <= max_w:
|
|
147
|
+
return s
|
|
148
|
+
while s and text_w(s + "…", size) > max_w:
|
|
149
|
+
s = s[:-1]
|
|
150
|
+
return s + "…"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def draw_rows(now, stale):
|
|
154
|
+
rows = view.rows(ROWS)
|
|
155
|
+
if not rows:
|
|
156
|
+
msg = "No agent sessions yet" if not stale else "Waiting for pit wall…"
|
|
157
|
+
text(msg, 160 - text_w(msg, 16) / 2, 105, 16, dim)
|
|
158
|
+
return
|
|
159
|
+
flash = view.alerting(now) and (now // 250) % 2 == 0
|
|
160
|
+
for idx, (i, s, selected) in enumerate(rows):
|
|
161
|
+
y = ROW_TOP + idx * ROW_H
|
|
162
|
+
if selected:
|
|
163
|
+
screen.pen = panel
|
|
164
|
+
screen.shape(shape.rounded_rectangle(4, y, 312, ROW_H - 2, 6))
|
|
165
|
+
state = s.get("state", "")
|
|
166
|
+
chip(state, 12, y + 8, 13, flash and s.get("id") == view.alert_id)
|
|
167
|
+
if state == "box":
|
|
168
|
+
right, rpen = model.fmt_age(s.get("age_s", 0)), amber
|
|
169
|
+
else:
|
|
170
|
+
right, rpen = STATE_WORD.get(state, ""), dim
|
|
171
|
+
rw = text_w(right, 12)
|
|
172
|
+
text(right, 308 - rw, y + 9, 12, rpen if not stale else faint)
|
|
173
|
+
max_w = 308 - rw - 10 - 34
|
|
174
|
+
live = s.get("focusable", True) and not stale
|
|
175
|
+
title, folder = s.get("title"), str(s.get("label", ""))
|
|
176
|
+
if title and title.strip().lower() != folder.strip().lower():
|
|
177
|
+
text(fit(title, 14, max_w), 34, y + 2, 14, white if live else dim)
|
|
178
|
+
text(fit(folder, 11, max_w), 34, y + 17, 11, sub if live else faint)
|
|
179
|
+
else:
|
|
180
|
+
text(fit(title or folder, 14, max_w), 34, y + 8, 14, white if live else dim)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def draw_footer(stale):
|
|
184
|
+
screen.pen = red if stale else panel
|
|
185
|
+
screen.shape(shape.rectangle(0, FOOT_Y + 2, 320, 240 - FOOT_Y - 2))
|
|
186
|
+
if stale:
|
|
187
|
+
msg = "No connection to pit wall"
|
|
188
|
+
text(msg, 160 - text_w(msg, 12) / 2, FOOT_Y + 5, 12, white)
|
|
189
|
+
return
|
|
190
|
+
x = 8
|
|
191
|
+
for key, what in (("SELECT", "focus"), ("RIGHT", "next box box"), ("LEFT", "top")):
|
|
192
|
+
text(key, x, FOOT_Y + 5, 12, amber)
|
|
193
|
+
x += text_w(key, 12) + 4
|
|
194
|
+
text(what, x, FOOT_Y + 5, 12, white)
|
|
195
|
+
x += text_w(what, 12) + 12
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def update():
|
|
199
|
+
global last_hello_ms
|
|
200
|
+
now = badge.ticks
|
|
201
|
+
for m in usb.read():
|
|
202
|
+
handle(m, "usb", now)
|
|
203
|
+
if ble is not None:
|
|
204
|
+
for m in ble.read():
|
|
205
|
+
handle(m, "ble", now)
|
|
206
|
+
|
|
207
|
+
if badge.pressed(BUTTON_UP):
|
|
208
|
+
view.move(-1)
|
|
209
|
+
elif badge.pressed(BUTTON_DOWN):
|
|
210
|
+
view.move(1)
|
|
211
|
+
elif badge.pressed(BUTTON_LEFT):
|
|
212
|
+
view.move(-len(view.sessions))
|
|
213
|
+
elif badge.pressed(BUTTON_SELECT):
|
|
214
|
+
sid = view.selected_id()
|
|
215
|
+
if sid:
|
|
216
|
+
send({"type": "focus", "id": sid}, now)
|
|
217
|
+
elif badge.pressed(BUTTON_RIGHT):
|
|
218
|
+
send({"type": "focus_next_waiting"}, now)
|
|
219
|
+
|
|
220
|
+
stale = view.stale(now)
|
|
221
|
+
if stale and now - last_hello_ms > 5000:
|
|
222
|
+
usb.send(hello("usb")) # the helper may have missed our first hello
|
|
223
|
+
last_hello_ms = now
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
badge.caselights((0.5 + 0.5 * math.sin(now / 120)) if view.alerting(now) else 0)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
if FONT is not None:
|
|
231
|
+
screen.font = FONT
|
|
232
|
+
screen.pen = bg
|
|
233
|
+
screen.clear()
|
|
234
|
+
draw_header(now)
|
|
235
|
+
draw_rows(now, stale)
|
|
236
|
+
draw_footer(stale)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
run(update)
|
|
Binary file
|