Skip to content

Commit d30478c

Browse files
Merge pull request #3 from NetDevAutomate/fix/ttyd-restore-and-improvements
feat: ttyd proxy, LAN auth, pop-out fix + multi-agent cleanup
2 parents e4f4d9a + 49573ae commit d30478c

30 files changed

Lines changed: 2566 additions & 107 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ site/
475475
*.db-shm
476476
*.db-journal
477477

478-
# Internal docs (kept locally, not published)
478+
# Internal/planning docs (kept locally, not published)
479479
docs/internal/
480480
docs/local_repo_docs/
481481
docs/plans/

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ graph LR
8181
```bash
8282
# Study sessions (tmux + AI agent + sidebar)
8383
studyctl study "topic" --energy 7 # Full tmux environment in one command
84+
studyctl study "topic" --web # Also start web dashboard + auto-open browser
85+
studyctl study "topic" --lan # LAN access with password auth (implies --web)
86+
studyctl study "topic" --lan --password SECRET # Explicit LAN password
8487
studyctl study --resume # Resume conversation from history
8588
studyctl study --end # End session (quit Claude also works)
8689
studyctl park "question" # Park tangential topic
@@ -140,6 +143,7 @@ Launch with `studyctl web`. Accessible from any device on the network.
140143
- Timer with energy-adaptive colour phases (green/amber/red)
141144
- Topic counters (wins, parked, review)
142145
- Session summary on completion
146+
- **Terminal panel** — embedded ttyd iframe proxied same-origin at `/terminal/` with draggable split-pane (stacked or side-by-side). Pop-out to new window and seamless return. HTTP Basic Auth when using `--lan`. Requires ttyd (optional but recommended).
143147
- HTMX + Alpine.js — no build step
144148

145149
## Optional Extras
@@ -149,6 +153,10 @@ pip install 'studyctl[all]' # Everything
149153
pip install 'studyctl[web]' # FastAPI web UI
150154
pip install 'studyctl[content]' # PDF splitting + NotebookLM
151155
pip install 'studyctl[notebooklm]' # NotebookLM API client
156+
157+
# ttyd — web terminal (enables the terminal panel in the live dashboard)
158+
brew install ttyd # macOS
159+
sudo apt install ttyd # Linux (or build from source)
152160
```
153161

154162
## Documentation

docs/cli-reference.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ studyctl study "Python Decorators" --energy 7 # Socratic mentor session
6363
studyctl study "Spark Internals" --mode co-study # User-driven co-study
6464
studyctl study "topic" --timer pomodoro # Override default timer
6565
studyctl study "topic" --agent claude --web # Explicit agent + web dashboard
66+
studyctl study "topic" --lan # Bind to 0.0.0.0, password-protected (implies --web)
67+
studyctl study "topic" --lan --password SECRET # Explicit password for LAN auth
6668
studyctl study "topic" --agent ollama # Local LLM via Ollama + LiteLLM
6769
studyctl study "topic" --agent lmstudio # Local LLM via LM Studio
6870
studyctl study --resume # Resume conversation (-r)
@@ -77,6 +79,9 @@ studyctl park "How does asyncio compare?" # Park mid-session
7779
- Sidebar shows timer, activity feed, counters (keyboard: `p` pause, `r` reset, `Q` end session)
7880
- IPC files for dashboard viewports
7981
- Optional web dashboard at `/session` via `--web`
82+
- `--web` auto-opens a browser to the dashboard on startup
83+
- `--lan` binds the web server and ttyd to `0.0.0.0` with HTTP Basic Auth, prints dashboard URL and password (implies `--web`). Password is auto-generated if not set via `--password` or `lan_password` in config
84+
- `--password SECRET` sets the LAN authentication password (used with `--lan`)
8085

8186
**Session lifecycle:**
8287
- **Start:** `studyctl study "topic"` — creates tmux session, agent, sidebar
@@ -153,10 +158,21 @@ studyctl web --host localhost # Local only
153158

154159
**Features:** Source/chapter filter, card count limiter (10/20/50/100/All), due cards badge, session history, 90-day study heatmap, Pomodoro timer (25min/5min), OpenDyslexic font toggle, dark/light theme, PWA installable.
155160

161+
**Live session dashboard** (`/session`): Real-time SSE activity feed, energy-adaptive timer, topic counters, and a **terminal panel** — an embedded ttyd iframe showing the tmux session live via same-origin proxy (`/terminal/`). The panel is draggable (stacked or side-by-side), has a layout toggle and panel-swap buttons, and can be popped out to a separate window (pop-out auto-closes when returning inline). ttyd is optional (`brew install ttyd`) but required for the terminal panel.
162+
156163
**Voice:** Uses Web Speech API (browser built-in). Two modes:
157164
- **Read once** — speaker icon on card or `T` key
158165
- **Auto-voice** — header toggle or `V` key (reads everything automatically)
159166

167+
**Web + terminal config** (`~/.config/studyctl/config.yaml`):
168+
169+
```yaml
170+
web_port: 8567 # web dashboard port (default 8567)
171+
ttyd_port: 7681 # ttyd web terminal port (default 7681)
172+
browser: "" # auto-open browser: chrome, safari, firefox, brave, or empty for system default
173+
lan_password: "" # persistent password for --lan mode (auto-generated if empty)
174+
```
175+
160176
---
161177
162178
## agent-session-tools

docs/roadmap.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,19 @@ Study from any device (iPad, laptop, phone) via ttyd + web dashboard, with optio
278278

279279
### ttyd Integration + Embedded Web Pane
280280

281-
- [ ] `studyctl study --lan` starts ttyd alongside tmux session (port 7681)
282-
- [ ] Embed ttyd as iframe in existing web dashboard
283-
- [ ] "Open Study Agent Session" button — iframe split (default) + pop-out new window
284-
- [ ] Doctor check for ttyd binary
285-
- [ ] Config support for ttyd port
281+
- [x] `studyctl study --lan` starts ttyd alongside tmux session (port 7681)
282+
- [x] Embed ttyd as iframe in existing web dashboard
283+
- [x] "Open Study Agent Session" button — iframe split (default) + pop-out new window
284+
- [x] Doctor check for ttyd binary
285+
- [x] Config support for ttyd port
286+
- [x] Draggable split-pane layout (stacked/side-by-side)
287+
- [x] Browser auto-open with configurable browser
288+
- [x] Claude Code trust bypass (`hasTrustDialogAccepted`)
289+
- [x] `window-size largest` tmux option for multi-client compatibility
290+
- [x] Same-origin reverse proxy — ttyd proxied through FastAPI at `/terminal/` (HTTP + WebSocket)
291+
- [x] Pop-out/return persistence — iframe stays in DOM (visibility CSS), pop-out auto-closes on return
292+
- [x] LAN password protection — HTTP Basic Auth middleware, auto-generated or explicit `--password`
293+
- [x] `lan_password` config option for persistent password
286294

287295
### Local LLMs via Ollama / LM Studio
288296

docs/setup-guide.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Step-by-step installation and configuration for Socratic Study Mentor.
2424
- **Obsidian** — for study notes (any vault structure works)
2525
- **Optional**: `notebooklm-py` for Google NotebookLM sync
2626
- **Optional**: `sentence-transformers` for semantic search
27+
- **Optional**: `ttyd` — enables web terminal access from browser or iPad (`brew install ttyd` on macOS, `apt install ttyd` on Linux)
2728

2829
> **tmux-resurrect / tmux-continuum users**: studyctl automatically cleans up
2930
> zombie sessions on startup, so resurrect-restored sessions are handled
@@ -173,6 +174,27 @@ Two voice modes in the PWA:
173174

174175
**Accessibility:** The `Aa` button toggles [OpenDyslexic](https://opendyslexic.org) font. The sun icon toggles light/dark theme. Both are persisted across sessions.
175176

177+
### Remote Study (iPad on the Bus)
178+
179+
Use `--lan` to make the web dashboard and terminal accessible from any device on your network — phone, iPad, second laptop:
180+
181+
```bash
182+
studyctl study "Python Decorators" --energy 7 --lan
183+
# Auto-generates password and saves LAN info to session state:
184+
# Dashboard: http://192.168.1.42:8567/session
185+
# Password: <auto-generated>
186+
187+
# Or set a known password:
188+
studyctl study "Python Decorators" --energy 7 --lan --password mysecret
189+
```
190+
191+
Access the live dashboard and embedded terminal from your iPad at `http://<mac-ip>:8567/session`. Enter any username and the displayed password when prompted. The terminal panel (ttyd iframe) is proxied through the web server on the same origin, so pop-out/return works seamlessly. ttyd must be installed for the terminal panel to work (`brew install ttyd`).
192+
193+
**Password sources** (checked in order):
194+
1. `--password` CLI flag
195+
2. `lan_password` in `~/.config/studyctl/config.yaml`
196+
3. Auto-generated (displayed in terminal output, saved to session state)
197+
176198
### Hosts — Cross-Machine Sync
177199

178200
#### Prerequisites: Passwordless SSH
@@ -304,6 +326,16 @@ Environment variable overrides:
304326
- `LOG_LEVEL` — set logging level (DEBUG, INFO, WARNING, ERROR)
305327
- `EMBEDDING_MODEL` — override embedding model
306328

329+
### Web Terminal Settings
330+
331+
```yaml
332+
# Web terminal (optional — requires ttyd installed)
333+
ttyd_port: 7681 # ttyd listens on this port (default 7681)
334+
web_port: 8567 # web dashboard port (default 8567)
335+
browser: "" # auto-open browser: chrome, safari, firefox, brave, or empty for system default
336+
lan_password: "" # persistent LAN password (auto-generated per session if empty)
337+
```
338+
307339
### TTS Voice Settings
308340

309341
```yaml

docs/system-overview.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ graph TB
6363
TMUX --> AGENT
6464
TMUX --> SIDEBAR
6565
STUDY -.->|"--web"| DASH
66+
STUDY -.->|"--lan"| TTYD["ttyd<br/>(port 7681)"]
67+
TTYD -.->|"attaches to"| TMUX
68+
DASH -.->|"/terminal/ proxy"| TTYD
6669
AGENT -->|writes| IPC
6770
SIDEBAR -->|polls| IPC
6871
DASH -->|polls SSE| IPC
@@ -151,11 +154,17 @@ sequenceDiagram
151154
U->>CLI: studyctl study "Decorators" --energy 7
152155
CLI->>DB: start_study_session()
153156
CLI->>IPC: Write session-state.json + empty topics/parking
154-
CLI->>TMUX: Create session (cwd=sessions/study-decorators-xxx/)
157+
CLI->>TMUX: Create session (cwd=sessions/study-decorators-xxx/, window-size largest)
155158
CLI->>TMUX: Split pane 75/25
156159
TMUX->>AGENT: Main pane: claude --append-system-prompt-file persona.md
157160
TMUX->>TUI: Sidebar pane: python -m studyctl.tui.sidebar
158161
162+
opt --lan flag
163+
CLI->>WEB: Start ttyd (attaches to tmux session, port 7681)
164+
CLI->>WEB: Start web with Basic Auth + ttyd proxy
165+
Note over WEB: Web dashboard proxies ttyd at /terminal/<br/>(same-origin — pop-out/return preserves WS)<br/>HTTP Basic Auth protects all routes on LAN
166+
end
167+
159168
loop Every 2 seconds
160169
TUI->>IPC: stat() mtime check
161170
IPC-->>TUI: Update timer, activity, counters
@@ -527,6 +536,7 @@ Here's a complete workflow from course materials to mastery:
527536
|-----------|----------|---------|
528537
| Python 3.12+ | Yes | `mise install python` or system package |
529538
| tmux 3.1+ | For `studyctl study` | `brew install tmux` / `apt install tmux` |
539+
| ttyd | Optional — remote terminal (`--lan`) | `brew install ttyd` / `apt install ttyd` |
530540
| Claude Code | For AI study sessions | `npm install -g @anthropic-ai/claude-code` |
531541
| pandoc | For markdown to PDF | `brew install pandoc` |
532542
| typst | For PDF rendering | `brew install typst` |

packages/studyctl/pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ dependencies = [
2626
content = ["pymupdf>=1.25", "httpx"]
2727
notebooklm = ["notebooklm-py>=0.3.4"]
2828
tui = ["textual>=0.80"]
29-
web = ["fastapi>=0.115", "uvicorn[standard]>=0.34"]
29+
web = ["fastapi>=0.115", "uvicorn[standard]>=0.34", "httpx>=0.27", "websockets>=12.0"]
3030
mcp = ["mcp[cli]>=1.0.0"]
3131
all = ["studyctl[content,web,notebooklm,tui,mcp]"]
3232

@@ -52,9 +52,15 @@ testpaths = ["tests"]
5252
addopts = "--tb=short -m 'not integration'"
5353
markers = [
5454
"integration: requires external infrastructure (tmux, real DB, network)",
55+
"e2e: end-to-end browser tests requiring Playwright + web server",
5556
]
5657

5758
[tool.pyright]
5859
pythonVersion = "3.12"
5960
typeCheckingMode = "basic"
6061
exclude = ["src/studyctl/tui", "src/studyctl/content", "src/studyctl/cli/_content.py", "src/studyctl/cli/_web.py", "src/studyctl/web", "src/studyctl/mcp"] # optional deps not in CI
62+
63+
[dependency-groups]
64+
dev = [
65+
"pytest-playwright>=0.7.2",
66+
]

packages/studyctl/src/studyctl/agent_launcher.py

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
and launch commands. The canonical persona content is shared; each adapter's
55
setup() callable transforms it for that agent's specific mechanism.
66
7-
Agent mechanisms (verified 2026-04-03):
7+
Agent mechanisms (verified 2026-04-04):
88
Claude: --append-system-prompt-file {temp_file}
99
Gemini: GEMINI.md written to session cwd (auto-loaded)
1010
Kiro: ~/.kiro/agents/study-mentor.json updated with file:// prompt ref
11-
OpenCode: ~/.config/opencode/agents/study-mentor.md with YAML frontmatter
11+
Crash recovery: stale backups restored on next setup
12+
OpenCode: .opencode/agents/study-mentor.md with YAML frontmatter (permission: format)
1213
"""
1314

1415
from __future__ import annotations
@@ -131,10 +132,19 @@ def _kiro_setup(canonical_content: str, _session_dir: Path) -> Path:
131132
# 4. Ensure target directory exists
132133
KIRO_AGENTS_DIR.mkdir(parents=True, exist_ok=True)
133134
target = KIRO_AGENTS_DIR / f"{KIRO_AGENT_NAME}.json"
135+
backup = target.with_suffix(target.suffix + _KIRO_BACKUP_SUFFIX)
136+
137+
# 4a. Recover from crash: if a backup exists, the previous session's
138+
# teardown never ran. Restore the user's original config first.
139+
if backup.exists():
140+
logger.warning(
141+
"Stale Kiro backup detected (previous session crashed?) — restoring %s",
142+
backup,
143+
)
144+
os.replace(backup, target)
134145

135146
# 5. Backup existing agent JSON if present
136147
if target.exists():
137-
backup = target.with_suffix(target.suffix + _KIRO_BACKUP_SUFFIX)
138148
shutil.copy2(target, backup)
139149

140150
# 6. Atomic write: temp file in same dir → os.replace()
@@ -176,6 +186,20 @@ def _kiro_teardown(_session_dir: Path) -> None:
176186
# ---------------------------------------------------------------------------
177187

178188

189+
def _mcp_command() -> list[str]:
190+
"""Build the studyctl-mcp server command.
191+
192+
Prefers the installed console script (pip/uv tool install).
193+
Falls back to uv run --project for development.
194+
"""
195+
binary = shutil.which("studyctl-mcp")
196+
if binary:
197+
return [binary]
198+
# Dev fallback: run from the repo workspace
199+
project_path = str(_REPO_ROOT / "packages" / "studyctl")
200+
return ["uv", "run", "--project", project_path, "studyctl-mcp"]
201+
202+
179203
def _gemini_setup(canonical_content: str, session_dir: Path) -> Path:
180204
"""Write GEMINI.md to session dir (auto-loaded by Gemini CLI from cwd)."""
181205
persona_path = session_dir / "GEMINI.md"
@@ -196,13 +220,12 @@ def _gemini_mcp(session_dir: Path) -> None:
196220
gemini_dir = session_dir / ".gemini"
197221
gemini_dir.mkdir(parents=True, exist_ok=True)
198222

199-
# Build MCP server command using the project path
200-
project_path = str(_REPO_ROOT / "packages" / "studyctl")
223+
cmd = _mcp_command()
201224
settings = {
202225
"mcpServers": {
203226
"studyctl-mcp": {
204-
"command": "uv",
205-
"args": ["run", "--project", project_path, "studyctl-mcp"],
227+
"command": cmd[0],
228+
"args": cmd[1:],
206229
},
207230
},
208231
}
@@ -217,8 +240,8 @@ def _gemini_mcp(session_dir: Path) -> None:
217240
# ~/.config/opencode/agents/ or project-local agents/. The setup
218241
# function writes the persona as a markdown file with YAML frontmatter
219242
# in the session directory. MCP uses a different schema from others:
220-
# "command" is a flat array, "enabled" (not "disabled"), "environment"
221-
# (not "env").
243+
# "command" is a flat array, "enabled" (not "disabled"),
244+
# "type": "local" required.
222245
# ---------------------------------------------------------------------------
223246

224247
_OPENCODE_AGENTS_DIR_NAME = ".opencode"
@@ -235,10 +258,12 @@ def _opencode_setup(canonical_content: str, session_dir: Path) -> Path:
235258
'description: "AuDHD-aware Socratic study mentor"\n'
236259
"mode: primary\n"
237260
"temperature: 0.3\n"
238-
"tools:\n"
239-
" write: true\n"
240-
" edit: true\n"
241-
" bash: true\n"
261+
"permission:\n"
262+
" edit: allow\n"
263+
" bash:\n"
264+
' "studyctl *": allow\n'
265+
' "session-* *": allow\n'
266+
' "*": ask\n'
242267
"---\n\n"
243268
)
244269
persona_path.write_text(frontmatter + canonical_content)
@@ -259,20 +284,17 @@ def _opencode_mcp(session_dir: Path) -> None:
259284
OpenCode's schema differs from others:
260285
- ``command`` is a flat array (binary + args merged)
261286
- ``enabled`` instead of ``disabled``
262-
- ``environment`` instead of ``env``
263287
- ``type: "local"`` required
264288
"""
265289
oc_dir = session_dir / _OPENCODE_AGENTS_DIR_NAME
266290
oc_dir.mkdir(parents=True, exist_ok=True)
267291

268-
project_path = str(_REPO_ROOT / "packages" / "studyctl")
269292
config = {
270293
"mcp": {
271294
"studyctl-mcp": {
272-
"command": ["uv", "run", "--project", project_path, "studyctl-mcp"],
295+
"command": _mcp_command(),
273296
"enabled": True,
274297
"type": "local",
275-
"environment": {},
276298
},
277299
},
278300
}

packages/studyctl/src/studyctl/cli/_doctor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def _get_registry():
3232
check_studyctl_installed,
3333
)
3434
from studyctl.doctor.database import check_review_db, check_sessions_db
35-
from studyctl.doctor.deps import check_optional_deps
35+
from studyctl.doctor.deps import check_optional_deps, check_system_binaries
3636
from studyctl.doctor.updates import check_pypi_versions
3737

3838
registry = CheckerRegistry()
@@ -48,6 +48,7 @@ def _get_registry():
4848
for fn in [check_obsidian_vault, check_review_directories, check_pandoc, check_tmux_resurrect]:
4949
registry.register("config")(fn)
5050
registry.register("deps")(check_optional_deps)
51+
registry.register("deps")(check_system_binaries)
5152
registry.register("agents")(check_agent_definitions)
5253
registry.register("agents")(check_agent_smoke_tests)
5354
registry.register("agents")(check_local_llm_servers)

0 commit comments

Comments
 (0)