pi-pier 0.2.7 → 0.3.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/README.md +26 -12
- package/package.json +8 -3
- package/schemas/efficiency-config.schema.json +77 -0
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +11 -0
- package/src/ansi-text.ts +134 -0
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +139 -88
- package/src/bootstrap.ts +13 -14
- package/src/compact-coordinator.ts +471 -0
- package/src/compact-economics-core.ts +328 -0
- package/src/config-catalog-core.ts +332 -0
- package/src/config-command.ts +388 -0
- package/src/dashboard-command.ts +108 -0
- package/src/efficiency-config-core.ts +464 -0
- package/src/efficiency-store.ts +299 -0
- package/src/focus-poller.ts +223 -0
- package/src/gc-core.ts +77 -18
- package/src/herdr-client.ts +284 -154
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +29 -16
- package/src/index-master.ts +29 -19
- package/src/index-notices.ts +35 -11
- package/src/index-pipe.ts +17 -7
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +7 -9
- package/src/index.ts +425 -388
- package/src/jev-client.ts +141 -0
- package/src/jev-core.ts +431 -0
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +98 -37
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +200 -0
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +100 -0
- package/src/pipe-channel.ts +65 -52
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +12 -12
- package/src/plugins/heat-plan.ts +158 -0
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +237 -123
- package/src/{core → plugins}/todo.ts +75 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +322 -0
- package/src/reducer-invoker.ts +419 -0
- package/src/renderers.ts +187 -0
- package/src/role-loader.ts +35 -29
- package/src/role-manifest.ts +23 -32
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -43
- package/src/session-tail.ts +102 -23
- package/src/settle-wake-core.ts +55 -22
- package/src/slim-frame.ts +29 -124
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +24 -30
- package/src/subagent-core.ts +256 -119
- package/src/subagent-gc.ts +131 -69
- package/src/subagent-poller.ts +401 -12
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +651 -28
- package/src/swallow.ts +65 -0
- package/src/terminal-core.ts +160 -69
- package/src/todo-core.ts +62 -113
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +30 -48
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +27 -35
- package/src/tool-error.ts +19 -0
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +55 -25
- package/src/core/subagent.ts +0 -839
- package/src/git-adapter.ts +0 -104
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -110
- package/src/subagent-poll-loop.ts +0 -285
- package/src/subagent-port.ts +0 -27
- package/src/subagent-scope.ts +0 -39
- package/src/subagent-session-io.ts +0 -147
package/src/herdr-client.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal herdr socket API client used inside the pi extension.
|
|
3
3
|
*
|
|
4
|
-
* Validated against herdr 0.
|
|
4
|
+
* Validated against herdr 0.9.1 (protocol 22; see WIRE.md). The request shapes pier builds are pinned
|
|
5
|
+
* to herdr's own `api schema` by test/herdr-client.test.ts — regenerate test/fixtures/herdr-contract.json
|
|
6
|
+
* and bump HERDR_PROTOCOL_EXPECTED when the server moves on:
|
|
5
7
|
* - Transport is NDJSON: request {id, method, params} → response {id, result} | {id, error:{code,message}};
|
|
6
8
|
* - **Control requests use one connection per request**; the server closes it after replying;
|
|
7
9
|
* - Windows targets are named pipes whose name is the complete socket_path with a \\.\pipe\ prefix;
|
|
8
|
-
* - pane.report_agent reports {pane_id, source, agent, state, message
|
|
9
|
-
*
|
|
10
|
-
* - pane.report_metadata reports {pane_id, source, title?, state_labels?,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - agent.wait {target, until[], timeout_ms?} → the matching agent, or an error on timeout;
|
|
14
|
-
* - agent.send_keys {target, keys[]} / pane.send_text {pane_id, text}.
|
|
10
|
+
* - pane.report_agent reports {pane_id, source, agent, state, message?} where state ∈
|
|
11
|
+
* idle|working|blocked|unknown ('done' is server-derived and is not reported by the client);
|
|
12
|
+
* - pane.report_metadata reports {pane_id, source, title?, state_labels?, clear_*, tokens?, ttl_ms?}
|
|
13
|
+
* (ttl_ms is capped at 24 h by the server; the first report of a session also clears legacy
|
|
14
|
+
* pi-herdr chunk tokens to null).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* (waitSettled marker polling, readPaneText parsing, onAgentState long-lived subscriptions)
|
|
19
|
-
* are replaced by session JSONL results (session-tail.ts) and server-side agent.wait.
|
|
20
|
-
*
|
|
21
|
-
* Design (DESIGN.md §4.1): without herdr, fall back to Noop so pi remains independent;
|
|
22
|
-
* failures in reporting calls stay silent and never affect the main pi flow.
|
|
16
|
+
* Reporting failures stay silent and never affect the main pi flow; without herdr the Noop client
|
|
17
|
+
* keeps pi independent (DESIGN.md §4.1).
|
|
23
18
|
*/
|
|
24
19
|
import { REPORT_AGENT_SOURCE, type TodoItem } from './vocab.ts';
|
|
25
20
|
import { BLOCKED_LABEL_KEY, SIDEBAR_ASK_TOKEN, formatBlockedLabel, formatPaneTitle, sidebarTodoTokens, staleTokenClearance } from './pane-title.ts';
|
|
@@ -28,7 +23,6 @@ import * as net from 'node:net';
|
|
|
28
23
|
|
|
29
24
|
/* ── Types ──────────────────────────────────────────────────────────── */
|
|
30
25
|
|
|
31
|
-
/** Herdr agent semantic state; schema has five observed states and 'done' is server-derived. */
|
|
32
26
|
export type HerdrAgentState = 'idle' | 'working' | 'blocked' | 'done' | 'unknown';
|
|
33
27
|
|
|
34
28
|
export interface HerdrEnv {
|
|
@@ -49,9 +43,54 @@ export interface AgentInfo {
|
|
|
49
43
|
session: string | null;
|
|
50
44
|
stateLabels: Record<string, string>;
|
|
51
45
|
tokens: Record<string, string | null>;
|
|
46
|
+
/** Herdr 0.9.1: foreground cwd of the process currently controlling the PTY. */
|
|
47
|
+
foregroundCwd?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PaneListItem {
|
|
51
|
+
paneId: string;
|
|
52
|
+
tabId: string;
|
|
53
|
+
workspaceId: string;
|
|
54
|
+
agentStatus: string;
|
|
55
|
+
foregroundCwd?: string | null;
|
|
56
|
+
terminalTitle?: string | null;
|
|
57
|
+
terminalTitleStripped?: string | null;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
export interface PaneLayoutCell {
|
|
61
|
+
paneId: string;
|
|
62
|
+
x: number;
|
|
63
|
+
y: number;
|
|
64
|
+
w: number;
|
|
65
|
+
h: number;
|
|
66
|
+
focused: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PaneLayoutSnapshot {
|
|
70
|
+
tabId: string | null;
|
|
71
|
+
zoomed: boolean;
|
|
72
|
+
focusedPaneId: string | null;
|
|
73
|
+
panes: PaneLayoutCell[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface OpenPluginPaneOptions {
|
|
77
|
+
pluginId: string;
|
|
78
|
+
entrypoint: string;
|
|
79
|
+
placement?: 'popup' | 'overlay' | 'split' | 'tab' | 'zoomed';
|
|
80
|
+
width?: string;
|
|
81
|
+
height?: string;
|
|
82
|
+
focus?: boolean;
|
|
83
|
+
cwd?: string;
|
|
84
|
+
env?: Record<string, string>;
|
|
85
|
+
targetPaneId?: string;
|
|
86
|
+
workspaceId?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type OpenPluginPaneResult =
|
|
90
|
+
| { mode: 'popup'; ok: true }
|
|
91
|
+
| { mode: 'pane'; paneId: string }
|
|
92
|
+
| { mode: 'fallback_tab'; tabId?: string; paneId?: string };
|
|
93
|
+
|
|
55
94
|
export interface TabInfo {
|
|
56
95
|
tabId: string;
|
|
57
96
|
workspaceId: string;
|
|
@@ -73,67 +112,83 @@ export function detectHerdrEnv(env: NodeJS.ProcessEnv = process.env): HerdrEnv |
|
|
|
73
112
|
};
|
|
74
113
|
}
|
|
75
114
|
|
|
76
|
-
/** Resolve herdr socket target (Windows named-pipe prefix vs Unix path). */
|
|
77
115
|
export function herdrSocketTarget(socketPath: string, platform: NodeJS.Platform = process.platform): string {
|
|
78
116
|
if (platform !== 'win32') return socketPath;
|
|
79
117
|
return socketPath.startsWith('\\\\.\\pipe\\') ? socketPath : `\\\\.\\pipe\\${socketPath}`;
|
|
80
118
|
}
|
|
81
119
|
|
|
120
|
+
/** B5: herdr configured but unreachable (stopped server, stale HERDR_SOCKET_PATH) — tool layers append
|
|
121
|
+
* this sentence because a raw errno tells the model nothing. Null when the failure is not transport. */
|
|
122
|
+
export function herdrUnavailableHint(err: unknown): string | null {
|
|
123
|
+
const msg = String((err as { message?: unknown })?.message ?? err);
|
|
124
|
+
if (!/ENOENT|ECONNREFUSED|ECONNRESET|EPIPE|ENOTCONN|not connected|socket|connect |timeout/i.test(msg)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return `herdr unreachable (${msg}) — check that the herdr server is running and HERDR_SOCKET_PATH matches its socket`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Wire protocol this client is written against (herdr 0.9.1). See test/fixtures/herdr-contract.json. */
|
|
131
|
+
export const HERDR_PROTOCOL_EXPECTED = 22;
|
|
132
|
+
|
|
133
|
+
export type WaitForOutputResult =
|
|
134
|
+
| { matched: true }
|
|
135
|
+
| { matched: false; reason: 'timeout' | 'unavailable' };
|
|
136
|
+
|
|
137
|
+
/** pane.read / agent.read selection: `recent` keeps ANSI so T6 fullscreen detection still sees it. */
|
|
138
|
+
export interface ReadOptions {
|
|
139
|
+
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
140
|
+
lines?: number;
|
|
141
|
+
stripAnsi?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Decoded read payload; `revision` is the server's buffer revision (0 when unreadable). */
|
|
145
|
+
export interface ReadBuffer {
|
|
146
|
+
text: string;
|
|
147
|
+
revision: number;
|
|
148
|
+
truncated: boolean;
|
|
149
|
+
}
|
|
82
150
|
|
|
83
151
|
export interface HerdrClientLike {
|
|
84
152
|
readonly available: boolean;
|
|
85
153
|
reportAgent(state: PaneAgentState, message: string | null): Promise<void>;
|
|
86
|
-
|
|
87
|
-
reportMetadata(meta: { session: string; items: TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
154
|
+
reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
88
155
|
/** M18: report write-lock tokens (lock-<hash> → paneId|path); null releases, and batches stay within 16 keys. */
|
|
89
156
|
reportLockTokens(tokens: Record<string, string | null>): Promise<void>;
|
|
90
|
-
/**
|
|
91
|
-
* D93: sidebar agent display name (display_agent is the role name).
|
|
92
|
-
* The report persists without a TTL; null clears it back to the detected value. Best effort.
|
|
93
|
-
*/
|
|
157
|
+
/** D93: sidebar agent display name (display_agent is the role name); null clears it. Persists, no TTL. */
|
|
94
158
|
reportDisplayAgent(name: string | null): Promise<void>;
|
|
95
159
|
/**
|
|
96
|
-
* D95: ask_user_question waiting marker (tokens['pi-ask']; null clears it).
|
|
97
|
-
*
|
|
160
|
+
* D95: ask_user_question waiting marker (tokens['pi-ask']; null clears it). The sidebar/heatmap
|
|
161
|
+
* grade blocked + pi-ask as the ask level, distinct from plain block.
|
|
98
162
|
*/
|
|
99
163
|
reportAskFlag(text: string | null): Promise<void>;
|
|
100
164
|
listAgents(): Promise<AgentInfo[]>;
|
|
101
|
-
/**
|
|
102
|
-
spawnSubPane(opts: { label: string; command: string[]; cwd: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
103
|
-
/** Inject text into a pane terminal (pi editor input + Enter); send_text+CR reaches it directly and is used only to start launchLine. */
|
|
165
|
+
/** Inject text into a pane terminal (pi editor input + Enter); the PTY channel is used only to start launchLine. */
|
|
104
166
|
sendPaneText(paneId: string, text: string): Promise<void>;
|
|
105
|
-
/**
|
|
167
|
+
/** Server-side wait; null on timeout, throws on any other error. */
|
|
106
168
|
waitAgent(paneId: string, until: HerdrAgentState[], timeoutMs: number): Promise<HerdrAgentState | null>;
|
|
107
169
|
/** Query the child-agent session path (agent_session.value, kind=path); return null when absent. */
|
|
108
170
|
getAgentSessionPath(paneId: string): Promise<string | null>;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/** v1.2: Split a new shell pane in the current tab after focus, placing it in the group tab; return its pane ID. */
|
|
112
|
-
splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string }): Promise<string>;
|
|
113
|
-
/** v1.2: Close a pane; observed behavior kills its process tree and herdr closes an empty tab. */
|
|
171
|
+
splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number }): Promise<string>;
|
|
172
|
+
/** Close a pane; herdr kills its process tree and closes an empty tab. */
|
|
114
173
|
closePane(paneId: string): Promise<void>;
|
|
115
|
-
/** v1.2: Create a tab with a root shell pane, returning tabId/paneId for group-tab infrastructure. */
|
|
116
174
|
createTab(opts: { workspaceId: string; label?: string; cwd?: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
listPanes(): Promise<PaneListItem[]>;
|
|
176
|
+
/** D91: layout tree by paneId or tabId; null on failure. */
|
|
177
|
+
exportLayout(opts?: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null>;
|
|
178
|
+
/** 0.9.1: live pane rects for the tab containing paneId. Best effort returns null. */
|
|
179
|
+
paneLayout(opts?: { paneId?: string }): Promise<PaneLayoutSnapshot | null>;
|
|
122
180
|
tabList(): Promise<TabInfo[]>;
|
|
123
|
-
/** v1.3:
|
|
124
|
-
tabGet(tabId: string): Promise<TabInfo | null>;
|
|
125
|
-
/** v1.3: Close a tab (tab.close), cascading to its panes. */
|
|
181
|
+
/** v1.3: tab.close cascades to the tab's panes. */
|
|
126
182
|
tabClose(tabId: string): Promise<void>;
|
|
127
|
-
/**
|
|
183
|
+
/** 0.9.1: Launch a manifest pane entrypoint with placement (popup/tab/split/overlay/zoomed). */
|
|
184
|
+
openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult>;
|
|
185
|
+
agentExplain(target: string): Promise<Record<string, unknown> | null>;
|
|
186
|
+
getServerVersion(): Promise<string | null>;
|
|
187
|
+
/** pane.send_keys; herdr key combos include ctrl+c, enter, and esc. */
|
|
128
188
|
sendPaneKeys(paneId: string, keys: string[]): Promise<void>;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
lines?: number;
|
|
133
|
-
stripAnsi?: boolean;
|
|
134
|
-
}): Promise<{ text: string; revision: number; truncated: boolean }>;
|
|
135
|
-
/** M14: Wait for output to match a substring or regex; return null on timeout and throw on error. */
|
|
136
|
-
waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<boolean | null>;
|
|
189
|
+
readPane(paneId: string, opts?: ReadOptions): Promise<ReadBuffer>;
|
|
190
|
+
readAgent(target: string, opts?: ReadOptions): Promise<ReadBuffer>;
|
|
191
|
+
waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult>;
|
|
137
192
|
close(): void;
|
|
138
193
|
}
|
|
139
194
|
|
|
@@ -153,56 +208,34 @@ function findIdIn(obj: unknown, key: string, depth = 0): string | null {
|
|
|
153
208
|
|
|
154
209
|
/* ── Noop ──────────────────────────────────────────────────────────── */
|
|
155
210
|
|
|
211
|
+
/** Stand-in outside a herdr pane: queries answer empty, pane creation throws, reporting is a no-op. */
|
|
156
212
|
export class NoopHerdrClient implements HerdrClientLike {
|
|
157
213
|
readonly available = false;
|
|
158
214
|
async reportAgent(): Promise<void> {}
|
|
159
|
-
async reportAgentSession(): Promise<void> {}
|
|
160
215
|
async reportMetadata(): Promise<void> {}
|
|
161
216
|
async reportLockTokens(): Promise<void> {}
|
|
162
217
|
async reportDisplayAgent(): Promise<void> {}
|
|
163
218
|
async reportAskFlag(): Promise<void> {}
|
|
164
|
-
async listAgents(): Promise<AgentInfo[]> {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
-
async spawnSubPane(): Promise<{ tabId: string; paneId: string }> {
|
|
168
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
169
|
-
}
|
|
170
219
|
async sendPaneText(): Promise<void> {}
|
|
171
|
-
async waitAgent(): Promise<null> {
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
async getAgentSessionPath(): Promise<null> {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
async focusPane(): Promise<void> {}
|
|
178
|
-
async splitPane(): Promise<string> {
|
|
179
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
180
|
-
}
|
|
181
220
|
async closePane(): Promise<void> {}
|
|
182
|
-
async createTab(): Promise<{ tabId: string; paneId: string }> {
|
|
183
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
184
|
-
}
|
|
185
|
-
async listPanes(): Promise<Array<{ paneId: string; tabId: string; workspaceId: string; agentStatus: string }>> {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
async exportLayout(): Promise<null> {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
async tabList(): Promise<TabInfo[]> {
|
|
192
|
-
return [];
|
|
193
|
-
}
|
|
194
|
-
async tabGet(): Promise<TabInfo | null> {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
221
|
async tabClose(): Promise<void> {}
|
|
198
222
|
async sendPaneKeys(): Promise<void> {}
|
|
199
|
-
async readPane(): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
200
|
-
return { text: '', revision: 0, truncated: false };
|
|
201
|
-
}
|
|
202
|
-
async waitForOutput(): Promise<boolean | null> {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
223
|
close(): void {}
|
|
224
|
+
async listAgents(): Promise<AgentInfo[]> { return []; }
|
|
225
|
+
async listPanes(): Promise<PaneListItem[]> { return []; }
|
|
226
|
+
async tabList(): Promise<TabInfo[]> { return []; }
|
|
227
|
+
async waitAgent(): Promise<null> { return null; }
|
|
228
|
+
async getAgentSessionPath(): Promise<null> { return null; }
|
|
229
|
+
async exportLayout(): Promise<null> { return null; }
|
|
230
|
+
async paneLayout(): Promise<null> { return null; }
|
|
231
|
+
async agentExplain(): Promise<null> { return null; }
|
|
232
|
+
async getServerVersion(): Promise<null> { return null; }
|
|
233
|
+
async readPane(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
234
|
+
async readAgent(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
235
|
+
async waitForOutput(): Promise<WaitForOutputResult> { return { matched: false, reason: 'unavailable' }; }
|
|
236
|
+
async splitPane(): Promise<string> { throw new Error('subagent requires a herdr-managed pane'); }
|
|
237
|
+
async createTab(): Promise<{ tabId: string; paneId: string }> { throw new Error('subagent requires a herdr-managed pane'); }
|
|
238
|
+
async openPluginPane(): Promise<OpenPluginPaneResult> { throw new Error('plugin pane requires a herdr-managed pane'); }
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
/* ── Implementation ────────────────────────────────────────────────── */
|
|
@@ -220,10 +253,7 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
220
253
|
return herdrSocketTarget(this.env.socketPath);
|
|
221
254
|
}
|
|
222
255
|
|
|
223
|
-
/**
|
|
224
|
-
* Control requests use one connection per request; the server closes it after replying, as observed in the protocol.
|
|
225
|
-
* Reading a complete response line finishes the request; responses are {id, result} or {id, error}.
|
|
226
|
-
*/
|
|
256
|
+
/** One connection per control request: the reply is a single {id,result}|{id,error} line. */
|
|
227
257
|
private request(method: string, params: Record<string, unknown>, timeoutMs = 15000): Promise<unknown> {
|
|
228
258
|
return new Promise((resolve, reject) => {
|
|
229
259
|
const sock = net.createConnection(this.target());
|
|
@@ -292,25 +322,13 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
292
322
|
...(message ? { message } : {}),
|
|
293
323
|
});
|
|
294
324
|
} catch {
|
|
295
|
-
/* Silent best effort. */
|
|
296
325
|
}
|
|
297
326
|
}
|
|
298
327
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
await this.request('pane.report_agent_session', {
|
|
303
|
-
pane_id: this.env.paneId,
|
|
304
|
-
source: REPORT_AGENT_SOURCE,
|
|
305
|
-
agent: 'pi',
|
|
306
|
-
agent_session_path: sessionPath,
|
|
307
|
-
});
|
|
308
|
-
} catch {
|
|
309
|
-
/* Silent best effort. */
|
|
310
|
-
}
|
|
311
|
-
}
|
|
328
|
+
// Single writer: herdr's native pi integration owns the session path, so pier never sends
|
|
329
|
+
// pane.report_agent_session; the activity badge stays on report_agent.
|
|
312
330
|
|
|
313
|
-
async reportMetadata(meta: { session: string; items: TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
331
|
+
async reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
314
332
|
try {
|
|
315
333
|
void meta.session;
|
|
316
334
|
const title = formatPaneTitle(meta.items, null, {
|
|
@@ -318,8 +336,8 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
318
336
|
lastWriteAt: meta.lastWriteAt,
|
|
319
337
|
});
|
|
320
338
|
const blocked = formatBlockedLabel(meta.items);
|
|
321
|
-
// D96:
|
|
322
|
-
//
|
|
339
|
+
// D96: stale cleanup needs its own batch — stale(16) + pi-todo(1) exceeds herdr's 16-token
|
|
340
|
+
// limit, and the rejected request would drop both the title and the tokens.
|
|
323
341
|
if (!this.clearedStaleTokens) {
|
|
324
342
|
await this.request('pane.report_metadata', {
|
|
325
343
|
pane_id: this.env.paneId,
|
|
@@ -336,12 +354,11 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
336
354
|
...(blocked
|
|
337
355
|
? { state_labels: { [BLOCKED_LABEL_KEY]: blocked } }
|
|
338
356
|
: { clear_state_labels: true }),
|
|
339
|
-
// D93:
|
|
357
|
+
// D93: the todo summary is a custom token; an empty string clears the key instead of leaving a stale summary.
|
|
340
358
|
tokens: sidebarTodoTokens(title),
|
|
341
359
|
ttl_ms: 86400000,
|
|
342
360
|
});
|
|
343
361
|
} catch {
|
|
344
|
-
/* Silent best effort: the title projection must never affect pi's main flow. */
|
|
345
362
|
}
|
|
346
363
|
}
|
|
347
364
|
|
|
@@ -349,7 +366,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
349
366
|
const keys = Object.keys(tokens);
|
|
350
367
|
if (keys.length === 0) return;
|
|
351
368
|
try {
|
|
352
|
-
// schema maxProperties=16, so split into independent best-effort requests.
|
|
353
369
|
for (let i = 0; i < keys.length; i += LOCK_BATCH_LIMIT) {
|
|
354
370
|
const batch: Record<string, string | null> = {};
|
|
355
371
|
for (const k of keys.slice(i, i + LOCK_BATCH_LIMIT)) batch[k] = tokens[k];
|
|
@@ -373,7 +389,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
373
389
|
...(name ? { display_agent: name } : { clear_display_agent: true }),
|
|
374
390
|
});
|
|
375
391
|
} catch {
|
|
376
|
-
/* Silent best effort: sidebar identity is supplementary. */
|
|
377
392
|
}
|
|
378
393
|
}
|
|
379
394
|
|
|
@@ -386,7 +401,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
386
401
|
ttl_ms: 86400000,
|
|
387
402
|
});
|
|
388
403
|
} catch {
|
|
389
|
-
/* Silent best effort: the human-gate marker is supplementary. */
|
|
390
404
|
}
|
|
391
405
|
}
|
|
392
406
|
|
|
@@ -407,20 +421,11 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
407
421
|
session: session?.value != null ? String(session.value) : null,
|
|
408
422
|
stateLabels: (a.state_labels ?? {}) as Record<string, string>,
|
|
409
423
|
tokens: (a.tokens ?? {}) as Record<string, string | null>,
|
|
424
|
+
...(a.foreground_cwd ? { foregroundCwd: String(a.foreground_cwd) } : {}),
|
|
410
425
|
};
|
|
411
426
|
});
|
|
412
427
|
}
|
|
413
428
|
|
|
414
|
-
async spawnSubPane(opts: { label: string; command: string[]; cwd: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }> {
|
|
415
|
-
const result = (await this.request('layout.apply', {
|
|
416
|
-
tab_label: opts.label,
|
|
417
|
-
root: { type: 'pane', command: opts.command, cwd: opts.cwd, ...(opts.env ? { env: opts.env } : {}) },
|
|
418
|
-
})) as Record<string, unknown>;
|
|
419
|
-
const paneId = findIdIn(result, 'pane_id');
|
|
420
|
-
if (!paneId) throw new Error('layout.apply: no pane_id in response');
|
|
421
|
-
return { tabId: findIdIn(result, 'tab_id') ?? '', paneId };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
429
|
async sendPaneText(paneId: string, text: string): Promise<void> {
|
|
425
430
|
// A trailing \r acts as Enter; observed behavior sends text to the pi editor and submits it.
|
|
426
431
|
await this.request('pane.send_text', { pane_id: paneId, text: text + '\r' });
|
|
@@ -438,6 +443,9 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
438
443
|
} catch (err) {
|
|
439
444
|
const msg = String((err as Error)?.message ?? err);
|
|
440
445
|
if (/timeout/i.test(msg)) return null;
|
|
446
|
+
// A14: herdr reports agent_not_found while a freshly spawned pane's agent is still registering;
|
|
447
|
+
// callers poll, so map it to the same "nothing yet" result as a timeout.
|
|
448
|
+
if (/agent_not_found|agent target .* not found/i.test(msg)) return null;
|
|
441
449
|
throw err;
|
|
442
450
|
}
|
|
443
451
|
}
|
|
@@ -448,17 +456,28 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
448
456
|
return a?.session ?? null;
|
|
449
457
|
}
|
|
450
458
|
|
|
451
|
-
async
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string } = {}): Promise<string> {
|
|
456
|
-
const result = (await this.request('pane.split', {
|
|
459
|
+
async splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number } = {}): Promise<string> {
|
|
460
|
+
const params: Record<string, unknown> = {
|
|
457
461
|
direction: opts.direction ?? 'right',
|
|
458
462
|
...(opts.targetPaneId ? { target_pane_id: opts.targetPaneId } : {}),
|
|
459
463
|
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
460
464
|
...(opts.env ? { env: opts.env } : {}),
|
|
461
|
-
|
|
465
|
+
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
466
|
+
...(opts.ratio !== undefined ? { ratio: opts.ratio } : {}),
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
return await this.splitPaneRequest(params);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (opts.ratio === undefined) throw err;
|
|
472
|
+
const msg = String((err as Error)?.message ?? err);
|
|
473
|
+
if (!/unknown|invalid_params|unexpected/i.test(msg)) throw err;
|
|
474
|
+
const { ratio: _ratio, ...rest } = params;
|
|
475
|
+
return await this.splitPaneRequest(rest);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private async splitPaneRequest(params: Record<string, unknown>): Promise<string> {
|
|
480
|
+
const result = (await this.request('pane.split', params)) as Record<string, unknown>;
|
|
462
481
|
const paneId = findIdIn(result, 'pane_id');
|
|
463
482
|
if (!paneId) throw new Error('pane.split: no pane_id in response');
|
|
464
483
|
return paneId;
|
|
@@ -481,17 +500,20 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
481
500
|
return { tabId, paneId: paneId ?? '' };
|
|
482
501
|
}
|
|
483
502
|
|
|
484
|
-
async listPanes(): Promise<
|
|
503
|
+
async listPanes(): Promise<PaneListItem[]> {
|
|
485
504
|
const result = (await this.request('pane.list', {})) as { panes?: Array<Record<string, unknown>> } | null;
|
|
486
505
|
return (result?.panes ?? []).map((p) => ({
|
|
487
506
|
paneId: String(p.pane_id ?? ''),
|
|
488
507
|
tabId: String(p.tab_id ?? ''),
|
|
489
508
|
workspaceId: String(p.workspace_id ?? ''),
|
|
490
509
|
agentStatus: String(p.agent_status ?? 'unknown'),
|
|
510
|
+
...(p.foreground_cwd ? { foregroundCwd: String(p.foreground_cwd) } : {}),
|
|
511
|
+
...(p.terminal_title ? { terminalTitle: String(p.terminal_title) } : {}),
|
|
512
|
+
...(p.terminal_title_stripped ? { terminalTitleStripped: String(p.terminal_title_stripped) } : {}),
|
|
491
513
|
}));
|
|
492
514
|
}
|
|
493
515
|
|
|
494
|
-
async exportLayout(opts: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown } | null> {
|
|
516
|
+
async exportLayout(opts: { paneId?: string; tabId?: string } = {}): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null> {
|
|
495
517
|
try {
|
|
496
518
|
const result = (await this.request('layout.export', {
|
|
497
519
|
...(opts.paneId ? { pane_id: opts.paneId } : {}),
|
|
@@ -504,6 +526,42 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
504
526
|
tabId: typeof layout.tab_id === 'string' ? layout.tab_id : null,
|
|
505
527
|
zoomed: Boolean(layout.zoomed),
|
|
506
528
|
root: layout.root ?? null,
|
|
529
|
+
// D-4: focus heat needs to know which pane the human is on; layout.export reports it.
|
|
530
|
+
focusedPaneId: typeof layout.focused_pane_id === 'string' ? layout.focused_pane_id : null,
|
|
531
|
+
};
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async paneLayout(opts: { paneId?: string } = {}): Promise<PaneLayoutSnapshot | null> {
|
|
538
|
+
try {
|
|
539
|
+
const result = (await this.request('pane.layout', {
|
|
540
|
+
...(opts.paneId ? { pane_id: opts.paneId } : {}),
|
|
541
|
+
})) as Record<string, unknown> | null;
|
|
542
|
+
const layout = (result?.layout ?? result) as Record<string, unknown> | null | undefined;
|
|
543
|
+
if (!layout || typeof layout !== 'object' || !Array.isArray(layout.panes)) return null;
|
|
544
|
+
const panes: PaneLayoutCell[] = [];
|
|
545
|
+
for (const raw of layout.panes) {
|
|
546
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
547
|
+
const p = raw as Record<string, unknown>;
|
|
548
|
+
const rect = (p.rect && typeof p.rect === 'object') ? p.rect as Record<string, unknown> : p;
|
|
549
|
+
const paneId = typeof p.pane_id === 'string' ? p.pane_id : null;
|
|
550
|
+
if (!paneId) continue;
|
|
551
|
+
panes.push({
|
|
552
|
+
paneId,
|
|
553
|
+
x: Number(rect.x) || 0,
|
|
554
|
+
y: Number(rect.y) || 0,
|
|
555
|
+
w: Number(rect.width ?? rect.w) || 0,
|
|
556
|
+
h: Number(rect.height ?? rect.h) || 0,
|
|
557
|
+
focused: p.focused === true,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
tabId: typeof layout.tab_id === 'string' ? layout.tab_id : null,
|
|
562
|
+
zoomed: Boolean(layout.zoomed),
|
|
563
|
+
focusedPaneId: typeof layout.focused_pane_id === 'string' ? layout.focused_pane_id : null,
|
|
564
|
+
panes,
|
|
507
565
|
};
|
|
508
566
|
} catch {
|
|
509
567
|
return null;
|
|
@@ -526,32 +584,93 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
526
584
|
return (result?.tabs ?? []).map((t) => this.mapTabInfo(t)).filter((t): t is TabInfo => t !== null);
|
|
527
585
|
}
|
|
528
586
|
|
|
529
|
-
async tabGet(tabId: string): Promise<TabInfo | null> {
|
|
530
|
-
const result = (await this.request('tab.get', { tab_id: tabId })) as { tab?: Record<string, unknown> | null } | null;
|
|
531
|
-
return this.mapTabInfo(result?.tab ?? null);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
587
|
async tabClose(tabId: string): Promise<void> {
|
|
535
588
|
await this.request('tab.close', { tab_id: tabId });
|
|
536
589
|
}
|
|
537
590
|
|
|
591
|
+
async openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult> {
|
|
592
|
+
const params: Record<string, unknown> = {
|
|
593
|
+
plugin_id: opts.pluginId,
|
|
594
|
+
entrypoint: opts.entrypoint,
|
|
595
|
+
...(opts.placement ? { placement: opts.placement } : {}),
|
|
596
|
+
...(opts.width ? { width: opts.width } : {}),
|
|
597
|
+
...(opts.height ? { height: opts.height } : {}),
|
|
598
|
+
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
599
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
600
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
601
|
+
...(opts.targetPaneId ? { target_pane_id: opts.targetPaneId } : {}),
|
|
602
|
+
...(opts.workspaceId ? { workspace_id: opts.workspaceId } : {}),
|
|
603
|
+
};
|
|
604
|
+
try {
|
|
605
|
+
const result = (await this.request('plugin.pane.open', params)) as Record<string, unknown> | null;
|
|
606
|
+
if (opts.placement === 'popup') {
|
|
607
|
+
return { mode: 'popup', ok: true };
|
|
608
|
+
}
|
|
609
|
+
return { mode: 'pane', paneId: findIdIn(result, 'pane_id') ?? '' };
|
|
610
|
+
} catch (err) {
|
|
611
|
+
// 0.9.0 serde rejects unknown `popup` as "unknown variant" / invalid_params — not a dedicated
|
|
612
|
+
// invalid_placement code. Any popup failure retries as tab; tab failure surfaces to Level 3.
|
|
613
|
+
if (opts.placement !== 'popup') throw err;
|
|
614
|
+
const { width: _width, height: _height, target_pane_id: _target, ...base } = params;
|
|
615
|
+
const fallbackRes = (await this.request('plugin.pane.open', { ...base, placement: 'tab' })) as Record<string, unknown> | null;
|
|
616
|
+
return {
|
|
617
|
+
mode: 'fallback_tab',
|
|
618
|
+
paneId: findIdIn(fallbackRes, 'pane_id') ?? undefined,
|
|
619
|
+
tabId: findIdIn(fallbackRes, 'tab_id') ?? undefined,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async agentExplain(target: string): Promise<Record<string, unknown> | null> {
|
|
625
|
+
try {
|
|
626
|
+
const result = (await this.request('agent.explain', { target })) as Record<string, unknown> | null;
|
|
627
|
+
if (!result || typeof result !== 'object') return null;
|
|
628
|
+
const nested = result.explain;
|
|
629
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
630
|
+
return nested as Record<string, unknown>;
|
|
631
|
+
}
|
|
632
|
+
return result;
|
|
633
|
+
} catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private cachedVersion: string | null = null;
|
|
639
|
+
|
|
640
|
+
async getServerVersion(): Promise<string | null> {
|
|
641
|
+
if (this.cachedVersion) return this.cachedVersion;
|
|
642
|
+
try {
|
|
643
|
+
const result = (await this.request('ping', {})) as { version?: unknown } | null;
|
|
644
|
+
if (typeof result?.version === 'string') {
|
|
645
|
+
this.cachedVersion = result.version;
|
|
646
|
+
return result.version;
|
|
647
|
+
}
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
|
|
538
653
|
async sendPaneKeys(paneId: string, keys: string[]): Promise<void> {
|
|
539
654
|
await this.request('pane.send_keys', { pane_id: paneId, keys });
|
|
540
655
|
}
|
|
541
656
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
657
|
+
/** Shared pane.read/agent.read decode: payload nested under `read` (older servers answer it flat). */
|
|
658
|
+
private async readBuffer(
|
|
659
|
+
method: 'pane.read' | 'agent.read',
|
|
660
|
+
target: { pane_id: string } | { target: string },
|
|
661
|
+
opts: ReadOptions,
|
|
662
|
+
stripAnsiDefault: boolean,
|
|
663
|
+
): Promise<ReadBuffer> {
|
|
664
|
+
const result = (await this.request(method, {
|
|
665
|
+
...target,
|
|
550
666
|
source: opts.source ?? 'recent',
|
|
551
667
|
format: 'text',
|
|
552
|
-
strip_ansi: opts.stripAnsi ??
|
|
668
|
+
strip_ansi: opts.stripAnsi ?? stripAnsiDefault,
|
|
553
669
|
...(opts.lines != null ? { lines: opts.lines } : {}),
|
|
554
|
-
})) as {
|
|
670
|
+
})) as {
|
|
671
|
+
read?: { text?: unknown; revision?: unknown; truncated?: unknown };
|
|
672
|
+
text?: unknown; revision?: unknown; truncated?: unknown;
|
|
673
|
+
} | null;
|
|
555
674
|
const payload = result?.read ?? result ?? {};
|
|
556
675
|
return {
|
|
557
676
|
text: typeof payload.text === 'string' ? payload.text : '',
|
|
@@ -560,7 +679,15 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
560
679
|
};
|
|
561
680
|
}
|
|
562
681
|
|
|
563
|
-
async
|
|
682
|
+
async readPane(paneId: string, opts: ReadOptions = {}): Promise<ReadBuffer> {
|
|
683
|
+
return this.readBuffer('pane.read', { pane_id: paneId }, opts, false);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async readAgent(target: string, opts: ReadOptions = {}): Promise<ReadBuffer> {
|
|
687
|
+
return this.readBuffer('agent.read', { target }, opts, true);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult> {
|
|
564
691
|
try {
|
|
565
692
|
await this.request('pane.wait_for_output', {
|
|
566
693
|
pane_id: paneId,
|
|
@@ -568,10 +695,13 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
568
695
|
match,
|
|
569
696
|
timeout_ms: timeoutMs,
|
|
570
697
|
}, timeoutMs + 5000);
|
|
571
|
-
return true;
|
|
698
|
+
return { matched: true };
|
|
572
699
|
} catch (err) {
|
|
573
700
|
const msg = String((err as Error)?.message ?? err);
|
|
574
|
-
if (/timeout/i.test(msg)) return
|
|
701
|
+
if (/timeout/i.test(msg)) return { matched: false, reason: 'timeout' };
|
|
702
|
+
if (/not supported|not implemented|unknown method|unavailable|method not found/i.test(msg)) {
|
|
703
|
+
return { matched: false, reason: 'unavailable' };
|
|
704
|
+
}
|
|
575
705
|
throw err;
|
|
576
706
|
}
|
|
577
707
|
}
|