One elevated window to rule them all.
ShinShell exists because William once had six PowerShell windows open, all of them
titled "Windows PowerShell," and confidently piped a deploy command into the wrong
one. If you have ever restarted the wrong service, git pushed from the wrong repo,
or watched a command execute in a terminal you swore was pointed at the Pi —
this app is for you. This app is for us.
It's a Windows desktop shell that consolidates the whole ShinTech dev loop — PowerShell terminals, Claude chat, an editor, and one-keystroke deploys to the Pi — into color-coded project windows so you always, always know which project you're about to break.
- Dev machine: ScarlettWitch (Windows 11 Pro, user
billk) - Prime deploy target: shinobi, a Raspberry Pi 5 that has seen things
- Dev root:
C:\Users\billk\Projects\ShinTech\ - Status: Phase 0 (discovery) complete ✅ — building toward M1
For Claude Code: this file is the authoritative brief.
docs/DISCOVERY.mdholds the Phase 0 inventory and William's resolved answers. Sibling repos are reference-only — look, don't touch.
Grab the latest ShinShell-Setup-<version>.exe from
Releases and run it.
We don't code-sign, so the first launch gets a SmartScreen "unrecognized app"
screen — More info → Run anyway. Every launch after that is silent
(§ Elevation). Already installed? It checks for updates on its own; see
docs/DEPLOYING.md if you want to force one or ship a new version.
Every open project gets its own OS window, tinted with that project's accent color, with a matching colored dot on the taskbar icon. Inside each window: tabs.
| Tab type | What it is |
|---|---|
terminal |
Real pwsh.exe via ConPTY. Oh-My-Posh renders untouched. Splits supported. |
claude-code |
A terminal preset that just runs claude in the project dir. |
claude-chat |
claude.ai in an embedded browser, session persists across restarts. |
editor |
Monaco — the literal VS Code editor component, not a sad textarea. |
scratchpad |
Auto-saved per-project markdown for notes and "don't forget" lists. |
log-tail |
SSH into the Pi and watch logs scroll by. Very soothing. |
deploy |
Big deploy button, flag toggles, streamed output, SSH health light. |
And the whole app runs persistently elevated with zero UAC prompts (§ Elevation), because clicking "Yes" forty times a day builds no character whatsoever.
| Project | Accent | Why |
|---|---|---|
| imq2 / Q2 | #33FF66 |
H9000 terminal green — Q2's own face |
| shinlink-os | #FF8000 |
McLaren papaya, matching its UI |
| AC1Companion | #E10600 |
Racing red, obviously |
If you're about to type a deploy command and the window chrome is green, you're talking to Q2. If it's papaya, hands off the CRSF hardware. That's the entire thesis of this application.
| Layer | Choice | Why |
|---|---|---|
| Shell | Electron + React + TypeScript | Same stack as AC1Companion; known territory |
| Terminal | xterm.js + node-pty | The VS Code combo. ConPTY → real pwsh → Oh-My-Posh just works |
| Browser tab | WebContentsView | Persistent session partition; Chrome user-agent override so Google OAuth doesn't sulk |
| Editor | Monaco | Free VS Code editor, zero excuses |
| File watch | chokidar | For watch-and-sync mode |
| Config | JSON in %APPDATA%/ShinShell/ |
Human-editable, portable |
| Packaging | electron-builder (NSIS) | AC1Companion already proved this pipeline |
| Updates | electron-updater + GitHub Releases | Feed is the publish block in package.json; NSIS installer replay means customInstall (scheduled task) re-registers on update too, not just fresh installs |
Rules of the road: all pty/fs work in the main process, typed IPC channels to the
renderer, PowerShell 7 (C:\Program Files\PowerShell\7\pwsh.exe) as the default
shell spawned with the normal profile so Oh-My-Posh loads exactly like standalone.
xterm.js WebGL renderer on, fit addon handling resizes.
The trick is a Windows Scheduled Task:
- The installer (elevated once) registers task
ShinShell— Run with highest privileges, trigger on demand, action: launch the app. - The desktop/taskbar shortcut runs
schtasks /run /tn "ShinShell"— the app launches elevated and UAC never says a word. - If the task is missing (new machine, someone got tidy), the app detects it's not elevated and offers a one-click self-repair.
- A small ADMIN badge lives in the UI at all times, because silent power is how accidents happen.
The honest tradeoff: everything inherits admin — terminals, Claude Code, the embedded browser. Also, Windows UIPI blocks drag-and-drop from non-elevated Explorer into elevated windows, so there's a proper Open File dialog instead. We accept this. We accepted it the fortieth time UAC asked if we were sure.
That inheritance is also what makes silent self-updates work: the app only
ever runs elevated (via the scheduled task), so the installer
quitAndInstall spawns for an update inherits the same elevated token —
no UAC prompt mid-update, same as every other launch.
Rebuilding/reinstalling? See docs/DEPLOYING.md.
- Terminals — unlimited pwsh tabs per project, opened in the project dir, Oh-My-Posh intact, copy/paste, search, splits, font config.
- Project windows — color-coded per the table above, taskbar overlay dots
(
setOverlayIcon), in-app project create/edit. - Claude chat tab — persistent login, back/forward/reload, external links punt to the default browser.
- Editor + scratchpad — Monaco with open/save into the project tree.
- Hotkey command system — see Hotkeys below.
- Zero-prompt elevation — see above.
- Session restore — kill the app, relaunch, everything comes back (windows, tabs, working dirs; scrollback gets to die).
- Log-tail tab — runs any configured SSH command (learned in Phase 0:
neither main app uses systemd, so no journalctl assumptions). Q2 logs to
imq2/logs/imq2.log; ShinLink's ground station logs to/home/shinobi/shinlink_os.log.tail -fis the workhorse; journalctl is only for the Pi-Zero vehicle services. Reconnect button, connection state dot. - Deploy tab + SSH health — deploy button with flag toggles (checkboxes
for
-dryrun/-restartinstead of a hotkey per permutation), streamed output, deploy history (last 20 runs, exit codes, durations), and a health light that TCP-pokes port 22 on the primary target every ~15s. - Port/process panel — lists listening ports, filterable, per-row kill. Ports named in project configs get highlighted, and any port claimed by two projects gets a special cross-project collision warning — a class of bug we discovered while writing this spec (UDP 8000: imq2's Forza listener vs. Watchtower's APRS feed, same Pi 🎉). Resolution: APRS moves to 8010; 8000–8003 is telemetry's block now, no squatters.
- Global summon — Ctrl+` toggles the most recent project window, quake-style.
- Watch-and-sync — chokidar on configured globs → deploy on save, debounced. The dev loop becomes save and it's on the Pi.
- Command palette — Ctrl+Shift+P fuzzy search over every saved command,
tab action, and project. Also home to deliberately-hotkey-less commands
like
Publish shinagent (public sanitized)— some things should require intent. - Git status in tab strip — branch + dirty dot per project.
- Theming — dark default, ShinTech/H9000 aesthetic encouraged, readability beats vibes.
Non-goals for v1: SSH multiplexing UI (just spawn ssh in a pty), plugins,
macOS/Linux, tray-only mode, settings sync. Scope creep is how apps die young.
One JSON per project in %APPDATA%/ShinShell/projects/. Real defaults for all
three projects live in docs/DISCOVERY.md §2 (extracted from the actual repos,
not vibes). The shape:
Command strings support {targets.<id>.<field>}, {workingDir}, and
{env.<NAME>} substitution. runIn is new-tab | active-terminal |
background. Targets accept hostnames or IPs (Pi Zeros use dynamic Tailscale
hostnames, so they'll be added ad hoc with healthCheck: false). Commands
marked "dangerous": true don't fire on the first keypress — see
arm-to-confirm in the UX section. lastKnownGitRemote is written
automatically on every successful window open (§UX8) — it's what ranks
rename-recovery suggestions when workingDir goes missing, not something
you're expected to hand-edit.
Seed drift: a project's config is only ever copied from these defaults
once, into a brand-new (empty) %APPDATA%/ShinShell/projects/ — after that
it's the user's file, and ensureSeeded() never touches it again. So a field
added to a default command later (like deploy-restart's dangerous: true)
still reaches an install that already has that project on disk,
getProject/listProjects backfill any command field present in the seed
but missing from the saved copy, matched by command id. Never overwrites a
field — or an explicit override like "dangerous": false — the user's copy
already has, and never resurrects a command the user deleted.
SSH convention (decided in Phase 0): aliases everywhere. ~/.ssh/config
defines Host shinobi (LAN) and Host shinobi-ts (Tailscale); ShinShell
configs and both projects' deploy.ps1 reference the alias. Hardcoded IPs are
how you end up deploying to a router. Note the Pi user is shinobi, not billk
— the machines have callsigns too.
Two scopes, one philosophy: same slot = same kind of action in every project, so muscle memory transfers.
Global (OS-wide):
| Key | Action |
|---|---|
| Ctrl+` | Summon/hide most recent project window |
| Ctrl+Alt+1..9 | Jump to project N |
| Ctrl+Alt+T | New terminal in active project |
Project-scoped (per the configs):
| Key | imq2 | shinlink-os | AC1Companion |
|---|---|---|---|
| Ctrl+1 | Deploy to Pi | Deploy to Pi | Deploy |
| Ctrl+Shift+1 | Deploy + restart Q2 | Deploy (dry run) | — |
| Ctrl+2 | Tail Q2 logs | Tail ground station log | — |
| Ctrl+3 | Q2 status (tmux) | (reserved) | — |
| Ctrl+4 | Restart Q2 (tmux) | (reserved) | — |
| Ctrl+5 | Attach Q2 tmux | (reserved) | — |
Plus the usual: Ctrl+T new tab, Ctrl+W close, Ctrl+Tab cycle. All bindings editable, with conflict detection. Commands can either execute in a new tab or be injected into the active terminal without pressing Enter — typed but not fired, cursor at the end, for the trust-but-verify crowd.
One deliberate exception: shinlink-os gets no remote "run" hotkey. The
ground station is a Tkinter GUI driving real GPIO (CPPM/S.BUS/CRSF) on the Pi's
attached display. You do not launch that over SSH. You walk over to it like a
gentleman.
The design test for everything in this section: does it reinforce knowing where you are, or reduce keystrokes you already type? If neither, it doesn't ship. These aren't features — they're guardrails on the features above.
- Color where your eyes actually are. Window chrome is easy to tune out
once a terminal is full-screen, so the accent color follows your focus: a
thin accent strip along the top of every tab's content area, and — the
sneaky one — an env var (
SHINSHELL_PROJECT,SHINSHELL_ACCENT) injected into every pty so an Oh-My-Posh segment renders the project name, in the project color, inside the prompt itself. You look at the prompt when you type. That's where the guardrail lives. - Arm-to-confirm on dangerous commands. Commands flagged
dangerous: truedon't fire on the hotkey — they arm. The deploy button pulses in the accent color showing exactly what's loaded ("Deploy + Restart Q2 → shinobi"), and the same key confirms within 3 seconds or it disarms. One extra keystroke, only where the wrong-window mistake actually hurts. Regular deploys stay one-touch. - Outcomes without babysitting. Deploys are long enough to tab away from:
setProgressBaron the taskbar icon while running, then a toast + brief taskbar flash on completion — green or red. A failed run also leaves its tab glowing red until it's been looked at. Exit code 1 does not get to scroll quietly into history. - Hotkey sidebar. Ctrl+1–5 means different things per project by
design, so the bindings live in a collapsible sidebar instead of
cluttering the top of the window: a slim icon rail on the right edge that
expands on Ctrl+/ (or clicking the rail) into a panel listing the
current project's commands — hotkey, label, target — in the project's
accent color, with
dangerouscommands marked. Rows are clickable (click = run, same arm rules apply), so it doubles as a mouse-friendly command list for the dev buddies. Auto-collapses on Esc or refocusing the terminal; pinnable open for learning week. Collapsed, it's ~40px of icons; the terminal keeps the real estate. - A health light that earns its tooltip. The SSH dot stays binary at a glance, but hovering it answers the whole "is the Pi okay" question: round-trip latency, LAN vs. Tailscale resolution, time since last successful deploy, and that deploy's exit code.
- Tab titles that describe state, not type. "Terminal 2" tells you
nothing;
~/imq2 (main*)— cwd + git branch + dirty marker — is orientation. Log-tail tabs surface connected/reconnecting state in the title. - Color-first quick-switch. Holding Ctrl+Alt pops a strip of colored project chips — alt-tab, but switching is a color choice instead of reading window titles. Same thesis, smallest possible form.
- Path validation + edit dialog. A project's
workingDirisn't guaranteed to still be there — folders get renamed, drives get unmounted. Every registered project is checked (app launch, window open, and again on launcher/window focus) without ever throwing; a missing folder swaps a launcher card's open button for a warning + Fix path…, and disables new tabs/commands/deploys/watch-sync in an already-open window (existing terminals keep running — their ptys don't care) with a toast pointing at the fix. Edit project details (kebab menu on a card, gear icon in the project window's hotkey panel, or the command palette) covers name/accent/workingDir/shell/env, with live path validation and a native folder picker. When the path's broken, the dialog scans its old parent directory and offers up to 3 "did you mean?" candidates — ranked by git remote match, then name similarity, then recency — and clicking one applies and saves in a single click, since a plain rename-in-place is the common case.
Build priority if it comes down to it: prompt indicator (1) → arm-to-confirm (2) → completion toasts (3). Those three attack the founding mistake directly.
Each milestone runs end-to-end before the next begins. Commit per milestone.
- M0 — Discovery ✅
docs/DISCOVERY.mdwritten, all 11 questions answered. - M1 — Terminal core: shell + xterm.js/node-pty tabs, Oh-My-Posh verified pixel-identical to standalone pwsh, splits, terminal session restore.
- M2 — Projects & windows: config schema, launcher window, per-project
windows, accent colors, taskbar dots, accent content strips, and the
SHINSHELL_*env vars + Oh-My-Posh prompt segment (UX 1). - M3 — Elevation & packaging: NSIS installer, scheduled-task registration, zero-prompt elevated launch verified, ADMIN badge. (Crib from AC1Companion's electron-builder setup.)
- M4 — Commands & hotkeys: saved commands, variable substitution, both
hotkey scopes, injection mode, arm-to-confirm for
dangerouscommands (UX 2), collapsible hotkey sidebar (UX 4). - M5 — Claude & editor tabs: claude-chat WebContentsView with OAuth verified, claude-code preset, Monaco + scratchpad.
- M6 — Pipeline features: deploy tab with flag toggles, SSH health with rich tooltip (UX 5), completion toasts + taskbar progress + red failed-tab glow (UX 3), log-tail, port panel with collision warnings, stateful tab titles (UX 6).
- M7 — Polish: watch-and-sync, command palette, git status, color-first quick-switcher (UX 7), theming, README.
- Taskbar click → elevated window in <3s, UAC silent
-
whoami /groupsshows elevated; Oh-My-Posh prompt identical to standalone pwsh - Two projects open = two windows, unmistakably different colors, distinguishable in the taskbar
- Ctrl+1 in the green window deploys Q2; Ctrl+1 in the papaya window deploys ShinLink — and you can tell which is which before you press it (the entire point)
- Claude tab stays signed in across app restarts
- Force-kill the app → relaunch restores every window, tab, and working dir
- Pull the Pi's ethernet → SSH light goes red within ~30s
- The prompt itself shows the project name in the project color — full-screen terminal, chrome invisible, you still know where you are
- Ctrl+Shift+1 arms (does not fire) Deploy + Restart; second press within 3s fires it; waiting disarms it
- A failed deploy leaves visible evidence (red tab glow + toast) even if you were in another window when it died
- Ctrl+/ expands the hotkey sidebar to a clickable command list in the project's accent color; clicking into a terminal (or Esc) collapses it again unless pinned
- Everything except the Claude tab and remote features works offline
- Top bar contains exactly three things — tab strip, SSH health dot, ADMIN badge — nothing else in
.tab-strip-row's component tree - Tab strip never wraps to a second row; scrolls horizontally with overflow arrows once tabs don't fit, arrows only appear when there's actually overflow
- Hotkey sidebar is a flex sibling of the tab content, not an overlay: expanding/collapsing/pinning it visibly resizes the content area, and every mounted terminal reflows (confirmed via
pty.resizefiring and the container's measured width changing) - Usable at 1080p landscape, 4K landscape, 1080×1920 portrait, and a narrow 800px window — no clipped controls, no overlap, no horizontal scrollbar on the document in portrait
- Terminal resize is debounced (~50ms) off a ResizeObserver; split-pane dividers clamp to a minimum pane size (~20 cols / 6 rows) instead of an arbitrary ratio
- Drag the window between two different-DPI monitors → prompt stays crisp (re-fit + redraw wired to the window's
movedevent anddisplay-metrics-changed/scaleFactorChanged; not yet manually verified on real mixed-DPI hardware) - Window position/size persists per window (keyed by project id) and restores on the correct monitor when it's still attached, falling back to a default position when it isn't (implemented; not yet verified across an actual monitor unplug/replug)
- Minimum window size (640×480) enforced on both the launcher and project windows
- Rename a project's folder while ShinShell is closed → launch → the launcher card shows a warning + "Fix path…" (no open button); the edit dialog's "did you mean?" ranks the renamed folder first via git remote match; one click applies and saves; the card immediately opens normally and window chrome (title, accent) is correct
- Rename a project's folder while its window is open → existing terminal(s) keep running untouched; attempting a new tab is blocked with a toast pointing at Edit project details, surfacing on the next window focus (no polling)
- A bad/missing workingDir never throws past pty spawn, background commands, or watch-sync start — verified via the above (no crash, just a graceful block + log line)
- Ask before adding dependencies beyond the architecture table.
- Sibling repos are read-only reference. The one-commit fixes noted in Phase 0 (shinlink deploy.ps1 alias, APRS → 8010, stale README path) happen in those repos, separately — not smuggled in here.
- No secrets in the repo. SSH rides the existing key setup; do not build password storage.
- Windows-only. Every hour spent on cross-platform abstraction is an hour the deploy button doesn't exist.
- When unsure how something actually works, check the sibling repos first, then ask William. The repos remember; humans improvise.
{ "id": "imq2", "name": "IMQ2 / Q2", "accentColor": "#33FF66", "workingDir": "C:/Users/billk/Projects/ShinTech/imq2", "shell": "pwsh.exe", "env": {}, "targets": [ { "id": "shinobi", "label": "Pi 5 (shinobi) — LAN", "host": "shinobi", "user": "shinobi", "port": 22, "healthCheck": true }, { "id": "shinobi-ts", "label": "Pi 5 (shinobi) — Tailscale", "host": "shinobi-ts", "user": "shinobi", "port": 22, "healthCheck": false } ], "commands": [ { "id": "deploy", "label": "Deploy to Pi", "command": "./deploy.ps1", "hotkey": "Ctrl+1", "runIn": "new-tab" }, { "id": "deploy-restart", "label": "Deploy + Restart Q2","command": "./deploy.ps1 -restart", "hotkey": "Ctrl+Shift+1", "runIn": "new-tab", "dangerous": true }, { "id": "logs", "label": "Tail Q2 logs", "command": "ssh {targets.shinobi.host} tail -n 50 -f imq2/logs/imq2.log", "hotkey": "Ctrl+2", "runIn": "new-tab" }, { "id": "attach", "label": "Attach Q2 tmux", "command": "ssh -t {targets.shinobi.host} 'cd imq2 && bash scripts/q2_attach.sh'", "hotkey": "Ctrl+5", "runIn": "new-tab" } ], "watchSync": { "enabled": false, "globs": ["**/*.py"], "ignore": ["**/__pycache__/**", "**/.git/**", "**/.venv/**"], "onChange": "deploy", "debounceMs": 1500 }, "ports": [8000, 8001, 8002, 8003, 8091, 8092, 8095, 8765, 8766, 8767], "restore": { "tabs": [] }, "lastKnownGitRemote": "git@github.com:ShinobiFPV/imq2.git" }