tuiboard 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,13 +43,32 @@ archive_column: Archive
43
43
  # zones:
44
44
  # planner: on # Today/Tomorrow cross-board panel (F1)
45
45
  # agenda: on # 24h agenda + calendar overlay (F2)
46
- # agents: on # live Claude Code session view (F3)
46
+ # agents: on # live agent sessions view (F3)
47
47
 
48
- # Optional: override what Enter does in the Agents zone ("open the selected
49
- # Claude Code session"). An argv array — the tokens {cwd} and {sessionId} are
48
+ # Enter in the Agents zone opens the selected session in a new tab/window of
49
+ # the terminal tuiboard runs in, detected automatically: tmux, herdr, WezTerm,
50
+ # Windows Terminal, Ghostty — else the OS default (Linux: xdg-terminal-exec,
51
+ # Windows: a new PowerShell window, macOS: Terminal.app). If nothing works, the
52
+ # resume command is copied to the clipboard instead. Force one when detection
53
+ # guesses wrong (e.g. a multiplexer env var leaking into another terminal):
54
+ # resume_terminal: auto # tmux | herdr | wezterm | windows-terminal | ghostty
55
+ # # | xdg-terminal-exec | windows-console | macos-terminal
56
+ #
57
+ # The session runs in your shell, which stays open when the agent exits.
58
+ # `auto` = the shell you started tuiboard from: on Windows Git Bash, Nushell or
59
+ # PowerShell (pwsh, else powershell); elsewhere your $SHELL. Force one with:
60
+ # resume_shell: auto # bash | zsh | fish | nu | pwsh | powershell | cmd
61
+ #
62
+ # Enter not doing what you expect? From the directory you run tuiboard in (in a
63
+ # checkout of the repo): `bun run agents:open <session-id-prefix> --dry-run`
64
+ # prints the detected terminal, shell and exact command; the `o` detail of a
65
+ # session also shows the full result of its last Enter.
66
+
67
+ # Optional: replace that with your own launcher script. An argv array — the tokens {cwd}, {sessionId} and {resume}
68
+ # (the agent's own resume command, e.g. `claude --resume <sessionId>`) are
50
69
  # substituted, then it's run directly (no shell). Point it at your own script
51
- # to spawn a custom terminal layout. When omitted, tuiboard just opens a new
52
- # WezTerm tab and runs `claude --resume <sessionId>`.
70
+ # to spawn a custom terminal layout. Takes precedence over resume_terminal —
71
+ # remove it to get the automatic behavior above.
53
72
  #
54
73
  # The first element must be a DIRECTLY EXECUTABLE program — a real binary on
55
74
  # PATH or an absolute path. It is NOT run through a shell, so shell builtins
@@ -65,11 +84,12 @@ archive_column: Archive
65
84
  # Optional: the command copied to your clipboard by `c` in the Agents zone — one
66
85
  # paste that cd's into the session's directory and resumes it, for when you want
67
86
  # to open the session yourself in a new tab/pane anywhere (no WezTerm needed).
68
- # The tokens {cwd} and {sessionId} are substituted. It's a plain string, so use
69
- # whatever chaining your shell wants. Default (works in bash/zsh/pwsh/cmd):
70
- # copy_resume_command: 'cd "{cwd}" && claude --resume {sessionId}'
87
+ # The tokens {cwd}, {sessionId} and {resume} (the agent's own resume command,
88
+ # e.g. `claude --resume <sessionId>`) are substituted. It's a plain string, so
89
+ # use whatever chaining your shell wants. Default (works in bash/zsh/pwsh/cmd):
90
+ # copy_resume_command: 'cd "{cwd}" && {resume}'
71
91
  # Nushell users typically want `;` instead of `&&`:
72
- # copy_resume_command: 'cd "{cwd}"; claude --resume {sessionId}'
92
+ # copy_resume_command: 'cd "{cwd}"; {resume}'
73
93
 
74
94
  # Optional: overlay read-only calendar events on the Agenda (the 24h timeline).
75
95
  # Connect a provider with `tuiboard calendar-setup google` / `... microsoft`,
package/CHANGELOG.md CHANGED
@@ -5,6 +5,75 @@ All notable changes to **tuiboard** are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.12.0] - 2026-09-17
11
+
12
+ The Agents zone stops being Claude-Code-only: it now lists **Codex, OpenCode
13
+ and Pi** sessions next to Claude Code, tells them apart at a glance, and
14
+ reopens any of them in whatever terminal and shell you use (#12).
15
+
16
+ ### Added
17
+ - **Codex sessions** (#20). Read-only from Codex's rollout files under
18
+ `$CODEX_HOME` (default `~/.codex`), including archived and zstd-compressed
19
+ ones, with names from `session_index.jsonl` / the state DB; resume with
20
+ `codex resume <id>`. Subagent threads are hidden.
21
+ - **OpenCode sessions** (#17). Read-only from OpenCode's SQLite store
22
+ (`$XDG_DATA_HOME/opencode/opencode.db`); resume with
23
+ `opencode --session <id>`.
24
+ - **Pi sessions** (#33). Read-only from Pi's JSONL sessions
25
+ (`~/.pi/agent/sessions/`, honoring `PI_CODING_AGENT_DIR`,
26
+ `PI_CODING_AGENT_SESSION_DIR` and `sessionDir`); names from `/name`; resume
27
+ with `pi --session <id>`.
28
+ - Codex, OpenCode and Pi keep no record of running processes, so their
29
+ sessions are busy while a turn is open and turn stale once it stops updating
30
+ for 30 minutes; an open-but-idle TUI of theirs isn't detected.
31
+ - **Harness badge, model and harness filter** (#23). Each session shows a
32
+ colored `cc` / `cx` / `oc` / `pi` badge and its model (`opus-5`,
33
+ `gpt-5.5-codex`, …), also in the `o` detail. `f` in the Agents zone cycles
34
+ all → cc → cx → oc → pi; the active filter shows in the panel title.
35
+ - **Two-line session cards** in the zoomed / fullscreen Agents view: title and
36
+ age on top, model · branch · directory underneath. The dashboard strip stays
37
+ one line per session and fits its fields to the real row width, dropping
38
+ model, then branch, then directory before shortening the title.
39
+ - **Enter opens sessions in any common terminal, not only WezTerm** (#25).
40
+ tuiboard detects where it runs — tmux, herdr, WezTerm, Windows Terminal (new
41
+ tab), Ghostty (new window) — and otherwise uses the OS default:
42
+ `xdg-terminal-exec` on Linux, a new console window on Windows, Terminal.app
43
+ on macOS. If launching fails, the resume command is copied to the clipboard
44
+ and the banner says so. New `resume_terminal` option forces one;
45
+ `resume_command` still wins.
46
+ - **Resumed sessions run in your shell** (#31). `auto` follows the shell you
47
+ started tuiboard from — on Windows Git Bash (Git's `bin\bash.exe`, never
48
+ WSL's), Nushell or PowerShell; `$SHELL` elsewhere — and the shell stays open
49
+ after the agent exits. New `resume_shell` option (`bash | zsh | fish | nu |
50
+ pwsh | powershell | cmd`) forces one.
51
+ - **Enter diagnostics.** The `o` detail shows the full result of a session's
52
+ last Enter, and `bun run agents:open <session-id-prefix> [--dry-run]` (in a
53
+ checkout) prints the detected terminal, shell and exact launch command.
54
+
55
+ ### Changed
56
+ - **Agent CLIs plug in through a common adapter interface** (#16). Claude Code
57
+ sessions behave as before. The `stale-pid` status is now `stale` (same glyph
58
+ and color), since not every agent writes PID records.
59
+ - **New `{resume}` token** for `resume_command` and `copy_resume_command`: the
60
+ selected agent's own resume command. The `copy_resume_command` default is now
61
+ `cd "{cwd}" && {resume}`; custom templates using `claude --resume
62
+ {sessionId}` keep working unchanged.
63
+ - **The Agents zone refreshes per agent.** A change under one agent's session
64
+ store re-scans only that agent, and a session writing non-stop still
65
+ refreshes at least once a second. Session stores that don't exist yet when
66
+ tuiboard starts (agent installed later, first session) are picked up within
67
+ a few seconds instead of needing a restart.
68
+
69
+ ### Fixed
70
+ - **Enter in Windows Terminal** (#29). `wt.exe` (and a Store-installed `pwsh`)
71
+ are App Execution Aliases that Bun's spawn can't find; Windows launches now
72
+ go through PowerShell's `Start-Process`, which resolves them. Session
73
+ directories stored with `/` (OpenCode) are passed as `\`.
74
+ - **Agent directories keep `/` on macOS/Linux** — the shortened path was always
75
+ joined with `\`.
76
+
8
77
  ## [0.11.0] - 2026-09-16
9
78
 
10
79
  ### Fixed
@@ -350,6 +419,8 @@ First public release on npm. This entry captures the full feature set at launch.
350
419
 
351
420
  Built with [OpenTUI](https://opentui.com) + SolidJS on Bun.
352
421
 
422
+ [0.12.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.12.0
423
+ [0.11.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.11.0
353
424
  [0.10.0]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.10.0
354
425
  [0.9.2]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.9.2
355
426
  [0.9.1]: https://github.com/NazzarenoGiannelli/tuiboard/releases/tag/v0.9.1
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  A terminal **kanban** board on plain markdown files, with three optional panels
4
4
  you switch on or off: a **Today/Tomorrow planner** across all your boards, a
5
5
  **24-hour agenda** with a read-only Google / Microsoft 365 calendar overlay, and
6
- a **live view of your Claude Code sessions**. Run it as a pure kanban, or any mix
6
+ a **live view of your coding-agent sessions** (Claude Code, Codex, OpenCode, Pi).
7
+ Run it as a pure kanban, or any mix
7
8
  of the four. The board is always on; the rest is opt-in (see [Zones](#zones)).
8
9
 
9
10
  Built with [OpenTUI](https://opentui.com) + SolidJS on Bun. Cross-platform
@@ -11,7 +12,7 @@ Built with [OpenTUI](https://opentui.com) + SolidJS on Bun. Cross-platform
11
12
  the Obsidian Tasks-plugin emoji vocabulary, so they open and edit fine in
12
13
  any markdown editor.
13
14
 
14
- ![tuiboard — kanban board, Today/Tomorrow panel, 24h agenda with calendar overlay, and live Claude Code agents in one terminal dashboard](docs/screenshot.png)
15
+ ![tuiboard — kanban board, Today/Tomorrow panel, 24h agenda with calendar overlay, and live coding-agent sessions in one terminal dashboard](docs/screenshot.png)
15
16
 
16
17
  ## Install
17
18
 
@@ -72,10 +73,24 @@ Starting fresh — no Obsidian, no special folders required:
72
73
  already contains `.md` files with `- [ ]` tasks — it auto-discovers them.
73
74
  4. **Run it:** `tuiboard` (or the short alias `tb`).
74
75
 
75
- **The Agent view needs zero setup.** tuiboard reads your local Claude Code
76
- sessions from `~/.claude/` automatically, so the live agent strip fills in as
77
- soon as you've used Claude Code — nothing to connect or configure. (Tools that
78
- don't write to `~/.claude`, like Codex, won't show up there.)
76
+ **The Agent view needs zero setup.** tuiboard reads your local agent sessions
77
+ automatically, read-only — Claude Code from `~/.claude/`, Codex from
78
+ `~/.codex/` (respects `$CODEX_HOME`), OpenCode from
79
+ `~/.local/share/opencode/opencode.db` (respects `$XDG_DATA_HOME`), Pi from
80
+ `~/.pi/agent/sessions/` (respects `$PI_CODING_AGENT_DIR`,
81
+ `$PI_CODING_AGENT_SESSION_DIR` and `sessionDir`) — so the live agent strip
82
+ fills in as soon as you've used any of them, even if you start the agent after
83
+ tuiboard. Nothing to connect or configure. Codex, OpenCode and Pi keep no
84
+ record of running processes, so their sessions show as busy while a turn is in
85
+ progress (stale if a turn stops updating for 30 minutes), never as
86
+ idle-but-open.
87
+
88
+ Every session carries a colored two-letter harness badge — `cc` Claude Code,
89
+ `cx` Codex, `oc` OpenCode, `pi` Pi — plus the model it ran on. The dashboard strip keeps
90
+ one line per session (on a narrow row the model gives way first, then the
91
+ branch, then the directory); zoom the Agents zone (`z`) or run
92
+ `tuiboard --view=agents` for two-line cards with the model, branch and full
93
+ directory under each title. `f` in the Agents zone filters by harness.
79
94
 
80
95
  See [Configure](#configure) for assignees, the done/archive column names, and
81
96
  the optional custom "open session in your terminal" command.
@@ -100,11 +115,11 @@ https://github.com/NazzarenoGiannelli/tuiboard). Set it up for me from scratch:
100
115
  home path) with a `boards:` list pointing at those files by ABSOLUTE path,
101
116
  plus `assignees: [...]`, `done_column: Done`, `archive_column: Archive`.
102
117
  4. Ask me which zones I want besides the kanban board: the Today/Tomorrow
103
- planner, the 24h agenda, and the live Claude Code agents view. For any I
104
- don't want, add a `zones:` block setting it to `off` (e.g. someone who
105
- doesn't use Claude Code would set `agents: off`). If I want them all, omit
106
- the block. Do NOT configure the agents view beyond on/off — it reads
107
- `~/.claude` automatically when enabled.
118
+ planner, the 24h agenda, and the live agent-sessions view (Claude Code,
119
+ Codex, OpenCode, Pi). For any I don't want, add a `zones:` block setting it
120
+ to `off` (e.g. someone who uses none of those agents would set
121
+ `agents: off`). If I want them all, omit the block. Do NOT configure the
122
+ agents view beyond on/off — it finds each agent's sessions automatically.
108
123
  5. Ask me whether I want to overlay my Google Calendar or Microsoft 365 events
109
124
  on the Agenda (skip this if I turned the agenda off). If yes, tell me to run
110
125
  `tuiboard calendar-setup google` (or `microsoft`) — it interviews me, opens
@@ -155,31 +170,40 @@ assignees: [Alice, Bob]
155
170
  done_column: Done
156
171
  archive_column: Archive
157
172
 
158
- # Optional: override Enter in the Agents zone. argv array, {cwd}/{sessionId}
159
- # substituted, run directly (no shell — element 0 must be a real binary/abs
160
- # path, NOT a shell builtin or Windows App Execution Alias). Defaults to
161
- # opening a WezTerm tab with `claude --resume <id>`. For a custom layout:
173
+ # Enter in the Agents zone opens the session in the terminal tuiboard runs in
174
+ # (tmux, herdr, WezTerm, Windows Terminal, Ghostty, else the OS default
175
+ # terminal; clipboard as last resort). Force one if detection guesses wrong:
176
+ # resume_terminal: windows-terminal # auto (default) | tmux | herdr | wezterm |
177
+ # ghostty | xdg-terminal-exec | windows-console | macos-terminal
178
+ # The session runs in your shell (auto: the one you started tuiboard from — Git
179
+ # Bash / Nushell / PowerShell on Windows, $SHELL elsewhere). Force one with:
180
+ # resume_shell: bash # auto | bash | zsh | fish | nu | pwsh | powershell | cmd
181
+
182
+ # Optional: replace Enter with your own launcher. argv array, {cwd}/{sessionId}/
183
+ # {resume} substituted, run directly (no shell — element 0 must be a real binary/abs
184
+ # path, NOT a shell builtin or Windows App Execution Alias). Takes precedence
185
+ # over resume_terminal. For a custom layout:
162
186
  # resume_command: ["nu", "C:/Users/you/.config/tuiboard/code-resume.nu", "{cwd}", "{sessionId}"]
163
187
 
164
188
  # Optional: the command `c` copies to the clipboard in the Agents zone — one
165
189
  # paste that cd's into the session dir and resumes it, so you can open it
166
- # yourself in any tab/pane (no WezTerm needed). {cwd}/{sessionId} substituted.
167
- # Default: 'cd "{cwd}" && claude --resume {sessionId}'. Nushell users:
168
- # copy_resume_command: 'cd "{cwd}"; claude --resume {sessionId}'
190
+ # yourself in any tab/pane (no WezTerm needed). {cwd}/{sessionId}/{resume}
191
+ # substituted. Default: 'cd "{cwd}" && {resume}'. Nushell users:
192
+ # copy_resume_command: 'cd "{cwd}"; {resume}'
169
193
  ```
170
194
 
171
195
  ## Zones
172
196
 
173
197
  tuiboard is four zones — **board** (kanban), **planner** (Today/Tomorrow across
174
198
  all boards), **agenda** (24h timeline + calendar overlay), and **agents** (live
175
- Claude Code sessions). Only want some of them? The board is always on; the other
199
+ coding-agent sessions). Only want some of them? The board is always on; the other
176
200
  three are yours to configure:
177
201
 
178
202
  ```yaml
179
203
  zones:
180
204
  planner: on # Today/Tomorrow panel (toggle at runtime with F1)
181
205
  agenda: on # 24h agenda + calendars (F2)
182
- agents: off # live Claude Code view (F3)
206
+ agents: off # live agent sessions view (F3)
183
207
  ```
184
208
 
185
209
  Each zone takes one of:
@@ -187,14 +211,14 @@ Each zone takes one of:
187
211
  | Value | Behavior |
188
212
  |---|---|
189
213
  | `on` | Enabled and shown at launch (the default). |
190
- | `off` | **Disabled entirely** — never rendered, skipped by `Shift-Tab`, its F-key is inert, and its background work never starts (no calendar fetch, no `~/.claude` reads). |
214
+ | `off` | **Disabled entirely** — never rendered, skipped by `Shift-Tab`, its F-key is inert, and its background work never starts (no calendar fetch, no agent session reads). |
191
215
  | `hidden` | Enabled but **collapsed at launch** — reveal it any time with its F-key. |
192
216
 
193
217
  `true`/`false` work as aliases for `on`/`off`. So a pure kanban is just
194
218
  `agenda: off` and `agents: off`; kanban + calendar is `agents: off`. The
195
219
  difference between `off` and the F-key hide: `off` means the feature never runs
196
- at all — handy if you don't use Claude Code and don't want tuiboard reading
197
- `~/.claude`.
220
+ at all — handy if you use none of the supported agents and don't want
221
+ tuiboard reading their session stores.
198
222
 
199
223
  ## Calendars (Agenda overlay)
200
224
 
@@ -376,7 +400,7 @@ Launch `tuiboard` with no flag for the default dashboard (every enabled zone).
376
400
  |---|---|---|
377
401
  | (none) | **Dashboard** — every enabled zone | Default; your configured layout |
378
402
  | `--view=planner` | Today/Tomorrow alone, full width | A narrow vertical strip beside other work |
379
- | `--view=board` | Kanban + planner panel only | Focus mode, or a single WezTerm pane |
403
+ | `--view=board` | Kanban + planner panel only | Focus mode, or a single terminal pane |
380
404
  | `--view=timeline` | Timeline fullscreen | Wall-mounted "what's now" |
381
405
  | `--view=agents` | Agent view fullscreen | Cross-machine session monitor |
382
406
 
@@ -404,10 +428,9 @@ session (until the next terminal resize).
404
428
  | `v` | Toggle Today/Tomorrow planner panel focus |
405
429
  | `Shift-Tab` | Cycle active zone (planner → board → timeline → agents) |
406
430
  | `+` | New board — create one, or adopt markdown files you already have (also the `+` chip in the top bar) |
407
- | `z` | Focus one pane (single-pane mode) — automatic below 100 columns |
408
431
  | `h` / `l` | In single-pane, walk the ring: planner → each column → agenda → agents, wrapping |
409
432
  | `F1` / `F2` / `F3` | Toggle visibility of Planner / Timeline / Agents zones |
410
- | `z` | Zoom active zone to full screen |
433
+ | `z` | Zoom active zone to full screen (single-pane below 100 columns is automatic, not triggered by `z`) |
411
434
  | `r` | Refresh everything — reload boards from disk, rescan agents, force-refetch the agenda calendar (bypasses the 30-min cache) |
412
435
 
413
436
  ### Agenda (timeline zone)
@@ -425,9 +448,10 @@ session (until the next terminal resize).
425
448
  | Key | Action |
426
449
  |---|---|
427
450
  | `j` / `k` | Move the cursor down / up the session list |
428
- | `Enter` | Open (resume) the selected session in a new WezTerm tab |
429
- | `c` | Copy a one-paste `cd … && claude --resume <id>` command for the selected session — drop it into any tab/pane to land in the right dir and resume (no WezTerm needed; format is `copy_resume_command`) |
430
- | `o` | Session detail (cwd, branch, last prompts, resume command) |
451
+ | `Enter` | Open (resume) the selected session in a new tab/window of your terminal — tmux, herdr, WezTerm, Windows Terminal, Ghostty, or the OS default; falls back to copying the command (`resume_terminal` to force one) |
452
+ | `c` | Copy a one-paste `cd … && <resume>` command (e.g. `claude --resume <id>`) for the selected session — drop it into any tab/pane to land in the right dir and resume (no WezTerm needed; format is `copy_resume_command`) |
453
+ | `o` | Session detail (harness, model, cwd, branch, last prompts, resume command, result of the last `Enter`) |
454
+ | `f` | Filter by harness: all → `cc` Claude Code → `cx` Codex → `oc` OpenCode → `pi` Pi (shown in the panel title; outside the Agents zone `f` is the board filter) |
431
455
 
432
456
  ### Task actions (work in board, planner, AND timeline zones)
433
457
 
@@ -534,6 +558,15 @@ clobber an edit made in the TUI or another editor in the meantime.
534
558
 
535
559
  See [CHANGELOG.md](CHANGELOG.md) for the full release history.
536
560
 
561
+ - **v0.12** — the Agents zone goes multi-agent: Codex, OpenCode and Pi sessions
562
+ next to Claude Code, with a colored harness badge, the model, a harness
563
+ filter (`f`) and two-line cards when zoomed. Enter reopens a session in the
564
+ terminal and shell you're using — Windows Terminal, Ghostty, tmux, herdr,
565
+ WezTerm, Git Bash, Nushell… — instead of WezTerm only.
566
+ - **v0.11** — zoomed columns no longer clip task titles short of the available
567
+ width, a board's custom name survives external edits instead of reverting to
568
+ the filename, and the keyboard reference (`?`) got a scroll + visual restyle
569
+ grouped by section.
537
570
  - **v0.10** — a task's note, read inside tuiboard: when a task's title is a
538
571
  link, `o` shows that note's text instead of pointing at Obsidian. Works with
539
572
  plain markdown links too, so the convention needs no vault.
@@ -553,7 +586,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the full release history.
553
586
  - **v0.5** — daily-driver ready. Kanban + planner + timeline + agents
554
587
  all functional, multi-select, undo, atomic file roundtrip, mouse click,
555
588
  responsive layout. Tested on Windows with WezTerm; Linux/macOS should
556
- work via the same OpenTUI binaries (untested).
589
+ work via the same OpenTUI binaries (untested at the time).
557
590
 
558
591
  ## Contributing
559
592
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tuiboard",
3
- "version": "0.11.0",
4
- "description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live Claude Code agent view. Use only the panels you want.",
3
+ "version": "0.12.0",
4
+ "description": "Terminal kanban for markdown task boards, with optional Today/Tomorrow planner, 24h agenda + calendar overlay, and a live view of your coding-agent sessions (Claude Code, Codex, OpenCode, Pi). Use only the panels you want.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Nazzareno Giannelli <nazzareno.giannelli@gmail.com>",
@@ -50,6 +50,7 @@
50
50
  "typecheck": "tsc --noEmit",
51
51
  "test": "bun test",
52
52
  "agents:check": "bun run src/scripts/agents-check.ts",
53
+ "agents:open": "bun run src/scripts/agents-open.ts",
53
54
  "summary": "bun run src/cli/summary.ts",
54
55
  "prepublishOnly": "bun run typecheck && bun test"
55
56
  },
@@ -19,6 +19,7 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
19
19
  import * as YAML from "js-yaml";
20
20
 
21
21
  import { isBoardFile } from "~/boards/scan";
22
+ import { LAUNCHERS, SHELLS, type Launcher, type Shell } from "~/input/open-session";
22
23
 
23
24
  export interface BoardConfig {
24
25
  /** Path to the .md file, absolute or relative to the config directory. */
@@ -38,20 +39,34 @@ export interface Config {
38
39
  archiveColumn: string;
39
40
  /**
40
41
  * Optional override for "open the selected agent session" (Enter in the
41
- * agents zone). An argv array; the tokens `{cwd}` and `{sessionId}` are
42
- * substituted, then it's spawned directly (no shell). Point it at your own
42
+ * agents zone). An argv array; the tokens `{cwd}`, `{sessionId}` and
43
+ * `{resume}` (the agent's own resume command, e.g. `claude --resume <id>`)
44
+ * are substituted, then it's spawned directly (no shell). Point it at your own
43
45
  * script to launch a custom terminal layout — e.g.
44
46
  * ["pwsh", "-NoProfile", "-File", "C:/.../code-resume.ps1", "{cwd}", "{sessionId}"]
45
- * When unset, tuiboard falls back to opening a tab + `claude --resume <id>`.
47
+ * When unset, tuiboard falls back to opening a tab + the agent's resume command.
46
48
  */
47
49
  resumeCommand?: string[];
50
+ /**
51
+ * Terminal Enter opens sessions in (config `resume_terminal`). `auto`
52
+ * detects it from the environment; set one explicitly when detection
53
+ * guesses wrong. Ignored when `resumeCommand` is set.
54
+ */
55
+ resumeTerminal: "auto" | Launcher;
56
+ /**
57
+ * Shell the resumed session runs in (config `resume_shell`). `auto` = the
58
+ * shell tuiboard was started from (Git Bash / Nushell / PowerShell on
59
+ * Windows, `$SHELL` elsewhere). Ignored when `resumeCommand` is set.
60
+ */
61
+ resumeShell: "auto" | Shell;
48
62
  /**
49
63
  * Template for the shell command copied to the clipboard by `c` in the agents
50
64
  * zone — one paste that `cd`s into the session's directory and resumes it.
51
- * The tokens `{cwd}` and `{sessionId}` are substituted. Default:
52
- * cd "{cwd}" && claude --resume {sessionId}
65
+ * The tokens `{cwd}`, `{sessionId}` and `{resume}` (the agent's own resume
66
+ * command, e.g. `claude --resume <id>`) are substituted. Default:
67
+ * cd "{cwd}" && {resume}
53
68
  * `&&` works in bash/zsh/pwsh/cmd; Nushell users may prefer
54
- * cd "{cwd}"; claude --resume {sessionId}
69
+ * cd "{cwd}"; {resume}
55
70
  */
56
71
  copyResumeCommand: string;
57
72
  /**
@@ -118,12 +133,14 @@ export interface CalendarsConfig {
118
133
  * paste. `&&` chains in bash/zsh/pwsh/cmd (Nushell users override with `;`).
119
134
  */
120
135
  export const DEFAULT_COPY_RESUME_COMMAND =
121
- 'cd "{cwd}" && claude --resume {sessionId}';
136
+ 'cd "{cwd}" && {resume}';
122
137
 
123
138
  export const DEFAULT_CONFIG: Omit<Config, "root" | "loaded" | "boards"> = {
124
139
  assignees: [],
125
140
  doneColumn: "Done",
126
141
  archiveColumn: "Archive",
142
+ resumeTerminal: "auto",
143
+ resumeShell: "auto",
127
144
  copyResumeCommand: DEFAULT_COPY_RESUME_COMMAND,
128
145
  zones: { planner: "on", agenda: "on", agents: "on" },
129
146
  };
@@ -197,6 +214,8 @@ interface RawConfig {
197
214
  done_column: string;
198
215
  archive_column: string;
199
216
  resume_command: string[];
217
+ resume_terminal: string;
218
+ resume_shell: string;
200
219
  copy_resume_command: string;
201
220
  calendars: {
202
221
  google?: {
@@ -342,6 +361,12 @@ function normalize(raw: Partial<RawConfig>, root: string, loaded: boolean): Conf
342
361
  Array.isArray(raw.resume_command) && raw.resume_command.length > 0
343
362
  ? raw.resume_command.map(String)
344
363
  : undefined,
364
+ resumeTerminal: (LAUNCHERS as readonly string[]).includes(raw.resume_terminal ?? "")
365
+ ? (raw.resume_terminal as Launcher)
366
+ : "auto",
367
+ resumeShell: (SHELLS as readonly string[]).includes(raw.resume_shell ?? "")
368
+ ? (raw.resume_shell as Shell)
369
+ : "auto",
345
370
  copyResumeCommand:
346
371
  typeof raw.copy_resume_command === "string" &&
347
372
  raw.copy_resume_command.trim().length > 0
@@ -14,6 +14,15 @@
14
14
  */
15
15
 
16
16
  import { isHiddenColumn } from "~/config/loader";
17
+ import {
18
+ LAUNCHER_NAME,
19
+ describePlan,
20
+ detectLauncher,
21
+ planLaunch,
22
+ runLaunchPlan,
23
+ systemLaunchEnv,
24
+ } from "~/input/open-session";
25
+ import { HARNESS, type AgentSession } from "~/store/agents";
17
26
  import { googleTokenCanWrite } from "~/store/calendar";
18
27
  import { isTask } from "~/parser/markdown";
19
28
  import {
@@ -227,6 +236,13 @@ export function handleKey(
227
236
  // Cycle the board filter — affects which open tasks show up in board
228
237
  // columns. Mirrors Python kanban `action_cycle_filter`. Cycle order:
229
238
  // all → today → overdue → tomorrow → followup → all.
239
+ // In the Agents zone `f` filters sessions by harness instead.
240
+ if (key.name === "f" && ui.activeZone === "agents") {
241
+ const next = store.cycleAgentsFilter();
242
+ const label = next === "all" ? "all harnesses" : `${HARNESS[next].code} · ${HARNESS[next].name}`;
243
+ store.flashBanner("info", `Agents: ${label}`);
244
+ return;
245
+ }
230
246
  if (key.name === "f") {
231
247
  const cycle = ["all", "today", "overdue", "tomorrow", "followup"] as const;
232
248
  const idx = cycle.indexOf(ui.filter);
@@ -477,24 +493,25 @@ function handleTimelineZone(
477
493
 
478
494
  function handleAgentsZone(store: TuiStore, key: KeyEvent): void {
479
495
  const ui = store.state.ui;
480
- const sessions = store.agents.sessions();
496
+ const sessions = store.agentSessions();
481
497
  if (key.name === "j" || key.name === "down") {
482
498
  store.setCursor(0, Math.min(sessions.length - 1, ui.row + 1));
483
499
  } else if (key.name === "k" || key.name === "up") {
484
500
  store.setCursor(0, Math.max(0, ui.row - 1));
485
501
  } else if (key.name === "enter" || key.name === "return") {
486
- // Open (resume) the selected session in a new WezTerm tab.
502
+ // Open (resume) the selected session in a new terminal tab/window.
487
503
  const target = sessions[ui.row];
488
- if (target) void openSessionInWezterm(store, target.cwd, target.sessionId);
504
+ if (target) void openSession(store, target);
489
505
  } else if (key.name === "c") {
490
506
  // Copy a one-paste "cd + resume" command for the selected session, so you
491
507
  // can drop it into any tab/pane anywhere and land in the right directory
492
- // resuming the right session (no WezTerm dependency, unlike Enter).
508
+ // resuming the right session (works in any terminal, unlike Enter).
493
509
  const target = sessions[ui.row];
494
510
  if (target) {
495
511
  const cmd = store.config.copyResumeCommand
496
512
  .replaceAll("{cwd}", target.cwd)
497
- .replaceAll("{sessionId}", target.sessionId);
513
+ .replaceAll("{sessionId}", target.sessionId)
514
+ .replaceAll("{resume}", target.resumeCommand);
498
515
  copyToClipboard(cmd).then(
499
516
  () => store.flashBanner("info", `📋 Copied resume command (${target.sessionId.slice(0, 8)})`),
500
517
  (err) => store.flashBanner("error", `Copy failed: ${err}`),
@@ -867,33 +884,26 @@ function fmtHm(m: number): string {
867
884
  }
868
885
 
869
886
  /**
870
- * Open (resume) a Claude Code session in a new WezTerm tab.
871
- *
872
- * Two steps:
873
- * 1. `wezterm cli spawn --cwd <cwd>` opens a new tab running your DEFAULT
874
- * shell in the session's directory (prints the new pane id).
875
- * 2. `wezterm cli send-text` types `claude --resume <id>` + Enter into it.
876
- *
877
- * Running it through the interactive shell (rather than `spawn -- claude …`
878
- * directly) means `claude` gets your full shell environment — PATH, env vars,
879
- * any wrapper — which is why the direct form exited 1. And if `claude` still
880
- * errors, you're left at a live prompt that shows it instead of a vanishing
881
- * tab. Failures (not inside WezTerm, `wezterm` off PATH) surface as a banner.
887
+ * Open (resume) an agent session in a new tab/window of the terminal tuiboard
888
+ * runs in — tmux, herdr, WezTerm, Windows Terminal, Ghostty, or the OS default
889
+ * — inside the user's shell (see open-session.ts). Config `resume_terminal` /
890
+ * `resume_shell` force them. When nothing can open it, the resume command lands
891
+ * on the clipboard instead. The outcome is kept for the session's detail modal.
882
892
  */
883
- async function openSessionInWezterm(
884
- store: TuiStore,
885
- cwd: string,
886
- sessionId: string,
887
- ): Promise<void> {
888
- const { spawn, spawnSync } = await import("node:child_process");
893
+ async function openSession(store: TuiStore, session: AgentSession): Promise<void> {
894
+ const { spawn } = await import("node:child_process");
895
+ const { cwd, sessionId } = session;
889
896
 
890
897
  // Custom override (config `resume_command`): an argv array with {cwd} /
891
- // {sessionId} placeholders, spawned directly (no shell). Lets you launch a
892
- // personal terminal layout without baking it into the distributed tool.
898
+ // {sessionId} / {resume} placeholders, spawned directly (no shell). Lets you
899
+ // launch a personal terminal layout without baking it into the distributed tool.
893
900
  const custom = store.config.resumeCommand;
894
901
  if (custom && custom.length > 0) {
895
902
  const argv = custom.map((arg) =>
896
- arg.replaceAll("{cwd}", cwd).replaceAll("{sessionId}", sessionId),
903
+ arg
904
+ .replaceAll("{cwd}", cwd)
905
+ .replaceAll("{sessionId}", sessionId)
906
+ .replaceAll("{resume}", session.resumeCommand),
897
907
  );
898
908
  const [cmd, ...rest] = argv;
899
909
  try {
@@ -909,39 +919,43 @@ async function openSessionInWezterm(
909
919
  store.flashBanner("error", `resume_command failed: ${e.message}`),
910
920
  );
911
921
  child.unref();
912
- store.flashBanner("info", `↗ Opening session (${sessionId.slice(0, 8)})`);
922
+ store.flashBanner("info", `↗ Opening session via resume_command (${sessionId.slice(0, 8)})`);
913
923
  } catch (e) {
914
924
  store.flashBanner("error", `resume_command failed: ${String(e)}`);
915
925
  }
916
926
  return;
917
927
  }
918
928
 
919
- try {
920
- const spawned = spawnSync("wezterm", ["cli", "spawn", "--cwd", cwd], {
921
- encoding: "utf8",
922
- windowsHide: true,
923
- });
924
- if (spawned.error) {
925
- store.flashBanner("error", `WezTerm launch failed: ${spawned.error.message}`);
926
- return;
927
- }
928
- if (spawned.status !== 0) {
929
- store.flashBanner(
930
- "error",
931
- `WezTerm spawn failed: ${(spawned.stderr || "").trim() || `exit ${spawned.status}`}`,
932
- );
933
- return;
934
- }
935
- const paneId = spawned.stdout.trim();
936
- // Type the resume command into the fresh pane (\r submits, like Enter).
937
- spawnSync(
938
- "wezterm",
939
- ["cli", "send-text", "--pane-id", paneId, "--no-paste"],
940
- { input: `claude --resume ${sessionId}\r`, encoding: "utf8", windowsHide: true },
929
+ const launchEnv = systemLaunchEnv();
930
+ const forced = store.config.resumeTerminal;
931
+ const launcher = forced === "auto" ? detectLauncher(launchEnv) : forced;
932
+ const fail = async (why: string) => {
933
+ const copied = await copyToClipboard(session.resumeCommand).then(
934
+ () => true,
935
+ () => false,
941
936
  );
942
- store.flashBanner("info", `↗ Opened session in WezTerm (${sessionId.slice(0, 8)})`);
937
+ const text = copied ? `${why} — resume command copied, paste it in ${cwd}` : why;
938
+ store.setLastLaunch(sessionId, false, text);
939
+ store.flashBanner(copied ? "warn" : "error", text);
940
+ };
941
+
942
+ if (!launcher) {
943
+ await fail("No supported terminal detected (set resume_terminal)");
944
+ return;
945
+ }
946
+ const plan = planLaunch(
947
+ launcher,
948
+ { cwd, resume: session.resumeCommand, shell: store.config.resumeShell },
949
+ launchEnv,
950
+ );
951
+ try {
952
+ await runLaunchPlan(plan);
953
+ const text = `↗ Opened session in ${LAUNCHER_NAME[launcher]} (${sessionId.slice(0, 8)})`;
954
+ store.setLastLaunch(sessionId, true, `${text}\n${describePlan(plan)}`);
955
+ store.flashBanner("info", text);
943
956
  } catch (e) {
944
- store.flashBanner("error", `WezTerm launch failed: ${String(e)}`);
957
+ const msg = e instanceof Error ? e.message : String(e);
958
+ await fail(`${LAUNCHER_NAME[launcher]} launch failed: ${msg}`);
945
959
  }
946
960
  }
947
961