lsd-pi 1.3.6 → 1.3.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.
Files changed (126) hide show
  1. package/README.md +82 -0
  2. package/dist/cli.js +2 -1
  3. package/dist/lsd-settings-manager.d.ts +2 -0
  4. package/dist/lsd-settings-manager.js +5 -0
  5. package/dist/resource-loader.js +33 -3
  6. package/dist/resources/extensions/cache-timer/index.js +3 -2
  7. package/dist/resources/extensions/mcp-client/index.js +72 -4
  8. package/dist/resources/extensions/slash-commands/plan.js +5 -5
  9. package/dist/resources/extensions/usage/index.js +34 -2
  10. package/dist/resources/extensions/voice/index.js +1 -0
  11. package/dist/resources/extensions/voice/push-to-talk.js +2 -0
  12. package/dist/welcome-screen.js +2 -2
  13. package/package.json +1 -1
  14. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts +2 -0
  15. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts.map +1 -0
  16. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js +72 -0
  17. package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js.map +1 -0
  18. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +4 -0
  19. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  20. package/packages/pi-coding-agent/dist/core/agent-session.js +29 -2
  21. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  22. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  23. package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  24. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  25. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
  26. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  27. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  28. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
  29. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
  30. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
  31. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
  32. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  33. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
  35. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/tool-priority.js +1 -1
  37. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
  39. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
  41. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
  43. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/main.js +1 -0
  46. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +12 -4
  48. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
  50. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  51. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
  52. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  53. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +2 -0
  54. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +16 -10
  56. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  57. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  59. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +26 -4
  60. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +16 -1
  62. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +128 -13
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +1 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +48 -4
  68. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +137 -6
  70. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +64 -15
  73. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  74. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +2 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
  78. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +73 -27
  83. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
  85. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -0
  88. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  89. package/packages/pi-coding-agent/package.json +1 -1
  90. package/packages/pi-coding-agent/src/core/agent-session.context-usage.test.ts +87 -0
  91. package/packages/pi-coding-agent/src/core/agent-session.ts +40 -2
  92. package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
  93. package/packages/pi-coding-agent/src/core/extensions/types.ts +3 -0
  94. package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
  95. package/packages/pi-coding-agent/src/core/settings-manager.ts +18 -0
  96. package/packages/pi-coding-agent/src/core/tool-priority.ts +1 -1
  97. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
  98. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
  99. package/packages/pi-coding-agent/src/main.ts +1 -0
  100. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +14 -4
  101. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
  102. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +13 -6
  103. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -4
  104. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +137 -14
  105. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +60 -4
  106. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +174 -6
  107. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +73 -15
  108. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +2 -1
  109. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  110. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +76 -27
  111. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
  112. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
  113. package/packages/pi-tui/dist/components/editor.js +3 -3
  114. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  115. package/packages/pi-tui/src/components/editor.ts +3 -3
  116. package/pkg/dist/modes/interactive/theme/themes.js +4 -4
  117. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  118. package/pkg/package.json +1 -1
  119. package/src/resources/extensions/cache-timer/index.ts +3 -2
  120. package/src/resources/extensions/mcp-client/index.ts +83 -4
  121. package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +16 -0
  122. package/src/resources/extensions/slash-commands/plan.ts +6 -6
  123. package/src/resources/extensions/usage/index.ts +40 -2
  124. package/src/resources/extensions/voice/index.ts +1 -0
  125. package/src/resources/extensions/voice/push-to-talk.ts +3 -0
  126. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +6 -0
package/README.md CHANGED
@@ -365,6 +365,88 @@ LSD discovers and connects to MCP servers configured in:
365
365
 
366
366
  Use `/configs` inside a session to scan for MCP servers from other AI tools (Claude Code, Cursor, Copilot, etc.) and import them.
367
367
 
368
+ ### Adding MCP servers to LSD config
369
+
370
+ LSD supports two transport types: **stdio** (launch a local process) and **HTTP** (connect to a running server).
371
+
372
+ #### Stdio server (local process)
373
+
374
+ Add to `.mcp.json` or `.lsd/mcp.json`:
375
+
376
+ ```json
377
+ {
378
+ "mcpServers": {
379
+ "my-server": {
380
+ "type": "stdio",
381
+ "command": "/absolute/path/to/executable",
382
+ "args": ["arg1", "arg2"],
383
+ "env": {
384
+ "API_KEY": "your-key",
385
+ "DEBUG": "true"
386
+ }
387
+ }
388
+ }
389
+ }
390
+ ```
391
+
392
+ If the server is installed as an npm package:
393
+
394
+ ```json
395
+ {
396
+ "mcpServers": {
397
+ "my-server": {
398
+ "type": "stdio",
399
+ "command": "npx",
400
+ "args": ["@my-org/mcp-server"],
401
+ "env": {
402
+ "API_KEY": "sk-..."
403
+ }
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ #### HTTP server (remote connection)
410
+
411
+ For MCP servers already running on a network endpoint:
412
+
413
+ ```json
414
+ {
415
+ "mcpServers": {
416
+ "remote-server": {
417
+ "type": "http",
418
+ "url": "http://localhost:8080/mcp",
419
+ "headers": {
420
+ "Authorization": "Bearer ${MCP_TOKEN}"
421
+ }
422
+ }
423
+ }
424
+ }
425
+ ```
426
+
427
+ Environment variables in `headers` and `env` are resolved at startup (use `${VAR_NAME}` syntax).
428
+
429
+ #### File placement
430
+
431
+ - **`.mcp.json`** — repo-shared configuration (commit to git)
432
+ - **`.lsd/mcp.json`** — local-only configuration (git-ignored, not shared)
433
+
434
+ If both files exist, server names are merged and the first definition found wins.
435
+
436
+ #### Managing MCP servers
437
+
438
+ Use the `/mcp` slash command inside a session:
439
+
440
+ | Command | Description |
441
+ |---------|-------------|
442
+ | `/mcp list` | List all configured servers and their status |
443
+ | `/mcp inspect <server>` | Connect and show available tools for a server |
444
+ | `/mcp enable <server>` | Enable a server |
445
+ | `/mcp disable <server>` | Disable a server |
446
+ | `/mcp reload` | Reload config and reconnect enabled servers |
447
+
448
+ MCP servers connect lazily — `/mcp inspect` or the first tool call triggers the connection.
449
+
368
450
  ---
369
451
 
370
452
  ## Sessions
package/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ import { getProjectSessionsDir } from './project-sessions.js';
17
17
  import { markStartup, printStartupTimings } from './startup-timings.js';
18
18
  import { bootstrapRtk, GSD_RTK_DISABLED_ENV } from './rtk.js';
19
19
  import { loadEffectivePreferences } from './shared-preferences.js';
20
+ import { createLsdSettingsManager } from './lsd-settings-manager.js';
20
21
  // ---------------------------------------------------------------------------
21
22
  // V8 compile cache — Node 22+ can cache compiled bytecode across runs,
22
23
  // eliminating repeated parse/compile overhead for unchanged modules.
@@ -270,7 +271,7 @@ const { resolveModelsJsonPath } = await import('./models-resolver.js');
270
271
  const modelsJsonPath = resolveModelsJsonPath();
271
272
  const modelRegistry = new ModelRegistry(authStorage, modelsJsonPath);
272
273
  markStartup('ModelRegistry');
273
- const settingsManager = SettingsManager.create(agentDir);
274
+ const settingsManager = createLsdSettingsManager(process.cwd(), agentDir);
274
275
  markStartup('SettingsManager.create');
275
276
  if (cliFlags.noSandbox) {
276
277
  process.env.PI_NO_SANDBOX = '1';
@@ -0,0 +1,2 @@
1
+ import { SettingsManager } from '@gsd/pi-coding-agent';
2
+ export declare function createLsdSettingsManager(cwd?: string, agentDir?: string): SettingsManager;
@@ -0,0 +1,5 @@
1
+ import { SettingsManager } from '@gsd/pi-coding-agent';
2
+ import { agentDir as defaultAgentDir } from './app-paths.js';
3
+ export function createLsdSettingsManager(cwd = process.cwd(), agentDir = defaultAgentDir) {
4
+ return SettingsManager.create(cwd, agentDir);
5
+ }
@@ -127,6 +127,25 @@ function collectFileEntries(dir, root, out) {
127
127
  }
128
128
  }
129
129
  }
130
+ function hasMissingResourceEntries(srcDir, destDir) {
131
+ if (!existsSync(srcDir))
132
+ return false;
133
+ if (!existsSync(destDir))
134
+ return true;
135
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
136
+ const srcPath = join(srcDir, entry.name);
137
+ const destPath = join(destDir, entry.name);
138
+ if (!existsSync(destPath))
139
+ return true;
140
+ if (entry.isDirectory() && hasMissingResourceEntries(srcPath, destPath))
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+ function hasMissingManagedResources(agentDir) {
146
+ return hasMissingResourceEntries(bundledExtensionsDir, join(agentDir, 'extensions'))
147
+ || hasMissingResourceEntries(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
148
+ }
130
149
  export function getNewerManagedResourceVersion(agentDir, currentVersion) {
131
150
  const managedVersion = readManagedResourceVersion(agentDir);
132
151
  if (!managedVersion) {
@@ -192,8 +211,18 @@ function syncResourceDir(srcDir, destDir) {
192
211
  for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
193
212
  if (entry.isDirectory()) {
194
213
  const target = join(destDir, entry.name);
195
- if (existsSync(target))
196
- rmSync(target, { recursive: true, force: true });
214
+ if (existsSync(target)) {
215
+ try {
216
+ rmSync(target, { recursive: true, force: true });
217
+ }
218
+ catch {
219
+ // Retry once — macOS can create .DS_Store mid-deletion causing ENOTEMPTY
220
+ try {
221
+ rmSync(target, { recursive: true, force: true });
222
+ }
223
+ catch { /* non-fatal */ }
224
+ }
225
+ }
197
226
  }
198
227
  }
199
228
  try {
@@ -386,7 +415,8 @@ export function initResources(agentDir) {
386
415
  // Version matches — check content fingerprint for same-version staleness.
387
416
  const currentHash = computeResourceFingerprint();
388
417
  const hasStaleExtensionFiles = hasStaleCompiledExtensionSiblings(join(agentDir, 'extensions'));
389
- if (manifest.contentHash && manifest.contentHash === currentHash && !hasStaleExtensionFiles) {
418
+ const hasMissingResources = hasMissingManagedResources(agentDir);
419
+ if (manifest.contentHash && manifest.contentHash === currentHash && !hasStaleExtensionFiles && !hasMissingResources) {
390
420
  return;
391
421
  }
392
422
  }
@@ -18,6 +18,7 @@ const IS_MEMORY_MAINTENANCE_WORKER = process.env.LSD_MEMORY_EXTRACT === "1" || p
18
18
  const IS_CACHE_TIMER_FORCED_OFF = process.env.LSD_DISABLE_CACHE_TIMER === "1" || process.env.GSD_DISABLE_CACHE_TIMER === "1";
19
19
  // ANSI color codes for timer display
20
20
  const ANSI_RESET = "\x1b[0m";
21
+ const ANSI_GREEN = "\x1b[32m";
21
22
  const ANSI_YELLOW = "\x1b[33m";
22
23
  const ANSI_RED = "\x1b[31m";
23
24
  function getSettingsPath() {
@@ -63,8 +64,8 @@ function formatElapsed(ms) {
63
64
  // 5–10 minutes: yellow
64
65
  return `${ANSI_YELLOW}⏱ ${time}${ANSI_RESET}`;
65
66
  }
66
- // Under 5 minutes: plain (inherits footer dim styling)
67
- return `⏱ ${time}`;
67
+ // Under 5 minutes: green
68
+ return `${ANSI_GREEN}⏱ ${time}${ANSI_RESET}`;
68
69
  }
69
70
  export default function cacheTimerExtension(pi) {
70
71
  if (IS_MEMORY_MAINTENANCE_WORKER || IS_CACHE_TIMER_FORCED_OFF) {
@@ -23,6 +23,7 @@ import { basename, dirname, join } from "node:path";
23
23
  const connections = new Map();
24
24
  let configCache = null;
25
25
  const toolCache = new Map();
26
+ let warmupPromise = null;
26
27
  const MCP_STATE_PATH = join(process.cwd(), ".lsd", "mcp-state.json");
27
28
  function normalizeServerName(name) {
28
29
  return name.trim().toLowerCase();
@@ -205,6 +206,46 @@ async function getOrConnect(name, signal) {
205
206
  connections.set(config.name, { client, transport });
206
207
  return client;
207
208
  }
209
+ async function warmupEnabledServers() {
210
+ if (warmupPromise)
211
+ return warmupPromise;
212
+ warmupPromise = (async () => {
213
+ const enabledServers = readConfigs().filter((server) => server.enabled);
214
+ if (enabledServers.length === 0)
215
+ return [];
216
+ const results = await Promise.allSettled(enabledServers.map(async (server) => {
217
+ const client = await getOrConnect(server.name);
218
+ const result = await client.listTools(undefined, { timeout: 30000 });
219
+ const tools = (result.tools ?? []).map((tool) => ({
220
+ name: tool.name,
221
+ description: tool.description ?? "",
222
+ inputSchema: tool.inputSchema,
223
+ }));
224
+ toolCache.set(server.name, tools);
225
+ return {
226
+ name: server.name,
227
+ status: "connected",
228
+ toolCount: tools.length,
229
+ };
230
+ }));
231
+ return results.map((result, index) => {
232
+ if (result.status === "fulfilled") {
233
+ return result.value;
234
+ }
235
+ return {
236
+ name: enabledServers[index]?.name ?? `server-${index + 1}`,
237
+ status: "error",
238
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
239
+ };
240
+ });
241
+ })();
242
+ try {
243
+ return await warmupPromise;
244
+ }
245
+ finally {
246
+ warmupPromise = null;
247
+ }
248
+ }
208
249
  async function closeAll() {
209
250
  const closing = Array.from(connections.entries()).map(async ([name, conn]) => {
210
251
  try {
@@ -219,6 +260,7 @@ async function closeAll() {
219
260
  toolCache.clear();
220
261
  }
221
262
  async function reloadMcpState() {
263
+ warmupPromise = null;
222
264
  await closeAll();
223
265
  configCache = null;
224
266
  }
@@ -331,6 +373,13 @@ async function handleMcpCommand(args, ctx) {
331
373
  const action = enabled ? "enabled" : "disabled";
332
374
  const changeText = result.changed ? action : `already ${action}`;
333
375
  ctx.ui.notify(`MCP server ${result.canonicalName} ${changeText}.`, "info");
376
+ if (enabled) {
377
+ const warmupResults = await warmupEnabledServers();
378
+ const warmupResult = warmupResults.find((entry) => entry.name === result.canonicalName);
379
+ if (warmupResult?.status === "error") {
380
+ ctx.ui.notify(`Failed to connect ${result.canonicalName}: ${warmupResult.error}`, "error");
381
+ }
382
+ }
334
383
  }
335
384
  catch (error) {
336
385
  const message = error instanceof Error ? error.message : String(error);
@@ -341,7 +390,12 @@ async function handleMcpCommand(args, ctx) {
341
390
  if (subcommand === "reload") {
342
391
  await reloadMcpState();
343
392
  const servers = readConfigs();
344
- ctx.ui.notify(`Reloaded MCP config ${servers.length} server(s) available.`, "info");
393
+ const warmupResults = await warmupEnabledServers();
394
+ const failed = warmupResults.filter((entry) => entry.status === "error");
395
+ const summary = failed.length > 0
396
+ ? `Reloaded MCP config — ${servers.length} server(s) available, ${failed.length} failed to connect.`
397
+ : `Reloaded MCP config — ${servers.length} server(s) available.`;
398
+ ctx.ui.notify(summary, failed.length > 0 ? "warning" : "info");
345
399
  return;
346
400
  }
347
401
  if (subcommand === "help") {
@@ -598,15 +652,29 @@ export default function (pi) {
598
652
  // ── Lifecycle ─────────────────────────────────────────────────────────────
599
653
  pi.on("session_start", async (_event, ctx) => {
600
654
  const servers = readConfigs();
655
+ const enabledServers = servers.filter((server) => server.enabled);
601
656
  if (servers.length > 0) {
602
- ctx.ui.notify(`MCP client ready — ${servers.filter((server) => server.enabled).length}/${servers.length} server(s) enabled`, "info");
657
+ ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled`, "info");
603
658
  }
659
+ if (enabledServers.length === 0)
660
+ return;
661
+ void warmupEnabledServers().then((results) => {
662
+ const failed = results.filter((entry) => entry.status === "error");
663
+ if (failed.length > 0) {
664
+ const failureSummary = failed.map((entry) => `${entry.name}: ${entry.error}`).join("; ");
665
+ ctx.ui.notify(`MCP autoconnect partial failure — ${failureSummary}`, "warning");
666
+ }
667
+ }).catch((error) => {
668
+ const message = error instanceof Error ? error.message : String(error);
669
+ ctx.ui.notify(`MCP autoconnect failed: ${message}`, "warning");
670
+ });
604
671
  });
605
672
  pi.on("session_shutdown", async () => {
673
+ warmupPromise = null;
606
674
  await closeAll();
607
675
  });
608
676
  pi.on("session_switch", async () => {
609
- await closeAll();
610
- configCache = null;
677
+ await reloadMcpState();
678
+ void warmupEnabledServers();
611
679
  });
612
680
  }
@@ -155,7 +155,7 @@ function readAutoSwitchPlanModelSetting() {
155
155
  return false;
156
156
  const raw = readFileSync(settingsPath, "utf-8");
157
157
  const parsed = JSON.parse(raw);
158
- return parsed.autoSwitchPlanModel === true;
158
+ return parsed.planModeAutoSwitchModel === true;
159
159
  }
160
160
  catch {
161
161
  return false;
@@ -332,10 +332,10 @@ function scheduleNewSession(pi, ctx) {
332
332
  pi.executeSlashCommand("/plan-execute-new-session");
333
333
  }
334
334
  async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false) {
335
- const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
336
- if (reasoningModel) {
337
- await setModelIfNeeded(pi, ctx, reasoningModel);
338
- }
335
+ // Do NOT switch to reasoning model during execution.
336
+ // The reasoning model is only for plan-mode investigation, not execution.
337
+ // If a coding model is configured and we're using a subagent, the explicit
338
+ // model="<planModeCodingModel>" in the kickoff message will handle it.
339
339
  state = {
340
340
  ...state,
341
341
  targetPermissionMode: permissionMode,
@@ -231,6 +231,7 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
231
231
  let projectLabel = basename(file);
232
232
  let headerResolved = false;
233
233
  let currentModel = "";
234
+ let lastUserTimestamp = 0;
234
235
  const raw = readFileSync(file, "utf-8");
235
236
  for (const line of raw.split("\n")) {
236
237
  if (!line.trim())
@@ -282,6 +283,8 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
282
283
  cacheWrite: 0,
283
284
  total: 0,
284
285
  cost: 0,
286
+ totalDurationMs: 0,
287
+ totalOutputForSpeed: 0,
285
288
  };
286
289
  existing.messages += 1;
287
290
  existing.input += input;
@@ -290,11 +293,25 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
290
293
  existing.cacheWrite += cacheWrite;
291
294
  existing.total += total;
292
295
  existing.cost += cost;
296
+ // Track tok/sec: use preceding user message timestamp
297
+ if (output > 0 && lastUserTimestamp > 0) {
298
+ const durationMs = timestamp - lastUserTimestamp;
299
+ if (durationMs > 0) {
300
+ existing.totalDurationMs += durationMs;
301
+ existing.totalOutputForSpeed += output;
302
+ }
303
+ }
293
304
  rows.set(key, existing);
294
305
  }
295
306
  else if (message.role === "user") {
296
307
  const timestamp = Number(message.timestamp ?? 0);
297
- if (!timestamp || timestamp < startMs || timestamp >= endMs)
308
+ if (!timestamp)
309
+ continue;
310
+ // Always track last user timestamp for tok/sec calculation,
311
+ // even if this user message is outside the time range
312
+ // (the assistant response may still be within range).
313
+ lastUserTimestamp = timestamp;
314
+ if (timestamp < startMs || timestamp >= endMs)
298
315
  continue;
299
316
  matchedUserPrompts++;
300
317
  const model = currentModel;
@@ -311,6 +328,8 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
311
328
  cacheWrite: 0,
312
329
  total: 0,
313
330
  cost: 0,
331
+ totalDurationMs: 0,
332
+ totalOutputForSpeed: 0,
314
333
  };
315
334
  existing.userPrompts += 1;
316
335
  userPromptRows.set(key, existing);
@@ -342,8 +361,10 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
342
361
  acc.cacheWrite += row.cacheWrite;
343
362
  acc.total += row.total;
344
363
  acc.cost += row.cost;
364
+ acc.totalDurationMs += row.totalDurationMs;
365
+ acc.totalOutputForSpeed += row.totalOutputForSpeed;
345
366
  return acc;
346
- }, { messages: 0, userPrompts: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 });
367
+ }, { messages: 0, userPrompts: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0, totalDurationMs: 0, totalOutputForSpeed: 0 });
347
368
  return {
348
369
  label: "",
349
370
  scope,
@@ -355,6 +376,11 @@ function collectUsage(sessionFiles, startMs, endMs, scope, groupBy) {
355
376
  totals,
356
377
  };
357
378
  }
379
+ function formatSpeed(totalDurationMs, totalOutputForSpeed) {
380
+ if (totalDurationMs <= 0)
381
+ return "—";
382
+ return Math.round(totalOutputForSpeed / (totalDurationMs / 1000)).toLocaleString();
383
+ }
358
384
  function renderTable(report) {
359
385
  const firstColumnHeader = report.groupBy === "project"
360
386
  ? "project"
@@ -371,7 +397,9 @@ function renderTable(report) {
371
397
  write: formatInt(row.cacheWrite),
372
398
  total: formatInt(row.total),
373
399
  cost: formatCost(row.cost),
400
+ speed: formatSpeed(row.totalDurationMs, row.totalOutputForSpeed),
374
401
  }));
402
+ const totalsSpeed = formatSpeed(report.totals.totalDurationMs, report.totals.totalOutputForSpeed);
375
403
  const widths = {
376
404
  label: Math.max(firstColumnHeader.length, ...displayRows.map((row) => row.label.length), 5),
377
405
  userPrompts: Math.max(11, ...displayRows.map((row) => row.userPrompts.length), String(report.totals.userPrompts).length),
@@ -382,6 +410,7 @@ function renderTable(report) {
382
410
  write: Math.max(5, ...displayRows.map((row) => row.write.length), formatInt(report.totals.cacheWrite).length),
383
411
  total: Math.max(5, ...displayRows.map((row) => row.total.length), formatInt(report.totals.total).length),
384
412
  cost: Math.max(7, ...displayRows.map((row) => row.cost.length), formatCost(report.totals.cost).length),
413
+ speed: Math.max(5, ...displayRows.map((row) => row.speed.length), totalsSpeed.length),
385
414
  };
386
415
  const header = [
387
416
  firstColumnHeader.padEnd(widths.label),
@@ -393,6 +422,7 @@ function renderTable(report) {
393
422
  "write".padStart(widths.write),
394
423
  "total".padStart(widths.total),
395
424
  "cost".padStart(widths.cost),
425
+ "tok/s".padStart(widths.speed),
396
426
  ].join(" ");
397
427
  const divider = "-".repeat(header.length);
398
428
  const body = displayRows.map((row) => [
@@ -405,6 +435,7 @@ function renderTable(report) {
405
435
  row.write.padStart(widths.write),
406
436
  row.total.padStart(widths.total),
407
437
  row.cost.padStart(widths.cost),
438
+ row.speed.padStart(widths.speed),
408
439
  ].join(" "));
409
440
  const totalsLine = [
410
441
  "TOTAL".padEnd(widths.label),
@@ -416,6 +447,7 @@ function renderTable(report) {
416
447
  formatInt(report.totals.cacheWrite).padStart(widths.write),
417
448
  formatInt(report.totals.total).padStart(widths.total),
418
449
  formatCost(report.totals.cost).padStart(widths.cost),
450
+ totalsSpeed.padStart(widths.speed),
419
451
  ].join(" ");
420
452
  return [header, divider, ...body, divider, totalsLine].join("\n");
421
453
  }
@@ -275,6 +275,7 @@ export default function (pi) {
275
275
  activationMode,
276
276
  editorText: ctx.ui.getEditorText(),
277
277
  holdToTalkSupported: isKittyProtocolActive(),
278
+ isEditorFocused: ctx.ui.isEditorFocused(),
278
279
  onUnsupported: () => {
279
280
  if (holdToTalkUnsupportedNotified)
280
281
  return;
@@ -2,6 +2,8 @@ import { isKeyRelease, Key, matchesKey } from "@gsd/pi-tui";
2
2
  export function handlePushToTalkInput(data, state) {
3
3
  if (!matchesKey(data, Key.space))
4
4
  return undefined;
5
+ if (!state.isEditorFocused)
6
+ return undefined;
5
7
  if (isKeyRelease(data)) {
6
8
  if (state.activationMode === "push-to-talk") {
7
9
  void Promise.resolve(state.stopVoice());
@@ -59,8 +59,8 @@ export function printWelcomeScreen(opts) {
59
59
  // Keep welcome colors anchored to the active CLI theme accent so the banner
60
60
  // feels native regardless of custom themes.
61
61
  const ACCENT = accentHex();
62
- const LOGO_EDGE = chalk.white;
63
- const LOGO_CENTER = chalk.whiteBright;
62
+ const LOGO_EDGE = chalk.hex(mixHex(ACCENT, '#000000', 0.35));
63
+ const LOGO_CENTER = chalk.hex(mixHex(ACCENT, '#ffffff', 0.2));
64
64
  const TITLE_BASE = chalk.bold;
65
65
  const TITLE_MARK = chalk.hex(mixHex(ACCENT, '#ffffff', 0.35)).bold;
66
66
  const VERSION = chalk.dim;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lsd-pi",
3
- "version": "1.3.6",
3
+ "version": "1.3.9",
4
4
  "description": "LSD — Looks Sort of Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent-session.context-usage.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session.context-usage.test.d.ts","sourceRoot":"","sources":["../../src/core/agent-session.context-usage.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,72 @@
1
+ import { describe, it, mock } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { AgentSession } from "./agent-session.js";
4
+ describe("AgentSession context usage caching", () => {
5
+ it("memoizes getContextUsage until context changes", () => {
6
+ let branchCalls = 0;
7
+ const messages = [
8
+ { role: "user", content: "hello" },
9
+ {
10
+ role: "assistant",
11
+ content: [{ type: "text", text: "hi" }],
12
+ usage: { input: 120, output: 30, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
13
+ stopReason: "end_turn",
14
+ provider: "test",
15
+ model: "fast-model",
16
+ },
17
+ ];
18
+ const fakeSession = {
19
+ model: { provider: "test", id: "fast-model", contextWindow: 1000 },
20
+ messages,
21
+ sessionManager: {
22
+ getBranch: () => {
23
+ branchCalls += 1;
24
+ return [];
25
+ },
26
+ },
27
+ _contextUsageRevision: 0,
28
+ _contextUsageCache: undefined,
29
+ };
30
+ const first = AgentSession.prototype.getContextUsage.call(fakeSession);
31
+ const second = AgentSession.prototype.getContextUsage.call(fakeSession);
32
+ assert.equal(branchCalls, 1);
33
+ assert.strictEqual(second, first);
34
+ messages.push({ role: "user", content: "follow up" });
35
+ AgentSession.prototype["_invalidateContextUsageCache"].call(fakeSession);
36
+ const third = AgentSession.prototype.getContextUsage.call(fakeSession);
37
+ assert.equal(branchCalls, 2);
38
+ assert.notStrictEqual(third, first);
39
+ assert.ok((third?.tokens ?? 0) > (first?.tokens ?? 0));
40
+ });
41
+ it("clears cached context usage before emitting session events", () => {
42
+ const listener = mock.fn(() => { });
43
+ const fakeSession = {
44
+ _contextUsageRevision: 3,
45
+ _contextUsageCache: {
46
+ revision: 3,
47
+ modelKey: "test/model:1000",
48
+ usage: { tokens: 123, contextWindow: 1000, percent: 12.3 },
49
+ },
50
+ _eventListeners: [listener],
51
+ _invalidateContextUsageCache: AgentSession.prototype["_invalidateContextUsageCache"],
52
+ };
53
+ AgentSession.prototype["_emit"].call(fakeSession, { type: "session_state_changed", reason: "set_model" });
54
+ assert.equal(fakeSession._contextUsageRevision, 4);
55
+ assert.equal(fakeSession._contextUsageCache, undefined);
56
+ assert.equal(listener.mock.callCount(), 1);
57
+ });
58
+ it("invalidates cached context usage for immediate bash history writes", () => {
59
+ const invalidate = mock.fn(() => { });
60
+ const appendMessage = mock.fn(() => { });
61
+ const fakeSession = {
62
+ isStreaming: false,
63
+ agent: { appendMessage },
64
+ sessionManager: { appendMessage },
65
+ _invalidateContextUsageCache: invalidate,
66
+ };
67
+ AgentSession.prototype.recordBashResult.call(fakeSession, "pwd", { output: "/tmp", exitCode: 0, cancelled: false, truncated: false, fullOutputPath: undefined, sandboxed: false });
68
+ assert.equal(invalidate.mock.callCount(), 1);
69
+ assert.equal(appendMessage.mock.callCount(), 2);
70
+ });
71
+ });
72
+ //# sourceMappingURL=agent-session.context-usage.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-session.context-usage.test.js","sourceRoot":"","sources":["../../src/core/agent-session.context-usage.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, mock } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { AgentSession } from \"./agent-session.js\";\n\ndescribe(\"AgentSession context usage caching\", () => {\n it(\"memoizes getContextUsage until context changes\", () => {\n let branchCalls = 0;\n const messages = [\n { role: \"user\", content: \"hello\" },\n {\n role: \"assistant\",\n content: [{ type: \"text\", text: \"hi\" }],\n usage: { input: 120, output: 30, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },\n stopReason: \"end_turn\",\n provider: \"test\",\n model: \"fast-model\",\n },\n ] as any[];\n\n const fakeSession = {\n model: { provider: \"test\", id: \"fast-model\", contextWindow: 1000 },\n messages,\n sessionManager: {\n getBranch: () => {\n branchCalls += 1;\n return [];\n },\n },\n _contextUsageRevision: 0,\n _contextUsageCache: undefined,\n } as any;\n\n const first = AgentSession.prototype.getContextUsage.call(fakeSession);\n const second = AgentSession.prototype.getContextUsage.call(fakeSession);\n\n assert.equal(branchCalls, 1);\n assert.strictEqual(second, first);\n\n messages.push({ role: \"user\", content: \"follow up\" } as any);\n AgentSession.prototype[\"_invalidateContextUsageCache\"].call(fakeSession);\n const third = AgentSession.prototype.getContextUsage.call(fakeSession);\n\n assert.equal(branchCalls, 2);\n assert.notStrictEqual(third, first);\n assert.ok((third?.tokens ?? 0) > (first?.tokens ?? 0));\n });\n\n it(\"clears cached context usage before emitting session events\", () => {\n const listener = mock.fn(() => { });\n const fakeSession = {\n _contextUsageRevision: 3,\n _contextUsageCache: {\n revision: 3,\n modelKey: \"test/model:1000\",\n usage: { tokens: 123, contextWindow: 1000, percent: 12.3 },\n },\n _eventListeners: [listener],\n _invalidateContextUsageCache: AgentSession.prototype[\"_invalidateContextUsageCache\"],\n } as any;\n\n AgentSession.prototype[\"_emit\"].call(fakeSession, { type: \"session_state_changed\", reason: \"set_model\" });\n\n assert.equal(fakeSession._contextUsageRevision, 4);\n assert.equal(fakeSession._contextUsageCache, undefined);\n assert.equal(listener.mock.callCount(), 1);\n });\n\n it(\"invalidates cached context usage for immediate bash history writes\", () => {\n const invalidate = mock.fn(() => { });\n const appendMessage = mock.fn(() => { });\n const fakeSession = {\n isStreaming: false,\n agent: { appendMessage },\n sessionManager: { appendMessage },\n _invalidateContextUsageCache: invalidate,\n } as any;\n\n AgentSession.prototype.recordBashResult.call(\n fakeSession,\n \"pwd\",\n { output: \"/tmp\", exitCode: 0, cancelled: false, truncated: false, fullOutputPath: undefined, sandboxed: false },\n );\n\n assert.equal(invalidate.mock.callCount(), 1);\n assert.equal(appendMessage.mock.callCount(), 2);\n });\n});\n"]}
@@ -179,6 +179,9 @@ export declare class AgentSession {
179
179
  private _cumulativeToolCalls;
180
180
  /** Cost of the most recent assistant response (for per-prompt display). */
181
181
  private _lastTurnCost;
182
+ /** Incremented whenever context-affecting state changes. */
183
+ private _contextUsageRevision;
184
+ private _contextUsageCache;
182
185
  private _bashAbortController;
183
186
  private _pendingBashMessages;
184
187
  private _extensionRunner;
@@ -211,6 +214,7 @@ export declare class AgentSession {
211
214
  /** Fallback resolver for cross-provider fallback */
212
215
  get fallbackResolver(): FallbackResolver;
213
216
  get sandboxManager(): SandboxManager | undefined;
217
+ private _invalidateContextUsageCache;
214
218
  /** Emit an event to all listeners */
215
219
  private _emit;
216
220
  private _emitSessionStateChanged;