my-pi 0.0.7 → 0.0.9

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.
@@ -1,11 +1,13 @@
1
1
  import { InteractiveMode as InteractiveMode$1, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, defineTool, getAgentDir, parseFrontmatter, runPrintMode as runPrintMode$1 } from "@mariozechner/pi-coding-agent";
2
2
  import { cpSync, existsSync, globSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
- import { basename, dirname, join, relative, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } 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
+ import { readFile } from "node:fs/promises";
10
+ import { EventEmitter } from "node:events";
9
11
  import { createHash } from "node:crypto";
10
12
  //#region src/extensions/chain.ts
11
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -399,6 +401,13 @@ const BUILTIN_EXTENSIONS = [
399
401
  "preset",
400
402
  "presets"
401
403
  ]
404
+ },
405
+ {
406
+ key: "lsp",
407
+ label: "LSP",
408
+ description: "Language Server Protocol tools (diagnostics, hover, definition, references)",
409
+ cli_flag: "--no-lsp",
410
+ aliases: ["lsp", "language-server"]
402
411
  }
403
412
  ];
404
413
  function get_builtin_extensions_config_path() {
@@ -814,47 +823,752 @@ ${task || "Continue from where the previous session left off."}
814
823
  });
815
824
  }
816
825
  //#endregion
817
- //#region src/mcp/client.ts
818
- var McpClient = class {
826
+ //#region src/lsp/client.ts
827
+ var LspClient = class extends EventEmitter {
819
828
  #proc = null;
820
- #config;
821
- #nextId = 1;
829
+ #options;
830
+ #next_id = 1;
822
831
  #pending = /* @__PURE__ */ new Map();
823
- #buffer = "";
824
- constructor(config) {
825
- this.#config = config;
832
+ #buffer = Buffer.alloc(0);
833
+ #initialized = false;
834
+ #open_docs = /* @__PURE__ */ new Map();
835
+ #diagnostics_by_uri = /* @__PURE__ */ new Map();
836
+ constructor(options) {
837
+ super();
838
+ this.#options = options;
826
839
  }
827
- async connect() {
828
- const { command, args = [], env } = this.#config;
829
- this.#proc = spawn(command, args, {
840
+ async start() {
841
+ this.#proc = spawn(this.#options.command, this.#options.args, {
830
842
  stdio: [
831
843
  "pipe",
832
844
  "pipe",
833
845
  "pipe"
834
846
  ],
835
- env: {
836
- ...process.env,
837
- ...env
847
+ env: process.env
848
+ });
849
+ this.#proc.on("error", (err) => {
850
+ this.emit("error", err);
851
+ });
852
+ this.#proc.on("close", () => {
853
+ for (const pending of this.#pending.values()) {
854
+ clearTimeout(pending.timer);
855
+ pending.reject(/* @__PURE__ */ new Error("LSP server closed"));
838
856
  }
857
+ this.#pending.clear();
858
+ this.#initialized = false;
859
+ this.#proc = null;
839
860
  });
840
- this.#proc.stdout.setEncoding("utf8");
861
+ this.#proc.stderr?.on("data", () => {});
841
862
  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 {}
863
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
864
+ this.#drain_buffer();
865
+ });
866
+ await this.#request("initialize", {
867
+ processId: process.pid,
868
+ rootUri: this.#options.root_uri,
869
+ capabilities: {
870
+ textDocument: {
871
+ publishDiagnostics: { relatedInformation: true },
872
+ hover: { contentFormat: ["markdown", "plaintext"] },
873
+ definition: { linkSupport: false },
874
+ references: {},
875
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true }
876
+ },
877
+ workspace: {
878
+ workspaceFolders: true,
879
+ symbol: {}
880
+ }
881
+ },
882
+ workspaceFolders: [{
883
+ uri: this.#options.root_uri,
884
+ name: "workspace"
885
+ }]
886
+ });
887
+ this.#notify("initialized", {});
888
+ this.#initialized = true;
889
+ }
890
+ is_ready() {
891
+ return this.#initialized;
892
+ }
893
+ async ensure_document_open(uri, text) {
894
+ const existing = this.#open_docs.get(uri);
895
+ if (existing) {
896
+ if (existing.text === text) return;
897
+ const next_version = existing.version + 1;
898
+ this.#open_docs.set(uri, {
899
+ version: next_version,
900
+ text
901
+ });
902
+ this.#notify("textDocument/didChange", {
903
+ textDocument: {
904
+ uri,
905
+ version: next_version
906
+ },
907
+ contentChanges: [{ text }]
908
+ });
909
+ return;
910
+ }
911
+ const language_id = this.#options.language_id_for_uri(uri) ?? "plaintext";
912
+ this.#open_docs.set(uri, {
913
+ version: 1,
914
+ text
915
+ });
916
+ this.#notify("textDocument/didOpen", { textDocument: {
917
+ uri,
918
+ languageId: language_id,
919
+ version: 1,
920
+ text
921
+ } });
922
+ }
923
+ async hover(uri, position) {
924
+ return await this.#request("textDocument/hover", {
925
+ textDocument: { uri },
926
+ position
927
+ }) ?? null;
928
+ }
929
+ async definition(uri, position) {
930
+ return normalize_location_result(await this.#request("textDocument/definition", {
931
+ textDocument: { uri },
932
+ position
933
+ }));
934
+ }
935
+ async references(uri, position, include_declaration) {
936
+ return await this.#request("textDocument/references", {
937
+ textDocument: { uri },
938
+ position,
939
+ context: { includeDeclaration: include_declaration }
940
+ }) ?? [];
941
+ }
942
+ async document_symbols(uri) {
943
+ return normalize_document_symbol_result(await this.#request("textDocument/documentSymbol", { textDocument: { uri } }));
944
+ }
945
+ get_diagnostics(uri) {
946
+ return this.#diagnostics_by_uri.get(uri) ?? [];
947
+ }
948
+ get_all_diagnostics() {
949
+ return new Map(this.#diagnostics_by_uri);
950
+ }
951
+ async wait_for_diagnostics(uri, timeout_ms = 1500) {
952
+ if (this.#diagnostics_by_uri.has(uri)) return this.get_diagnostics(uri);
953
+ return new Promise((resolve) => {
954
+ const handler = (event_uri) => {
955
+ if (event_uri !== uri) return;
956
+ this.off("diagnostics", handler);
957
+ clearTimeout(timer);
958
+ resolve(this.get_diagnostics(uri));
959
+ };
960
+ const timer = setTimeout(() => {
961
+ this.off("diagnostics", handler);
962
+ resolve(this.get_diagnostics(uri));
963
+ }, timeout_ms);
964
+ this.on("diagnostics", handler);
965
+ });
966
+ }
967
+ async stop() {
968
+ if (this.#initialized) try {
969
+ await this.#request("shutdown", null, 1e3);
970
+ this.#notify("exit", null);
971
+ } catch {}
972
+ for (const pending of this.#pending.values()) {
973
+ clearTimeout(pending.timer);
974
+ pending.reject(/* @__PURE__ */ new Error("LSP client stopped"));
975
+ }
976
+ this.#pending.clear();
977
+ if (this.#proc) {
978
+ this.#proc.kill();
979
+ this.#proc = null;
980
+ }
981
+ this.#initialized = false;
982
+ }
983
+ #request(method, params, timeout_override) {
984
+ return new Promise((resolve, reject) => {
985
+ const id = this.#next_id++;
986
+ const timeout_ms = timeout_override ?? this.#options.request_timeout_ms ?? 3e4;
987
+ const timer = setTimeout(() => {
988
+ if (this.#pending.has(id)) {
989
+ this.#pending.delete(id);
990
+ reject(/* @__PURE__ */ new Error(`LSP request ${method} timed out`));
991
+ }
992
+ }, timeout_ms);
993
+ this.#pending.set(id, {
994
+ resolve,
995
+ reject,
996
+ timer
997
+ });
998
+ try {
999
+ this.#send({
1000
+ jsonrpc: "2.0",
1001
+ id,
1002
+ method,
1003
+ params
1004
+ });
1005
+ } catch (error) {
1006
+ clearTimeout(timer);
1007
+ this.#pending.delete(id);
1008
+ reject(error);
856
1009
  }
857
1010
  });
1011
+ }
1012
+ #notify(method, params) {
1013
+ this.#send({
1014
+ jsonrpc: "2.0",
1015
+ method,
1016
+ params
1017
+ });
1018
+ }
1019
+ #send(message) {
1020
+ if (!this.#proc?.stdin?.writable) throw new Error("LSP server not connected");
1021
+ const body = Buffer.from(JSON.stringify(message), "utf8");
1022
+ const header = Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii");
1023
+ this.#proc.stdin.write(Buffer.concat([header, body]));
1024
+ }
1025
+ #drain_buffer() {
1026
+ while (true) {
1027
+ const header_end = this.#buffer.indexOf("\r\n\r\n");
1028
+ if (header_end === -1) return;
1029
+ const match = this.#buffer.subarray(0, header_end).toString("ascii").match(/Content-Length:\s*(\d+)/i);
1030
+ if (!match) {
1031
+ this.#buffer = this.#buffer.subarray(header_end + 4);
1032
+ continue;
1033
+ }
1034
+ const length = Number(match[1]);
1035
+ const body_start = header_end + 4;
1036
+ if (this.#buffer.length < body_start + length) return;
1037
+ const body = this.#buffer.subarray(body_start, body_start + length);
1038
+ this.#buffer = this.#buffer.subarray(body_start + length);
1039
+ try {
1040
+ this.#handle_message(JSON.parse(body.toString("utf8")));
1041
+ } catch (error) {
1042
+ this.emit("error", error);
1043
+ }
1044
+ }
1045
+ }
1046
+ #handle_message(message) {
1047
+ if (message.id != null && this.#pending.has(Number(message.id))) {
1048
+ const pending = this.#pending.get(Number(message.id));
1049
+ this.#pending.delete(Number(message.id));
1050
+ clearTimeout(pending.timer);
1051
+ if (message.error) pending.reject(/* @__PURE__ */ new Error(`LSP error ${message.error.code}: ${message.error.message}`));
1052
+ else pending.resolve(message.result);
1053
+ return;
1054
+ }
1055
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
1056
+ const params = message.params;
1057
+ this.#diagnostics_by_uri.set(params.uri, params.diagnostics);
1058
+ this.emit("diagnostics", params.uri);
1059
+ return;
1060
+ }
1061
+ if (message.method && message.id != null) this.#send({
1062
+ jsonrpc: "2.0",
1063
+ id: message.id,
1064
+ result: null
1065
+ });
1066
+ }
1067
+ };
1068
+ function normalize_location_result(result) {
1069
+ if (!result) return [];
1070
+ return (Array.isArray(result) ? result : [result]).map((entry) => {
1071
+ if ("uri" in entry) return entry;
1072
+ return {
1073
+ uri: entry.targetUri,
1074
+ range: entry.targetSelectionRange ?? entry.targetRange
1075
+ };
1076
+ });
1077
+ }
1078
+ function normalize_document_symbol_result(result) {
1079
+ if (!result) return [];
1080
+ if (result.length === 0 || "range" in result[0] && "selectionRange" in result[0]) return result;
1081
+ return result.map((entry) => ({
1082
+ name: entry.name,
1083
+ kind: entry.kind,
1084
+ range: entry.location.range,
1085
+ selectionRange: entry.location.range,
1086
+ containerName: entry.containerName,
1087
+ uri: entry.location.uri
1088
+ }));
1089
+ }
1090
+ function file_path_to_uri(file_path) {
1091
+ return pathToFileURL(file_path).href;
1092
+ }
1093
+ //#endregion
1094
+ //#region src/lsp/servers.ts
1095
+ const EXTENSION_LANGUAGES = {
1096
+ ".ts": "typescript",
1097
+ ".tsx": "typescript",
1098
+ ".mts": "typescript",
1099
+ ".cts": "typescript",
1100
+ ".js": "typescript",
1101
+ ".jsx": "typescript",
1102
+ ".mjs": "typescript",
1103
+ ".cjs": "typescript",
1104
+ ".py": "python",
1105
+ ".rs": "rust",
1106
+ ".go": "go",
1107
+ ".rb": "ruby",
1108
+ ".java": "java",
1109
+ ".lua": "lua",
1110
+ ".svelte": "svelte"
1111
+ };
1112
+ const LANGUAGE_SERVERS = {
1113
+ typescript: {
1114
+ language: "typescript",
1115
+ command: "typescript-language-server",
1116
+ args: ["--stdio"]
1117
+ },
1118
+ python: {
1119
+ language: "python",
1120
+ command: "pylsp",
1121
+ args: []
1122
+ },
1123
+ rust: {
1124
+ language: "rust",
1125
+ command: "rust-analyzer",
1126
+ args: []
1127
+ },
1128
+ go: {
1129
+ language: "go",
1130
+ command: "gopls",
1131
+ args: ["serve"]
1132
+ },
1133
+ ruby: {
1134
+ language: "ruby",
1135
+ command: "solargraph",
1136
+ args: ["stdio"]
1137
+ },
1138
+ java: {
1139
+ language: "java",
1140
+ command: "jdtls",
1141
+ args: []
1142
+ },
1143
+ lua: {
1144
+ language: "lua",
1145
+ command: "lua-language-server",
1146
+ args: []
1147
+ },
1148
+ svelte: {
1149
+ language: "svelte",
1150
+ command: "svelteserver",
1151
+ args: ["--stdio"]
1152
+ }
1153
+ };
1154
+ function detect_language(file_path) {
1155
+ return EXTENSION_LANGUAGES[extname(file_path).toLowerCase()];
1156
+ }
1157
+ function list_supported_languages() {
1158
+ return Object.keys(LANGUAGE_SERVERS).sort();
1159
+ }
1160
+ function resolve_server_command(command, cwd = process.cwd()) {
1161
+ if (!command) return command;
1162
+ if (isAbsolute(command) || command.includes("/") || command.includes("\\")) return command;
1163
+ for (const dir of ancestor_directories(cwd)) {
1164
+ const local_bin = resolve_local_binary(dir, command);
1165
+ if (local_bin) return local_bin;
1166
+ }
1167
+ return command;
1168
+ }
1169
+ function get_server_config(language, cwd = process.cwd()) {
1170
+ const base = LANGUAGE_SERVERS[language];
1171
+ if (!base) return void 0;
1172
+ return {
1173
+ ...base,
1174
+ command: resolve_server_command(base.command, cwd)
1175
+ };
1176
+ }
1177
+ function language_id_for_file(file_path) {
1178
+ return detect_language(file_path);
1179
+ }
1180
+ function ancestor_directories(start) {
1181
+ const dirs = [];
1182
+ let current = resolve(start);
1183
+ while (true) {
1184
+ dirs.push(current);
1185
+ const parent = dirname(current);
1186
+ if (parent === current) break;
1187
+ current = parent;
1188
+ }
1189
+ return dirs;
1190
+ }
1191
+ function resolve_local_binary(directory, command) {
1192
+ return [join(directory, "node_modules", ".bin", command), join(directory, "node_modules", ".bin", `${command}.cmd`)].find((candidate) => existsSync(candidate));
1193
+ }
1194
+ //#endregion
1195
+ //#region src/extensions/lsp.ts
1196
+ function create_lsp_extension(options = {}) {
1197
+ const create_client = options.create_client ?? ((client_options) => new LspClient(client_options));
1198
+ const read_file = options.read_file ?? ((path) => readFile(path, "utf-8"));
1199
+ return async function lsp(pi) {
1200
+ const cwd = options.cwd?.() ?? process.cwd();
1201
+ const root_uri = file_path_to_uri(cwd);
1202
+ const clients_by_language = /* @__PURE__ */ new Map();
1203
+ const failed_languages = /* @__PURE__ */ new Map();
1204
+ const resolve_abs = (file) => isAbsolute(file) ? file : resolve(cwd, file);
1205
+ const clear_language_state = async (language) => {
1206
+ if (language) {
1207
+ const state = clients_by_language.get(language);
1208
+ if (state) {
1209
+ await state.client.stop();
1210
+ clients_by_language.delete(language);
1211
+ }
1212
+ failed_languages.delete(language);
1213
+ return;
1214
+ }
1215
+ await Promise.allSettled(Array.from(clients_by_language.values()).map((state) => state.client.stop()));
1216
+ clients_by_language.clear();
1217
+ failed_languages.clear();
1218
+ };
1219
+ const get_or_start_client = async (file_path) => {
1220
+ const language = detect_language(file_path);
1221
+ if (!language) return void 0;
1222
+ const existing = clients_by_language.get(language);
1223
+ if (existing) return existing;
1224
+ if (failed_languages.has(language)) throw new Error(failed_languages.get(language));
1225
+ const server_config = get_server_config(language, cwd);
1226
+ if (!server_config) return void 0;
1227
+ const client = create_client({
1228
+ command: server_config.command,
1229
+ args: server_config.args,
1230
+ root_uri,
1231
+ language_id_for_uri: (uri) => language_id_for_file(uri)
1232
+ });
1233
+ try {
1234
+ await client.start();
1235
+ } catch (error) {
1236
+ const message = error instanceof Error ? error.message : String(error);
1237
+ const failure = `Failed to start ${server_config.command} for ${language}: ${message}. Install the language server and ensure it is available.`;
1238
+ failed_languages.set(language, failure);
1239
+ throw new Error(failure);
1240
+ }
1241
+ const state = {
1242
+ client,
1243
+ language,
1244
+ root_uri,
1245
+ command: server_config.command
1246
+ };
1247
+ clients_by_language.set(language, state);
1248
+ failed_languages.delete(language);
1249
+ return state;
1250
+ };
1251
+ const open_file = async (state, abs_path) => {
1252
+ const text = await read_file(abs_path);
1253
+ const uri = file_path_to_uri(abs_path);
1254
+ await state.client.ensure_document_open(uri, text);
1255
+ return uri;
1256
+ };
1257
+ const get_file_state = async (file) => {
1258
+ const abs = resolve_abs(file);
1259
+ const state = await get_or_start_client(abs);
1260
+ if (!state) return void 0;
1261
+ return {
1262
+ abs,
1263
+ uri: await open_file(state, abs),
1264
+ state
1265
+ };
1266
+ };
1267
+ pi.registerTool(defineTool({
1268
+ name: "lsp_diagnostics",
1269
+ label: "LSP: diagnostics",
1270
+ description: "Get language server diagnostics (errors, warnings, hints) for a file. Uses the project language server and returns empty output if the file is clean.",
1271
+ parameters: Type.Object({
1272
+ file: Type.String({ description: "Path to the file to check (relative to cwd or absolute)." }),
1273
+ wait_ms: Type.Optional(Type.Number({ description: "Max ms to wait for diagnostics after opening the file. Default 1500." }))
1274
+ }),
1275
+ execute: async (_id, params) => {
1276
+ const result = await get_file_state(params.file);
1277
+ if (!result) return {
1278
+ content: [{
1279
+ type: "text",
1280
+ text: `No language server configured for ${resolve_abs(params.file)}`
1281
+ }],
1282
+ details: {}
1283
+ };
1284
+ const diagnostics = await result.state.client.wait_for_diagnostics(result.uri, params.wait_ms ?? 1500);
1285
+ return {
1286
+ content: [{
1287
+ type: "text",
1288
+ text: format_diagnostics(result.abs, diagnostics)
1289
+ }],
1290
+ details: {}
1291
+ };
1292
+ }
1293
+ }));
1294
+ pi.registerTool(defineTool({
1295
+ name: "lsp_hover",
1296
+ label: "LSP: hover",
1297
+ description: "Get hover info (types, docs) at a position in a file. Positions are zero-based.",
1298
+ parameters: Type.Object({
1299
+ file: Type.String(),
1300
+ line: Type.Number({ description: "Zero-based line number." }),
1301
+ character: Type.Number({ description: "Zero-based character offset." })
1302
+ }),
1303
+ execute: async (_id, params) => {
1304
+ const result = await get_file_state(params.file);
1305
+ if (!result) return {
1306
+ content: [{
1307
+ type: "text",
1308
+ text: `No language server configured for ${resolve_abs(params.file)}`
1309
+ }],
1310
+ details: {}
1311
+ };
1312
+ return {
1313
+ content: [{
1314
+ type: "text",
1315
+ text: format_hover(await result.state.client.hover(result.uri, {
1316
+ line: params.line,
1317
+ character: params.character
1318
+ }))
1319
+ }],
1320
+ details: {}
1321
+ };
1322
+ }
1323
+ }));
1324
+ pi.registerTool(defineTool({
1325
+ name: "lsp_definition",
1326
+ label: "LSP: go to definition",
1327
+ description: "Find definition locations for the symbol at a position. Positions are zero-based.",
1328
+ parameters: Type.Object({
1329
+ file: Type.String(),
1330
+ line: Type.Number(),
1331
+ character: Type.Number()
1332
+ }),
1333
+ execute: async (_id, params) => {
1334
+ const result = await get_file_state(params.file);
1335
+ if (!result) return {
1336
+ content: [{
1337
+ type: "text",
1338
+ text: `No language server configured for ${resolve_abs(params.file)}`
1339
+ }],
1340
+ details: {}
1341
+ };
1342
+ return {
1343
+ content: [{
1344
+ type: "text",
1345
+ text: format_locations(await result.state.client.definition(result.uri, {
1346
+ line: params.line,
1347
+ character: params.character
1348
+ }), "No definition found.")
1349
+ }],
1350
+ details: {}
1351
+ };
1352
+ }
1353
+ }));
1354
+ pi.registerTool(defineTool({
1355
+ name: "lsp_references",
1356
+ label: "LSP: find references",
1357
+ description: "Find references to the symbol at a position. Positions are zero-based.",
1358
+ parameters: Type.Object({
1359
+ file: Type.String(),
1360
+ line: Type.Number(),
1361
+ character: Type.Number(),
1362
+ include_declaration: Type.Optional(Type.Boolean())
1363
+ }),
1364
+ execute: async (_id, params) => {
1365
+ const result = await get_file_state(params.file);
1366
+ if (!result) return {
1367
+ content: [{
1368
+ type: "text",
1369
+ text: `No language server configured for ${resolve_abs(params.file)}`
1370
+ }],
1371
+ details: {}
1372
+ };
1373
+ return {
1374
+ content: [{
1375
+ type: "text",
1376
+ text: format_locations(await result.state.client.references(result.uri, {
1377
+ line: params.line,
1378
+ character: params.character
1379
+ }, params.include_declaration ?? true), "No references found.")
1380
+ }],
1381
+ details: {}
1382
+ };
1383
+ }
1384
+ }));
1385
+ pi.registerTool(defineTool({
1386
+ name: "lsp_document_symbols",
1387
+ label: "LSP: document symbols",
1388
+ description: "List symbols in a file (functions, classes, variables) using the language server.",
1389
+ parameters: Type.Object({ file: Type.String() }),
1390
+ execute: async (_id, params) => {
1391
+ const result = await get_file_state(params.file);
1392
+ if (!result) return {
1393
+ content: [{
1394
+ type: "text",
1395
+ text: `No language server configured for ${resolve_abs(params.file)}`
1396
+ }],
1397
+ details: {}
1398
+ };
1399
+ const symbols = await result.state.client.document_symbols(result.uri);
1400
+ return {
1401
+ content: [{
1402
+ type: "text",
1403
+ text: format_document_symbols(result.abs, symbols)
1404
+ }],
1405
+ details: {}
1406
+ };
1407
+ }
1408
+ }));
1409
+ pi.registerCommand("lsp", {
1410
+ description: "Show or manage language server state",
1411
+ getArgumentCompletions: (prefix) => {
1412
+ const parts = prefix.trim().split(/\s+/);
1413
+ const subcommands = [
1414
+ "status",
1415
+ "list",
1416
+ "restart"
1417
+ ];
1418
+ if (!prefix.trim()) return subcommands.map((value) => ({
1419
+ value,
1420
+ label: value
1421
+ }));
1422
+ if (parts.length <= 1) return subcommands.filter((value) => value.startsWith(parts[0])).map((value) => ({
1423
+ value,
1424
+ label: value
1425
+ }));
1426
+ if (parts[0] === "restart") {
1427
+ const candidate = parts[1] ?? "";
1428
+ return ["all", ...list_supported_languages()].filter((value) => value.startsWith(candidate)).map((value) => ({
1429
+ value: `restart ${value}`,
1430
+ label: value
1431
+ }));
1432
+ }
1433
+ return null;
1434
+ },
1435
+ handler: async (args, ctx) => {
1436
+ await handle_lsp_command(args, ctx, cwd, clients_by_language, failed_languages, clear_language_state);
1437
+ }
1438
+ });
1439
+ pi.on("session_shutdown", async () => {
1440
+ await clear_language_state();
1441
+ });
1442
+ };
1443
+ }
1444
+ var lsp_default = create_lsp_extension();
1445
+ async function handle_lsp_command(args, ctx, cwd, clients_by_language, failed_languages, clear_language_state) {
1446
+ const [subcommand = "status", target] = args.trim() ? args.trim().split(/\s+/, 2) : [];
1447
+ switch (subcommand) {
1448
+ case "status":
1449
+ case "list":
1450
+ ctx.ui.notify(format_status_lines(cwd, clients_by_language, failed_languages).join("\n"));
1451
+ return;
1452
+ case "restart":
1453
+ if (!target || target === "all") {
1454
+ await clear_language_state();
1455
+ ctx.ui.notify("Restarted all language server state.");
1456
+ return;
1457
+ }
1458
+ if (!list_supported_languages().includes(target)) {
1459
+ ctx.ui.notify(`Unknown language: ${target}. Use one of: ${list_supported_languages().join(", ")}`, "warning");
1460
+ return;
1461
+ }
1462
+ await clear_language_state(target);
1463
+ ctx.ui.notify(`Restarted ${target} language server state.`);
1464
+ return;
1465
+ default: ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: status, list, restart`, "warning");
1466
+ }
1467
+ }
1468
+ function format_status_lines(cwd, clients_by_language, failed_languages) {
1469
+ const lines = [];
1470
+ for (const language of list_supported_languages()) {
1471
+ const running = clients_by_language.get(language);
1472
+ if (running) {
1473
+ lines.push(`${language}: running (ready=${running.client.is_ready()}) — ${running.command}`);
1474
+ continue;
1475
+ }
1476
+ const failed = failed_languages.get(language);
1477
+ if (failed) {
1478
+ lines.push(`${language}: failed — ${failed}`);
1479
+ continue;
1480
+ }
1481
+ const config = get_server_config(language, cwd);
1482
+ if (config) lines.push(`${language}: idle — ${config.command}`);
1483
+ }
1484
+ return lines.length > 0 ? lines : ["No language servers configured for this project."];
1485
+ }
1486
+ function severity_label(severity) {
1487
+ switch (severity) {
1488
+ case 1: return "error";
1489
+ case 2: return "warning";
1490
+ case 3: return "info";
1491
+ case 4: return "hint";
1492
+ default: return "info";
1493
+ }
1494
+ }
1495
+ function format_diagnostics(file, diagnostics) {
1496
+ if (diagnostics.length === 0) return `${file}: no diagnostics`;
1497
+ const lines = [`${file}: ${diagnostics.length} diagnostic(s)`];
1498
+ for (const d of diagnostics) {
1499
+ const position = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
1500
+ const source = d.source ? ` [${d.source}]` : "";
1501
+ const code = d.code != null ? ` (${d.code})` : "";
1502
+ lines.push(` ${position} ${severity_label(d.severity)}${source}${code}: ${d.message}`);
1503
+ }
1504
+ return lines.join("\n");
1505
+ }
1506
+ function format_hover(hover) {
1507
+ if (!hover) return "No hover info.";
1508
+ const contents = hover.contents;
1509
+ const extract = (item) => typeof item === "string" ? item : item.value ?? "";
1510
+ if (Array.isArray(contents)) return contents.map(extract).join("\n\n").trim() || "No hover info.";
1511
+ return extract(contents).trim() || "No hover info.";
1512
+ }
1513
+ function format_locations(locations, empty_message) {
1514
+ if (locations.length === 0) return empty_message;
1515
+ return locations.map((loc) => {
1516
+ return `${file_url_to_path_or_value(loc.uri)}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
1517
+ }).join("\n");
1518
+ }
1519
+ function format_document_symbols(file, symbols) {
1520
+ if (symbols.length === 0) return `${file}: no symbols`;
1521
+ const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
1522
+ append_symbol_lines(lines, symbols, 1);
1523
+ return lines.join("\n");
1524
+ }
1525
+ function append_symbol_lines(lines, symbols, depth) {
1526
+ for (const symbol of symbols) {
1527
+ const indent = " ".repeat(depth);
1528
+ const detail = symbol.detail ? ` — ${symbol.detail}` : "";
1529
+ const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
1530
+ lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
1531
+ if (symbol.children?.length) append_symbol_lines(lines, symbol.children, depth + 1);
1532
+ }
1533
+ }
1534
+ function symbol_kind_label(kind) {
1535
+ return {
1536
+ 2: "module",
1537
+ 3: "namespace",
1538
+ 5: "class",
1539
+ 6: "method",
1540
+ 7: "property",
1541
+ 8: "field",
1542
+ 9: "constructor",
1543
+ 11: "interface",
1544
+ 12: "function",
1545
+ 13: "variable",
1546
+ 14: "constant",
1547
+ 23: "struct",
1548
+ 24: "event"
1549
+ }[kind] ?? "symbol";
1550
+ }
1551
+ function file_url_to_path_or_value(uri) {
1552
+ try {
1553
+ return uri.startsWith("file:") ? fileURLToPath(uri) : uri;
1554
+ } catch {
1555
+ return uri;
1556
+ }
1557
+ }
1558
+ //#endregion
1559
+ //#region src/mcp/client.ts
1560
+ var McpClient = class {
1561
+ #proc = null;
1562
+ #config;
1563
+ #nextId = 1;
1564
+ #pending = /* @__PURE__ */ new Map();
1565
+ #buffer = "";
1566
+ #sessionId;
1567
+ constructor(config) {
1568
+ this.#config = config;
1569
+ }
1570
+ async connect() {
1571
+ if (this.#config.transport === "stdio") await this.#connect_stdio();
858
1572
  await this.#request("initialize", {
859
1573
  protocolVersion: "2024-11-05",
860
1574
  capabilities: {},
@@ -863,7 +1577,7 @@ var McpClient = class {
863
1577
  version: "0.0.1"
864
1578
  }
865
1579
  });
866
- this.#send({
1580
+ await this.#send({
867
1581
  jsonrpc: "2.0",
868
1582
  method: "notifications/initialized"
869
1583
  });
@@ -878,12 +1592,39 @@ var McpClient = class {
878
1592
  });
879
1593
  }
880
1594
  async disconnect() {
1595
+ if (this.#config.transport === "http") await this.#disconnect_http();
881
1596
  if (this.#proc) {
882
1597
  this.#proc.kill();
883
1598
  this.#proc = null;
884
1599
  }
885
1600
  this.#pending.clear();
886
1601
  }
1602
+ async #connect_stdio() {
1603
+ const { command, args = [], env } = this.#config;
1604
+ this.#proc = spawn(command, args, {
1605
+ stdio: [
1606
+ "pipe",
1607
+ "pipe",
1608
+ "pipe"
1609
+ ],
1610
+ env: {
1611
+ ...process.env,
1612
+ ...env
1613
+ }
1614
+ });
1615
+ this.#proc.stdout.setEncoding("utf8");
1616
+ this.#proc.stdout.on("data", (chunk) => {
1617
+ this.#buffer += chunk;
1618
+ const lines = this.#buffer.split("\n");
1619
+ this.#buffer = lines.pop() || "";
1620
+ for (const line of lines) {
1621
+ if (!line.trim()) continue;
1622
+ try {
1623
+ this.#handle_message(JSON.parse(line));
1624
+ } catch {}
1625
+ }
1626
+ });
1627
+ }
887
1628
  #request(method, params) {
888
1629
  return new Promise((resolve, reject) => {
889
1630
  const id = this.#nextId++;
@@ -896,6 +1637,11 @@ var McpClient = class {
896
1637
  id,
897
1638
  method,
898
1639
  params
1640
+ }).catch((error) => {
1641
+ if (this.#pending.has(id)) {
1642
+ this.#pending.delete(id);
1643
+ reject(error);
1644
+ }
899
1645
  });
900
1646
  setTimeout(() => {
901
1647
  if (this.#pending.has(id)) {
@@ -905,13 +1651,156 @@ var McpClient = class {
905
1651
  }, 3e4);
906
1652
  });
907
1653
  }
908
- #send(msg) {
1654
+ async #send(msg) {
1655
+ if (this.#config.transport === "http") {
1656
+ await this.#send_http(msg);
1657
+ return;
1658
+ }
909
1659
  if (!this.#proc?.stdin?.writable) throw new Error("MCP server not connected");
910
1660
  this.#proc.stdin.write(JSON.stringify(msg) + "\n");
911
1661
  }
1662
+ async #send_http(msg) {
1663
+ const config = this.#config;
1664
+ const headers = new Headers(config.headers ?? {});
1665
+ headers.set("content-type", "application/json");
1666
+ headers.set("accept", "application/json, text/event-stream");
1667
+ if (this.#sessionId) headers.set("mcp-session-id", this.#sessionId);
1668
+ const response = await fetch(config.url, {
1669
+ method: "POST",
1670
+ headers,
1671
+ body: JSON.stringify(msg)
1672
+ });
1673
+ const sessionId = response.headers.get("mcp-session-id");
1674
+ if (sessionId) this.#sessionId = sessionId;
1675
+ if (!response.ok) {
1676
+ const body = await response.text().catch(() => "");
1677
+ throw new Error(`MCP HTTP ${response.status}${body ? `: ${body}` : ""}`);
1678
+ }
1679
+ if (response.status === 204) return;
1680
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
1681
+ await this.#consume_sse_response(response, config.name);
1682
+ return;
1683
+ }
1684
+ const body = await response.text();
1685
+ if (!body.trim()) return;
1686
+ let parsed;
1687
+ try {
1688
+ parsed = JSON.parse(body);
1689
+ } catch {
1690
+ throw new Error(`Invalid MCP HTTP response from ${config.name}: ${body.slice(0, 200)}`);
1691
+ }
1692
+ this.#dispatch_message(parsed);
1693
+ }
1694
+ async #disconnect_http() {
1695
+ const config = this.#config;
1696
+ if (!this.#sessionId) return;
1697
+ const headers = new Headers(config.headers ?? {});
1698
+ headers.set("mcp-session-id", this.#sessionId);
1699
+ const response = await fetch(config.url, {
1700
+ method: "DELETE",
1701
+ headers
1702
+ });
1703
+ if (response.status !== 405 && !response.ok) {
1704
+ const body = await response.text().catch(() => "");
1705
+ throw new Error(`MCP HTTP disconnect ${response.status}${body ? `: ${body}` : ""}`);
1706
+ }
1707
+ this.#sessionId = void 0;
1708
+ }
1709
+ async #consume_sse_response(response, server_name) {
1710
+ if (!response.body) return;
1711
+ const reader = response.body.getReader();
1712
+ const decoder = new TextDecoder();
1713
+ let buffer = "";
1714
+ let event_lines = [];
1715
+ const flush_event = () => {
1716
+ if (event_lines.length === 0) return;
1717
+ const data_lines = event_lines.filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
1718
+ event_lines = [];
1719
+ if (data_lines.length === 0) return;
1720
+ const payload = data_lines.join("\n").trim();
1721
+ if (!payload) return;
1722
+ try {
1723
+ this.#dispatch_message(JSON.parse(payload));
1724
+ } catch {
1725
+ throw new Error(`Invalid MCP SSE payload from ${server_name}: ${payload.slice(0, 200)}`);
1726
+ }
1727
+ };
1728
+ while (true) {
1729
+ const { done, value } = await reader.read();
1730
+ buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
1731
+ const lines = buffer.replace(/\r\n/g, "\n").split("\n");
1732
+ buffer = lines.pop() ?? "";
1733
+ for (const line of lines) {
1734
+ if (line === "") {
1735
+ flush_event();
1736
+ continue;
1737
+ }
1738
+ if (line.startsWith(":")) continue;
1739
+ event_lines.push(line);
1740
+ }
1741
+ if (done) break;
1742
+ }
1743
+ if (buffer.trim()) event_lines.push(buffer.trim());
1744
+ flush_event();
1745
+ }
1746
+ #dispatch_message(message) {
1747
+ if (Array.isArray(message)) {
1748
+ for (const item of message) this.#dispatch_message(item);
1749
+ return;
1750
+ }
1751
+ if (!message || typeof message !== "object") return;
1752
+ this.#handle_message(message);
1753
+ }
1754
+ #handle_message(msg) {
1755
+ if (msg.id == null || !this.#pending.has(msg.id)) return;
1756
+ const pending = this.#pending.get(msg.id);
1757
+ this.#pending.delete(msg.id);
1758
+ if (msg.error) {
1759
+ pending.reject(/* @__PURE__ */ new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
1760
+ return;
1761
+ }
1762
+ pending.resolve(msg.result);
1763
+ }
912
1764
  };
913
1765
  //#endregion
914
1766
  //#region src/mcp/config.ts
1767
+ function is_string_record(value, label, name) {
1768
+ if (value === void 0) return true;
1769
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid MCP server "${name}": ${label} must be an object of string values`);
1770
+ 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`);
1771
+ return true;
1772
+ }
1773
+ function parse_server(name, entry) {
1774
+ const type = typeof entry.type === "string" ? entry.type.trim().toLowerCase() : "";
1775
+ if (type && ![
1776
+ "stdio",
1777
+ "http",
1778
+ "streamable-http"
1779
+ ].includes(type)) throw new Error(`Invalid MCP server "${name}": unsupported transport type "${type}"`);
1780
+ if (type === "http" || type === "streamable-http" || entry.url !== void 0) {
1781
+ if (typeof entry.url !== "string" || !entry.url.trim()) throw new Error(`Invalid MCP server "${name}": http transport requires a url`);
1782
+ is_string_record(entry.headers, "headers", name);
1783
+ const headers = entry.headers;
1784
+ return {
1785
+ name,
1786
+ transport: "http",
1787
+ url: entry.url.trim(),
1788
+ ...headers ? { headers } : {}
1789
+ };
1790
+ }
1791
+ if (typeof entry.command !== "string" || !entry.command.trim()) throw new Error(`Invalid MCP server "${name}": stdio transport requires a command`);
1792
+ 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`);
1793
+ is_string_record(entry.env, "env", name);
1794
+ const args = entry.args;
1795
+ const env = entry.env;
1796
+ return {
1797
+ name,
1798
+ transport: "stdio",
1799
+ command: entry.command.trim(),
1800
+ ...args ? { args } : {},
1801
+ ...env ? { env } : {}
1802
+ };
1803
+ }
915
1804
  function read_config(path) {
916
1805
  if (!existsSync(path)) return {};
917
1806
  const raw = readFileSync(path, "utf-8");
@@ -924,12 +1813,7 @@ function load_mcp_config(cwd) {
924
1813
  ...global_servers,
925
1814
  ...project_servers
926
1815
  };
927
- return Object.entries(merged).map(([name, server]) => ({
928
- name,
929
- command: server.command,
930
- args: server.args,
931
- env: server.env
932
- }));
1816
+ return Object.entries(merged).map(([name, server]) => parse_server(name, server));
933
1817
  }
934
1818
  //#endregion
935
1819
  //#region src/extensions/mcp.ts
@@ -1144,6 +2028,9 @@ function get_global_presets_path() {
1144
2028
  function get_project_presets_path(cwd) {
1145
2029
  return join(cwd, ".pi", "presets.json");
1146
2030
  }
2031
+ function get_persisted_prompt_state_path() {
2032
+ return join(getAgentDir(), "prompt-preset-state.json");
2033
+ }
1147
2034
  function read_prompt_presets_file(path) {
1148
2035
  if (!existsSync(path)) return {};
1149
2036
  try {
@@ -1195,6 +2082,61 @@ function remove_project_prompt_preset(cwd, name) {
1195
2082
  remaining
1196
2083
  };
1197
2084
  }
2085
+ function normalize_prompt_preset_state(input) {
2086
+ if (!input || typeof input !== "object") return void 0;
2087
+ const candidate = input;
2088
+ return {
2089
+ base_name: typeof candidate.base_name === "string" && candidate.base_name.trim() ? candidate.base_name.trim() : null,
2090
+ 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() : []
2091
+ };
2092
+ }
2093
+ function read_persisted_prompt_states(path = get_persisted_prompt_state_path()) {
2094
+ if (!existsSync(path)) return {
2095
+ version: 1,
2096
+ projects: {}
2097
+ };
2098
+ try {
2099
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
2100
+ const raw_projects = parsed.projects && typeof parsed.projects === "object" ? parsed.projects : {};
2101
+ const projects = {};
2102
+ for (const [cwd, value] of Object.entries(raw_projects)) {
2103
+ const normalized = normalize_prompt_preset_state(value);
2104
+ if (!normalized) continue;
2105
+ projects[cwd] = normalized;
2106
+ }
2107
+ return {
2108
+ version: typeof parsed.version === "number" ? parsed.version : 1,
2109
+ projects
2110
+ };
2111
+ } catch {
2112
+ return {
2113
+ version: 1,
2114
+ projects: {}
2115
+ };
2116
+ }
2117
+ }
2118
+ function load_persisted_prompt_state(cwd, path = get_persisted_prompt_state_path()) {
2119
+ return read_persisted_prompt_states(path).projects[cwd];
2120
+ }
2121
+ function save_persisted_prompt_state(cwd, state, path = get_persisted_prompt_state_path()) {
2122
+ const persisted = read_persisted_prompt_states(path);
2123
+ persisted.projects[cwd] = normalize_prompt_preset_state(state) ?? {
2124
+ base_name: null,
2125
+ layer_names: []
2126
+ };
2127
+ const dir = dirname(path);
2128
+ if (!existsSync(dir)) mkdirSync(dir, {
2129
+ recursive: true,
2130
+ mode: 448
2131
+ });
2132
+ const tmp = `${path}.tmp-${Date.now()}`;
2133
+ writeFileSync(tmp, JSON.stringify({
2134
+ version: 1,
2135
+ projects: Object.fromEntries(Object.entries(persisted.projects).sort(([a], [b]) => a.localeCompare(b)))
2136
+ }, null, " ") + "\n", { mode: 384 });
2137
+ renameSync(tmp, path);
2138
+ return path;
2139
+ }
1198
2140
  function get_last_preset_state(ctx) {
1199
2141
  const entries = ctx.sessionManager.getEntries();
1200
2142
  for (let i = entries.length - 1; i >= 0; i--) {
@@ -1258,16 +2200,129 @@ function format_active_details(active_base_name, active_layers, presets) {
1258
2200
  }
1259
2201
  return parts.join("\n") || "No preset or layers active";
1260
2202
  }
2203
+ function get_footer_prompt_status(active_base_name, active_layers) {
2204
+ if (!active_base_name && active_layers.size === 0) return;
2205
+ return `prompt:${active_base_name ?? "none"}${active_layers.size > 0 ? ` +${active_layers.size}` : ""}`;
2206
+ }
2207
+ function sanitize_status_text(text) {
2208
+ return text.replace(/[\r\n\t]/g, " ").replace(/ +/g, " ").trim();
2209
+ }
2210
+ function format_token_count(count) {
2211
+ if (count < 1e3) return count.toString();
2212
+ if (count < 1e4) return `${(count / 1e3).toFixed(1)}k`;
2213
+ if (count < 1e6) return `${Math.round(count / 1e3)}k`;
2214
+ if (count < 1e7) return `${(count / 1e6).toFixed(1)}M`;
2215
+ return `${Math.round(count / 1e6)}M`;
2216
+ }
2217
+ function get_current_thinking_level(ctx) {
2218
+ const entries = ctx.sessionManager.getEntries();
2219
+ for (let i = entries.length - 1; i >= 0; i--) {
2220
+ const entry = entries[i];
2221
+ if (entry.type === "thinking_level_change" && typeof entry.thinkingLevel === "string") return entry.thinkingLevel;
2222
+ }
2223
+ return ctx.model?.reasoning ? "high" : "off";
2224
+ }
2225
+ function render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers) {
2226
+ let total_input = 0;
2227
+ let total_output = 0;
2228
+ let total_cache_read = 0;
2229
+ let total_cache_write = 0;
2230
+ let total_cost = 0;
2231
+ for (const entry of ctx.sessionManager.getEntries()) if (entry.type === "message" && entry.message.role === "assistant") {
2232
+ total_input += entry.message.usage.input;
2233
+ total_output += entry.message.usage.output;
2234
+ total_cache_read += entry.message.usage.cacheRead;
2235
+ total_cache_write += entry.message.usage.cacheWrite;
2236
+ total_cost += entry.message.usage.cost.total;
2237
+ }
2238
+ const context_usage = ctx.getContextUsage();
2239
+ const context_window = context_usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
2240
+ const context_percent_value = context_usage?.percent ?? 0;
2241
+ const context_percent = context_usage?.percent !== null ? context_percent_value.toFixed(1) : "?";
2242
+ let pwd = ctx.cwd;
2243
+ const home = process.env.HOME || process.env.USERPROFILE;
2244
+ if (home && pwd.startsWith(home)) pwd = `~${pwd.slice(home.length)}`;
2245
+ const branch = footer_data.getGitBranch();
2246
+ if (branch) pwd = `${pwd} (${branch})`;
2247
+ const session_name = ctx.sessionManager.getSessionName();
2248
+ if (session_name) pwd = `${pwd} • ${session_name}`;
2249
+ const stats_parts = [];
2250
+ if (total_input) stats_parts.push(`↑${format_token_count(total_input)}`);
2251
+ if (total_output) stats_parts.push(`↓${format_token_count(total_output)}`);
2252
+ if (total_cache_read) stats_parts.push(`R${format_token_count(total_cache_read)}`);
2253
+ if (total_cache_write) stats_parts.push(`W${format_token_count(total_cache_write)}`);
2254
+ const using_subscription = ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
2255
+ if (total_cost || using_subscription) stats_parts.push(`$${total_cost.toFixed(3)}${using_subscription ? " (sub)" : ""}`);
2256
+ const context_percent_display = context_percent === "?" ? `?/${format_token_count(context_window)}` : `${context_percent}%/${format_token_count(context_window)}`;
2257
+ let context_percent_str = context_percent_display;
2258
+ if (context_percent_value > 90) context_percent_str = theme.fg("error", context_percent_display);
2259
+ else if (context_percent_value > 70) context_percent_str = theme.fg("warning", context_percent_display);
2260
+ stats_parts.push(context_percent_str);
2261
+ let stats_left = stats_parts.join(" ");
2262
+ let stats_left_width = visibleWidth(stats_left);
2263
+ if (stats_left_width > width) {
2264
+ stats_left = truncateToWidth(stats_left, width, "...");
2265
+ stats_left_width = visibleWidth(stats_left);
2266
+ }
2267
+ const model_name = ctx.model?.id || "no-model";
2268
+ const thinking_level = get_current_thinking_level(ctx);
2269
+ let right_side_without_provider = model_name;
2270
+ if (ctx.model?.reasoning) right_side_without_provider = thinking_level === "off" ? `${model_name} • thinking off` : `${model_name} • ${thinking_level}`;
2271
+ let right_side = right_side_without_provider;
2272
+ if (footer_data.getAvailableProviderCount() > 1 && ctx.model) {
2273
+ right_side = `(${ctx.model.provider}) ${right_side_without_provider}`;
2274
+ if (stats_left_width + 2 + visibleWidth(right_side) > width) right_side = right_side_without_provider;
2275
+ }
2276
+ const right_side_width = visibleWidth(right_side);
2277
+ const total_needed = stats_left_width + 2 + right_side_width;
2278
+ let stats_line;
2279
+ if (total_needed <= width) {
2280
+ const padding = " ".repeat(width - stats_left_width - right_side_width);
2281
+ stats_line = stats_left + padding + right_side;
2282
+ } else {
2283
+ const available_for_right = width - stats_left_width - 2;
2284
+ if (available_for_right > 0) {
2285
+ const truncated_right = truncateToWidth(right_side, available_for_right, "");
2286
+ const truncated_right_width = visibleWidth(truncated_right);
2287
+ const padding = " ".repeat(Math.max(0, width - stats_left_width - truncated_right_width));
2288
+ stats_line = stats_left + padding + truncated_right;
2289
+ } else stats_line = stats_left;
2290
+ }
2291
+ const dim_stats_left = theme.fg("dim", stats_left);
2292
+ const remainder = stats_line.slice(stats_left.length);
2293
+ const dim_remainder = theme.fg("dim", remainder);
2294
+ const lines = [truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "...")), dim_stats_left + dim_remainder];
2295
+ const prompt_status = get_footer_prompt_status(active_base_name, active_layers);
2296
+ if (prompt_status) {
2297
+ const themed_status = theme.fg("dim", prompt_status);
2298
+ const status_width = visibleWidth(themed_status);
2299
+ const aligned_status = status_width >= width ? truncateToWidth(themed_status, width, theme.fg("dim", "...")) : `${" ".repeat(width - status_width)}${themed_status}`;
2300
+ lines.push(aligned_status);
2301
+ }
2302
+ 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));
2303
+ if (other_statuses.length > 0) lines.push(truncateToWidth(other_statuses.join(" "), width, theme.fg("dim", "...")));
2304
+ return lines;
2305
+ }
1261
2306
  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}`);
2307
+ ctx.ui.setStatus("preset", void 0);
2308
+ if (!ctx.hasUI) return;
2309
+ ctx.ui.setFooter((tui, theme, footer_data) => {
2310
+ return {
2311
+ dispose: footer_data.onBranchChange(() => tui.requestRender()),
2312
+ invalidate() {},
2313
+ render(width) {
2314
+ return render_footer_lines(ctx, theme, footer_data, width, active_base_name, active_layers);
2315
+ }
2316
+ };
2317
+ });
1265
2318
  }
1266
- function persist_state(pi, active_base_name, active_layers) {
1267
- pi.appendEntry(PRESET_STATE_TYPE, {
2319
+ function persist_state(pi, ctx, active_base_name, active_layers) {
2320
+ const state = {
1268
2321
  base_name: active_base_name ?? null,
1269
2322
  layer_names: [...active_layers].sort()
1270
- });
2323
+ };
2324
+ pi.appendEntry(PRESET_STATE_TYPE, state);
2325
+ save_persisted_prompt_state(ctx.cwd, state);
1271
2326
  }
1272
2327
  function normalize_active_state(presets, active_base_name, active_layers) {
1273
2328
  return {
@@ -1308,7 +2363,7 @@ async function prompt_presets(pi) {
1308
2363
  active_base_name = next_base_name;
1309
2364
  active_layers = new Set(next_layers);
1310
2365
  set_status(ctx, active_base_name, active_layers);
1311
- if (options?.persist !== false) persist_state(pi, active_base_name, active_layers);
2366
+ if (options?.persist !== false) persist_state(pi, ctx, active_base_name, active_layers);
1312
2367
  if (options?.notify) ctx.ui.notify(options.notify, "info");
1313
2368
  }
1314
2369
  function activate_base(name, ctx, options) {
@@ -1384,7 +2439,7 @@ async function prompt_presets(pi) {
1384
2439
  active_base_name = normalized.active_base_name;
1385
2440
  active_layers = normalized.active_layers;
1386
2441
  set_status(ctx, active_base_name, active_layers);
1387
- persist_state(pi, active_base_name, active_layers);
2442
+ persist_state(pi, ctx, active_base_name, active_layers);
1388
2443
  const fallback = presets[name];
1389
2444
  if (mode === "reset" && fallback) {
1390
2445
  ctx.ui.notify(`Reset "${name}" to ${get_prompt_source_label(fallback.source)} preset`, "info");
@@ -1681,7 +2736,7 @@ async function prompt_presets(pi) {
1681
2736
  set_status(ctx, active_base_name, active_layers);
1682
2737
  return;
1683
2738
  }
1684
- const restored = get_last_preset_state(ctx);
2739
+ const restored = get_last_preset_state(ctx) ?? load_persisted_prompt_state(ctx.cwd);
1685
2740
  if (restored) {
1686
2741
  active_base_name = restored.base_name ?? void 0;
1687
2742
  active_layers = new Set(restored.layer_names ?? []);
@@ -1702,6 +2757,7 @@ async function prompt_presets(pi) {
1702
2757
  });
1703
2758
  pi.on("session_shutdown", async (_event, ctx) => {
1704
2759
  ctx.ui.setStatus("preset", void 0);
2760
+ ctx.ui.setFooter(void 0);
1705
2761
  });
1706
2762
  }
1707
2763
  //#endregion
@@ -2500,7 +3556,8 @@ const BUILTIN_EXTENSION_FACTORIES = {
2500
3556
  "filter-output": filter_output,
2501
3557
  handoff,
2502
3558
  recall,
2503
- "prompt-presets": prompt_presets
3559
+ "prompt-presets": prompt_presets,
3560
+ lsp: lsp_default
2504
3561
  };
2505
3562
  const PACKAGE_THEME_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "themes");
2506
3563
  function get_force_disabled_builtins(options) {
@@ -2512,6 +3569,7 @@ function get_force_disabled_builtins(options) {
2512
3569
  if (!options.handoff) force_disabled.add("handoff");
2513
3570
  if (!options.recall) force_disabled.add("recall");
2514
3571
  if (!options.prompt_presets) force_disabled.add("prompt-presets");
3572
+ if (!options.lsp) force_disabled.add("lsp");
2515
3573
  return force_disabled;
2516
3574
  }
2517
3575
  function create_builtin_extension_factory(key, extension, force_disabled) {
@@ -2533,7 +3591,7 @@ function create_extensions_override(managed_inline_paths) {
2533
3591
  };
2534
3592
  }
2535
3593
  async function create_my_pi(options = {}) {
2536
- const { cwd = process.cwd(), extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, model, system_prompt, append_system_prompt } = options;
3594
+ const { cwd = process.cwd(), extensions = [], extensionFactories: user_factories = [], mcp = true, skills = true, chain = true, filter_output = true, handoff = true, recall = true, prompt_presets = true, lsp = true, model, system_prompt, append_system_prompt } = options;
2537
3595
  const resolved_extensions = extensions.map((p) => resolve(cwd, p));
2538
3596
  const force_disabled = get_force_disabled_builtins({
2539
3597
  mcp,
@@ -2542,7 +3600,8 @@ async function create_my_pi(options = {}) {
2542
3600
  filter_output,
2543
3601
  handoff,
2544
3602
  recall,
2545
- prompt_presets
3603
+ prompt_presets,
3604
+ lsp
2546
3605
  });
2547
3606
  const managed_extension_factories = [create_extensions_extension({ force_disabled }), ...BUILTIN_EXTENSIONS.map((extension) => create_builtin_extension_factory(extension.key, BUILTIN_EXTENSION_FACTORIES[extension.key], force_disabled))];
2548
3607
  const managed_inline_paths = managed_extension_factories.map((_, index) => `<inline:${index + 1}>`);
@@ -2591,4 +3650,4 @@ async function create_my_pi(options = {}) {
2591
3650
  //#endregion
2592
3651
  export { create_my_pi as n, runPrintMode$1 as r, InteractiveMode$1 as t };
2593
3652
 
2594
- //# sourceMappingURL=api-DtthDVIa.js.map
3653
+ //# sourceMappingURL=api-Dcq2JalY.js.map