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.
- package/README.md +82 -0
- package/dist/cli.js +2 -1
- package/dist/lsd-settings-manager.d.ts +2 -0
- package/dist/lsd-settings-manager.js +5 -0
- package/dist/resource-loader.js +33 -3
- package/dist/resources/extensions/cache-timer/index.js +3 -2
- package/dist/resources/extensions/mcp-client/index.js +72 -4
- package/dist/resources/extensions/slash-commands/plan.js +5 -5
- package/dist/resources/extensions/usage/index.js +34 -2
- package/dist/resources/extensions/voice/index.js +1 -0
- package/dist/resources/extensions/voice/push-to-talk.js +2 -0
- package/dist/welcome-screen.js +2 -2
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js +72 -0
- package/packages/pi-coding-agent/dist/core/agent-session.context-usage.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +29 -2
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +12 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.js +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +1 -0
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +12 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
- package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +16 -10
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +26 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +16 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +128 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +48 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +137 -6
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +64 -15
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +2 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +73 -27
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +3 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.context-usage.test.ts +87 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +3 -0
- package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +18 -0
- package/packages/pi-coding-agent/src/core/tool-priority.ts +1 -1
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
- package/packages/pi-coding-agent/src/main.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +14 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +13 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -4
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +137 -14
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +60 -4
- package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +174 -6
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +73 -15
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +76 -27
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
- package/packages/pi-tui/dist/components/editor.js +3 -3
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +3 -3
- package/pkg/dist/modes/interactive/theme/themes.js +4 -4
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/cache-timer/index.ts +3 -2
- package/src/resources/extensions/mcp-client/index.ts +83 -4
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +16 -0
- package/src/resources/extensions/slash-commands/plan.ts +6 -6
- package/src/resources/extensions/usage/index.ts +40 -2
- package/src/resources/extensions/voice/index.ts +1 -0
- package/src/resources/extensions/voice/push-to-talk.ts +3 -0
- 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 =
|
|
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';
|
package/dist/resource-loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
67
|
-
return
|
|
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
|
-
|
|
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 — ${
|
|
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
|
|
610
|
-
|
|
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.
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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());
|
package/dist/welcome-screen.js
CHANGED
|
@@ -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.
|
|
63
|
-
const LOGO_CENTER = chalk.
|
|
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
|
@@ -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;
|