Sessions
Every Claude Code, Codex and Copilot session, and which one needs you
palupdated 2026-09-29DeveloperIn pal's registryMenu bar: Sessions1 deep link
In pal's registry: the button shows it in pal, which asks before installing; the command does the same from a terminal. This page can't see what your pal has installed.




What it does
Every Claude Code, Codex and Copilot CLI session on this machine and what each is doing: working, your turn, waiting on you, or ended. Enter brings the session's terminal to the front.
- One row per session, sorted by who needs you: waiting on you, your turn, working, ended
- Spots a session stuck on a permission prompt; a hook can report the exact state
- The last prompt, the last reply, the pending command, the model, folder, branch and token count
enterjumps to the session's terminal (tmux, kitty, iTerm2, Terminal, WezTerm); an ended one resumescmd+treads the transcript live in the panel; send a line to a tmux session withcmd+enter- A bar item counts sessions by state and raises an alarm while one waits on you
Palettes 1
-
Sessions
sessionsliveEvery session by state: waiting on you, your turn, working, ended. The pane shows the last exchange and, for a blocked one, the tool call left hanging.
- ↵Focus the session's terminal (an ended one: resume it in a new terminal)
- ⌘↵Send the line typed in the bar to a session in a tmux pane
- ⌘TThe transcript as a view: the last entries as a chat, [ for older, c copies the reply, s sends a line over tmux
- ⌘OOpen the transcript in the editor
- ⌘⇧OReveal the transcript
- ⌘EOpen the folder in the editor
- ⌘CCopy the resume command
- ⌘⇧CCopy the session id
- ⌘DKill the process (asks first; works on marked rows)
Actions
- Focus the terminalThe window the session runs in, in front; a tmux pane is selected on the way.↵
- Resume in a terminalOn an ended session: a terminal opens in its folder running the resume command.↵
- Send the lineOn a session in a tmux pane: the bar's text typed into it and sent.⌘↵
- TranscriptThe conversation as a view inside pal, live while the session runs.⌘T
- Open transcript in the editorThe transcript file in the editor named in Settings, else the OS's text editor.⌘O
- Open in editorThe session's folder in the editor named in Settings, Finder when it is not on PATH.⌘E
- Copy resume commandThe CLI's own resume line for this session.⌘C
- KillSIGTERM to the session's process, after a confirm; the transcript stays.⌘D
Links 1
Sessions answers these pal:// routes from a web page, a script or a keybind; pal call is the same route from a terminal. A link from a page asks first unless you trust the extension. How links work.
-
Record a session's state from a hook, for ten minutes, over what the files say
statepal call sessions/state agent=<agent> session=<session> state=<state>agent*claude, codex or copilotsession*The session id the CLI gave the hookstate*working, waiting (your turn) or blocked (a prompt is up)
Settings
Extension, [extensions.sessions]
agentsWhich agents' files are read: claude, codex, copilot.
stale_minutesA session with no process behind it stays listed as ended this long after its last write, so a crash is seen; older ones are only under the Recent filter.
recent_hoursHow far back the Recent filter lists sessions to resume.
terminalWhere Focus looks for the session's window and what Resume opens. Auto tries a tmux pane, then kitty, iTerm2, Terminal, WezTerm, then raises the app the process runs under by its pid; a named one tries tmux then that alone; tmux only stops after the pane. One of auto, kitty, iterm, terminal, wezterm, tmux.
editorThe command Open in editor and Open transcript run (found on PATH, then in Homebrew's and ~/.local/bin); the OS opener when it is not found.
Defaults shown. Change them in pal's settings window or in the config file.
The README
Everything Sessions does, key by key, from the extension's own README.md
Sessions
Every AI coding session on this machine and what each is doing: Claude Code, Codex and Copilot CLI, read from the files the three CLIs keep for themselves, with the process table saying which of them is still there. Nothing is asked of the agents and nothing is installed into them; a hook line per CLI (below) is optional and makes the state exact.
Sessions (sessions) is one row per session under a header per state,
in the order that matters: Waiting on you? (red), Your turn
(amber), Working (blue), Ended (grey). The row is the session's
title (Claude's own ai-title, Codex's first prompt, Copilot's session
name), the subtitle the agent, the folder and the branch; on the right
the state tag, the model, how long the state has held, and the tty or the
tmux pane. Filters: All, Claude, Codex, Copilot, and Recent, every
session written in the last recent_hours whether or not anything runs
it, which makes the palette a way to resume one. A live palette, primary
at the root: type the folder, the branch, the agent or the first letters
of the id.
The pane shows the last exchange (your last prompt, the assistant's last text) and, for a session waiting on you, the tool call left hanging with its command or path; under it the agent and version, the model, the folder and branch, when it started, the last write, the number of turns and how long the last one took, the tokens the file reports, the permission mode, the process, the session id and the transcript path.
Transcript (cmd+t, t in the popover) opens the conversation as a
view without leaving pal: a header (the agent's mark, the title, the
state with its age, folder, branch, model, turns, tokens), then the last
15 entries oldest to newest as a chat. A prompt is a sunken block with a
rail in the accent colour, the assistant's text plain paragraphs (cut at
1200 characters with a line counting the rest), a tool call one compact
row (its name, the command or path, a dot: green done, red failed, grey
still running) lit when the session is waiting on that call, a stretch of
thinking one muted line ("thought for 12 s"). While the level is shown
the file is watched and every write redraws it, so a running session
streams into the view. The fold keeps the last 60 entries of a file, not
the whole of it; older ones are not shown.
| key | what |
|---|---|
[ |
15 older entries |
f |
Focus the terminal |
c |
Copy the last reply |
o |
Open the transcript in the editor |
s |
Send a line (a tmux-backed session): the search row becomes a field, Enter types the line into the pane |
r |
Refresh |
up / down |
Scroll |
escape |
Back |
Marked sessions resume (the ended ones), open or reveal their transcripts, open their folders, copy their resume commands, ids or folders one per line, or take SIGTERM together; the popover marks its rows for the same.
Where the files are
| agent | files | resume |
|---|---|---|
| Claude Code | ~/.claude/projects/<cwd slug>/<id>.jsonl, one line per entry (user, assistant, system with turn_duration, ai-title, permission-mode) |
claude --resume <id> |
| Codex | ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<id>.jsonl (session_meta, event_msg with task_started / task_complete, response_item messages and tool calls) |
codex resume <id> |
| Copilot CLI | ~/.copilot/session-state/<id>/events.jsonl with workspace.yaml beside it, and ~/.copilot/open-sessions-state.json |
copilot --resume=<id> |
Each file is folded once and then from the byte it was last read to,
so a 50 MB transcript costs one read; a directory the listed files sit in
is watched, and a write there re-renders the bar within half a second.
Files older than recent_hours are not read at all.
How a state is derived
- Live or not.
pslists theclaude,codexandcopilotprocesses (matched on the command's basename, soclaude-stateandcodex mcp-serverare not sessions) andlsof(Linux:/proc) their working directories. A Copilot session is paired to its process by theinuse.<pid>.lockin its directory; a Claude or Codex file is paired to a process of that agent on the same directory, the newest file to the newest process. Two sessions in one directory both list, and which pid each gets is a guess. - Working: the file's last turn has not ended. Claude: a user prompt
after the last
turn_duration; Codex:task_startedafter the lasttask_complete; Copilot:working: trueinopen-sessions-state.jsonorassistant.turn_startafter the lastturn_end. A working file not written forstale_minutesis an interrupted turn (Escape leaves no mark) and reads as your turn. - Waiting on you?: working, the assistant's last tool call has no
result after 20 s, and the process used no cpu across two
psreads 2 s apart. That is what a permission prompt looks like from outside, and it is a guess: a long-running command with idle cpu (asleep, a network wait) looks the same. The question mark stays unless a hook said so. - Your turn: the turn ended and nothing came since.
- Ended: no process runs on the directory and the file was written
within
stale_minutes, so a crash or a closed window is seen for a while; older than that the session is only under Recent.
A session whose work runs in a subagent reads as your turn once the parent's own turn ended: the parent is, in fact, waiting.
Exact states from a hook
pal://sessions/state?agent=<claude|codex|copilot>&session=<id>&state=<working|waiting|blocked>
records a state for ten minutes over anything the files say (pal call sessions/state agent=claude session=<id> state=working from a shell).
Claude Code hands every hook the session id as JSON on stdin, so a few
lines in ~/.claude/settings.json cover it:
| hook | command |
|---|---|
UserPromptSubmit |
pal call sessions/state agent=claude state=working session=$(jq -r .session_id) |
Stop |
pal call sessions/state agent=claude state=waiting session=$(jq -r .session_id) |
Notification |
pal call sessions/state agent=claude state=blocked session=$(jq -r .session_id) |
PostToolUse |
pal call sessions/state agent=claude state=working session=$(jq -r .session_id) (the prompt was answered; back to work) |
The same lines with agent=codex or agent=copilot serve a Codex
or Copilot hook that gives the session id the same way. Without hooks the
files decide, as above; with them the tag reads waiting on you with no
question mark and the pane says "from a hook".
Focus: which terminals
Enter on a live row brings its window in front, by a ladder of rungs each tried in turn until one answers; a rung whose tool is missing or whose socket does not answer is skipped with a line in pal's log, and the toast ("Could not find its window") comes only when every rung failed:
- tmux:
tmux list-panes -afinds the pane on the process's tty (or whose shell is an ancestor of it);switch-client,select-windowandselect-paneselect it inside its server, and the client attached to that session gives the tty the next rungs look for. A pane with no client attached gets a new terminal window runningtmux attach. - kitty:
kitten @ lslists windows with the pid of their shell; the one that is an ancestor of the process is focused withkitten @ focus-window, then the app is brought in front (focus-window alone does not raise kitty over another app). kitty is optional and never assumed; it needsallow_remote_control yesand alisten_on unix:/tmp/<name>inkitty.conf, and pal finds the socket from that line: kitty appends its pid to the path, so<path>-<pid>is tried for every running kitty, then<path>itself (KITTY_LISTEN_ONfirst when pal itself runs inside kitty). The socket that answered is kept until it stops answering. - iTerm2 and Terminal: an AppleScript walks the tabs for the one whose tty matches and selects it; only when the app is running, since the script would launch it otherwise.
- WezTerm:
wezterm cli listnames each pane's tty; the matching pane is activated withwezterm cli activate-paneand the app raised. Only while WezTerm runs. - By pid: the nearest ancestor of the process that is a GUI app (its
command inside a
.appbundle) is brought to the front through System Events by its pid. For a terminal with one window per process (Alacritty) that is exactly the window; for one process with many windows (Ghostty, Warp, VS Code's terminal) it raises the app with its last window, which is the most macOS offers without an API of the app's own. - The app: the
.appbundle is activated withopen, the last resort when no ancestor could be raised.
The terminal setting narrows this: tmux only stops after the pane,
kitty, iTerm2, Terminal and WezTerm try tmux then that app alone,
Auto runs the whole ladder. The same setting says what Resume in a
terminal opens for an ended session: a new window in that app running
the resume command in the session's folder, or a new tmux window.
The host runs under launchd's PATH (/usr/bin:/bin:/usr/sbin:/sbin), so
tmux, kitten, wezterm, the editor and the agents' own CLIs are
looked for on PATH and then in /opt/homebrew/bin, /usr/local/bin,
~/.local/bin, kitty's and WezTerm's app bundles.
Keyboard
| keys | action | what |
|---|---|---|
enter |
Focus the terminal | the ladder above; on an ended row, Resume in a terminal |
cmd+enter |
Send the line | a tmux-backed session only: the bar's text argument typed into the pane with send-keys -l, then Enter |
cmd+t |
Transcript | the conversation as a view, above |
cmd+o |
Open transcript in the editor | the file in the editor command when it is found, else open -t (TextEdit); a refusal is a toast with the reason |
cmd+shift+o |
Reveal transcript | in Finder (the file manager on Linux) |
| Open folder | the session's folder | |
cmd+e |
Open in editor | <editor> <folder> when the editor is on PATH, else the folder opens |
cmd+c |
Copy resume command | claude --resume <id>, codex resume <id>, copilot --resume=<id> |
cmd+shift+c |
Copy session id | |
| Copy folder path | ||
cmd+d |
Kill | kill -TERM <pid> after a confirm; marked rows go together |
The bar item
sessions shows the count as its title with a segment per state: red
!N waiting on you, amber ·N your turn, blue …N working; an alarm
while anything waits on you; hidden with no session at all (Settings can
keep it as a muted glyph). The core asks every 10 s and on show and wake;
the file watcher pushes sooner. The popover is a row per session under
the same headers, every session (the popover scrolls), the ring on the
row the keys act on (a click moves it):
| key | what |
|---|---|
enter |
Focus the terminal (an ended row: resume it) |
t |
The transcript as a view, over the list |
o |
Open the transcript in the editor |
r |
Copy the resume command |
x |
Kill (asks first) |
s |
Open the palette on a tmux session's row, to type a line into it |
p |
Open the Sessions palette |
up / down, j / k |
Move the ring |
Settings
| id | default | what |
|---|---|---|
agents |
claude, codex, copilot |
which agents' files are read |
stale_minutes |
30 | how long an ended session stays listed, and how long a silent turn counts as working |
recent_hours |
24 | how far back Recent lists, and how old a file may be to be read at all |
terminal |
Auto | the Focus ladder and what Resume opens: Auto, kitty, iTerm2, Terminal, WezTerm, tmux only |
editor |
code |
the command Open in editor and Open transcript run; the OS opener when it is not found |




