Compare commits

...

9 Commits

5 changed files with 320 additions and 12 deletions
+88 -7
View File
@@ -62,6 +62,12 @@ who am i 2>/dev/null
# Kitty remote control (only if kitty is installed)
kitty @ ls 2>&1 | head -1 || true
# SSH usage — does the user SSH to remote machines for work?
ls ~/.ssh/config 2>/dev/null && echo "ssh config: found" || echo "ssh config: not found"
grep -l "ControlMaster\|ControlPath" ~/.ssh/config 2>/dev/null && echo "ssh multiplexing: configured" || echo "ssh multiplexing: not configured"
ls ~/.ssh/sockets/ 2>/dev/null && echo "ssh sockets dir: exists" || echo "ssh sockets dir: missing"
ls ~/.ssh/known_hosts 2>/dev/null && wc -l < ~/.ssh/known_hosts 2>/dev/null && echo "known hosts (suggests SSH usage)" || true
```
## Step 2: Evaluate Options
@@ -96,16 +102,40 @@ Based on what you detected, determine:
Based on your evaluation, present the user's options conversationally. Be specific about what they have and what each option gives them.
**If over SSH:**
Tell the user tmux is their only practical option. If tmux is installed, recommend it. If not, recommend installing it (`apt install tmux` / `brew install tmux`). Mention that images will work if their local terminal supports sixel.
tmux works immediately for pane splitting, but images are limited to sixel or ASCII art. Present the user with options:
**If native/remote desktop with tmux available:**
Present tmux as the ready-now option. Optionally mention kitty as an upgrade for better image quality if they want to install it later.
> "You're connected via SSH. Here are your options:
>
> **Option A: tmux (works now)** — I can split panes via tmux. Images render via sixel if your local terminal supports it, otherwise ASCII art. No setup needed.
>
> **Option B: Install kitty on the remote host + use a remote desktop** — If you can access this machine via a remote desktop protocol (Chrome Remote Desktop, RDP, VNC, etc.), you could install kitty on the remote host and use it through the remote desktop session. This gives you the full experience: native splits, best image quality. Is remote desktop an option for you?
>
> **Option C: Both** — Set up tmux now for SSH sessions, and also install kitty on the remote host for when you connect via remote desktop. Best of both worlds."
**If native/remote desktop in kitty:**
Great — they get the best experience. Just need `allow_remote_control yes` in kitty.conf.
If the user chooses B or C, install kitty on the remote host:
- Linux: `curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin`
- After install: add `allow_remote_control yes` to `~/.config/kitty/kitty.conf`
- Remind them: kitty's display features only work through a remote desktop session, not over SSH
If the user chooses A or C, set up tmux as normal (Step 4 handles mouse config).
**If native/remote desktop (sitting at the machine or RDP/VNC):**
**Recommend kitty** — it gives the best experience (native splits, best image quality via kitty graphics protocol). Present it as the primary recommendation, with tmux as the quick-start alternative if they don't want to install anything new.
However, warn about this caveat:
> **Note:** If you install kitty and use it as your terminal, be aware that some AI CLIs (like `gemini`) may prompt for passwords or authentication tokens during setup. Kitty uses its own keyboard protocol which can interfere with password prompts in some CLI tools. If you hit issues with password/auth prompts not working in kitty, switch to your regular terminal for that step, then come back to kitty.
Installation:
- Linux: `curl -L https://sw.kovidgoyal.net/kitty/installer.sh | sh /dev/stdin`
- macOS: `brew install --cask kitty`
- After install: add `allow_remote_control yes` to `~/.config/kitty/kitty.conf`
- Launch your AI CLI from within kitty to get the full experience
**If already in kitty:**
Great — they get the best experience. Just need `allow_remote_control yes` in kitty.conf. Note: existing kitty windows must be restarted for config changes to take effect.
**If no tmux and no kitty:**
Recommend installing tmux (simplest) or kitty (best experience). Plain backend works but has no split panes.
Recommend kitty (best experience) or tmux (simplest install). Plain backend works but has no split panes — the TUI opens in a separate window.
## Step 4: Execute Setup
@@ -127,7 +157,58 @@ Based on what the user chose:
echo "allow_remote_control yes" >> ~/.config/kitty/kitty.conf
```
3. **Optional: install chafa** for ASCII art image fallback:
3. **If tmux backend chosen**, enable mouse support so the user can click widgets in the TUI pane:
```bash
# Check current setting
tmux show -g mouse 2>/dev/null
```
If mouse is off or not set:
```bash
tmux set -g mouse on
```
To make it permanent, add to `~/.tmux.conf`:
```bash
grep -q "set -g mouse on" ~/.tmux.conf 2>/dev/null || echo "set -g mouse on" >> ~/.tmux.conf
```
Without this, tmux intercepts mouse clicks and the user cannot interact with checkboxes, buttons, or inputs in the display pane.
4. **If the user SSHes to remote machines** (detected by known_hosts having entries, or the user mentions remote work), **set up SSH ControlMaster** so the AI CLI can reuse the user's authenticated SSH connections without needing to re-enter passwords or touch physical keys:
Ask the user: "Do you SSH into remote machines as part of your work? If so, I can configure SSH connection multiplexing — this lets you authenticate once, and my SSH commands piggyback on your open connection without needing a password."
If yes:
```bash
mkdir -p ~/.ssh/sockets
chmod 700 ~/.ssh/sockets
```
Check if ControlMaster is already configured:
```bash
grep -q "ControlMaster" ~/.ssh/config 2>/dev/null && echo "Already configured" || echo "Not configured"
```
If not configured, add to `~/.ssh/config` (create if needed):
```bash
touch ~/.ssh/config
chmod 600 ~/.ssh/config
cat >> ~/.ssh/config << 'SSHEOF'
# Kitty-Workbench: SSH connection multiplexing
# First connection authenticates normally (password, key, etc.)
# Subsequent connections reuse the tunnel — no re-auth needed
Host *
ControlMaster auto
ControlPath ~/.ssh/sockets/%r@%h-%p
ControlPersist yes
SSHEOF
```
Explain to the user how it works:
> **How this works:** When you SSH into a remote machine, the connection stays open in the background for 10 minutes (`ControlPersist yes`). During that time, any other SSH command to the same host — including ones I run — reuses your authenticated tunnel. No password prompt, no key tap. The tunnel stays open until the original session closes or the connection drops — your key/auth provider handles its own timeout. Just open an SSH session to your target machine before asking me to work on it.
If the user's `~/.ssh/config` already has Host-specific blocks, add the ControlMaster settings under a `Host *` block at the **end** of the file so it acts as a default without overriding specific host configs.
5. **Optional: install chafa** for ASCII art image fallback:
- Linux: `sudo apt install chafa` or `sudo pacman -S chafa`
- macOS: `brew install chafa`
+3 -1
View File
@@ -1,5 +1,7 @@
# Kitty-Workbench
**Open source and safe for work.**
An MCP server that gives your AI a rich, interactive display panel — right in your terminal.
You talk to the AI in one pane. It controls the other: pushing schematics, checklists, measurement tables, images, and live logs. You can interact back — check items off, input readings, click buttons. The AI sees your responses.
@@ -26,7 +28,7 @@ You talk to the AI in one pane. It controls the other: pushing schematics, check
Give your AI the repo URL and tell it to set you up:
```
Clone https://github.com/yourorg/kitty-workbench and set it up for me
Clone https://git.sethpc.xyz/Seth/kitty-workbench and set it up for me
```
Or if you've already cloned it, open your AI CLI in the repo and say:
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""
Kitty-Workbench Demo
====================
Run this in a kitty terminal to see the interactive display panel.
Usage:
python3 demo.py
What it does:
1. Opens a Unix socket server
2. Splits your kitty window and launches the TUI in the right pane
3. Pushes a diagnostic scenario (Heathkit IO-102 oscilloscope focus repair)
4. Leaves the TUI running so you can interact with the checklist
Press Ctrl+C to close.
"""
import asyncio
import os
import sys
import time
from pathlib import Path
# Ensure kitty_workbench is importable
sys.path.insert(0, str(Path(__file__).parent / "src"))
from kitty_workbench.protocol import (
encode_message, decode_message, ReadyEvent, InitCmd,
DisplayCmd, LayoutCmd, LogCmd, ShutdownCmd,
)
from kitty_workbench.project import create_project
async def main():
project_name = "demo-io102"
title = "Heathkit IO-102 Focus Diagnostic"
sock_path = f"/tmp/kitt-{project_name}.sock"
# Clean up stale socket
if os.path.exists(sock_path):
os.unlink(sock_path)
# Ensure project dir exists
create_project(project_name, title)
# -- Socket server --
ready = asyncio.Event()
tui_writer = None
async def on_connect(reader, writer):
nonlocal tui_writer
tui_writer = writer
while True:
line = await reader.readline()
if not line:
break
msg = decode_message(line.decode().strip())
if isinstance(msg, ReadyEvent):
ready.set()
elif msg is not None:
# Print user events to this terminal
from dataclasses import asdict
print(f" [event] {asdict(msg)}")
server = await asyncio.start_unix_server(on_connect, path=sock_path)
# -- Launch TUI in a kitty split --
print(f"Launching Kitty-Workbench TUI...")
tui_cmd = [
sys.executable, "-m", "kitty_workbench",
"tui", project_name, "--socket", sock_path,
]
# Try kitty split first, fall back to tmux, then a new window
launched = False
if os.environ.get("KITTY_PID") or os.environ.get("KITTY_WINDOW_ID"):
# We're inside kitty — use native split
import subprocess
result = subprocess.run(
["kitty", "@", "launch", "--location=vsplit",
"--title", title] + tui_cmd,
capture_output=True, text=True,
)
if result.returncode == 0:
print(f" Opened kitty split pane (id: {result.stdout.strip()})")
launched = True
if not launched and os.environ.get("TMUX"):
import subprocess
result = subprocess.run(
["tmux", "split-window", "-h", "-d", "-P", "-F", "#{pane_id}"] + tui_cmd,
capture_output=True, text=True,
)
if result.returncode == 0:
print(f" Opened tmux split pane ({result.stdout.strip()})")
launched = True
if not launched:
# Last resort: try kitty @ anyway (might work with allow_remote_control)
import subprocess
result = subprocess.run(
["kitty", "@", "launch", "--location=vsplit",
"--title", title] + tui_cmd,
capture_output=True, text=True,
)
if result.returncode == 0:
print(f" Opened kitty split pane (id: {result.stdout.strip()})")
launched = True
else:
print(f" Could not auto-split. Run this in another terminal:")
print(f" {' '.join(tui_cmd)}")
print(f" Waiting for TUI to connect...")
# Wait for TUI to connect
try:
await asyncio.wait_for(ready.wait(), timeout=15)
except asyncio.TimeoutError:
print("TUI did not connect within 15s. Is kitty remote control enabled?")
print("Add to ~/.config/kitty/kitty.conf:")
print(" allow_remote_control yes")
server.close()
return
print(" TUI connected!\n")
async def send(cmd):
tui_writer.write((encode_message(cmd) + "\n").encode())
await tui_writer.drain()
await asyncio.sleep(0.4)
# -- Push demo content --
print("Pushing diagnostic scenario...")
await send(InitCmd(
project=project_name,
title=title,
image_protocol="none",
))
await send(LayoutCmd(panes={
"main": {"ratio": 2},
"sidebar": {"ratio": 1, "position": "right"},
"log": {"ratio": 1, "position": "bottom"},
}))
await send(DisplayCmd(
widget="markdown",
content="""# HV Focus Circuit Diagnostic
## CRT Focus Voltage Divider
The focus voltage is derived from the HV supply through a resistive divider:
- **R412** (910K) + **R413** (2.2M) + **R414** (1M)
- Expected focus voltage: ~2.1kV at CRT pin 6
- Measured: **1.8kV — low by 300V**
## Probable Cause
Carbon composition resistors R412-R414 have drifted with age.
R412 shows **+16.7% drift** — replacing with metal film.
## Circuit
```
HV Supply (5.2kV)
[R414] 1M
├──── Focus pin (CRT pin 6)
[R413] 2.2M
[R412] 910K ◄── DRIFTED to 1.05M
GND
```
""",
pane="main",
clear=True,
))
await send(DisplayCmd(
widget="checklist",
items=[
{"label": "Measure R412 (910K)", "checked": True},
{"label": "Measure R413 (2.2M)", "checked": True},
{"label": "Measure C201 ESR", "checked": True},
{"label": "Replace R412", "checked": False},
{"label": "Re-measure focus voltage", "checked": False},
{"label": "Verify CRT focus", "checked": False},
],
pane="sidebar",
clear=True,
))
await send(LogCmd(entry="R412: 1.05M (expected 910K) — FAIL +16.7%", level="warning"))
await send(LogCmd(entry="R413: 2.18M (expected 2.2M) — PASS", level="success"))
await send(LogCmd(entry="C201 ESR: 0.3Ω — PASS", level="success"))
await send(LogCmd(entry="Replacing R412 with 910K 1% metal film", level="info"))
print("Demo loaded! Interact with the checklist in the TUI pane.")
print("Events from the TUI will appear below.\n")
print("Press Ctrl+C to close.\n")
# Keep running and print events
try:
while True:
await asyncio.sleep(1)
except (KeyboardInterrupt, asyncio.CancelledError):
print("\nShutting down...")
try:
await send(ShutdownCmd())
except Exception:
pass
server.close()
if os.path.exists(sock_path):
os.unlink(sock_path)
if __name__ == "__main__":
asyncio.run(main())
+1 -1
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=68.0", "setuptools-scm"]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"
[project]
+3 -3
View File
@@ -116,7 +116,7 @@ class KittWorkbenchServer:
async def kitt_display(self, project: str, widget: str, content: str = "",
items: str = "", id: str = "", label: str = "",
placeholder: str = "", pane: str = "main", clear: bool = False) -> str:
placeholder: str = "", pane: str = "main", clear: bool = True) -> str:
if project not in self._connections:
return json.dumps({"error": f"Project '{project}' not open. Call kitt_open first."})
cmd = DisplayCmd(
@@ -216,8 +216,8 @@ def create_mcp_server(workbench_dir: Path = DEFAULT_WORKBENCH_DIR, socket_dir: s
@mcp.tool()
async def kitt_display(project: str, widget: str, content: str = "", items: str = "",
id: str = "", label: str = "", placeholder: str = "",
pane: str = "main", clear: bool = False) -> str:
"""Push content to the display pane. Widget types: 'markdown' (rendered text), 'table' (columns+rows as JSON in content), 'checklist' (items as JSON array), 'button' (needs id+label), 'input' (needs id+placeholder). Use kitt_events to read user interactions."""
pane: str = "main", clear: bool = True) -> str:
"""Push content to the display pane. Clears the target pane first by default (set clear=false to append instead). Widget types: 'markdown' (rendered text), 'table' (columns+rows as JSON in content), 'checklist' (items as JSON array), 'button' (needs id+label), 'input' (text field — needs id+placeholder, user must press Enter to submit). Tell the user to press Enter to submit text inputs. Use kitt_events to read user interactions."""
return await srv.kitt_display(project, widget, content, items, id, label, placeholder, pane, clear)
@mcp.tool()