diff --git a/docs/shell/scripts.mdx b/docs/shell/scripts.mdx
index af9dd75..5f3c91a 100644
--- a/docs/shell/scripts.mdx
+++ b/docs/shell/scripts.mdx
@@ -383,7 +383,7 @@ The project root is the git toplevel, else the pane's directory.
**Run.** One-off items open a full-width split along the bottom of the window (`split-window -v -f -l 25%`), 25% of its height, in the task's directory. The command is passed to `split-window` as separate arguments, so no shell reparses it. `--run` calls `pj --run`, which keeps pj's `openlogs` wrapping of `dev`/`start`/`serve`/`watch` tasks, and `--cmd` runs a quick action. While the command runs, ctrl-c stops only the command (the script traps `SIGINT`), so the output stays. Once the command exits, the script prints `[exit N] enter or ctrl-c to close`, and enter or ctrl-c closes the pane; other keys are ignored.
-**Background.** Long-running items, marked `· background`, are ` run dev`, `docker compose up`, and pj tasks named `dev`/`start`/`serve`/`watch`/`preview` or with one of those as a `:` segment (`dev:web`, `api:dev`). They run through `--bg` in a detached session named `:` (`leetcode:bun-run-dev`, `leetcode:docker-compose-up`), shown in a rounded 90%×80% popup attached to it. q detaches the popup and leaves the task running: the session's `key-table` is `pj-task`, which binds only `q`, so other keys reach the task instead of root-table bindings (prefix bindings still work). ctrl-c stops the task and ends the session, closing the popup. If the task exits on its own, the `[exit N]` prompt keeps its output until enter. Picking the item again, or its **Running** entry, reopens the popup instead of starting a second copy. The session has `status off`, `detach-on-destroy on`, and `@pj_task` set to the task's root, which is how the Running group finds it. Session names contain `:`, so the script targets them by id (`$N`).
+**Background.** Long-running items, marked `· background`, are ` run dev`, `docker compose up`, and pj tasks named `dev`/`start`/`serve`/`watch`/`preview` or with one of those as a `:` segment (`dev:web`, `api:dev`). They run through `--bg` in a detached session named `:` (`leetcode:bun-run-dev`, `leetcode:docker-compose-up`), shown in a rounded 90%×80% popup attached to it. q detaches the popup and leaves the task running: the session's `key-table` is `pj-task`, which binds only `q` and ctrl-c, so other keys reach the task instead of root-table bindings (prefix bindings still work). ctrl-c runs `confirm-before` (`Stop ? (y/n)`); y sends ctrl-c to the task, which stops it and ends the session, closing the popup, and any other key cancels. If the task exits on its own, the `[exit N]` prompt keeps its output until enter. Picking the item again, or its **Running** entry, reopens the popup instead of starting a second copy. The session has `status off`, `detach-on-destroy on`, and `@pj_task` set to the task's root, which is how the Running group finds it. Session names contain `:`, so the script targets them by id (`$N`).
**omp.** tmux `prefix a` (Ghostty ctrl+shift+a) runs `pj-palette --bg :omp omp`, so omp gets the same kind of background session and Running entry. Its popup docks in the bottom-right corner of the client, 35% wide and 60% tall. omp runs directly (no `[exit N]` prompt) and needs q, so its session uses the `pj-app` key table instead: ctrl-c hides the popup, ctrl-q kills the session (quits omp). `prefix a` inside any session with `@pj_task` also hides, which makes ctrl+shift+a a toggle. The popup border shows only the session name.
diff --git a/docs/terminal/ghostty.mdx b/docs/terminal/ghostty.mdx
index 6a3c202..629e362 100644
--- a/docs/terminal/ghostty.mdx
+++ b/docs/terminal/ghostty.mdx
@@ -37,7 +37,7 @@ Ghostty reads both `config` and `config.ghostty`. `config` holds all settings; `
Every Ghostty window gets its own tmux session; no two windows mirror the same one. `~/.local/bin/ghostty-tmux` (source `dot_local/bin/executable_ghostty-tmux`) runs for each new window:
-1. If a detached session exists (left by a closed window, or restored by continuum after a reboot), it attaches to the oldest one.
+1. If a detached session exists (left by a closed window, or restored by continuum after a reboot), it attaches to the oldest one, by session id. Background task sessions from `pj-palette` (`@pj_task` set, named `:`) are never reclaimed.
2. Otherwise it starts a new session.
Before starting tmux, the script puts mise's shims on `PATH`. A server started from a desktop-launched Ghostty therefore finds the tools tmux plugins shell out to: bun for the palette, fzf, and zoxide.
diff --git a/docs/terminal/tmux.mdx b/docs/terminal/tmux.mdx
index d8a258e..2dd82e5 100644
--- a/docs/terminal/tmux.mdx
+++ b/docs/terminal/tmux.mdx
@@ -42,7 +42,7 @@ These settings are what [yazi](/tools/yazi) needs inside tmux. `allow-passthroug
| `prefix q` | Reload config |
| `prefix /` | Copy mode, then search upward |
| `prefix o` | tmux-sessionx: fuzzy-find, switch to, create, rename (`ctrl-r`), or kill (`alt-backspace`) sessions, with a preview |
-| `prefix t` | Project Tasks palette ([`pj-palette`](/shell/scripts#pj-palette)): install/dev/sync/make/compose quick actions plus every `pj` task. One-off tasks run in a full-width bottom split (25% of the window); dev servers and `docker compose up` run in a background `:` session shown in a popup (`q` hides, `ctrl-c` stops), listed under Running to reopen |
+| `prefix t` | Project Tasks palette ([`pj-palette`](/shell/scripts#pj-palette)): install/dev/sync/make/compose quick actions plus every `pj` task. One-off tasks run in a full-width bottom split (25% of the window); dev servers and `docker compose up` run in a background `:` session shown in a popup (`q` hides, `ctrl-c` asks `y/n` then stops), listed under Running to reopen |
| `prefix y` / `prefix e` | Yazi / Neovim in a new window in the pane cwd (Ghostty `ctrl+shift+f` / `ctrl+shift+e`) |
| `prefix g` | Lazygit in a 90% popup in the pane cwd (Ghostty `ctrl+shift+g`) |
| `prefix a` | omp in a background session `:omp` (pane cwd), shown in a popup docked bottom-right (35% × 60%) and listed in the [omp palette](/shell/scripts#pj-palette) and under Running in the tasks palette. In the popup, `ctrl-c` hides it and `ctrl-q` quits omp. Inside any background-task popup, `prefix a` hides it too, so Ghostty `ctrl+shift+a` toggles omp |
@@ -97,7 +97,7 @@ Labels longer than 27 characters end in `…`. The icon comes from tmux-nerd-fon
| File | Contents |
| --- | --- |
| `commands.json` | The kitty launchers ported as palette items: **Lazygit** (90% popup in the pane's cwd; kitty `ctrl+shift+g`), **Yazi** (new window; kitty `ctrl+shift+f`), **Neovim Split** (nvim in a split to the left, 60% of the pane; alias `nv`; kitty `ctrl+shift+e`), **Shell History (atuin)** (`atuin-pick` in a 25% split below; kitty `ctrl+shift+p`). Also **btop** (80% popup; aliases `top`, `htop`), **Lazydocker** (90% popup in the pane's cwd; alias `dd`), **mandible** (80% popup running [`mandible-pick`](/shell/scripts#mandible-pick); alias `man`), **television** (`tv channels`, the [channel picker](/tools/television), in a new window in the pane's cwd; alias `tv`), **gitframe** (git diff viewer in a new window in the pane's cwd; alias `gf`), **omp** (opens the omp palette; aliases `agent`, `ai`), **Project Tasks** (opens the tasks palette), **Sessions (sessionx)**, **Save Sessions**, and **Restore Sessions** (resurrect). Programs are started with `exec`, so the tab icon is the program's, not the shell's |
-| `palettes/tasks.json` | The **Project Tasks** palette (`prefix t`, Ghostty `ctrl+shift+p` / `ctrl+shift+space`), fed by [`pj-palette`](/shell/scripts#pj-palette): running background tasks under Running, then `bun install`, `bun run dev`, `uv sync`, `uv run python`, `make`, `go run .`, `cargo run`, `docker compose up` as Quick items when the project has them, then every task `pj` finds, grouped by tool. One-off tasks run in a full-width split along the bottom (25% of the window) in their directory; `ctrl-c` stops a running task, and once it has exited, `enter` or `ctrl-c` closes the pane. Dev servers (`dev`/`start`/`serve`/`watch`/`preview` tasks) and `docker compose up` run in a background session `:` shown in a popup: `q` hides it with the task still running, `ctrl-c` stops the task and closes it |
+| `palettes/tasks.json` | The **Project Tasks** palette (`prefix t`, Ghostty `ctrl+shift+p` / `ctrl+shift+space`), fed by [`pj-palette`](/shell/scripts#pj-palette): running background tasks under Running, then `bun install`, `bun run dev`, `uv sync`, `uv run python`, `make`, `go run .`, `cargo run`, `docker compose up` as Quick items when the project has them, then every task `pj` finds, grouped by tool. One-off tasks run in a full-width split along the bottom (25% of the window) in their directory; `ctrl-c` stops a running task, and once it has exited, `enter` or `ctrl-c` closes the pane. Dev servers (`dev`/`start`/`serve`/`watch`/`preview` tasks) and `docker compose up` run in a background session `:` shown in a popup: `q` hides it with the task still running, `ctrl-c` asks `Stop ? (y/n)` and on `y` stops the task and closes it |
| `palettes/omp.json` | The **omp** palette, fed by [`pj-palette --omp`](/shell/scripts#pj-palette): this tmux session's `:omp` (open it, or start it when not running), every other tmux session's running omp, and a Quit item per session. Running agents have a green icon and show `open` or `hidden` |
| `hidden.json` | Hides the built-in **Switch Theme...** item, and with it the Appearance section: the palette stays on the fixed cyberdream `theme.json` |
| `shortcuts.json` | Ghostty `ctrl+shift` chords shown next to the built-in items they trigger (New Window → `Ctrl+Shift+T`, …) |
diff --git a/dot_local/bin/executable_ghostty-tmux b/dot_local/bin/executable_ghostty-tmux
index 0881475..9169c71 100755
--- a/dot_local/bin/executable_ghostty-tmux
+++ b/dot_local/bin/executable_ghostty-tmux
@@ -2,8 +2,11 @@
# Ghostty's `command`: every Ghostty window gets its own tmux session, never
# a mirror of another window's. Reclaim the oldest detached session (left by
# a closed window, or restored by tmux-continuum after a reboot) so closed
-# windows don't pile up sessions; otherwise start a new one. The first window
-# after boot starts the server, which is what triggers continuum's restore.
+# windows don't pile up sessions; otherwise start a new one. pj-palette's
+# background task sessions (@pj_task, named :) are never
+# reclaimed, and targets use the session id since names may contain ':'.
+# The first window after boot starts the server, which is what triggers
+# continuum's restore.
# ponytail: two windows opened in the same instant can claim the same detached
# session; take a lock (flock) here if that ever bites.
@@ -12,8 +15,8 @@
# (bun for tmux-palette, fzf, zoxide).
[[ -d $HOME/.local/share/mise/shims ]] && PATH="$HOME/.local/share/mise/shims:$PATH"
-oldest=$(tmux list-sessions -f '#{==:#{session_attached},0}' \
- -F '#{session_created} #{session_name}' 2>/dev/null | sort -n | head -1 | cut -d' ' -f2-)
+oldest=$(tmux list-sessions -f '#{&&:#{==:#{session_attached},0},#{!:#{@pj_task}}}' \
+ -F '#{session_created} #{session_id}' 2>/dev/null | sort -n | head -1 | cut -d' ' -f2)
-[[ -n $oldest ]] && exec tmux attach-session -t "=$oldest"
+[[ -n $oldest ]] && exec tmux attach-session -t "$oldest"
exec tmux new-session
diff --git a/dot_local/bin/executable_pj-palette b/dot_local/bin/executable_pj-palette
index 045989d..11f08fe 100755
--- a/dot_local/bin/executable_pj-palette
+++ b/dot_local/bin/executable_pj-palette
@@ -25,10 +25,10 @@
# Long-running items (dev/start/serve/watch/preview tasks, compose up) run in a
# detached session named : (leetcode:bun-run-dev),
# shown in a popup: q hides the popup and leaves the task running, ctrl-c
-# stops the task and ends its session. Running ones are listed first, under
-# Running, to bring them back. tmux.conf's prefix + a starts omp the same way
-# (:omp); omp needs q, so its popup hides with ctrl-c (or prefix + a)
-# and ctrl-q quits it.
+# asks for confirmation, then stops the task and ends its session. Running
+# ones are listed first, under Running, to bring them back. tmux.conf's
+# prefix + a starts omp the same way (:omp); omp needs q, so its
+# popup hides with ctrl-c (or prefix + a) and ctrl-q quits it.
set -uo pipefail
self=$(readlink -f "$0")
@@ -86,10 +86,11 @@ case ${1-} in
set -t "$id" detach-on-destroy on
# Each session gets a key table for its hide/quit keys; other keys
# reach the program. A task's output takes no input, so q hides and
- # ctrl-c stops the task. Interactive programs (omp) need q and typing,
- # so ctrl-c hides and ctrl-q kills the session.
+ # ctrl-c (after a y/n confirm) stops the task. Interactive programs
+ # (omp) need q and typing, so ctrl-c hides and ctrl-q kills the session.
if [ "$1" = "$self" ]; then
- tmux set -t "$id" key-table pj-task \; bind -T pj-task q detach-client
+ tmux set -t "$id" key-table pj-task \; bind -T pj-task q detach-client \; \
+ bind -T pj-task C-c confirm-before -p 'Stop #{session_name}? (y/n)' 'send-keys C-c'
else
tmux set -t "$id" key-table pj-app \; bind -T pj-app C-c detach-client \; \
bind -T pj-app C-q kill-session