lsd-pi 1.3.7 → 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/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/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/tool-priority.js +1 -1
- package/packages/pi-coding-agent/dist/core/tool-priority.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/tool-execution.d.ts +2 -0
- 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 +17 -1
- 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.js +1 -1
- 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/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +37 -14
- 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 +1 -0
- 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.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.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/tool-priority.ts +1 -1
- package/packages/pi-coding-agent/src/main.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +37 -15
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +4 -0
- package/pkg/package.json +1 -1
- 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
|
@@ -56,19 +56,9 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
56
56
|
"bash", "bg_shell",
|
|
57
57
|
"web_search", "search-the-web", "google_search", "search_and_read",
|
|
58
58
|
"fetch_page", "resolve_library", "get_library_docs",
|
|
59
|
+
"subagent", "await_subagent",
|
|
59
60
|
]);
|
|
60
61
|
|
|
61
|
-
const shouldStartToolHidden = (toolName: string): boolean => {
|
|
62
|
-
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
63
|
-
if (!collapseToolCalls || host.collapsedToolCallsExpanded) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
if (ALWAYS_DIRECT_TOOLS.has(toolName)) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
return shouldCollapse(toolName, false);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
62
|
const hasVisibleRender = (child: { render?: (width: number) => string[] } | undefined): boolean => {
|
|
73
63
|
if (!child?.render) return true;
|
|
74
64
|
try {
|
|
@@ -190,7 +180,7 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
190
180
|
(text) => theme.fg("accent", text),
|
|
191
181
|
host.defaultWorkingMessage,
|
|
192
182
|
);
|
|
193
|
-
host.loadingAnimation.setCycleMessages(host.workingMessages,
|
|
183
|
+
host.loadingAnimation.setCycleMessages(host.workingMessages, 10_000);
|
|
194
184
|
host.statusContainer.addChild(host.loadingAnimation);
|
|
195
185
|
host.startLoadingTips();
|
|
196
186
|
// Show steer/queue + expand hint in editor bottom border while agent is running
|
|
@@ -253,9 +243,19 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
253
243
|
host.ui,
|
|
254
244
|
);
|
|
255
245
|
component.setExpanded(host.toolOutputExpanded);
|
|
256
|
-
|
|
246
|
+
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
247
|
+
const shouldHide = collapseToolCalls && !host.collapsedToolCallsExpanded && shouldCollapse(content.name, false) && !ALWAYS_DIRECT_TOOLS.has(content.name);
|
|
257
248
|
host.chatContainer.addChild(component);
|
|
249
|
+
if (shouldHide) {
|
|
250
|
+
// Check for adjacent summary AFTER adding to container so indexOf works
|
|
251
|
+
const adjacentSummary = findAdjacentCollapsedToolSummary(content.name, component);
|
|
252
|
+
if (adjacentSummary) {
|
|
253
|
+
component.setIndented(true);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
component.setHidden(shouldHide);
|
|
258
257
|
host.pendingTools.set(content.id, component);
|
|
258
|
+
host.updateEditorExpandHint();
|
|
259
259
|
} else {
|
|
260
260
|
host.pendingTools.get(content.id)?.updateArgs(content.arguments);
|
|
261
261
|
}
|
|
@@ -273,9 +273,19 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
273
273
|
host.ui,
|
|
274
274
|
);
|
|
275
275
|
component.setExpanded(host.toolOutputExpanded);
|
|
276
|
-
|
|
276
|
+
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
277
|
+
const shouldHide = collapseToolCalls && !host.collapsedToolCallsExpanded && shouldCollapse(content.name, false) && !ALWAYS_DIRECT_TOOLS.has(content.name);
|
|
277
278
|
host.chatContainer.addChild(component);
|
|
279
|
+
if (shouldHide) {
|
|
280
|
+
// Check for adjacent summary AFTER adding to container so indexOf works
|
|
281
|
+
const adjacentSummary = findAdjacentCollapsedToolSummary(content.name, component);
|
|
282
|
+
if (adjacentSummary) {
|
|
283
|
+
component.setIndented(true);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
component.setHidden(shouldHide);
|
|
278
287
|
host.pendingTools.set(content.id, component);
|
|
288
|
+
host.updateEditorExpandHint();
|
|
279
289
|
}
|
|
280
290
|
} else if (content.type === "webSearchResult") {
|
|
281
291
|
const component = host.pendingTools.get(content.toolUseId);
|
|
@@ -356,9 +366,19 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
356
366
|
host.ui,
|
|
357
367
|
);
|
|
358
368
|
component.setExpanded(host.toolOutputExpanded);
|
|
359
|
-
|
|
369
|
+
const collapseToolCalls = host.settingsManager.getCollapseToolCalls?.() ?? false;
|
|
370
|
+
const shouldHide = collapseToolCalls && !host.collapsedToolCallsExpanded && shouldCollapse(event.toolName, false) && !ALWAYS_DIRECT_TOOLS.has(event.toolName);
|
|
360
371
|
host.chatContainer.addChild(component);
|
|
372
|
+
if (shouldHide) {
|
|
373
|
+
// Check for adjacent summary AFTER adding to container so indexOf works
|
|
374
|
+
const adjacentSummary = findAdjacentCollapsedToolSummary(event.toolName, component);
|
|
375
|
+
if (adjacentSummary) {
|
|
376
|
+
component.setIndented(true);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
component.setHidden(shouldHide);
|
|
361
380
|
host.pendingTools.set(event.toolCallId, component);
|
|
381
|
+
host.updateEditorExpandHint();
|
|
362
382
|
host.ui.requestRender();
|
|
363
383
|
}
|
|
364
384
|
break;
|
|
@@ -410,6 +430,8 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
|
|
|
410
430
|
if (collapseToolCalls && shouldCollapse(event.toolName, event.isError) && !ALWAYS_DIRECT_TOOLS.has(event.toolName)) {
|
|
411
431
|
appendCollapsedToolSummary(event.toolName, component.getElapsed(), component);
|
|
412
432
|
component.setHidden(!host.collapsedToolCallsExpanded);
|
|
433
|
+
component.setIndented(false);
|
|
434
|
+
host.updateEditorExpandHint();
|
|
413
435
|
} else {
|
|
414
436
|
component.setHidden(false);
|
|
415
437
|
resetCollapsedToolSummary();
|
package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function createExtensionUIContext(host: any): ExtensionUIContext {
|
|
|
55
55
|
},
|
|
56
56
|
getToolsExpanded: () => host.collapsedToolCallsExpanded || host.toolOutputExpanded,
|
|
57
57
|
setToolsExpanded: (expanded) => host.setToolsExpanded(expanded),
|
|
58
|
+
isEditorFocused: () => !!host.editor.focused,
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -1538,7 +1538,7 @@ export class InteractiveMode {
|
|
|
1538
1538
|
this.defaultEditor.onExtensionShortcut = undefined;
|
|
1539
1539
|
this.updateTerminalTitle();
|
|
1540
1540
|
if (this.loadingAnimation) {
|
|
1541
|
-
this.loadingAnimation.setCycleMessages(this.workingMessages);
|
|
1541
|
+
this.loadingAnimation.setCycleMessages(this.workingMessages, 10_000);
|
|
1542
1542
|
}
|
|
1543
1543
|
}
|
|
1544
1544
|
|
|
@@ -1881,7 +1881,7 @@ export class InteractiveMode {
|
|
|
1881
1881
|
/** Reset the loading animation back to cycling working messages. */
|
|
1882
1882
|
private resetLoadingMessage(): void {
|
|
1883
1883
|
if (this.loadingAnimation) {
|
|
1884
|
-
this.loadingAnimation.setCycleMessages(this.workingMessages);
|
|
1884
|
+
this.loadingAnimation.setCycleMessages(this.workingMessages, 10_000);
|
|
1885
1885
|
}
|
|
1886
1886
|
}
|
|
1887
1887
|
|
|
@@ -3110,6 +3110,7 @@ export class InteractiveMode {
|
|
|
3110
3110
|
}
|
|
3111
3111
|
if (child instanceof ToolExecutionComponent) {
|
|
3112
3112
|
child.setHidden(!showCollapsedToolCalls && child.shouldHideWhenCollapsed(collapseToolCalls));
|
|
3113
|
+
child.setIndented(false);
|
|
3113
3114
|
}
|
|
3114
3115
|
if (child instanceof ToolSummaryLine) {
|
|
3115
3116
|
child.setHidden(!collapseToolCalls || showCollapsedToolCalls);
|
|
@@ -422,6 +422,10 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
422
422
|
setToolsExpanded(_expanded: boolean) {
|
|
423
423
|
// Tool expansion not supported in RPC mode - no TUI
|
|
424
424
|
},
|
|
425
|
+
|
|
426
|
+
isEditorFocused() {
|
|
427
|
+
return false;
|
|
428
|
+
},
|
|
425
429
|
});
|
|
426
430
|
|
|
427
431
|
// Set up extensions with RPC-based UI context.
|
package/pkg/package.json
CHANGED
|
@@ -62,6 +62,14 @@ interface McpState {
|
|
|
62
62
|
const connections = new Map<string, ManagedConnection>();
|
|
63
63
|
let configCache: McpServerConfig[] | null = null;
|
|
64
64
|
const toolCache = new Map<string, McpToolSchema[]>();
|
|
65
|
+
let warmupPromise: Promise<McpWarmupResult[]> | null = null;
|
|
66
|
+
|
|
67
|
+
interface McpWarmupResult {
|
|
68
|
+
name: string;
|
|
69
|
+
status: "connected" | "error";
|
|
70
|
+
toolCount?: number;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
const MCP_STATE_PATH = join(process.cwd(), ".lsd", "mcp-state.json");
|
|
67
75
|
|
|
@@ -263,6 +271,49 @@ async function getOrConnect(name: string, signal?: AbortSignal): Promise<Client>
|
|
|
263
271
|
return client;
|
|
264
272
|
}
|
|
265
273
|
|
|
274
|
+
async function warmupEnabledServers(): Promise<McpWarmupResult[]> {
|
|
275
|
+
if (warmupPromise) return warmupPromise;
|
|
276
|
+
|
|
277
|
+
warmupPromise = (async () => {
|
|
278
|
+
const enabledServers = readConfigs().filter((server) => server.enabled);
|
|
279
|
+
if (enabledServers.length === 0) return [];
|
|
280
|
+
|
|
281
|
+
const results = await Promise.allSettled(enabledServers.map(async (server) => {
|
|
282
|
+
const client = await getOrConnect(server.name);
|
|
283
|
+
const result = await client.listTools(undefined, { timeout: 30000 });
|
|
284
|
+
const tools: McpToolSchema[] = (result.tools ?? []).map((tool) => ({
|
|
285
|
+
name: tool.name,
|
|
286
|
+
description: tool.description ?? "",
|
|
287
|
+
inputSchema: tool.inputSchema as Record<string, unknown> | undefined,
|
|
288
|
+
}));
|
|
289
|
+
toolCache.set(server.name, tools);
|
|
290
|
+
return {
|
|
291
|
+
name: server.name,
|
|
292
|
+
status: "connected" as const,
|
|
293
|
+
toolCount: tools.length,
|
|
294
|
+
};
|
|
295
|
+
}));
|
|
296
|
+
|
|
297
|
+
return results.map((result, index) => {
|
|
298
|
+
if (result.status === "fulfilled") {
|
|
299
|
+
return result.value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
name: enabledServers[index]?.name ?? `server-${index + 1}`,
|
|
304
|
+
status: "error" as const,
|
|
305
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
})();
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await warmupPromise;
|
|
312
|
+
} finally {
|
|
313
|
+
warmupPromise = null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
266
317
|
async function closeAll(): Promise<void> {
|
|
267
318
|
const closing = Array.from(connections.entries()).map(async ([name, conn]) => {
|
|
268
319
|
try {
|
|
@@ -277,6 +328,7 @@ async function closeAll(): Promise<void> {
|
|
|
277
328
|
}
|
|
278
329
|
|
|
279
330
|
async function reloadMcpState(): Promise<void> {
|
|
331
|
+
warmupPromise = null;
|
|
280
332
|
await closeAll();
|
|
281
333
|
configCache = null;
|
|
282
334
|
}
|
|
@@ -402,6 +454,14 @@ async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Pro
|
|
|
402
454
|
const action = enabled ? "enabled" : "disabled";
|
|
403
455
|
const changeText = result.changed ? action : `already ${action}`;
|
|
404
456
|
ctx.ui.notify(`MCP server ${result.canonicalName} ${changeText}.`, "info");
|
|
457
|
+
|
|
458
|
+
if (enabled) {
|
|
459
|
+
const warmupResults = await warmupEnabledServers();
|
|
460
|
+
const warmupResult = warmupResults.find((entry) => entry.name === result.canonicalName);
|
|
461
|
+
if (warmupResult?.status === "error") {
|
|
462
|
+
ctx.ui.notify(`Failed to connect ${result.canonicalName}: ${warmupResult.error}`, "error");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
405
465
|
} catch (error) {
|
|
406
466
|
const message = error instanceof Error ? error.message : String(error);
|
|
407
467
|
ctx.ui.notify(message, "error");
|
|
@@ -412,7 +472,12 @@ async function handleMcpCommand(args: string, ctx: ExtensionCommandContext): Pro
|
|
|
412
472
|
if (subcommand === "reload") {
|
|
413
473
|
await reloadMcpState();
|
|
414
474
|
const servers = readConfigs();
|
|
415
|
-
|
|
475
|
+
const warmupResults = await warmupEnabledServers();
|
|
476
|
+
const failed = warmupResults.filter((entry) => entry.status === "error");
|
|
477
|
+
const summary = failed.length > 0
|
|
478
|
+
? `Reloaded MCP config — ${servers.length} server(s) available, ${failed.length} failed to connect.`
|
|
479
|
+
: `Reloaded MCP config — ${servers.length} server(s) available.`;
|
|
480
|
+
ctx.ui.notify(summary, failed.length > 0 ? "warning" : "info");
|
|
416
481
|
return;
|
|
417
482
|
}
|
|
418
483
|
|
|
@@ -733,17 +798,31 @@ export default function(pi: ExtensionAPI) {
|
|
|
733
798
|
|
|
734
799
|
pi.on("session_start", async (_event, ctx) => {
|
|
735
800
|
const servers = readConfigs();
|
|
801
|
+
const enabledServers = servers.filter((server) => server.enabled);
|
|
736
802
|
if (servers.length > 0) {
|
|
737
|
-
ctx.ui.notify(`MCP client ready — ${
|
|
803
|
+
ctx.ui.notify(`MCP client ready — ${enabledServers.length}/${servers.length} server(s) enabled`, "info");
|
|
738
804
|
}
|
|
805
|
+
if (enabledServers.length === 0) return;
|
|
806
|
+
|
|
807
|
+
void warmupEnabledServers().then((results) => {
|
|
808
|
+
const failed = results.filter((entry) => entry.status === "error");
|
|
809
|
+
if (failed.length > 0) {
|
|
810
|
+
const failureSummary = failed.map((entry) => `${entry.name}: ${entry.error}`).join("; ");
|
|
811
|
+
ctx.ui.notify(`MCP autoconnect partial failure — ${failureSummary}`, "warning");
|
|
812
|
+
}
|
|
813
|
+
}).catch((error) => {
|
|
814
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
815
|
+
ctx.ui.notify(`MCP autoconnect failed: ${message}`, "warning");
|
|
816
|
+
});
|
|
739
817
|
});
|
|
740
818
|
|
|
741
819
|
pi.on("session_shutdown", async () => {
|
|
820
|
+
warmupPromise = null;
|
|
742
821
|
await closeAll();
|
|
743
822
|
});
|
|
744
823
|
|
|
745
824
|
pi.on("session_switch", async () => {
|
|
746
|
-
await
|
|
747
|
-
|
|
825
|
+
await reloadMcpState();
|
|
826
|
+
void warmupEnabledServers();
|
|
748
827
|
});
|
|
749
828
|
}
|
|
@@ -53,3 +53,19 @@ test("#3029: getOrConnect normalizes name for connection cache lookup", () => {
|
|
|
53
53
|
"getOrConnect should use config.name (canonical) as the connections cache key",
|
|
54
54
|
);
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
test("enabled MCP servers are warmed up on session start", () => {
|
|
58
|
+
assert.match(
|
|
59
|
+
source,
|
|
60
|
+
/pi\.on\("session_start", async \(_event, ctx\) => {[\s\S]*?warmupEnabledServers\(/,
|
|
61
|
+
"session_start should trigger MCP autoconnect warmup for enabled servers",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("warmupEnabledServers preloads tool schemas during autoconnect", () => {
|
|
66
|
+
assert.match(
|
|
67
|
+
source,
|
|
68
|
+
/async function warmupEnabledServers\([\s\S]*?client\.listTools\(undefined, \{ timeout: 30000 \}\)[\s\S]*?toolCache\.set\(/,
|
|
69
|
+
"warmupEnabledServers should list tools and populate tool cache during startup",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -191,8 +191,8 @@ function readAutoSwitchPlanModelSetting(): boolean {
|
|
|
191
191
|
const settingsPath = join(getAgentDir(), "settings.json");
|
|
192
192
|
if (!existsSync(settingsPath)) return false;
|
|
193
193
|
const raw = readFileSync(settingsPath, "utf-8");
|
|
194
|
-
const parsed = JSON.parse(raw) as {
|
|
195
|
-
return parsed.
|
|
194
|
+
const parsed = JSON.parse(raw) as { planModeAutoSwitchModel?: unknown };
|
|
195
|
+
return parsed.planModeAutoSwitchModel === true;
|
|
196
196
|
} catch {
|
|
197
197
|
return false;
|
|
198
198
|
}
|
|
@@ -418,10 +418,10 @@ async function approvePlan(
|
|
|
418
418
|
permissionMode: RestorablePermissionMode,
|
|
419
419
|
executeWithSubagent = false,
|
|
420
420
|
): Promise<void> {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
421
|
+
// Do NOT switch to reasoning model during execution.
|
|
422
|
+
// The reasoning model is only for plan-mode investigation, not execution.
|
|
423
|
+
// If a coding model is configured and we're using a subagent, the explicit
|
|
424
|
+
// model="<planModeCodingModel>" in the kickoff message will handle it.
|
|
425
425
|
|
|
426
426
|
state = {
|
|
427
427
|
...state,
|
|
@@ -75,6 +75,8 @@ type UsageRow = {
|
|
|
75
75
|
cacheWrite: number;
|
|
76
76
|
total: number;
|
|
77
77
|
cost: number;
|
|
78
|
+
totalDurationMs: number;
|
|
79
|
+
totalOutputForSpeed: number;
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
type UsageReport = {
|
|
@@ -313,6 +315,7 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
313
315
|
let projectLabel = basename(file);
|
|
314
316
|
let headerResolved = false;
|
|
315
317
|
let currentModel = "";
|
|
318
|
+
let lastUserTimestamp = 0;
|
|
316
319
|
|
|
317
320
|
const raw = readFileSync(file, "utf-8");
|
|
318
321
|
for (const line of raw.split("\n")) {
|
|
@@ -367,6 +370,8 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
367
370
|
cacheWrite: 0,
|
|
368
371
|
total: 0,
|
|
369
372
|
cost: 0,
|
|
373
|
+
totalDurationMs: 0,
|
|
374
|
+
totalOutputForSpeed: 0,
|
|
370
375
|
};
|
|
371
376
|
|
|
372
377
|
existing.messages += 1;
|
|
@@ -376,10 +381,27 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
376
381
|
existing.cacheWrite += cacheWrite;
|
|
377
382
|
existing.total += total;
|
|
378
383
|
existing.cost += cost;
|
|
384
|
+
|
|
385
|
+
// Track tok/sec: use preceding user message timestamp
|
|
386
|
+
if (output > 0 && lastUserTimestamp > 0) {
|
|
387
|
+
const durationMs = timestamp - lastUserTimestamp;
|
|
388
|
+
if (durationMs > 0) {
|
|
389
|
+
existing.totalDurationMs += durationMs;
|
|
390
|
+
existing.totalOutputForSpeed += output;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
379
394
|
rows.set(key, existing);
|
|
380
395
|
} else if (message.role === "user") {
|
|
381
396
|
const timestamp = Number(message.timestamp ?? 0);
|
|
382
|
-
if (!timestamp
|
|
397
|
+
if (!timestamp) continue;
|
|
398
|
+
|
|
399
|
+
// Always track last user timestamp for tok/sec calculation,
|
|
400
|
+
// even if this user message is outside the time range
|
|
401
|
+
// (the assistant response may still be within range).
|
|
402
|
+
lastUserTimestamp = timestamp;
|
|
403
|
+
|
|
404
|
+
if (timestamp < startMs || timestamp >= endMs) continue;
|
|
383
405
|
|
|
384
406
|
matchedUserPrompts++;
|
|
385
407
|
const model = currentModel;
|
|
@@ -397,6 +419,8 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
397
419
|
cacheWrite: 0,
|
|
398
420
|
total: 0,
|
|
399
421
|
cost: 0,
|
|
422
|
+
totalDurationMs: 0,
|
|
423
|
+
totalOutputForSpeed: 0,
|
|
400
424
|
};
|
|
401
425
|
|
|
402
426
|
existing.userPrompts += 1;
|
|
@@ -431,9 +455,11 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
431
455
|
acc.cacheWrite += row.cacheWrite;
|
|
432
456
|
acc.total += row.total;
|
|
433
457
|
acc.cost += row.cost;
|
|
458
|
+
acc.totalDurationMs += row.totalDurationMs;
|
|
459
|
+
acc.totalOutputForSpeed += row.totalOutputForSpeed;
|
|
434
460
|
return acc;
|
|
435
461
|
},
|
|
436
|
-
{ messages: 0, userPrompts: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 },
|
|
462
|
+
{ messages: 0, userPrompts: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0, totalDurationMs: 0, totalOutputForSpeed: 0 },
|
|
437
463
|
);
|
|
438
464
|
|
|
439
465
|
return {
|
|
@@ -448,6 +474,11 @@ function collectUsage(sessionFiles: string[], startMs: number, endMs: number, sc
|
|
|
448
474
|
};
|
|
449
475
|
}
|
|
450
476
|
|
|
477
|
+
function formatSpeed(totalDurationMs: number, totalOutputForSpeed: number): string {
|
|
478
|
+
if (totalDurationMs <= 0) return "—";
|
|
479
|
+
return Math.round(totalOutputForSpeed / (totalDurationMs / 1000)).toLocaleString();
|
|
480
|
+
}
|
|
481
|
+
|
|
451
482
|
function renderTable(report: UsageReport): string {
|
|
452
483
|
const firstColumnHeader = report.groupBy === "project"
|
|
453
484
|
? "project"
|
|
@@ -465,8 +496,11 @@ function renderTable(report: UsageReport): string {
|
|
|
465
496
|
write: formatInt(row.cacheWrite),
|
|
466
497
|
total: formatInt(row.total),
|
|
467
498
|
cost: formatCost(row.cost),
|
|
499
|
+
speed: formatSpeed(row.totalDurationMs, row.totalOutputForSpeed),
|
|
468
500
|
}));
|
|
469
501
|
|
|
502
|
+
const totalsSpeed = formatSpeed(report.totals.totalDurationMs, report.totals.totalOutputForSpeed);
|
|
503
|
+
|
|
470
504
|
const widths = {
|
|
471
505
|
label: Math.max(firstColumnHeader.length, ...displayRows.map((row) => row.label.length), 5),
|
|
472
506
|
userPrompts: Math.max(11, ...displayRows.map((row) => row.userPrompts.length), String(report.totals.userPrompts).length),
|
|
@@ -477,6 +511,7 @@ function renderTable(report: UsageReport): string {
|
|
|
477
511
|
write: Math.max(5, ...displayRows.map((row) => row.write.length), formatInt(report.totals.cacheWrite).length),
|
|
478
512
|
total: Math.max(5, ...displayRows.map((row) => row.total.length), formatInt(report.totals.total).length),
|
|
479
513
|
cost: Math.max(7, ...displayRows.map((row) => row.cost.length), formatCost(report.totals.cost).length),
|
|
514
|
+
speed: Math.max(5, ...displayRows.map((row) => row.speed.length), totalsSpeed.length),
|
|
480
515
|
};
|
|
481
516
|
|
|
482
517
|
const header = [
|
|
@@ -489,6 +524,7 @@ function renderTable(report: UsageReport): string {
|
|
|
489
524
|
"write".padStart(widths.write),
|
|
490
525
|
"total".padStart(widths.total),
|
|
491
526
|
"cost".padStart(widths.cost),
|
|
527
|
+
"tok/s".padStart(widths.speed),
|
|
492
528
|
].join(" ");
|
|
493
529
|
|
|
494
530
|
const divider = "-".repeat(header.length);
|
|
@@ -502,6 +538,7 @@ function renderTable(report: UsageReport): string {
|
|
|
502
538
|
row.write.padStart(widths.write),
|
|
503
539
|
row.total.padStart(widths.total),
|
|
504
540
|
row.cost.padStart(widths.cost),
|
|
541
|
+
row.speed.padStart(widths.speed),
|
|
505
542
|
].join(" "));
|
|
506
543
|
|
|
507
544
|
const totalsLine = [
|
|
@@ -514,6 +551,7 @@ function renderTable(report: UsageReport): string {
|
|
|
514
551
|
formatInt(report.totals.cacheWrite).padStart(widths.write),
|
|
515
552
|
formatInt(report.totals.total).padStart(widths.total),
|
|
516
553
|
formatCost(report.totals.cost).padStart(widths.cost),
|
|
554
|
+
totalsSpeed.padStart(widths.speed),
|
|
517
555
|
].join(" ");
|
|
518
556
|
|
|
519
557
|
return [header, divider, ...body, divider, totalsLine].join("\n");
|
|
@@ -303,6 +303,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
303
303
|
activationMode,
|
|
304
304
|
editorText: ctx.ui.getEditorText(),
|
|
305
305
|
holdToTalkSupported: isKittyProtocolActive(),
|
|
306
|
+
isEditorFocused: ctx.ui.isEditorFocused(),
|
|
306
307
|
onUnsupported: () => {
|
|
307
308
|
if (holdToTalkUnsupportedNotified) return;
|
|
308
309
|
holdToTalkUnsupportedNotified = true;
|
|
@@ -8,6 +8,7 @@ export interface PushToTalkState {
|
|
|
8
8
|
activationMode: VoiceActivationMode | null;
|
|
9
9
|
editorText: string;
|
|
10
10
|
holdToTalkSupported: boolean;
|
|
11
|
+
isEditorFocused: boolean;
|
|
11
12
|
onUnsupported?(): void;
|
|
12
13
|
startPushToTalk(): void | Promise<void>;
|
|
13
14
|
stopVoice(): void | Promise<void>;
|
|
@@ -16,6 +17,8 @@ export interface PushToTalkState {
|
|
|
16
17
|
export function handlePushToTalkInput(data: string, state: PushToTalkState): ReturnType<TerminalInputHandler> {
|
|
17
18
|
if (!matchesKey(data, Key.space)) return undefined;
|
|
18
19
|
|
|
20
|
+
if (!state.isEditorFocused) return undefined;
|
|
21
|
+
|
|
19
22
|
if (isKeyRelease(data)) {
|
|
20
23
|
if (state.activationMode === "push-to-talk") {
|
|
21
24
|
void Promise.resolve(state.stopVoice());
|
|
@@ -15,6 +15,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
15
15
|
activationMode: null,
|
|
16
16
|
editorText: "",
|
|
17
17
|
holdToTalkSupported: true,
|
|
18
|
+
isEditorFocused: true,
|
|
18
19
|
startPushToTalk: () => { startCalls += 1; },
|
|
19
20
|
stopVoice: () => { stopCalls += 1; },
|
|
20
21
|
});
|
|
@@ -32,6 +33,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
32
33
|
activationMode: null,
|
|
33
34
|
editorText: "hello",
|
|
34
35
|
holdToTalkSupported: true,
|
|
36
|
+
isEditorFocused: true,
|
|
35
37
|
startPushToTalk: () => { startCalls += 1; },
|
|
36
38
|
stopVoice: () => { },
|
|
37
39
|
});
|
|
@@ -48,6 +50,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
48
50
|
activationMode: "push-to-talk",
|
|
49
51
|
editorText: "",
|
|
50
52
|
holdToTalkSupported: true,
|
|
53
|
+
isEditorFocused: true,
|
|
51
54
|
startPushToTalk: () => { startCalls += 1; },
|
|
52
55
|
stopVoice: () => { },
|
|
53
56
|
});
|
|
@@ -64,6 +67,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
64
67
|
activationMode: "push-to-talk",
|
|
65
68
|
editorText: "",
|
|
66
69
|
holdToTalkSupported: true,
|
|
70
|
+
isEditorFocused: true,
|
|
67
71
|
startPushToTalk: () => { },
|
|
68
72
|
stopVoice: () => { stopCalls += 1; },
|
|
69
73
|
});
|
|
@@ -80,6 +84,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
80
84
|
activationMode: "toggle",
|
|
81
85
|
editorText: "",
|
|
82
86
|
holdToTalkSupported: true,
|
|
87
|
+
isEditorFocused: true,
|
|
83
88
|
startPushToTalk: () => { },
|
|
84
89
|
stopVoice: () => { stopCalls += 1; },
|
|
85
90
|
});
|
|
@@ -97,6 +102,7 @@ describe("voice push-to-talk handler", () => {
|
|
|
97
102
|
activationMode: null,
|
|
98
103
|
editorText: "",
|
|
99
104
|
holdToTalkSupported: false,
|
|
105
|
+
isEditorFocused: true,
|
|
100
106
|
onUnsupported: () => { notifyCalls += 1; },
|
|
101
107
|
startPushToTalk: () => { startCalls += 1; },
|
|
102
108
|
stopVoice: () => { },
|