my-pi 0.0.7 → 0.0.8

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
@@ -112,8 +112,9 @@ await runPrintMode(runtime, {
112
112
  ## MCP Servers
113
113
 
114
114
  MCP servers are configured via `mcp.json` files and managed as a pi
115
- extension. Servers are spawned on startup and their tools registered
116
- via `pi.registerTool()`.
115
+ extension. Stdio servers are spawned on startup, HTTP servers are
116
+ connected remotely, and their tools are registered via
117
+ `pi.registerTool()`.
117
118
 
118
119
  ### Global config
119
120
 
@@ -148,6 +149,25 @@ via `pi.registerTool()`.
148
149
  }
149
150
  ```
150
151
 
152
+ HTTP MCP servers are supported too:
153
+
154
+ ```json
155
+ {
156
+ "mcpServers": {
157
+ "pm-platform": {
158
+ "type": "http",
159
+ "url": "https://project.cloudlobsters.com/api/mcp",
160
+ "headers": {
161
+ "Authorization": "Bearer ..."
162
+ }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ Use `"type": "http"` or `"type": "streamable-http"` for remote MCP
169
+ servers. If `url` is present, my-pi treats the entry as HTTP.
170
+
151
171
  Project servers merge with global servers. If both define the same
152
172
  server name, the project config wins.
153
173
 
@@ -185,7 +205,7 @@ In interactive mode:
185
205
  ### How it works
186
206
 
187
207
  1. Pi extension loads `mcp.json` configs (global + project)
188
- 2. Spawns each MCP server as a child process (stdio transport)
208
+ 2. Connects to each MCP server using stdio or HTTP transport
189
209
  3. Performs the MCP `initialize` handshake
190
210
  4. Calls `tools/list` to discover available tools
191
211
  5. Registers each tool via `pi.registerTool()` as
@@ -272,6 +292,9 @@ CLI layering is supported too:
272
292
  - `--system-prompt "You are terse and technical."`
273
293
  - `--append-system-prompt "Prefer one short paragraph."`
274
294
 
295
+ Interactive `/preset` selections are also restored on later sessions
296
+ for the same project via `~/.pi/agent/prompt-preset-state.json`.
297
+
275
298
  This repo also includes an example `.pi/presets.json` with sample base
276
299
  presets and layers.
277
300
 
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { Type } from "@sinclair/typebox";
6
6
  import { execFileSync, spawn } from "node:child_process";
7
7
  import { homedir } from "node:os";
8
- import { Container, SettingsList, Text } from "@mariozechner/pi-tui";
8
+ import { Container, SettingsList, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
9
9
  import { createHash } from "node:crypto";
10
10
  //#region src/extensions/chain.ts
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -821,40 +821,12 @@ var McpClient = class {
821
821
  #nextId = 1;
822
822
  #pending = /* @__PURE__ */ new Map();
823
823
  #buffer = "";
824
+ #sessionId;
824
825
  constructor(config) {
825
826
  this.#config = config;
826
827
  }
827
828
  async connect() {
828
- const { command, args = [], env } = this.#config;
829
- this.#proc = spawn(command, args, {
830
- stdio: [
831
- "pipe",
832
- "pipe",
833
- "pipe"
834
- ],
835
- env: {
836
- ...process.env,
837
- ...env
838
- }
839
- });
840
- this.#proc.stdout.setEncoding("utf8");
841
- this.#proc.stdout.on("data", (chunk) => {
842
- this.#buffer += chunk;
843
- const lines = this.#buffer.split("\n");
844
- this.#buffer = lines.pop() || "";
845
- for (const line of lines) {
846
- if (!line.trim()) continue;
847
- try {
848
- const msg = JSON.parse(line);
849
- if (msg.id != null && this.#pending.has(msg.id)) {
850
- const p = this.#pending.get(msg.id);
851
- this.#pending.delete(msg.id);
852
- if (msg.error) p.reject(/* @__PURE__ */ new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
853
- else p.resolve(msg.result);
854
- }
855
- } catch {}
856
- }
857
- });
829
+ if (this.#config.transport === "stdio") await this.#connect_stdio();
858
830
  await this.#request("initialize", {
859
831
  protocolVersion: "2024-11-05",
860
832
  capabilities: {},
@@ -863,7 +835,7 @@ var McpClient = class {
863
835
  version: "0.0.1"
864
836
  }
865
837
  });
866
- this.#send({
838
+ await this.#send({
867
839
  jsonrpc: "2.0",
868
840
  method: "notifications/initialized"
869
841
  });
@@ -878,12 +850,39 @@ var McpClient = class {
878
850
  });
879
851
  }
880
852
  async disconnect() {
853
+ if (this.#config.transport === "http") await this.#disconnect_http();
881
854
  if (this.#proc) {
882
855
  this.#proc.kill();
883
856
  this.#proc = null;
884
857
  }
885
858
  this.#pending.clear();
886
859
  }
860
+ async #connect_stdio() {
861
+ const { command, args = [], env } = this.#config;
862
+ this.#proc = spawn(command, args, {
863
+ stdio: [
864
+ "pipe",
865
+ "pipe",
866
+ "pipe"
867
+ ],
868
+ env: {
869
+ ...process.env,
870
+ ...env
871
+ }
872
+ });
873
+ this.#proc.stdout.setEncoding("utf8");
874
+ this.#proc.stdout.on("data", (chunk) => {
875
+ this.#buffer += chunk;
876
+ const lines = this.#buffer.split("\n");
877
+ this.#buffer = lines.pop() || "";
878
+ for (const line of lines) {
879
+ if (!line.trim()) continue;
880
+ try {
881
+ this.#handle_message(JSON.parse(line));
882
+ } catch {}
883
+ }
884
+ });
885
+ }
887
886
  #request(method, params) {
888
887
  return new Promise((resolve, reject) => {
889
888
  const id = this.#nextId++;
@@ -896,6 +895,11 @@ var McpClient = class {
896
895
  id,
897
896
  method,
898
897
  params
898
+ }).catch((error) => {
899
+ if (this.#pending.has(id)) {
900
+ this.#pending.delete(id);
901
+ reject(error);
902
+ }
899
903
  });
900
904
  setTimeout(() => {
901
905
  if (this.#pending.has(id)) {
@@ -905,13 +909,156 @@ var McpClient = class {
905
909
  }, 3e4);
906
910
  });
907
911
  }
908
- #send(msg) {
912
+ async #send(msg) {
913
+ if (this.#config.transport === "http") {
914
+ await this.#send_http(msg);
915
+ return;
916
+ }
909
917
  if (!this.#proc?.stdin?.writable) throw new Error("MCP server not connected");
910
918
  this.#proc.stdin.write(JSON.stringify(msg) + "\n");
911
919
  }
920
+ async #send_http(msg) {
921
+ const config = this.#config;
922
+ const headers = new Headers(config.headers ?? {});
923
+ headers.set("content-type", "application/json");
924
+ headers.set("accept", "application/json, text/event-stream");
925
+ if (this.#sessionId) headers.set("mcp-session-id", this.#sessionId);
926
+ const response = await fetch(config.url, {
927
+ method: "POST",
928
+ headers,
929
+ body: JSON.stringify(msg)
930
+ });
931
+ const sessionId = response.headers.get("mcp-session-id");
932
+ if (sessionId) this.#sessionId = sessionId;
933
+ if (!response.ok) {
934
+ const body = await response.text().catch(() => "");
935
+ throw new Error(`MCP HTTP ${response.status}${body ? `: ${body}` : ""}`);
936
+ }
937
+ if (response.status === 204) return;
938
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
939
+ await this.#consume_sse_response(response, config.name);
940
+ return;
941
+ }
942
+ const body = await response.text();
943
+ if (!body.trim()) return;
944
+ let parsed;
945
+ try {
946
+ parsed = JSON.parse(body);
947
+ } catch {
948
+ throw new Error(`Invalid MCP HTTP response from ${config.name}: ${body.slice(0, 200)}`);
949
+ }
950
+ this.#dispatch_message(parsed);
951
+ }
952
+ async #disconnect_http() {
953
+ const config = this.#config;
954
+ if (!this.#sessionId) return;
955
+ const headers = new Headers(config.headers ?? {});
956
+ headers.set("mcp-session-id", this.#sessionId);
957
+ const response = await fetch(config.url, {
958
+ method: "DELETE",
959
+ headers
960
+ });
961
+ if (response.status !== 405 && !response.ok) {
962
+ const body = await response.text().catch(() => "");
963
+ throw new Error(`MCP HTTP disconnect ${response.status}${body ? `: ${body}` : ""}`);
964
+ }
965
+ this.#sessionId = void 0;
966
+ }
967
+ async #consume_sse_response(response, server_name) {
968
+ if (!response.body) return;
969
+ const reader = response.body.getReader();
970
+ const decoder = new TextDecoder();
971
+ let buffer = "";
972
+ let event_lines = [];
973
+ const flush_event = () => {
974
+ if (event_lines.length === 0) return;
975
+ const data_lines = event_lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
976
+ event_lines = [];
977
+ if (data_lines.length === 0) return;
978
+ const payload = data_lines.join("\n").trim();
979
+ if (!payload) return;
980
+ try {
981
+ this.#dispatch_message(JSON.parse(payload));
982
+ } catch {
983
+ throw new Error(`Invalid MCP SSE payload from ${server_name}: ${payload.slice(0, 200)}`);
984
+ }
985
+ };
986
+ while (true) {
987
+ const { done, value } = await reader.read();
988
+ buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
989
+ const lines = buffer.replace(/\r\n/g, "\n").split("\n");
990
+ buffer = lines.pop() ?? "";
991
+ for (const line of lines) {
992
+ if (line === "") {
993
+ flush_event();
994
+ continue;
995
+ }
996
+ if (line.startsWith(":")) continue;
997
+ event_lines.push(line);
998
+ }
999
+ if (done) break;
1000
+ }
1001
+ if (buffer.trim()) event_lines.push(buffer.trim());
1002
+ flush_event();
1003
+ }
1004
+ #dispatch_message(message) {
1005
+ if (Array.isArray(message)) {
1006
+ for (const item of message) this.#dispatch_message(item);
1007
+ return;
1008
+ }
1009
+ if (!message || typeof message !== "object") return;
1010
+ this.#handle_message(message);
1011
+ }
1012
+ #handle_message(msg) {
1013
+ if (msg.id == null || !this.#pending.has(msg.id)) return;
1014
+ const pending = this.#pending.get(msg.id);
1015
+ this.#pending.delete(msg.id);
1016
+ if (msg.error) {
1017
+ pending.reject(/* @__PURE__ */ new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
1018
+ return;
1019
+ }
1020
+ pending.resolve(msg.result);
1021
+ }
912
1022
  };
913
1023
  //#endregion
914
1024
  //#region src/mcp/config.ts
1025
+ function is_string_record(value, label, name) {
1026
+ if (value === void 0) return true;
1027
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid MCP server "${name}": ${label} must be an object of string values`);
1028
+ for (const [key, entry] of Object.entries(value)) if (typeof entry !== "string") throw new Error(`Invalid MCP server "${name}": ${label}.${key} must be a string`);
1029
+ return true;
1030
+ }
1031
+ function parse_server(name, entry) {
1032
+ const type = typeof entry.type === "string" ? entry.type.trim().toLowerCase() : "";
1033
+ if (type && ![
1034
+ "stdio",
1035
+ "http",
1036
+ "streamable-http"
1037
+ ].includes(type)) throw new Error(`Invalid MCP server "${name}": unsupported transport type "${type}"`);
1038
+ if (type === "http" || type === "streamable-http" || entry.url !== void 0) {
1039
+ if (typeof entry.url !== "string" || !entry.url.trim()) throw new Error(`Invalid MCP server "${name}": http transport requires a url`);
1040
+ is_string_record(entry.headers, "headers", name);
1041
+ const headers = entry.headers;
1042
+ return {
1043
+ name,
1044
+ transport: "http",
1045
+ url: entry.url.trim(),
1046
+ ...headers ? { headers } : {}
1047
+ };
1048
+ }
1049
+ if (typeof entry.command !== "string" || !entry.command.trim()) throw new Error(`Invalid MCP server "${name}": stdio transport requires a command`);
1050
+ if (entry.args !== void 0 && (!Array.isArray(entry.args) || entry.args.some((value) => typeof value !== "string"))) throw new Error(`Invalid MCP server "${name}": args must be an array of strings`);
1051
+ is_string_record(entry.env, "env", name);
1052
+ const args = entry.args;
1053
+ const env = entry.env;
1054
+ return {
1055
+ name,
1056
+ transport: "stdio",
1057
+ command: entry.command.trim(),
1058
+ ...args ? { args } : {},
1059
+ ...env ? { env } : {}
1060
+ };
1061
+ }
915
1062
  function read_config(path) {
916
1063
  if (!existsSync(path)) return {};
917
1064
  const raw = readFileSync(path, "utf-8");
@@ -924,12 +1071,7 @@ function load_mcp_config(cwd) {
924
1071
  ...global_servers,
925
1072
  ...project_servers
926
1073
  };
927
- return Object.entries(merged).map(([name, server]) => ({
928
- name,
929
- command: server.command,
930
- args: server.args,
931
- env: server.env
932
- }));
1074
+ return Object.entries(merged).map(([name, server]) => parse_server(name, server));
933
1075
  }
934
1076
  //#endregion
935
1077
  //#region src/extensions/mcp.ts
@@ -1144,6 +1286,9 @@ function get_global_presets_path() {
1144
1286
  function get_project_presets_path(cwd) {
1145
1287
  return join(cwd, ".pi", "presets.json");
1146
1288
  }
1289
+ function get_persisted_prompt_state_path() {
1290
+ return join(getAgentDir(), "prompt-preset-state.json");
1291
+ }
1147
1292
  function read_prompt_presets_file(path) {
1148
1293
  if (!existsSync(path)) return {};
1149
1294
  try {
@@ -1195,6 +1340,61 @@ function remove_project_prompt_preset(cwd, name) {
1195
1340
  remaining
1196
1341
  };
1197
1342
  }
1343
+ function normalize_prompt_preset_state(input) {
1344
+ if (!input || typeof input !== "object") return void 0;
1345
+ const candidate = input;
1346
+ return {
1347
+ base_name: typeof candidate.base_name === "string" && candidate.base_name.trim() ? candidate.base_name.trim() : null,
1348
+ layer_names: Array.isArray(candidate.layer_names) ? [...new Set(candidate.layer_names.filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim()))].sort() : []
1349
+ };
1350
+ }
1351
+ function read_persisted_prompt_states(path = get_persisted_prompt_state_path()) {
1352
+ if (!existsSync(path)) return {
1353
+ version: 1,
1354
+ projects: {}
1355
+ };
1356
+ try {
1357
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1358
+ const raw_projects = parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {};
1359
+ const projects = {};
1360
+ for (const [cwd, value] of Object.entries(raw_projects)) {
1361
+ const normalized = normalize_prompt_preset_state(value);
1362
+ if (!normalized) continue;
1363
+ projects[cwd] = normalized;
1364
+ }
1365
+ return {
1366
+ version: typeof parsed.version === "number" ? parsed.version : 1,
1367
+ projects
1368
+ };
1369
+ } catch {
1370
+ return {
1371
+ version: 1,
1372
+ projects: {}
1373
+ };
1374
+ }
1375
+ }
1376
+ function load_persisted_prompt_state(cwd, path = get_persisted_prompt_state_path()) {
1377
+ return read_persisted_prompt_states(path).projects[cwd];
1378
+ }
1379
+ function save_persisted_prompt_state(cwd, state, path = get_persisted_prompt_state_path()) {
1380
+ const persisted = read_persisted_prompt_states(path);
1381
+ persisted.projects[cwd] = normalize_prompt_preset_state(state) ?? {
1382
+ base_name: null,
1383
+ layer_names: []
1384
+ };
1385
+ const dir = dirname(path);
1386
+ if (!existsSync(dir)) mkdirSync(dir, {
1387
+ recursive: true,
1388
+ mode: 448
1389
+ });
1390
+ const tmp = `${path}.tmp-${Date.now()}`;
1391
+ writeFileSync(tmp, JSON.stringify({
1392
+ version: 1,
1393
+ projects: Object.fromEntries(Object.entries(persisted.projects).sort(([a], [b]) => a.localeCompare(b)))
1394
+ }, null, " ") + "\n", { mode: 384 });
1395
+ renameSync(tmp, path);
1396
+ return path;
1397
+ }
1198
1398
  function get_last_preset_state(ctx) {
1199
1399
  const entries = ctx.sessionManager.getEntries();
1200
1400
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -1258,16 +1458,129 @@ function format_active_details(active_base_name, active_layers, presets) {
1258
1458
  }
1259
1459
  return parts.join("\n") || "No preset or layers active";
1260
1460
  }
1461
+ function get_footer_prompt_status(active_base_name, active_layers) {
1462
+ if (!active_base_name && active_layers.size === 0) return;
1463
+ return `prompt:${active_base_name ?? "none"}${active_layers.size > 0 ? ` +${active_layers.size}` : ""}`;
1464
+ }
1465
+ function sanitize_status_text(text) {
1466
+ return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
1467
+ }
1468
+ function format_token_count(count) {
1469
+ if (count < 1e3) return count.toString();
1470
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
1471
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
1472
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
1473
+ return `${Math.round(count / 1e6)}M`;
1474
+ }
1475
+ function get_current_thinking_level(ctx) {
1476
+ const entries = ctx.sessionManager.getEntries();
1477
+ for (let i = entries.length - 1; i >= 0; i--) {
1478
+ const entry = entries[i];
1479
+ if (entry.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") return entry.thinkingLevel;
1480
+ }
1481
+ return ctx.model?.reasoning ? "high" : "off";
1482
+ }
1483
+ function render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers) {
1484
+ let total_input = 0;
1485
+ let total_output = 0;
1486
+ let total_cache_read = 0;
1487
+ let total_cache_write = 0;
1488
+ let total_cost = 0;
1489
+ for (const entry of ctx.sessionManager.getEntries()) if (entry.type === "message" && entry.message.role === "assistant") {
1490
+ total_input += entry.message.usage.input;
1491
+ total_output += entry.message.usage.output;
1492
+ total_cache_read += entry.message.usage.cacheRead;
1493
+ total_cache_write += entry.message.usage.cacheWrite;
1494
+ total_cost += entry.message.usage.cost.total;
1495
+ }
1496
+ const context_usage = ctx.getContextUsage();
1497
+ const context_window = context_usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
1498
+ const context_percent_value = context_usage?.percent ?? 0;
1499
+ const context_percent = context_usage?.percent !== null ? context_percent_value.toFixed(1) : "?";
1500
+ let pwd = ctx.cwd;
1501
+ const home = process.env.HOME || process.env.USERPROFILE;
1502
+ if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
1503
+ const branch = footer_data.getGitBranch();
1504
+ if (branch) pwd = `${pwd} (${branch})`;
1505
+ const session_name = ctx.sessionManager.getSessionName();
1506
+ if (session_name) pwd = `${pwd} • ${session_name}`;
1507
+ const stats_parts = [];
1508
+ if (total_input) stats_parts.push(`↑${format_token_count(total_input)}`);
1509
+ if (total_output) stats_parts.push(`↓${format_token_count(total_output)}`);
1510
+ if (total_cache_read) stats_parts.push(`R${format_token_count(total_cache_read)}`);
1511
+ if (total_cache_write) stats_parts.push(`W${format_token_count(total_cache_write)}`);
1512
+ const using_subscription = ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
1513
+ if (total_cost || using_subscription) stats_parts.push(`$${total_cost.toFixed(3)}${using_subscription ? " (sub)" : ""}`);
1514
+ const context_percent_display = context_percent === "?" ? `?/${format_token_count(context_window)}` : `${context_percent}%/${format_token_count(context_window)}`;
1515
+ let context_percent_str = context_percent_display;
1516
+ if (context_percent_value > 90) context_percent_str = theme.fg("error", context_percent_display);
1517
+ else if (context_percent_value > 70) context_percent_str = theme.fg("warning", context_percent_display);
1518
+ stats_parts.push(context_percent_str);
1519
+ let stats_left = stats_parts.join(" ");
1520
+ let stats_left_width = visibleWidth(stats_left);
1521
+ if (stats_left_width > width) {
1522
+ stats_left = truncateToWidth(stats_left, width, "...");
1523
+ stats_left_width = visibleWidth(stats_left);
1524
+ }
1525
+ const model_name = ctx.model?.id || "no-model";
1526
+ const thinking_level = get_current_thinking_level(ctx);
1527
+ let right_side_without_provider = model_name;
1528
+ if (ctx.model?.reasoning) right_side_without_provider = thinking_level === "off" ? `${model_name} • thinking off` : `${model_name} • ${thinking_level}`;
1529
+ let right_side = right_side_without_provider;
1530
+ if (footer_data.getAvailableProviderCount() > 1 && ctx.model) {
1531
+ right_side = `(${ctx.model.provider}) ${right_side_without_provider}`;
1532
+ if (stats_left_width + 2 + visibleWidth(right_side) > width) right_side = right_side_without_provider;
1533
+ }
1534
+ const right_side_width = visibleWidth(right_side);
1535
+ const total_needed = stats_left_width + 2 + right_side_width;
1536
+ let stats_line;
1537
+ if (total_needed <= width) {
1538
+ const padding = " ".repeat(width - stats_left_width - right_side_width);
1539
+ stats_line = stats_left + padding + right_side;
1540
+ } else {
1541
+ const available_for_right = width - stats_left_width - 2;
1542
+ if (available_for_right > 0) {
1543
+ const truncated_right = truncateToWidth(right_side, available_for_right, "");
1544
+ const truncated_right_width = visibleWidth(truncated_right);
1545
+ const padding = " ".repeat(Math.max(0, width - stats_left_width - truncated_right_width));
1546
+ stats_line = stats_left + padding + truncated_right;
1547
+ } else stats_line = stats_left;
1548
+ }
1549
+ const dim_stats_left = theme.fg("dim", stats_left);
1550
+ const remainder = stats_line.slice(stats_left.length);
1551
+ const dim_remainder = theme.fg("dim", remainder);
1552
+ const lines = [truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")), dim_stats_left + dim_remainder];
1553
+ const prompt_status = get_footer_prompt_status(active_base_name, active_layers);
1554
+ if (prompt_status) {
1555
+ const themed_status = theme.fg("dim", prompt_status);
1556
+ const status_width = visibleWidth(themed_status);
1557
+ const aligned_status = status_width >= width ? truncateToWidth(themed_status, width, theme.fg("dim", "...")) : `${" ".repeat(width - status_width)}${themed_status}`;
1558
+ lines.push(aligned_status);
1559
+ }
1560
+ const other_statuses = Array.from(footer_data.getExtensionStatuses().entries()).filter(([key]) => key !== "preset").sort(([a], [b]) => a.localeCompare(b)).map(([, text]) => sanitize_status_text(text));
1561
+ if (other_statuses.length > 0) lines.push(truncateToWidth(other_statuses.join(" "), width, theme.fg("dim", "...")));
1562
+ return lines;
1563
+ }
1261
1564
  function set_status(ctx, active_base_name, active_layers) {
1262
- const label = active_base_name ?? "none";
1263
- const layer_suffix = active_layers.size > 0 ? ` +${active_layers.size}` : "";
1264
- ctx.ui.setStatus("preset", `prompt:${label}${layer_suffix}`);
1565
+ ctx.ui.setStatus("preset", void 0);
1566
+ if (!ctx.hasUI) return;
1567
+ ctx.ui.setFooter((tui, theme, footer_data) => {
1568
+ return {
1569
+ dispose: footer_data.onBranchChange(() => tui.requestRender()),
1570
+ invalidate() {},
1571
+ render(width) {
1572
+ return render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers);
1573
+ }
1574
+ };
1575
+ });
1265
1576
  }
1266
- function persist_state(pi, active_base_name, active_layers) {
1267
- pi.appendEntry(PRESET_STATE_TYPE, {
1577
+ function persist_state(pi, ctx, active_base_name, active_layers) {
1578
+ const state = {
1268
1579
  base_name: active_base_name ?? null,
1269
1580
  layer_names: [...active_layers].sort()
1270
- });
1581
+ };
1582
+ pi.appendEntry(PRESET_STATE_TYPE, state);
1583
+ save_persisted_prompt_state(ctx.cwd, state);
1271
1584
  }
1272
1585
  function normalize_active_state(presets, active_base_name, active_layers) {
1273
1586
  return {
@@ -1308,7 +1621,7 @@ async function prompt_presets(pi) {
1308
1621
  active_base_name = next_base_name;
1309
1622
  active_layers = new Set(next_layers);
1310
1623
  set_status(ctx, active_base_name, active_layers);
1311
- if (options?.persist !== false) persist_state(pi, active_base_name, active_layers);
1624
+ if (options?.persist !== false) persist_state(pi, ctx, active_base_name, active_layers);
1312
1625
  if (options?.notify) ctx.ui.notify(options.notify, "info");
1313
1626
  }
1314
1627
  function activate_base(name, ctx, options) {
@@ -1384,7 +1697,7 @@ async function prompt_presets(pi) {
1384
1697
  active_base_name = normalized.active_base_name;
1385
1698
  active_layers = normalized.active_layers;
1386
1699
  set_status(ctx, active_base_name, active_layers);
1387
- persist_state(pi, active_base_name, active_layers);
1700
+ persist_state(pi, ctx, active_base_name, active_layers);
1388
1701
  const fallback = presets[name];
1389
1702
  if (mode === "reset" && fallback) {
1390
1703
  ctx.ui.notify(`Reset "${name}" to ${get_prompt_source_label(fallback.source)} preset`, "info");
@@ -1681,7 +1994,7 @@ async function prompt_presets(pi) {
1681
1994
  set_status(ctx, active_base_name, active_layers);
1682
1995
  return;
1683
1996
  }
1684
- const restored = get_last_preset_state(ctx);
1997
+ const restored = get_last_preset_state(ctx) ?? load_persisted_prompt_state(ctx.cwd);
1685
1998
  if (restored) {
1686
1999
  active_base_name = restored.base_name ?? void 0;
1687
2000
  active_layers = new Set(restored.layer_names ?? []);
@@ -1702,6 +2015,7 @@ async function prompt_presets(pi) {
1702
2015
  });
1703
2016
  pi.on("session_shutdown", async (_event, ctx) => {
1704
2017
  ctx.ui.setStatus("preset", void 0);
2018
+ ctx.ui.setFooter(void 0);
1705
2019
  });
1706
2020
  }
1707
2021
  //#endregion
@@ -2591,4 +2905,4 @@ async function create_my_pi(options = {}) {
2591
2905
  //#endregion
2592
2906
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
2593
2907
 
2594
- //# sourceMappingURL=api-DtthDVIa.js.map
2908
+ //# sourceMappingURL=api-BA6XuDOE.js.map