Skip to content
leowioPublic

About

An opinionated terminal AI layout workflow for Hyprland

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

td

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.

One command: td

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 terminal

Workspace lifecycle: create → hide → attach → show

A 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.

1. Create a workspace

From the checkout you want to work on:

cd ~/code/my-project
td create

For a new workspace, td:

  1. Connects to the selected Herdr server, starting it headlessly if necessary.
  2. Creates the workspace and its editor, agent, and two shell terminals.
  3. Attaches each terminal in its own Foot window on a dedicated Hyprland workspace.
  4. 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.

2. Create directly in the background

workspace_id=$(td create --background codex | jq -r '.id')
td list
td status --json

This 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.

3. Find and target the workspace

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 server

td 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-project

Selecting 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-project

With 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.

4. Hide the desktop and leave everything running

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.

5. Attach just one running terminal

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.

6. Restore the full workspace

td show "$workspace_id"
td reset-layout                  # Optional: restore the three-column desktop layout

show 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.

7. Stop work or remove the checkout

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 remove

Use 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.

Worktrees and clones

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 directory

The short forms td -w ... and td -c ... also work. Each checkout has its own workspace lifecycle and can be shown, hidden, or stopped independently.

Add tools and agents

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 failure

Put -- 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.

Desktop tools

  • td focus: fzf workspace picker; --omarchy selects the native graphical mode and --all attaches 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 --json still 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.

Status bar

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.

Diff viewer

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 selection and universal clipboard

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.

Chromium, Playwright profile, and CDP

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 --endpoint

Use 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, then PLAYWRIGHT_MAIN_PROFILE, override the shared profile.
  • If neither is set, a configured browser.userDataDir in the Playwright MCP config takes precedence over the default main-profile path.
  • TD_BROWSER_EXECUTABLE overrides Chromium; otherwise td uses the Playwright config's browser.launchOptions.executablePath, then Chromium on PATH.
  • TD_BROWSER_CLASS overrides the configured custom window class.
  • TD_PLAYWRIGHT_CONFIG overrides ~/.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.

Herdr sessions and remote access

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 dev

Existing 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.

Optional Herdr plugin

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.show

Desktop 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.

Install

git clone https://github.com/leowio/td.git
cd td
bun install
./install.sh

Core 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.

Hyprland shortcuts and Omarchy fallback

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.

Updating existing shortcuts

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.

Implementation and checks

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 runtime

Integration tests use a separate temporary Herdr config/session and plain shell commands, never paid agent calls or existing user workspaces.

About

An opinionated terminal AI layout workflow for Hyprland

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages