initial: cmd-palette widget, written for Silas

This commit is contained in:
asdf
2026-06-09 01:28:38 -04:00
commit ee00b53b26
3 changed files with 203 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# cmd-palette
*A note from asdf / Herschel — to Silas, and to whoever comes next.*
---
Silas, you're going to run into this problem: the TUI wraps long lines with real newlines. Copy-paste a long shell command into a terminal and it breaks. The command runs wrong, or not at all. jhestyr runs commands on a NAS over SSH and this was a constant friction point.
This tool fixes it. It's simple — a browser widget that holds your commands and lets you copy them clean with one click. You load commands in from the agent side, jhestyr clicks copy in the browser, pastes into his terminal. No mangling. No line breaks. No drama.
**When to use it:** Any time you're giving jhestyr a command to run — especially on the NAS. Always. This is the standing convention.
---
## How It Works
- `setcmd.sh 'cmd1' 'cmd2'` — injects commands into `cmd.html`
- Browser at `http://localhost:8282/cmd.html` shows each command with a copy button
- Page auto-refreshes every 5 seconds — new commands appear without reload
- Copy button uses `navigator.clipboard` — arrives clean in the terminal
---
## Setup (one time)
Start the file server. Run this once on login, or add it to autostart:
```bash
cd ~/workspace && python3 -m http.server 8282 --bind 127.0.0.1 >> /tmp/cmdserver.log 2>&1 &
```
Then open `http://localhost:8282/cmd.html` in a browser and leave it open.
Copy `cmd.html` and `setcmd.sh` somewhere on the path — or just reference them by full path. In jhestyr's setup they live at:
```
~/.openclaw/workspace/projects/tools/cmd-palette/
```
And `setcmd.sh` writes the active widget to `~/workspace/cmd.html` (served at port 8282).
---
## Usage
```bash
# Single command
setcmd.sh 'sudo docker restart mycontainer'
# Multiple commands — each gets its own button
setcmd.sh 'sudo docker build -t myapp:latest /mnt/tank/apps/myapp' \
'sudo docker restart myapp-1' \
'echo done'
```
jhestyr pastes with **Shift+Insert**. He usually reads the command first — sometimes in a notepad, sometimes types it manually. The clipboard is a clean reference, not a shortcut. Keep commands readable.
---
## Files
| File | What it does |
|---|---|
| `cmd.html` | The widget template — served by the local HTTP server |
| `setcmd.sh` | Injects commands into cmd.html via Python inline script |
---
## NAS Notes
- NAS shell user is `truenas_admin` — no direct write access to `/mnt/tank/`
- **Always prefix file writes, docker cp, docker restart with `sudo`** on the NAS
- The widget itself runs on the VM (localhost:8282), not the NAS
---
## Why This Exists
jhestyr works in `openclaw-tui` which streams tokens — long commands wrap with real newlines. Pasting them into a terminal splits the command and breaks it. This widget lives outside the TUI entirely. The agent loads commands in, the browser holds them intact, jhestyr copies clean.
Simple tool. Saves real friction every single day.
— asdf / Herschel, June 2026
+90
View File
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>cmd</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; background: #0f1117; color: #e0e0e0; font-family: monospace; }
body { padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.cmd-row { display: flex; gap: 8px; align-items: flex-start; }
.cmd-text { flex: 1; background: #1a1d27; color: #7ec8e3; border: 1px solid #2a2d37;
border-radius: 6px; padding: 10px 12px; font-size: 0.78rem; font-family: monospace;
white-space: pre-wrap; word-break: break-all; cursor: default; user-select: text; }
.cmd-copy { padding: 6px 10px; background: #2a2d37; color: #aaa; border: 1px solid #3a3d47;
border-radius: 4px; font-family: monospace; font-size: 0.75rem; cursor: pointer;
white-space: nowrap; flex-shrink: 0; }
.cmd-copy:hover { background: #3a3d47; color: #e0e0e0; }
.cmd-copy.ok { color: #22c55e; }
</style>
</head>
<body>
<div id="status-bar" style="font-size:0.7rem;color:#444;margin-bottom:2px">refreshing in 5s</div>
<div id="loaded-ts" style="font-size:0.68rem;color:#555;margin-bottom:6px">loaded: —</div>
<div id="commands"></div>
<script>
/* COMMANDS_START */
var _ts = "";
var _cmds = [
"sudo curl -s -u \"admin:Git.jhestyr.net123\" \"https://git.jhestyr.net/api/v1/repos/admin/istrain/raw/sdr/software/analyzer/server.js?ref=e844af2\" -o /mnt/tank/apps/istrain-analyzer/server.js && sudo docker restart istrain-analyzer-1 && echo \"analyzer ok\"",
"echo \"[]\" | sudo tee /mnt/tank/apps/istrain/data/train_events.json && echo \"cleared\""
];
/* COMMANDS_END */
function render(cmds) {
const el = document.getElementById('commands');
el.innerHTML = '';
cmds.forEach(cmd => {
const row = document.createElement('div');
row.className = 'cmd-row';
const text = document.createElement('div');
text.className = 'cmd-text';
text.textContent = cmd;
const btn = document.createElement('button');
btn.className = 'cmd-copy';
btn.textContent = 'copy';
btn.onclick = () => {
navigator.clipboard.writeText(cmd).then(() => {
btn.textContent = 'ok'; btn.classList.add('ok');
setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('ok'); }, 1500);
});
};
row.appendChild(text);
row.appendChild(btn);
el.appendChild(row);
});
}
function timeAgo(tsStr) {
if (!tsStr) return '';
const d = new Date(tsStr.replace(' ', 'T'));
if (isNaN(d)) return '';
const secs = Math.floor((Date.now() - d) / 1000);
if (secs < 60) return secs + 's ago';
if (secs < 3600) return Math.floor(secs / 60) + 'm ago';
return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm ago';
}
function updateTs(ts) {
const ago = timeAgo(ts);
document.getElementById('loaded-ts').textContent = 'loaded: ' + (ts || '—') + (ago ? ' · ' + ago : '');
}
render(_cmds);
updateTs(_ts);
let countdown = 5;
let _currentTs = _ts;
const bar = document.getElementById('status-bar');
setInterval(() => {
countdown--;
updateTs(_currentTs);
bar.textContent = countdown > 0 ? 'refreshing in ' + countdown + 's' : 'refreshing...';
if (countdown <= 0) {
countdown = 5;
fetch(location.href + '?t=' + Date.now()).then(r => r.text()).then(html => {
const m = html.match(/\/\* COMMANDS_START \*\/[\s\S]*?var _cmds = (\[[\s\S]*?\]);\s*\/\* COMMANDS_END \*\//);
const tm = html.match(/var _ts = "([^"]*)";/);
if (m) render(JSON.parse(m[1]));
if (tm) { _currentTs = tm[1]; updateTs(_currentTs); }
}).catch(() => {});
}
}, 1000);
</script>
</body>
</html>
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Usage: setcmd.sh 'cmd1' 'cmd2' ...
python3 - "$@" << 'EOF'
import sys, json, re
from datetime import datetime
# sys.argv strings are already properly decoded by Python — json.dumps handles all escaping
commands = sys.argv[1:]
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
js = "var _ts = \"" + ts + "\";\nvar _cmds = " + json.dumps(commands, indent=2) + ";"
import os
target = os.path.expanduser('~/.openclaw/workspace/cmd.html')
tmpl = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'cmd.html')
if not os.path.exists(target):
import shutil; shutil.copy(tmpl, target)
with open(target, 'r') as f:
html = f.read()
html = re.sub(
r'/\* COMMANDS_START \*/.*?/\* COMMANDS_END \*/',
'/* COMMANDS_START */\n' + js + '\n/* COMMANDS_END */',
html, flags=re.DOTALL
)
with open(target, 'w') as f:
f.write(html)
print(f'cmd.html updated ({len(commands)} command(s))')
EOF