v0.8.1
- PermissionRequest hook: the state file records tool, detail and description of a pending permission; Notification keeps it. - devdesk attach --answer (presses 1/Esc only if the same wait is on and the prompt is visible), --peek (last exchange from the transcript), --reply (bracketed paste + Enter, streamed like uploads). - Inbox: Allow/Deny/Open for permission waits; Peek & reply for other waits and finished sessions. Matrix messages include the command. Closes #22 Closes #23 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DevDesk
A persistent browser terminal for Claude Code, at https://dev.lotusguild.org.
- Persistent sessions: closing the tab, restarting the web service or upgrading the package never stops Claude.
- Survives reboots: after a reboot, every named session resumes its own conversation.
- Styling: uses the LotusGuild Terminal Design System (web_template).
- Packaging: ships as a
.debfrom Gitea's apt registry.
Browser ─https─▶ NPM (wildcard cert, Authelia two_factor, injects ttyd credential)
─http─▶ ttyd :7681 [devdesk.service] ── /ws?arg=<name> → `devdesk attach <name>` (tmux client)
tmux server [devdesk-tmux.service] ── one session per project:
window "claude": devdesk-claude-run → claude --resume <recorded id>
window "shell"
Why two units
The tmux server runs in its own unit, so every Claude process lives in that unit's cgroup.
- ttyd (
devdesk.service) only starts tmux clients (tmux -Nnever starts a server). It can restart freely, and does so on package upgrades. devdesk-tmux.serviceis installed with--no-stop-on-upgrade, so apt never touches it.- Only
systemctl stop devdesk-tmux(or removing the package) ends the sessions.
Sessions
- Any name works. Sessions don't have to be repo work:
- A name matching a project folder (
tinker_tickets, ortest-codefortest code) starts there. - Anything else (
pbs-upgrade,ceph-check) starts in$DEVDESK_DEFAULT_DIR, which is/root/codeby default. mainis the projects root.
- A name matching a project folder (
- Opening one:
https://dev.lotusguild.org/?arg=<name>opens or creates that session.- With no
?arg=, DevDesk reattaches the session you used last in this browser (or the most recently active one). The fzf picker, listing running (●), dormant (○) and new (+) sessions, is under the session chip's menu. - Session chip (header): click to switch sessions; its dot shows Claude's state.
- + New in the Sessions panel takes any name plus an optional project folder.
- Rename / end: the ✎ and × buttons on each session in the panel. Renaming keeps the session, its running Claude and its recorded conversation. Ending stops Claude; the conversation stays in Claude's history.
- Windows: each session has a
claudewindow (0) and ashellwindow (1). - Conversation tracking: a Claude
SessionStarthook records each session's conversation ID in/var/lib/devdesk/registry.json. After a reboot the session resumes that conversation, even when other Claude instances, such as code-server, use the same directory. - Session status: Claude hooks (
devdesk-state-hook) mark each session working, waiting on you (with Claude's message) or idle.- The sidebar shows it with context % and a context bar; the session chip's dot shows the current session's state.
- A "… needs you" chip jumps to other sessions that are waiting, and a desktop notification fires when one starts waiting.
- The 🔔 inbox lists what needs you (waiting sessions, firing Grafana alerts) and what happened while you were away (a background session finished or started waiting, CI passed or failed); its badge counts them.
- Answer from the inbox: a session waiting on a permission shows the exact command/file and Allow / Deny (the key is pressed only if that prompt is still on screen); other waits and finished sessions offer Peek & reply: the last exchange and a reply box.
- Matrix "Claude needs you": when a session has waited for
DEVDESK_MATRIX_DELAYseconds (30) and no browser tab or SSH client is attached to it,devdesk-notify-matrixposts one message per wait, with a link, to the hookshot webhook URL in/etc/devdesk/matrix-webhook(mode 600; no file = off). - File & image drop: drag files onto the terminal or paste a screenshot.
- They're uploaded through ttyd to
/var/lib/devdesk/uploads/<session>/(25 MB max, pruned after 7 days), and the path is pasted into Claude. - For images, Claude attaches them itself.
- They're uploaded through ttyd to
- Ops Alerts panel: firing and pending Grafana alert rules, each with a link to the rule.
- Ask Claude types an investigation prompt into the current session without sending it.
- Configure
DEVDESK_GRAFANA_URL, plus a Viewer token inDEVDESK_GRAFANA_TOKEN_FILE.
- Repo / CI panel: branch, ahead/behind, uncommitted changes, HEAD's Gitea CI status and open PRs for the session's folder. When CI fails, Ask Claude to fix CI types a prompt to read the failed job's log.
- Files (Ctrl+Shift+E), laid out like VS Code: an explorer tree (git colours, gitignored entries dimmed) and an editor with tabs (a click opens a preview tab, a double-click keeps it), Go to file (Ctrl+P in the pane), markdown previews (sanitized, with relative images), images, and a Changes view of uncommitted files with line counts, a C badge on files Claude edited, and diffs against HEAD. @Claude types
@pathinto the prompt. Tabs and open folders are remembered per session. Paths can't leave the session folder.- Editing: text files are editable; ● marks unsaved tabs; Ctrl+S (or Save) writes atomically and keeps the file's mode and owner. If the file changed on disk since you opened it (Claude edited it), saving asks: Compare (a diff of disk vs yours), Overwrite or Cancel; a change on disk while you edit shows a banner instead of replacing your text. Unsaved edits are kept as drafts in the browser, so a reload or a session switch doesn't lose them. + creates a file (folders as needed). The editor bundle (
/usr/share/devdesk/assets/editor.js) loads over ttyd the first time and is cached by the browser.
- Editing: text files are editable; ● marks unsaved tabs; Ctrl+S (or Save) writes atomically and keeps the file's mode and owner. If the file changed on disk since you opened it (Claude edited it), saving asks: Compare (a diff of disk vs yours), Overwrite or Cancel; a change on disk while you edit shows a banner instead of replacing your text. Unsaved edits are kept as drafts in the browser, so a reload or a session switch doesn't lose them. + creates a file (folders as needed). The editor bundle (
- Context: a Claude
statusLinecommand (devdesk-statusline) snapshots each session's model, context-window use and cost to/var/lib/devdesk/context/<session>.json. The Sessions panel shows it as a bar (cost in the tooltip), and Claude's own footer shows a one-line summary. - Light mode: the terminal switches palettes with the page. DevDesk starts Claude with its
dark-ansitheme so Claude draws with the terminal's 16 colours, and the light palette maps bright white to ink; tmux and the status line use ANSI colours too. A session picks up Claude's theme the next time Claude starts in it (/exit, then ⏎ resumes the same conversation). - Phone: a key bar replaces the shortcut hints (Esc, Tab, ⇧Tab, ^C, ↑, ↓, ⏎, Claude/Shell window), and the panels open as a drawer.
- SSH:
devdesk attach <name>,devdesk new <name> [project],devdesk rename <old> <new>,devdesk end <name>,devdesk ls,devdesk pick,devdesk repo <name>,devdesk alerts.
Keys
| Key | Action |
|---|---|
| Shift+Enter | Newline in Claude's prompt (sends Ctrl+J) |
| Esc | Interrupt Claude |
| Ctrl-b 0 / 1 | Claude window / shell window |
| Ctrl-b s | Switch session (the URL follows) |
| Ctrl-b d | Detach (the session keeps running) |
| Shift+drag | Native browser selection (plain drag goes to tmux copy-mode, which copies via OSC 52) |
| Ctrl+Shift+C / V | Copy selection / paste |
| Ctrl+Shift+P | Command palette (also the ⌕ button) |
| Ctrl+Shift+E | Files pane (Esc: back, then close) |
The ☀/◐ button switches light and dark mode (saved per browser, shared with the other web_template apps' lt_theme preference). Text size, desktop notifications (with a mute), the keyboard shortcuts sheet and fullscreen are in the ⋯ menu; everything is also in the command palette. In Chromium, fullscreen also captures Ctrl+W/Ctrl+T, which browsers otherwise keep for themselves.
Configuration
- Put your changes in
/etc/devdesk/env.local. It's read after the packagedenvand never shipped, soapt upgradenever asks about it. Example:DEVDESK_REMOTE_CONTROL=1. /etc/devdesk/envholds the packaged defaults; don't edit it. Its keys:- port, bind address, projects root and
DEVDESK_DEFAULT_DIR DEVDESK_GITEA_URL/DEVDESK_GITEA_ALIASESfor the Repo/CI panel. Public repos need no token; for private ones put aread:repositorytoken in/etc/devdesk/gitea-tokenCLAUDE_BINandCLAUDE_ARGSDEVDESK_REMOTE_CONTROL=1, which also starts each session with Claude Remote Control so it can be continued from the Claude appDEVDESK_PUBLIC_URL,DEVDESK_MATRIX_WEBHOOK_FILEandDEVDESK_MATRIX_DELAYfor the Matrix notifications
- port, bind address, projects root and
/etc/devdesk/secret.envis generated on install. It holds the ttyd credential that NPM must inject; see docs/DEPLOY.md./etc/devdesk/tmux.local.confis optional and is sourced after the packagedtmux.conf.
Build
Needs Node ≥ 18, debhelper, bats, tmux, jq, shellcheck:
make # dist/index.html (single inlined page) + build/ttyd (pinned, sha256-verified)
make lint test
dpkg-buildpackage -us -uc -b && lintian ../devdesk_*_amd64.deb
apt install ../devdesk_*_amd64.deb
CI (.gitea/workflows/build-deb.yml):
- Every push lints, tests, builds and smoke-tests the package and uploads it as an artifact.
- A
v*tag also publishes tohttps://code.lotusguild.org/api/packages/LotusGuild/debian, creates a release, and auto-deploys to CT 126. A signed webhook runsdevdesk-deploy, which upgrades the package; see docs/DEPLOY.md.
Roadmap
Tracked in Gitea issues by milestone: v0.6.0 file viewer, v0.7.0 editor + inbox, backlog.
Releases
28
devdesk v0.12.0
Latest
Languages
JavaScript
52.9%
Shell
35%
CSS
9.2%
HTML
2.4%
Makefile
0.5%