@muzi3/herdr 0.1.2 → 0.1.4
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 +12 -1
- package/extensions/blocked-state.ts +118 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| `herdr-guide` | user-invoked 向导 | 引导用户理解、安装、配置与排查 Herdr(`herdr.dev/agent-guide.md`) |
|
|
8
8
|
| `herdr-agent-state` | 扩展 | 通过 Herdr 的 pane socket 上报 agent 状态(working / blocked / idle)及会话引用,供 Herdr 面板展示 |
|
|
9
9
|
| `pi-hot-restart` | 扩展 | 提供 `/pi-hot-restart` 命令:批量重启当前 herdr 里的所有 pi 窗口(跳过 working),并恢复到原 session |
|
|
10
|
+
| `blocked-state` | 扩展 | 把 pi 的阻塞式交互弹窗(`ui_prompt_start` / `ui_prompt_end`,含 `ask_user_question`)转发为 herdr 的 `herdr:blocked` 事件,pane 状态变为 `blocked`(label 带问题摘要);桌面通知交给 herdr 自己发 |
|
|
10
11
|
|
|
11
12
|
## 来源与许可
|
|
12
13
|
|
|
@@ -19,7 +20,7 @@ Herdr 以 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 许可发布
|
|
|
19
20
|
|
|
20
21
|
> 扩展源文件由 Herdr 集成管理,重装或更新集成时会覆盖;请在旁边添加自定义 hooks 而非编辑它。
|
|
21
22
|
|
|
22
|
-
`pi-hot-restart.ts` 是本包自带的补充扩展,与官方 `herdr-agent-state` 互不干扰,在 `package.json` 的 `pi.extensions` 中显式声明(因 `extensions/index.ts` 存在时 pi 不会扫描同目录兄弟文件)。
|
|
23
|
+
`blocked-state.ts` 与 `pi-hot-restart.ts` 是本包自带的补充扩展,与官方 `herdr-agent-state` 互不干扰,在 `package.json` 的 `pi.extensions` 中显式声明(因 `extensions/index.ts` 存在时 pi 不会扫描同目录兄弟文件)。
|
|
23
24
|
|
|
24
25
|
## 安装
|
|
25
26
|
|
|
@@ -41,6 +42,15 @@ pi install npm:@muzi3/herdr
|
|
|
41
42
|
3. 弹出确认框,展示将重启的窗口清单与数量;
|
|
42
43
|
4. 对每个目标窗口:向 pi 前台进程组发 SIGTERM 优雅退出(session 落盘),等待退出后用 `pi --session <path|id>` 在原 pane 恢复。
|
|
43
44
|
|
|
45
|
+
### 等用户输入时上报 blocked
|
|
46
|
+
|
|
47
|
+
herdr 的 pi 集成只认 `herdr:blocked` 事件,且 herdr 0.9.0 对 pi 仍没有终端 blocked 识别规则(内置规则只有一条 `working_literal`,匹配 `Working...`),所以 `ask_user_question` / confirm 弹窗期间 pane 会一直停在 `working`。`blocked-state` 订阅 pi core 的 `ui_prompt_start` / `ui_prompt_end`(`select`/`confirm`/`input`/`editor`/`custom` 统一包装,只在外层触发):
|
|
48
|
+
|
|
49
|
+
- 等待期发 `herdr:blocked { active: true, label }`,结束后发 `{ active: false }`;label 形如 `等待回答:<问题 header>`(`custom` 无 title,取 `rpiv:ask-user:prompt` 的问题内容)、`等待确认:<title>`。
|
|
50
|
+
- 契约同 pi-subagents 的 herdr-status bridge:herdr 的 `blockedCount` 是计数式的,切 label 前先发 `active:false` 再发 `active:true`;本扩展严格配对,只占一个计数。
|
|
51
|
+
|
|
52
|
+
桌面通知交给 herdr 自己发(`~/.config/herdr/config.toml` 设 `[ui.toast] delivery = "terminal"`):通知经终端转交给 Ghostty 显示,**点击横幅会把 Ghostty 窗口调到前面**。注意它只能到窗口粒度 —— herdr 的 pane 是 herdr TUI 内部的概念,点通知不会切到对应 pane;正文也固定为 `<工作区名> · <工作区编号> · <tab 名>`,不含 pane id 与问题原文。早期版本试过自建横幅(`osascript` / `terminal-notifier`),内容能带 pane 与问题原文,但要么不支持点击动作、要么在本机通知权限不稳;点击跳转要能捕获回调的通知器(如 alerter)才可能做到,为避免与 herdr 自己的通知重复弹两份,自建横幅已整体移除。
|
|
53
|
+
|
|
44
54
|
### 扩展环境变量
|
|
45
55
|
|
|
46
56
|
扩展需在 Herdr 集成环境中运行,依赖以下环境变量:
|
|
@@ -50,6 +60,7 @@ pi install npm:@muzi3/herdr
|
|
|
50
60
|
| `HERDR_ENV` | 为 `1` 时启用 |
|
|
51
61
|
| `HERDR_SOCKET_PATH` | Herdr pane socket 路径 |
|
|
52
62
|
| `HERDR_PANE_ID` | 当前 pane id |
|
|
63
|
+
| `HERDR_PANE_ID` | 当前 pane id(同时作为扩展的运行门槛) |
|
|
53
64
|
|
|
54
65
|
未设置这些变量时 `herdr-agent-state` 扩展静默跳过。`pi-hot-restart` 通过 PATH 调用 `herdr` CLI(`agent list` / `pane process-info` / `pane run`),不依赖上述变量。
|
|
55
66
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* blocked-state
|
|
3
|
+
*
|
|
4
|
+
* 把 pi 的阻塞式交互提示转发成 herdr 的 blocked 状态。
|
|
5
|
+
*
|
|
6
|
+
* herdr 的 pi 集成(同目录 index.ts)只监听 `herdr:blocked` 事件再上报
|
|
7
|
+
* `state: "blocked"`,而它内置的终端识别规则只有一条 working_literal(匹配
|
|
8
|
+
* "Working..."),没有任何 blocked 规则 —— 没有这个桥,ask_user_question 期间
|
|
9
|
+
* pane 一直显示 working,切到别的 pane 就看不出这个会话正等着人回话。
|
|
10
|
+
*
|
|
11
|
+
* 信号来源是 pi core 的 `ui_prompt_start` / `ui_prompt_end`:runner 统一包装了
|
|
12
|
+
* select/confirm/input/editor/custom,只在外层提示触发,所以覆盖所有阻塞弹窗。
|
|
13
|
+
* `custom` 不带 title,额外借 `rpiv:ask-user:prompt` 的问题内容补 label。
|
|
14
|
+
*
|
|
15
|
+
* herdr 侧的 blockedCount 是计数式的:切 label 前必须先发 `{active:false}` 降自己
|
|
16
|
+
* 那一份再发 `{active:true}`。本扩展严格配对 raise/lower,只占一个计数。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
|
|
21
|
+
/** herdr pi 集成的入站事件通道(由 index.ts 消费)。 */
|
|
22
|
+
const HERDR_BLOCKED_EVENT = "herdr:blocked";
|
|
23
|
+
/** @juicesharp/rpiv-ask-user-question 的公开事件通道。 */
|
|
24
|
+
const ASK_USER_PROMPT_EVENT = "rpiv:ask-user:prompt";
|
|
25
|
+
|
|
26
|
+
/** 与 herdr 集成同一门槛:确认自己确实跑在 herdr 的 pane 里。 */
|
|
27
|
+
const IN_HERDR = process.env.HERDR_ENV === "1" && !!process.env.HERDR_PANE_ID;
|
|
28
|
+
const MAX_LABEL_CHARS = 60;
|
|
29
|
+
const WAITING = "等待回答";
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** label 是一行状态文字,换行/多空格先折叠掉。 */
|
|
36
|
+
function flatten(text: string): string {
|
|
37
|
+
return text.replace(/\s+/g, " ").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function truncate(text: string, max: number): string {
|
|
41
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 取第一条问题的 header(短)与原文(长)。 */
|
|
45
|
+
function readQuestion(data: unknown): { header?: string; question?: string } | undefined {
|
|
46
|
+
if (!isRecord(data) || !Array.isArray(data.questions)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const first = data.questions.find(isRecord);
|
|
50
|
+
if (!first) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const header = typeof first.header === "string" ? flatten(first.header) : "";
|
|
54
|
+
const question = typeof first.question === "string" ? flatten(first.question) : "";
|
|
55
|
+
if (!header && !question) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return { header: header || undefined, question: question || undefined };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default function (pi: ExtensionAPI) {
|
|
62
|
+
if (!IN_HERDR) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let rootSession = false;
|
|
67
|
+
let raised = false;
|
|
68
|
+
let raisedLabel: string | undefined;
|
|
69
|
+
let askUser: { header?: string; question?: string } | undefined;
|
|
70
|
+
|
|
71
|
+
function lower(): void {
|
|
72
|
+
if (!raised) return;
|
|
73
|
+
raised = false;
|
|
74
|
+
raisedLabel = undefined;
|
|
75
|
+
pi.events.emit(HERDR_BLOCKED_EVENT, { active: false });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function raise(detail: string | undefined): void {
|
|
79
|
+
const label = detail ? `${WAITING}:${truncate(detail, MAX_LABEL_CHARS)}` : WAITING;
|
|
80
|
+
if (raised && raisedLabel === label) return;
|
|
81
|
+
lower(); // 先降自己那一份再抬高,保证 label 切换时计数不残留
|
|
82
|
+
raised = true;
|
|
83
|
+
raisedLabel = label;
|
|
84
|
+
pi.events.emit(HERDR_BLOCKED_EVENT, { active: true, label });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pi.events.on(ASK_USER_PROMPT_EVENT, (data) => {
|
|
88
|
+
askUser = readQuestion(data);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("ui_prompt_start", (event) => {
|
|
92
|
+
if (!rootSession) return;
|
|
93
|
+
// custom 提示(ask_user_question 走这条)的 title 是空的,用问题内容补 label。
|
|
94
|
+
raise(event.kind === "custom" ? (askUser?.header ?? askUser?.question) : event.title);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.on("ui_prompt_end", () => {
|
|
98
|
+
askUser = undefined;
|
|
99
|
+
if (!rootSession) return;
|
|
100
|
+
lower();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.on("session_start", (_event, ctx) => {
|
|
104
|
+
// 与 herdr 集成同一门槛:RPC/print 没有可展示的 PTY;
|
|
105
|
+
// reload 会同时重载集成与本扩展,两侧计数一起归零。
|
|
106
|
+
if (ctx?.mode !== "tui") return;
|
|
107
|
+
rootSession = true;
|
|
108
|
+
raised = false;
|
|
109
|
+
raisedLabel = undefined;
|
|
110
|
+
askUser = undefined;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
pi.on("session_shutdown", () => {
|
|
114
|
+
lower();
|
|
115
|
+
rootSession = false;
|
|
116
|
+
askUser = undefined;
|
|
117
|
+
});
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muzi3/herdr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Herdr for pi: the herdr operate skill (control Herdr from inside a pane), the herdr-guide skill (teach a human to set up and troubleshoot Herdr), and the herdr-agent-state extension (report agent state to Herdr).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
23
|
"./extensions/index.ts",
|
|
24
|
-
"./extensions/pi-hot-restart.ts"
|
|
24
|
+
"./extensions/pi-hot-restart.ts",
|
|
25
|
+
"./extensions/blocked-state.ts"
|
|
25
26
|
],
|
|
26
27
|
"skills": [
|
|
27
28
|
"./skills"
|