Persistent AI development workspaces, presented as native Hyprland windows.
Herdr runs the editor, agents, shells, and dev servers. td brings their terminals onto your desktop as independent Foot windows. Hyprland owns window layout; there is no second tiling layout to manage inside td.
┌──────────────────┬───────────┬───────────┐
│ │ │ terminal │
│ editor │ agent ├───────────┤
│ │ │ terminal │
└──────────────────┴───────────┴───────────┘
optional browser below the editor
Hide the windows → the same processes keep running in Herdr.
Show them again → attach to the same live terminals.
Everything is a td subcommand. td with no subcommand is shorthand for
td create; both create or reuse a workspace for the current directory.
Run td --help for the full command list.
td create # Editor + agent + two shells, shown on the desktop
td create codex # Choose the initial agent
td create --background # Start entirely in the background
td list # Find desktop and background workspaces
td focus # Choose a workspace with terminal + fzf
td focus --omarchy # Use Omarchy's built-in graphical menu
td focus --all # Attach every Herdr workspace on this server
td show <workspace> # Attach all of a workspace's terminals as windows
td hide <workspace> # Detach the desktop views; keep processes running
td attach # Open the workspace picker (same as td focus)
td attach <terminal> # Attach just one terminal
td stop <workspace> # End the workspace's processes
td close # Close the focused window and its backing terminalA workspace is a persistent collection of running terminals for a checkout. A window is an optional view into one of those terminals. The workspace can exist with no windows, a single attached window, or a full desktop layout.
From the checkout you want to work on:
cd ~/code/my-project
td createFor a new workspace, td:
- Connects to the selected Herdr server, starting it headlessly if necessary.
- Creates the workspace and its editor, agent, and two shell terminals.
- Attaches each terminal in its own Foot window on a dedicated Hyprland workspace.
- Applies the editor-left, agent-middle, terminals-right layout and starts the local watcher for new terminals.
$EDITOR selects the editor (default nvim). TD_AI_CMD selects the initial
agent command (default claude --dangerously-skip-permissions). Override the agent
or start additional command terminals when creating a new workspace:
td create codex
td create opencode -- 'bun dev' 'bun test'
# Equivalent short forms:
td codex
td opencode -- 'bun dev' 'bun test'With startup commands, td creates an editor, an agent, a spare shell, and a terminal for each command. A command that finishes returns to an interactive shell.
Creation is idempotent. Running td create again reuses the checkout's
workspace and presents its existing terminals. It does not restart agents or rerun
startup commands. To add work after creation, use td agent or td terminal.
If exactly one existing Herdr workspace already uses the directory, td adopts it
without replacing terminals or adding the default set of tools. Add any missing
editor or shell with td editor and td terminal. If several Herdr workspaces
match, choose one explicitly with td show <workspace-id>.
Desktop names include a short hash of the canonical directory and server identity, so matching repository basenames and separate Herdr servers do not collide.
workspace_id=$(td create --background codex | jq -r '.id')
td list
td status --jsonThis creates the same running tools, but opens no desktop windows and needs no Hyprland or Foot. Capture the returned ID, as above, when controlling the workspace from another project, an agent, or a script.
Add a task or command without presenting a window:
td spawn --workspace "$workspace_id" --background \
--agent codex --message 'Review the current diff and report findings'
td terminal --workspace "$workspace_id" --background -- bun test--background suppresses presentation for the new terminal even if other windows
from the workspace are visible. Running td create --background for a workspace
that is already visible does not hide it; use td hide for that transition.
td list # ID, name, desktop/background state, directory
td list --json # Includes terminal IDs, roles, agents, and td adoption state
td status --json # Includes reported native agent session references
td focus # fzf picker, including background workspaces
td focus --omarchy # Built-in Omarchy picker (Super+Alt+Enter)
td focus --walker # Optional alternative if Walker is installed
td focus "$workspace_id" # Skip the picker and focus/attach this workspace
td focus --all # Attach all Herdr workspaces on the selected servertd focus uses fzf in the current terminal by default. When launched from a
desktop shortcut without a controlling terminal, it opens a temporary Foot window
for fzf. Use --omarchy for the same built-in menu as Omarchy's app picker, or
--walker if you separately install Walker. The pickers show each workspace's
name, desktop/background state, and directory, plus Create workspace here and
Attach all workspaces options. Press Escape to cancel without changing workspaces.
The picker and td list use Herdr's full live workspace inventory, including
workspaces created directly in Herdr that td has never managed. Discovery does not
adopt or change them. Selection adopts the workspace and attaches its live terminals
without restarting them. JSON listings expose managed: false until adoption.
No td workspace is required to use the attachment shortcut. If Herdr has no
workspaces, td attach --omarchy creates and presents one, starting Herdr if necessary.
With existing workspaces, Create workspace here creates or reuses the workspace
for the current directory instead of requiring you to leave the picker and run
td create first. Existing Herdr workspaces for that directory may be adopted.
For desktop shortcuts, the creation directory comes from the focused managed terminal, or Omarchy's active-terminal directory resolver outside td. Otherwise td uses the calling process's current directory. You can choose a directory explicitly:
td attach --omarchy --directory ~/code/my-projectSelecting a workspace focuses its desktop and attaches its current terminals,
just like td show. Attach all workspaces, or td focus --all, does that for
every workspace on the selected Herdr server, including background and previously
unadopted workspaces. Use td attach <terminal> when you
only want one terminal rather than the full workspace.
td attach without an ID opens the same workspace picker; td attach --omarchy
and td attach --all select its native graphical and attach-all modes.
show, hide, and stop accept a workspace ID, its td name, or its checkout path:
td show "$workspace_id"
td show ~/code/my-projectWith no target, td prefers the calling Herdr terminal's workspace, then the focused td desktop context. On an ordinary graphical workspace it does not select a hidden td workspace just because the launcher's directory happens to match. Without a graphical session, it can resolve the current checkout. An agent running in one workspace does not follow your focus into another. Use an explicit ID when coordinating work across projects. IDs belong to the selected Herdr server.
From your local graphical session:
td hide "$workspace_id"From the current td desktop, Super+Alt+Shift+W runs td hide for the whole
workspace. This detaches its windows without stopping the backend processes.
td closes the attached Foot views and parks associated browser windows on a hidden special workspace. The editor, agent conversations, shells, and dev servers remain alive in Herdr. They remain inspectable and controllable through Herdr's CLI.
Closing an individual Foot window has the same detach-only effect for that
terminal. The watcher leaves it closed until you explicitly attach it or show the
workspace again. Closing the last terminal window returns the workspace to
background mode on the next reconciliation; td hide makes the transition explicit.
Closing a window is different from exiting the shell inside it. Exiting a shell
or running td stop ends backend work; detaching a window does not.
Super+W closes only the td window, leaving its terminal running.
Super+Alt+W runs td close: it closes the focused window and terminates its
Herdr pane. Herdr removes the backing tab when its last pane closes; sibling
panes in a shared tab remain running. On an ordinary application window this is
just a normal window close, and it works without a running Herdr server.
You can inspect a single agent or dev server without restoring the entire desktop.
Find its terminal in td list --json. The terminals[].id field is a Herdr pane ID;
terminals[].terminal is its terminal ID. td attach accepts either.
For example, select the initial agent terminal:
agent_terminal=$(td list --json | jq -r --arg workspace "$workspace_id" '
.[] | select(.id == $workspace)
| [.terminals[] | select(.role == "ai")][0].id
')
td attach "$agent_terminal"This opens one Foot window attached to the same live terminal and conversation.
If that view is already open, td focuses it rather than duplicating it. Other
existing background terminals stay hidden. Newly created terminals can appear
through the watcher while the workspace is partially shown; use --background
when creating one that should remain hidden.
td show "$workspace_id"
td reset-layout # Optional: restore the three-column desktop layoutshow attaches every current terminal, including ones that were explicitly created
in the background, and returns parked browser windows. It reuses existing views and
does not rerun commands. Terminal identities and running processes stay the same.
The watcher presents newly created terminals while the workspace is shown, including ones created by agents using Herdr directly. Hyprland still owns all window layout.
td stop "$workspace_id" # End terminal processes; keep checkout and branch
td cleanup --dry-run # Preview sibling checkout/Compose-stack cleanup
td cleanup # Interactively choose checkouts to removeUse hide when you want to return later to the same running processes. Use stop
when the work should end. stop does not delete files or branches, and td never
implicitly stops sibling Herdr worktree workspaces. cleanup is the separate
checkout-removal workflow and also tears down selected Compose stacks and volumes.
| Operation | Desktop windows | Backend terminals | Checkout |
|---|---|---|---|
td create --background |
None opened | Created/reused | Kept |
td attach <terminal> |
One attached/focused | Same processes | Kept |
td show <workspace> |
All attached | Same processes | Kept |
td hide <workspace> |
Terminal views detached; browsers parked | Keep running | Kept |
td stop <workspace> |
Terminal views lose their backend | Stopped | Kept |
td close / Super+Alt+W |
Focused window closed | Its backing terminal stopped | Kept |
td cleanup |
Selected td backends stopped | Stopped | Selected checkouts removed |
Persistence here means survival across client detachment. After a machine or Herdr server restart, Herdr's own restore and agent-resume policy applies instead.
td create -w feature-branch # Open/create a sibling Git worktree
td create --background -w feature-branch # Prepare it without desktop windows
td create -c my-clone codex # Clone to a sibling directoryThe short forms td -w ... and td -c ... also work. Each checkout has its own
workspace lifecycle and can be shown, hidden, or stopped independently.
The commands below work inside a td workspace, including from its background
terminal processes. Use --workspace <id> to target another workspace.
td agent codex
td agent --background claude
td terminal -- bun test
td editor
td spawn -a codex -m 'Review the current diff'
td spawn --background -w <workspace-id> -a opencode -m 'Run the test suite'
td fork # Fork the calling/focused agent's exact session
td fork --pane <pane-id>
td fork --agent opencode --session <session-id>
td browser https://example.com # Chromium with the Playwright login profile + CDP
td browser --endpoint # CDP URL for agents to attach to this browser
td diff # Interactive branch diff in an attached window
td diff develop
td diff --inline develop # Use the current terminal
td dispatch terminal # Try td, then use the normal Omarchy launcher on failurePut -- before application arguments that overlap td flags.
--background suppresses automatic presentation even when the project is shown.
Without a desktop connection, helpers create background terminals automatically.
td terminal also works as a normal terminal launcher outside td: if no td desktop
or caller context applies, it falls back to omarchy launch terminal. The same
applies when Herdr is not running. Commands such as td terminal -- bun test retain
their arguments in the fallback terminal. Explicit td options such as --workspace
or --background remain backend requests and never silently fall back.
Agent state, foreground working directory, and native session identity come from
Herdr. There is no /proc session scraping, window-title matching, or fallback to
the most recent conversation. When a session ID is unavailable, install the
appropriate Herdr integration or provide the exact ID explicitly:
herdr integration install opencode
herdr integration install claude
herdr integration install codex
td fork --agent opencode --session <session-id>Initial prompts support Claude, Codex, and OpenCode. Herdr's CLI remains available for agent orchestration, terminal reads, input, and waits. IDs returned by td work with the corresponding Herdr commands on the same server.
Codex and Claude agent launches use bypass mode by default:
--dangerously-bypass-approvals-and-sandbox for Codex and
--dangerously-skip-permissions for Claude. This applies to td agent, td spawn,
new workspace agents, forks, and the Omarchy shortcut fallback. Existing bypass
flags are not duplicated. OpenCode and other agent commands keep their own options.
td focus: fzf workspace picker;--omarchyselects the native graphical mode and--allattaches every Herdr workspace on the selected server without a picker.td reset-layout: restore editor/browser left, agents middle, terminals right. This rearranges Hyprland windows without changing Herdr topology or processes.td overview: temporarily collect visible td agent windows onto one desktop.td status --waybar: bar JSON for workspaces with attached td windows only. Labels are capped at 10 characters including...when truncated; tooltips keep the full workspace names and agent states.td status --jsonstill lists all workspaces.td cleanup: remove selected sibling checkouts and their Compose stacks. It stops matching td workspaces on the selected Herdr server before deleting files.
The desktop uses Hyprland's configured dwindle layout and Lua dispatchers.
td doesn't modify compositor configuration. Resource controls and systemd slice
management have been removed; the old --performance and TD_MEMORY_* settings
are no longer part of td.
install-bar.sh installs an adaptive QML widget on the Omarchy Quickshell bar,
immediately after Workspaces by default. It preserves existing td status placement
and options when upgrading a command widget. install.sh runs this step when an
Omarchy shell.json exists; headless installs skip it.
The corresponding bar.layout.left entry in ~/.config/omarchy/shell.json is:
{
"id": "td.status",
"type": "qml",
"source": "/home/your-user/.config/omarchy/bar/modules/TdStatus.qml",
"exec": "td status --waybar",
"interval": 1,
"onClick": "td attach --omarchy",
"keepSpace": false
}It updates once per second and hides when no td windows are attached. The widget measures the space between itself and neighboring center/right widgets separately on every monitor, using logical dimensions that account for display scaling. It truncates the entire status strip with an ellipsis only when necessary; existing per-name formatting and tooltip content stay unchanged. The external screen can show a longer strip than the laptop screen without covering other bar components.
Clicking the indicator opens the native attachment picker. The CLI output remains
compatible with Waybar's custom JSON modules on systems using Waybar. Run
bash ./install-bar.sh to install only the adaptive Omarchy widget.
td diff keeps its standalone TUI in diff.ts; its window attaches to a
Herdr-owned terminal. Run td diff --inline to use the current terminal.
The comparison includes staged, unstaged, and untracked worktree changes. During an unfinished merge, it accounts for the pending merge parents so incoming base-branch changes do not appear as branch changes. Commit lists remain relative to the target branch. File and commit lists render only their visible rows to keep large comparisons responsive.
Normal-mode keys: c, f, d toggle commit/file/diff panes; h/l change focus;
j/k navigate; / starts an fzf query; q exits. Enter expands folders, Tab
selects files or commits. Alt-A/M/D/R/C/T/U filters change status; Alt-X clears
the query. Queries combine status and path terms, e.g. ^M test. Ctrl-C copies
selected text or clears the current selection. Query mode uses Enter to apply
and Esc to restore the previous query.
Foot runs herdr terminal attach directly. Clicks and scrolling use Herdr and the
application's normal mouse handling, with no interaction proxy or custom wheel
bindings. Hold Shift while dragging to select text in Foot, then Super+C
uses Omarchy universal copy through Foot's Ctrl+Insert binding. The installed window
rule tags td attachments as terminals so Super+C does not send Ctrl+C to the agent.
Super+V uses Shift+Insert. For automatic selection copying, set
selection-target=both under [main] in your Foot config.
td browser [url] opens a new Chromium window with a loopback CDP endpoint. It
works on ordinary desktops as well as td workspaces. It uses the custom class
from Playwright's configured --class argument (default playwright-chromium),
preserving its Hyprland floating geometry and no-focus behavior. The browser is
placed on the workspace where td browser was invoked, overriding any fixed
workspace assignment in the class rule. In td, hide/show parks and restores it to
that project's workspace. Desktop shortcuts follow the active workspace; launches
from a managed agent terminal follow that terminal's project even if another
desktop is focused. Multiple windows reuse one browser
process and profile; window tracking uses individual window addresses so hiding
one project doesn't move another project's browser windows.
The profile defaults to ~/.cache/ms-playwright-mcp/main-profile, reusing saved
logins with Chromium's --password-store=basic. The existing profile and config
directory names are launcher conventions; a Playwright MCP server is not required.
Agents automate the visible td browser directly with Playwright over CDP.
Discover and connect to the live browser:
td browser https://example.com
td browser --endpointUse playwright-core or an existing playwright installation in a Node.js script:
import { chromium } from "playwright-core";
const browser = await chromium.connectOverCDP(cdpEndpoint);
const context = browser.contexts()[0];
// Identify the intended page by URL/title before interacting with it.
const pages = context.pages();
// When finished, disconnect this CDP client while leaving Chromium running.
await browser.close();The endpoint is discovered from the profile's DevToolsActivePort and verified
against the live browser ID. Chromium picks a free port instead of assuming 9222;
--endpoint only reports a running connection and doesn't open another window.
Discovery is profile-based: one endpoint may expose windows from multiple td
workspaces. It does not automatically select the calling workspace's existing
page. Headed/headless mode and other process-level launch options require a new
browser process to take effect; subsequent windows reuse the running profile.
Configuration:
TD_BROWSER_PROFILE, thenPLAYWRIGHT_MAIN_PROFILE, override the shared profile.- If neither is set, a configured
browser.userDataDirin the Playwright MCP config takes precedence over the default main-profile path. TD_BROWSER_EXECUTABLEoverrides Chromium; otherwise td uses the Playwright config'sbrowser.launchOptions.executablePath, then Chromium on PATH.TD_BROWSER_CLASSoverrides the configured custom window class.TD_PLAYWRIGHT_CONFIGoverrides~/.config/playwright-mcp/config.json.
If the shared profile is already open without CDP, td reports that conflict. Close
that browser and reopen it through td to enable CDP. Browser shortcuts try td first
and fall back to omarchy launch browser when the td launch fails. They do not
change the system default browser.
td uses the calling Herdr socket/session when present, otherwise the default
server. A new server is started headlessly if needed by td. To select a named
session explicitly:
TD_HERDR_SESSION=dev td --background
TD_HERDR_SESSION=dev td list
TD_HERDR_SESSION=dev td show /path/to/project
herdr session attach devExisting Herdr clients can access td's terminals, locally or remotely. On a remote
machine with td installed, td --background needs no Hyprland. Use Herdr's SSH
client or saved-machine CLI to access and control that server from elsewhere.
The GUI browser and desktop watcher always belong to the local graphical session.
Current boundary: individual remote terminals presented as local Foot windows
are not implemented. Herdr 0.9.3's --machine forwarding does not include direct
interactive terminal attachment. Remote access uses Herdr's existing client; local
td windows use its direct terminal attachment. td does not force controller
takeover when another client owns a terminal.
td works without a plugin or a Herdr fork. The included manifest adds show, hide, and diff actions to Herdr:
herdr plugin link /path/to/td
herdr plugin action invoke leowio.td.showDesktop actions require a local Herdr server with access to the graphical session. Installing this plugin on a remote server does not grant it access to your laptop's Hyprland compositor. The watcher is a local td process, not a Herdr startup hook.
git clone https://github.com/leowio/td.git
cd td
bun install
./install.shCore requirements: Herdr 0.9.3+, Bun, Git, jq, and flock.
Desktop presentation additionally requires Hyprland with Lua dispatchers and
dwindle, and Foot. fzf powers workspace selection and diff queries.
Omarchy's built-in menu powers td focus --omarchy; it requires no Walker package.
walker remains an optional alternative for td focus --walker.
bat and sem-cli optionally enhance diff rendering.
The installer links only td into ~/.local/bin and removes obsolete helper
symlinks belonging to this checkout. Implementation helpers are private files under
runtime/helpers/; they are not installed as separate commands.
It also runs install-keybindings.sh to install the desktop shortcuts below when
an existing Hyprland Lua configuration is present. Headless installs skip that step.
When Omarchy's shell configuration exists, it also installs the adaptive status
widget through install-bar.sh.
It does not install Herdr integrations or agent skills.
config/hypr/td.lua is installed as ~/.config/hypr/td.lua (respecting
XDG_CONFIG_HOME). The dedicated installer adds require("hypr.td") at the end
of hyprland.lua, after your personal bindings. Existing files are backed up before
changes, and reinstalling does not duplicate the loader. With an active Hyprland
session, installation reloads the config and checks for configuration errors.
| Shortcut | Try first | On failure |
|---|---|---|
| Super+Alt+Enter | td attach --omarchy (native workspace picker; creates one if none exist) |
— |
| Super+Alt+W | td close (close focused window and backing pane) |
Normal window close for non-td windows |
| Super+Alt+Shift+W | td hide (detach the current td workspace; keep processes running) |
— |
| Super+Shift+D | td reset-layout (organize the current td workspace; replaces Docker) |
— |
| Super+Alt+1 | td agent opencode |
omarchy launch terminal opencode |
| Super+Alt+2 | td agent codex |
omarchy launch terminal codex |
| Super+Enter | td terminal |
omarchy launch terminal |
| Super+Shift+Enter / Super+Shift+B | td browser (Chromium + Playwright profile + CDP) |
omarchy launch browser |
| Super+Shift+N | td editor |
omarchy launch editor <active-directory> |
| Super+Alt+Backslash | td diff |
omarchy launch terminal td diff --inline |
Application bindings use the dedicated dispatcher, td dispatch <tool> [args...], which
tries the td command first and invokes the Omarchy fallback only on a nonzero exit.
Desktop dispatch follows the focused desktop, ignoring inherited Herdr caller IDs
and the launcher's cwd so shortcuts on ordinary workspaces open ordinary windows.
Argument boundaries are preserved. Diff uses the same viewer in a normal terminal
when no td workspace is available. Browser launches Chromium with CDP even outside
td; Omarchy remains the fallback on launch failure, and successful launches are
never repeated by the dispatcher.
Super+Alt+Enter directly opens the native Omarchy attachment menu, replacing the
standalone Herdr launcher. It works from an ordinary desktop workspace too, and
creates a td workspace when Herdr has no workspaces yet.
The agent shortcuts replace the previous Alt+1/2/3 OpenCode/Claude/Codex trio with Alt+1 OpenCode and Alt+2 Codex. There is no Claude shortcut or duplicate Alt+3 Codex shortcut. The module also unbinds Omarchy's group-window selectors for Alt+1/2 to avoid dispatching two actions. Super+Shift+1/2 retain their move-window bindings.
Run bash ./install-keybindings.sh to install just the keybindings. Edit the
repository's config/hypr/td.lua to customize them, then validate with
hyprctl reload && hyprctl configerrors.
Update keybindings, Waybar commands, and automation to use the unified entrypoint:
| Previous command | Replacement |
|---|---|
td-agent |
td agent |
td-terminal |
td terminal |
td-editor |
td editor |
td-spawn |
td spawn |
td-agent-fork |
td fork |
td-opencode-fork <id> |
td fork --agent opencode --session <id> |
td-browser |
td browser |
td-diff |
td diff |
td-pick / td pick |
td focus (fzf) or td focus --omarchy |
td-ai-overview |
td overview |
td-ai-status |
td status --waybar |
td-reset-layout |
td reset-layout |
td-cwd |
td cwd |
td-cleanup |
td cleanup |
td-layout <name> <directory> ... |
Run td create ... from that directory |
The former runtime, window-state, and Hyprland-dispatch helpers are internal implementation details. Call the public commands above instead.
runtime/herdr.ts handles Herdr's JSON socket API. workspace.ts manages workspace
and terminal lifecycle. Each new terminal gets an independent backing tab—an
implementation detail, not a td UI concept. desktop.ts attaches views and handles
presentation; cli.ts handles runtime commands. scripts/td is the sole public
entrypoint and dispatches desktop operations to private shell helpers in
runtime/helpers/.
State is scoped by Herdr socket identity. Durable project/role mappings live under
$XDG_STATE_HOME/td (default ~/.local/state/td); presentation state and watcher logs
live under $XDG_RUNTIME_DIR/td. TD_STATE_DIR and TD_RUNTIME_DIR override those
roots. A process-scoped flock serializes mutations. The watcher reconciles live
Herdr snapshots once per second, so missed events don't leave permanent stale state.
bunx tsc --noEmit
bun test runtime
# Optional: opens temporary Foot windows and restores original focus afterward
TD_TEST_DESKTOP=1 bun test runtimeIntegration tests use a separate temporary Herdr config/session and plain shell commands, never paid agent calls or existing user workspaces.