@gavin-hu/my-pi-agent 0.16.0 → 0.18.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.
- package/README.md +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/sessions` board: a docked list over the bus, with three scopes.
|
|
3
|
+
*
|
|
4
|
+
* `Tab` cycles Sessions → Inbox → Claims. Sessions and claims come from the bus
|
|
5
|
+
* snapshot, mail from a read-only peek, so the board announces nothing and
|
|
6
|
+
* consumes nothing. The rows, summaries, and focus blocks are in `rows.ts`; this
|
|
7
|
+
* module owns the screen, the scope it shows, and the two verbs that change this
|
|
8
|
+
* session's own state.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { matchesKey, Key } from "@earendil-works/pi-tui";
|
|
13
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
14
|
+
import { formatDuration, sanitizeLine } from "../../lib/format.ts";
|
|
15
|
+
import type { ScreenHint, ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
16
|
+
import { type DetailContents, DockListScreen } from "../../lib/tui/dock-screen.ts";
|
|
17
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
18
|
+
import { keyValueRow } from "../../lib/tui/rows.ts";
|
|
19
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
20
|
+
import { shortId } from "./format.ts";
|
|
21
|
+
import {
|
|
22
|
+
type SessionRow,
|
|
23
|
+
claimRowLine,
|
|
24
|
+
count,
|
|
25
|
+
messageRowLine,
|
|
26
|
+
sessionDetailBlock,
|
|
27
|
+
sessionRowLine,
|
|
28
|
+
sessionRows,
|
|
29
|
+
summaryCells,
|
|
30
|
+
} from "./rows.ts";
|
|
31
|
+
import type { BoardSnapshot, BusMessage, ClaimRecord } from "./types.ts";
|
|
32
|
+
|
|
33
|
+
/** The scopes `Tab` cycles through. */
|
|
34
|
+
export type BoardScope = "sessions" | "inbox" | "claims";
|
|
35
|
+
|
|
36
|
+
/** One row of the board, whichever scope is showing. */
|
|
37
|
+
type BoardRow =
|
|
38
|
+
| { kind: "session"; row: SessionRow }
|
|
39
|
+
| { kind: "message"; message: BusMessage }
|
|
40
|
+
| { kind: "claim"; claim: ClaimRecord };
|
|
41
|
+
|
|
42
|
+
const SCOPE_ORDER: readonly BoardScope[] = ["sessions", "inbox", "claims"];
|
|
43
|
+
|
|
44
|
+
const SCOPE_TITLE: Readonly<Record<BoardScope, string>> = {
|
|
45
|
+
sessions: "Sessions",
|
|
46
|
+
inbox: "Inbox",
|
|
47
|
+
claims: "Claims",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SCOPE_EMPTY: Readonly<Record<BoardScope, string>> = {
|
|
51
|
+
sessions: "No sessions on this bus.",
|
|
52
|
+
inbox: "No unread mail.",
|
|
53
|
+
claims: "No claims.",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** A relative age for a message or a claim, e.g. `2.0s ago`, `5m00s ago`. */
|
|
57
|
+
function ago(at: number, now: number): string {
|
|
58
|
+
return `${formatDuration(Math.max(0, now - at))} ago`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A message body as prose: one paragraph per source line, control characters gone. */
|
|
62
|
+
function messageProse(message: BusMessage): string {
|
|
63
|
+
return message.text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => sanitizeLine(line))
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The paths a message mentions, or an empty string. */
|
|
70
|
+
function messagePaths(message: BusMessage): string {
|
|
71
|
+
return message.paths?.length ? message.paths.map((path) => sanitizeLine(path)).join(", ") : "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The pane's width-dependent facts: identity, above the claim body. */
|
|
75
|
+
function paneFacts(theme: Theme, row: SessionRow, now: number, width: number): string[] {
|
|
76
|
+
const labelWidth = 10;
|
|
77
|
+
return [
|
|
78
|
+
keyValueRow(theme, width, { label: "mode", labelWidth, value: sanitizeLine(row.session.mode) }),
|
|
79
|
+
keyValueRow(theme, width, { label: "id", labelWidth, value: shortId(row.session.id) }),
|
|
80
|
+
keyValueRow(theme, width, {
|
|
81
|
+
label: "last seen",
|
|
82
|
+
labelWidth,
|
|
83
|
+
value: ago(row.session.lastSeen, now),
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The pane body: every claim this session holds, in path order. */
|
|
89
|
+
function paneLines(row: SessionRow, now: number): string[] {
|
|
90
|
+
return row.claims.map((claim) => `${sanitizeLine(claim.path)}${SEPARATORS.item}held ${ago(claim.at, now)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A cheap signature of what the board draws, so a poll repaints only on a change. */
|
|
94
|
+
function boardKey(board: BoardSnapshot | undefined): string {
|
|
95
|
+
if (!board) return "";
|
|
96
|
+
return [
|
|
97
|
+
...board.status.sessions.map(
|
|
98
|
+
(session) =>
|
|
99
|
+
`${session.id}:${session.label}:${session.unread}:${session.lastSeen}:${session.mode}:${session.cwd}`,
|
|
100
|
+
),
|
|
101
|
+
...board.status.claims.map((claim) => `${claim.path}:${claim.owner}:${claim.ownerLabel}:${claim.at}`),
|
|
102
|
+
board.status.watching ? `${board.status.watching.armed}:${board.status.watching.wake}` : "",
|
|
103
|
+
...board.messages.map((message) => `${message.id}:${message.at}`),
|
|
104
|
+
].join("|");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SessionListCallbacks {
|
|
108
|
+
/** Arm (or disarm) this session's inbox watch, returning the line to show. */
|
|
109
|
+
toggleWatch(armed: boolean): Promise<string>;
|
|
110
|
+
/** Release one of this session's claims, returning the line to show. */
|
|
111
|
+
releaseClaim(path: string): Promise<string>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface SessionListOptions {
|
|
115
|
+
/** The latest board, or undefined before the first read. */
|
|
116
|
+
latest(): BoardSnapshot | undefined;
|
|
117
|
+
theme: Theme;
|
|
118
|
+
callbacks: SessionListCallbacks;
|
|
119
|
+
onClose(): void;
|
|
120
|
+
requestRender(): void;
|
|
121
|
+
now?: () => number;
|
|
122
|
+
viewportRowsSource?: ViewportRowsSource;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The `/sessions` board.
|
|
127
|
+
*
|
|
128
|
+
* The rows are derived from a board the command's poll supplies, so the screen
|
|
129
|
+
* never reads the bus itself; `liveDetail` keeps an open pane in step with that
|
|
130
|
+
* board, so a peer that expires or a message that is consumed closes its own pane.
|
|
131
|
+
* The focus is anchored by a scope-prefixed id, so switching scopes lands on the
|
|
132
|
+
* first row of the new list instead of a row that is no longer there.
|
|
133
|
+
*/
|
|
134
|
+
export class SessionListComponent extends DockListScreen<BoardRow> {
|
|
135
|
+
private board: BoardSnapshot | undefined;
|
|
136
|
+
private scope: BoardScope = "sessions";
|
|
137
|
+
private readonly callbacks: SessionListCallbacks;
|
|
138
|
+
private readonly now: () => number;
|
|
139
|
+
|
|
140
|
+
constructor(options: SessionListOptions) {
|
|
141
|
+
super({
|
|
142
|
+
theme: options.theme,
|
|
143
|
+
title: "Sessions",
|
|
144
|
+
detailTitle: "Details",
|
|
145
|
+
emptyList: "No sessions on this bus.",
|
|
146
|
+
emptyDetail: "Nothing to show.",
|
|
147
|
+
requestRender: options.requestRender,
|
|
148
|
+
onClose: options.onClose,
|
|
149
|
+
viewportRowsSource: options.viewportRowsSource,
|
|
150
|
+
});
|
|
151
|
+
this.callbacks = options.callbacks;
|
|
152
|
+
this.now = options.now ?? Date.now;
|
|
153
|
+
this.board = options.latest();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Replace the board; a poll that changed nothing does not repaint. */
|
|
157
|
+
setBoard(board: BoardSnapshot | undefined): void {
|
|
158
|
+
const before = boardKey(this.board);
|
|
159
|
+
this.board = board;
|
|
160
|
+
if (boardKey(board) !== before) this.options.requestRender();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The scope currently listed, for a test or a caller that wants to label it. */
|
|
164
|
+
get currentScope(): BoardScope {
|
|
165
|
+
return this.scope;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
protected headerLabel(): string {
|
|
169
|
+
return SCOPE_TITLE[this.scope];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
protected emptyLabel(): string {
|
|
173
|
+
return SCOPE_EMPTY[this.scope];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
protected rows(): readonly BoardRow[] {
|
|
177
|
+
switch (this.scope) {
|
|
178
|
+
case "sessions":
|
|
179
|
+
return sessionRows(this.board?.status).map((row) => ({ kind: "session", row }));
|
|
180
|
+
case "inbox":
|
|
181
|
+
return (this.board?.messages ?? []).map((message) => ({ kind: "message", message }));
|
|
182
|
+
case "claims":
|
|
183
|
+
return (this.board?.status.claims ?? []).map((claim) => ({ kind: "claim", claim }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected idOf(row: BoardRow): string {
|
|
188
|
+
switch (row.kind) {
|
|
189
|
+
case "session":
|
|
190
|
+
return `s:${row.row.session.id}`;
|
|
191
|
+
case "message":
|
|
192
|
+
return `m:${row.message.id}`;
|
|
193
|
+
case "claim":
|
|
194
|
+
return `c:${row.claim.path}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
protected rowLine(row: BoardRow, width: number, selected: boolean): string {
|
|
199
|
+
switch (row.kind) {
|
|
200
|
+
case "session":
|
|
201
|
+
return sessionRowLine(row.row, this.theme, width, { selected });
|
|
202
|
+
case "message":
|
|
203
|
+
return messageRowLine(row.message, this.theme, width, { selected });
|
|
204
|
+
case "claim":
|
|
205
|
+
return claimRowLine(row.claim, this.theme, width, {
|
|
206
|
+
selected,
|
|
207
|
+
self: row.claim.owner === this.me(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected detailBlock(row: BoardRow): string[] {
|
|
213
|
+
switch (row.kind) {
|
|
214
|
+
case "session":
|
|
215
|
+
return sessionDetailBlock(row.row, this.now());
|
|
216
|
+
case "message": {
|
|
217
|
+
const lines = [`received: ${ago(row.message.at, this.now())}`];
|
|
218
|
+
const paths = messagePaths(row.message);
|
|
219
|
+
if (paths) lines.push(`paths: ${paths}`);
|
|
220
|
+
return lines;
|
|
221
|
+
}
|
|
222
|
+
case "claim":
|
|
223
|
+
return [`held ${ago(row.claim.at, this.now())}`];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
protected summaryCells(rows: readonly BoardRow[]): string[] {
|
|
228
|
+
const watching = this.board?.status.watching;
|
|
229
|
+
switch (this.scope) {
|
|
230
|
+
case "sessions": {
|
|
231
|
+
const sessions = rows.flatMap((row) => (row.kind === "session" ? [row.row] : []));
|
|
232
|
+
return summaryCells(this.theme, sessions, watching);
|
|
233
|
+
}
|
|
234
|
+
case "inbox": {
|
|
235
|
+
const cells = [paint(this.theme, "meta", count(rows.length, "unread message"))];
|
|
236
|
+
if (watching) cells.push(this.watchCell(watching));
|
|
237
|
+
return cells;
|
|
238
|
+
}
|
|
239
|
+
case "claims": {
|
|
240
|
+
const own = rows.flatMap((row) => (row.kind === "claim" && row.claim.owner === this.me() ? [row] : []));
|
|
241
|
+
const cells = [paint(this.theme, "meta", count(rows.length, "claim"))];
|
|
242
|
+
if (own.length > 0) cells.push(paint(this.theme, "label", `${own.length} yours`));
|
|
243
|
+
return cells;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
protected listHints(): ScreenHint[] {
|
|
249
|
+
const tab = (scope: BoardScope): ScreenHint => ({ text: `Tab ${SCOPE_TITLE[scope].toLowerCase()}` });
|
|
250
|
+
const enter = (verb: string): ScreenHint => ({ text: `Enter ${verb}`, color: "accent" });
|
|
251
|
+
const hints: ScreenHint[] = ["Esc", tab(this.nextScope())];
|
|
252
|
+
switch (this.scope) {
|
|
253
|
+
case "sessions":
|
|
254
|
+
hints.push(enter("open"), this.watchHint(), "↑/↓", "PgUp/PgDn");
|
|
255
|
+
break;
|
|
256
|
+
case "inbox":
|
|
257
|
+
hints.push(enter("read"), this.watchHint(), "↑/↓", "PgUp/PgDn");
|
|
258
|
+
break;
|
|
259
|
+
case "claims":
|
|
260
|
+
hints.push(enter("open"), { text: "x release", color: "warning" }, "↑/↓", "PgUp/PgDn");
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
return hints;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
protected detailHints(row: BoardRow | undefined): ScreenHint[] {
|
|
267
|
+
if (row?.kind === "claim" && row.claim.owner === this.me()) {
|
|
268
|
+
return ["Esc back", { text: "x release", color: "warning" }, "↑/↓ scroll", "g/G ends"];
|
|
269
|
+
}
|
|
270
|
+
return ["Esc back", "↑/↓ scroll", "g/G ends", "PgUp/PgDn"];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
protected detailFor(row: BoardRow): DetailContents {
|
|
274
|
+
switch (row.kind) {
|
|
275
|
+
case "session":
|
|
276
|
+
return {
|
|
277
|
+
title: `${sanitizeLine(row.row.session.label)}${row.row.self ? " · you" : ""}`,
|
|
278
|
+
subtitle: sanitizeLine(row.row.session.cwd),
|
|
279
|
+
lines: paneLines(row.row, this.now()),
|
|
280
|
+
tail: false,
|
|
281
|
+
facts: (width) => paneFacts(this.theme, row.row, this.now(), width),
|
|
282
|
+
};
|
|
283
|
+
case "message": {
|
|
284
|
+
const paths = messagePaths(row.message);
|
|
285
|
+
return {
|
|
286
|
+
title: `${sanitizeLine(row.message.fromLabel)}${SEPARATORS.item}${sanitizeLine(row.message.kind)}`,
|
|
287
|
+
subtitle: `${ago(row.message.at, this.now())}${paths ? `${SEPARATORS.item}paths: ${paths}` : ""}`,
|
|
288
|
+
text: messageProse(row.message),
|
|
289
|
+
tail: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
case "claim": {
|
|
293
|
+
// A claim's pane is the session that holds it: identity plus everything it holds.
|
|
294
|
+
const owner = this.ownerRow(row.claim.owner);
|
|
295
|
+
return {
|
|
296
|
+
title: sanitizeLine(row.claim.ownerLabel),
|
|
297
|
+
subtitle: `holds ${sanitizeLine(row.claim.path)}${SEPARATORS.item}held ${ago(row.claim.at, this.now())}`,
|
|
298
|
+
lines: owner ? paneLines(owner, this.now()) : [],
|
|
299
|
+
tail: false,
|
|
300
|
+
...(owner ? { facts: (width: number) => paneFacts(this.theme, owner, this.now(), width) } : {}),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected detailById(id: string): { lines: string[] } | undefined {
|
|
307
|
+
const row = this.rows().find((candidate) => this.idOf(candidate) === id);
|
|
308
|
+
if (!row) return undefined;
|
|
309
|
+
switch (row.kind) {
|
|
310
|
+
case "session":
|
|
311
|
+
return { lines: paneLines(row.row, this.now()) };
|
|
312
|
+
case "claim": {
|
|
313
|
+
const owner = this.ownerRow(row.claim.owner);
|
|
314
|
+
return { lines: owner ? paneLines(owner, this.now()) : [] };
|
|
315
|
+
}
|
|
316
|
+
case "message":
|
|
317
|
+
// The pane's body is prose, which the base owns; its presence is all this answers.
|
|
318
|
+
return { lines: [] };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
protected liveDetail(): boolean {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
protected handleVerb(key: string, focused: BoardRow | undefined): void {
|
|
327
|
+
if (matchesKey(key, Key.tab)) {
|
|
328
|
+
this.cycleScope();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (key === "w") {
|
|
332
|
+
this.toggleWatch();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (key === "x" && focused?.kind === "claim") this.release(focused.claim);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
protected handleDetailVerb(key: string, row: BoardRow): boolean {
|
|
339
|
+
if (key !== "x" || row.kind !== "claim") return false;
|
|
340
|
+
this.release(row.claim);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private me(): string {
|
|
345
|
+
return this.board?.status.me.id ?? "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The row for the session with `id`, when it is still on the board. */
|
|
349
|
+
private ownerRow(id: string): SessionRow | undefined {
|
|
350
|
+
return sessionRows(this.board?.status).find((row) => row.session.id === id);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private nextScope(): BoardScope {
|
|
354
|
+
const index = SCOPE_ORDER.indexOf(this.scope);
|
|
355
|
+
return SCOPE_ORDER[(index + 1) % SCOPE_ORDER.length] ?? "sessions";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private cycleScope(): void {
|
|
359
|
+
this.scope = this.nextScope();
|
|
360
|
+
this.options.requestRender();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private watchCell(watching: { armed: boolean; wake: boolean }): string {
|
|
364
|
+
return paint(this.theme, "ok", watching.wake ? "watch wake" : "watch on");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private watchHint(): ScreenHint {
|
|
368
|
+
return this.board?.status.watching
|
|
369
|
+
? { text: "w watch off", color: "warning" }
|
|
370
|
+
: { text: "w watch on", color: "accent" };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private toggleWatch(): void {
|
|
374
|
+
const armed = this.board?.status.watching !== undefined;
|
|
375
|
+
void this.callbacks
|
|
376
|
+
.toggleWatch(armed)
|
|
377
|
+
.then((line) => this.notify(line))
|
|
378
|
+
.catch((error) => this.notify(messageOf(error)));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private release(claim: ClaimRecord): void {
|
|
382
|
+
const path = sanitizeLine(claim.path);
|
|
383
|
+
if (claim.owner !== this.me()) {
|
|
384
|
+
this.notify(`${path} is ${sanitizeLine(claim.ownerLabel)}'s claim; not released.`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.confirm(`Release ${path}? y/N`, () => {
|
|
388
|
+
void this.callbacks
|
|
389
|
+
.releaseClaim(claim.path)
|
|
390
|
+
.then((line) => this.notify(line))
|
|
391
|
+
.catch((error) => this.notify(messageOf(error)));
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shapes for the session bus.
|
|
3
|
+
*
|
|
4
|
+
* These describe the files and the tool result only; the file I/O lives in
|
|
5
|
+
* `store.ts` and the lifecycle in `runtime.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MessageKind, SessionMode } from "./protocol.ts";
|
|
9
|
+
|
|
10
|
+
/** One registered Pi session. */
|
|
11
|
+
export interface SessionRecord {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
pid: number;
|
|
15
|
+
/** Working directory of the session; the worktree root when there is one. */
|
|
16
|
+
cwd: string;
|
|
17
|
+
/** Repository root the session belongs to, for relative claims. */
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
/** The mode the session was started in. */
|
|
20
|
+
mode: SessionMode;
|
|
21
|
+
startedAt: number;
|
|
22
|
+
lastSeen: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One message waiting in a recipient's inbox. */
|
|
26
|
+
export interface BusMessage {
|
|
27
|
+
id: string;
|
|
28
|
+
from: string;
|
|
29
|
+
fromLabel: string;
|
|
30
|
+
/** Recipient session id. */
|
|
31
|
+
to: string;
|
|
32
|
+
toLabel: string;
|
|
33
|
+
kind: MessageKind;
|
|
34
|
+
text: string;
|
|
35
|
+
paths?: string[];
|
|
36
|
+
at: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One repository-relative claim. */
|
|
40
|
+
export interface ClaimRecord {
|
|
41
|
+
path: string;
|
|
42
|
+
owner: string;
|
|
43
|
+
ownerLabel: string;
|
|
44
|
+
at: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What `session status` returns. */
|
|
48
|
+
export interface SessionStatus {
|
|
49
|
+
me: { id: string; label: string };
|
|
50
|
+
sessions: Array<SessionRecord & { unread: number }>;
|
|
51
|
+
claims: ClaimRecord[];
|
|
52
|
+
/** The newest split in this repository, and who holds each unit, on an explicit status call. */
|
|
53
|
+
units?: {
|
|
54
|
+
team: string;
|
|
55
|
+
joined: number;
|
|
56
|
+
total: number;
|
|
57
|
+
rows: Array<{ unit: string; lead?: boolean; holder?: string }>;
|
|
58
|
+
};
|
|
59
|
+
/** Present only when this session armed its inbox watch. */
|
|
60
|
+
watching?: { armed: boolean; wake: boolean };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A conflict returned by `session claim`. */
|
|
64
|
+
export interface ClaimConflict {
|
|
65
|
+
path: string;
|
|
66
|
+
owner: string;
|
|
67
|
+
ownerLabel: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What the board reads in one poll: the bus as it stands, and this session's
|
|
72
|
+
* unread mail. Peeking never consumes, so the notice and `inbox` are unaffected.
|
|
73
|
+
*/
|
|
74
|
+
export interface BoardSnapshot {
|
|
75
|
+
status: SessionStatus;
|
|
76
|
+
messages: BusMessage[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Structured result carried in the `session` tool's details. */
|
|
80
|
+
export interface SessionDetails {
|
|
81
|
+
action: "status" | "send" | "inbox" | "claim" | "release" | "join" | "leave" | "watch" | "name";
|
|
82
|
+
/** With join/leave: the split and the unit, so a resumed transcript still says which. */
|
|
83
|
+
team?: string;
|
|
84
|
+
unit?: string;
|
|
85
|
+
status?: SessionStatus;
|
|
86
|
+
delivered?: string[];
|
|
87
|
+
messageId?: string;
|
|
88
|
+
messages?: BusMessage[];
|
|
89
|
+
claimed?: string[];
|
|
90
|
+
conflicts?: ClaimConflict[];
|
|
91
|
+
released?: string[];
|
|
92
|
+
notOwned?: string[];
|
|
93
|
+
/** Whether the inbox watch is on, and whether it raises a turn. */
|
|
94
|
+
watching?: { armed: boolean; wake: boolean };
|
|
95
|
+
/** With name: the label the bus settled on, which is what this session is called. */
|
|
96
|
+
label?: string;
|
|
97
|
+
error?: string;
|
|
98
|
+
}
|
|
@@ -210,7 +210,7 @@ for another agent (typically `worker`) to execute, and it works headlessly. It
|
|
|
210
210
|
returns the plan as text and writes nothing. For **interactive** planning where
|
|
211
211
|
the user reviews and approves a saved plan, use the plan-mode extension instead
|
|
212
212
|
(`enter_plan_mode` / `write_plan` / `exit_plan_mode`), which operates on the
|
|
213
|
-
main session and persists to
|
|
213
|
+
main session and persists to the agent directory. Plan mode keeps `subagent` available
|
|
214
214
|
for exploration but forces it read-only, so delegated recon is safe mid-plan.
|
|
215
215
|
|
|
216
216
|
## External agents
|
|
@@ -275,9 +275,18 @@ child's system prompt for behavior; the tool list is the enforcement.
|
|
|
275
275
|
→ grep /retry/ in ~/src
|
|
276
276
|
→ read ~/src/http.ts:40-120
|
|
277
277
|
Retries are configured in http.ts:64...
|
|
278
|
-
|
|
278
|
+
|
|
279
|
+
2 turns ↑12.4k ↓1.1k R8.0k ctx:18.2k claude-sonnet-4-5
|
|
279
280
|
```
|
|
280
281
|
|
|
282
|
+
The footnote is the same in every mode: a blank line, the faint usage row, then —
|
|
283
|
+
when the run has more to show — the expand affordance, so the hint is always the
|
|
284
|
+
block's last line. The row carries turns, token counts, context size, and the
|
|
285
|
+
model — never money. A price needs a currency and a tariff, and those belong to
|
|
286
|
+
the status bar, which shows every child run as its own `↳` roll-up priced in the
|
|
287
|
+
configured currency (see [Non-goals](#non-goals)). A parallel or chain `Total:`
|
|
288
|
+
row sums the counters, and names a model only when every task ran the same one.
|
|
289
|
+
|
|
281
290
|
**Expanded** (the `app.tools.expand` key, `ctrl+o` by default): the full task,
|
|
282
291
|
every intermediate assistant message and tool call, the final output rendered as
|
|
283
292
|
Markdown, and per-step/per-task usage with a total.
|
|
@@ -296,7 +305,9 @@ the first failing step also reports `stopped at step N`. Running tasks show an
|
|
|
296
305
|
elapsed-time label on the shared 1s repaint timer (the package's common
|
|
297
306
|
duration format, e.g. `12.0s`), and a finished run keeps its duration. A result
|
|
298
307
|
that hit tool errors appends `N tool errors` to its
|
|
299
|
-
usage line (and to the multi `Total:`).
|
|
308
|
+
usage line (and to the multi `Total:`). A usage payload the child reports with a
|
|
309
|
+
non-number (a string, `NaN`) contributes nothing, so a malformed event cannot
|
|
310
|
+
corrupt the row.
|
|
300
311
|
|
|
301
312
|
A chain step's expanded `Task` section shows the step's original instruction,
|
|
302
313
|
with `{previous}` left in place, rather than the substituted prior output that
|
|
@@ -317,15 +328,17 @@ step's `Output`.
|
|
|
317
328
|
details; keeping it on disk is opt-in, parent-written, and bounded by age and
|
|
318
329
|
count. It is an audit and handoff record, not a resume point: replaying a run
|
|
319
330
|
whose writes already landed is not attempted.
|
|
320
|
-
- **No cost attribution to the parent session.** A subagent's
|
|
331
|
+
- **No cost attribution to the parent session.** A subagent's token counts are
|
|
321
332
|
captured in the tool result and shown in the transcript, but they belong to a
|
|
322
|
-
separate process and are not added to the dispatching session's totals.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
separate process and are not added to the dispatching session's totals. The
|
|
334
|
+
child's own dollar cost stays in the tool details (and in a run's `meta.json`)
|
|
335
|
+
and is never printed in the transcript: only the status bar prices a delegated
|
|
336
|
+
run, in the configured currency and tariff. Each finished run publishes
|
|
337
|
+
`my-pi-agent/subagent-finished` (`SubagentFinishedEvent` in `lib/events.ts`)
|
|
338
|
+
and bytefly's footer shows the session's child runs as their own `↳` roll-up,
|
|
339
|
+
priced separately from the session total (the last 500 runs). The roll-up is
|
|
340
|
+
per process: a resumed session starts from zero, and a run that never produced
|
|
341
|
+
an assistant message publishes nothing.
|
|
329
342
|
|
|
330
343
|
## Limitations
|
|
331
344
|
|
|
@@ -421,7 +434,7 @@ step's `Output`.
|
|
|
421
434
|
| `agents.ts` | Built-in agent definitions, external markdown discovery, and pool resolution. |
|
|
422
435
|
| `schema.ts` | TypeBox params, limits, and `resolveMode`. |
|
|
423
436
|
| `invocation.ts` | `pi` subprocess invocation and argument construction. |
|
|
424
|
-
| `stream.ts` | JSON-event parsing, usage accounting, output helpers (pure). |
|
|
437
|
+
| `stream.ts` | JSON-event parsing, guarded usage accounting, output helpers (pure). |
|
|
425
438
|
| `transcripts.ts` | Per-run transcript buffer, run ids, the store directory, and pruning. |
|
|
426
439
|
| `config.ts` | The `subagent` section of `bytefly.json`: deadline, transcripts, retention. |
|
|
427
440
|
| `run.ts` | Runs one subagent process and captures its result (including the background abort-as-result path). |
|
|
@@ -429,6 +442,6 @@ step's `Output`.
|
|
|
429
442
|
| `background-tui.ts` | The `/agents` board: a `DockListScreen` with a live output pane and width-aware facts. |
|
|
430
443
|
| `commands.ts` | The `/agents` command. |
|
|
431
444
|
| `ui.ts` | The running-count footer chip and finish notices. |
|
|
432
|
-
| `format.ts` | Token/usage/tool-call formatting (pure). |
|
|
445
|
+
| `format.ts` | Token/usage/total line and tool-call formatting (pure, money-free). |
|
|
433
446
|
| `render.ts` | `renderCall` / `renderResult` transcript rendering. |
|
|
434
447
|
| `types.ts` | Shared result and process types. |
|