@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 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
- Status display is controlled by `config.json` in the extension directory. Copy `config.json.example` to get started:
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`。选择会保存到 Pi 的用户扩展配置目录;显式设置的 `PI_TERMINAL_MUX` / `PI_SUBAGENT_MUX` 优先级更高。`/subagent-config` 和 `/pi-subagent-config` 仍作为兼容别名保留。
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
- 状态显示由扩展目录下的 `config.json` 控制。复制 `config.json.example` 开始:
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))
@@ -1,4 +1,5 @@
1
1
  {
2
+ "herdrMode": "split",
2
3
  "status": {
3
4
  "enabled": true
4
5
  }
@@ -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 terminal multiplexer used by subagents"
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{value}。可选:auto、muxy、cmux、tmux、zellij、wezterm、herdr、otty、orca。",
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.9.0",
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.3.1"
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
- function muxPreferenceLabel(preference: SubagentMuxPreference): string {
972
- return preference === "auto" ? i18n.t("muxAuto") : preference;
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().toLowerCase();
1032
+ const requested = args.trim();
980
1033
  if (requested) {
981
- const preference = requested === "auto" || (SUBAGENT_MUX_BACKENDS as readonly string[]).includes(requested)
982
- ? requested as SubagentMuxPreference
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 = saveMuxPreference(preference);
990
- ctx.ui.notify(i18n.t("muxSaved", { value: muxPreferenceLabel(saved.mux) }), "info");
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<{ preference: SubagentMuxPreference; label: string }> = [
1062
+ const options: Array<MuxConfigSelection & { label: string }> = [
1005
1063
  {
1006
- preference: "auto",
1007
- label: `${current.mux === "auto" ? "●" : "○"} ${i18n.t("muxAuto")} — ${detected ?? i18n.t("muxUnavailable")}`,
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 = saveMuxPreference(selected.preference);
1025
- ctx.ui.notify(i18n.t("muxSaved", { value: muxPreferenceLabel(saved.mux) }), "info");
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
- function readStoredPreference(path: string): SubagentMuxPreference | null {
64
+ /** 读取配置对象;不存在、格式错误或非对象时返回空对象。 */
65
+ function readConfigObject(path: string): Record<string, unknown> {
37
66
  try {
38
- if (!existsSync(path)) return null;
39
- const parsed = JSON.parse(readFileSync(path, "utf-8")) as { mux?: unknown };
40
- return normalizePreference(parsed.mux);
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 null;
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
- /** Apply the persisted setting when no explicit environment override exists. */
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()) return;
60
- const preference = readStoredPreference(path);
61
- if (!preference || preference === "auto") return;
62
- process.env.PI_TERMINAL_MUX = preference;
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
- let existing: Record<string, unknown> = {};
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 };