promptparty 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.
- promptparty-0.1.0/.gitignore +8 -0
- promptparty-0.1.0/LICENSE +21 -0
- promptparty-0.1.0/PKG-INFO +136 -0
- promptparty-0.1.0/README.md +115 -0
- promptparty-0.1.0/docs/demo.gif +0 -0
- promptparty-0.1.0/promptparty/__init__.py +3 -0
- promptparty-0.1.0/promptparty/cli.py +151 -0
- promptparty-0.1.0/promptparty/server.py +189 -0
- promptparty-0.1.0/promptparty/static/index.html +284 -0
- promptparty-0.1.0/pyproject.toml +35 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 k.moudra@seznam.cz
|
|
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,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: promptparty
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cadence sync for teams agent-coding in the same room β one dashboard that says when to PROMPT and when to TALK.
|
|
5
|
+
Project-URL: Homepage, https://github.com/moudrkat/promptparty
|
|
6
|
+
Project-URL: Issues, https://github.com/moudrkat/promptparty/issues
|
|
7
|
+
Author-email: k.moudra@seznam.cz
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agentic-coding,claude,claude-code,collaboration,hackathon,hooks,llm,pair-programming
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Communications :: Chat
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: fastapi>=0.110
|
|
19
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# promptparty π
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**Cadence sync for teams agent-coding in the same room.** One shared dashboard
|
|
27
|
+
that tells everyone when it's time to **β¨οΈ PROMPT** and when it's time to
|
|
28
|
+
**π£οΈ TALK** β because the worst part of group agentic coding is everyone
|
|
29
|
+
heads-down at different moments, waiting on each other, and the conversation
|
|
30
|
+
dying.
|
|
31
|
+
|
|
32
|
+
The rhythm promptparty gives you:
|
|
33
|
+
|
|
34
|
+
1. **β¨οΈ PROMPT** β someone's agent finished (or needs a permission). Read the
|
|
35
|
+
output, fire the next prompt.
|
|
36
|
+
2. **π£οΈ TALK** β *all* agents are generating. Hands off the keyboard: discuss
|
|
37
|
+
what's coming, plan the next move, actually talk to each other.
|
|
38
|
+
3. Agents finish β the dashboard flips back to PROMPT (with an optional chime).
|
|
39
|
+
|
|
40
|
+
Everyone keeps their **own terminal, own Claude Code session, own model** β
|
|
41
|
+
promptparty never calls a model and needs **no API key**. It only watches the
|
|
42
|
+
states.
|
|
43
|
+
|
|
44
|
+
## Quickstart (60 seconds)
|
|
45
|
+
|
|
46
|
+
**One person** starts the server and opens a room (put it on a big screen or a
|
|
47
|
+
spare browser tab):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx promptparty # or: pipx install promptparty && promptparty
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Each teammate** runs one command inside the project they're coding in:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uvx promptparty hooks --server http://192.168.1.20:8765 --room a1b2c3 --name Katka
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
(The dashboard's βοΈ panel shows this exact command with your room pre-filled.)
|
|
60
|
+
That installs [Claude Code hooks](https://code.claude.com/docs) into
|
|
61
|
+
`./.claude/settings.json` that report automatically:
|
|
62
|
+
|
|
63
|
+
| you do | dashboard shows |
|
|
64
|
+
|---|---|
|
|
65
|
+
| submit a prompt (`UserPromptSubmit`) | π€ agent running β *free to talk* |
|
|
66
|
+
| agent finishes its turn (`Stop`) | β
agent done β *read & prompt!* |
|
|
67
|
+
| agent asks for permission (`Notification`) | π needs a decision |
|
|
68
|
+
|
|
69
|
+
Restart your Claude Code session (or reload hooks) and you're live.
|
|
70
|
+
|
|
71
|
+
### Not using Claude Code?
|
|
72
|
+
|
|
73
|
+
Any tool can report with one request β wire it into whatever hooks your tool
|
|
74
|
+
has, or an alias:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
curl -X POST http://SERVER:8765/api/ROOM/event \
|
|
78
|
+
-H 'content-type: application/json' \
|
|
79
|
+
-d '{"name":"Katka","state":"generating"}'
|
|
80
|
+
# states: prompting | generating | ready | blocked | away
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
There are also manual buttons at the bottom of the dashboard.
|
|
84
|
+
|
|
85
|
+
## What you get
|
|
86
|
+
|
|
87
|
+
- π¦ A giant phase banner: **TALK** (all agents cooking) / **PROMPT**
|
|
88
|
+
(with *who* the room is waiting on).
|
|
89
|
+
- π A card per person: state, model, how long they've been in it. Blocked
|
|
90
|
+
agents pulse red so permission prompts stop rotting unnoticed.
|
|
91
|
+
- π Optional chime on phase flips β you don't even need the screen in view.
|
|
92
|
+
- π Session stats: talk time vs prompt time and total prompts fired.
|
|
93
|
+
(Great post-hackathon bragging material.)
|
|
94
|
+
- π§Ή Zero config, zero database, zero API keys. Rooms live in memory.
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
A single FastAPI process keeps per-room state and pushes it to dashboards over
|
|
99
|
+
WebSockets. Hook reports are plain HTTP POSTs. The phase rule is one line:
|
|
100
|
+
|
|
101
|
+
> if every non-away member's agent is generating β **TALK**, otherwise β **PROMPT**.
|
|
102
|
+
|
|
103
|
+
That's the whole trick. It works because agent turns are long enough (tens of
|
|
104
|
+
seconds to minutes) that "all agents running" is a genuine conversational
|
|
105
|
+
window.
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
promptparty [serve] [--port 8765] [--host 0.0.0.0]
|
|
111
|
+
promptparty hooks --server URL --room SLUG --name YOU [--settings PATH]
|
|
112
|
+
promptparty hooks --remove [--settings PATH]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Playing from different networks? Put the server behind any tunnel
|
|
116
|
+
(`ngrok http 8765`, `cloudflared tunnel --url http://localhost:8765`).
|
|
117
|
+
|
|
118
|
+
## Roadmap
|
|
119
|
+
|
|
120
|
+
- [ ] `PreCompact` / `SubagentStop` hook nuances (long multi-agent turns)
|
|
121
|
+
- [ ] Per-room history graph (talk/prompt timeline you can screenshot)
|
|
122
|
+
- [ ] Team pomodoro mode: enforced talk windows
|
|
123
|
+
- [ ] Adapters for aider / codex / opencode hooks out of the box
|
|
124
|
+
|
|
125
|
+
PRs welcome β this was born at a hackathon and stays intentionally tiny: one
|
|
126
|
+
Python file of server, one HTML file of dashboard, no build step.
|
|
127
|
+
|
|
128
|
+
## Security notes
|
|
129
|
+
|
|
130
|
+
- Anyone with the room URL can view and post states. It's presence data, not
|
|
131
|
+
code β but run it on a network you trust.
|
|
132
|
+
- Rooms are in-memory and vanish on restart.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT Β© [moudrkat](https://github.com/moudrkat)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# promptparty π
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Cadence sync for teams agent-coding in the same room.** One shared dashboard
|
|
6
|
+
that tells everyone when it's time to **β¨οΈ PROMPT** and when it's time to
|
|
7
|
+
**π£οΈ TALK** β because the worst part of group agentic coding is everyone
|
|
8
|
+
heads-down at different moments, waiting on each other, and the conversation
|
|
9
|
+
dying.
|
|
10
|
+
|
|
11
|
+
The rhythm promptparty gives you:
|
|
12
|
+
|
|
13
|
+
1. **β¨οΈ PROMPT** β someone's agent finished (or needs a permission). Read the
|
|
14
|
+
output, fire the next prompt.
|
|
15
|
+
2. **π£οΈ TALK** β *all* agents are generating. Hands off the keyboard: discuss
|
|
16
|
+
what's coming, plan the next move, actually talk to each other.
|
|
17
|
+
3. Agents finish β the dashboard flips back to PROMPT (with an optional chime).
|
|
18
|
+
|
|
19
|
+
Everyone keeps their **own terminal, own Claude Code session, own model** β
|
|
20
|
+
promptparty never calls a model and needs **no API key**. It only watches the
|
|
21
|
+
states.
|
|
22
|
+
|
|
23
|
+
## Quickstart (60 seconds)
|
|
24
|
+
|
|
25
|
+
**One person** starts the server and opens a room (put it on a big screen or a
|
|
26
|
+
spare browser tab):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx promptparty # or: pipx install promptparty && promptparty
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Each teammate** runs one command inside the project they're coding in:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uvx promptparty hooks --server http://192.168.1.20:8765 --room a1b2c3 --name Katka
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
(The dashboard's βοΈ panel shows this exact command with your room pre-filled.)
|
|
39
|
+
That installs [Claude Code hooks](https://code.claude.com/docs) into
|
|
40
|
+
`./.claude/settings.json` that report automatically:
|
|
41
|
+
|
|
42
|
+
| you do | dashboard shows |
|
|
43
|
+
|---|---|
|
|
44
|
+
| submit a prompt (`UserPromptSubmit`) | π€ agent running β *free to talk* |
|
|
45
|
+
| agent finishes its turn (`Stop`) | β
agent done β *read & prompt!* |
|
|
46
|
+
| agent asks for permission (`Notification`) | π needs a decision |
|
|
47
|
+
|
|
48
|
+
Restart your Claude Code session (or reload hooks) and you're live.
|
|
49
|
+
|
|
50
|
+
### Not using Claude Code?
|
|
51
|
+
|
|
52
|
+
Any tool can report with one request β wire it into whatever hooks your tool
|
|
53
|
+
has, or an alias:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl -X POST http://SERVER:8765/api/ROOM/event \
|
|
57
|
+
-H 'content-type: application/json' \
|
|
58
|
+
-d '{"name":"Katka","state":"generating"}'
|
|
59
|
+
# states: prompting | generating | ready | blocked | away
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
There are also manual buttons at the bottom of the dashboard.
|
|
63
|
+
|
|
64
|
+
## What you get
|
|
65
|
+
|
|
66
|
+
- π¦ A giant phase banner: **TALK** (all agents cooking) / **PROMPT**
|
|
67
|
+
(with *who* the room is waiting on).
|
|
68
|
+
- π A card per person: state, model, how long they've been in it. Blocked
|
|
69
|
+
agents pulse red so permission prompts stop rotting unnoticed.
|
|
70
|
+
- π Optional chime on phase flips β you don't even need the screen in view.
|
|
71
|
+
- π Session stats: talk time vs prompt time and total prompts fired.
|
|
72
|
+
(Great post-hackathon bragging material.)
|
|
73
|
+
- π§Ή Zero config, zero database, zero API keys. Rooms live in memory.
|
|
74
|
+
|
|
75
|
+
## How it works
|
|
76
|
+
|
|
77
|
+
A single FastAPI process keeps per-room state and pushes it to dashboards over
|
|
78
|
+
WebSockets. Hook reports are plain HTTP POSTs. The phase rule is one line:
|
|
79
|
+
|
|
80
|
+
> if every non-away member's agent is generating β **TALK**, otherwise β **PROMPT**.
|
|
81
|
+
|
|
82
|
+
That's the whole trick. It works because agent turns are long enough (tens of
|
|
83
|
+
seconds to minutes) that "all agents running" is a genuine conversational
|
|
84
|
+
window.
|
|
85
|
+
|
|
86
|
+
## Options
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
promptparty [serve] [--port 8765] [--host 0.0.0.0]
|
|
90
|
+
promptparty hooks --server URL --room SLUG --name YOU [--settings PATH]
|
|
91
|
+
promptparty hooks --remove [--settings PATH]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Playing from different networks? Put the server behind any tunnel
|
|
95
|
+
(`ngrok http 8765`, `cloudflared tunnel --url http://localhost:8765`).
|
|
96
|
+
|
|
97
|
+
## Roadmap
|
|
98
|
+
|
|
99
|
+
- [ ] `PreCompact` / `SubagentStop` hook nuances (long multi-agent turns)
|
|
100
|
+
- [ ] Per-room history graph (talk/prompt timeline you can screenshot)
|
|
101
|
+
- [ ] Team pomodoro mode: enforced talk windows
|
|
102
|
+
- [ ] Adapters for aider / codex / opencode hooks out of the box
|
|
103
|
+
|
|
104
|
+
PRs welcome β this was born at a hackathon and stays intentionally tiny: one
|
|
105
|
+
Python file of server, one HTML file of dashboard, no build step.
|
|
106
|
+
|
|
107
|
+
## Security notes
|
|
108
|
+
|
|
109
|
+
- Anyone with the room URL can view and post states. It's presence data, not
|
|
110
|
+
code β but run it on a network you trust.
|
|
111
|
+
- Rooms are in-memory and vanish on restart.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT Β© [moudrkat](https://github.com/moudrkat)
|
|
Binary file
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""promptparty CLI.
|
|
2
|
+
|
|
3
|
+
promptparty start the server (default)
|
|
4
|
+
promptparty hooks --server URL --room SLUG --name YOU
|
|
5
|
+
wire Claude Code hooks in ./.claude/settings.json
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import socket
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Claude Code hook event -> promptparty state
|
|
18
|
+
HOOK_EVENTS = {
|
|
19
|
+
"UserPromptSubmit": "generating", # you hit enter -> agent starts working
|
|
20
|
+
"Stop": "ready", # agent finished its turn -> read & prompt
|
|
21
|
+
"Notification": "blocked", # agent waits on a permission / question
|
|
22
|
+
"SessionStart": "ready",
|
|
23
|
+
"SessionEnd": "away",
|
|
24
|
+
}
|
|
25
|
+
MARKER = "promptparty-hook"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _lan_ip() -> str:
|
|
29
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
30
|
+
try:
|
|
31
|
+
s.connect(("10.255.255.255", 1))
|
|
32
|
+
return s.getsockname()[0]
|
|
33
|
+
except Exception:
|
|
34
|
+
return "127.0.0.1"
|
|
35
|
+
finally:
|
|
36
|
+
s.close()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_serve(args: argparse.Namespace) -> None:
|
|
40
|
+
ip = _lan_ip()
|
|
41
|
+
print("\n promptparty is up β open a room and put it on a shared screen:\n")
|
|
42
|
+
print(f" local: http://127.0.0.1:{args.port}/")
|
|
43
|
+
print(f" network: http://{ip}:{args.port}/\n")
|
|
44
|
+
print(" Opening / creates a fresh room; share the /r/... URL with the room.")
|
|
45
|
+
print(" Expand the βοΈ panel on the dashboard for the per-person hook command.\n")
|
|
46
|
+
|
|
47
|
+
import uvicorn
|
|
48
|
+
|
|
49
|
+
uvicorn.run("promptparty.server:app", host=args.host, port=args.port,
|
|
50
|
+
log_level="warning")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hook_command(url: str, name: str, state: str) -> str:
|
|
54
|
+
payload = json.dumps({"name": name, "state": state})
|
|
55
|
+
return (
|
|
56
|
+
f"curl -sm2 -A {MARKER} -X POST {url} "
|
|
57
|
+
f"-H 'content-type: application/json' -d '{payload}' >/dev/null 2>&1 || true"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_hooks(args: argparse.Namespace) -> None:
|
|
62
|
+
url = f"{args.server.rstrip('/')}/api/{args.room}/event"
|
|
63
|
+
settings_path = Path(args.settings)
|
|
64
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
data = {}
|
|
67
|
+
if settings_path.exists():
|
|
68
|
+
try:
|
|
69
|
+
data = json.loads(settings_path.read_text())
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
print(f"error: {settings_path} is not valid JSON β fix it first.",
|
|
72
|
+
file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
hooks = data.setdefault("hooks", {})
|
|
76
|
+
for event, state in HOOK_EVENTS.items():
|
|
77
|
+
entries = hooks.get(event, [])
|
|
78
|
+
# drop any previous promptparty hooks (re-running updates server/room/name)
|
|
79
|
+
entries = [e for e in entries if MARKER not in json.dumps(e)]
|
|
80
|
+
entries.append({
|
|
81
|
+
"hooks": [{"type": "command",
|
|
82
|
+
"command": _hook_command(url, args.name, state)}],
|
|
83
|
+
})
|
|
84
|
+
hooks[event] = entries
|
|
85
|
+
|
|
86
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
87
|
+
print(f"β promptparty hooks for '{args.name}' written to {settings_path}")
|
|
88
|
+
print(f" reporting to {url}")
|
|
89
|
+
print(" Restart (or /hooks reload) your Claude Code session to pick them up.")
|
|
90
|
+
print(f" Undo any time: promptparty hooks --remove --settings {settings_path}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_hooks_remove(args: argparse.Namespace) -> None:
|
|
94
|
+
settings_path = Path(args.settings)
|
|
95
|
+
if not settings_path.exists():
|
|
96
|
+
print(f"{settings_path} does not exist β nothing to remove.")
|
|
97
|
+
return
|
|
98
|
+
data = json.loads(settings_path.read_text())
|
|
99
|
+
hooks = data.get("hooks", {})
|
|
100
|
+
removed = 0
|
|
101
|
+
for event in list(hooks):
|
|
102
|
+
before = len(hooks[event])
|
|
103
|
+
hooks[event] = [e for e in hooks[event] if MARKER not in json.dumps(e)]
|
|
104
|
+
removed += before - len(hooks[event])
|
|
105
|
+
if not hooks[event]:
|
|
106
|
+
del hooks[event]
|
|
107
|
+
settings_path.write_text(json.dumps(data, indent=2) + "\n")
|
|
108
|
+
print(f"β removed {removed} promptparty hook(s) from {settings_path}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main() -> None:
|
|
112
|
+
parser = argparse.ArgumentParser(
|
|
113
|
+
prog="promptparty",
|
|
114
|
+
description="Cadence sync for people agent-coding in the same room: "
|
|
115
|
+
"one dashboard that says when to PROMPT and when to TALK.",
|
|
116
|
+
)
|
|
117
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
118
|
+
|
|
119
|
+
p_serve = sub.add_parser("serve", help="start the server (default)")
|
|
120
|
+
p_serve.add_argument("--host", default="0.0.0.0")
|
|
121
|
+
p_serve.add_argument("--port", type=int, default=8765)
|
|
122
|
+
|
|
123
|
+
p_hooks = sub.add_parser(
|
|
124
|
+
"hooks", help="install Claude Code hooks that auto-report your agent state")
|
|
125
|
+
p_hooks.add_argument("--server", help="server URL, e.g. http://192.168.1.20:8765")
|
|
126
|
+
p_hooks.add_argument("--room", help="room slug from the dashboard URL (/r/<slug>)")
|
|
127
|
+
p_hooks.add_argument("--name", help="your name as shown on the dashboard")
|
|
128
|
+
p_hooks.add_argument("--settings", default=".claude/settings.json",
|
|
129
|
+
help="settings file to edit (default: ./.claude/settings.json)")
|
|
130
|
+
p_hooks.add_argument("--remove", action="store_true",
|
|
131
|
+
help="remove promptparty hooks instead of installing")
|
|
132
|
+
|
|
133
|
+
args = parser.parse_args()
|
|
134
|
+
|
|
135
|
+
if args.cmd == "hooks":
|
|
136
|
+
if args.remove:
|
|
137
|
+
cmd_hooks_remove(args)
|
|
138
|
+
return
|
|
139
|
+
missing = [f"--{k}" for k in ("server", "room", "name") if not getattr(args, k)]
|
|
140
|
+
if missing:
|
|
141
|
+
print(f"error: missing {', '.join(missing)}", file=sys.stderr)
|
|
142
|
+
sys.exit(2)
|
|
143
|
+
cmd_hooks(args)
|
|
144
|
+
else:
|
|
145
|
+
if args.cmd is None:
|
|
146
|
+
args.host, args.port = "0.0.0.0", 8765
|
|
147
|
+
cmd_serve(args)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""promptparty server β a cadence synchronizer for people agent-coding in the
|
|
2
|
+
same physical room.
|
|
3
|
+
|
|
4
|
+
Everyone prompts their own agent (Claude Code or anything else) in their own
|
|
5
|
+
terminal; the agents report state changes here (via hooks or a curl one-liner)
|
|
6
|
+
and one shared dashboard tells the room when it's time to PROMPT (someone's
|
|
7
|
+
agent is waiting for input) and when it's time to TALK (all agents are
|
|
8
|
+
generating β hands off the keyboard).
|
|
9
|
+
|
|
10
|
+
No model is called from here and no API key is needed.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import secrets
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
|
22
|
+
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
|
23
|
+
|
|
24
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
25
|
+
|
|
26
|
+
# Member states, in "who's holding the room up" order:
|
|
27
|
+
# blocked β agent needs a human decision (permission prompt etc.)
|
|
28
|
+
# ready β agent finished, output waiting to be read / next prompt due
|
|
29
|
+
# prompting β human is typing a prompt right now
|
|
30
|
+
# generating β agent is working; the human is FREE TO TALK
|
|
31
|
+
# away β not participating right now
|
|
32
|
+
STATES = {"blocked", "ready", "prompting", "generating", "away"}
|
|
33
|
+
|
|
34
|
+
app = FastAPI(title="promptparty")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Room:
|
|
39
|
+
slug: str
|
|
40
|
+
members: dict[str, dict] = field(default_factory=dict)
|
|
41
|
+
sockets: set = field(default_factory=set)
|
|
42
|
+
phase: str = "idle"
|
|
43
|
+
phase_since: float = field(default_factory=time.time) # display timer
|
|
44
|
+
last_flush: float = field(default_factory=time.time) # totals accounting
|
|
45
|
+
totals: dict = field(default_factory=lambda: {"talk": 0.0, "prompt": 0.0})
|
|
46
|
+
prompts_fired: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
rooms: dict[str, Room] = {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_room(slug: str) -> Room:
|
|
53
|
+
if slug not in rooms:
|
|
54
|
+
rooms[slug] = Room(slug=slug)
|
|
55
|
+
return rooms[slug]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def compute_phase(room: Room) -> str:
|
|
59
|
+
active = [m for m in room.members.values() if m["state"] != "away"]
|
|
60
|
+
if not active:
|
|
61
|
+
return "idle"
|
|
62
|
+
if all(m["state"] == "generating" for m in active):
|
|
63
|
+
return "talk"
|
|
64
|
+
return "prompt"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def snapshot(room: Room) -> dict:
|
|
68
|
+
return {
|
|
69
|
+
"type": "state",
|
|
70
|
+
"room": room.slug,
|
|
71
|
+
"phase": room.phase,
|
|
72
|
+
"phase_since": room.phase_since,
|
|
73
|
+
"last_flush": room.last_flush,
|
|
74
|
+
"totals": room.totals,
|
|
75
|
+
"prompts_fired": room.prompts_fired,
|
|
76
|
+
"now": time.time(),
|
|
77
|
+
"members": [
|
|
78
|
+
{"name": n, **m} for n, m in sorted(room.members.items())
|
|
79
|
+
],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def broadcast(room: Room) -> None:
|
|
84
|
+
msg = snapshot(room)
|
|
85
|
+
dead = []
|
|
86
|
+
for ws in list(room.sockets):
|
|
87
|
+
try:
|
|
88
|
+
await ws.send_json(msg)
|
|
89
|
+
except Exception:
|
|
90
|
+
dead.append(ws)
|
|
91
|
+
for ws in dead:
|
|
92
|
+
room.sockets.discard(ws)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _transition(room: Room) -> None:
|
|
96
|
+
"""Flush elapsed time into totals and recompute the phase."""
|
|
97
|
+
now = time.time()
|
|
98
|
+
if room.phase in ("talk", "prompt"):
|
|
99
|
+
room.totals[room.phase] += now - room.last_flush
|
|
100
|
+
room.last_flush = now
|
|
101
|
+
new = compute_phase(room)
|
|
102
|
+
if new != room.phase:
|
|
103
|
+
room.phase = new
|
|
104
|
+
room.phase_since = now
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def set_state(room: Room, name: str, state: str,
|
|
108
|
+
detail: str = "", model: str = "") -> None:
|
|
109
|
+
name = name.strip()[:32] or "anon"
|
|
110
|
+
if state not in STATES:
|
|
111
|
+
state = "ready"
|
|
112
|
+
member = room.members.setdefault(
|
|
113
|
+
name, {"state": "ready", "since": time.time(), "detail": "", "model": ""}
|
|
114
|
+
)
|
|
115
|
+
if state == "generating" and member["state"] != "generating":
|
|
116
|
+
room.prompts_fired += 1
|
|
117
|
+
member["state"] = state
|
|
118
|
+
member["since"] = time.time()
|
|
119
|
+
member["detail"] = str(detail)[:120]
|
|
120
|
+
if model:
|
|
121
|
+
member["model"] = str(model)[:40]
|
|
122
|
+
_transition(room)
|
|
123
|
+
await broadcast(room)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def remove_member(room: Room, name: str) -> None:
|
|
127
|
+
room.members.pop(name, None)
|
|
128
|
+
_transition(room)
|
|
129
|
+
await broadcast(room)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------- http api
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.post("/api/{slug}/event")
|
|
136
|
+
async def post_event(slug: str, request: Request) -> JSONResponse:
|
|
137
|
+
"""State report from a hook / curl: {"name": "...", "state": "..."}"""
|
|
138
|
+
try:
|
|
139
|
+
data = await request.json()
|
|
140
|
+
except Exception:
|
|
141
|
+
data = {}
|
|
142
|
+
name = str(data.get("name", "anon"))
|
|
143
|
+
state = str(data.get("state", "ready"))
|
|
144
|
+
room = get_room(slug)
|
|
145
|
+
await set_state(room, name, state,
|
|
146
|
+
detail=str(data.get("detail", "")),
|
|
147
|
+
model=str(data.get("model", "")))
|
|
148
|
+
return JSONResponse({"ok": True, "phase": room.phase})
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.get("/")
|
|
152
|
+
async def index() -> RedirectResponse:
|
|
153
|
+
return RedirectResponse(f"/r/{secrets.token_urlsafe(3)}", status_code=307)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.get("/r/{slug}")
|
|
157
|
+
async def room_page(slug: str) -> FileResponse:
|
|
158
|
+
return FileResponse(STATIC_DIR / "index.html")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.get("/healthz")
|
|
162
|
+
async def healthz() -> JSONResponse:
|
|
163
|
+
return JSONResponse({"ok": True, "rooms": len(rooms)})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------- websocket
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.websocket("/ws/{slug}")
|
|
170
|
+
async def ws_endpoint(ws: WebSocket, slug: str) -> None:
|
|
171
|
+
await ws.accept()
|
|
172
|
+
room = get_room(slug)
|
|
173
|
+
room.sockets.add(ws)
|
|
174
|
+
await ws.send_json(snapshot(room))
|
|
175
|
+
try:
|
|
176
|
+
while True:
|
|
177
|
+
msg = await ws.receive_json()
|
|
178
|
+
kind = msg.get("type")
|
|
179
|
+
if kind == "set":
|
|
180
|
+
await set_state(room, str(msg.get("name", "anon")),
|
|
181
|
+
str(msg.get("state", "ready")))
|
|
182
|
+
elif kind == "remove":
|
|
183
|
+
await remove_member(room, str(msg.get("name", "")))
|
|
184
|
+
elif kind == "ping":
|
|
185
|
+
await ws.send_json({"type": "pong"})
|
|
186
|
+
except (WebSocketDisconnect, Exception):
|
|
187
|
+
pass
|
|
188
|
+
finally:
|
|
189
|
+
room.sockets.discard(ws)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>promptparty</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0d1117; --panel: #161b22; --panel2: #1c2330; --border: #2b3444;
|
|
10
|
+
--text: #e6edf3; --dim: #8b98a9;
|
|
11
|
+
--talk: #22c55e; --prompt: #f97316; --blocked: #f87171; --idle: #64748b;
|
|
12
|
+
}
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
html, body { height: 100%; }
|
|
15
|
+
body {
|
|
16
|
+
margin: 0; background: var(--bg); color: var(--text);
|
|
17
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
18
|
+
display: flex; flex-direction: column;
|
|
19
|
+
}
|
|
20
|
+
header {
|
|
21
|
+
display: flex; align-items: center; gap: 12px; padding: 10px 18px;
|
|
22
|
+
background: var(--panel); border-bottom: 1px solid var(--border);
|
|
23
|
+
}
|
|
24
|
+
header .logo { font-weight: 800; font-size: 17px; }
|
|
25
|
+
header .logo span { color: var(--prompt); }
|
|
26
|
+
header .meta { color: var(--dim); font-size: 13px; }
|
|
27
|
+
header .spacer { flex: 1; }
|
|
28
|
+
button.ghost {
|
|
29
|
+
background: none; border: 1px solid var(--border); color: var(--dim);
|
|
30
|
+
border-radius: 8px; padding: 5px 10px; font-size: 12.5px; cursor: pointer;
|
|
31
|
+
}
|
|
32
|
+
button.ghost:hover { color: var(--text); border-color: var(--dim); }
|
|
33
|
+
button.ghost.on { color: var(--talk); border-color: var(--talk); }
|
|
34
|
+
|
|
35
|
+
#banner {
|
|
36
|
+
text-align: center; padding: 34px 20px 26px; transition: background .4s;
|
|
37
|
+
}
|
|
38
|
+
#banner .phase { font-size: clamp(38px, 7vw, 72px); font-weight: 900; letter-spacing: .02em; }
|
|
39
|
+
#banner .sub { color: var(--dim); font-size: 16px; margin-top: 6px; }
|
|
40
|
+
#banner .timer { font-variant-numeric: tabular-nums; font-size: 22px; margin-top: 8px; color: var(--dim); }
|
|
41
|
+
body.phase-talk #banner { background: linear-gradient(180deg, rgba(34,197,94,.16), transparent); }
|
|
42
|
+
body.phase-talk #banner .phase { color: var(--talk); }
|
|
43
|
+
body.phase-prompt #banner { background: linear-gradient(180deg, rgba(249,115,22,.16), transparent); }
|
|
44
|
+
body.phase-prompt #banner .phase { color: var(--prompt); }
|
|
45
|
+
body.phase-idle #banner .phase { color: var(--idle); }
|
|
46
|
+
|
|
47
|
+
#members {
|
|
48
|
+
flex: 1; display: flex; flex-wrap: wrap; gap: 14px; align-content: flex-start;
|
|
49
|
+
justify-content: center; padding: 8px 20px 20px; overflow-y: auto;
|
|
50
|
+
}
|
|
51
|
+
.card {
|
|
52
|
+
width: 240px; background: var(--panel); border: 1px solid var(--border);
|
|
53
|
+
border-radius: 14px; padding: 14px 16px; position: relative;
|
|
54
|
+
}
|
|
55
|
+
.card .name { font-weight: 800; font-size: 17px; }
|
|
56
|
+
.card .model { color: var(--dim); font-size: 11.5px; }
|
|
57
|
+
.card .state { margin-top: 8px; font-weight: 700; font-size: 14px; }
|
|
58
|
+
.card .elapsed { color: var(--dim); font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
|
59
|
+
.card .x {
|
|
60
|
+
position: absolute; top: 8px; right: 10px; color: var(--dim); cursor: pointer;
|
|
61
|
+
background: none; border: none; font-size: 13px;
|
|
62
|
+
}
|
|
63
|
+
.card.s-generating { border-color: rgba(34,197,94,.5); }
|
|
64
|
+
.card.s-generating .state { color: var(--talk); }
|
|
65
|
+
.card.s-ready { border-color: rgba(249,115,22,.6); }
|
|
66
|
+
.card.s-ready .state { color: var(--prompt); }
|
|
67
|
+
.card.s-prompting .state { color: var(--prompt); }
|
|
68
|
+
.card.s-blocked { border-color: rgba(248,113,113,.6); animation: pulse 1.2s infinite; }
|
|
69
|
+
.card.s-blocked .state { color: var(--blocked); }
|
|
70
|
+
.card.s-away { opacity: .45; }
|
|
71
|
+
@keyframes pulse { 50% { box-shadow: 0 0 0 4px rgba(248,113,113,.15); } }
|
|
72
|
+
|
|
73
|
+
footer {
|
|
74
|
+
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
|
|
75
|
+
padding: 10px 18px; border-top: 1px solid var(--border);
|
|
76
|
+
background: var(--panel); color: var(--dim); font-size: 13px;
|
|
77
|
+
}
|
|
78
|
+
footer b { color: var(--text); }
|
|
79
|
+
#ratio { flex: 1; min-width: 160px; height: 8px; border-radius: 4px; overflow: hidden;
|
|
80
|
+
background: var(--panel2); display: flex; }
|
|
81
|
+
#ratio .t { background: var(--talk); }
|
|
82
|
+
#ratio .p { background: var(--prompt); }
|
|
83
|
+
|
|
84
|
+
details#setup {
|
|
85
|
+
margin: 0 18px 14px; background: var(--panel); border: 1px solid var(--border);
|
|
86
|
+
border-radius: 12px; padding: 10px 16px; font-size: 13.5px;
|
|
87
|
+
}
|
|
88
|
+
details#setup summary { cursor: pointer; color: var(--dim); font-weight: 600; }
|
|
89
|
+
details#setup pre {
|
|
90
|
+
background: #0a0e14; border: 1px solid var(--border); border-radius: 8px;
|
|
91
|
+
padding: 10px 12px; overflow-x: auto; font-size: 12.5px; user-select: all;
|
|
92
|
+
}
|
|
93
|
+
details#setup .hint { color: var(--dim); }
|
|
94
|
+
|
|
95
|
+
#manual { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
96
|
+
padding: 0 18px 12px; font-size: 13px; color: var(--dim); }
|
|
97
|
+
#manual input {
|
|
98
|
+
background: var(--panel2); color: var(--text); border: 1px solid var(--border);
|
|
99
|
+
border-radius: 8px; padding: 6px 10px; font: inherit; font-size: 13px; width: 120px;
|
|
100
|
+
}
|
|
101
|
+
#manual button {
|
|
102
|
+
background: var(--panel2); color: var(--text); border: 1px solid var(--border);
|
|
103
|
+
border-radius: 8px; padding: 6px 10px; font-size: 12.5px; cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
#manual button:hover { border-color: var(--dim); }
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body class="phase-idle">
|
|
109
|
+
<header>
|
|
110
|
+
<div class="logo">prompt<span>party</span> π</div>
|
|
111
|
+
<div class="meta" id="roommeta"></div>
|
|
112
|
+
<div class="spacer"></div>
|
|
113
|
+
<button class="ghost" id="sound">π sound: off</button>
|
|
114
|
+
<button class="ghost" id="copylink">copy room link</button>
|
|
115
|
+
</header>
|
|
116
|
+
|
|
117
|
+
<div id="banner">
|
|
118
|
+
<div class="phase" id="phase">β¦</div>
|
|
119
|
+
<div class="sub" id="sub">waiting for players</div>
|
|
120
|
+
<div class="timer" id="timer"></div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div id="members"></div>
|
|
124
|
+
|
|
125
|
+
<div id="manual">
|
|
126
|
+
<span>no hooks? report manually:</span>
|
|
127
|
+
<input id="myname" placeholder="your name" maxlength="32">
|
|
128
|
+
<button data-s="prompting">β¨οΈ prompting</button>
|
|
129
|
+
<button data-s="generating">π€ agent running</button>
|
|
130
|
+
<button data-s="ready">β
agent done</button>
|
|
131
|
+
<button data-s="away">πͺ away</button>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<details id="setup">
|
|
135
|
+
<summary>βοΈ hook up Claude Code (auto-reporting) β click to expand</summary>
|
|
136
|
+
<p>Run this once per teammate, inside the project you're coding in:</p>
|
|
137
|
+
<pre id="hookcmd"></pre>
|
|
138
|
+
<p class="hint">That installs Claude Code hooks that report your agent's state here
|
|
139
|
+
automatically: prompt submitted β <b>agent running</b>, turn finished β <b>agent done</b>,
|
|
140
|
+
permission needed β <b>blocked</b>. Any other tool can report with:</p>
|
|
141
|
+
<pre id="curlcmd"></pre>
|
|
142
|
+
</details>
|
|
143
|
+
|
|
144
|
+
<footer>
|
|
145
|
+
<span>π£ talk <b id="t-talk">0:00</b></span>
|
|
146
|
+
<div id="ratio"><div class="t" style="flex:1"></div></div>
|
|
147
|
+
<span>β¨οΈ prompt <b id="t-prompt">0:00</b></span>
|
|
148
|
+
<span>π prompts fired: <b id="fired">0</b></span>
|
|
149
|
+
</footer>
|
|
150
|
+
|
|
151
|
+
<script>
|
|
152
|
+
(() => {
|
|
153
|
+
const $ = (id) => document.getElementById(id);
|
|
154
|
+
const room = location.pathname.split("/").pop();
|
|
155
|
+
let ws, state = null, soundOn = false, lastPhase = null;
|
|
156
|
+
|
|
157
|
+
$("roommeta").textContent = `room ${room}`;
|
|
158
|
+
$("hookcmd").textContent =
|
|
159
|
+
`uvx promptparty hooks --server ${location.origin} --room ${room} --name YOURNAME`;
|
|
160
|
+
$("curlcmd").textContent =
|
|
161
|
+
`curl -X POST ${location.origin}/api/${room}/event -H 'content-type: application/json' \\\n` +
|
|
162
|
+
` -d '{"name":"YOURNAME","state":"generating"}' # or: ready | prompting | blocked | away`;
|
|
163
|
+
|
|
164
|
+
const fmt = (s) => {
|
|
165
|
+
s = Math.max(0, Math.floor(s));
|
|
166
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const STATE_LABEL = {
|
|
170
|
+
generating: "π€ agent running β free to talk",
|
|
171
|
+
ready: "β
agent done β read & prompt!",
|
|
172
|
+
prompting: "β¨οΈ typing a promptβ¦",
|
|
173
|
+
blocked: "π needs a decision!",
|
|
174
|
+
away: "πͺ away",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
function beep(freq, dur = 0.18) {
|
|
178
|
+
if (!soundOn) return;
|
|
179
|
+
try {
|
|
180
|
+
const ctx = beep.ctx || (beep.ctx = new (window.AudioContext || window.webkitAudioContext)());
|
|
181
|
+
const o = ctx.createOscillator(), g = ctx.createGain();
|
|
182
|
+
o.frequency.value = freq; o.type = "sine";
|
|
183
|
+
g.gain.setValueAtTime(0.12, ctx.currentTime);
|
|
184
|
+
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
|
|
185
|
+
o.connect(g); g.connect(ctx.destination);
|
|
186
|
+
o.start(); o.stop(ctx.currentTime + dur);
|
|
187
|
+
} catch (e) {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function render() {
|
|
191
|
+
if (!state) return;
|
|
192
|
+
const drift = state._recv - state.now; // map server clock to local
|
|
193
|
+
const now = Date.now() / 1000 - drift;
|
|
194
|
+
|
|
195
|
+
document.body.className = "phase-" + (state.phase === "idle" ? "idle" : state.phase);
|
|
196
|
+
const ph = $("phase"), sub = $("sub");
|
|
197
|
+
if (state.phase === "talk") {
|
|
198
|
+
ph.textContent = "π£οΈ TALK";
|
|
199
|
+
sub.textContent = "all agents are cooking β hands off the keyboard";
|
|
200
|
+
} else if (state.phase === "prompt") {
|
|
201
|
+
const holding = state.members.filter(m => ["ready", "blocked", "prompting"].includes(m.state));
|
|
202
|
+
ph.textContent = "β¨οΈ PROMPT";
|
|
203
|
+
sub.textContent = holding.length
|
|
204
|
+
? `waiting on: ${holding.map(m => m.name).join(", ")}`
|
|
205
|
+
: "get your prompts in";
|
|
206
|
+
} else {
|
|
207
|
+
ph.textContent = "promptparty";
|
|
208
|
+
sub.textContent = "waiting for players β expand βοΈ below to join";
|
|
209
|
+
}
|
|
210
|
+
$("timer").textContent = state.phase === "idle" ? "" : fmt(now - state.phase_since);
|
|
211
|
+
|
|
212
|
+
const box = $("members");
|
|
213
|
+
box.innerHTML = "";
|
|
214
|
+
for (const m of state.members) {
|
|
215
|
+
const el = document.createElement("div");
|
|
216
|
+
el.className = "card s-" + m.state;
|
|
217
|
+
el.innerHTML = `<button class="x" title="remove">β</button>
|
|
218
|
+
<div class="name"></div><div class="model"></div>
|
|
219
|
+
<div class="state"></div><div class="elapsed"></div>`;
|
|
220
|
+
el.querySelector(".name").textContent = m.name;
|
|
221
|
+
el.querySelector(".model").textContent = m.model || "";
|
|
222
|
+
el.querySelector(".state").textContent = STATE_LABEL[m.state] || m.state;
|
|
223
|
+
el.querySelector(".elapsed").textContent = "for " + fmt(now - m.since);
|
|
224
|
+
el.querySelector(".x").onclick = () =>
|
|
225
|
+
ws.send(JSON.stringify({ type: "remove", name: m.name }));
|
|
226
|
+
box.appendChild(el);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let talk = state.totals.talk, prompt = state.totals.prompt;
|
|
230
|
+
if (state.phase === "talk") talk += now - state.last_flush;
|
|
231
|
+
if (state.phase === "prompt") prompt += now - state.last_flush;
|
|
232
|
+
$("t-talk").textContent = fmt(talk);
|
|
233
|
+
$("t-prompt").textContent = fmt(prompt);
|
|
234
|
+
$("fired").textContent = state.prompts_fired;
|
|
235
|
+
const total = talk + prompt || 1;
|
|
236
|
+
$("ratio").innerHTML =
|
|
237
|
+
`<div class="t" style="flex:${talk / total}"></div><div class="p" style="flex:${prompt / total}"></div>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function connect() {
|
|
241
|
+
const proto = location.protocol === "https:" ? "wss" : "ws";
|
|
242
|
+
ws = new WebSocket(`${proto}://${location.host}/ws/${room}`);
|
|
243
|
+
ws.onclose = () => setTimeout(connect, 1500);
|
|
244
|
+
ws.onmessage = (ev) => {
|
|
245
|
+
const m = JSON.parse(ev.data);
|
|
246
|
+
if (m.type !== "state") return;
|
|
247
|
+
m._recv = Date.now() / 1000;
|
|
248
|
+
state = m;
|
|
249
|
+
if (lastPhase && m.phase !== lastPhase) {
|
|
250
|
+
if (m.phase === "talk") { beep(660); setTimeout(() => beep(880), 180); }
|
|
251
|
+
if (m.phase === "prompt") { beep(440); setTimeout(() => beep(330), 180); }
|
|
252
|
+
}
|
|
253
|
+
lastPhase = m.phase;
|
|
254
|
+
render();
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
connect();
|
|
258
|
+
setInterval(render, 1000);
|
|
259
|
+
|
|
260
|
+
$("sound").onclick = () => {
|
|
261
|
+
soundOn = !soundOn;
|
|
262
|
+
$("sound").textContent = `π sound: ${soundOn ? "on" : "off"}`;
|
|
263
|
+
$("sound").classList.toggle("on", soundOn);
|
|
264
|
+
if (soundOn) beep(660);
|
|
265
|
+
};
|
|
266
|
+
$("copylink").onclick = async () => {
|
|
267
|
+
await navigator.clipboard.writeText(location.href);
|
|
268
|
+
$("copylink").textContent = "copied!";
|
|
269
|
+
setTimeout(() => $("copylink").textContent = "copy room link", 1200);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
$("myname").value = localStorage.getItem("promptparty-name") || "";
|
|
273
|
+
for (const btn of document.querySelectorAll("#manual button")) {
|
|
274
|
+
btn.onclick = () => {
|
|
275
|
+
const name = $("myname").value.trim();
|
|
276
|
+
if (!name) { $("myname").focus(); return; }
|
|
277
|
+
localStorage.setItem("promptparty-name", name);
|
|
278
|
+
ws.send(JSON.stringify({ type: "set", name, state: btn.dataset.s }));
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
})();
|
|
282
|
+
</script>
|
|
283
|
+
</body>
|
|
284
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "promptparty"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Cadence sync for teams agent-coding in the same room β one dashboard that says when to PROMPT and when to TALK."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ email = "k.moudra@seznam.cz" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["llm", "claude", "claude-code", "agentic-coding", "pair-programming", "collaboration", "hackathon", "hooks"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Communications :: Chat",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"fastapi>=0.110",
|
|
24
|
+
"uvicorn[standard]>=0.29",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/moudrkat/promptparty"
|
|
29
|
+
Issues = "https://github.com/moudrkat/promptparty/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
promptparty = "promptparty.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["promptparty"]
|