@maplezzk/pi-interactive-subagents 3.9.0 → 3.10.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 +6 -3
- package/README.zh-CN.md +6 -3
- package/config.json.example +1 -0
- package/locales/index.json +12 -4
- package/package.json +2 -2
- package/pi-extension/subagents/index.ts +100 -19
- package/pi-extension/subagents/mux-config.ts +85 -24
package/README.md
CHANGED
|
@@ -59,9 +59,9 @@ zellij --session pi # then run: pi
|
|
|
59
59
|
# just run pi inside Orca — no wrapper needed
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Optional: set `PI_SUBAGENT_MUX=muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca` to force a specific backend.
|
|
62
|
+
Optional: set `PI_SUBAGENT_MUX=muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca` to force a specific backend. Herdr uses its original split layout by default; set `PI_SUBAGENT_HERDR_MODE=tab` for one background tab per subagent, or `split` to select pane splitting explicitly.
|
|
63
63
|
|
|
64
|
-
You can also configure it from inside Pi with `/config:subagent`. Run it without arguments for an interactive menu, or use `/config:subagent auto|muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca` for a direct choice. The selection is persisted in Pi's user extension config directory; explicit `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` environment variables take precedence. `/subagent-config` and `/pi-subagent-config` remain available as compatibility aliases.
|
|
64
|
+
You can also configure it from inside Pi with `/config:subagent`. Run it without arguments for an interactive menu, or use `/config:subagent auto|muxy|cmux|tmux|zellij|wezterm|herdr [split|tab]|otty|orca` for a direct choice. For example, `/config:subagent herdr tab` persists both the Herdr backend and tab mode. The selection is persisted in Pi's user extension config directory; explicit `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` and `PI_SUBAGENT_HERDR_MODE` environment variables take precedence. `/subagent-config` and `/pi-subagent-config` remain available as compatibility aliases.
|
|
65
65
|
|
|
66
66
|
> **Otty notes:**
|
|
67
67
|
> - Otty sets `TERM_PROGRAM=otty` automatically when pi runs inside it; the backend detects this env var.
|
|
@@ -159,7 +159,7 @@ These labels are no longer derived from session-file growth. Session JSONL is st
|
|
|
159
159
|
|
|
160
160
|
#### Configuration
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
Subagent settings, including status display and the persisted Herdr mode, are controlled by `config.json` in the extension directory. Copy `config.json.example` to get started:
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
165
|
cp config.json.example config.json
|
|
@@ -167,12 +167,15 @@ cp config.json.example config.json
|
|
|
167
167
|
|
|
168
168
|
```json
|
|
169
169
|
{
|
|
170
|
+
"herdrMode": "split",
|
|
170
171
|
"status": {
|
|
171
172
|
"enabled": true
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
```
|
|
175
176
|
|
|
177
|
+
`herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field.
|
|
178
|
+
|
|
176
179
|
`config.json` is gitignored so local overrides don't get committed.
|
|
177
180
|
|
|
178
181
|
---
|
package/README.zh-CN.md
CHANGED
|
@@ -32,10 +32,10 @@ tmux new -A -s pi 'pi'
|
|
|
32
32
|
zellij --session pi # 然后运行 pi
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
可选:设置 `PI_SUBAGENT_MUX=muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca` 强制指定后端。
|
|
35
|
+
可选:设置 `PI_SUBAGENT_MUX=muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca` 强制指定后端。Herdr 默认保持原有分屏布局;设置 `PI_SUBAGENT_HERDR_MODE=tab` 后每个 subagent 创建独立后台 Tab,设置为 `split` 可显式选择分屏模式。
|
|
36
36
|
|
|
37
37
|
也可以在 Pi 内通过 `/config:subagent` 配置:不带参数打开选择菜单,或直接运行
|
|
38
|
-
`/config:subagent auto|muxy|cmux|tmux|zellij|wezterm|herdr|otty|orca
|
|
38
|
+
`/config:subagent auto|muxy|cmux|tmux|zellij|wezterm|herdr [split|tab]|otty|orca`。例如 `/config:subagent herdr tab` 会同时持久化 Herdr 后端与 Tab 模式。选择会保存到 Pi 的用户扩展配置目录;显式设置的 `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` 与 `PI_SUBAGENT_HERDR_MODE` 优先级更高。`/subagent-config` 和 `/pi-subagent-config` 仍作为兼容别名保留。
|
|
39
39
|
|
|
40
40
|
## 主要能力
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ zellij --session pi # 然后运行 pi
|
|
|
50
50
|
|
|
51
51
|
## 配置
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
状态显示与持久化 Herdr 模式由扩展目录下的 `config.json` 控制。复制 `config.json.example` 开始:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
cp config.json.example config.json
|
|
@@ -58,12 +58,15 @@ cp config.json.example config.json
|
|
|
58
58
|
|
|
59
59
|
```json
|
|
60
60
|
{
|
|
61
|
+
"herdrMode": "split",
|
|
61
62
|
"status": {
|
|
62
63
|
"enabled": true
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
`herdrMode` 支持 `split`(默认,兼容原有 pane 布局)和 `tab`(每个 subagent 独立后台 Tab)。`/config:subagent herdr split|tab` 会更新该字段。
|
|
69
|
+
|
|
67
70
|
## 致谢
|
|
68
71
|
|
|
69
72
|
- 原作者 **HazAT** 的设计与实现([原始仓库](https://github.com/HazAT/pi-interactive-subagents))
|
package/config.json.example
CHANGED
package/locales/index.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"en-US": "[Auto reminder]\n• Done → call subagent_done to finish.\n• Before finishing, self-check: are you spinning in place? If so, converge your result immediately and hand it back to the main agent with caller_ping — don't overthink.\n• Still working → ignore."
|
|
5
5
|
},
|
|
6
6
|
"muxCommandDescription": {
|
|
7
|
-
"zh-CN": "配置 subagent
|
|
8
|
-
"en-US": "Configure the
|
|
7
|
+
"zh-CN": "配置 subagent 使用的终端复用器及 Herdr 布局模式",
|
|
8
|
+
"en-US": "Configure the subagent multiplexer and Herdr layout mode"
|
|
9
9
|
},
|
|
10
10
|
"muxConfigTitle": {
|
|
11
11
|
"zh-CN": "选择 subagent 终端复用器(当前:{value})",
|
|
@@ -32,8 +32,16 @@
|
|
|
32
32
|
"en-US": "Subagent multiplexer set to: {value}"
|
|
33
33
|
},
|
|
34
34
|
"muxInvalid": {
|
|
35
|
-
"zh-CN": "不支持的 mux
|
|
36
|
-
"en-US": "Unsupported mux: {value}. Choose auto, muxy, cmux, tmux, zellij, wezterm, herdr, otty, or orca."
|
|
35
|
+
"zh-CN": "不支持的 mux 配置:{value}。可选:auto、muxy、cmux、tmux、zellij、wezterm、herdr [split|tab]、otty、orca。",
|
|
36
|
+
"en-US": "Unsupported mux setting: {value}. Choose auto, muxy, cmux, tmux, zellij, wezterm, herdr [split|tab], otty, or orca."
|
|
37
|
+
},
|
|
38
|
+
"herdrModeSplit": {
|
|
39
|
+
"zh-CN": "分屏",
|
|
40
|
+
"en-US": "split panes"
|
|
41
|
+
},
|
|
42
|
+
"herdrModeTab": {
|
|
43
|
+
"zh-CN": "独立 Tab",
|
|
44
|
+
"en-US": "separate tabs"
|
|
37
45
|
},
|
|
38
46
|
"muxInteractiveOnly": {
|
|
39
47
|
"zh-CN": "请在支持 UI 的 Pi 会话中运行 /config:subagent。",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maplezzk/pi-interactive-subagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Interactive async subagents for pi — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. Fork of HazAT/pi-interactive-subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"ajv": "^8.20.0",
|
|
61
61
|
"pi-extensions-i18n": "^0.3.1",
|
|
62
|
-
"pi-terminal-mux": "^0.
|
|
62
|
+
"pi-terminal-mux": "^0.4.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
65
|
"@earendil-works/pi-coding-agent": ">=0.80.0 <0.81.0",
|
|
@@ -44,9 +44,14 @@ import {
|
|
|
44
44
|
|
|
45
45
|
import {
|
|
46
46
|
applyPersistedMuxPreference,
|
|
47
|
+
HERDR_SURFACE_MODE_TAB,
|
|
48
|
+
HERDR_SURFACE_MODES,
|
|
49
|
+
loadHerdrModeConfig,
|
|
47
50
|
loadMuxConfig,
|
|
51
|
+
saveHerdrMode,
|
|
48
52
|
saveMuxPreference,
|
|
49
53
|
SUBAGENT_MUX_BACKENDS,
|
|
54
|
+
type HerdrSurfaceMode,
|
|
50
55
|
type SubagentMuxPreference,
|
|
51
56
|
} from "./mux-config.ts";
|
|
52
57
|
|
|
@@ -968,26 +973,78 @@ function isMuxBackendAvailable(backend: MuxBackend): boolean {
|
|
|
968
973
|
}
|
|
969
974
|
}
|
|
970
975
|
|
|
971
|
-
|
|
972
|
-
|
|
976
|
+
const AUTO_MUX_PREFERENCE: SubagentMuxPreference = "auto";
|
|
977
|
+
const HERDR_MUX_BACKEND: MuxBackend = "herdr";
|
|
978
|
+
const MAX_MUX_CONFIG_TOKENS = 2;
|
|
979
|
+
|
|
980
|
+
interface MuxConfigSelection {
|
|
981
|
+
preference: SubagentMuxPreference;
|
|
982
|
+
herdrMode?: HerdrSurfaceMode;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/** 返回本地化的 Herdr surface 模式名称。 */
|
|
986
|
+
function herdrModeLabel(mode: HerdrSurfaceMode): string {
|
|
987
|
+
return mode === HERDR_SURFACE_MODE_TAB
|
|
988
|
+
? i18n.t("herdrModeTab")
|
|
989
|
+
: i18n.t("herdrModeSplit");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** 格式化当前 mux;Herdr 同时展示 split/tab 子模式。 */
|
|
993
|
+
function muxPreferenceLabel(preference: SubagentMuxPreference, herdrMode?: HerdrSurfaceMode): string {
|
|
994
|
+
if (preference === AUTO_MUX_PREFERENCE) return i18n.t("muxAuto");
|
|
995
|
+
if (preference === HERDR_MUX_BACKEND && herdrMode) {
|
|
996
|
+
return `herdr (${herdrModeLabel(herdrMode)})`;
|
|
997
|
+
}
|
|
998
|
+
return preference;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** 解析 `/config:subagent` 直接参数,Herdr 可追加 split/tab。 */
|
|
1002
|
+
function parseMuxConfigRequest(requested: string): MuxConfigSelection | null {
|
|
1003
|
+
const tokens = requested.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1004
|
+
if (tokens.length === 0 || tokens.length > MAX_MUX_CONFIG_TOKENS) return null;
|
|
1005
|
+
|
|
1006
|
+
const requestedMux = tokens[0];
|
|
1007
|
+
const preference = requestedMux === AUTO_MUX_PREFERENCE || (SUBAGENT_MUX_BACKENDS as readonly string[]).includes(requestedMux)
|
|
1008
|
+
? requestedMux as SubagentMuxPreference
|
|
1009
|
+
: null;
|
|
1010
|
+
if (!preference) return null;
|
|
1011
|
+
|
|
1012
|
+
const requestedMode = tokens[1];
|
|
1013
|
+
if (!requestedMode) return { preference };
|
|
1014
|
+
if (preference !== HERDR_MUX_BACKEND || !(HERDR_SURFACE_MODES as readonly string[]).includes(requestedMode)) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
return { preference, herdrMode: requestedMode as HerdrSurfaceMode };
|
|
973
1018
|
}
|
|
974
1019
|
|
|
1020
|
+
/** 保存 mux 选择,并在 Herdr 选项携带模式时一并持久化。 */
|
|
1021
|
+
function saveMuxConfigSelection(selection: MuxConfigSelection): MuxConfigSelection {
|
|
1022
|
+
const savedMux = saveMuxPreference(selection.preference);
|
|
1023
|
+
const savedMode = selection.herdrMode ? saveHerdrMode(selection.herdrMode).herdrMode : undefined;
|
|
1024
|
+
return { preference: savedMux.mux, herdrMode: savedMode };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** 注册 mux 配置命令及其兼容别名,并持久化用户选择。 */
|
|
975
1028
|
function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
976
1029
|
const command = {
|
|
977
1030
|
description: i18n.t("muxCommandDescription"),
|
|
978
1031
|
handler: async (args, ctx) => {
|
|
979
|
-
const requested = args.trim()
|
|
1032
|
+
const requested = args.trim();
|
|
980
1033
|
if (requested) {
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
: null;
|
|
984
|
-
if (!preference) {
|
|
1034
|
+
const selection = parseMuxConfigRequest(requested);
|
|
1035
|
+
if (!selection) {
|
|
985
1036
|
ctx.ui.notify(i18n.t("muxInvalid", { value: requested }), "warning");
|
|
986
1037
|
return;
|
|
987
1038
|
}
|
|
988
1039
|
try {
|
|
989
|
-
const saved =
|
|
990
|
-
|
|
1040
|
+
const saved = saveMuxConfigSelection(selection);
|
|
1041
|
+
const herdrMode = saved.preference === HERDR_MUX_BACKEND
|
|
1042
|
+
? saved.herdrMode ?? loadHerdrModeConfig().herdrMode
|
|
1043
|
+
: undefined;
|
|
1044
|
+
ctx.ui.notify(
|
|
1045
|
+
i18n.t("muxSaved", { value: muxPreferenceLabel(saved.preference, herdrMode) }),
|
|
1046
|
+
"info",
|
|
1047
|
+
);
|
|
991
1048
|
} catch (error) {
|
|
992
1049
|
ctx.ui.notify(String(error), "error");
|
|
993
1050
|
}
|
|
@@ -1000,20 +1057,38 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1000
1057
|
}
|
|
1001
1058
|
|
|
1002
1059
|
const current = loadMuxConfig();
|
|
1060
|
+
const currentHerdrMode = loadHerdrModeConfig().herdrMode;
|
|
1003
1061
|
const detected = getMuxBackend();
|
|
1004
|
-
const options: Array<
|
|
1062
|
+
const options: Array<MuxConfigSelection & { label: string }> = [
|
|
1005
1063
|
{
|
|
1006
|
-
preference:
|
|
1007
|
-
label: `${current.mux ===
|
|
1064
|
+
preference: AUTO_MUX_PREFERENCE,
|
|
1065
|
+
label: `${current.mux === AUTO_MUX_PREFERENCE ? "●" : "○"} ${i18n.t("muxAuto")} — ${detected ?? i18n.t("muxUnavailable")}`,
|
|
1008
1066
|
},
|
|
1009
|
-
...SUBAGENT_MUX_BACKENDS.map((backend) => ({
|
|
1010
|
-
preference: backend,
|
|
1011
|
-
label: `${current.mux === backend ? "●" : "○"} ${backend} — ${isMuxBackendAvailable(backend) ? i18n.t("muxAvailable") : i18n.t("muxUnavailable")}`,
|
|
1012
|
-
})),
|
|
1013
1067
|
];
|
|
1068
|
+
for (const backend of SUBAGENT_MUX_BACKENDS) {
|
|
1069
|
+
const available = isMuxBackendAvailable(backend)
|
|
1070
|
+
? i18n.t("muxAvailable")
|
|
1071
|
+
: i18n.t("muxUnavailable");
|
|
1072
|
+
if (backend === HERDR_MUX_BACKEND) {
|
|
1073
|
+
for (const herdrMode of HERDR_SURFACE_MODES) {
|
|
1074
|
+
const selected = current.mux === backend && currentHerdrMode === herdrMode;
|
|
1075
|
+
options.push({
|
|
1076
|
+
preference: backend,
|
|
1077
|
+
herdrMode,
|
|
1078
|
+
label: `${selected ? "●" : "○"} ${muxPreferenceLabel(backend, herdrMode)} — ${available}`,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
options.push({
|
|
1084
|
+
preference: backend,
|
|
1085
|
+
label: `${current.mux === backend ? "●" : "○"} ${backend} — ${available}`,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1014
1089
|
const exit = i18n.t("muxExit");
|
|
1015
1090
|
const choice = await ctx.ui.select(
|
|
1016
|
-
i18n.t("muxConfigTitle", { value: muxPreferenceLabel(current.mux) }),
|
|
1091
|
+
i18n.t("muxConfigTitle", { value: muxPreferenceLabel(current.mux, currentHerdrMode) }),
|
|
1017
1092
|
[...options.map((option) => option.label), exit],
|
|
1018
1093
|
);
|
|
1019
1094
|
if (!choice || choice === exit) return;
|
|
@@ -1021,8 +1096,13 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1021
1096
|
const selected = options.find((option) => option.label === choice);
|
|
1022
1097
|
if (!selected) return;
|
|
1023
1098
|
try {
|
|
1024
|
-
const saved =
|
|
1025
|
-
ctx.ui.notify(
|
|
1099
|
+
const saved = saveMuxConfigSelection(selected);
|
|
1100
|
+
ctx.ui.notify(
|
|
1101
|
+
i18n.t("muxSaved", {
|
|
1102
|
+
value: muxPreferenceLabel(saved.preference, saved.herdrMode),
|
|
1103
|
+
}),
|
|
1104
|
+
"info",
|
|
1105
|
+
);
|
|
1026
1106
|
} catch (error) {
|
|
1027
1107
|
ctx.ui.notify(String(error), "error");
|
|
1028
1108
|
}
|
|
@@ -1035,6 +1115,7 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1035
1115
|
|
|
1036
1116
|
export const __test__ = {
|
|
1037
1117
|
borderLine,
|
|
1118
|
+
parseMuxConfigRequest,
|
|
1038
1119
|
getShellReadyDelayMs,
|
|
1039
1120
|
renderSubagentWidgetLines,
|
|
1040
1121
|
loadAgentDefaults,
|
|
@@ -5,14 +5,25 @@ import type { MuxBackend } from "pi-terminal-mux";
|
|
|
5
5
|
|
|
6
6
|
export type SubagentMuxPreference = "auto" | MuxBackend;
|
|
7
7
|
export type SubagentMuxConfigSource = "environment" | "file" | "default";
|
|
8
|
+
export const HERDR_SURFACE_MODE_SPLIT = "split";
|
|
9
|
+
export const HERDR_SURFACE_MODE_TAB = "tab";
|
|
10
|
+
export const HERDR_SURFACE_MODES = [HERDR_SURFACE_MODE_SPLIT, HERDR_SURFACE_MODE_TAB] as const;
|
|
11
|
+
export type HerdrSurfaceMode = (typeof HERDR_SURFACE_MODES)[number];
|
|
8
12
|
|
|
9
13
|
export interface SubagentMuxConfig {
|
|
10
14
|
mux: SubagentMuxPreference;
|
|
11
15
|
source: SubagentMuxConfigSource;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
export interface SubagentHerdrModeConfig {
|
|
19
|
+
herdrMode: HerdrSurfaceMode;
|
|
20
|
+
source: SubagentMuxConfigSource;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
const BACKENDS: readonly MuxBackend[] = ["muxy", "cmux", "tmux", "zellij", "wezterm", "herdr", "otty", "orca"];
|
|
15
24
|
const CONFIG_FILE = "config.json";
|
|
25
|
+
const HERDR_MODE_ENV = "PI_SUBAGENT_HERDR_MODE";
|
|
26
|
+
const DEFAULT_HERDR_MODE: HerdrSurfaceMode = HERDR_SURFACE_MODE_SPLIT;
|
|
16
27
|
|
|
17
28
|
function isMuxBackend(value: unknown): value is MuxBackend {
|
|
18
29
|
return typeof value === "string" && (BACKENDS as readonly string[]).includes(value);
|
|
@@ -23,26 +34,61 @@ function normalizePreference(value: unknown): SubagentMuxPreference | null {
|
|
|
23
34
|
return isMuxBackend(value) ? value : null;
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** 将未知配置值收窄为受支持的 Herdr surface 模式。 */
|
|
38
|
+
function normalizeHerdrMode(value: unknown): HerdrSurfaceMode | null {
|
|
39
|
+
return typeof value === "string" && (HERDR_SURFACE_MODES as readonly string[]).includes(value)
|
|
40
|
+
? value as HerdrSurfaceMode
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
function environmentPreference(): MuxBackend | null {
|
|
27
45
|
const value = (process.env.PI_TERMINAL_MUX ?? process.env.PI_SUBAGENT_MUX ?? "").trim().toLowerCase();
|
|
28
46
|
return isMuxBackend(value) ? value : null;
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
/** 读取有效的 Herdr 模式环境变量;非法值留给 pi-terminal-mux 显式报错。 */
|
|
50
|
+
function environmentHerdrMode(): HerdrSurfaceMode | null {
|
|
51
|
+
return normalizeHerdrMode(process.env[HERDR_MODE_ENV]?.trim().toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 判断用户是否显式提供了非空 Herdr 模式环境变量,包括待报错的非法值。 */
|
|
55
|
+
function hasHerdrModeEnvironmentOverride(): boolean {
|
|
56
|
+
return !!process.env[HERDR_MODE_ENV]?.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
/** User-level config shared by the subagent extension across Pi sessions. */
|
|
32
60
|
export function muxConfigPath(): string {
|
|
33
61
|
return join(getAgentDir(), "extensions", "pi-interactive-subagents", CONFIG_FILE);
|
|
34
62
|
}
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
/** 读取配置对象;不存在、格式错误或非对象时返回空对象。 */
|
|
65
|
+
function readConfigObject(path: string): Record<string, unknown> {
|
|
37
66
|
try {
|
|
38
|
-
if (!existsSync(path)) return
|
|
39
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8")) as
|
|
40
|
-
return
|
|
67
|
+
if (!existsSync(path)) return {};
|
|
68
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
69
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
70
|
+
? parsed as Record<string, unknown>
|
|
71
|
+
: {};
|
|
41
72
|
} catch {
|
|
42
|
-
return
|
|
73
|
+
return {};
|
|
43
74
|
}
|
|
44
75
|
}
|
|
45
76
|
|
|
77
|
+
/** 写入完整配置对象并保留稳定的格式与结尾换行。 */
|
|
78
|
+
function writeConfigObject(path: string, config: Record<string, unknown>): void {
|
|
79
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readStoredPreference(path: string): SubagentMuxPreference | null {
|
|
84
|
+
return normalizePreference(readConfigObject(path).mux);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 读取持久化 Herdr 模式;旧配置缺少字段时返回 null。 */
|
|
88
|
+
function readStoredHerdrMode(path: string): HerdrSurfaceMode | null {
|
|
89
|
+
return normalizeHerdrMode(readConfigObject(path).herdrMode);
|
|
90
|
+
}
|
|
91
|
+
|
|
46
92
|
/** Environment variables override the persisted slash-command setting. */
|
|
47
93
|
export function loadMuxConfig(path = muxConfigPath()): SubagentMuxConfig {
|
|
48
94
|
const env = environmentPreference();
|
|
@@ -54,12 +100,30 @@ export function loadMuxConfig(path = muxConfigPath()): SubagentMuxConfig {
|
|
|
54
100
|
return { mux: "auto", source: "default" };
|
|
55
101
|
}
|
|
56
102
|
|
|
57
|
-
/**
|
|
103
|
+
/** Herdr mode environment variables override the persisted setting; default stays split. */
|
|
104
|
+
export function loadHerdrModeConfig(path = muxConfigPath()): SubagentHerdrModeConfig {
|
|
105
|
+
const env = environmentHerdrMode();
|
|
106
|
+
if (env) return { herdrMode: env, source: "environment" };
|
|
107
|
+
|
|
108
|
+
const stored = readStoredHerdrMode(path);
|
|
109
|
+
if (stored) return { herdrMode: stored, source: "file" };
|
|
110
|
+
|
|
111
|
+
return { herdrMode: DEFAULT_HERDR_MODE, source: "default" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Apply persisted mux and Herdr mode settings when no explicit environment override exists. */
|
|
58
115
|
export function applyPersistedMuxPreference(path = muxConfigPath()): void {
|
|
59
|
-
if (environmentPreference())
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
if (!environmentPreference()) {
|
|
117
|
+
const preference = readStoredPreference(path);
|
|
118
|
+
if (preference && preference !== "auto") {
|
|
119
|
+
process.env.PI_TERMINAL_MUX = preference;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!hasHerdrModeEnvironmentOverride()) {
|
|
124
|
+
const herdrMode = readStoredHerdrMode(path);
|
|
125
|
+
if (herdrMode) process.env[HERDR_MODE_ENV] = herdrMode;
|
|
126
|
+
}
|
|
63
127
|
}
|
|
64
128
|
|
|
65
129
|
/** Persist a slash-command selection and make it effective immediately. */
|
|
@@ -67,20 +131,7 @@ export function saveMuxPreference(preference: SubagentMuxPreference, path = muxC
|
|
|
67
131
|
const normalized = normalizePreference(preference);
|
|
68
132
|
if (!normalized) throw new Error(`Unsupported subagent mux preference: ${String(preference)}`);
|
|
69
133
|
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
if (existsSync(path)) {
|
|
73
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
74
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
75
|
-
existing = parsed as Record<string, unknown>;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// A malformed file is replaced with a valid config by this explicit save.
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
83
|
-
writeFileSync(path, `${JSON.stringify({ ...existing, mux: normalized }, null, 2)}\n`, "utf-8");
|
|
134
|
+
writeConfigObject(path, { ...readConfigObject(path), mux: normalized });
|
|
84
135
|
|
|
85
136
|
if (normalized === "auto") {
|
|
86
137
|
delete process.env.PI_TERMINAL_MUX;
|
|
@@ -93,4 +144,14 @@ export function saveMuxPreference(preference: SubagentMuxPreference, path = muxC
|
|
|
93
144
|
return { mux: normalized, source: "file" };
|
|
94
145
|
}
|
|
95
146
|
|
|
147
|
+
/** Persist a Herdr split/tab selection and make it effective immediately. */
|
|
148
|
+
export function saveHerdrMode(mode: HerdrSurfaceMode, path = muxConfigPath()): SubagentHerdrModeConfig {
|
|
149
|
+
const normalized = normalizeHerdrMode(mode);
|
|
150
|
+
if (!normalized) throw new Error(`Unsupported Herdr surface mode: ${String(mode)}`);
|
|
151
|
+
|
|
152
|
+
writeConfigObject(path, { ...readConfigObject(path), herdrMode: normalized });
|
|
153
|
+
process.env[HERDR_MODE_ENV] = normalized;
|
|
154
|
+
return { herdrMode: normalized, source: "file" };
|
|
155
|
+
}
|
|
156
|
+
|
|
96
157
|
export { BACKENDS as SUBAGENT_MUX_BACKENDS };
|