@huseynsnmz/pi-board 0.14.0 → 0.15.1
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 +17 -0
- package/README.md +97 -88
- package/package.json +1 -1
- package/runner/title-runner.mjs +1 -1
- package/src/commands/agent-board.ts +4 -1
- package/src/commands/bg.ts +28 -5
- package/src/commands/board-model.ts +21 -0
- package/src/core/config.mjs +77 -0
- package/src/core/load-config.mjs +4 -0
- package/src/core/rows.mjs +20 -0
- package/src/index.ts +9 -0
- package/src/runtime/service.mjs +28 -1
- package/src/ui/dashboard.ts +319 -282
- package/src/ui/double-left.ts +7 -17
- package/src/ui/external-editor.ts +25 -0
- package/src/ui/focus.ts +26 -0
- package/src/ui/waiting.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.1] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
- **keys**: `Ctrl+S` groups by directory, `Shift+Up` and `Shift+Down` reorder, `Ctrl+G` opens your editor
|
|
13
|
+
- **state**: a session waiting on a permission prompt or other dialog shows as `Needs answer`
|
|
14
|
+
|
|
15
|
+
## [0.15.0] - 2026-11-04
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **keys**: typing starts a task with no key first, and the keys follow Claude Code's agent view
|
|
20
|
+
- **board**: `Left` twice puts the session you're in on the board, so you can come back to it
|
|
21
|
+
- **config**: settings move to `~/.pi/agent/extensions/pi-board/config.json`
|
|
22
|
+
- **model**: `/board-model` chooses the helper model, which is now off until you choose one
|
|
23
|
+
- **ui**: drawn with frames and Pi's theme colors, to match pi-frame
|
|
24
|
+
|
|
8
25
|
## [0.14.0] - 2026-11-04
|
|
9
26
|
|
|
10
27
|
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,
|
|
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.
|
|
85
|
-
2.
|
|
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,
|
|
94
|
-
- Press `
|
|
95
|
-
- Press `Enter`, `
|
|
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
|
|
99
|
+
The input is always ready:
|
|
101
100
|
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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
|
-
- `
|
|
117
|
-
- `
|
|
118
|
-
-
|
|
119
|
-
- `
|
|
120
|
-
- `
|
|
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
|
-
|
|
|
131
|
-
| `
|
|
132
|
-
| `Enter` | Start
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
|
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
| `Ctrl+
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
| `
|
|
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 `
|
|
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
|
-
|
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
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
package/runner/title-runner.mjs
CHANGED
|
@@ -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 ??
|
|
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) {
|
package/src/commands/bg.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|
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
|
);
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* launch background workers and the detached runner, and keeps a small footer status with
|
|
6
6
|
* the count of sessions needing attention. See docs/EXPLORATION.md for the design.
|
|
7
7
|
*/
|
|
8
|
+
import "./core/load-config.mjs";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
import { createConnection } from "node:net";
|
|
10
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -15,8 +16,10 @@ import { createService, envInt } from "./runtime/service.mjs";
|
|
|
15
16
|
import { attachWarmHostSweeper } from "./core/warm-host-sweeper.mjs";
|
|
16
17
|
import { openDashboard, registerAgentBoardCommand } from "./commands/agent-board.js";
|
|
17
18
|
import { registerBgCommand } from "./commands/bg.js";
|
|
19
|
+
import { registerBoardModelCommand } from "./commands/board-model.ts";
|
|
18
20
|
import { announceBoardClosedOn } from "./ui/chrome.ts";
|
|
19
21
|
import { watchDoubleLeft } from "./ui/double-left.ts";
|
|
22
|
+
import { watchWaiting } from "./ui/waiting.ts";
|
|
20
23
|
|
|
21
24
|
const RUNNER_SCRIPT = fileURLToPath(new URL("../runner/job-runner.mjs", import.meta.url));
|
|
22
25
|
const PTY_RUNNER_SCRIPT = fileURLToPath(new URL("../runner/pty-runner.mjs", import.meta.url));
|
|
@@ -40,6 +43,7 @@ let hostedSessionEpoch = 0;
|
|
|
40
43
|
export default function piAgentBoard(pi: ExtensionAPI): void {
|
|
41
44
|
announceBoardClosedOn(pi.events);
|
|
42
45
|
let stopDoubleLeft: (() => void) | null = null;
|
|
46
|
+
let stopWaiting: (() => void) | null = null;
|
|
43
47
|
const root = defaultRoot();
|
|
44
48
|
const { piCommand, piArgsPrefix } = resolvePiInvocation();
|
|
45
49
|
|
|
@@ -85,6 +89,7 @@ export default function piAgentBoard(pi: ExtensionAPI): void {
|
|
|
85
89
|
|
|
86
90
|
registerAgentBoardCommand(pi, commandOpts);
|
|
87
91
|
registerBgCommand(pi, commandOpts);
|
|
92
|
+
registerBoardModelCommand(pi);
|
|
88
93
|
pi.registerFlag("agent-board", {
|
|
89
94
|
description: "Open the agent-board dashboard on startup",
|
|
90
95
|
type: "boolean",
|
|
@@ -198,6 +203,10 @@ export default function piAgentBoard(pi: ExtensionAPI): void {
|
|
|
198
203
|
stopDoubleLeft?.();
|
|
199
204
|
stopDoubleLeft = watchDoubleLeft(ctx);
|
|
200
205
|
}
|
|
206
|
+
if (ctx.mode === "tui") {
|
|
207
|
+
stopWaiting?.();
|
|
208
|
+
stopWaiting = watchWaiting(ctx, (waiting) => syncForeground({ type: "agent_board_waiting", waiting }, ctx));
|
|
209
|
+
}
|
|
201
210
|
if (isHostedChild && !hostedEditorReporter && typeof ctx.ui?.getEditorText === "function" && hostedViewId) {
|
|
202
211
|
const { createEditorStateReporter } = await import("./core/editor-state-reporter.mjs");
|
|
203
212
|
hostedEditorReporter = createEditorStateReporter({
|
package/src/runtime/service.mjs
CHANGED
|
@@ -1377,7 +1377,8 @@ export function createService(opts) {
|
|
|
1377
1377
|
const pty = ptySupport();
|
|
1378
1378
|
if (!pty.ok) return { ok: false, error: "PTY unavailable", fallbackReason: nodePtyFallbackMessage(pty) };
|
|
1379
1379
|
if (isAgentBusy(row)) return { ok: false, error: "A non-live background run is active for this session" };
|
|
1380
|
-
|
|
1380
|
+
// A missing file is a session that was put on the board before its first message. Pi starts
|
|
1381
|
+
// it at that path, as it would have.
|
|
1381
1382
|
|
|
1382
1383
|
const lock = tryAcquireLockImpl(root, viewId, "host-start", { identity: serviceIdentity() });
|
|
1383
1384
|
if (!lock.acquired) {
|
|
@@ -1809,6 +1810,16 @@ export function createService(opts) {
|
|
|
1809
1810
|
appendDiagnostic(root, row.meta.id, { source: "evidence", level: "warn", code: "evidence_reduce_failed", message: "Could not reduce hosted evidence", details: { error: err instanceof Error ? err.message : String(err) } });
|
|
1810
1811
|
}
|
|
1811
1812
|
|
|
1813
|
+
if (event.type === "agent_board_waiting") {
|
|
1814
|
+
status.semanticState = event.waiting ? "needs_input" : "working";
|
|
1815
|
+
status.processState = "alive";
|
|
1816
|
+
status.question = event.waiting ? "Waiting on a prompt. Attach to answer it." : null;
|
|
1817
|
+
status.summary = event.waiting ? "Waiting on a prompt" : "Running…";
|
|
1818
|
+
status.lastActivityAt = now;
|
|
1819
|
+
void writeForegroundState(row, status);
|
|
1820
|
+
return true;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1812
1823
|
if (event.type === "input" || event.type === "before_agent_start" || event.type === "agent_start") {
|
|
1813
1824
|
status.semanticState = "working";
|
|
1814
1825
|
status.processState = "alive";
|
|
@@ -2293,6 +2304,22 @@ export function createService(opts) {
|
|
|
2293
2304
|
return { ok: true };
|
|
2294
2305
|
},
|
|
2295
2306
|
|
|
2307
|
+
/**
|
|
2308
|
+
* Gives these rows this order, numbered from the lowest they had: rows created in the same
|
|
2309
|
+
* millisecond have no order between them to swap.
|
|
2310
|
+
* @param {string[]} viewIds
|
|
2311
|
+
*/
|
|
2312
|
+
setRowOrder(viewIds) {
|
|
2313
|
+
const metas = viewIds.map((id) => loadRow(root, id)?.meta);
|
|
2314
|
+
if (metas.some((meta) => !meta)) return { ok: false, error: "Unknown session" };
|
|
2315
|
+
const base = Math.min(...metas.map((meta) => meta.order ?? meta.createdAt ?? 0));
|
|
2316
|
+
metas.forEach((meta, index) => {
|
|
2317
|
+
meta.order = base + index;
|
|
2318
|
+
writeMeta(root, meta);
|
|
2319
|
+
});
|
|
2320
|
+
return { ok: true };
|
|
2321
|
+
},
|
|
2322
|
+
|
|
2296
2323
|
/** @param {string} viewId @param {string} name */
|
|
2297
2324
|
rename(viewId, name) {
|
|
2298
2325
|
const clean = String(name || "").trim();
|