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 +26 -3
- package/dist/{api-DtthDVIa.js → api-BA6XuDOE.js} +363 -49
- package/dist/api-BA6XuDOE.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -4
- package/src/extensions/prompt-presets.test.ts +38 -0
- package/src/extensions/prompt-presets.ts +385 -10
- package/src/mcp/client.test.ts +217 -0
- package/src/mcp/client.ts +219 -45
- package/src/mcp/config.test.ts +141 -93
- package/src/mcp/config.ts +110 -22
- package/dist/api-DtthDVIa.js.map +0 -1
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.
|
|
116
|
-
via
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
ctx.ui.
|
|
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
|
-
|
|
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-
|
|
2908
|
+
//# sourceMappingURL=api-BA6XuDOE.js.map
|