@huseynsnmz/pi-board 0.14.0 → 0.15.3

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ conventional commits by `scripts/release_helper.mjs`. Entries are
5
5
  forward-only: they begin with the first release after this file landed —
6
6
  for earlier history, see the git log and the pull-request list.
7
7
 
8
+ ## [0.15.3] - 2026-11-04
9
+
10
+ ### Fixes
11
+
12
+ - **state**: Pi no longer crashes on `/new`, `/resume`, `/fork` or `/reload` (from 0.15.0)
13
+
14
+ ## [0.15.2] - 2026-11-04
15
+
16
+ ### Changes
17
+
18
+ - **deps**: exact versions everywhere, with Pi 1.5.0, TypeScript 7 and @types/node 26 for development
19
+ - **deps**: remove the postinstall script, since Pi 1.5.0 no longer ships the versions it patched
20
+
21
+ ## [0.15.1] - 2026-11-04
22
+
23
+ ### Features
24
+
25
+ - **keys**: `Ctrl+S` groups by directory, `Shift+Up` and `Shift+Down` reorder, `Ctrl+G` opens your editor
26
+ - **state**: a session waiting on a permission prompt or other dialog shows as `Needs answer`
27
+
28
+ ## [0.15.0] - 2026-11-04
29
+
30
+ ### Features
31
+
32
+ - **keys**: typing starts a task with no key first, and the keys follow Claude Code's agent view
33
+ - **board**: `Left` twice puts the session you're in on the board, so you can come back to it
34
+ - **config**: settings move to `~/.pi/agent/extensions/pi-board/config.json`
35
+ - **model**: `/board-model` chooses the helper model, which is now off until you choose one
36
+ - **ui**: drawn with frames and Pi's theme colors, to match pi-frame
37
+
8
38
  ## [0.14.0] - 2026-11-04
9
39
 
10
40
  First release of `@huseynsnmz/pi-board`, a fork of `@zhuxixi/pi-agent-board` 0.13.1.
package/README.md CHANGED
@@ -79,28 +79,27 @@ rm ~/.pi/agent/extensions/agent-board
79
79
 
80
80
  ## Quick Start
81
81
 
82
- Open the board with `/agent-board` inside Pi, or press `Left` twice on an empty prompt, then:
82
+ Open the board with `/agent-board` inside Pi, or press `Left` twice on an empty prompt. That also puts the session you're in on the board, and `Esc` or `Enter` on its row brings you back. Then:
83
83
 
84
- 1. Press `i` to enter INSERT mode.
85
- 2. Type a task.
86
- 3. Press `Enter`. It starts right away in the current folder, with this session's model and thinking level.
84
+ 1. Type a task.
85
+ 2. Press `Enter`. It starts right away in the current folder, with this session's model and thinking level.
87
86
 
88
87
  To choose the folder, model or thinking level first, press `Ctrl+N` while typing. With an empty input, `Enter` attaches/resumes the selected session. The row starts in `Queued`, then moves through `Running` to a terminal state such as `Needs answer`, `Needs instructions`, `Done`, `Failed`, or `Stopped`.
89
88
 
90
89
  From the board:
91
90
 
92
91
  - Press `Space` to peek at the selected session's summary, blocker, and latest output.
93
- - In Peek, press `r` to reply without attaching.
94
- - Press `v` for a read-only transcript, or `e` for evidence and diagnostics.
95
- - Press `Enter`, `Right`, or `>` to attach to the real Pi session.
92
+ - In Peek, type to reply without attaching.
93
+ - Press `Ctrl+O` for a read-only transcript, or `Ctrl+E` for evidence and diagnostics.
94
+ - Press `Enter`, `Left` or `Right` to attach to the real Pi session.
96
95
  - In PTY attach mode, press `Ctrl+Left` to return to the board at any time (even mid-draft), or `Ctrl+\` — the unconditional escape: it detaches regardless of editor state on any terminal that forwards the raw 0x1c byte (raw, kitty, and modifyOtherKeys keyboard modes all work). `Left` detaches only while the child Pi reports an empty editor; otherwise (draft, or unknown editor state) it is forwarded to the child. `Ctrl+]` is not a detach key — it is passed through to the child Pi editor. When the host is disconnected, `Left` always exits. `Ctrl+\` caveats: on keyboard layouts where `\` sits on AltGr (e.g. German) the chord may never produce the byte — use `Ctrl+Left` there; in legacy keyboard modes `Ctrl+4` emits the same byte and also detaches; and the key is captured from the child while attached, so the child's own `Ctrl+\` binding (e.g. the vim terminal-mode prefix) is unavailable. Automatic themes also resolve correctly in attach sessions: the child's background probe (OSC 11) and color-scheme change notifications are forwarded to your real terminal, replies flow back to the child when your terminal answers, and attach re-probes the background once when it settles so the theme is right from the first frame. Set `AGENT_BOARD_FORWARD_TERMINAL_QUERIES=0` to disable this (default on); keyboard-protocol negotiation (kitty/DA) is not forwarded — it happens once at child spawn, before attach exists. `Ctrl+X` opens an in-place delete confirm: press `y` (or Enter) to archive the row and return to the board with the next row selected — any other key cancels and is passed through to the session. While attached, the child Pi's own `Ctrl+X` bindings (copy in `/tree` and fullscreen, clear-all in the models dialog) are captured by this gesture.
97
96
 
98
97
  ## Dashboard Workflow
99
98
 
100
- The dashboard has two input modes:
99
+ The input is always ready:
101
100
 
102
- - **Normal mode** owns dashboard shortcuts such as navigation, peek, attach, and filtering.
103
- - **INSERT mode** owns text editing. Press `i` before typing or pasting a task; `/` is literal while editing a prompt.
101
+ - With an empty input, keys drive the board. `/`, `!`, `?` and `Space` are shortcuts only here.
102
+ - Once you've typed something, keys edit the task, and `/` is literal.
104
103
 
105
104
  When you press `Ctrl+N` while typing a task, the **Start session** dialog lets you review:
106
105
 
@@ -113,11 +112,11 @@ Launch preferences are persisted and reused for later sessions. **Start & attach
113
112
 
114
113
  Session actions are deliberately confirmation-aware:
115
114
 
116
- - `d` confirms moving an inactive session to **Done**. Manual completion is the default.
117
- - `h` toggles the on-hold state (manual verdict — automated writers are fenced).
118
- - Press `x`, then `y` to confirm deleting the selected row. `Ctrl+X` twice quickly still deletes without confirmation. Archiving removes the row from the board but preserves its underlying Pi session file.
119
- - `X` archives inactive rows in the selected state; live work is skipped.
120
- - `m` enters multi-select mode; see [Multi-select](#multi-select) for its keys.
115
+ - `Ctrl+D` confirms moving an inactive session to **Done**. Manual completion is the default.
116
+ - `Ctrl+B` toggles the on-hold state (manual verdict — automated writers are fenced).
117
+ - `Ctrl+X` stops a session, and a second press within 2 seconds deletes its row. Deleting removes the row from the board but preserves its underlying Pi session file.
118
+ - `Ctrl+K` archives inactive rows in the selected state; live work is skipped.
119
+ - `Ctrl+A` enters multi-select mode; see [Multi-select](#multi-select) for its keys.
121
120
 
122
121
  ## Views and Actions
123
122
 
@@ -125,34 +124,46 @@ Shortcuts are scoped to the view where they are available:
125
124
 
126
125
  ### Main list
127
126
 
127
+ Typing goes straight into the input, as in Claude Code's agent view, so there's no mode to enter first.
128
+
128
129
  | Key | Action |
129
130
  | --- | --- |
130
- | `Up` / `Down` / `j` / `k` | Move the selection. |
131
- | `i` | Enter INSERT mode for a new task. |
132
- | `Enter` | Start a draft right away, or attach/resume when the input is empty. |
133
- | `Right` / `>` / `l` | Attach to the selected session. |
134
- | `Space` | Open Peek. |
135
- | `v` | Open the read-only transcript. |
136
- | `e` | Open Evidence / Diagnostics. |
137
- | `/` | Enter filter mode. |
138
- | `Ctrl+N` | Enter INSERT mode with a pre-filled `hello` prompt. |
139
- | `Left` twice | From an empty Pi prompt, open the board. |
140
- | `Ctrl+R` | Rename the selected session. |
141
- | `Ctrl+T` | Pin or unpin the selected session. |
142
- | `Ctrl+S` | Stop the selected active session. |
143
- | `d` | Confirm marking the selected inactive session Done. |
144
- | `h` | Hold or unhold the selected inactive session. |
145
- | `x`, then `y` | Archive/delete the selected row (confirmation prompt). |
146
- | `Ctrl+X` twice quickly | Archive/delete the selected row without confirmation. |
147
- | `X` | Delete inactive rows in the selected state. |
148
- | `m` | Enter multi-select mode. |
149
- | `!` | Open node-pty diagnostics and fix steps. |
150
- | `?` | Open the help overlay. |
151
- | `Esc` | Clear a draft, or exit when the input is empty. |
131
+ | Any text | Start writing a task. |
132
+ | `Enter` | Start the task, or attach to the selected session when the input is empty. |
133
+ | `Ctrl+Enter` | Start the task and attach to it. |
134
+ | `Shift+Enter` / `Ctrl+J` | New line in the task. |
135
+ | `Ctrl+N` | While typing, choose the folder, model and thinking level first. |
136
+ | `Left` / `Right` | Attach to the selected session. |
137
+ | `Left` twice | From an empty Pi prompt, put this session on the board and open it. |
138
+ | `Up` / `Down` | Move between rows. |
139
+ | `PgUp` / `PgDn` | Move by a screenful. |
140
+ | `Home` / `End` | First or last row. |
141
+ | `Alt+Up` / `Alt+Down` | Previous or next group. |
142
+ | `Alt+1`..`Alt+9` | Attach to that row. |
143
+ | `Space` | Open or close Peek. |
144
+ | `/` / `Ctrl+F` | Filter the list. |
145
+ | `Ctrl+S` | Group by state or by directory. |
146
+ | `Shift+Up` / `Shift+Down` | Move the row up or down its group. |
147
+ | `Ctrl+G` | Write the task in `$VISUAL` or `$EDITOR`. |
148
+ | `Ctrl+T` | Pin or unpin. |
149
+ | `Ctrl+R` | Rename. |
150
+ | `Ctrl+X` | Stop the session; press again within 2 seconds to delete it. |
151
+ | `Ctrl+D` | Confirm marking an inactive session Done. |
152
+ | `Ctrl+B` | Hold or unhold an inactive session. |
153
+ | `Ctrl+A` | Select several rows. |
154
+ | `Ctrl+O` | Read-only transcript. |
155
+ | `Ctrl+E` | Evidence / Diagnostics. |
156
+ | `Ctrl+K` | Delete inactive rows in the selected state. |
157
+ | `!` | node-pty diagnostics and fix steps. |
158
+ | `?` | Help. |
159
+ | `Esc` | Clear a draft, or leave the board. |
160
+ | `Ctrl+C` | Clear a draft; press twice to leave. |
161
+
162
+ On macOS, the `Alt` keys need your terminal to send Option as Alt.
152
163
 
153
164
  ### Multi-select
154
165
 
155
- Press `m` in the main list to act on several sessions at once.
166
+ Press `Ctrl+A` in the main list to act on several sessions at once.
156
167
 
157
168
  | Key | Action |
158
169
  | --- | --- |
@@ -172,15 +183,15 @@ Peek shows the selected session's summary, blocker or question, latest output, a
172
183
 
173
184
  | Key | Action |
174
185
  | --- | --- |
175
- | `r` or `Enter` | Enter reply mode; type a follow-up and press `Enter` to send it without attaching. |
176
- | `a`, `Right`, `>`, or `l` | Attach to the session. |
177
- | `v` | Open the read-only transcript. |
178
- | `e` | Open Evidence / Diagnostics. |
179
- | `d` | Confirm marking the inactive session Done. |
180
- | `h` | Hold or unhold the session. |
186
+ | Any text | Start a reply, and press `Enter` to send it without attaching. |
187
+ | `Left` / `Right` | Attach to the session. |
188
+ | `Up` / `Down` | Move to the previous or next session. |
189
+ | `Ctrl+O` | Open the read-only transcript. |
190
+ | `Ctrl+E` | Open Evidence / Diagnostics. |
191
+ | `Ctrl+D` | Confirm marking the inactive session Done. |
192
+ | `Ctrl+B` | Hold or unhold the session. |
181
193
  | `!` | Open node-pty diagnostics and fix steps. |
182
- | `Up` / `Down` / `j` / `k` | Move to the previous or next session. |
183
- | `Esc` | Return to the main list. |
194
+ | `Space` / `Esc` | Return to the main list. |
184
195
 
185
196
  When a Pi question or questionnaire tool is pending, inline reply is rejected; attach to answer that interactive question in the real Pi session.
186
197
 
@@ -320,33 +331,47 @@ Other current limitations:
320
331
 
321
332
  ## Configuration
322
333
 
323
- Set these variables before starting Pi. Model-backed features fall back gracefully where noted, so disabling them does not prevent the core dashboard from working.
334
+ Settings live in `~/.pi/agent/extensions/pi-board/config.json`. Everything is optional, and a missing or broken file means the defaults. Restart Pi after editing it.
324
335
 
325
- | Variable | Default / values | Purpose |
336
+ ```json
337
+ {
338
+ "model": "provider/model-id",
339
+ "autoDone": false
340
+ }
341
+ ```
342
+
343
+ ### Helper model
344
+
345
+ Titles, row summaries and telling `Needs answer` from `Needs instructions` can use a model. **It's off by default**, so the board makes no model calls of its own and uses built-in rules instead.
346
+
347
+ Run `/board-model` inside Pi to pick a model or turn it off. It's saved to the file above and applies to sessions started from then on. These calls run through Pi, so any model Pi can use works, and they send up to 6,000 characters of a session's latest reply to that model.
348
+
349
+ ### Other settings
350
+
351
+ | Key | Default | Purpose |
326
352
  | --- | --- | --- |
327
- | `AGENT_BOARD_ROOT` | `~/.pi/agent/agent-board/` | Override the durable store location. |
328
- | `AGENT_BOARD_AUTO_STATE` | enabled; `off` disables | Enable automatic terminal-state refinement after a turn. |
329
- | `AGENT_BOARD_AUTO_STATE_MODEL` | `gpt-4o`; `off` uses heuristics | Model for classifying the terminal state of a finished turn. |
330
- | `AGENT_BOARD_AUTO_STATE_NO_DONE` | unset = manual Done; `0`, `false`, `off`, or `no` restores auto-Done | Keep completion manual by default, or restore automatic `Done` classification. |
331
- | `AGENT_BOARD_SUMMARY_MODEL` | `gpt-4o`; `off` disables | Generate short row summaries. Heuristic summaries remain available as a fallback. |
332
- | `AGENT_BOARD_TITLE_MODEL` | `openai-codex/gpt-5.5`; `off` disables | Generate a short session title after dispatch. The initial slug remains if generation fails. |
333
- | `AGENT_BOARD_TITLE_THINKING_LEVEL` | `low`; `off` omits the option | Thinking level used by title generation. |
334
- | `AGENT_BOARD_CODE_REFS` | enabled; `off` disables | Extract issue/PR badges from session evidence. |
335
- | `AGENT_BOARD_DISABLE_PTY` | unset; `1` disables | Disable PTY host and attach mode. |
336
- | `AGENT_BOARD_FORCE_PTY` | unset; `1` forces the PTY path | Force the PTY availability path when diagnosing or controlling fallback behavior. |
337
- | `AGENT_BOARD_ATTACH_MOUSE` | enabled; `0`, `off`, or `false` disables | Disable attach-view mouse handling and use terminal-native selection instead. |
338
- | `AGENT_BOARD_ENABLE_MOUSE_SCROLL` | enabled; `0` disables | Compatibility switch to disable attach-view mouse scrolling. |
339
- | `AGENT_BOARD_WHEEL_LINES` | `1`, clamped to `1..50` | Lines scrolled per mouse-wheel notch in the attach view (regular TUI mode; in fullscreen the TUI's own wheel accelerator decides the line count). |
340
- | `AGENT_BOARD_MAX_WARM_HOSTS` | `4`, clamped to `0..50` | Maximum number of idle PTY hosts retained for faster attach. |
341
- | `AGENT_BOARD_WARM_HOST_TTL_MS` | `600000` (10 minutes); `0` disables TTL eviction | How long an idle warm host may remain before it is evicted. |
342
- | `AGENT_BOARD_ATTACH_NATIVE_PASTE` | enabled; `0` disables | Disable X11 PRIMARY selection copy and middle-click paste integration. |
343
- | `AGENT_BOARD_FORWARD_OSC52` | enabled; `0` disables | Disable OSC 52 clipboard sequence forwarding from an attached session. |
344
- | `AGENT_BOARD_FORWARD_IMAGES` | enabled; `0` disables | Disable terminal image/file passthrough forwarding from an attached session. |
345
- | `AGENT_BOARD_IME_FIX` | enabled; `0` disables | Disable the attach-view IME cursor coalescer if your terminal has compatibility problems. |
346
- | `AGENT_BOARD_TERMINAL_SNAPSHOT` | enabled; `0` forces legacy | Force the pre-phase-4 attach path (screen.log replay + jiggle); escape hatch / rollback switch for the snapshot+subscribe attach protocol. |
347
- | `AGENT_BOARD_REPLY_GUARD` | enabled; `0`, `off`, or `false` disables | Discard terminal reply fragments (device attributes, kitty flags, cell size, DCS) that pi-tui delivers as input, and forward color replies whole. While a reply head is open, up to 64 bytes / 250ms of following input is absorbed. |
348
-
349
- `AGENT_BOARD_TERMINAL_SNAPSHOT=0` routes attach onto the legacy fallback family: the screen.log replay tail plus the shrink-and-hold jiggle redraw. These paths exist only for pre-protocol runners and as a rollback hatch — they are not part of the snapshot+subscribe protocol path (issue #91 phase 4). They will be retired once the installed runner fleet is on the snapshot protocol baseline (detectable on the wire via hello protocol fields).
353
+ | `model` | off | Helper model, as `provider/id`. |
354
+ | `groupBy` | `state` | `state` or `directory`. `Ctrl+S` changes it. |
355
+ | `root` | `~/.pi/agent/agent-board/` | Where the board stores its state. |
356
+ | `titleThinking` | `low` | Thinking level for titles. |
357
+ | `autoDone` | `false` | Let the helper model mark sessions Done. |
358
+ | `codeRefs` | `true` | Issue and pull request badges. |
359
+ | `disablePty` | `false` | Turn off live terminal sessions and attach. |
360
+ | `attachMouse` | `true` | Mouse handling in attached sessions. |
361
+ | `mouseScroll` | `true` | Mouse wheel scrolling in attached sessions. |
362
+ | `wheelLines` | `1` | Lines per wheel notch, from 1 to 50. |
363
+ | `maxWarmHosts` | `4` | Idle sessions kept ready for fast attach, from 0 to 50. |
364
+ | `warmHostTtlMs` | `600000` | How long an idle one is kept; `0` keeps it. |
365
+ | `nativePaste` | `true` | X11 PRIMARY copy and middle-click paste. |
366
+ | `forwardClipboard` | `true` | Forward OSC 52 clipboard writes from attached sessions. |
367
+ | `forwardImages` | `true` | Forward terminal images from attached sessions. |
368
+ | `imeFix` | `true` | IME cursor fix in attached sessions. |
369
+ | `terminalSnapshot` | `true` | Newer attach path; `false` uses the older one. |
370
+ | `replyGuard` | `true` | Discard stray terminal replies delivered as input. |
371
+
372
+ An `AGENT_BOARD_*` environment variable that's already set overrides the file for that run.
373
+
374
+ Setting `terminalSnapshot` to `false` routes attach onto the legacy fallback family: the screen.log replay tail plus the shrink-and-hold jiggle redraw. These paths exist only for pre-protocol runners and as a rollback hatch — they are not part of the snapshot+subscribe protocol path (issue #91 phase 4). They will be retired once the installed runner fleet is on the snapshot protocol baseline (detectable on the wire via hello protocol fields).
350
375
 
351
376
  Older `AGENT_VIEW_*` names are still read in selected compatibility paths. Prefer `AGENT_BOARD_*` for new setups. Internal child markers are managed by Agent Board and are not user settings.
352
377
 
@@ -413,23 +438,7 @@ npm run verify
413
438
 
414
439
  ## Publishing
415
440
 
416
- Releases run through `scripts/release.mjs` and are published by CI when a GitHub Release is created. The flow checks the parts that are easy to get wrong — the changelog range, tag ancestry against `main`, and drift from a late merge — instead of assuming them. See [docs/RELEASE.md](docs/RELEASE.md) for the full process, the one-time npm trusted-publisher setup, and failure recovery.
417
-
418
- ```bash
419
- node scripts/release.mjs precheck # gate: clean main, nothing in flight
420
- node scripts/release.mjs prepare minor # changelog + verify + bump + tag + release branch
421
- node scripts/release.mjs open-pr 0.9.0 # push the branch and tag, open the release PR
422
- # merge the PR with a merge commit, then:
423
- node scripts/release.mjs finish 0.9.0 # GitHub Release → CI publishes to npm
424
- ```
425
-
426
- After publishing, users install the scoped package with:
427
-
428
- ```bash
429
- pi install npm:@huseynsnmz/pi-board
430
- ```
431
-
432
- The Pi package gallery uses the `pi.video` and `pi.image` URLs from `package.json`.
441
+ Published to npm by hand, with `npm publish --access public`. There's no publish workflow.
433
442
 
434
443
  ## Further Reading
435
444
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huseynsnmz/pi-board",
3
- "version": "0.14.0",
3
+ "version": "0.15.3",
4
4
  "description": "A board for Pi's background sessions: start, watch, reply to and attach to them. A fork of @zhuxixi/pi-agent-board.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -51,7 +51,6 @@
51
51
  "image": "https://github.com/zhuxixi/pi-agent-board/raw/refs/heads/main/assets/banner.png"
52
52
  },
53
53
  "scripts": {
54
- "postinstall": "node scripts/patch-vulns.mjs",
55
54
  "typecheck": "tsc --noEmit",
56
55
  "test": "node scripts/run-tests.mjs",
57
56
  "test:coverage": "c8 node scripts/run-tests.mjs",
@@ -73,14 +72,20 @@
73
72
  }
74
73
  },
75
74
  "devDependencies": {
76
- "@earendil-works/pi-coding-agent": "1.1.0",
77
- "@earendil-works/pi-tui": "1.1.0",
78
- "@types/node": "^25.9.1",
79
- "c8": "^12.0.0",
80
- "typescript": "^5.9.3"
75
+ "@earendil-works/pi-coding-agent": "1.5.0",
76
+ "@earendil-works/pi-tui": "1.5.0",
77
+ "@types/node": "26.6.6",
78
+ "c8": "12.0.0",
79
+ "typescript": "7.0.2"
81
80
  },
82
81
  "dependencies": {
83
- "@xterm/headless": "^6.0.0",
84
- "node-pty": "^1.1.0"
82
+ "@xterm/headless": "6.0.0",
83
+ "node-pty": "1.1.0"
84
+ },
85
+ "overrides": {
86
+ "test-exclude": {
87
+ "glob": "14.0.3",
88
+ "minimatch": "11.0.4"
89
+ }
85
90
  }
86
91
  }
@@ -25,7 +25,7 @@ function main() {
25
25
  async function maybeGenerateTitle(config) {
26
26
  const configured = process.env.AGENT_BOARD_TITLE_MODEL ?? process.env.AGENT_VIEW_TITLE_MODEL;
27
27
  if (configured === "off") return;
28
- const model = config.model ?? configured ?? DEFAULT_TITLE_MODEL;
28
+ const model = configured ?? config.model ?? DEFAULT_TITLE_MODEL;
29
29
  const thinking = process.env.AGENT_BOARD_TITLE_THINKING_LEVEL ?? process.env.AGENT_VIEW_TITLE_THINKING_LEVEL ?? DEFAULT_TITLE_THINKING_LEVEL;
30
30
  const prompt = titlePrompt(config.prompt);
31
31
  const args = [...config.piArgsPrefix, "--mode", "json", "-p", "--no-session", "--model", model];
@@ -73,7 +73,7 @@ export function registerAgentBoardCommand(pi: ExtensionAPI, opts: AgentBoardComm
73
73
  }
74
74
 
75
75
  export async function openDashboard(
76
- ctx: Pick<ExtensionCommandContext, "ui" | "cwd" | "modelRegistry" | "model">,
76
+ ctx: Pick<ExtensionCommandContext, "ui" | "cwd" | "modelRegistry" | "model" | "sessionManager">,
77
77
  service: ReturnType<typeof createService>,
78
78
  options: {
79
79
  initialSelectedId?: string | null;
@@ -104,6 +104,7 @@ export async function openDashboard(
104
104
  service,
105
105
  root: service.getRoot(),
106
106
  defaultCwd: ctx.cwd,
107
+ currentSessionFile: ctx.sessionManager?.getSessionFile?.() ?? null,
107
108
  initialSelectedId: options.initialSelectedId,
108
109
  initialNotice: options.initialNotice,
109
110
  availableModels,
@@ -159,6 +160,8 @@ export async function dashboardAttachLoop(
159
160
  });
160
161
  pendingNotice = undefined;
161
162
  if (result.action !== "attach") return;
163
+ // That session is the one on screen behind the board: going back to it is just closing.
164
+ if (result.viewId === currentId) return;
162
165
  selectedId = result.viewId;
163
166
  const outcome = await attach(ctx, service, root, result.viewId, result.stopFirst);
164
167
  if (outcome.afterDelete) {
@@ -3,6 +3,8 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
3
3
  import { createService } from "../runtime/service.mjs";
4
4
  import { dashboardAttachLoop } from "./agent-board.js";
5
5
 
6
+ const NEW_SESSION = "new session";
7
+
6
8
  export interface BgCommandOptions {
7
9
  root: string;
8
10
  runnerScript: string;
@@ -28,10 +30,6 @@ async function handleBgCommand(args: string, ctx: ExtensionCommandContext, opts:
28
30
  return;
29
31
  }
30
32
  const sessionFile = ctx.sessionManager.getSessionFile();
31
- if (!sessionFile) {
32
- ctx.ui.notify("No current session file is available to background.", "warning");
33
- return;
34
- }
35
33
  const service = createService({
36
34
  root: opts.root,
37
35
  runnerScript: opts.runnerScript,
@@ -50,6 +48,11 @@ async function handleBgCommand(args: string, ctx: ExtensionCommandContext, opts:
50
48
  }
51
49
  },
52
50
  });
51
+ // A session that isn't saved anywhere has nothing to come back to, but the board still opens.
52
+ if (!sessionFile) {
53
+ await dashboardAttachLoop(ctx, service, opts.root, null, opts.getThinkingLevel);
54
+ return;
55
+ }
53
56
  const model = modelRef(ctx.model as any);
54
57
  // adoptSession is async (routes through the view-state coordinator, issue #91).
55
58
  const adopted = await service.adoptSession({
@@ -57,7 +60,8 @@ async function handleBgCommand(args: string, ctx: ExtensionCommandContext, opts:
57
60
  cwd: ctx.cwd,
58
61
  model,
59
62
  thinkingLevel: opts.getThinkingLevel(),
60
- name: "background-session",
63
+ // A row that exists keeps its name, which may be one you gave it.
64
+ name: hasOwnName(service, sessionFile) ? undefined : sessionName(ctx),
61
65
  });
62
66
  if (!adopted.ok || !adopted.viewId) {
63
67
  ctx.ui.notify(adopted.error ?? "Could not background current session.", "warning");
@@ -72,6 +76,25 @@ async function handleBgCommand(args: string, ctx: ExtensionCommandContext, opts:
72
76
  await dashboardAttachLoop(ctx, service, opts.root, adopted.viewId, opts.getThinkingLevel);
73
77
  }
74
78
 
79
+ function hasOwnName(service: ReturnType<typeof createService>, sessionFile: string): boolean {
80
+ const row = service.rows().find((candidate) => candidate.meta.sessionFile === sessionFile);
81
+ return row !== undefined && row.meta.name !== NEW_SESSION;
82
+ }
83
+
84
+ /** Its name if it has one, then what you first asked. A session with neither is new. */
85
+ function sessionName(ctx: ExtensionCommandContext): string {
86
+ const named = ctx.sessionManager.getSessionName?.();
87
+ if (named) return named;
88
+ for (const entry of ctx.sessionManager.getEntries() as any[]) {
89
+ if (entry.type !== "message" || entry.message?.role !== "user") continue;
90
+ const content = entry.message.content;
91
+ const text = typeof content === "string" ? content : (content ?? []).map((part: any) => part.text ?? "").join(" ");
92
+ const line = String(text).trim().split("\n")[0];
93
+ if (line) return line.slice(0, 48);
94
+ }
95
+ return NEW_SESSION;
96
+ }
97
+
75
98
  function modelRef(model: any): string | null {
76
99
  if (!model || typeof model !== "object") return null;
77
100
  if (typeof model.provider === "string" && typeof model.id === "string") return `${model.provider}/${model.id}`;
@@ -0,0 +1,21 @@
1
+ /** `/board-model`: which model writes the board's titles, summaries and states. */
2
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
+ import { readConfig, setHelperModel } from "../core/config.mjs";
4
+
5
+ const OFF = "Off: use built-in rules, no model calls";
6
+
7
+ export function registerBoardModelCommand(pi: ExtensionAPI): void {
8
+ pi.registerCommand("board-model", {
9
+ description: "Choose the model for the board's titles, summaries and states, or turn it off",
10
+ handler: async (_args, ctx: ExtensionCommandContext) => {
11
+ if (!ctx.hasUI) return;
12
+ const refs = ctx.modelRegistry.getAvailable().map((model) => `${model.provider}/${model.id}`);
13
+ const current = readConfig().model;
14
+ const choice = await ctx.ui.select(`Board helper model (now: ${current ?? "off"})`, [OFF, ...refs]);
15
+ if (choice === undefined) return;
16
+ const model = choice === OFF ? null : choice;
17
+ setHelperModel(model);
18
+ ctx.ui.notify(model ? `The board's helper calls now use ${model}.` : "The board's helper model calls are off.", "info");
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The board's settings, from `~/.pi/agent/extensions/pi-board/config.json`.
3
+ *
4
+ * The runners are separate processes that read `AGENT_BOARD_*` from their environment, so a
5
+ * setting reaches them by being put there before anything starts. A variable that's already set
6
+ * wins, which keeps one run's override and the tests working.
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import * as os from "node:os";
10
+ import * as path from "node:path";
11
+
12
+ /** Which model each kind of helper call uses when the config has none of its own. */
13
+ const MODEL_VARS = ["AGENT_BOARD_AUTO_STATE_MODEL", "AGENT_BOARD_SUMMARY_MODEL", "AGENT_BOARD_TITLE_MODEL"];
14
+
15
+ const off = (value) => (value ? "1" : "0");
16
+ const SETTINGS = {
17
+ root: ["AGENT_BOARD_ROOT", String],
18
+ titleThinking: ["AGENT_BOARD_TITLE_THINKING_LEVEL", String],
19
+ autoDone: ["AGENT_BOARD_AUTO_STATE_NO_DONE", (value) => (value ? "0" : "1")],
20
+ codeRefs: ["AGENT_BOARD_CODE_REFS", (value) => (value ? "1" : "off")],
21
+ disablePty: ["AGENT_BOARD_DISABLE_PTY", (value) => (value ? "1" : "")],
22
+ attachMouse: ["AGENT_BOARD_ATTACH_MOUSE", off],
23
+ mouseScroll: ["AGENT_BOARD_ENABLE_MOUSE_SCROLL", off],
24
+ wheelLines: ["AGENT_BOARD_WHEEL_LINES", String],
25
+ maxWarmHosts: ["AGENT_BOARD_MAX_WARM_HOSTS", String],
26
+ warmHostTtlMs: ["AGENT_BOARD_WARM_HOST_TTL_MS", String],
27
+ nativePaste: ["AGENT_BOARD_ATTACH_NATIVE_PASTE", off],
28
+ forwardClipboard: ["AGENT_BOARD_FORWARD_OSC52", off],
29
+ forwardImages: ["AGENT_BOARD_FORWARD_IMAGES", off],
30
+ imeFix: ["AGENT_BOARD_IME_FIX", off],
31
+ terminalSnapshot: ["AGENT_BOARD_TERMINAL_SNAPSHOT", off],
32
+ replyGuard: ["AGENT_BOARD_REPLY_GUARD", off],
33
+ };
34
+
35
+ /** @param {NodeJS.ProcessEnv} [env] */
36
+ export function configPath(env = process.env) {
37
+ const agentDir = env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
38
+ return path.join(agentDir, "extensions", "pi-board", "config.json");
39
+ }
40
+
41
+ /** A missing or broken file is the defaults: the board must still open. */
42
+ export function readConfig(file = configPath()) {
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
45
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+
51
+ export function writeConfig(changes, file = configPath()) {
52
+ mkdirSync(path.dirname(file), { recursive: true });
53
+ writeFileSync(file, `${JSON.stringify({ ...readConfig(file), ...changes }, null, 2)}\n`);
54
+ }
55
+
56
+ /** Helper model calls cost money and send text to a provider, so they're off until one is chosen. */
57
+ export function configEnv(config) {
58
+ const vars = {};
59
+ for (const [key, [name, convert]] of Object.entries(SETTINGS)) {
60
+ if (config[key] !== undefined && config[key] !== null) vars[name] = convert(config[key]);
61
+ }
62
+ const models = typeof config.model === "string" && config.model ? config.model : "off";
63
+ for (const name of MODEL_VARS) vars[name] = models;
64
+ return vars;
65
+ }
66
+
67
+ export function applyConfig(env = process.env, config = readConfig(configPath(env))) {
68
+ for (const [name, value] of Object.entries(configEnv(config))) {
69
+ if (env[name] === undefined) env[name] = value;
70
+ }
71
+ }
72
+
73
+ /** Takes effect for sessions started from now on. */
74
+ export function setHelperModel(model, env = process.env, file = configPath(env)) {
75
+ writeConfig({ model }, file);
76
+ for (const name of MODEL_VARS) env[name] = model ?? "off";
77
+ }
@@ -0,0 +1,4 @@
1
+ /** Imported first, because some modules read their settings as they load. */
2
+ import { applyConfig } from "./config.mjs";
3
+
4
+ applyConfig();
package/src/core/rows.mjs CHANGED
@@ -154,6 +154,7 @@ export function rowView(row, now) {
154
154
  folderName,
155
155
  folderPath,
156
156
  pinned: Boolean(row.meta.pinned),
157
+ order: row.meta.order ?? row.meta.createdAt ?? 0,
157
158
  state,
158
159
  alive: Boolean(row.alive),
159
160
  hostAlive: Boolean(row.hostAlive),
@@ -257,6 +258,24 @@ export function groupRowsByFolder(rows, now) {
257
258
  return groups;
258
259
  }
259
260
 
261
+ /**
262
+ * The other way to see the list: one group per directory, whatever state its rows are in.
263
+ * @param {Row[]} rows
264
+ * @param {number} now
265
+ * @returns {Array<{ key: string, name: string, path: string, rows: RowView[] }>}
266
+ */
267
+ export function groupRowsByDirectory(rows, now) {
268
+ const byFolder = new Map();
269
+ for (const view of rows.map((r) => rowView(r, now))) {
270
+ const existing = byFolder.get(view.folderKey);
271
+ if (existing) existing.rows.push(view);
272
+ else byFolder.set(view.folderKey, { key: view.folderKey, name: view.folderName, path: view.folderPath, rows: [view] });
273
+ }
274
+ const folders = [...byFolder.values()];
275
+ for (const folder of folders) folder.rows = sortRowViews(folder.rows);
276
+ return folders.sort((a, b) => Math.min(...a.rows.map((r) => r.createdAt)) - Math.min(...b.rows.map((r) => r.createdAt)) || a.name.localeCompare(b.name));
277
+ }
278
+
260
279
  /**
261
280
  * Issue #147: the row the board should select after `id` is removed — the
262
281
  * next visible row in dashboard order, else the previous one, else null
@@ -286,6 +305,7 @@ function sortRowViews(views) {
286
305
  return views.sort(
287
306
  (a, b) =>
288
307
  Number(b.pinned) - Number(a.pinned) ||
308
+ a.order - b.order ||
289
309
  a.createdAt - b.createdAt ||
290
310
  a.id.localeCompare(b.id),
291
311
  );