Compare commits
9 Commits
f6a1cc4ebd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bc0827a3be | |||
| dce4c93cff | |||
| 705d53f957 | |||
| ea9b11faa4 | |||
| edebdcbab0 | |||
| 26c9d2cd77 | |||
| f5dd2a6f58 | |||
| ef34625104 | |||
| 679846e877 |
+88
-7
@@ -62,6 +62,12 @@ who am i 2>/dev/null
|
|||||||
|
|
||||||
# Kitty remote control (only if kitty is installed)
|
# Kitty remote control (only if kitty is installed)
|
||||||
kitty @ ls 2>&1 | head -1 || true
|
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
|
## 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.
|
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:**
|
**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:**
|
> "You're connected via SSH. Here are your options:
|
||||||
Present tmux as the ready-now option. Optionally mention kitty as an upgrade for better image quality if they want to install it later.
|
>
|
||||||
|
> **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:**
|
If the user chooses B or C, install kitty on the remote host:
|
||||||
Great — they get the best experience. Just need `allow_remote_control yes` in kitty.conf.
|
- 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:**
|
**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
|
## Step 4: Execute Setup
|
||||||
|
|
||||||
@@ -127,7 +157,58 @@ Based on what the user chose:
|
|||||||
echo "allow_remote_control yes" >> ~/.config/kitty/kitty.conf
|
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`
|
- Linux: `sudo apt install chafa` or `sudo pacman -S chafa`
|
||||||
- macOS: `brew install chafa`
|
- macOS: `brew install chafa`
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Kitty-Workbench
|
# Kitty-Workbench
|
||||||
|
|
||||||
|
**Open source and safe for work.**
|
||||||
|
|
||||||
An MCP server that gives your AI a rich, interactive display panel — right in your terminal.
|
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.
|
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:
|
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:
|
Or if you've already cloned it, open your AI CLI in the repo and say:
|
||||||
|
|||||||
@@ -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
@@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=68.0", "setuptools-scm"]
|
requires = ["setuptools>=68.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class KittWorkbenchServer:
|
|||||||
|
|
||||||
async def kitt_display(self, project: str, widget: str, content: str = "",
|
async def kitt_display(self, project: str, widget: str, content: str = "",
|
||||||
items: str = "", id: str = "", label: 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:
|
if project not in self._connections:
|
||||||
return json.dumps({"error": f"Project '{project}' not open. Call kitt_open first."})
|
return json.dumps({"error": f"Project '{project}' not open. Call kitt_open first."})
|
||||||
cmd = DisplayCmd(
|
cmd = DisplayCmd(
|
||||||
@@ -216,8 +216,8 @@ def create_mcp_server(workbench_dir: Path = DEFAULT_WORKBENCH_DIR, socket_dir: s
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def kitt_display(project: str, widget: str, content: str = "", items: str = "",
|
async def kitt_display(project: str, widget: str, content: str = "", items: str = "",
|
||||||
id: str = "", label: str = "", placeholder: str = "",
|
id: str = "", label: str = "", placeholder: str = "",
|
||||||
pane: str = "main", clear: bool = False) -> str:
|
pane: str = "main", clear: bool = True) -> 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."""
|
"""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)
|
return await srv.kitt_display(project, widget, content, items, id, label, placeholder, pane, clear)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
|
|||||||
Reference in New Issue
Block a user