@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.2
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/CHANGELOG.md +92 -20
- package/dist/cli.js +3485 -3448
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +42 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +139 -54
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +221 -42
- package/src/session/agent-session.ts +1285 -348
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +48 -0
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +7 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
package/src/mcp/manager.ts
CHANGED
|
@@ -572,8 +572,14 @@ export class MCPManager {
|
|
|
572
572
|
};
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Ownership is matched via `mcpServerName`, never a `mcp__${name}_` name
|
|
577
|
+
* prefix: tool names are lossy-sanitized, so one server's sanitized name
|
|
578
|
+
* can prefix another's (`atlassian` vs `atlassian:atlassian`) and a name
|
|
579
|
+
* with sanitized characters never prefix-matches its own tools at all.
|
|
580
|
+
*/
|
|
575
581
|
#replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
|
|
576
|
-
this.#tools = this.#tools.filter(t =>
|
|
582
|
+
this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
|
|
577
583
|
this.#tools.push(...tools);
|
|
578
584
|
// Stable sort by name so reconnect order does not perturb the array.
|
|
579
585
|
// See `sortMCPToolsByName` for the cache-stability rationale.
|
|
@@ -762,8 +768,8 @@ export class MCPManager {
|
|
|
762
768
|
}
|
|
763
769
|
|
|
764
770
|
// Remove tools from this server and notify consumers
|
|
765
|
-
const hadTools = this.#tools.some(t => t.name
|
|
766
|
-
this.#tools = this.#tools.filter(t =>
|
|
771
|
+
const hadTools = this.#tools.some(t => t.mcpServerName === name);
|
|
772
|
+
this.#tools = this.#tools.filter(t => t.mcpServerName !== name);
|
|
767
773
|
if (hadTools) this.#onToolsChanged?.(this.#tools);
|
|
768
774
|
|
|
769
775
|
// Notify prompt consumers so stale commands are cleared
|
|
@@ -54,6 +54,18 @@ export interface StdioSpawnCommand {
|
|
|
54
54
|
* grandchildren keep stdout routed through our pipe (#3544).
|
|
55
55
|
*/
|
|
56
56
|
detached: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Pass argv to `Bun.spawn` verbatim (Windows only), suppressing the
|
|
59
|
+
* default libuv backslash-quoting.
|
|
60
|
+
*
|
|
61
|
+
* Set when `cmd` already holds a `cmd.exe /d /e:ON /v:OFF /c "<line>"`
|
|
62
|
+
* command line escaped for `cmd.exe`'s parser (see `buildCmdExeArgv`).
|
|
63
|
+
* libuv's quoting targets `CommandLineToArgvW`, not `cmd.exe`, so letting
|
|
64
|
+
* it re-quote a batch launch would corrupt arguments and re-open the
|
|
65
|
+
* `%VAR%` / quote-injection holes the escaping closes (BatBadBut,
|
|
66
|
+
* CVE-2024-24576).
|
|
67
|
+
*/
|
|
68
|
+
windowsVerbatimArguments?: boolean;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/** Inputs used to resolve platform-specific stdio spawn behavior. */
|
|
@@ -203,23 +215,6 @@ async function resolveWindowsNpmShimCommand(
|
|
|
203
215
|
};
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
function quoteCmdArg(value: string): string {
|
|
207
|
-
if (value.length === 0) return '""';
|
|
208
|
-
let result = '"';
|
|
209
|
-
for (const char of value) {
|
|
210
|
-
if (char === '"') {
|
|
211
|
-
result += '^"';
|
|
212
|
-
} else if (char === "^") {
|
|
213
|
-
result += "^^";
|
|
214
|
-
} else if (char === "%") {
|
|
215
|
-
result += "^%";
|
|
216
|
-
} else {
|
|
217
|
-
result += char;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return `${result}"`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
function isWindowsBatchCommand(command: string): boolean {
|
|
224
219
|
return WINDOWS_BATCH_EXTENSIONS.has(path.extname(command).toLowerCase());
|
|
225
220
|
}
|
|
@@ -229,10 +224,93 @@ function resolveComSpec(env: Record<string, string | undefined>): string {
|
|
|
229
224
|
return comspec && comspec.length > 0 ? comspec : "cmd.exe";
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
227
|
+
// Argument bytes cmd.exe delivers unchanged without quoting. Anything outside
|
|
228
|
+
// this set (spaces, quotes, `%`, shell metacharacters, non-ASCII) forces the
|
|
229
|
+
// quoted+escaped path below. Mirrors the fuzz-tested allow-list from Zig's
|
|
230
|
+
// BatBadBut mitigation.
|
|
231
|
+
const CMD_SAFE_ARG = /^[A-Za-z0-9#$*+\-./:?@\\_]+$/;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Escape the interior of a `cmd.exe`-quoted token: neutralize `%VAR%` expansion
|
|
235
|
+
* and double any backslash run that precedes a quote (including the caller's
|
|
236
|
+
* closing quote) so `CommandLineToArgvW` delivers the backslashes literally.
|
|
237
|
+
*
|
|
238
|
+
* `cmd.exe` re-parses the whole `/c` string and expands `%…%` *before* the
|
|
239
|
+
* batch shim's own argv split runs, so both the command path and every argument
|
|
240
|
+
* must pass through this. Percent → `%%cd:~,%` (which expands to nothing,
|
|
241
|
+
* leaving a literal `%`) and `"` → `""` are the documented BatBadBut mitigation
|
|
242
|
+
* (CVE-2024-24576). The caller supplies the surrounding double quotes.
|
|
243
|
+
*
|
|
244
|
+
* @see https://flatt.tech/research/posts/batbadbut-you-cant-securely-execute-commands-on-windows/
|
|
245
|
+
*/
|
|
246
|
+
function escapeCmdQuotedInterior(value: string): string {
|
|
247
|
+
let out = "";
|
|
248
|
+
let backslashes = 0;
|
|
249
|
+
for (const ch of value) {
|
|
250
|
+
if (ch === "\\") {
|
|
251
|
+
backslashes += 1;
|
|
252
|
+
out += ch;
|
|
253
|
+
} else if (ch === '"') {
|
|
254
|
+
out += "\\".repeat(backslashes);
|
|
255
|
+
out += '""';
|
|
256
|
+
backslashes = 0;
|
|
257
|
+
} else if (ch === "%") {
|
|
258
|
+
out += "%%cd:~,%";
|
|
259
|
+
backslashes = 0;
|
|
260
|
+
} else {
|
|
261
|
+
backslashes = 0;
|
|
262
|
+
out += ch;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Double the trailing backslash run so it stays literal before the closing
|
|
266
|
+
// quote the caller appends.
|
|
267
|
+
out += "\\".repeat(backslashes);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Reject bytes that cannot round-trip through `cmd.exe`'s `/c` command line. */
|
|
272
|
+
function assertCmdBatchToken(value: string, kind: "command" | "argument"): void {
|
|
273
|
+
// NUL/LF act as an end-of-command marker and CR is stripped, so any of them
|
|
274
|
+
// would silently truncate or corrupt the launch.
|
|
275
|
+
if (/[\0\r\n]/.test(value)) {
|
|
276
|
+
throw new Error(`Windows batch MCP ${kind} cannot contain NUL, CR, or LF characters`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Escape one argument for `cmd.exe`'s command-line pre-parse so a `.cmd`/`.bat`
|
|
282
|
+
* shim receives it verbatim. Quotes only when the argument is empty, ends in a
|
|
283
|
+
* backslash, or holds a byte outside {@link CMD_SAFE_ARG}; the quoted body is
|
|
284
|
+
* escaped by {@link escapeCmdQuotedInterior}.
|
|
285
|
+
*
|
|
286
|
+
* @throws when the argument contains NUL, CR, or LF (see {@link assertCmdBatchToken}).
|
|
287
|
+
*/
|
|
288
|
+
function escapeCmdBatchArg(arg: string): string {
|
|
289
|
+
assertCmdBatchToken(arg, "argument");
|
|
290
|
+
const needsQuotes = arg.length === 0 || arg.endsWith("\\") || !CMD_SAFE_ARG.test(arg);
|
|
291
|
+
// An unquoted arg is pure allow-list bytes (no `%`, `"`, or trailing `\`), so
|
|
292
|
+
// it needs no interior escaping.
|
|
293
|
+
return needsQuotes ? `"${escapeCmdQuotedInterior(arg)}"` : arg;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Build the `cmd.exe` argv for a Windows `.cmd`/`.bat` (or unresolved bare)
|
|
298
|
+
* MCP command.
|
|
299
|
+
*
|
|
300
|
+
* The trailing element is a single `/c` string wrapped in an outer quote pair
|
|
301
|
+
* that `cmd.exe` strips (its opening-quote rule). The command token is always
|
|
302
|
+
* quoted and, like every argument, escaped so a `%` in the resolved path (e.g.
|
|
303
|
+
* `C:\work\%TOKEN%\server.cmd`) is not expanded before the shim launches.
|
|
304
|
+
* `/e:ON` keeps command extensions on (required for the `%%cd:~,%` trick) and
|
|
305
|
+
* `/v:OFF` disables delayed expansion. The result MUST be spawned with
|
|
306
|
+
* `windowsVerbatimArguments` so libuv passes it through unmodified.
|
|
307
|
+
*/
|
|
308
|
+
function buildCmdExeArgv(comspec: string, command: string, args: readonly string[]): string[] {
|
|
309
|
+
assertCmdBatchToken(command, "command");
|
|
310
|
+
let line = `""${escapeCmdQuotedInterior(command)}"`;
|
|
311
|
+
for (const arg of args) line += ` ${escapeCmdBatchArg(arg)}`;
|
|
312
|
+
line += '"';
|
|
313
|
+
return [comspec, "/d", "/e:ON", "/v:OFF", "/c", line];
|
|
236
314
|
}
|
|
237
315
|
|
|
238
316
|
/**
|
|
@@ -245,7 +323,7 @@ function buildCmdExeCommand(command: string, args: readonly string[]): string {
|
|
|
245
323
|
* only appends `.exe` for extensionless names — `.cmd`/`.bat` are never
|
|
246
324
|
* tried, so `npx` (which exists only as `npx.cmd` on Windows) crashes the
|
|
247
325
|
* subprocess immediately. When the resolver can't pin the command down,
|
|
248
|
-
* route through `cmd.exe
|
|
326
|
+
* route through `cmd.exe` so Windows's own PATHEXT lookup runs.
|
|
249
327
|
*/
|
|
250
328
|
export async function resolveStdioSpawnCommand(
|
|
251
329
|
config: MCPStdioServerConfig,
|
|
@@ -271,9 +349,10 @@ export async function resolveStdioSpawnCommand(
|
|
|
271
349
|
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide, detached };
|
|
272
350
|
|
|
273
351
|
return {
|
|
274
|
-
cmd:
|
|
352
|
+
cmd: buildCmdExeArgv(resolveComSpec(options.env), resolvedCommand, args),
|
|
275
353
|
windowsHide,
|
|
276
354
|
detached,
|
|
355
|
+
windowsVerbatimArguments: true,
|
|
277
356
|
};
|
|
278
357
|
}
|
|
279
358
|
|
|
@@ -388,6 +467,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
388
467
|
stderr: "pipe",
|
|
389
468
|
windowsHide: spawnCommand.windowsHide,
|
|
390
469
|
detached: spawnCommand.detached,
|
|
470
|
+
windowsVerbatimArguments: spawnCommand.windowsVerbatimArguments,
|
|
391
471
|
});
|
|
392
472
|
|
|
393
473
|
this.#connected = true;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* `save` callback.
|
|
17
17
|
*/
|
|
18
18
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
19
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
19
|
+
import type { Model, UsageReport } from "@oh-my-pi/pi-ai";
|
|
20
20
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
21
21
|
import {
|
|
22
22
|
type Component,
|
|
@@ -38,6 +38,9 @@ import {
|
|
|
38
38
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
39
39
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
40
40
|
import type { Settings } from "../../config/settings";
|
|
41
|
+
import type { PerAdvisorStat } from "../../session/agent-session";
|
|
42
|
+
import type { OAuthAccountIdentity } from "../../session/auth-storage";
|
|
43
|
+
import { formatCompactQuota } from "../controllers/command-controller";
|
|
41
44
|
import { getSelectListTheme, theme } from "../theme/theme";
|
|
42
45
|
import { HookEditorComponent } from "./hook-editor";
|
|
43
46
|
import { buildBrowserItems, ModelBrowser, sortModelItems } from "./model-browser";
|
|
@@ -63,6 +66,11 @@ export interface AdvisorConfigCallbacks {
|
|
|
63
66
|
requestRender: () => void;
|
|
64
67
|
/** Surface a transient status/warning line to the user. */
|
|
65
68
|
notify: (message: string) => void;
|
|
69
|
+
/** Live advisor usage stats; lets the preview show tokens/cost per advisor. */
|
|
70
|
+
getAdvisorStats?: () => PerAdvisorStat[];
|
|
71
|
+
getUsageReports?: () => Promise<UsageReport[] | null>;
|
|
72
|
+
/** Resolve the active OAuth identity for quota filtering (per-advisor account stickiness). */
|
|
73
|
+
resolveActiveAccount?: (provider: string, sessionId?: string) => OAuthAccountIdentity | undefined;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface AdvisorConfigDeps {
|
|
@@ -126,6 +134,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
126
134
|
#cb: AdvisorConfigCallbacks;
|
|
127
135
|
#scope: AdvisorConfigScope;
|
|
128
136
|
#doc: WatchdogConfigDoc;
|
|
137
|
+
/** Cached usage reports (quota/window/reset) prefetched on overlay open. */
|
|
138
|
+
#cachedReports: UsageReport[] | null = null;
|
|
129
139
|
#dirty = false;
|
|
130
140
|
|
|
131
141
|
#screen: Screen = "list";
|
|
@@ -157,6 +167,16 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
157
167
|
this.#doc = doc;
|
|
158
168
|
this.#ensureRosterVisible();
|
|
159
169
|
this.#showList();
|
|
170
|
+
// Prefetch usage reports for quota display; non-fatal if unavailable.
|
|
171
|
+
if (callbacks.getUsageReports) {
|
|
172
|
+
void callbacks
|
|
173
|
+
.getUsageReports()
|
|
174
|
+
.then(r => {
|
|
175
|
+
this.#cachedReports = r;
|
|
176
|
+
this.#cb.requestRender();
|
|
177
|
+
})
|
|
178
|
+
.catch(() => {});
|
|
179
|
+
}
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
// ───────────────────────────── render ─────────────────────────────
|
|
@@ -275,6 +295,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
275
295
|
const lines = [
|
|
276
296
|
theme.bold(advisor.name || "(unnamed)"),
|
|
277
297
|
"",
|
|
298
|
+
`${theme.fg("dim", "Enabled:")} ${advisor.enabled === false ? "○ off" : "● on"}`,
|
|
278
299
|
`${theme.fg("dim", "Model:")} ${model}`,
|
|
279
300
|
`${theme.fg("dim", "Tools:")} ${tools}`,
|
|
280
301
|
"",
|
|
@@ -282,6 +303,34 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
282
303
|
];
|
|
283
304
|
const instr = advisor.instructions?.trim();
|
|
284
305
|
lines.push(...(instr ? wrap(instr, bodyWidth) : [theme.fg("muted", "(none)")]));
|
|
306
|
+
// Show live usage stats when available from the session.
|
|
307
|
+
const liveStat = this.#cb.getAdvisorStats?.()?.find(s => s.name === (advisor.name || "default"));
|
|
308
|
+
if (liveStat && (liveStat.status === "running" || liveStat.status === "quota_exhausted")) {
|
|
309
|
+
lines.push("", theme.fg("dim", "Usage:"));
|
|
310
|
+
const spendParts: string[] = [
|
|
311
|
+
`${liveStat.tokens.input.toLocaleString()} in`,
|
|
312
|
+
`${liveStat.tokens.output.toLocaleString()} out`,
|
|
313
|
+
];
|
|
314
|
+
if (liveStat.tokens.cacheRead > 0) spendParts.push(`${liveStat.tokens.cacheRead.toLocaleString()} cache`);
|
|
315
|
+
lines.push(theme.fg("dim", ` Tokens: ${spendParts.join(", ")}`));
|
|
316
|
+
if (liveStat.cost > 0) lines.push(theme.fg("dim", ` Cost: $${liveStat.cost.toFixed(4)}`));
|
|
317
|
+
if (liveStat.contextWindow > 0) {
|
|
318
|
+
const pct = Math.round((liveStat.contextTokens / liveStat.contextWindow) * 100);
|
|
319
|
+
lines.push(
|
|
320
|
+
theme.fg(
|
|
321
|
+
"dim",
|
|
322
|
+
` Context: ${liveStat.contextTokens.toLocaleString()}/${liveStat.contextWindow.toLocaleString()} (${pct}%)`,
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const quotaProvider =
|
|
328
|
+
(advisor.model?.includes("/") ? advisor.model.split("/")[0] : null) ?? liveStat?.model?.provider;
|
|
329
|
+
if (this.#cachedReports && quotaProvider) {
|
|
330
|
+
const activeAccount = this.#cb.resolveActiveAccount?.(quotaProvider, liveStat?.sessionId);
|
|
331
|
+
const quota = formatCompactQuota(quotaProvider, this.#cachedReports, Date.now(), activeAccount);
|
|
332
|
+
if (quota) lines.push(theme.fg("dim", ` ${quota}`));
|
|
333
|
+
}
|
|
285
334
|
return lines.map(line => truncateToWidth(line, bodyWidth));
|
|
286
335
|
}
|
|
287
336
|
|
|
@@ -311,7 +360,8 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
311
360
|
advisor.name === "default" &&
|
|
312
361
|
!advisor.model?.trim() &&
|
|
313
362
|
advisor.tools === undefined &&
|
|
314
|
-
!advisor.instructions?.trim()
|
|
363
|
+
!advisor.instructions?.trim() &&
|
|
364
|
+
advisor.enabled !== false
|
|
315
365
|
);
|
|
316
366
|
}
|
|
317
367
|
|
|
@@ -325,7 +375,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
325
375
|
this.#ensureRosterVisible();
|
|
326
376
|
const items: SelectItem[] = this.#doc.advisors.map((advisor, index) => ({
|
|
327
377
|
value: `advisor:${index}`,
|
|
328
|
-
label: advisor.name || "(unnamed)"
|
|
378
|
+
label: `${advisor.enabled === false ? "○" : "●"} ${advisor.name || "(unnamed)"}`,
|
|
329
379
|
description: this.#advisorSummary(advisor),
|
|
330
380
|
}));
|
|
331
381
|
items.push({ value: "add", label: "+ Add advisor" });
|
|
@@ -395,6 +445,11 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
395
445
|
const toolsDescription = formatAdvisorTools(advisor.tools, "no tools");
|
|
396
446
|
const items: SelectItem[] = [
|
|
397
447
|
{ value: "name", label: "Name", description: advisor.name },
|
|
448
|
+
{
|
|
449
|
+
value: "toggleEnabled",
|
|
450
|
+
label: "Enabled",
|
|
451
|
+
description: advisor.enabled === false ? "○ off" : "● on",
|
|
452
|
+
},
|
|
398
453
|
{ value: "model", label: "Model", description: modelDescription },
|
|
399
454
|
];
|
|
400
455
|
if (advisor.model?.trim()) {
|
|
@@ -414,6 +469,13 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
414
469
|
|
|
415
470
|
#onDetailSelect(index: number, field: string): void {
|
|
416
471
|
switch (field) {
|
|
472
|
+
case "toggleEnabled": {
|
|
473
|
+
const a = this.#doc.advisors[index];
|
|
474
|
+
a.enabled = a.enabled === false ? undefined : false;
|
|
475
|
+
this.#dirty = true;
|
|
476
|
+
this.#showDetail(index);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
417
479
|
case "name":
|
|
418
480
|
this.#showNameEditor(index);
|
|
419
481
|
return;
|
|
@@ -556,7 +556,7 @@ export class AskDialogComponent implements Component {
|
|
|
556
556
|
}
|
|
557
557
|
const isEnter = matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n";
|
|
558
558
|
const isSpace = matchesKey(keyData, "space") || keyData === " ";
|
|
559
|
-
if (!isEnter && !isSpace) return;
|
|
559
|
+
if (!isEnter && !(question.multi && isSpace)) return;
|
|
560
560
|
if (rowItem.kind === "other") {
|
|
561
561
|
void this.#promptForCustomInput(question, state, rowItem);
|
|
562
562
|
return;
|
|
@@ -20,6 +20,13 @@ import { DynamicBorder } from "./dynamic-border";
|
|
|
20
20
|
export interface HookEditorOptions {
|
|
21
21
|
/** When true, use prompt-style keybindings with the legacy ask prompt chrome. */
|
|
22
22
|
promptStyle?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Max rows the inner Editor may occupy. When omitted, the editor is
|
|
25
|
+
* bounded to the current terminal height minus the component's chrome
|
|
26
|
+
* (≈10 rows) so long content scrolls instead of pushing the submit
|
|
27
|
+
* hint out of view.
|
|
28
|
+
*/
|
|
29
|
+
maxHeight?: number;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** Interactive multiline dialog used by hooks and the ask tool's Other response. */
|
|
@@ -50,8 +57,11 @@ export class HookEditorComponent extends Container implements Focusable {
|
|
|
50
57
|
this.addChild(new DynamicBorder());
|
|
51
58
|
this.addChild(new Spacer(1));
|
|
52
59
|
|
|
53
|
-
// Title
|
|
54
|
-
|
|
60
|
+
// Title. Prompt-style renders the borderless editor's `> ` gutter at
|
|
61
|
+
// column 0, so pad the title to match; hook-style keeps the 1-col indent
|
|
62
|
+
// that lines up with its bordered editor body (#5313).
|
|
63
|
+
const chromePadX = this.#promptStyle ? 0 : 1;
|
|
64
|
+
this.addChild(new Text(theme.fg("accent", title), chromePadX, 0));
|
|
55
65
|
this.addChild(new Spacer(1));
|
|
56
66
|
|
|
57
67
|
// Editor
|
|
@@ -61,6 +71,11 @@ export class HookEditorComponent extends Container implements Focusable {
|
|
|
61
71
|
this.#editor.setPromptGutter("> ");
|
|
62
72
|
this.#editor.disableSubmit = true;
|
|
63
73
|
}
|
|
74
|
+
// Bound the editor so long content scrolls instead of pushing the
|
|
75
|
+
// submit hint off-screen. Caller may override via options.maxHeight.
|
|
76
|
+
const termRows = this.#tui.terminal?.rows ?? process.stdout.rows ?? 40;
|
|
77
|
+
this.#editor.setMaxHeight(options?.maxHeight ?? Math.max(3, termRows - 12));
|
|
78
|
+
this.#editor.setScrollbarVisible(true);
|
|
64
79
|
if (prefill) {
|
|
65
80
|
this.#editor.setText(prefill);
|
|
66
81
|
}
|
|
@@ -72,7 +87,7 @@ export class HookEditorComponent extends Container implements Focusable {
|
|
|
72
87
|
const hint = this.#promptStyle
|
|
73
88
|
? "enter or ctrl+q submit esc cancel ctrl+g external editor"
|
|
74
89
|
: "ctrl+q/ctrl+enter submit esc cancel ctrl+g external editor";
|
|
75
|
-
this.addChild(new Text(theme.fg("dim", hint),
|
|
90
|
+
this.addChild(new Text(theme.fg("dim", hint), chromePadX, 0));
|
|
76
91
|
|
|
77
92
|
this.addChild(new Spacer(1));
|
|
78
93
|
this.addChild(new DynamicBorder());
|