lsd-pi 1.3.7 → 1.3.10
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 +230 -54
- package/dist/resources/extensions/mcp-client/mcp-manager-component.js +220 -0
- package/dist/resources/extensions/slash-commands/plan.js +72 -18
- package/dist/resources/extensions/subagent/agents.js +7 -0
- package/dist/resources/extensions/subagent/index.js +25 -8
- package/dist/resources/extensions/subagent/model-resolution.js +1 -0
- 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/__tests__/tool-summary-line.test.js +104 -2
- 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/assistant-message.d.ts +39 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +135 -18
- package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.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.d.ts +21 -2
- 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 +147 -9
- 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 +51 -13
- 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 +112 -18
- 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-state.d.ts +4 -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 +4 -0
- 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 +34 -4
- 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/__tests__/tool-summary-line.test.ts +129 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +158 -18
- 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 +164 -10
- package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +60 -13
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +123 -20
- 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-state.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +34 -4
- 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 +259 -58
- package/src/resources/extensions/mcp-client/mcp-manager-component.ts +256 -0
- package/src/resources/extensions/mcp-client/tests/mcp-manager-component.test.ts +141 -0
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +32 -0
- package/src/resources/extensions/slash-commands/plan.ts +76 -19
- package/src/resources/extensions/subagent/agents.ts +9 -0
- package/src/resources/extensions/subagent/index.ts +30 -8
- package/src/resources/extensions/subagent/model-resolution.ts +1 -0
- 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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Key, SelectList, matchesKey, truncateToWidth } from "@gsd/pi-tui";
|
|
2
|
+
function getSelectListTheme(theme) {
|
|
3
|
+
return {
|
|
4
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
5
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
6
|
+
description: (text) => theme.fg("muted", text),
|
|
7
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
8
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function serversToItems(servers) {
|
|
12
|
+
return servers.map((server) => ({
|
|
13
|
+
value: server.name,
|
|
14
|
+
label: server.name,
|
|
15
|
+
description: [
|
|
16
|
+
server.enabled ? "enabled" : "disabled",
|
|
17
|
+
server.transport,
|
|
18
|
+
server.connected ? "● connected" : "○ offline",
|
|
19
|
+
`${server.toolCount} tools`,
|
|
20
|
+
server.sourceLabel || undefined,
|
|
21
|
+
].filter(Boolean).join(" "),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export class McpManagerComponent {
|
|
25
|
+
theme;
|
|
26
|
+
callbacks;
|
|
27
|
+
selectList;
|
|
28
|
+
mode = "list";
|
|
29
|
+
inspectServerName = "";
|
|
30
|
+
inspectLines = [];
|
|
31
|
+
inspectScrollOffset = 0;
|
|
32
|
+
statusMessage = "";
|
|
33
|
+
busy = false;
|
|
34
|
+
statusTimeout = null;
|
|
35
|
+
constructor(callbacks, theme) {
|
|
36
|
+
this.callbacks = callbacks;
|
|
37
|
+
this.theme = theme;
|
|
38
|
+
this.selectList = new SelectList([], 8, getSelectListTheme(theme));
|
|
39
|
+
this.bindSelectList();
|
|
40
|
+
this.refreshList();
|
|
41
|
+
}
|
|
42
|
+
invalidate() {
|
|
43
|
+
this.selectList.invalidate();
|
|
44
|
+
}
|
|
45
|
+
dispose() {
|
|
46
|
+
if (this.statusTimeout) {
|
|
47
|
+
clearTimeout(this.statusTimeout);
|
|
48
|
+
this.statusTimeout = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
getMode() {
|
|
52
|
+
return this.mode;
|
|
53
|
+
}
|
|
54
|
+
handleInput(data) {
|
|
55
|
+
if (this.mode === "inspect") {
|
|
56
|
+
this.handleInspectInput(data);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
60
|
+
this.callbacks.onClose();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (data === "i") {
|
|
64
|
+
void this.handleInspect();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (data === "r") {
|
|
68
|
+
void this.handleReconnect();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.selectList.handleInput(data);
|
|
72
|
+
this.callbacks.requestRender();
|
|
73
|
+
}
|
|
74
|
+
render(width) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
const add = (line = "") => lines.push(truncateToWidth(line, width));
|
|
77
|
+
const divider = this.theme.fg("border", "─".repeat(Math.max(width, 1)));
|
|
78
|
+
add(divider);
|
|
79
|
+
if (this.mode === "inspect") {
|
|
80
|
+
add(this.theme.bold(this.theme.fg("toolTitle", ` MCP Tools · ${this.inspectServerName}`)) +
|
|
81
|
+
this.theme.fg("dim", " esc/q: back ↑↓/pgup/pgdn/home/end: scroll"));
|
|
82
|
+
add("");
|
|
83
|
+
const bodyHeight = Math.max(8, width > 0 ? 18 : 8);
|
|
84
|
+
const maxOffset = Math.max(0, this.inspectLines.length - bodyHeight);
|
|
85
|
+
this.inspectScrollOffset = Math.max(0, Math.min(this.inspectScrollOffset, maxOffset));
|
|
86
|
+
const visibleLines = this.inspectLines.slice(this.inspectScrollOffset, this.inspectScrollOffset + bodyHeight);
|
|
87
|
+
for (const line of visibleLines)
|
|
88
|
+
add(line);
|
|
89
|
+
if (visibleLines.length === 0)
|
|
90
|
+
add(this.theme.fg("dim", " No tool information"));
|
|
91
|
+
add("");
|
|
92
|
+
add(divider);
|
|
93
|
+
add(this.theme.fg("dim", ` ${this.inspectLines.length} lines`));
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
add(this.theme.bold(this.theme.fg("toolTitle", " MCP Servers")) +
|
|
97
|
+
this.theme.fg("dim", " ↑↓ navigate enter: toggle i: inspect r: reconnect esc: close"));
|
|
98
|
+
add("");
|
|
99
|
+
lines.push(...this.selectList.render(width));
|
|
100
|
+
add("");
|
|
101
|
+
add(divider);
|
|
102
|
+
const servers = this.callbacks.getServers();
|
|
103
|
+
const enabled = servers.filter((server) => server.enabled).length;
|
|
104
|
+
let footer = this.theme.fg("dim", ` ${servers.length} servers · ${enabled} enabled`);
|
|
105
|
+
if (this.busy)
|
|
106
|
+
footer += this.theme.fg("accent", " · working…");
|
|
107
|
+
if (this.statusMessage)
|
|
108
|
+
footer += this.theme.fg("accent", ` — ${this.statusMessage}`);
|
|
109
|
+
add(footer);
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
bindSelectList() {
|
|
113
|
+
this.selectList.onSelect = () => {
|
|
114
|
+
void this.handleToggle();
|
|
115
|
+
};
|
|
116
|
+
this.selectList.onCancel = () => {
|
|
117
|
+
this.callbacks.onClose();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
refreshList(preferredName) {
|
|
121
|
+
const currentSelected = preferredName ?? this.selectList.getSelectedItem()?.value;
|
|
122
|
+
this.selectList = new SelectList(serversToItems(this.callbacks.getServers()), 8, getSelectListTheme(this.theme));
|
|
123
|
+
this.bindSelectList();
|
|
124
|
+
if (currentSelected) {
|
|
125
|
+
const items = this.callbacks.getServers();
|
|
126
|
+
const index = items.findIndex((item) => item.name === currentSelected);
|
|
127
|
+
if (index >= 0)
|
|
128
|
+
this.selectList.setSelectedIndex(index);
|
|
129
|
+
}
|
|
130
|
+
this.callbacks.requestRender();
|
|
131
|
+
}
|
|
132
|
+
setStatus(message) {
|
|
133
|
+
this.statusMessage = message;
|
|
134
|
+
this.callbacks.requestRender();
|
|
135
|
+
if (this.statusTimeout)
|
|
136
|
+
clearTimeout(this.statusTimeout);
|
|
137
|
+
if (!message)
|
|
138
|
+
return;
|
|
139
|
+
this.statusTimeout = setTimeout(() => {
|
|
140
|
+
this.statusMessage = "";
|
|
141
|
+
this.callbacks.requestRender();
|
|
142
|
+
}, 3000);
|
|
143
|
+
this.statusTimeout.unref?.();
|
|
144
|
+
}
|
|
145
|
+
getSelectedName() {
|
|
146
|
+
return this.selectList.getSelectedItem()?.value;
|
|
147
|
+
}
|
|
148
|
+
async runBusy(task) {
|
|
149
|
+
if (this.busy)
|
|
150
|
+
return;
|
|
151
|
+
this.busy = true;
|
|
152
|
+
this.callbacks.requestRender();
|
|
153
|
+
try {
|
|
154
|
+
await task();
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
this.busy = false;
|
|
158
|
+
this.callbacks.requestRender();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async handleToggle() {
|
|
162
|
+
const name = this.getSelectedName();
|
|
163
|
+
if (!name)
|
|
164
|
+
return;
|
|
165
|
+
await this.runBusy(async () => {
|
|
166
|
+
this.setStatus(`Toggling ${name}...`);
|
|
167
|
+
const updated = await this.callbacks.onToggle(name);
|
|
168
|
+
this.refreshList(updated?.name ?? name);
|
|
169
|
+
if (updated) {
|
|
170
|
+
this.setStatus(`${updated.name}: ${updated.enabled ? "enabled" : "disabled"}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async handleInspect() {
|
|
175
|
+
const name = this.getSelectedName();
|
|
176
|
+
if (!name)
|
|
177
|
+
return;
|
|
178
|
+
await this.runBusy(async () => {
|
|
179
|
+
this.setStatus(`Loading tools for ${name}...`);
|
|
180
|
+
const text = await this.callbacks.onInspect(name);
|
|
181
|
+
this.inspectServerName = name;
|
|
182
|
+
this.inspectLines = text.split("\n");
|
|
183
|
+
this.inspectScrollOffset = 0;
|
|
184
|
+
this.mode = "inspect";
|
|
185
|
+
this.setStatus("");
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async handleReconnect() {
|
|
189
|
+
const name = this.getSelectedName();
|
|
190
|
+
if (!name)
|
|
191
|
+
return;
|
|
192
|
+
await this.runBusy(async () => {
|
|
193
|
+
this.setStatus(`Reconnecting ${name}...`);
|
|
194
|
+
const updated = await this.callbacks.onReconnect(name);
|
|
195
|
+
this.refreshList(updated?.name ?? name);
|
|
196
|
+
this.setStatus(updated ? `${updated.name}: reconnected` : `${name}: reconnect failed`);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
handleInspectInput(data) {
|
|
200
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
201
|
+
this.mode = "list";
|
|
202
|
+
this.callbacks.requestRender();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const page = 12;
|
|
206
|
+
if (matchesKey(data, Key.up))
|
|
207
|
+
this.inspectScrollOffset -= 1;
|
|
208
|
+
else if (matchesKey(data, Key.down))
|
|
209
|
+
this.inspectScrollOffset += 1;
|
|
210
|
+
else if (matchesKey(data, Key.pageUp))
|
|
211
|
+
this.inspectScrollOffset -= page;
|
|
212
|
+
else if (matchesKey(data, Key.pageDown))
|
|
213
|
+
this.inspectScrollOffset += page;
|
|
214
|
+
else if (matchesKey(data, Key.home))
|
|
215
|
+
this.inspectScrollOffset = 0;
|
|
216
|
+
else if (matchesKey(data, Key.end))
|
|
217
|
+
this.inspectScrollOffset = Number.MAX_SAFE_INTEGER;
|
|
218
|
+
this.callbacks.requestRender();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -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;
|
|
@@ -277,7 +277,7 @@ async function setModelIfNeeded(pi, ctx, modelRef) {
|
|
|
277
277
|
return true;
|
|
278
278
|
}
|
|
279
279
|
function buildExecutionKickoffMessage(options) {
|
|
280
|
-
const { permissionMode, executeWithSubagent = false } = options;
|
|
280
|
+
const { permissionMode, executeWithSubagent = false, executionNote } = options;
|
|
281
281
|
const task = state.task.trim();
|
|
282
282
|
if (!executeWithSubagent) {
|
|
283
283
|
const details = [
|
|
@@ -287,6 +287,9 @@ function buildExecutionKickoffMessage(options) {
|
|
|
287
287
|
details.push(`Original task: ${task}`);
|
|
288
288
|
if (state.latestPlanPath)
|
|
289
289
|
details.push(`Use the approved plan artifact at ${state.latestPlanPath} as the execution plan.`);
|
|
290
|
+
if (executionNote)
|
|
291
|
+
details.push(`User execution note: ${executionNote}`);
|
|
292
|
+
details.push("After implementation: guide the user through verification by presenting a concise checklist based on the plan's Acceptance Criteria and Verification Plan. Run applicable checks (build, lint, tests) and report results.");
|
|
290
293
|
return details.join(" ");
|
|
291
294
|
}
|
|
292
295
|
const codingModel = readPlanModeCodingModel();
|
|
@@ -303,13 +306,24 @@ function buildExecutionKickoffMessage(options) {
|
|
|
303
306
|
details.push(`Original task: ${task}`);
|
|
304
307
|
if (state.latestPlanPath)
|
|
305
308
|
details.push(`Primary plan artifact: ${state.latestPlanPath}`);
|
|
309
|
+
if (executionNote) {
|
|
310
|
+
details.push(`User execution note: ${executionNote}`);
|
|
311
|
+
// If the note contains a model request, surface it explicitly so the agent
|
|
312
|
+
// doesn't silently drop it. The model override will be resolved by
|
|
313
|
+
// normalizeSubagentModel when the subagent tool is invoked.
|
|
314
|
+
const modelMatch = executionNote.match(/\b(?:model|use\s+model)\s+["']?([\w.-]+(?:\/[\w.-]+)?)["']?\b/i);
|
|
315
|
+
if (modelMatch?.[1]) {
|
|
316
|
+
details.push(`The user explicitly requested model "${modelMatch[1]}". You MUST pass model="${modelMatch[1]}" in the subagent tool call. If normalizeSubagentModel cannot resolve this model, report the error to the user instead of silently falling back.`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
306
319
|
details.push("Important: if the plan is large and you estimate it would exceed a single subagent's context window (~200k tokens), " +
|
|
307
320
|
"split execution across multiple sequential subagents instead of one. " +
|
|
308
321
|
"Use the subagent tool's chain mode: pass a \"chain\" array where each entry covers one self-contained phase or group of steps from the plan. " +
|
|
309
322
|
"Each chain entry should include the agent name, a focused task description for that phase, and may reference {previous} to receive the prior phase's output as handoff context. " +
|
|
310
323
|
"Only split when genuinely needed — prefer a single subagent for plans that fit comfortably.");
|
|
311
324
|
details.push("After all subagents complete: (1) do a quick review of the implementation — check that the plan steps were actually carried out, spot obvious issues or missed pieces, and verify the code compiles/passes lint if applicable. " +
|
|
312
|
-
"(2) Then
|
|
325
|
+
"(2) Then guide the user through verification: present a concise checklist based on the plan's Acceptance Criteria and Verification Plan sections. Run applicable checks (build, lint, tests) and report results. " +
|
|
326
|
+
"(3) Summarize what was done, what (if anything) needs follow-up, and flag any concerns found during review.");
|
|
313
327
|
return details.join(" ");
|
|
314
328
|
}
|
|
315
329
|
let pendingNewSession = null;
|
|
@@ -331,11 +345,11 @@ function scheduleNewSession(pi, ctx) {
|
|
|
331
345
|
// Must use the /prefix so tryExecuteExtensionCommand parses the name correctly.
|
|
332
346
|
pi.executeSlashCommand("/plan-execute-new-session");
|
|
333
347
|
}
|
|
334
|
-
async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
348
|
+
async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false, executionNote) {
|
|
349
|
+
// Do NOT switch to reasoning model during execution.
|
|
350
|
+
// The reasoning model is only for plan-mode investigation, not execution.
|
|
351
|
+
// If a coding model is configured and we're using a subagent, the explicit
|
|
352
|
+
// model="<planModeCodingModel>" in the kickoff message will handle it.
|
|
339
353
|
state = {
|
|
340
354
|
...state,
|
|
341
355
|
targetPermissionMode: permissionMode,
|
|
@@ -347,7 +361,7 @@ async function approvePlan(pi, ctx, permissionMode, executeWithSubagent = false)
|
|
|
347
361
|
// subagent tool with the default session model BEFORE it ever sees the
|
|
348
362
|
// explicit model="<planModeCodingModel>" instruction. Steering ensures the
|
|
349
363
|
// configured plan-mode coding model reaches the subagent invocation.
|
|
350
|
-
await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent }), { deliverAs: "steer" });
|
|
364
|
+
await pi.sendUserMessage(buildExecutionKickoffMessage({ permissionMode, executeWithSubagent, executionNote }), { deliverAs: "steer" });
|
|
351
365
|
}
|
|
352
366
|
async function cancelPlan(pi, ctx, clearTask = true) {
|
|
353
367
|
const restoreMode = state.previousMode ?? "accept-on-edit";
|
|
@@ -359,10 +373,12 @@ async function cancelPlan(pi, ctx, clearTask = true) {
|
|
|
359
373
|
function buildPlanModeSystemPrompt() {
|
|
360
374
|
const details = [
|
|
361
375
|
"You are currently in plan mode.",
|
|
376
|
+
"Terse output. All technical substance stays. Only fluff dies. Fragments OK.",
|
|
362
377
|
"Investigate, clarify scope, and produce a persisted execution plan before making source changes.",
|
|
363
378
|
"If requirements are ambiguous or constraints are missing, ask concise clarifying questions before drafting or saving a plan.",
|
|
364
379
|
`Before writing or updating a plan artifact, make sure your confidence is at least ${MIN_PLAN_CONFIDENCE}/10. If confidence is lower, investigate more or ask clarifying questions first.`,
|
|
365
380
|
"Include an explicit confidence line in every saved plan, for example: \"Confidence: 8/10\" or higher.",
|
|
381
|
+
"Every saved plan MUST include explicit \"Acceptance Criteria\" and \"Verification Plan\" sections. Plans missing these sections will be rejected for approval.",
|
|
366
382
|
"When adjusting an existing saved plan, prefer the edit tool for targeted changes. Rewrite the whole file only when the structure changes substantially or an exact edit is impractical.",
|
|
367
383
|
"Do not modify source files or run side-effect commands while plan mode is active.",
|
|
368
384
|
"Persist plan artifacts under .lsd/plan/.",
|
|
@@ -411,6 +427,29 @@ function buildApprovalActionInstructions() {
|
|
|
411
427
|
function buildApprovalDialogInstructions() {
|
|
412
428
|
return buildApprovalActionInstructions();
|
|
413
429
|
}
|
|
430
|
+
/** Required heading patterns for plan artifacts. Matches common variants. */
|
|
431
|
+
const REQUIRED_PLAN_SECTIONS = [
|
|
432
|
+
{ pattern: /\b(acceptance\s*criteria|success\s*criteria|done\s*criteria)\b/i, label: "Acceptance Criteria" },
|
|
433
|
+
{ pattern: /\b(verification\s*(plan|steps|strategy)|how\s+to\s+verify|testing\s*plan)\b/i, label: "Verification Plan" },
|
|
434
|
+
];
|
|
435
|
+
function validatePlanArtifact(markdown) {
|
|
436
|
+
const missing = [];
|
|
437
|
+
for (const section of REQUIRED_PLAN_SECTIONS) {
|
|
438
|
+
if (!section.pattern.test(markdown)) {
|
|
439
|
+
missing.push(section.label);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { valid: missing.length === 0, missing };
|
|
443
|
+
}
|
|
444
|
+
function buildPlanValidationSteeringMessage(planPath, missing) {
|
|
445
|
+
const missingList = missing.map((m) => `- **${m}**`).join("\n");
|
|
446
|
+
return [
|
|
447
|
+
`Plan artifact saved at ${planPath}.`,
|
|
448
|
+
`The plan is missing required sections before it can be approved for implementation:`,
|
|
449
|
+
missingList,
|
|
450
|
+
`Please revise the plan to include these sections, then re-save. Do not ask for approval until all required sections are present.`,
|
|
451
|
+
].join("\n\n");
|
|
452
|
+
}
|
|
414
453
|
function buildApprovalSteeringMessage(planPath) {
|
|
415
454
|
return [
|
|
416
455
|
`Plan artifact saved at ${planPath}.`,
|
|
@@ -621,13 +660,26 @@ export default function planCommand(pi) {
|
|
|
621
660
|
return;
|
|
622
661
|
}
|
|
623
662
|
const planMarkdown = readPlanArtifact(path);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
663
|
+
if (!planMarkdown) {
|
|
664
|
+
ctx.ui?.notify?.("Plan artifact could not be read for validation", "warning");
|
|
665
|
+
pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const validation = validatePlanArtifact(planMarkdown);
|
|
669
|
+
if (!validation.valid) {
|
|
670
|
+
ctx.ui?.notify?.("Plan missing required sections — see guidance below", "warning");
|
|
671
|
+
pi.sendUserMessage(buildPlanValidationSteeringMessage(path, validation.missing), { deliverAs: "steer" });
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
pi.sendMessage({
|
|
675
|
+
customType: "plan-mode-preview",
|
|
676
|
+
content: buildPlanPreviewMessage(path, planMarkdown),
|
|
677
|
+
display: true,
|
|
678
|
+
});
|
|
679
|
+
ctx.ui?.notify?.("/plan to show plan", "info");
|
|
680
|
+
pi.sendUserMessage(buildApprovalSteeringMessage(path), { deliverAs: "steer" });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
631
683
|
}
|
|
632
684
|
return;
|
|
633
685
|
}
|
|
@@ -676,12 +728,13 @@ export default function planCommand(pi) {
|
|
|
676
728
|
permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
|
|
677
729
|
executeWithSubagent: false,
|
|
678
730
|
};
|
|
731
|
+
const permissionNote = getAnswerNote(permissionAnswer);
|
|
679
732
|
state = { ...state, targetPermissionMode: executionMode.permissionMode };
|
|
680
733
|
if (executionMode.executeWithSubagent) {
|
|
681
734
|
const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
|
|
682
735
|
ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
|
|
683
736
|
}
|
|
684
|
-
await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
|
|
737
|
+
await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent, permissionNote);
|
|
685
738
|
return;
|
|
686
739
|
}
|
|
687
740
|
// ── First question answered (action) ──────────────────────────────────
|
|
@@ -699,12 +752,13 @@ export default function planCommand(pi) {
|
|
|
699
752
|
permissionMode: DEFAULT_APPROVAL_PERMISSION_MODE,
|
|
700
753
|
executeWithSubagent: false,
|
|
701
754
|
};
|
|
755
|
+
const actionNote = getAnswerNote(actionAnswer);
|
|
702
756
|
state = { ...state, targetPermissionMode: executionMode.permissionMode };
|
|
703
757
|
if (executionMode.executeWithSubagent) {
|
|
704
758
|
const modeLabel = executionMode.permissionMode === "danger-full-access" ? "bypass" : "auto";
|
|
705
759
|
ctx.ui?.notify?.(`Plan approved: subagent(${modeLabel})`, "info");
|
|
706
760
|
}
|
|
707
|
-
await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent);
|
|
761
|
+
await approvePlan(pi, ctx, executionMode.permissionMode, executionMode.executeWithSubagent, actionNote);
|
|
708
762
|
return;
|
|
709
763
|
}
|
|
710
764
|
if (actionSelection.includes(REVIEW_LABEL)) {
|
|
@@ -6,6 +6,8 @@ import * as path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
|
|
8
8
|
const PROJECT_AGENT_DIR_CANDIDATES = [".lsd", ".gsd", ".pi"];
|
|
9
|
+
/** Fixed read-only tool set for the reserved `scout` agent. */
|
|
10
|
+
const SCOUT_ALLOWED_TOOLS = ["read", "lsp", "grep", "find", "ls"];
|
|
9
11
|
function normalizeAgentModel(model) {
|
|
10
12
|
const trimmed = model?.trim();
|
|
11
13
|
if (!trimmed)
|
|
@@ -117,6 +119,11 @@ export function discoverAgents(cwd, scope) {
|
|
|
117
119
|
else {
|
|
118
120
|
addAgents(projectAgents);
|
|
119
121
|
}
|
|
122
|
+
// Enforce reserved agent tool policies — scout is always read-only
|
|
123
|
+
const scout = agentMap.get("scout");
|
|
124
|
+
if (scout) {
|
|
125
|
+
scout.tools = [...SCOUT_ALLOWED_TOOLS];
|
|
126
|
+
}
|
|
120
127
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
121
128
|
}
|
|
122
129
|
export function formatAgentList(agents, maxItems) {
|
|
@@ -997,11 +997,13 @@ export default function (pi) {
|
|
|
997
997
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
998
998
|
const hasSingle = Boolean(params.agent && params.task);
|
|
999
999
|
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
|
1000
|
-
const makeDetails = (mode) => (results) => ({
|
|
1000
|
+
const makeDetails = (mode) => (results, opts) => ({
|
|
1001
1001
|
mode,
|
|
1002
1002
|
agentScope,
|
|
1003
1003
|
projectAgentsDir: discovery.projectAgentsDir,
|
|
1004
1004
|
results,
|
|
1005
|
+
totalChainSteps: opts?.totalSteps,
|
|
1006
|
+
currentChainStep: opts?.currentStep,
|
|
1005
1007
|
});
|
|
1006
1008
|
const trackInProcessDepth = (started, depth, ancestry) => {
|
|
1007
1009
|
const sessionId = started.handle.sessionId;
|
|
@@ -1090,7 +1092,8 @@ export default function (pi) {
|
|
|
1090
1092
|
for (let i = 0; i < params.chain.length; i++) {
|
|
1091
1093
|
const step = params.chain[i];
|
|
1092
1094
|
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
1093
|
-
// Create update callback that includes all previous results
|
|
1095
|
+
// Create update callback that includes all previous results + chain progress
|
|
1096
|
+
const chainTotalSteps = params.chain.length;
|
|
1094
1097
|
const chainUpdate = onUpdate
|
|
1095
1098
|
? (partial) => {
|
|
1096
1099
|
// Combine completed results with current streaming result
|
|
@@ -1099,7 +1102,10 @@ export default function (pi) {
|
|
|
1099
1102
|
const allResults = [...results, currentResult];
|
|
1100
1103
|
onUpdate({
|
|
1101
1104
|
content: partial.content,
|
|
1102
|
-
details: makeDetails("chain")(allResults
|
|
1105
|
+
details: makeDetails("chain")(allResults, {
|
|
1106
|
+
totalSteps: chainTotalSteps,
|
|
1107
|
+
currentStep: i + 1,
|
|
1108
|
+
}),
|
|
1103
1109
|
});
|
|
1104
1110
|
}
|
|
1105
1111
|
}
|
|
@@ -1117,7 +1123,7 @@ export default function (pi) {
|
|
|
1117
1123
|
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
1118
1124
|
return {
|
|
1119
1125
|
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
1120
|
-
details: makeDetails("chain")(results),
|
|
1126
|
+
details: makeDetails("chain")(results, { totalSteps: params.chain.length }),
|
|
1121
1127
|
isError: true,
|
|
1122
1128
|
};
|
|
1123
1129
|
}
|
|
@@ -1125,7 +1131,7 @@ export default function (pi) {
|
|
|
1125
1131
|
}
|
|
1126
1132
|
return {
|
|
1127
1133
|
content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
|
|
1128
|
-
details: makeDetails("chain")(results),
|
|
1134
|
+
details: makeDetails("chain")(results, { totalSteps: params.chain.length }),
|
|
1129
1135
|
};
|
|
1130
1136
|
}
|
|
1131
1137
|
if (params.tasks && params.tasks.length > 0) {
|
|
@@ -1653,13 +1659,24 @@ export default function (pi) {
|
|
|
1653
1659
|
};
|
|
1654
1660
|
if (details.mode === "chain") {
|
|
1655
1661
|
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1656
|
-
const
|
|
1662
|
+
const isRunning = details.currentChainStep != null;
|
|
1663
|
+
const icon = !isRunning && successCount === details.results.length
|
|
1664
|
+
? theme.fg("success", "✓")
|
|
1665
|
+
: isRunning
|
|
1666
|
+
? theme.fg("accent", "◉")
|
|
1667
|
+
: theme.fg("error", "✗");
|
|
1668
|
+
const totalLabel = details.totalChainSteps != null
|
|
1669
|
+
? `${details.results.length}/${details.totalChainSteps} steps`
|
|
1670
|
+
: `${details.results.length} steps`;
|
|
1671
|
+
const progressLabel = isRunning
|
|
1672
|
+
? ` (step ${details.currentChainStep} running)`
|
|
1673
|
+
: "";
|
|
1657
1674
|
if (expanded) {
|
|
1658
1675
|
const container = new Container();
|
|
1659
1676
|
container.addChild(new Text(icon +
|
|
1660
1677
|
" " +
|
|
1661
1678
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1662
|
-
theme.fg("accent", `${
|
|
1679
|
+
theme.fg("accent", `${totalLabel}${progressLabel}`), 0, 0));
|
|
1663
1680
|
for (const r of details.results) {
|
|
1664
1681
|
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1665
1682
|
const displayItems = getDisplayItems(r.messages);
|
|
@@ -1693,7 +1710,7 @@ export default function (pi) {
|
|
|
1693
1710
|
let text = icon +
|
|
1694
1711
|
" " +
|
|
1695
1712
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1696
|
-
theme.fg("accent", `${
|
|
1713
|
+
theme.fg("accent", `${totalLabel}${progressLabel}`);
|
|
1697
1714
|
for (const r of details.results) {
|
|
1698
1715
|
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1699
1716
|
const displayItems = getDisplayItems(r.messages);
|
|
@@ -11,6 +11,7 @@ const BARE_MODEL_PROVIDER_RULES = [
|
|
|
11
11
|
matches: (modelId) => /^(mistral-|ministral-|codestral-)/.test(modelId),
|
|
12
12
|
},
|
|
13
13
|
{ provider: "groq", matches: (modelId) => modelId.startsWith("llama-") || modelId.startsWith("mixtral-") },
|
|
14
|
+
{ provider: "zhipu", matches: (modelId) => modelId.startsWith("glm-") },
|
|
14
15
|
];
|
|
15
16
|
export function inferProviderForBareModel(modelId) {
|
|
16
17
|
const normalizedModelId = modelId.trim().toLowerCase();
|
|
@@ -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());
|